From 3471a44f923c0f729b7f6eefb73ba6dd8539297e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 3 Dec 2021 21:06:14 +0100 Subject: [PATCH 0001/2644] 2022! Happy New Year! (#60936) --- homeassistant/const.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 9da00de9a9a..c4e90dc2d57 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,8 +5,8 @@ from typing import Final from homeassistant.backports.enum import StrEnum -MAJOR_VERSION: Final = 2021 -MINOR_VERSION: Final = 12 +MAJOR_VERSION: Final = 2022 +MINOR_VERSION: Final = 1 PATCH_VERSION: Final = "0.dev0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" From 8c3014169f81084810c9a62ff7c56569ce7c4e9a Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 3 Dec 2021 18:01:48 -0500 Subject: [PATCH 0002/2644] Fix nzbget datetime return value (#60953) --- homeassistant/components/nzbget/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py index 5bfde7e7c2b..9f94d458f42 100644 --- a/homeassistant/components/nzbget/sensor.py +++ b/homeassistant/components/nzbget/sensor.py @@ -127,6 +127,6 @@ class NZBGetSensor(NZBGetEntity, SensorEntity): if "UpTimeSec" in sensor_type and value > 0: uptime = utcnow() - timedelta(seconds=value) - return uptime.replace(microsecond=0).isoformat() + return uptime.replace(microsecond=0) return value From dcf3bae5009fbfb050a84637581fd8b82b1fc7ad Mon Sep 17 00:00:00 2001 From: Penny Wood Date: Sat, 4 Dec 2021 07:28:23 +0800 Subject: [PATCH 0003/2644] Updated code as per comment in #60676 (#60682) * Minor code refactor --- homeassistant/components/izone/climate.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py index db47b087c0d..b21089d8baf 100644 --- a/homeassistant/components/izone/climate.py +++ b/homeassistant/components/izone/climate.py @@ -207,10 +207,9 @@ class ControllerDevice(ClimateEntity): """Handle controller data updates.""" if ctrl is not self._controller: return + if not self.available: + return self.async_write_ha_state() - for zone in self.zones.values(): - if zone.hass is not None: - zone.async_schedule_update_ha_state() self.async_on_remove( async_dispatcher_connect( @@ -492,6 +491,21 @@ class ZoneDevice(ClimateEntity): async def async_added_to_hass(self): """Call on adding to hass.""" + @callback + def controller_update(ctrl: Controller) -> None: + """Handle controller data updates.""" + if ctrl.device_uid != self._controller.unique_id: + return + if not self.available: + return + self.async_write_ha_state() + + self.async_on_remove( + async_dispatcher_connect( + self.hass, DISPATCH_CONTROLLER_UPDATE, controller_update + ) + ) + @callback def zone_update(ctrl: Controller, zone: Zone) -> None: """Handle zone data updates.""" From 68ca0a05c8a849ba374539e3c6a883555a567abf Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 4 Dec 2021 00:13:16 +0000 Subject: [PATCH 0004/2644] [ci skip] Translation update --- .../components/adax/translations/tr.json | 2 +- .../components/adguard/translations/bg.json | 2 +- .../components/adguard/translations/tr.json | 2 +- .../components/agent_dvr/translations/tr.json | 2 +- .../components/airtouch4/translations/tr.json | 2 +- .../components/airvisual/translations/tr.json | 2 +- .../alarmdecoder/translations/tr.json | 2 +- .../components/asuswrt/translations/tr.json | 2 +- .../components/atag/translations/tr.json | 2 +- .../components/axis/translations/tr.json | 2 +- .../components/balboa/translations/tr.json | 2 +- .../components/bond/translations/tr.json | 2 +- .../components/bosch_shc/translations/tr.json | 2 +- .../components/broadlink/translations/tr.json | 2 +- .../components/brother/translations/bg.json | 3 +- .../components/brother/translations/tr.json | 2 +- .../components/bsblan/translations/tr.json | 2 +- .../cert_expiry/translations/tr.json | 2 +- .../coolmaster/translations/tr.json | 2 +- .../components/daikin/translations/tr.json | 2 +- .../components/deconz/translations/tr.json | 2 +- .../components/directv/translations/tr.json | 2 +- .../components/dlna_dmr/translations/tr.json | 2 +- .../components/doorbird/translations/tr.json | 2 +- .../components/dsmr/translations/tr.json | 2 +- .../components/dunehd/translations/tr.json | 2 +- .../components/elgato/translations/tr.json | 2 +- .../components/emonitor/translations/tr.json | 2 +- .../enphase_envoy/translations/tr.json | 2 +- .../components/epson/translations/tr.json | 2 +- .../components/esphome/translations/tr.json | 2 +- .../evil_genius_labs/translations/tr.json | 2 +- .../components/flo/translations/tr.json | 2 +- .../components/flux_led/translations/tr.json | 2 +- .../forked_daapd/translations/tr.json | 2 +- .../components/foscam/translations/tr.json | 2 +- .../components/freebox/translations/tr.json | 2 +- .../components/fritz/translations/tr.json | 4 +-- .../components/fritzbox/translations/tr.json | 2 +- .../fritzbox_callmonitor/translations/tr.json | 2 +- .../components/fronius/translations/de.json | 7 +++- .../components/fronius/translations/et.json | 7 +++- .../components/fronius/translations/ru.json | 7 +++- .../components/fronius/translations/tr.json | 2 +- .../components/glances/translations/tr.json | 2 +- .../components/goalzero/translations/tr.json | 2 +- .../components/harmony/translations/tr.json | 2 +- .../components/heos/translations/tr.json | 2 +- .../components/hlk_sw16/translations/tr.json | 2 +- .../components/hue/translations/tr.json | 4 +-- .../components/hyperion/translations/tr.json | 2 +- .../components/ialarm/translations/tr.json | 2 +- .../components/icloud/translations/bg.json | 5 +++ .../components/iotawatt/translations/tr.json | 2 +- .../components/ipp/translations/tr.json | 2 +- .../keenetic_ndms2/translations/tr.json | 2 +- .../components/kmtronic/translations/tr.json | 2 +- .../components/knx/translations/tr.json | 4 +-- .../components/kodi/translations/tr.json | 2 +- .../components/konnected/translations/bg.json | 20 +++++++++++ .../kostal_plenticore/translations/tr.json | 2 +- .../components/melcloud/translations/bg.json | 5 +++ .../meteo_france/translations/bg.json | 4 +++ .../components/mikrotik/translations/tr.json | 2 +- .../minecraft_server/translations/tr.json | 2 +- .../modern_forms/translations/tr.json | 2 +- .../components/mutesync/translations/tr.json | 2 +- .../components/nam/translations/tr.json | 2 +- .../components/nanoleaf/translations/tr.json | 2 +- .../components/nest/translations/he.json | 1 + .../components/netatmo/translations/bg.json | 3 ++ .../components/netgear/translations/tr.json | 2 +- .../nfandroidtv/translations/tr.json | 2 +- .../components/nuki/translations/tr.json | 2 +- .../components/nut/translations/tr.json | 2 +- .../components/nzbget/translations/tr.json | 2 +- .../components/octoprint/translations/tr.json | 2 +- .../components/onewire/translations/tr.json | 2 +- .../components/onvif/translations/tr.json | 4 +-- .../opengarage/translations/tr.json | 2 +- .../p1_monitor/translations/tr.json | 2 +- .../philips_js/translations/tr.json | 2 +- .../components/pi_hole/translations/tr.json | 2 +- .../progettihwsw/translations/tr.json | 2 +- .../components/ps4/translations/tr.json | 2 +- .../rainforest_eagle/translations/tr.json | 2 +- .../rainmachine/translations/tr.json | 2 +- .../components/rfxtrx/translations/bg.json | 6 ++++ .../components/ring/translations/bg.json | 3 ++ .../components/roomba/translations/tr.json | 4 +-- .../components/rpi_power/translations/bg.json | 3 +- .../components/samsungtv/translations/tr.json | 2 +- .../components/shelly/translations/tr.json | 2 +- .../components/sma/translations/tr.json | 2 +- .../components/smappee/translations/tr.json | 2 +- .../components/soma/translations/tr.json | 2 +- .../system_bridge/translations/tr.json | 2 +- .../components/tailscale/translations/bg.json | 23 +++++++++++++ .../components/tailscale/translations/he.json | 23 +++++++++++++ .../components/tailscale/translations/pl.json | 18 ++++++++++ .../tesla_wall_connector/translations/he.json | 19 +++++++++++ .../tesla_wall_connector/translations/tr.json | 2 +- .../components/tolo/translations/tr.json | 2 +- .../components/tplink/translations/tr.json | 2 +- .../tractive/translations/sensor.pl.json | 9 +++++ .../components/tradfri/translations/tr.json | 2 +- .../translations/bg.json | 20 +++++++++++ .../translations/he.json | 20 +++++++++++ .../translations/ja.json | 4 ++- .../translations/pl.json | 10 ++++++ .../tuya/translations/select.en.json | 34 +------------------ .../tuya/translations/select.he.json | 4 +++ .../components/venstar/translations/tr.json | 2 +- .../components/vilfo/translations/bg.json | 8 +++++ .../components/vizio/translations/tr.json | 2 +- .../vlc_telnet/translations/tr.json | 2 +- .../components/volumio/translations/tr.json | 2 +- .../components/wled/translations/tr.json | 2 +- .../yale_smart_alarm/translations/bg.json | 3 +- .../yale_smart_alarm/translations/he.json | 3 +- .../yamaha_musiccast/translations/tr.json | 2 +- .../components/youless/translations/tr.json | 2 +- 122 files changed, 331 insertions(+), 141 deletions(-) create mode 100644 homeassistant/components/tailscale/translations/bg.json create mode 100644 homeassistant/components/tailscale/translations/he.json create mode 100644 homeassistant/components/tailscale/translations/pl.json create mode 100644 homeassistant/components/tesla_wall_connector/translations/he.json create mode 100644 homeassistant/components/tractive/translations/sensor.pl.json create mode 100644 homeassistant/components/trafikverket_weatherstation/translations/bg.json create mode 100644 homeassistant/components/trafikverket_weatherstation/translations/he.json create mode 100644 homeassistant/components/trafikverket_weatherstation/translations/pl.json diff --git a/homeassistant/components/adax/translations/tr.json b/homeassistant/components/adax/translations/tr.json index af5cdd5d16a..2407831bd5b 100644 --- a/homeassistant/components/adax/translations/tr.json +++ b/homeassistant/components/adax/translations/tr.json @@ -11,7 +11,7 @@ "user": { "data": { "account_id": "Hesap Kimli\u011fi", - "host": "Ana bilgisayar", + "host": "Sunucu", "password": "Parola" } } diff --git a/homeassistant/components/adguard/translations/bg.json b/homeassistant/components/adguard/translations/bg.json index 2eb6a211ecd..9838fd97c13 100644 --- a/homeassistant/components/adguard/translations/bg.json +++ b/homeassistant/components/adguard/translations/bg.json @@ -4,7 +4,7 @@ "existing_instance_updated": "\u0410\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430\u0449\u0430\u0442\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." }, "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "step": { "hassio_confirm": { diff --git a/homeassistant/components/adguard/translations/tr.json b/homeassistant/components/adguard/translations/tr.json index 7d9ddf4db0f..c5577fdae4e 100644 --- a/homeassistant/components/adguard/translations/tr.json +++ b/homeassistant/components/adguard/translations/tr.json @@ -14,7 +14,7 @@ }, "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "password": "Parola", "port": "Port", "ssl": "SSL sertifikas\u0131 kullan\u0131r", diff --git a/homeassistant/components/agent_dvr/translations/tr.json b/homeassistant/components/agent_dvr/translations/tr.json index 3ff1b149bdd..dcfcbed9376 100644 --- a/homeassistant/components/agent_dvr/translations/tr.json +++ b/homeassistant/components/agent_dvr/translations/tr.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "port": "Port" }, "title": "Agent DVR'\u0131 kurun" diff --git a/homeassistant/components/airtouch4/translations/tr.json b/homeassistant/components/airtouch4/translations/tr.json index 852639e6350..3add2bcac20 100644 --- a/homeassistant/components/airtouch4/translations/tr.json +++ b/homeassistant/components/airtouch4/translations/tr.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "host": "Ana bilgisayar" + "host": "Sunucu" }, "title": "AirTouch 4 ba\u011flant\u0131 ayr\u0131nt\u0131lar\u0131n\u0131z\u0131 ayarlay\u0131n." } diff --git a/homeassistant/components/airvisual/translations/tr.json b/homeassistant/components/airvisual/translations/tr.json index 6ecfc74ad76..bcfe6825372 100644 --- a/homeassistant/components/airvisual/translations/tr.json +++ b/homeassistant/components/airvisual/translations/tr.json @@ -32,7 +32,7 @@ }, "node_pro": { "data": { - "ip_address": "Ana bilgisayar", + "ip_address": "Sunucu", "password": "Parola" }, "description": "Ki\u015fisel bir AirVisual \u00fcnitesini izleyin. Parola, \u00fcnitenin kullan\u0131c\u0131 aray\u00fcz\u00fcnden al\u0131nabilir.", diff --git a/homeassistant/components/alarmdecoder/translations/tr.json b/homeassistant/components/alarmdecoder/translations/tr.json index ec30201055b..244f962a9f6 100644 --- a/homeassistant/components/alarmdecoder/translations/tr.json +++ b/homeassistant/components/alarmdecoder/translations/tr.json @@ -14,7 +14,7 @@ "data": { "device_baudrate": "Cihaz Baud H\u0131z\u0131", "device_path": "Cihaz Yolu", - "host": "Ana bilgisayar", + "host": "Sunucu", "port": "Port" }, "title": "Ba\u011flant\u0131 ayarlar\u0131n\u0131 yap\u0131land\u0131r\u0131n" diff --git a/homeassistant/components/asuswrt/translations/tr.json b/homeassistant/components/asuswrt/translations/tr.json index 2bae8499c02..19012b706fe 100644 --- a/homeassistant/components/asuswrt/translations/tr.json +++ b/homeassistant/components/asuswrt/translations/tr.json @@ -14,7 +14,7 @@ "step": { "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "mode": "Mod", "name": "Ad", "password": "Parola", diff --git a/homeassistant/components/atag/translations/tr.json b/homeassistant/components/atag/translations/tr.json index d4c5dd8acad..218835b9325 100644 --- a/homeassistant/components/atag/translations/tr.json +++ b/homeassistant/components/atag/translations/tr.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "port": "Port" }, "title": "Cihaza ba\u011flan\u0131n" diff --git a/homeassistant/components/axis/translations/tr.json b/homeassistant/components/axis/translations/tr.json index 2ec66ab688d..77a4c59e08b 100644 --- a/homeassistant/components/axis/translations/tr.json +++ b/homeassistant/components/axis/translations/tr.json @@ -15,7 +15,7 @@ "step": { "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "password": "Parola", "port": "Port", "username": "Kullan\u0131c\u0131 Ad\u0131" diff --git a/homeassistant/components/balboa/translations/tr.json b/homeassistant/components/balboa/translations/tr.json index f83ec51d377..8be6c335bd6 100644 --- a/homeassistant/components/balboa/translations/tr.json +++ b/homeassistant/components/balboa/translations/tr.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "host": "Ana bilgisayar" + "host": "Sunucu" }, "title": "Balboa Wi-Fi cihaz\u0131na ba\u011flan\u0131n" } diff --git a/homeassistant/components/bond/translations/tr.json b/homeassistant/components/bond/translations/tr.json index e00bdd6370c..84df4b6da5c 100644 --- a/homeassistant/components/bond/translations/tr.json +++ b/homeassistant/components/bond/translations/tr.json @@ -20,7 +20,7 @@ "user": { "data": { "access_token": "Eri\u015fim Anahtar\u0131", - "host": "Ana bilgisayar" + "host": "Sunucu" } } } diff --git a/homeassistant/components/bosch_shc/translations/tr.json b/homeassistant/components/bosch_shc/translations/tr.json index b974ef63ebf..f48286cf9cd 100644 --- a/homeassistant/components/bosch_shc/translations/tr.json +++ b/homeassistant/components/bosch_shc/translations/tr.json @@ -27,7 +27,7 @@ }, "user": { "data": { - "host": "Ana bilgisayar" + "host": "Sunucu" }, "description": "Ev Asistan\u0131 ile izleme ve kontrole izin vermek i\u00e7in Bosch Ak\u0131ll\u0131 Ev Denetleyicinizi kurun.", "title": "SHC kimlik do\u011frulama parametreleri" diff --git a/homeassistant/components/broadlink/translations/tr.json b/homeassistant/components/broadlink/translations/tr.json index b2f79ddaac0..c14901a238d 100644 --- a/homeassistant/components/broadlink/translations/tr.json +++ b/homeassistant/components/broadlink/translations/tr.json @@ -37,7 +37,7 @@ }, "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "timeout": "Zaman a\u015f\u0131m\u0131" }, "title": "Cihaza ba\u011flan\u0131n" diff --git a/homeassistant/components/brother/translations/bg.json b/homeassistant/components/brother/translations/bg.json index a2b013df69e..4a51b0abde1 100644 --- a/homeassistant/components/brother/translations/bg.json +++ b/homeassistant/components/brother/translations/bg.json @@ -4,7 +4,8 @@ "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" }, "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "wrong_host": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0438\u043c\u0435 \u043d\u0430 \u0445\u043e\u0441\u0442 \u0438\u043b\u0438 IP \u0430\u0434\u0440\u0435\u0441." }, "flow_title": "{model} {serial_number}", "step": { diff --git a/homeassistant/components/brother/translations/tr.json b/homeassistant/components/brother/translations/tr.json index a883e719c0d..c989844c6f7 100644 --- a/homeassistant/components/brother/translations/tr.json +++ b/homeassistant/components/brother/translations/tr.json @@ -13,7 +13,7 @@ "step": { "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "type": "Yaz\u0131c\u0131n\u0131n t\u00fcr\u00fc" }, "description": "Brother yaz\u0131c\u0131 entegrasyonunu ayarlay\u0131n. Yap\u0131land\u0131rmayla ilgili sorunlar\u0131n\u0131z varsa \u015fu adrese gidin: https://www.home-assistant.io/integrations/brother" diff --git a/homeassistant/components/bsblan/translations/tr.json b/homeassistant/components/bsblan/translations/tr.json index 623dab14446..28df383fbc5 100644 --- a/homeassistant/components/bsblan/translations/tr.json +++ b/homeassistant/components/bsblan/translations/tr.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "passkey": "Ge\u00e7i\u015f anahtar\u0131 dizesi", "password": "Parola", "port": "Port", diff --git a/homeassistant/components/cert_expiry/translations/tr.json b/homeassistant/components/cert_expiry/translations/tr.json index 0f8b7c45a6f..173e78adb44 100644 --- a/homeassistant/components/cert_expiry/translations/tr.json +++ b/homeassistant/components/cert_expiry/translations/tr.json @@ -12,7 +12,7 @@ "step": { "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "name": "Sertifikan\u0131n ad\u0131", "port": "Port" }, diff --git a/homeassistant/components/coolmaster/translations/tr.json b/homeassistant/components/coolmaster/translations/tr.json index dbca2862bfb..8950dfa0220 100644 --- a/homeassistant/components/coolmaster/translations/tr.json +++ b/homeassistant/components/coolmaster/translations/tr.json @@ -12,7 +12,7 @@ "fan_only": "Yaln\u0131zca fan modunu destekler", "heat": "Is\u0131tma modunu destekler", "heat_cool": "Otomatik \u0131s\u0131tma/so\u011futma modunu destekler", - "host": "Ana bilgisayar", + "host": "Sunucu", "off": "Kapat\u0131labilir" }, "title": "CoolMasterNet ba\u011flant\u0131 ayr\u0131nt\u0131lar\u0131n\u0131z\u0131 ayarlay\u0131n." diff --git a/homeassistant/components/daikin/translations/tr.json b/homeassistant/components/daikin/translations/tr.json index 0e548c96f0f..a5e0f0ab9f2 100644 --- a/homeassistant/components/daikin/translations/tr.json +++ b/homeassistant/components/daikin/translations/tr.json @@ -14,7 +14,7 @@ "user": { "data": { "api_key": "API Anahtar\u0131", - "host": "Ana bilgisayar", + "host": "Sunucu", "password": "Parola" }, "description": "IP Adresi de\u011ferini girin. \n\n API Anahtar\u0131 ve Parola \u00f6\u011felerinin s\u0131ras\u0131yla BRP072Cxx ve SKYFi cihazlar\u0131 taraf\u0131ndan kullan\u0131ld\u0131\u011f\u0131n\u0131 unutmay\u0131n.", diff --git a/homeassistant/components/deconz/translations/tr.json b/homeassistant/components/deconz/translations/tr.json index 5d259bd6be8..021c4845989 100644 --- a/homeassistant/components/deconz/translations/tr.json +++ b/homeassistant/components/deconz/translations/tr.json @@ -23,7 +23,7 @@ }, "manual_input": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "port": "Port" } }, diff --git a/homeassistant/components/directv/translations/tr.json b/homeassistant/components/directv/translations/tr.json index bcafc53ce66..27a9dc80cb5 100644 --- a/homeassistant/components/directv/translations/tr.json +++ b/homeassistant/components/directv/translations/tr.json @@ -18,7 +18,7 @@ }, "user": { "data": { - "host": "Ana bilgisayar" + "host": "Sunucu" } } } diff --git a/homeassistant/components/dlna_dmr/translations/tr.json b/homeassistant/components/dlna_dmr/translations/tr.json index 3a19f9455ad..59c5221b870 100644 --- a/homeassistant/components/dlna_dmr/translations/tr.json +++ b/homeassistant/components/dlna_dmr/translations/tr.json @@ -32,7 +32,7 @@ }, "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "url": "URL" }, "description": "Yap\u0131land\u0131rmak i\u00e7in bir cihaz se\u00e7in veya bir URL girmek i\u00e7in bo\u015f b\u0131rak\u0131n", diff --git a/homeassistant/components/doorbird/translations/tr.json b/homeassistant/components/doorbird/translations/tr.json index 8f36c27fc40..1bb08b6f81e 100644 --- a/homeassistant/components/doorbird/translations/tr.json +++ b/homeassistant/components/doorbird/translations/tr.json @@ -14,7 +14,7 @@ "step": { "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "name": "Cihaz ad\u0131", "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" diff --git a/homeassistant/components/dsmr/translations/tr.json b/homeassistant/components/dsmr/translations/tr.json index 1b282b456d6..2bdcd58a865 100644 --- a/homeassistant/components/dsmr/translations/tr.json +++ b/homeassistant/components/dsmr/translations/tr.json @@ -18,7 +18,7 @@ "setup_network": { "data": { "dsmr_version": "DSMR s\u00fcr\u00fcm\u00fcn\u00fc se\u00e7in", - "host": "Ana bilgisayar", + "host": "Sunucu", "port": "Port" }, "title": "Ba\u011flant\u0131 adresini se\u00e7in" diff --git a/homeassistant/components/dunehd/translations/tr.json b/homeassistant/components/dunehd/translations/tr.json index d51539ba12d..121267c34e3 100644 --- a/homeassistant/components/dunehd/translations/tr.json +++ b/homeassistant/components/dunehd/translations/tr.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "Ana bilgisayar" + "host": "Sunucu" }, "description": "Dune HD entegrasyonunu ayarlay\u0131n. Yap\u0131land\u0131rmayla ilgili sorunlar\u0131n\u0131z varsa \u015fu adrese gidin: https://www.home-assistant.io/integrations/dunehd \n\n Oynat\u0131c\u0131n\u0131z\u0131n a\u00e7\u0131k oldu\u011fundan emin olun.", "title": "Dune HD" diff --git a/homeassistant/components/elgato/translations/tr.json b/homeassistant/components/elgato/translations/tr.json index c49720b9525..eac6b36ced0 100644 --- a/homeassistant/components/elgato/translations/tr.json +++ b/homeassistant/components/elgato/translations/tr.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "port": "Port" }, "description": "Elgato Light'\u0131n\u0131z\u0131 Home Assistant ile entegre olacak \u015fekilde ayarlay\u0131n." diff --git a/homeassistant/components/emonitor/translations/tr.json b/homeassistant/components/emonitor/translations/tr.json index 58c48f5f688..72d9a2fcc6b 100644 --- a/homeassistant/components/emonitor/translations/tr.json +++ b/homeassistant/components/emonitor/translations/tr.json @@ -15,7 +15,7 @@ }, "user": { "data": { - "host": "Ana bilgisayar" + "host": "Sunucu" } } } diff --git a/homeassistant/components/enphase_envoy/translations/tr.json b/homeassistant/components/enphase_envoy/translations/tr.json index 5cbf60dbc06..4bcc08e0672 100644 --- a/homeassistant/components/enphase_envoy/translations/tr.json +++ b/homeassistant/components/enphase_envoy/translations/tr.json @@ -13,7 +13,7 @@ "step": { "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" } diff --git a/homeassistant/components/epson/translations/tr.json b/homeassistant/components/epson/translations/tr.json index a369ce95960..89d4c16be19 100644 --- a/homeassistant/components/epson/translations/tr.json +++ b/homeassistant/components/epson/translations/tr.json @@ -7,7 +7,7 @@ "step": { "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "name": "Ad" } } diff --git a/homeassistant/components/esphome/translations/tr.json b/homeassistant/components/esphome/translations/tr.json index 31f54809e95..b2cede2b572 100644 --- a/homeassistant/components/esphome/translations/tr.json +++ b/homeassistant/components/esphome/translations/tr.json @@ -37,7 +37,7 @@ }, "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "port": "Port" }, "description": "L\u00fctfen [ESPHome](https://esphomelib.com/) d\u00fc\u011f\u00fcm\u00fcn\u00fcz\u00fcn ba\u011flant\u0131 ayarlar\u0131n\u0131 girin." diff --git a/homeassistant/components/evil_genius_labs/translations/tr.json b/homeassistant/components/evil_genius_labs/translations/tr.json index b97a1354e2e..4fc3d5ccab2 100644 --- a/homeassistant/components/evil_genius_labs/translations/tr.json +++ b/homeassistant/components/evil_genius_labs/translations/tr.json @@ -7,7 +7,7 @@ "step": { "user": { "data": { - "host": "Ana bilgisayar" + "host": "Sunucu" } } } diff --git a/homeassistant/components/flo/translations/tr.json b/homeassistant/components/flo/translations/tr.json index 3fdcebd112c..fb81a411823 100644 --- a/homeassistant/components/flo/translations/tr.json +++ b/homeassistant/components/flo/translations/tr.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" } diff --git a/homeassistant/components/flux_led/translations/tr.json b/homeassistant/components/flux_led/translations/tr.json index 5ec66bfaec5..01bdbe36849 100644 --- a/homeassistant/components/flux_led/translations/tr.json +++ b/homeassistant/components/flux_led/translations/tr.json @@ -15,7 +15,7 @@ }, "user": { "data": { - "host": "Ana bilgisayar" + "host": "Sunucu" }, "description": "Ana bilgisayar\u0131 bo\u015f b\u0131rak\u0131rsan\u0131z, cihazlar\u0131 bulmak i\u00e7in ke\u015fif kullan\u0131lacakt\u0131r." } diff --git a/homeassistant/components/forked_daapd/translations/tr.json b/homeassistant/components/forked_daapd/translations/tr.json index f34158852a5..6c838e93055 100644 --- a/homeassistant/components/forked_daapd/translations/tr.json +++ b/homeassistant/components/forked_daapd/translations/tr.json @@ -16,7 +16,7 @@ "step": { "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "name": "Kolay Ad\u0131", "password": "API parolas\u0131 (parola yoksa bo\u015f b\u0131rak\u0131n)", "port": "API Port" diff --git a/homeassistant/components/foscam/translations/tr.json b/homeassistant/components/foscam/translations/tr.json index 4b0f450d735..e6e5adc434c 100644 --- a/homeassistant/components/foscam/translations/tr.json +++ b/homeassistant/components/foscam/translations/tr.json @@ -12,7 +12,7 @@ "step": { "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "password": "Parola", "port": "Port", "rtsp_port": "RTSP port", diff --git a/homeassistant/components/freebox/translations/tr.json b/homeassistant/components/freebox/translations/tr.json index 7ebe2634020..6690b2a5b23 100644 --- a/homeassistant/components/freebox/translations/tr.json +++ b/homeassistant/components/freebox/translations/tr.json @@ -15,7 +15,7 @@ }, "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "port": "Port" }, "title": "Freebox" diff --git a/homeassistant/components/fritz/translations/tr.json b/homeassistant/components/fritz/translations/tr.json index febbd1999cc..686c248cd39 100644 --- a/homeassistant/components/fritz/translations/tr.json +++ b/homeassistant/components/fritz/translations/tr.json @@ -32,7 +32,7 @@ }, "start_config": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "password": "Parola", "port": "Port", "username": "Kullan\u0131c\u0131 Ad\u0131" @@ -42,7 +42,7 @@ }, "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "password": "Parola", "port": "Port", "username": "Kullan\u0131c\u0131 Ad\u0131" diff --git a/homeassistant/components/fritzbox/translations/tr.json b/homeassistant/components/fritzbox/translations/tr.json index 53c8359f546..300ca1a096a 100644 --- a/homeassistant/components/fritzbox/translations/tr.json +++ b/homeassistant/components/fritzbox/translations/tr.json @@ -28,7 +28,7 @@ }, "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" }, diff --git a/homeassistant/components/fritzbox_callmonitor/translations/tr.json b/homeassistant/components/fritzbox_callmonitor/translations/tr.json index e6b02a8176d..f96f3cc2d6e 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/tr.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/tr.json @@ -17,7 +17,7 @@ }, "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "password": "Parola", "port": "Port", "username": "Kullan\u0131c\u0131 Ad\u0131" diff --git a/homeassistant/components/fronius/translations/de.json b/homeassistant/components/fronius/translations/de.json index b67ed566326..eed094491a5 100644 --- a/homeassistant/components/fronius/translations/de.json +++ b/homeassistant/components/fronius/translations/de.json @@ -1,13 +1,18 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "invalid_host": "Ung\u00fcltiger Hostname oder IP-Adresse" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", "unknown": "Unerwarteter Fehler" }, + "flow_title": "{device}", "step": { + "confirm_discovery": { + "description": "M\u00f6chtest du {device} zu Home Assistant hinzuf\u00fcgen?" + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/fronius/translations/et.json b/homeassistant/components/fronius/translations/et.json index 6ccf725ac6f..5fd7f471545 100644 --- a/homeassistant/components/fronius/translations/et.json +++ b/homeassistant/components/fronius/translations/et.json @@ -1,13 +1,18 @@ { "config": { "abort": { - "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "invalid_host": "Vigane hostinimi v\u00f5i IP aadress" }, "error": { "cannot_connect": "\u00dchendamine nurjus", "unknown": "Ootamatu t\u00f5rge" }, + "flow_title": "{device}", "step": { + "confirm_discovery": { + "description": "Kas soovid lisada {device} Home Assistanti?" + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/fronius/translations/ru.json b/homeassistant/components/fronius/translations/ru.json index 02f67288518..473834c8797 100644 --- a/homeassistant/components/fronius/translations/ru.json +++ b/homeassistant/components/fronius/translations/ru.json @@ -1,13 +1,18 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, + "flow_title": "{device}", "step": { + "confirm_discovery": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c {device} \u0432 Home Assistant?" + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442" diff --git a/homeassistant/components/fronius/translations/tr.json b/homeassistant/components/fronius/translations/tr.json index 4f45dd9fa1e..4d5b9a173db 100644 --- a/homeassistant/components/fronius/translations/tr.json +++ b/homeassistant/components/fronius/translations/tr.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "host": "Ana bilgisayar" + "host": "Sunucu" }, "description": "Fronius cihaz\u0131n\u0131z\u0131n IP adresini veya yerel ana bilgisayar ad\u0131n\u0131 yap\u0131land\u0131r\u0131n.", "title": "Fronius SolarNet" diff --git a/homeassistant/components/glances/translations/tr.json b/homeassistant/components/glances/translations/tr.json index 3a8f4852f86..50b2ef9cef1 100644 --- a/homeassistant/components/glances/translations/tr.json +++ b/homeassistant/components/glances/translations/tr.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "name": "Ad", "password": "Parola", "port": "Port", diff --git a/homeassistant/components/goalzero/translations/tr.json b/homeassistant/components/goalzero/translations/tr.json index 1101717545b..a2c5b4f8986 100644 --- a/homeassistant/components/goalzero/translations/tr.json +++ b/homeassistant/components/goalzero/translations/tr.json @@ -17,7 +17,7 @@ }, "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "name": "Ad" }, "description": "\u00d6ncelikle Goal Zero uygulamas\u0131n\u0131 indirmeniz gerekiyor: https://www.goalzero.com/product-features/yeti-app/ \n\n Yeti'nizi Wi-fi a\u011f\u0131n\u0131za ba\u011flamak i\u00e7in talimatlar\u0131 izleyin. Y\u00f6nlendiricinizde DHCP rezervasyonu yap\u0131lmas\u0131 \u00f6nerilir. Kurulmazsa, Home Assistant yeni ip adresini alg\u0131layana kadar cihaz kullan\u0131lamayabilir. Y\u00f6nlendiricinizin kullan\u0131m k\u0131lavuzuna bak\u0131n.", diff --git a/homeassistant/components/harmony/translations/tr.json b/homeassistant/components/harmony/translations/tr.json index cb3658d47fb..b4400dcb06a 100644 --- a/homeassistant/components/harmony/translations/tr.json +++ b/homeassistant/components/harmony/translations/tr.json @@ -15,7 +15,7 @@ }, "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "name": "Hub Ad\u0131" }, "title": "Logitech Harmony Hub'\u0131 Kur" diff --git a/homeassistant/components/heos/translations/tr.json b/homeassistant/components/heos/translations/tr.json index afe563e9a96..7853bf8639e 100644 --- a/homeassistant/components/heos/translations/tr.json +++ b/homeassistant/components/heos/translations/tr.json @@ -9,7 +9,7 @@ "step": { "user": { "data": { - "host": "Ana bilgisayar" + "host": "Sunucu" }, "description": "L\u00fctfen bir Heos cihaz\u0131n\u0131n ana bilgisayar ad\u0131n\u0131 veya IP adresini girin (tercihen a\u011fa kabloyla ba\u011fl\u0131 olan).", "title": "Heos'a ba\u011flan\u0131n" diff --git a/homeassistant/components/hlk_sw16/translations/tr.json b/homeassistant/components/hlk_sw16/translations/tr.json index 3fdcebd112c..fb81a411823 100644 --- a/homeassistant/components/hlk_sw16/translations/tr.json +++ b/homeassistant/components/hlk_sw16/translations/tr.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" } diff --git a/homeassistant/components/hue/translations/tr.json b/homeassistant/components/hue/translations/tr.json index a883283048f..64d0e70b41d 100644 --- a/homeassistant/components/hue/translations/tr.json +++ b/homeassistant/components/hue/translations/tr.json @@ -17,7 +17,7 @@ "step": { "init": { "data": { - "host": "Ana bilgisayar" + "host": "Sunucu" }, "title": "Hue k\u00f6pr\u00fcs\u00fcn\u00fc se\u00e7in" }, @@ -27,7 +27,7 @@ }, "manual": { "data": { - "host": "Ana bilgisayar" + "host": "Sunucu" }, "title": "Bir Hue k\u00f6pr\u00fcs\u00fcn\u00fc manuel olarak yap\u0131land\u0131rma" } diff --git a/homeassistant/components/hyperion/translations/tr.json b/homeassistant/components/hyperion/translations/tr.json index 0368ce2dde3..a0a00d22a7f 100644 --- a/homeassistant/components/hyperion/translations/tr.json +++ b/homeassistant/components/hyperion/translations/tr.json @@ -35,7 +35,7 @@ }, "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "port": "Port" } } diff --git a/homeassistant/components/ialarm/translations/tr.json b/homeassistant/components/ialarm/translations/tr.json index b524964cfd8..5066ca89866 100644 --- a/homeassistant/components/ialarm/translations/tr.json +++ b/homeassistant/components/ialarm/translations/tr.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "port": "Port" } } diff --git a/homeassistant/components/icloud/translations/bg.json b/homeassistant/components/icloud/translations/bg.json index f7f1faf74f5..52c6ed9b018 100644 --- a/homeassistant/components/icloud/translations/bg.json +++ b/homeassistant/components/icloud/translations/bg.json @@ -13,6 +13,11 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "username": "Email" } + }, + "verification_code": { + "data": { + "verification_code": "\u041a\u043e\u0434 \u0437\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430" + } } } } diff --git a/homeassistant/components/iotawatt/translations/tr.json b/homeassistant/components/iotawatt/translations/tr.json index af6af5b8633..a6704dc875f 100644 --- a/homeassistant/components/iotawatt/translations/tr.json +++ b/homeassistant/components/iotawatt/translations/tr.json @@ -15,7 +15,7 @@ }, "user": { "data": { - "host": "Ana bilgisayar" + "host": "Sunucu" } } } diff --git a/homeassistant/components/ipp/translations/tr.json b/homeassistant/components/ipp/translations/tr.json index cca4103324f..e3d0251a1b0 100644 --- a/homeassistant/components/ipp/translations/tr.json +++ b/homeassistant/components/ipp/translations/tr.json @@ -18,7 +18,7 @@ "user": { "data": { "base_path": "Yaz\u0131c\u0131n\u0131n yolu", - "host": "Ana bilgisayar", + "host": "Sunucu", "port": "Port", "ssl": "SSL sertifikas\u0131 kullan\u0131r", "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n" diff --git a/homeassistant/components/keenetic_ndms2/translations/tr.json b/homeassistant/components/keenetic_ndms2/translations/tr.json index 099ca45fc71..20bb3ad757e 100644 --- a/homeassistant/components/keenetic_ndms2/translations/tr.json +++ b/homeassistant/components/keenetic_ndms2/translations/tr.json @@ -12,7 +12,7 @@ "step": { "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "password": "Parola", "port": "Port", "username": "Kullan\u0131c\u0131 Ad\u0131" diff --git a/homeassistant/components/kmtronic/translations/tr.json b/homeassistant/components/kmtronic/translations/tr.json index c10979d7c83..291ebecf80c 100644 --- a/homeassistant/components/kmtronic/translations/tr.json +++ b/homeassistant/components/kmtronic/translations/tr.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" } diff --git a/homeassistant/components/knx/translations/tr.json b/homeassistant/components/knx/translations/tr.json index 18efaa74586..6fe72d8259c 100644 --- a/homeassistant/components/knx/translations/tr.json +++ b/homeassistant/components/knx/translations/tr.json @@ -10,7 +10,7 @@ "step": { "manual_tunnel": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "individual_address": "Ba\u011flant\u0131 i\u00e7in bireysel adres", "port": "Port", "route_back": "Geri Y\u00f6nlendirme / NAT Modu" @@ -53,7 +53,7 @@ }, "tunnel": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "port": "Port", "route_back": "Geri Y\u00f6nlendirme / NAT Modu" } diff --git a/homeassistant/components/kodi/translations/tr.json b/homeassistant/components/kodi/translations/tr.json index a8d11ddcb9b..18d5dd55224 100644 --- a/homeassistant/components/kodi/translations/tr.json +++ b/homeassistant/components/kodi/translations/tr.json @@ -27,7 +27,7 @@ }, "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "port": "Port", "ssl": "SSL sertifikas\u0131 kullan\u0131r" }, diff --git a/homeassistant/components/konnected/translations/bg.json b/homeassistant/components/konnected/translations/bg.json index 1c804131ae8..196218941fc 100644 --- a/homeassistant/components/konnected/translations/bg.json +++ b/homeassistant/components/konnected/translations/bg.json @@ -1,6 +1,11 @@ { "config": { "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "step": { @@ -14,6 +19,16 @@ }, "options": { "step": { + "options_binary": { + "data": { + "name": "\u0418\u043c\u0435 (\u043f\u043e \u0438\u0437\u0431\u043e\u0440)" + } + }, + "options_digital": { + "data": { + "name": "\u0418\u043c\u0435 (\u043f\u043e \u0438\u0437\u0431\u043e\u0440)" + } + }, "options_io": { "data": { "1": "\u0417\u043e\u043d\u0430 1", @@ -33,6 +48,11 @@ "8": "\u0417\u043e\u043d\u0430 8", "9": "\u0417\u043e\u043d\u0430 9" } + }, + "options_switch": { + "data": { + "name": "\u0418\u043c\u0435 (\u043f\u043e \u0438\u0437\u0431\u043e\u0440)" + } } } } diff --git a/homeassistant/components/kostal_plenticore/translations/tr.json b/homeassistant/components/kostal_plenticore/translations/tr.json index 573047e61b5..f0742d20e78 100644 --- a/homeassistant/components/kostal_plenticore/translations/tr.json +++ b/homeassistant/components/kostal_plenticore/translations/tr.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "password": "Parola" } } diff --git a/homeassistant/components/melcloud/translations/bg.json b/homeassistant/components/melcloud/translations/bg.json index b8a429313ec..4f9bd8388fa 100644 --- a/homeassistant/components/melcloud/translations/bg.json +++ b/homeassistant/components/melcloud/translations/bg.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/meteo_france/translations/bg.json b/homeassistant/components/meteo_france/translations/bg.json index 9a8d591cd77..c426ae62387 100644 --- a/homeassistant/components/meteo_france/translations/bg.json +++ b/homeassistant/components/meteo_france/translations/bg.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, "step": { "cities": { "data": { diff --git a/homeassistant/components/mikrotik/translations/tr.json b/homeassistant/components/mikrotik/translations/tr.json index f1f825290b2..628703168ec 100644 --- a/homeassistant/components/mikrotik/translations/tr.json +++ b/homeassistant/components/mikrotik/translations/tr.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "name": "Ad", "password": "Parola", "port": "Port", diff --git a/homeassistant/components/minecraft_server/translations/tr.json b/homeassistant/components/minecraft_server/translations/tr.json index f7bc8de5a61..8846a35f468 100644 --- a/homeassistant/components/minecraft_server/translations/tr.json +++ b/homeassistant/components/minecraft_server/translations/tr.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "name": "Ad" }, "description": "G\u00f6zetmeye izin vermek i\u00e7in Minecraft server nesnesini ayarla.", diff --git a/homeassistant/components/modern_forms/translations/tr.json b/homeassistant/components/modern_forms/translations/tr.json index b0f884ef68f..36caba41ee6 100644 --- a/homeassistant/components/modern_forms/translations/tr.json +++ b/homeassistant/components/modern_forms/translations/tr.json @@ -14,7 +14,7 @@ }, "user": { "data": { - "host": "Ana bilgisayar" + "host": "Sunucu" }, "description": "Modern Forms fan\u0131n\u0131z\u0131 Home Assistant ile entegre olacak \u015fekilde ayarlay\u0131n." }, diff --git a/homeassistant/components/mutesync/translations/tr.json b/homeassistant/components/mutesync/translations/tr.json index e45abded24b..1734890893e 100644 --- a/homeassistant/components/mutesync/translations/tr.json +++ b/homeassistant/components/mutesync/translations/tr.json @@ -8,7 +8,7 @@ "step": { "user": { "data": { - "host": "Ana bilgisayar" + "host": "Sunucu" } } } diff --git a/homeassistant/components/nam/translations/tr.json b/homeassistant/components/nam/translations/tr.json index e252cff4fcd..34117104663 100644 --- a/homeassistant/components/nam/translations/tr.json +++ b/homeassistant/components/nam/translations/tr.json @@ -32,7 +32,7 @@ }, "user": { "data": { - "host": "Ana bilgisayar" + "host": "Sunucu" }, "description": "Nettigo Air Monitor entegrasyonunu kurun." } diff --git a/homeassistant/components/nanoleaf/translations/tr.json b/homeassistant/components/nanoleaf/translations/tr.json index 55cbde518f2..d3c3969dc48 100644 --- a/homeassistant/components/nanoleaf/translations/tr.json +++ b/homeassistant/components/nanoleaf/translations/tr.json @@ -20,7 +20,7 @@ }, "user": { "data": { - "host": "Ana bilgisayar" + "host": "Sunucu" } } } diff --git a/homeassistant/components/nest/translations/he.json b/homeassistant/components/nest/translations/he.json index b1f12277c22..743b80f69a1 100644 --- a/homeassistant/components/nest/translations/he.json +++ b/homeassistant/components/nest/translations/he.json @@ -2,6 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05db\u05ea\u05d5\u05d1\u05ea URL \u05dc\u05d0\u05d9\u05e9\u05d5\u05e8.", + "invalid_access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3.", "no_url_available": "\u05d0\u05d9\u05df \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d6\u05de\u05d9\u05e0\u05d4. \u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e2\u05dc \u05e9\u05d2\u05d9\u05d0\u05d4 \u05d6\u05d5, [\u05e2\u05d9\u05d9\u05df \u05d1\u05e1\u05e2\u05d9\u05e3 \u05d4\u05e2\u05d6\u05e8\u05d4] ({docs_url})", "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7", diff --git a/homeassistant/components/netatmo/translations/bg.json b/homeassistant/components/netatmo/translations/bg.json index 4b02d4b6c6e..95a038871be 100644 --- a/homeassistant/components/netatmo/translations/bg.json +++ b/homeassistant/components/netatmo/translations/bg.json @@ -5,6 +5,9 @@ "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." }, + "create_entry": { + "default": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u0435\u043d\u043e" + }, "step": { "pick_implementation": { "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043c\u0435\u0442\u043e\u0434 \u0437\u0430 \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" diff --git a/homeassistant/components/netgear/translations/tr.json b/homeassistant/components/netgear/translations/tr.json index fbdb76ce1db..07066485015 100644 --- a/homeassistant/components/netgear/translations/tr.json +++ b/homeassistant/components/netgear/translations/tr.json @@ -9,7 +9,7 @@ "step": { "user": { "data": { - "host": "Ana bilgisayar (\u0130ste\u011fe ba\u011fl\u0131)", + "host": "Sunucu (\u0130ste\u011fe ba\u011fl\u0131)", "password": "Parola", "port": "Port (\u0130ste\u011fe ba\u011fl\u0131)", "ssl": "SSL sertifikas\u0131 kullan\u0131r", diff --git a/homeassistant/components/nfandroidtv/translations/tr.json b/homeassistant/components/nfandroidtv/translations/tr.json index 01bc2be90c1..b894cf83687 100644 --- a/homeassistant/components/nfandroidtv/translations/tr.json +++ b/homeassistant/components/nfandroidtv/translations/tr.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "name": "Ad" }, "description": "Bu entegrasyon, Android TV i\u00e7in Bildirimler uygulamas\u0131n\u0131 gerektirir. \n\n Android TV i\u00e7in: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\n Fire TV i\u00e7in: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK \n\n Y\u00f6nlendiricinizde DHCP rezervasyonu (y\u00f6nlendiricinizin kullan\u0131m k\u0131lavuzuna bak\u0131n) veya cihazda statik bir IP adresi ayarlamal\u0131s\u0131n\u0131z. Aksi takdirde, cihaz sonunda kullan\u0131lamaz hale gelecektir.", diff --git a/homeassistant/components/nuki/translations/tr.json b/homeassistant/components/nuki/translations/tr.json index 67ff0fb0faf..4932d5eff04 100644 --- a/homeassistant/components/nuki/translations/tr.json +++ b/homeassistant/components/nuki/translations/tr.json @@ -18,7 +18,7 @@ }, "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "port": "Port", "token": "Eri\u015fim Anahtar\u0131" } diff --git a/homeassistant/components/nut/translations/tr.json b/homeassistant/components/nut/translations/tr.json index 0d8b170c017..7802c451fd0 100644 --- a/homeassistant/components/nut/translations/tr.json +++ b/homeassistant/components/nut/translations/tr.json @@ -23,7 +23,7 @@ }, "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "password": "Parola", "port": "Port", "username": "Kullan\u0131c\u0131 Ad\u0131" diff --git a/homeassistant/components/nzbget/translations/tr.json b/homeassistant/components/nzbget/translations/tr.json index 27b101fb59a..f03c96daf90 100644 --- a/homeassistant/components/nzbget/translations/tr.json +++ b/homeassistant/components/nzbget/translations/tr.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "name": "Ad", "password": "Parola", "port": "Port", diff --git a/homeassistant/components/octoprint/translations/tr.json b/homeassistant/components/octoprint/translations/tr.json index 44fb5399973..499861eac20 100644 --- a/homeassistant/components/octoprint/translations/tr.json +++ b/homeassistant/components/octoprint/translations/tr.json @@ -17,7 +17,7 @@ "step": { "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "path": "Uygulama Yolu", "port": "Ba\u011flant\u0131 Noktas\u0131 Numaras\u0131", "ssl": "SSL Kullan", diff --git a/homeassistant/components/onewire/translations/tr.json b/homeassistant/components/onewire/translations/tr.json index 3d29d26fb01..e3ed5f7ce45 100644 --- a/homeassistant/components/onewire/translations/tr.json +++ b/homeassistant/components/onewire/translations/tr.json @@ -10,7 +10,7 @@ "step": { "owserver": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "port": "Port" }, "title": "Sunucu ayr\u0131nt\u0131lar\u0131n\u0131 ayarla" diff --git a/homeassistant/components/onvif/translations/tr.json b/homeassistant/components/onvif/translations/tr.json index 559350eb0c3..c471775e8fa 100644 --- a/homeassistant/components/onvif/translations/tr.json +++ b/homeassistant/components/onvif/translations/tr.json @@ -20,7 +20,7 @@ }, "configure": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "name": "Ad", "password": "Parola", "port": "Port", @@ -43,7 +43,7 @@ }, "manual_input": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "name": "Ad", "port": "Port" }, diff --git a/homeassistant/components/opengarage/translations/tr.json b/homeassistant/components/opengarage/translations/tr.json index 2a851ad2046..5580e148058 100644 --- a/homeassistant/components/opengarage/translations/tr.json +++ b/homeassistant/components/opengarage/translations/tr.json @@ -12,7 +12,7 @@ "user": { "data": { "device_key": "Cihaz Anahtar\u0131", - "host": "Ana bilgisayar", + "host": "Sunucu", "port": "Port", "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n" } diff --git a/homeassistant/components/p1_monitor/translations/tr.json b/homeassistant/components/p1_monitor/translations/tr.json index f445e82b585..88684e7bcb0 100644 --- a/homeassistant/components/p1_monitor/translations/tr.json +++ b/homeassistant/components/p1_monitor/translations/tr.json @@ -7,7 +7,7 @@ "step": { "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "name": "Ad" }, "description": "Home Assistant ile entegre etmek i\u00e7in P1 Monitor'\u00fc kurun." diff --git a/homeassistant/components/philips_js/translations/tr.json b/homeassistant/components/philips_js/translations/tr.json index 884c74b0ffa..5f89f1ab545 100644 --- a/homeassistant/components/philips_js/translations/tr.json +++ b/homeassistant/components/philips_js/translations/tr.json @@ -20,7 +20,7 @@ "user": { "data": { "api_version": "API S\u00fcr\u00fcm\u00fc", - "host": "Ana bilgisayar" + "host": "Sunucu" } } } diff --git a/homeassistant/components/pi_hole/translations/tr.json b/homeassistant/components/pi_hole/translations/tr.json index 8484e2310f1..3d9b617f197 100644 --- a/homeassistant/components/pi_hole/translations/tr.json +++ b/homeassistant/components/pi_hole/translations/tr.json @@ -15,7 +15,7 @@ "user": { "data": { "api_key": "API Anahtar\u0131", - "host": "Ana bilgisayar", + "host": "Sunucu", "location": "Konum", "name": "Ad", "port": "Port", diff --git a/homeassistant/components/progettihwsw/translations/tr.json b/homeassistant/components/progettihwsw/translations/tr.json index 6c78ef295df..845c773752d 100644 --- a/homeassistant/components/progettihwsw/translations/tr.json +++ b/homeassistant/components/progettihwsw/translations/tr.json @@ -31,7 +31,7 @@ }, "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "port": "Port" }, "title": "Panoyu kur" diff --git a/homeassistant/components/ps4/translations/tr.json b/homeassistant/components/ps4/translations/tr.json index 895491bfe4b..a526daff2a6 100644 --- a/homeassistant/components/ps4/translations/tr.json +++ b/homeassistant/components/ps4/translations/tr.json @@ -21,7 +21,7 @@ "link": { "data": { "code": "PIN Kodu", - "ip_address": "\u0130p Adresi", + "ip_address": "Ip Adresi", "name": "Ad", "region": "B\u00f6lge" }, diff --git a/homeassistant/components/rainforest_eagle/translations/tr.json b/homeassistant/components/rainforest_eagle/translations/tr.json index 39886e8669d..e12e22e8215 100644 --- a/homeassistant/components/rainforest_eagle/translations/tr.json +++ b/homeassistant/components/rainforest_eagle/translations/tr.json @@ -12,7 +12,7 @@ "user": { "data": { "cloud_id": "Bulut kimli\u011fi", - "host": "Ana bilgisayar", + "host": "Sunucu", "install_code": "Y\u00fckleme kodu" } } diff --git a/homeassistant/components/rainmachine/translations/tr.json b/homeassistant/components/rainmachine/translations/tr.json index 4a45215a737..7db1bffd5c8 100644 --- a/homeassistant/components/rainmachine/translations/tr.json +++ b/homeassistant/components/rainmachine/translations/tr.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "ip_address": "Ana makine ad\u0131 veya IP adresi", + "ip_address": "Sunucu ad\u0131 veya IP adresi", "password": "Parola", "port": "Port" }, diff --git a/homeassistant/components/rfxtrx/translations/bg.json b/homeassistant/components/rfxtrx/translations/bg.json index 82061aa125d..725534d7ac5 100644 --- a/homeassistant/components/rfxtrx/translations/bg.json +++ b/homeassistant/components/rfxtrx/translations/bg.json @@ -21,5 +21,11 @@ "title": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" } } + }, + "options": { + "error": { + "already_configured_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + } } } \ No newline at end of file diff --git a/homeassistant/components/ring/translations/bg.json b/homeassistant/components/ring/translations/bg.json index 90fdd44ba71..b0254a1bdf6 100644 --- a/homeassistant/components/ring/translations/bg.json +++ b/homeassistant/components/ring/translations/bg.json @@ -9,6 +9,9 @@ }, "step": { "2fa": { + "data": { + "2fa": "\u0414\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u0435\u043d \u043a\u043e\u0434" + }, "title": "\u0414\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" }, "user": { diff --git a/homeassistant/components/roomba/translations/tr.json b/homeassistant/components/roomba/translations/tr.json index 695ab709749..0904205993d 100644 --- a/homeassistant/components/roomba/translations/tr.json +++ b/homeassistant/components/roomba/translations/tr.json @@ -32,7 +32,7 @@ "manual": { "data": { "blid": "BLID", - "host": "Ana bilgisayar" + "host": "Sunucu" }, "description": "A\u011f\u0131n\u0131zda Roomba veya Braava bulunamad\u0131.", "title": "Cihaza manuel olarak ba\u011flan\u0131n" @@ -42,7 +42,7 @@ "blid": "BLID", "continuous": "S\u00fcrekli", "delay": "Gecikme", - "host": "Ana bilgisayar", + "host": "Sunucu", "password": "Parola" }, "description": "Roomba veya Braava'y\u0131 se\u00e7in.", diff --git a/homeassistant/components/rpi_power/translations/bg.json b/homeassistant/components/rpi_power/translations/bg.json index cc9ca7efe6c..476415635d1 100644 --- a/homeassistant/components/rpi_power/translations/bg.json +++ b/homeassistant/components/rpi_power/translations/bg.json @@ -8,5 +8,6 @@ "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0437\u0430\u043f\u043e\u0447\u043d\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0442\u0430?" } } - } + }, + "title": "\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u043d\u0430 \u0437\u0430\u0445\u0440\u0430\u043d\u0432\u0430\u043d\u0435\u0442\u043e \u043d\u0430 Raspberry Pi" } \ No newline at end of file diff --git a/homeassistant/components/samsungtv/translations/tr.json b/homeassistant/components/samsungtv/translations/tr.json index 862fe05414e..399f135bbe8 100644 --- a/homeassistant/components/samsungtv/translations/tr.json +++ b/homeassistant/components/samsungtv/translations/tr.json @@ -25,7 +25,7 @@ }, "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "name": "Ad" }, "description": "Samsung TV bilgilerini gir. Daha \u00f6nce hi\u00e7 Home Assistant'a ba\u011flamad\u0131ysan, TV'nde izin isteyen bir pencere g\u00f6receksindir." diff --git a/homeassistant/components/shelly/translations/tr.json b/homeassistant/components/shelly/translations/tr.json index c03f0a50987..0a70a067690 100644 --- a/homeassistant/components/shelly/translations/tr.json +++ b/homeassistant/components/shelly/translations/tr.json @@ -22,7 +22,7 @@ }, "user": { "data": { - "host": "Ana bilgisayar" + "host": "Sunucu" }, "description": "Kurulumdan \u00f6nce pille \u00e7al\u0131\u015fan cihazlar uyand\u0131r\u0131lmal\u0131d\u0131r, art\u0131k \u00fczerindeki bir d\u00fc\u011fmeyi kullanarak cihaz\u0131 uyand\u0131rabilirsiniz." } diff --git a/homeassistant/components/sma/translations/tr.json b/homeassistant/components/sma/translations/tr.json index dec1abfeaac..80594f0937a 100644 --- a/homeassistant/components/sma/translations/tr.json +++ b/homeassistant/components/sma/translations/tr.json @@ -14,7 +14,7 @@ "user": { "data": { "group": "Grup", - "host": "Ana bilgisayar", + "host": "Sunucu", "password": "Parola", "ssl": "SSL sertifikas\u0131 kullan\u0131r", "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n" diff --git a/homeassistant/components/smappee/translations/tr.json b/homeassistant/components/smappee/translations/tr.json index e75ea381bd1..0c44a518781 100644 --- a/homeassistant/components/smappee/translations/tr.json +++ b/homeassistant/components/smappee/translations/tr.json @@ -19,7 +19,7 @@ }, "local": { "data": { - "host": "Ana bilgisayar" + "host": "Sunucu" }, "description": "Smappee yerel entegrasyonunu ba\u015flatmak i\u00e7in ana bilgisayar\u0131 girin" }, diff --git a/homeassistant/components/soma/translations/tr.json b/homeassistant/components/soma/translations/tr.json index d7dfbff2889..f99fdba2073 100644 --- a/homeassistant/components/soma/translations/tr.json +++ b/homeassistant/components/soma/translations/tr.json @@ -13,7 +13,7 @@ "step": { "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "port": "Port" }, "description": "L\u00fctfen SOMA Connect'inizin ba\u011flant\u0131 ayarlar\u0131n\u0131 girin.", diff --git a/homeassistant/components/system_bridge/translations/tr.json b/homeassistant/components/system_bridge/translations/tr.json index 9c269e6e5fb..0a67529a511 100644 --- a/homeassistant/components/system_bridge/translations/tr.json +++ b/homeassistant/components/system_bridge/translations/tr.json @@ -21,7 +21,7 @@ "user": { "data": { "api_key": "API Anahtar\u0131", - "host": "Ana bilgisayar", + "host": "Sunucu", "port": "Port" }, "description": "L\u00fctfen ba\u011flant\u0131 bilgilerinizi giriniz." diff --git a/homeassistant/components/tailscale/translations/bg.json b/homeassistant/components/tailscale/translations/bg.json new file mode 100644 index 00000000000..a580355fe12 --- /dev/null +++ b/homeassistant/components/tailscale/translations/bg.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + } + }, + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tailscale/translations/he.json b/homeassistant/components/tailscale/translations/he.json new file mode 100644 index 00000000000..ab2a8017dc6 --- /dev/null +++ b/homeassistant/components/tailscale/translations/he.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API" + } + }, + "user": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tailscale/translations/pl.json b/homeassistant/components/tailscale/translations/pl.json new file mode 100644 index 00000000000..78ffdd12821 --- /dev/null +++ b/homeassistant/components/tailscale/translations/pl.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "api_key": "Klucz API" + } + }, + "user": { + "data": { + "api_key": "Klucz API", + "tailnet": "Tailnet" + }, + "description": "Aby uwierzytelni\u0107 si\u0119 w Tailscale, musisz utworzy\u0107 klucz API na stronie https://login.tailscale.com/admin/settings/authkeys.\n\nTailnet to nazwa Twojej sieci Tailscale. Mo\u017cna j\u0105 znale\u017a\u0107 w lewym g\u00f3rnym rogu w panelu administracyjnym Tailscale (obok logo Tailscale)." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla_wall_connector/translations/he.json b/homeassistant/components/tesla_wall_connector/translations/he.json new file mode 100644 index 00000000000..c8d48aecaff --- /dev/null +++ b/homeassistant/components/tesla_wall_connector/translations/he.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "flow_title": "{serial_number} ({host})", + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla_wall_connector/translations/tr.json b/homeassistant/components/tesla_wall_connector/translations/tr.json index 5eaeba841e2..b409c4ddc33 100644 --- a/homeassistant/components/tesla_wall_connector/translations/tr.json +++ b/homeassistant/components/tesla_wall_connector/translations/tr.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "Ana bilgisayar" + "host": "Sunucu" }, "title": "Tesla Duvar Ba\u011flant\u0131s\u0131n\u0131 Yap\u0131land\u0131r\u0131n" } diff --git a/homeassistant/components/tolo/translations/tr.json b/homeassistant/components/tolo/translations/tr.json index 2d5c8117cd6..f5dd98e93ba 100644 --- a/homeassistant/components/tolo/translations/tr.json +++ b/homeassistant/components/tolo/translations/tr.json @@ -14,7 +14,7 @@ }, "user": { "data": { - "host": "Ana bilgisayar" + "host": "Sunucu" }, "description": "TOLO Sauna cihaz\u0131n\u0131z\u0131n ana bilgisayar ad\u0131n\u0131 veya IP adresini girin." } diff --git a/homeassistant/components/tplink/translations/tr.json b/homeassistant/components/tplink/translations/tr.json index 3a1710c39d2..5e4dca23c37 100644 --- a/homeassistant/components/tplink/translations/tr.json +++ b/homeassistant/components/tplink/translations/tr.json @@ -23,7 +23,7 @@ }, "user": { "data": { - "host": "Ana bilgisayar" + "host": "Sunucu" }, "description": "Ana bilgisayar\u0131 bo\u015f b\u0131rak\u0131rsan\u0131z, cihazlar\u0131 bulmak i\u00e7in ke\u015fif kullan\u0131lacakt\u0131r." } diff --git a/homeassistant/components/tractive/translations/sensor.pl.json b/homeassistant/components/tractive/translations/sensor.pl.json new file mode 100644 index 00000000000..b49f022e279 --- /dev/null +++ b/homeassistant/components/tractive/translations/sensor.pl.json @@ -0,0 +1,9 @@ +{ + "state": { + "tractive__tracker_state": { + "not_reporting": "Nie odpowiada", + "operational": "Operacyjny", + "system_startup": "Uruchamianie systemu" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/translations/tr.json b/homeassistant/components/tradfri/translations/tr.json index 26db68abc5c..fabd1e35f97 100644 --- a/homeassistant/components/tradfri/translations/tr.json +++ b/homeassistant/components/tradfri/translations/tr.json @@ -13,7 +13,7 @@ "step": { "auth": { "data": { - "host": "Ana Bilgisayar", + "host": "Sunucu", "security_code": "G\u00fcvenlik Kodu" }, "description": "G\u00fcvenlik kodunu a\u011f ge\u00e7idinizin arkas\u0131nda bulabilirsiniz.", diff --git a/homeassistant/components/trafikverket_weatherstation/translations/bg.json b/homeassistant/components/trafikverket_weatherstation/translations/bg.json new file mode 100644 index 00000000000..2e96cb420e6 --- /dev/null +++ b/homeassistant/components/trafikverket_weatherstation/translations/bg.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + }, + "step": { + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447", + "name": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435", + "station": "\u0421\u0442\u0430\u043d\u0446\u0438\u044f" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_weatherstation/translations/he.json b/homeassistant/components/trafikverket_weatherstation/translations/he.json new file mode 100644 index 00000000000..e6eceda994c --- /dev/null +++ b/homeassistant/components/trafikverket_weatherstation/translations/he.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "step": { + "user": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API", + "name": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9", + "station": "\u05ea\u05d7\u05e0\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_weatherstation/translations/ja.json b/homeassistant/components/trafikverket_weatherstation/translations/ja.json index d6e2cf28221..5a022f011e2 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/ja.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/ja.json @@ -5,7 +5,9 @@ }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "invalid_station": "\u6307\u5b9a\u3055\u308c\u305f\u540d\u524d\u3067weather station\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f", + "more_stations": "\u6307\u5b9a\u3055\u308c\u305f\u540d\u524d\u3067\u8907\u6570\u306eweather station\u304c\u898b\u3064\u304b\u308a\u307e\u3057\u305f" }, "step": { "user": { diff --git a/homeassistant/components/trafikverket_weatherstation/translations/pl.json b/homeassistant/components/trafikverket_weatherstation/translations/pl.json new file mode 100644 index 00000000000..28f8e9e81dd --- /dev/null +++ b/homeassistant/components/trafikverket_weatherstation/translations/pl.json @@ -0,0 +1,10 @@ +{ + "config": { + "error": { + "cannot_connect": "Nie uda\u0142o si\u0119 po\u0142\u0105czy\u0107", + "invalid_auth": "Niepoprawna autentykacja", + "invalid_station": "Nie mo\u017cna znale\u017a\u0107 stacji pogodowej o podanej nazwie", + "more_stations": "Znaleziono wiele stacji pogodowych o podanej nazwie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/select.en.json b/homeassistant/components/tuya/translations/select.en.json index 22127d17f8a..55c8d3f2f90 100644 --- a/homeassistant/components/tuya/translations/select.en.json +++ b/homeassistant/components/tuya/translations/select.en.json @@ -48,38 +48,6 @@ "on": "On", "power_off": "Off", "power_on": "On" - }, - "tuya__vacuum_cistern": { - "low": "Low", - "middle": "Middle", - "high": "High", - "closed": "Closed" - }, - "tuya__vacuum_collection": { - "small": "Small", - "middle": "Middle", - "large": "Large" - }, - "tuya__vacuum_mode": { - "standby": "Standby", - "random": "Random", - "smart": "Smart", - "wall_follow": "Follow Wall", - "mop": "Mop", - "spiral": "Spiral", - "left_spiral": "Spiral Left", - "right_spiral": "Spiral Right", - "bow": "Bow", - "left_bow": "Bow Lef", - "right_bow": "Bow Right", - "partial_bow": "Bow Partially", - "chargego": "Return to dock", - "single": "Single", - "zone": "Zone", - "pose": "Pose", - "point": "Point", - "part": "Part", - "pick_zone": "Pick Zone" } } -} +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/select.he.json b/homeassistant/components/tuya/translations/select.he.json index c9e9a0bd138..f3555baadfe 100644 --- a/homeassistant/components/tuya/translations/select.he.json +++ b/homeassistant/components/tuya/translations/select.he.json @@ -8,6 +8,10 @@ "1": "\u05db\u05d1\u05d5\u05d9", "2": "\u05de\u05d5\u05e4\u05e2\u05dc" }, + "tuya__fingerbot_mode": { + "click": "\u05d3\u05d7\u05d9\u05e4\u05d4", + "switch": "\u05de\u05ea\u05d2" + }, "tuya__led_type": { "led": "\u05dc\u05d3" }, diff --git a/homeassistant/components/venstar/translations/tr.json b/homeassistant/components/venstar/translations/tr.json index a6cb7f6ddd8..5bd255daa48 100644 --- a/homeassistant/components/venstar/translations/tr.json +++ b/homeassistant/components/venstar/translations/tr.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "password": "Parola", "pin": "PIN Kodu", "ssl": "SSL sertifikas\u0131 kullan\u0131r", diff --git a/homeassistant/components/vilfo/translations/bg.json b/homeassistant/components/vilfo/translations/bg.json index ffb69776060..871d2e268f7 100644 --- a/homeassistant/components/vilfo/translations/bg.json +++ b/homeassistant/components/vilfo/translations/bg.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/vizio/translations/tr.json b/homeassistant/components/vizio/translations/tr.json index 093334fb485..77487c0d127 100644 --- a/homeassistant/components/vizio/translations/tr.json +++ b/homeassistant/components/vizio/translations/tr.json @@ -30,7 +30,7 @@ "data": { "access_token": "Eri\u015fim Anahtar\u0131", "device_class": "Cihaz tipi", - "host": "Ana bilgisayar", + "host": "Sunucu", "name": "Ad" }, "description": "Eri\u015fim Anahtar\u0131 yaln\u0131zca TV'ler i\u00e7in gereklidir. Bir TV yap\u0131land\u0131r\u0131yorsan\u0131z ve hen\u00fcz bir Eri\u015fim Anahtar\u0131 , e\u015fle\u015ftirme i\u015fleminden ge\u00e7mek i\u00e7in bo\u015f b\u0131rak\u0131n.", diff --git a/homeassistant/components/vlc_telnet/translations/tr.json b/homeassistant/components/vlc_telnet/translations/tr.json index 6e71fed22b2..d626d6cfbdb 100644 --- a/homeassistant/components/vlc_telnet/translations/tr.json +++ b/homeassistant/components/vlc_telnet/translations/tr.json @@ -25,7 +25,7 @@ }, "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "name": "Ad", "password": "Parola", "port": "Port" diff --git a/homeassistant/components/volumio/translations/tr.json b/homeassistant/components/volumio/translations/tr.json index 1d06eb89d56..979f9532a15 100644 --- a/homeassistant/components/volumio/translations/tr.json +++ b/homeassistant/components/volumio/translations/tr.json @@ -15,7 +15,7 @@ }, "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "port": "Port" } } diff --git a/homeassistant/components/wled/translations/tr.json b/homeassistant/components/wled/translations/tr.json index 938fa8d7f69..3ead94c82b6 100644 --- a/homeassistant/components/wled/translations/tr.json +++ b/homeassistant/components/wled/translations/tr.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "Ana bilgisayar" + "host": "Sunucu" }, "description": "WLED'inizi Home Assistant ile t\u00fcmle\u015ftirmek i\u00e7in ayarlay\u0131n." }, diff --git a/homeassistant/components/yale_smart_alarm/translations/bg.json b/homeassistant/components/yale_smart_alarm/translations/bg.json index b4e9a017081..02fa5da73c3 100644 --- a/homeassistant/components/yale_smart_alarm/translations/bg.json +++ b/homeassistant/components/yale_smart_alarm/translations/bg.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u041f\u0440\u043e\u0444\u0438\u043b\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + "already_configured": "\u041f\u0440\u043e\u0444\u0438\u043b\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" }, "error": { "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" diff --git a/homeassistant/components/yale_smart_alarm/translations/he.json b/homeassistant/components/yale_smart_alarm/translations/he.json index 41f5d4493bf..fd3b0c9d361 100644 --- a/homeassistant/components/yale_smart_alarm/translations/he.json +++ b/homeassistant/components/yale_smart_alarm/translations/he.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "error": { "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" diff --git a/homeassistant/components/yamaha_musiccast/translations/tr.json b/homeassistant/components/yamaha_musiccast/translations/tr.json index 37be5646212..955ef6646c4 100644 --- a/homeassistant/components/yamaha_musiccast/translations/tr.json +++ b/homeassistant/components/yamaha_musiccast/translations/tr.json @@ -14,7 +14,7 @@ }, "user": { "data": { - "host": "Ana bilgisayar" + "host": "Sunucu" }, "description": "Home Assistant ile entegre etmek i\u00e7in MusicCast'i kurun." } diff --git a/homeassistant/components/youless/translations/tr.json b/homeassistant/components/youless/translations/tr.json index afa9c9323f2..fcb766d8e9f 100644 --- a/homeassistant/components/youless/translations/tr.json +++ b/homeassistant/components/youless/translations/tr.json @@ -6,7 +6,7 @@ "step": { "user": { "data": { - "host": "Ana bilgisayar", + "host": "Sunucu", "name": "Ad" } } From 566716d6979ae702f55300044256566239e60c68 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 3 Dec 2021 21:05:01 -0700 Subject: [PATCH 0005/2644] Ensure that inactive RainMachine switch that is toggled on is toggled back off (#60959) --- homeassistant/components/rainmachine/switch.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index ab39ca1a669..5a178718c9b 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -277,6 +277,8 @@ class RainMachineActivitySwitch(RainMachineBaseSwitch): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" if not self.coordinator.data[self.entity_description.uid]["active"]: + self._attr_is_on = False + self.async_write_ha_state() raise HomeAssistantError( f"Cannot turn on an inactive program/zone: {self.name}" ) From 7fbe1dbc99b7cdf503820d2d0356090766ae1f7e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Dec 2021 19:57:46 -1000 Subject: [PATCH 0006/2644] Remove legacy fan compatibility shim (#59781) --- homeassistant/components/demo/fan.py | 80 ++----- homeassistant/components/fan/__init__.py | 268 +++-------------------- homeassistant/components/template/fan.py | 113 +--------- tests/components/demo/test_fan.py | 30 ++- tests/components/fan/test_init.py | 18 +- tests/components/template/test_fan.py | 165 +++----------- 6 files changed, 124 insertions(+), 550 deletions(-) diff --git a/homeassistant/components/demo/fan.py b/homeassistant/components/demo/fan.py index c406ffdb214..cf5cb7708d2 100644 --- a/homeassistant/components/demo/fan.py +++ b/homeassistant/components/demo/fan.py @@ -2,10 +2,6 @@ from __future__ import annotations from homeassistant.components.fan import ( - SPEED_HIGH, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_OFF, SUPPORT_DIRECTION, SUPPORT_OSCILLATE, SUPPORT_PRESET_MODE, @@ -26,33 +22,25 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= """Set up the demo fan platform.""" async_add_entities( [ - # These fans implement the old model - DemoFan( + DemoPercentageFan( hass, "fan1", "Living Room Fan", FULL_SUPPORT, - None, [ - SPEED_OFF, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_HIGH, PRESET_MODE_AUTO, PRESET_MODE_SMART, PRESET_MODE_SLEEP, PRESET_MODE_ON, ], ), - DemoFan( + DemoPercentageFan( hass, "fan2", "Ceiling Fan", LIMITED_SUPPORT, None, - [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH], ), - # These fans implement the newer model AsyncDemoPercentageFan( hass, "fan3", @@ -64,7 +52,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= PRESET_MODE_SLEEP, PRESET_MODE_ON, ], - None, ), DemoPercentageFan( hass, @@ -77,7 +64,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= PRESET_MODE_SLEEP, PRESET_MODE_ON, ], - None, ), AsyncDemoPercentageFan( hass, @@ -90,7 +76,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= PRESET_MODE_SLEEP, PRESET_MODE_ON, ], - [], ), ] ) @@ -111,15 +96,12 @@ class BaseDemoFan(FanEntity): name: str, supported_features: int, preset_modes: list[str] | None, - speed_list: list[str] | None, ) -> None: """Initialize the entity.""" self.hass = hass self._unique_id = unique_id self._supported_features = supported_features - self._speed = SPEED_OFF self._percentage = None - self._speed_list = speed_list self._preset_modes = preset_modes self._preset_mode = None self._oscillating = None @@ -161,52 +143,6 @@ class BaseDemoFan(FanEntity): return self._supported_features -class DemoFan(BaseDemoFan, FanEntity): - """A demonstration fan component that uses legacy fan speeds.""" - - @property - def speed(self) -> str: - """Return the current speed.""" - return self._speed - - @property - def speed_list(self): - """Return the speed list.""" - return self._speed_list - - def turn_on( - self, - speed: str = None, - percentage: int = None, - preset_mode: str = None, - **kwargs, - ) -> None: - """Turn on the entity.""" - if speed is None: - speed = SPEED_MEDIUM - self.set_speed(speed) - - def turn_off(self, **kwargs) -> None: - """Turn off the entity.""" - self.oscillate(False) - self.set_speed(SPEED_OFF) - - def set_speed(self, speed: str) -> None: - """Set the speed of the fan.""" - self._speed = speed - self.schedule_update_ha_state() - - def set_direction(self, direction: str) -> None: - """Set the direction of the fan.""" - self._direction = direction - self.schedule_update_ha_state() - - def oscillate(self, oscillating: bool) -> None: - """Set oscillation.""" - self._oscillating = oscillating - self.schedule_update_ha_state() - - class DemoPercentageFan(BaseDemoFan, FanEntity): """A demonstration fan component that uses percentages.""" @@ -238,7 +174,7 @@ class DemoPercentageFan(BaseDemoFan, FanEntity): def set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" - if preset_mode in self.preset_modes: + if self.preset_modes and preset_mode in self.preset_modes: self._preset_mode = preset_mode self._percentage = None self.schedule_update_ha_state() @@ -266,6 +202,16 @@ class DemoPercentageFan(BaseDemoFan, FanEntity): """Turn off the entity.""" self.set_percentage(0) + def set_direction(self, direction: str) -> None: + """Set the direction of the fan.""" + self._direction = direction + self.schedule_update_ha_state() + + def oscillate(self, oscillating: bool) -> None: + """Set oscillation.""" + self._oscillating = oscillating + self.schedule_update_ha_state() + class AsyncDemoPercentageFan(BaseDemoFan, FanEntity): """An async demonstration fan component that uses percentages.""" diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 9289c899b57..87f50c7d938 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -72,32 +72,7 @@ ATTR_DIRECTION = "direction" ATTR_PRESET_MODE = "preset_mode" ATTR_PRESET_MODES = "preset_modes" -# Invalid speeds do not conform to the entity model, but have crept -# into core integrations at some point so we are temporarily -# accommodating them in the transition to percentages. _NOT_SPEED_OFF = "off" -_NOT_SPEED_ON = "on" -_NOT_SPEED_AUTO = "auto" -_NOT_SPEED_SMART = "smart" -_NOT_SPEED_INTERVAL = "interval" -_NOT_SPEED_IDLE = "idle" -_NOT_SPEED_FAVORITE = "favorite" -_NOT_SPEED_SLEEP = "sleep" -_NOT_SPEED_SILENT = "silent" - -_NOT_SPEEDS_FILTER = { - _NOT_SPEED_OFF, - _NOT_SPEED_ON, - _NOT_SPEED_AUTO, - _NOT_SPEED_SMART, - _NOT_SPEED_INTERVAL, - _NOT_SPEED_IDLE, - _NOT_SPEED_SILENT, - _NOT_SPEED_SLEEP, - _NOT_SPEED_FAVORITE, -} - -_FAN_NATIVE = "_fan_native" OFF_SPEED_VALUES = [SPEED_OFF, None] @@ -220,12 +195,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return await component.async_unload_entry(entry) -def _fan_native(method): - """Native fan method not overridden.""" - setattr(method, _FAN_NATIVE, True) - return method - - @dataclass class FanEntityDescription(ToggleEntityDescription): """A class that describes fan entities.""" @@ -243,19 +212,17 @@ class FanEntity(ToggleEntity): _attr_speed_count: int _attr_supported_features: int = 0 - @_fan_native def set_speed(self, speed: str) -> None: """Set the speed of the fan.""" raise NotImplementedError() async def async_set_speed_deprecated(self, speed: str): """Set the speed of the fan.""" - _LOGGER.warning( - "The fan.set_speed service is deprecated, use fan.set_percentage or fan.set_preset_mode instead" + _LOGGER.error( + "The fan.set_speed service is deprecated and will fail in 2022.3 and later, use fan.set_percentage or fan.set_preset_mode instead" ) await self.async_set_speed(speed) - @_fan_native async def async_set_speed(self, speed: str): """Set the speed of the fan.""" if speed == SPEED_OFF: @@ -263,38 +230,20 @@ class FanEntity(ToggleEntity): return if self.preset_modes and speed in self.preset_modes: - if not hasattr(self.async_set_preset_mode, _FAN_NATIVE): - await self.async_set_preset_mode(speed) - return - if not hasattr(self.set_preset_mode, _FAN_NATIVE): - await self.hass.async_add_executor_job(self.set_preset_mode, speed) - return - else: - if not hasattr(self.async_set_percentage, _FAN_NATIVE): - await self.async_set_percentage(self.speed_to_percentage(speed)) - return - if not hasattr(self.set_percentage, _FAN_NATIVE): - await self.hass.async_add_executor_job( - self.set_percentage, self.speed_to_percentage(speed) - ) - return + await self.async_set_preset_mode(speed) + return - await self.hass.async_add_executor_job(self.set_speed, speed) + await self.async_set_percentage(self.speed_to_percentage(speed)) - @_fan_native def set_percentage(self, percentage: int) -> None: """Set the speed of the fan, as a percentage.""" raise NotImplementedError() - @_fan_native async def async_set_percentage(self, percentage: int) -> None: """Set the speed of the fan, as a percentage.""" if percentage == 0: await self.async_turn_off() - elif not hasattr(self.set_percentage, _FAN_NATIVE): - await self.hass.async_add_executor_job(self.set_percentage, percentage) - else: - await self.async_set_speed(self.percentage_to_speed(percentage)) + await self.hass.async_add_executor_job(self.set_percentage, percentage) async def async_increase_speed(self, percentage_step: int | None = None) -> None: """Increase the speed of the fan.""" @@ -325,26 +274,18 @@ class FanEntity(ToggleEntity): await self.async_set_percentage(new_percentage) - @_fan_native def set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" - self._valid_preset_mode_or_raise(preset_mode) - self.set_speed(preset_mode) + raise NotImplementedError() - @_fan_native async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" - if not hasattr(self.set_preset_mode, _FAN_NATIVE): - await self.hass.async_add_executor_job(self.set_preset_mode, preset_mode) - return - - self._valid_preset_mode_or_raise(preset_mode) - await self.async_set_speed(preset_mode) + await self.hass.async_add_executor_job(self.set_preset_mode, preset_mode) def _valid_preset_mode_or_raise(self, preset_mode): """Raise NotValidPresetModeError on invalid preset_mode.""" preset_modes = self.preset_modes - if preset_mode not in preset_modes: + if not preset_modes or preset_mode not in preset_modes: raise NotValidPresetModeError( f"The preset_mode {preset_mode} is not a valid preset_mode: {preset_modes}" ) @@ -380,16 +321,15 @@ class FanEntity(ToggleEntity): This _compat version wraps async_turn_on with backwards and forward compatibility. - After the transition to percentage and preset_modes concludes, it - should be removed. + This compatibility shim will be removed in 2022.3 """ if preset_mode is not None: self._valid_preset_mode_or_raise(preset_mode) speed = preset_mode percentage = None elif speed is not None: - _LOGGER.warning( - "Calling fan.turn_on with the speed argument is deprecated, use percentage or preset_mode instead" + _LOGGER.error( + "Calling fan.turn_on with the speed argument is deprecated and will fail in 2022.3 and later, use percentage or preset_mode instead" ) if self.preset_modes and speed in self.preset_modes: preset_mode = speed @@ -441,52 +381,20 @@ class FanEntity(ToggleEntity): """Return true if the entity is on.""" return self.speed not in [SPEED_OFF, None] - @property - def _implemented_percentage(self) -> bool: - """Return true if percentage has been implemented.""" - return not hasattr(self.set_percentage, _FAN_NATIVE) or not hasattr( - self.async_set_percentage, _FAN_NATIVE - ) - - @property - def _implemented_preset_mode(self) -> bool: - """Return true if preset_mode has been implemented.""" - return not hasattr(self.set_preset_mode, _FAN_NATIVE) or not hasattr( - self.async_set_preset_mode, _FAN_NATIVE - ) - - @property - def _implemented_speed(self) -> bool: - """Return true if speed has been implemented.""" - return not hasattr(self.set_speed, _FAN_NATIVE) or not hasattr( - self.async_set_speed, _FAN_NATIVE - ) - @property def speed(self) -> str | None: """Return the current speed.""" - if self._implemented_preset_mode and (preset_mode := self.preset_mode): + if preset_mode := self.preset_mode: return preset_mode - if self._implemented_percentage: - if (percentage := self.percentage) is None: - return None - return self.percentage_to_speed(percentage) - return None + if (percentage := self.percentage) is None: + return None + return self.percentage_to_speed(percentage) @property def percentage(self) -> int | None: """Return the current speed as a percentage.""" if hasattr(self, "_attr_percentage"): return self._attr_percentage - - if ( - not self._implemented_preset_mode - and self.preset_modes - and self.speed in self.preset_modes - ): - return None - if self.speed is not None and not self._implemented_percentage: - return self.speed_to_percentage(self.speed) return 0 @property @@ -494,10 +402,6 @@ class FanEntity(ToggleEntity): """Return the number of speeds the fan supports.""" if hasattr(self, "_attr_speed_count"): return self._attr_speed_count - - speed_list = speed_list_without_preset_modes(self.speed_list) - if speed_list: - return len(speed_list) return 100 @property @@ -508,11 +412,9 @@ class FanEntity(ToggleEntity): @property def speed_list(self) -> list: """Get the list of available speeds.""" - speeds = [] - if self._implemented_percentage: - speeds += [SPEED_OFF, *LEGACY_SPEED_LIST] - if self._implemented_preset_mode and self.preset_modes: - speeds += self.preset_modes + speeds = [SPEED_OFF, *LEGACY_SPEED_LIST] + if preset_modes := self.preset_modes: + speeds.extend(preset_modes) return speeds @property @@ -540,78 +442,21 @@ class FanEntity(ToggleEntity): return attrs - @property - def _speed_list_without_preset_modes(self) -> list: - """Return the speed list without preset modes. - - This property provides forward and backwards - compatibility for conversion to percentage speeds. - """ - if not self._implemented_speed: - return LEGACY_SPEED_LIST - return speed_list_without_preset_modes(self.speed_list) - - def speed_to_percentage(self, speed: str) -> int: - """ - Map a speed to a percentage. - - Officially this should only have to deal with the 4 pre-defined speeds: - - return { - SPEED_OFF: 0, - SPEED_LOW: 33, - SPEED_MEDIUM: 66, - SPEED_HIGH: 100, - }[speed] - - Unfortunately lots of fans make up their own speeds. So the default - mapping is more dynamic. - """ + def speed_to_percentage(self, speed: str) -> int: # pylint: disable=no-self-use + """Map a legacy speed to a percentage.""" if speed in OFF_SPEED_VALUES: return 0 - - speed_list = self._speed_list_without_preset_modes - - if speed_list and speed not in speed_list: + if speed not in LEGACY_SPEED_LIST: raise NotValidSpeedError(f"The speed {speed} is not a valid speed.") + return ordered_list_item_to_percentage(LEGACY_SPEED_LIST, speed) - try: - return ordered_list_item_to_percentage(speed_list, speed) - except ValueError as ex: - raise NoValidSpeedsError( - f"The speed_list {speed_list} does not contain any valid speeds." - ) from ex - - def percentage_to_speed(self, percentage: int) -> str: - """ - Map a percentage onto self.speed_list. - - Officially, this should only have to deal with 4 pre-defined speeds. - - if value == 0: - return SPEED_OFF - elif value <= 33: - return SPEED_LOW - elif value <= 66: - return SPEED_MEDIUM - else: - return SPEED_HIGH - - Unfortunately there is currently a high degree of non-conformancy. - Until fans have been corrected a more complicated and dynamic - mapping is used. - """ + def percentage_to_speed( # pylint: disable=no-self-use + self, percentage: int + ) -> str: + """Map a percentage to a legacy speed.""" if percentage == 0: return SPEED_OFF - - speed_list = self._speed_list_without_preset_modes - - try: - return percentage_to_ordered_list_item(speed_list, percentage) - except ValueError as ex: - raise NoValidSpeedsError( - f"The speed_list {speed_list} does not contain any valid speeds." - ) from ex + return percentage_to_ordered_list_item(LEGACY_SPEED_LIST, percentage) @final @property @@ -652,10 +497,6 @@ class FanEntity(ToggleEntity): """ if hasattr(self, "_attr_preset_mode"): return self._attr_preset_mode - - speed = self.speed - if self.preset_modes and speed in self.preset_modes: - return speed return None @property @@ -666,55 +507,4 @@ class FanEntity(ToggleEntity): """ if hasattr(self, "_attr_preset_modes"): return self._attr_preset_modes - - return preset_modes_from_speed_list(self.speed_list) - - -def speed_list_without_preset_modes(speed_list: list): - """Filter out non-speeds from the speed list. - - The goal is to get the speeds in a list from lowest to - highest by removing speeds that are not valid or out of order - so we can map them to percentages. - - Examples: - input: ["off", "low", "low-medium", "medium", "medium-high", "high", "auto"] - output: ["low", "low-medium", "medium", "medium-high", "high"] - - input: ["off", "auto", "low", "medium", "high"] - output: ["low", "medium", "high"] - - input: ["off", "1", "2", "3", "4", "5", "6", "7", "smart"] - output: ["1", "2", "3", "4", "5", "6", "7"] - - input: ["Auto", "Silent", "Favorite", "Idle", "Medium", "High", "Strong"] - output: ["Medium", "High", "Strong"] - """ - - return [speed for speed in speed_list if speed.lower() not in _NOT_SPEEDS_FILTER] - - -def preset_modes_from_speed_list(speed_list: list): - """Filter out non-preset modes from the speed list. - - The goal is to return only preset modes. - - Examples: - input: ["off", "low", "low-medium", "medium", "medium-high", "high", "auto"] - output: ["auto"] - - input: ["off", "auto", "low", "medium", "high"] - output: ["auto"] - - input: ["off", "1", "2", "3", "4", "5", "6", "7", "smart"] - output: ["smart"] - - input: ["Auto", "Silent", "Favorite", "Idle", "Medium", "High", "Strong"] - output: ["Auto", "Silent", "Favorite", "Idle"] - """ - - return [ - speed - for speed in speed_list - if speed.lower() in _NOT_SPEEDS_FILTER and speed.lower() != SPEED_OFF - ] + return None diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 4a872d51dbc..d9481e8f7af 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -1,4 +1,6 @@ """Support for Template fans.""" +from __future__ import annotations + import logging import voluptuous as vol @@ -12,16 +14,11 @@ from homeassistant.components.fan import ( DIRECTION_FORWARD, DIRECTION_REVERSE, ENTITY_ID_FORMAT, - SPEED_HIGH, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_OFF, SUPPORT_DIRECTION, SUPPORT_OSCILLATE, SUPPORT_PRESET_MODE, SUPPORT_SET_SPEED, FanEntity, - preset_modes_from_speed_list, ) from homeassistant.const import ( CONF_ENTITY_ID, @@ -45,10 +42,8 @@ from .template_entity import TemplateEntity _LOGGER = logging.getLogger(__name__) CONF_FANS = "fans" -CONF_SPEED_LIST = "speeds" CONF_SPEED_COUNT = "speed_count" CONF_PRESET_MODES = "preset_modes" -CONF_SPEED_TEMPLATE = "speed_template" CONF_PERCENTAGE_TEMPLATE = "percentage_template" CONF_PRESET_MODE_TEMPLATE = "preset_mode_template" CONF_OSCILLATING_TEMPLATE = "oscillating_template" @@ -67,14 +62,10 @@ _VALID_DIRECTIONS = [DIRECTION_FORWARD, DIRECTION_REVERSE] FAN_SCHEMA = vol.All( cv.deprecated(CONF_ENTITY_ID), - cv.deprecated(CONF_SPEED_LIST), - cv.deprecated(CONF_SPEED_TEMPLATE), - cv.deprecated(CONF_SET_SPEED_ACTION), vol.Schema( { vol.Optional(CONF_FRIENDLY_NAME): cv.string, vol.Required(CONF_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_SPEED_TEMPLATE): cv.template, vol.Optional(CONF_PERCENTAGE_TEMPLATE): cv.template, vol.Optional(CONF_PRESET_MODE_TEMPLATE): cv.template, vol.Optional(CONF_OSCILLATING_TEMPLATE): cv.template, @@ -82,16 +73,11 @@ FAN_SCHEMA = vol.All( vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA, - vol.Optional(CONF_SET_SPEED_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SET_PERCENTAGE_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SET_PRESET_MODE_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SET_OSCILLATING_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SET_DIRECTION_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SPEED_COUNT): vol.Coerce(int), - vol.Optional( - CONF_SPEED_LIST, - default=[SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH], - ): cv.ensure_list, vol.Optional(CONF_PRESET_MODES): cv.ensure_list, vol.Optional(CONF_ENTITY_ID): cv.entity_ids, vol.Optional(CONF_UNIQUE_ID): cv.string, @@ -112,7 +98,6 @@ async def _async_create_entities(hass, config): friendly_name = device_config.get(CONF_FRIENDLY_NAME, device) state_template = device_config[CONF_VALUE_TEMPLATE] - speed_template = device_config.get(CONF_SPEED_TEMPLATE) percentage_template = device_config.get(CONF_PERCENTAGE_TEMPLATE) preset_mode_template = device_config.get(CONF_PRESET_MODE_TEMPLATE) oscillating_template = device_config.get(CONF_OSCILLATING_TEMPLATE) @@ -127,7 +112,6 @@ async def _async_create_entities(hass, config): set_oscillating_action = device_config.get(CONF_SET_OSCILLATING_ACTION) set_direction_action = device_config.get(CONF_SET_DIRECTION_ACTION) - speed_list = device_config[CONF_SPEED_LIST] speed_count = device_config.get(CONF_SPEED_COUNT) preset_modes = device_config.get(CONF_PRESET_MODES) unique_id = device_config.get(CONF_UNIQUE_ID) @@ -138,7 +122,6 @@ async def _async_create_entities(hass, config): device, friendly_name, state_template, - speed_template, percentage_template, preset_mode_template, oscillating_template, @@ -152,7 +135,6 @@ async def _async_create_entities(hass, config): set_oscillating_action, set_direction_action, speed_count, - speed_list, preset_modes, unique_id, ) @@ -175,7 +157,6 @@ class TemplateFan(TemplateEntity, FanEntity): device_id, friendly_name, state_template, - speed_template, percentage_template, preset_mode_template, oscillating_template, @@ -189,7 +170,6 @@ class TemplateFan(TemplateEntity, FanEntity): set_oscillating_action, set_direction_action, speed_count, - speed_list, preset_modes, unique_id, ): @@ -202,7 +182,6 @@ class TemplateFan(TemplateEntity, FanEntity): self._name = friendly_name self._template = state_template - self._speed_template = speed_template self._percentage_template = percentage_template self._preset_mode_template = preset_mode_template self._oscillating_template = oscillating_template @@ -243,13 +222,12 @@ class TemplateFan(TemplateEntity, FanEntity): ) self._state = STATE_OFF - self._speed = None self._percentage = None self._preset_mode = None self._oscillating = None self._direction = None - if self._speed_template or self._percentage_template: + if self._percentage_template: self._supported_features |= SUPPORT_SET_SPEED if self._preset_mode_template and preset_modes: self._supported_features |= SUPPORT_PRESET_MODE @@ -263,17 +241,9 @@ class TemplateFan(TemplateEntity, FanEntity): # Number of valid speeds self._speed_count = speed_count - # List of valid speeds - self._speed_list = speed_list - # List of valid preset modes self._preset_modes = preset_modes - @property - def _implemented_speed(self): - """Return true if speed has been implemented.""" - return bool(self._set_speed_script or self._speed_template) - @property def name(self): """Return the display name of this fan.""" @@ -295,27 +265,15 @@ class TemplateFan(TemplateEntity, FanEntity): return self._speed_count or 100 @property - def speed_list(self) -> list: - """Get the list of available speeds.""" - return self._speed_list - - @property - def preset_modes(self) -> list: + def preset_modes(self) -> list[str]: """Get the list of available preset modes.""" - if self._preset_modes is not None: - return self._preset_modes - return preset_modes_from_speed_list(self._speed_list) + return self._preset_modes @property def is_on(self): """Return true if device is on.""" return self._state == STATE_ON - @property - def speed(self): - """Return the current speed.""" - return self._speed - @property def preset_mode(self): """Return the current preset mode.""" @@ -366,30 +324,12 @@ class TemplateFan(TemplateEntity, FanEntity): """Turn off the fan.""" await self._off_script.async_run(context=self._context) self._state = STATE_OFF - - async def async_set_speed(self, speed: str) -> None: - """Set the speed of the fan.""" - if speed not in self.speed_list: - _LOGGER.error( - "Received invalid speed: %s. Expected: %s", speed, self.speed_list - ) - return - - self._state = STATE_OFF if speed == SPEED_OFF else STATE_ON - self._speed = speed + self._percentage = 0 self._preset_mode = None - self._percentage = self.speed_to_percentage(speed) - - if self._set_speed_script: - await self._set_speed_script.async_run( - {ATTR_SPEED: self._speed}, context=self._context - ) async def async_set_percentage(self, percentage: int) -> None: """Set the percentage speed of the fan.""" - speed_list = self.speed_list self._state = STATE_OFF if percentage == 0 else STATE_ON - self._speed = self.percentage_to_speed(percentage) if speed_list else None self._percentage = percentage self._preset_mode = None @@ -400,7 +340,7 @@ class TemplateFan(TemplateEntity, FanEntity): async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the preset_mode of the fan.""" - if preset_mode not in self.preset_modes: + if self.preset_modes and preset_mode not in self.preset_modes: _LOGGER.error( "Received invalid preset_mode: %s. Expected: %s", preset_mode, @@ -410,7 +350,6 @@ class TemplateFan(TemplateEntity, FanEntity): self._state = STATE_ON self._preset_mode = preset_mode - self._speed = preset_mode self._percentage = None if self._set_preset_mode_script: @@ -491,14 +430,6 @@ class TemplateFan(TemplateEntity, FanEntity): self._update_percentage, none_on_template_error=True, ) - if self._speed_template is not None: - self.add_template_attribute( - "_speed", - self._speed_template, - None, - self._update_speed, - none_on_template_error=True, - ) if self._oscillating_template is not None: self.add_template_attribute( "_oscillating", @@ -517,27 +448,6 @@ class TemplateFan(TemplateEntity, FanEntity): ) await super().async_added_to_hass() - @callback - def _update_speed(self, speed): - # Validate speed - speed = str(speed) - - if speed in self._speed_list: - self._speed = speed - self._percentage = self.speed_to_percentage(speed) - self._preset_mode = speed if speed in self.preset_modes else None - elif speed in (STATE_UNAVAILABLE, STATE_UNKNOWN): - self._speed = None - self._percentage = 0 - self._preset_mode = None - else: - _LOGGER.error( - "Received invalid speed: %s. Expected: %s", speed, self._speed_list - ) - self._speed = None - self._percentage = 0 - self._preset_mode = None - @callback def _update_percentage(self, percentage): # Validate percentage @@ -545,19 +455,15 @@ class TemplateFan(TemplateEntity, FanEntity): percentage = int(float(percentage)) except ValueError: _LOGGER.error("Received invalid percentage: %s", percentage) - self._speed = None self._percentage = 0 self._preset_mode = None return if 0 <= percentage <= 100: self._percentage = percentage - if self._speed_list: - self._speed = self.percentage_to_speed(percentage) self._preset_mode = None else: _LOGGER.error("Received invalid percentage: %s", percentage) - self._speed = None self._percentage = 0 self._preset_mode = None @@ -566,12 +472,10 @@ class TemplateFan(TemplateEntity, FanEntity): # Validate preset mode preset_mode = str(preset_mode) - if preset_mode in self.preset_modes: - self._speed = preset_mode + if self.preset_modes and preset_mode in self.preset_modes: self._percentage = None self._preset_mode = preset_mode elif preset_mode in (STATE_UNAVAILABLE, STATE_UNKNOWN): - self._speed = None self._percentage = None self._preset_mode = None else: @@ -580,7 +484,6 @@ class TemplateFan(TemplateEntity, FanEntity): preset_mode, self.preset_mode, ) - self._speed = None self._percentage = None self._preset_mode = None diff --git a/tests/components/demo/test_fan.py b/tests/components/demo/test_fan.py index a788e69b0d3..4767a99d0b3 100644 --- a/tests/components/demo/test_fan.py +++ b/tests/components/demo/test_fan.py @@ -321,7 +321,7 @@ async def test_set_direction(hass, fan_entity_id): assert state.attributes[fan.ATTR_DIRECTION] == fan.DIRECTION_REVERSE -@pytest.mark.parametrize("fan_entity_id", FULL_FAN_ENTITY_IDS) +@pytest.mark.parametrize("fan_entity_id", LIMITED_AND_FULL_FAN_ENTITY_IDS) async def test_set_speed(hass, fan_entity_id): """Test setting the speed of the device.""" state = hass.states.get(fan_entity_id) @@ -336,6 +336,34 @@ async def test_set_speed(hass, fan_entity_id): state = hass.states.get(fan_entity_id) assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_LOW + await hass.services.async_call( + fan.DOMAIN, + fan.SERVICE_SET_SPEED, + {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_SPEED: fan.SPEED_OFF}, + blocking=True, + ) + state = hass.states.get(fan_entity_id) + assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_OFF + + +@pytest.mark.parametrize("fan_entity_id", FANS_WITH_PRESET_MODES) +async def test_set_preset_mode_with_legacy_speed_service(hass, fan_entity_id): + """Test setting the preset mode is possible with the legacy service for backwards compat.""" + state = hass.states.get(fan_entity_id) + assert state.state == STATE_OFF + + await hass.services.async_call( + fan.DOMAIN, + fan.SERVICE_SET_SPEED, + {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_SPEED: PRESET_MODE_AUTO}, + blocking=True, + ) + state = hass.states.get(fan_entity_id) + assert state.state == STATE_ON + assert state.attributes[fan.ATTR_SPEED] == PRESET_MODE_AUTO + assert state.attributes[fan.ATTR_PERCENTAGE] is None + assert state.attributes[fan.ATTR_PRESET_MODE] == PRESET_MODE_AUTO + @pytest.mark.parametrize("fan_entity_id", FANS_WITH_PRESET_MODES) async def test_set_preset_mode(hass, fan_entity_id): diff --git a/tests/components/fan/test_init.py b/tests/components/fan/test_init.py index 3167cb16e67..d29d4378941 100644 --- a/tests/components/fan/test_init.py +++ b/tests/components/fan/test_init.py @@ -2,7 +2,7 @@ import pytest -from homeassistant.components.fan import FanEntity, NotValidPresetModeError +from homeassistant.components.fan import FanEntity class BaseFan(FanEntity): @@ -16,8 +16,8 @@ def test_fanentity(): """Test fan entity methods.""" fan = BaseFan() assert fan.state == "off" - assert len(fan.speed_list) == 0 - assert len(fan.preset_modes) == 0 + assert len(fan.speed_list) == 4 # legacy compat off,low,medium,high + assert fan.preset_modes is None assert fan.supported_features == 0 assert fan.percentage_step == 1 assert fan.speed_count == 100 @@ -26,10 +26,10 @@ def test_fanentity(): with pytest.raises(NotImplementedError): fan.oscillate(True) with pytest.raises(NotImplementedError): - fan.set_speed("slow") + fan.set_speed("low") with pytest.raises(NotImplementedError): fan.set_percentage(0) - with pytest.raises(NotValidPresetModeError): + with pytest.raises(NotImplementedError): fan.set_preset_mode("auto") with pytest.raises(NotImplementedError): fan.turn_on() @@ -42,8 +42,8 @@ async def test_async_fanentity(hass): fan = BaseFan() fan.hass = hass assert fan.state == "off" - assert len(fan.speed_list) == 0 - assert len(fan.preset_modes) == 0 + assert len(fan.speed_list) == 4 # legacy compat off,low,medium,high + assert fan.preset_modes is None assert fan.supported_features == 0 assert fan.percentage_step == 1 assert fan.speed_count == 100 @@ -52,10 +52,10 @@ async def test_async_fanentity(hass): with pytest.raises(NotImplementedError): await fan.async_oscillate(True) with pytest.raises(NotImplementedError): - await fan.async_set_speed("slow") + await fan.async_set_speed("low") with pytest.raises(NotImplementedError): await fan.async_set_percentage(0) - with pytest.raises(NotValidPresetModeError): + with pytest.raises(NotImplementedError): await fan.async_set_preset_mode("auto") with pytest.raises(NotImplementedError): await fan.async_turn_on() diff --git a/tests/components/template/test_fan.py b/tests/components/template/test_fan.py index 8ec771a05f3..1f1947183de 100644 --- a/tests/components/template/test_fan.py +++ b/tests/components/template/test_fan.py @@ -18,6 +18,7 @@ from homeassistant.components.fan import ( SPEED_OFF, SUPPORT_PRESET_MODE, SUPPORT_SET_SPEED, + NotValidSpeedError, ) from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE @@ -29,8 +30,6 @@ _TEST_FAN = "fan.test_fan" _STATE_INPUT_BOOLEAN = "input_boolean.state" # Represent for fan's state _STATE_AVAILABILITY_BOOLEAN = "availability_boolean.state" -# Represent for fan's speed -_SPEED_INPUT_SELECT = "input_select.speed" # Represent for fan's preset mode _PRESET_MODE_INPUT_SELECT = "input_select.preset_mode" # Represent for fan's speed percentage @@ -148,7 +147,6 @@ async def test_wrong_template_config(hass, start_ha): {% endif %} """, "percentage_template": "{{ states('input_number.percentage') }}", - "speed_template": "{{ states('input_select.speed') }}", "preset_mode_template": "{{ states('input_select.preset_mode') }}", "oscillating_template": "{{ states('input_select.osc') }}", "direction_template": "{{ states('input_select.direction') }}", @@ -170,7 +168,7 @@ async def test_templates_with_entities(hass, start_ha): _verify(hass, STATE_OFF, None, 0, None, None, None) hass.states.async_set(_STATE_INPUT_BOOLEAN, True) - hass.states.async_set(_SPEED_INPUT_SELECT, SPEED_MEDIUM) + hass.states.async_set(_PERCENTAGE_INPUT_NUMBER, 66) hass.states.async_set(_OSC_INPUT, "True") for set_state, set_value, speed, value in [ @@ -262,7 +260,6 @@ async def test_templates_with_entities2(hass, entity, tests, start_ha): "test_fan": { "availability_template": "{{ is_state('availability_boolean.state', 'on') }}", "value_template": "{{ 'on' }}", - "speed_template": "{{ 'medium' }}", "oscillating_template": "{{ 1 == 1 }}", "direction_template": "{{ 'forward' }}", "turn_on": {"service": "script.fan_on"}, @@ -307,9 +304,9 @@ async def test_availability_template_with_entities(hass, start_ha): "fans": { "test_fan": { "value_template": "{{ 'on' }}", - "speed_template": "{{ 'unavailable' }}", "oscillating_template": "{{ 'unavailable' }}", "direction_template": "{{ 'unavailable' }}", + "percentage_template": "{{ 0 }}", "turn_on": {"service": "script.fan_on"}, "turn_off": {"service": "script.fan_off"}, } @@ -325,9 +322,9 @@ async def test_availability_template_with_entities(hass, start_ha): "fans": { "test_fan": { "value_template": "{{ 'on' }}", - "speed_template": "{{ 'medium' }}", "oscillating_template": "{{ 1 == 1 }}", "direction_template": "{{ 'forward' }}", + "percentage_template": "{{ 66 }}", "turn_on": {"service": "script.fan_on"}, "turn_off": {"service": "script.fan_off"}, } @@ -343,9 +340,9 @@ async def test_availability_template_with_entities(hass, start_ha): "fans": { "test_fan": { "value_template": "{{ 'abc' }}", - "speed_template": "{{ '0' }}", "oscillating_template": "{{ 'xyz' }}", "direction_template": "{{ 'right' }}", + "percentage_template": "{{ 0 }}", "turn_on": {"service": "script.fan_on"}, "turn_off": {"service": "script.fan_off"}, } @@ -372,7 +369,6 @@ async def test_template_with_unavailable_entities(hass, states, start_ha): "test_fan": { "value_template": "{{ 'on' }}", "availability_template": "{{ x - 12 }}", - "speed_template": "{{ states('input_select.speed') }}", "preset_mode_template": "{{ states('input_select.preset_mode') }}", "oscillating_template": "{{ states('input_select.osc') }}", "direction_template": "{{ states('input_select.direction') }}", @@ -411,38 +407,34 @@ async def test_set_speed(hass): await _register_components(hass, preset_modes=["auto", "smart"]) await common.async_turn_on(hass, _TEST_FAN) - for cmd, t_state, type, state, value in [ - (SPEED_HIGH, SPEED_HIGH, SPEED_HIGH, STATE_ON, 100), - (SPEED_MEDIUM, SPEED_MEDIUM, SPEED_MEDIUM, STATE_ON, 66), - (SPEED_OFF, SPEED_OFF, SPEED_OFF, STATE_OFF, 0), - (SPEED_MEDIUM, SPEED_MEDIUM, SPEED_MEDIUM, STATE_ON, 66), - ("invalid", SPEED_MEDIUM, SPEED_MEDIUM, STATE_ON, 66), + for cmd, type, state, value in [ + (SPEED_HIGH, SPEED_HIGH, STATE_ON, 100), + (SPEED_MEDIUM, SPEED_MEDIUM, STATE_ON, 66), + (SPEED_OFF, SPEED_OFF, STATE_OFF, 0), + (SPEED_MEDIUM, SPEED_MEDIUM, STATE_ON, 66), ]: await common.async_set_speed(hass, _TEST_FAN, cmd) - assert hass.states.get(_SPEED_INPUT_SELECT).state == t_state + assert float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state) == value _verify(hass, state, type, value, None, None, None) + with pytest.raises(NotValidSpeedError): + await common.async_set_speed(hass, _TEST_FAN, "invalid") + + assert float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state) == 66 + _verify(hass, STATE_ON, SPEED_MEDIUM, 66, None, None, None) + async def test_set_invalid_speed(hass): """Test set invalid speed when fan has valid speed.""" await _register_components(hass) await common.async_turn_on(hass, _TEST_FAN) - for extra in [SPEED_HIGH, "invalid"]: - await common.async_set_speed(hass, _TEST_FAN, extra) - assert hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_HIGH - _verify(hass, STATE_ON, SPEED_HIGH, 100, None, None, None) + await common.async_set_speed(hass, _TEST_FAN, SPEED_HIGH) + assert float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state) == 100 + _verify(hass, STATE_ON, SPEED_HIGH, 100, None, None, None) - -async def test_custom_speed_list(hass): - """Test set custom speed list.""" - await _register_components(hass, ["1", "2", "3"]) - - await common.async_turn_on(hass, _TEST_FAN) - for extra in ["1", SPEED_MEDIUM]: - await common.async_set_speed(hass, _TEST_FAN, extra) - assert hass.states.get(_SPEED_INPUT_SELECT).state == "1" - _verify(hass, STATE_ON, "1", 33, None, None, None) + with pytest.raises(NotValidSpeedError): + await common.async_set_speed(hass, _TEST_FAN, "invalid") async def test_set_invalid_direction_from_initial_stage(hass, calls): @@ -610,7 +602,7 @@ def _verify( state = hass.states.get(_TEST_FAN) attributes = state.attributes assert state.state == str(expected_state) - assert attributes.get(ATTR_SPEED) == expected_speed + assert attributes.get(ATTR_SPEED) == expected_speed or SPEED_OFF assert attributes.get(ATTR_PERCENTAGE) == expected_percentage assert attributes.get(ATTR_OSCILLATING) == expected_oscillating assert attributes.get(ATTR_DIRECTION) == expected_direction @@ -643,27 +635,12 @@ async def _register_components( }, ) - with assert_setup_component(4, "input_select"): + with assert_setup_component(3, "input_select"): assert await setup.async_setup_component( hass, "input_select", { "input_select": { - "speed": { - "name": "Speed", - "options": [ - "", - SPEED_OFF, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_HIGH, - "1", - "2", - "3", - "auto", - "smart", - ], - }, "preset_mode": { "name": "Preset Mode", "options": ["auto", "smart"], @@ -688,7 +665,6 @@ async def _register_components( test_fan_config = { "value_template": value_template, - "speed_template": "{{ states('input_select.speed') }}", "preset_mode_template": "{{ states('input_select.preset_mode') }}", "percentage_template": "{{ states('input_number.percentage') }}", "oscillating_template": "{{ states('input_select.osc') }}", @@ -697,17 +673,19 @@ async def _register_components( "service": "input_boolean.turn_on", "entity_id": _STATE_INPUT_BOOLEAN, }, - "turn_off": { - "service": "input_boolean.turn_off", - "entity_id": _STATE_INPUT_BOOLEAN, - }, - "set_speed": { - "service": "input_select.select_option", - "data_template": { - "entity_id": _SPEED_INPUT_SELECT, - "option": "{{ speed }}", + "turn_off": [ + { + "service": "input_boolean.turn_off", + "entity_id": _STATE_INPUT_BOOLEAN, }, - }, + { + "service": "input_number.set_value", + "data_template": { + "entity_id": _PERCENTAGE_INPUT_NUMBER, + "value": 0, + }, + }, + ], "set_preset_mode": { "service": "input_select.select_option", "data_template": { @@ -738,9 +716,6 @@ async def _register_components( }, } - if speed_list: - test_fan_config["speeds"] = speed_list - if preset_modes: test_fan_config["preset_modes"] = preset_modes @@ -940,71 +915,3 @@ async def test_implemented_preset_mode(hass, start_ha): attributes = state.attributes assert attributes.get("percentage") is None assert attributes.get("supported_features") & SUPPORT_PRESET_MODE - - -@pytest.mark.parametrize("count,domain", [(1, DOMAIN)]) -@pytest.mark.parametrize( - "config", - [ - { - DOMAIN: { - "platform": "template", - "fans": { - "mechanical_ventilation": { - "friendly_name": "Mechanische ventilatie", - "unique_id": "a2fd2e38-674b-4b47-b5ef-cc2362211a72", - "value_template": "{{ states('light.mv_snelheid') }}", - "speed_template": "{{ 'fast' }}", - "speeds": ["slow", "fast"], - "set_preset_mode": [ - { - "service": "light.turn_on", - "target": { - "entity_id": "light.mv_snelheid", - }, - "data": {"brightness_pct": "{{ percentage }}"}, - } - ], - "turn_on": [ - { - "service": "switch.turn_off", - "target": { - "entity_id": "switch.mv_automatisch", - }, - }, - { - "service": "light.turn_on", - "target": { - "entity_id": "light.mv_snelheid", - }, - "data": {"brightness_pct": 40}, - }, - ], - "turn_off": [ - { - "service": "light.turn_off", - "target": { - "entity_id": "light.mv_snelheid", - }, - }, - { - "service": "switch.turn_on", - "target": { - "entity_id": "switch.mv_automatisch", - }, - }, - ], - }, - }, - } - }, - ], -) -async def test_implemented_speed(hass, start_ha): - """Test a fan that implements speed.""" - assert len(hass.states.async_all()) == 1 - - state = hass.states.get("fan.mechanical_ventilation") - attributes = state.attributes - assert attributes["percentage"] == 100 - assert attributes["speed"] == "fast" From b8071c688b679fda7f7a6ec8e40df403c351050f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 3 Dec 2021 23:24:22 -0800 Subject: [PATCH 0007/2644] Correctly type the SSDP callback function (#60964) --- homeassistant/components/yeelight/scanner.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/yeelight/scanner.py b/homeassistant/components/yeelight/scanner.py index 648168ff84a..1756fbe865c 100644 --- a/homeassistant/components/yeelight/scanner.py +++ b/homeassistant/components/yeelight/scanner.py @@ -7,7 +7,7 @@ from ipaddress import IPv4Address, IPv6Address import logging from urllib.parse import urlparse -from async_upnp_client.search import SsdpSearchListener +from async_upnp_client.search import SsdpHeaders, SsdpSearchListener from homeassistant import config_entries from homeassistant.components import network, ssdp @@ -174,10 +174,9 @@ class YeelightScanner: # of another discovery async_call_later(self._hass, 1, _async_start_flow) - async def _async_process_entry(self, response: ssdp.SsdpServiceInfo): + async def _async_process_entry(self, headers: SsdpHeaders): """Process a discovery.""" - _LOGGER.debug("Discovered via SSDP: %s", response) - headers = response.ssdp_headers + _LOGGER.debug("Discovered via SSDP: %s", headers) unique_id = headers["id"] host = urlparse(headers["location"]).hostname current_entry = self._unique_id_capabilities.get(unique_id) From 10e669e69e1d0ab5371ac4a48817a55d13ce807b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Dec 2021 21:41:58 -1000 Subject: [PATCH 0008/2644] Fix yeelight name changing to ip address if discovery fails (#60967) --- homeassistant/components/yeelight/__init__.py | 2 +- homeassistant/components/yeelight/device.py | 4 +++- tests/components/yeelight/test_init.py | 22 +++++++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index 79249ae8c44..d1b8e9d4f46 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -244,7 +244,7 @@ async def _async_get_device( # Set up device bulb = AsyncBulb(host, model=model or None) - device = YeelightDevice(hass, host, entry.options, bulb) + device = YeelightDevice(hass, host, {**entry.options, **entry.data}, bulb) # start listening for local pushes await device.bulb.async_listen(device.async_update_callback) diff --git a/homeassistant/components/yeelight/device.py b/homeassistant/components/yeelight/device.py index 5f70866b229..02228e5d9fc 100644 --- a/homeassistant/components/yeelight/device.py +++ b/homeassistant/components/yeelight/device.py @@ -7,7 +7,7 @@ import logging from yeelight import BulbException from yeelight.aio import KEY_CONNECTED -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_ID, CONF_NAME from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later @@ -199,6 +199,8 @@ class YeelightDevice: elif self.capabilities: # Generate name from model and id when capabilities is available self._name = _async_unique_name(self.capabilities) + elif self.model and (id_ := self._config.get(CONF_ID)): + self._name = f"Yeelight {async_format_model_id(self.model, id_)}" else: self._name = self._host # Default name is host diff --git a/tests/components/yeelight/test_init.py b/tests/components/yeelight/test_init.py index 13c71d656bb..dc3d602edeb 100644 --- a/tests/components/yeelight/test_init.py +++ b/tests/components/yeelight/test_init.py @@ -588,3 +588,25 @@ async def test_non_oserror_exception_on_first_update( await hass.async_block_till_done() assert hass.states.get("light.test_name").state != STATE_UNAVAILABLE + + +async def test_async_setup_with_discovery_not_working(hass: HomeAssistant): + """Test we can setup even if discovery is broken.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "127.0.0.1", CONF_ID: ID}, + options={}, + unique_id=ID, + ) + config_entry.add_to_hass(hass) + + with _patch_discovery( + no_device=True + ), _patch_discovery_timeout(), _patch_discovery_interval(), patch( + f"{MODULE}.AsyncBulb", return_value=_mocked_bulb() + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED + + assert hass.states.get("light.yeelight_color_0x15243f").state == STATE_ON From 156435d1c28e02cc24b11b4b1304e7cc46f7f1e2 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 4 Dec 2021 08:57:24 +0100 Subject: [PATCH 0009/2644] Update integrations to use `async_setup_platforms` (#60956) * Replace forward_entry_setup with setup_platforms * Apply suggestions from code review Co-authored-by: Marvin Wichmann Co-authored-by: Marvin Wichmann --- .../components/ambiclimate/__init__.py | 13 +++++---- homeassistant/components/cast/__init__.py | 13 +++++---- homeassistant/components/ios/__init__.py | 13 +++++---- .../components/nest/legacy/__init__.py | 13 +++++---- .../components/plum_lightpad/__init__.py | 14 ++++++---- homeassistant/components/roon/server.py | 9 ++---- homeassistant/components/withings/__init__.py | 20 ++++++------- homeassistant/components/zwave/__init__.py | 28 +++++++++---------- 8 files changed, 65 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/ambiclimate/__init__.py b/homeassistant/components/ambiclimate/__init__.py index ac6334638a4..18cf8c177d9 100644 --- a/homeassistant/components/ambiclimate/__init__.py +++ b/homeassistant/components/ambiclimate/__init__.py @@ -1,7 +1,9 @@ """Support for Ambiclimate devices.""" import voluptuous as vol -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, Platform +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from . import config_flow @@ -19,6 +21,8 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +PLATFORMS = [Platform.CLIMATE] + async def async_setup(hass, config) -> bool: """Set up Ambiclimate components.""" @@ -34,10 +38,7 @@ async def async_setup(hass, config) -> bool: return True -async def async_setup_entry(hass, entry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Ambiclimate from a config entry.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "climate") - ) - + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True diff --git a/homeassistant/components/cast/__init__.py b/homeassistant/components/cast/__init__.py index 9ccac6e4f6c..4401553869f 100644 --- a/homeassistant/components/cast/__init__.py +++ b/homeassistant/components/cast/__init__.py @@ -4,6 +4,8 @@ import logging import voluptuous as vol from homeassistant import config_entries +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from . import home_assistant_cast @@ -15,6 +17,8 @@ CONFIG_SCHEMA = cv.deprecated(DOMAIN) _LOGGER = logging.getLogger(__name__) +PLATFORMS = [Platform.MEDIA_PLAYER] + async def async_setup(hass, config): """Set up the Cast component.""" @@ -41,13 +45,12 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, entry: config_entries.ConfigEntry): +async def async_setup_entry( + hass: HomeAssistant, entry: config_entries.ConfigEntry +) -> bool: """Set up Cast from a config entry.""" await home_assistant_cast.async_setup_ha_cast(hass, entry) - - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "media_player") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True diff --git a/homeassistant/components/ios/__init__.py b/homeassistant/components/ios/__init__.py index 048107910d1..0f1cadb55ba 100644 --- a/homeassistant/components/ios/__init__.py +++ b/homeassistant/components/ios/__init__.py @@ -6,7 +6,8 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components.http import HomeAssistantView -from homeassistant.core import callback +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -209,6 +210,8 @@ IDENTIFY_SCHEMA = vol.Schema( CONFIGURATION_FILE = ".ios.conf" +PLATFORMS = [Platform.SENSOR] + def devices_with_push(hass): """Return a dictionary of push enabled targets.""" @@ -272,11 +275,11 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, entry): +async def async_setup_entry( + hass: HomeAssistant, entry: config_entries.ConfigEntry +) -> bool: """Set up an iOS entry.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "sensor") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) hass.http.register_view(iOSIdentifyDeviceView(hass.config.path(CONFIGURATION_FILE))) hass.http.register_view(iOSPushConfigView(hass.data[DOMAIN][CONF_USER][CONF_PUSH])) diff --git a/homeassistant/components/nest/legacy/__init__.py b/homeassistant/components/nest/legacy/__init__.py index 85ff98f6420..2259ee0ad5c 100644 --- a/homeassistant/components/nest/legacy/__init__.py +++ b/homeassistant/components/nest/legacy/__init__.py @@ -17,6 +17,7 @@ from homeassistant.const import ( CONF_STRUCTURE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, + Platform, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv @@ -29,7 +30,12 @@ from .const import DATA_NEST, DATA_NEST_CONFIG, DOMAIN, SIGNAL_NEST_UPDATE _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["climate", "camera", "sensor", "binary_sensor"] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.CAMERA, + Platform.CLIMATE, + Platform.SENSOR, +] # Configuration for the legacy nest API SERVICE_CANCEL_ETA = "cancel_eta" @@ -134,10 +140,7 @@ async def async_setup_legacy_entry(hass: HomeAssistant, entry: ConfigEntry) -> b if not await hass.async_add_executor_job(hass.data[DATA_NEST].initialize): return False - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) def validate_structures(target_structures): all_structures = [structure.name for structure in nest.structures] diff --git a/homeassistant/components/plum_lightpad/__init__.py b/homeassistant/components/plum_lightpad/__init__.py index f92d087b79d..cca40f4e5a9 100644 --- a/homeassistant/components/plum_lightpad/__init__.py +++ b/homeassistant/components/plum_lightpad/__init__.py @@ -6,7 +6,12 @@ from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ( + CONF_PASSWORD, + CONF_USERNAME, + EVENT_HOMEASSISTANT_STOP, + Platform, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv @@ -32,7 +37,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -PLATFORMS = ["light"] +PLATFORMS = [Platform.LIGHT] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -71,10 +76,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = plum - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) def cleanup(event): """Clean up resources.""" diff --git a/homeassistant/components/roon/server.py b/homeassistant/components/roon/server.py index d216dca419d..8301bf73fdf 100644 --- a/homeassistant/components/roon/server.py +++ b/homeassistant/components/roon/server.py @@ -4,7 +4,7 @@ import logging from roonapi import RoonApi -from homeassistant.const import CONF_API_KEY, CONF_HOST +from homeassistant.const import CONF_API_KEY, CONF_HOST, Platform from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.util.dt import utcnow @@ -12,6 +12,7 @@ from .const import CONF_ROON_ID, ROON_APPINFO _LOGGER = logging.getLogger(__name__) FULL_SYNC_INTERVAL = 30 +PLATFORMS = [Platform.MEDIA_PLAYER] class RoonServer: @@ -51,11 +52,7 @@ class RoonServer: self.roon_id = core_id if core_id is not None else host # initialize media_player platform - hass.async_create_task( - hass.config_entries.async_forward_entry_setup( - self.config_entry, "media_player" - ) - ) + hass.config_entries.async_setup_platforms(self.config_entry, PLATFORMS) # Initialize Roon background polling asyncio.create_task(self.async_do_loop()) diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index 7132b3bff9d..800f8b654bb 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -13,13 +13,16 @@ from withings_api import WithingsAuth from withings_api.common import NotifyAppli from homeassistant.components import webhook -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.webhook import ( async_unregister as async_unregister_webhook, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_WEBHOOK_ID +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + CONF_WEBHOOK_ID, + Platform, +) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import async_call_later @@ -36,6 +39,7 @@ from .common import ( ) DOMAIN = const.DOMAIN +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] CONFIG_SCHEMA = vol.Schema( { @@ -143,12 +147,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Start subscription check in the background, outside this component's setup. async_call_later(hass, 1, async_call_later_callback) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, BINARY_SENSOR_DOMAIN) - ) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, SENSOR_DOMAIN) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -162,8 +161,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await asyncio.gather( data_manager.async_unsubscribe_webhook(), - hass.config_entries.async_forward_entry_unload(entry, BINARY_SENSOR_DOMAIN), - hass.config_entries.async_forward_entry_unload(entry, SENSOR_DOMAIN), + hass.config_entries.async_unload_platforms(entry, PLATFORMS), ) async_remove_data_manager(hass, entry) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index ef3ab223248..f21b75aaffc 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -15,8 +15,9 @@ from homeassistant.const import ( ATTR_VIA_DEVICE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, + Platform, ) -from homeassistant.core import CoreState, callback +from homeassistant.core import CoreState, HomeAssistant, callback from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import ( @@ -98,14 +99,14 @@ DEFAULT_CONF_REFRESH_VALUE = False DEFAULT_CONF_REFRESH_DELAY = 5 PLATFORMS = [ - "binary_sensor", - "climate", - "cover", - "fan", - "lock", - "light", - "sensor", - "switch", + Platform.BINARY_SENSOR, + Platform.CLIMATE, + Platform.COVER, + Platform.FAN, + Platform.LIGHT, + Platform.LOCK, + Platform.SENSOR, + Platform.SWITCH, ] RENAME_NODE_SCHEMA = vol.Schema( @@ -341,7 +342,9 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, config_entry): # noqa: C901 +async def async_setup_entry( # noqa: C901 + hass: HomeAssistant, config_entry: config_entries.ConfigEntry +) -> bool: """Set up Z-Wave from a config entry. Will automatically load components to support devices found on the network. @@ -1021,10 +1024,7 @@ async def async_setup_entry(hass, config_entry): # noqa: C901 hass.services.async_register(DOMAIN, const.SERVICE_START_NETWORK, start_zwave) - for entry_component in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, entry_component) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True From 1485020a2e676e688f6982c8f0f2a725295c38d1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 4 Dec 2021 09:15:26 +0100 Subject: [PATCH 0010/2644] Upgrade netdata to 1.0.1 (#60971) --- homeassistant/components/netdata/manifest.json | 2 +- homeassistant/components/netdata/sensor.py | 4 +--- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/netdata/manifest.json b/homeassistant/components/netdata/manifest.json index 9d79f54450c..34fbf45c529 100644 --- a/homeassistant/components/netdata/manifest.json +++ b/homeassistant/components/netdata/manifest.json @@ -2,7 +2,7 @@ "domain": "netdata", "name": "Netdata", "documentation": "https://www.home-assistant.io/integrations/netdata", - "requirements": ["netdata==0.2.0"], + "requirements": ["netdata==1.0.1"], "codeowners": ["@fabaff"], "iot_class": "local_polling" } diff --git a/homeassistant/components/netdata/sensor.py b/homeassistant/components/netdata/sensor.py index d1fa87a6e5d..3b1e9a0ed47 100644 --- a/homeassistant/components/netdata/sensor.py +++ b/homeassistant/components/netdata/sensor.py @@ -16,7 +16,6 @@ from homeassistant.const import ( PERCENTAGE, ) from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle @@ -61,8 +60,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= port = config.get(CONF_PORT) resources = config.get(CONF_RESOURCES) - session = async_get_clientsession(hass) - netdata = NetdataData(Netdata(host, hass.loop, session, port=port)) + netdata = NetdataData(Netdata(host, port=port)) await netdata.async_update() if netdata.api.metrics is None: diff --git a/requirements_all.txt b/requirements_all.txt index e49e0aae85d..974b5014205 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1050,7 +1050,7 @@ ndms2_client==0.1.1 nessclient==0.9.15 # homeassistant.components.netdata -netdata==0.2.0 +netdata==1.0.1 # homeassistant.components.discovery netdisco==3.0.0 From b2ee62ba8d9dd777db7e28bcfbb41e2c06fbdb44 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 4 Dec 2021 09:16:00 +0100 Subject: [PATCH 0011/2644] Upgrade luftdaten to 0.7.1 (#60970) --- homeassistant/components/luftdaten/__init__.py | 5 +---- homeassistant/components/luftdaten/config_flow.py | 4 +--- homeassistant/components/luftdaten/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py index a87c67620cd..f8a67fff2f3 100644 --- a/homeassistant/components/luftdaten/__init__.py +++ b/homeassistant/components/luftdaten/__init__.py @@ -24,7 +24,6 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval @@ -175,11 +174,9 @@ async def async_setup_entry(hass, config_entry): hass.async_create_task(hass.config_entries.async_remove(config_entry.entry_id)) return False - session = async_get_clientsession(hass) - try: luftdaten = LuftDatenData( - Luftdaten(config_entry.data[CONF_SENSOR_ID], hass.loop, session), + Luftdaten(config_entry.data[CONF_SENSOR_ID]), config_entry.data.get(CONF_SENSORS, {}).get( CONF_MONITORED_CONDITIONS, SENSOR_KEYS ), diff --git a/homeassistant/components/luftdaten/config_flow.py b/homeassistant/components/luftdaten/config_flow.py index f13fcc831dc..56dee86e9fb 100644 --- a/homeassistant/components/luftdaten/config_flow.py +++ b/homeassistant/components/luftdaten/config_flow.py @@ -13,7 +13,6 @@ from homeassistant.const import ( CONF_SHOW_ON_MAP, ) from homeassistant.core import callback -from homeassistant.helpers import aiohttp_client import homeassistant.helpers.config_validation as cv from .const import CONF_SENSOR_ID, DEFAULT_SCAN_INTERVAL, DOMAIN @@ -69,8 +68,7 @@ class LuftDatenFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if sensor_id in configured_sensors(self.hass): return self._show_form({CONF_SENSOR_ID: "already_configured"}) - session = aiohttp_client.async_get_clientsession(self.hass) - luftdaten = Luftdaten(user_input[CONF_SENSOR_ID], self.hass.loop, session) + luftdaten = Luftdaten(user_input[CONF_SENSOR_ID]) try: await luftdaten.get_data() valid = await luftdaten.validate_sensor() diff --git a/homeassistant/components/luftdaten/manifest.json b/homeassistant/components/luftdaten/manifest.json index f296093b556..fd355bd8d3c 100644 --- a/homeassistant/components/luftdaten/manifest.json +++ b/homeassistant/components/luftdaten/manifest.json @@ -3,7 +3,7 @@ "name": "Luftdaten", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/luftdaten", - "requirements": ["luftdaten==0.6.5"], + "requirements": ["luftdaten==0.7.1"], "codeowners": ["@fabaff"], "quality_scale": "gold", "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 974b5014205..18b61d884ae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -966,7 +966,7 @@ logi_circle==0.2.2 london-tube-status==0.2 # homeassistant.components.luftdaten -luftdaten==0.6.5 +luftdaten==0.7.1 # homeassistant.components.lupusec lupupy==0.0.21 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 82d1aefb1bc..027cec2714a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -591,7 +591,7 @@ libsoundtouch==0.8 logi_circle==0.2.2 # homeassistant.components.luftdaten -luftdaten==0.6.5 +luftdaten==0.7.1 # homeassistant.components.nmap_tracker mac-vendor-lookup==0.1.11 From 2fe48702f38d76e41607db89478cf2b2ad18e84e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 4 Dec 2021 00:17:13 -0800 Subject: [PATCH 0012/2644] Fix statistics registering at start callback (#60963) --- homeassistant/components/statistics/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 23a4a31d936..a931a4cf806 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -243,8 +243,7 @@ class StatisticsSensor(SensorEntity): self._add_state_to_queue(new_state) self.async_schedule_update_ha_state(True) - @callback - def async_stats_sensor_startup(_): + async def async_stats_sensor_startup(_): """Add listener and get recorded state.""" _LOGGER.debug("Startup for %s", self.entity_id) From a59ec9ca5e76b04562ccc30166e6a5de9ed3091b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 4 Dec 2021 00:20:12 -0800 Subject: [PATCH 0013/2644] Handle invalid device registry entry type (#60966) Co-authored-by: Franck Nijhof --- homeassistant/helpers/device_registry.py | 6 ++++- tests/helpers/test_device_registry.py | 32 ++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index c8ae7fd148c..e31b77d3ae2 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -172,7 +172,11 @@ class DeviceRegistryStore(storage.Store): # From version 1.1 for device in old_data["devices"]: # Introduced in 0.110 - device["entry_type"] = device.get("entry_type") + try: + device["entry_type"] = DeviceEntryType(device.get("entry_type")) + except ValueError: + device["entry_type"] = None + # Introduced in 0.79 # renamed in 0.95 device["via_device_id"] = device.get("via_device_id") or device.get( diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index a689cc9ac3d..455c90b8f65 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -252,7 +252,19 @@ async def test_migration_1_1_to_1_2(hass, hass_storage): "model": "model", "name": "name", "sw_version": "version", - } + }, + # Invalid entry type + { + "config_entries": [None], + "connections": [], + "entry_type": "INVALID_VALUE", + "id": "invalid-entry-type", + "identifiers": [["serial", "mock-id-invalid-entry"]], + "manufacturer": None, + "model": None, + "name": None, + "sw_version": None, + }, ], }, } @@ -300,7 +312,23 @@ async def test_migration_1_1_to_1_2(hass, hass_storage): "name_by_user": None, "sw_version": "new_version", "via_device_id": None, - } + }, + { + "area_id": None, + "config_entries": [None], + "configuration_url": None, + "connections": [], + "disabled_by": None, + "entry_type": None, + "id": "invalid-entry-type", + "identifiers": [["serial", "mock-id-invalid-entry"]], + "manufacturer": None, + "model": None, + "name_by_user": None, + "name": None, + "sw_version": None, + "via_device_id": None, + }, ], "deleted_devices": [], }, From ad63149927f3a1408a71c467ca0e33ab2922ca2b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Dec 2021 22:20:56 -1000 Subject: [PATCH 0014/2644] Fix dimmable effects for flux_led model 0x33 v9+ (#60972) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 60efb52934c..565b560b45e 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.25.10"], + "requirements": ["flux_led==0.25.12"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 18b61d884ae..5556ee81ef1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.25.10 +flux_led==0.25.12 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 027cec2714a..ceb373f326c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.25.10 +flux_led==0.25.12 # homeassistant.components.homekit fnvhash==0.1.0 From a553054fdef0a4ec3b437f9684f8b3699adfcab3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Dec 2021 22:44:16 -1000 Subject: [PATCH 0015/2644] Fix flood lights not turning on/off with flux_led (#60973) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 565b560b45e..1f7c84e73d6 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.25.12"], + "requirements": ["flux_led==0.25.13"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 5556ee81ef1..765b67088f2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.25.12 +flux_led==0.25.13 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ceb373f326c..e8b2c62eeb4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.25.12 +flux_led==0.25.13 # homeassistant.components.homekit fnvhash==0.1.0 From 6d6e0dd8bf6dcf2d1cea937b04e9eef286936c75 Mon Sep 17 00:00:00 2001 From: Thomas Dietrich Date: Sat, 4 Dec 2021 09:50:47 +0100 Subject: [PATCH 0016/2644] Add unique_id to the statistics component (#59205) * Implement optional manually defined uniqueid * Fix test case via mocked environment --- homeassistant/components/statistics/sensor.py | 10 ++++++++ tests/components/statistics/test_sensor.py | 24 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index a931a4cf806..44548c7d1a7 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -17,6 +17,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_ENTITY_ID, CONF_NAME, + CONF_UNIQUE_ID, STATE_UNAVAILABLE, STATE_UNKNOWN, ) @@ -115,6 +116,7 @@ _PLATFORM_SCHEMA_BASE = PLATFORM_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_STATE_CHARACTERISTIC, default=STAT_DEFAULT): vol.In( [ STAT_AVERAGE_LINEAR, @@ -170,6 +172,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= StatisticsSensor( source_entity_id=config.get(CONF_ENTITY_ID), name=config.get(CONF_NAME), + unique_id=config.get(CONF_UNIQUE_ID), state_characteristic=config.get(CONF_STATE_CHARACTERISTIC), samples_max_buffer_size=config.get(CONF_SAMPLES_MAX_BUFFER_SIZE), samples_max_age=config.get(CONF_MAX_AGE), @@ -190,6 +193,7 @@ class StatisticsSensor(SensorEntity): self, source_entity_id, name, + unique_id, state_characteristic, samples_max_buffer_size, samples_max_age, @@ -201,6 +205,7 @@ class StatisticsSensor(SensorEntity): self._source_entity_id = source_entity_id self.is_binary = self._source_entity_id.split(".")[0] == "binary_sensor" self._name = name + self._unique_id = unique_id self._state_characteristic = state_characteristic if self._state_characteristic == STAT_DEFAULT: self._state_characteristic = STAT_COUNT if self.is_binary else STAT_MEAN @@ -335,6 +340,11 @@ class StatisticsSensor(SensorEntity): """Return the name of the sensor.""" return self._name + @property + def unique_id(self): + """Return the unique id of the sensor.""" + return self._unique_id + @property def state_class(self): """Return the state class of this entity.""" diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index 658d6a089e7..50f6993e44c 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -17,6 +17,7 @@ from homeassistant.const import ( STATE_UNKNOWN, TEMP_CELSIUS, ) +from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component, setup_component from homeassistant.util import dt as dt_util @@ -35,6 +36,29 @@ def mock_legacy_time(legacy_patchable_time): yield +async def test_unique_id(hass): + """Test configuration defined unique_id.""" + assert await async_setup_component( + hass, + "sensor", + { + "sensor": [ + { + "platform": "statistics", + "name": "test", + "entity_id": "sensor.test_monitored", + "unique_id": "uniqueid_sensor_test", + }, + ] + }, + ) + await hass.async_block_till_done() + + entity_reg = er.async_get(hass) + entity_id = entity_reg.async_get_entity_id("sensor", DOMAIN, "uniqueid_sensor_test") + assert entity_id == "sensor.test" + + class TestStatisticsSensor(unittest.TestCase): """Test the Statistics sensor.""" From 267896cfc0cc0c78762ca7b9b58132f6e6ba10a0 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sat, 4 Dec 2021 04:17:17 -0500 Subject: [PATCH 0017/2644] Address late review of eight_sleep (#60951) --- homeassistant/components/eight_sleep/__init__.py | 6 ++---- homeassistant/components/eight_sleep/binary_sensor.py | 3 ++- homeassistant/components/eight_sleep/sensor.py | 10 ++++++++-- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/eight_sleep/__init__.py b/homeassistant/components/eight_sleep/__init__.py index 09229ce767e..28de45392eb 100644 --- a/homeassistant/components/eight_sleep/__init__.py +++ b/homeassistant/components/eight_sleep/__init__.py @@ -96,7 +96,7 @@ CONFIG_SCHEMA = vol.Schema( ) -def _get_device_unique_id(eight: EightSleep, user_obj: EightUser = None) -> str: +def _get_device_unique_id(eight: EightSleep, user_obj: EightUser | None = None) -> str: """Get the device's unique ID.""" unique_id = eight.deviceid if user_obj: @@ -199,7 +199,6 @@ class EightSleepHeatDataCoordinator(DataUpdateCoordinator): _LOGGER, name=f"{DOMAIN}_heat", update_interval=HEAT_SCAN_INTERVAL, - update_method=self._async_update_data, ) async def _async_update_data(self) -> None: @@ -217,7 +216,6 @@ class EightSleepUserDataCoordinator(DataUpdateCoordinator): _LOGGER, name=f"{DOMAIN}_user", update_interval=USER_SCAN_INTERVAL, - update_method=self._async_update_data, ) async def _async_update_data(self) -> None: @@ -240,7 +238,7 @@ class EightSleepBaseEntity(CoordinatorEntity): self._eight = eight self._side = side self._sensor = sensor - self._usrobj: EightUser = None + self._usrobj: EightUser | None = None if self._side: self._usrobj = self._eight.users[self._eight.fetch_userid(self._side)] full_sensor_name = self._sensor diff --git a/homeassistant/components/eight_sleep/binary_sensor.py b/homeassistant/components/eight_sleep/binary_sensor.py index 7240d65d262..cb1b2e36f79 100644 --- a/homeassistant/components/eight_sleep/binary_sensor.py +++ b/homeassistant/components/eight_sleep/binary_sensor.py @@ -62,7 +62,7 @@ class EightHeatSensor(EightSleepBaseEntity, BinarySensorEntity): """Initialize the sensor.""" super().__init__(name, coordinator, eight, side, sensor) self._attr_device_class = DEVICE_CLASS_OCCUPANCY - + assert self._usrobj _LOGGER.debug( "Presence Sensor: %s, Side: %s, User: %s", self._sensor, @@ -73,4 +73,5 @@ class EightHeatSensor(EightSleepBaseEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Return true if the binary sensor is on.""" + assert self._usrobj return bool(self._usrobj.bed_presence) diff --git a/homeassistant/components/eight_sleep/sensor.py b/homeassistant/components/eight_sleep/sensor.py index 42270ad4fc4..6cb2d5bf13c 100644 --- a/homeassistant/components/eight_sleep/sensor.py +++ b/homeassistant/components/eight_sleep/sensor.py @@ -106,6 +106,7 @@ class EightHeatSensor(EightSleepBaseEntity, SensorEntity): """Initialize the sensor.""" super().__init__(name, coordinator, eight, side, sensor) self._attr_native_unit_of_measurement = PERCENTAGE + assert self._usrobj _LOGGER.debug( "Heat Sensor: %s, Side: %s, User: %s", @@ -117,11 +118,13 @@ class EightHeatSensor(EightSleepBaseEntity, SensorEntity): @property def native_value(self) -> int: """Return the state of the sensor.""" + assert self._usrobj return self._usrobj.heating_level @property def extra_state_attributes(self) -> dict[str, Any]: """Return device state attributes.""" + assert self._usrobj return { ATTR_TARGET_HEAT: self._usrobj.target_heating_level, ATTR_ACTIVE_HEAT: self._usrobj.now_heating, @@ -167,6 +170,9 @@ class EightUserSensor(EightSleepUserEntity, SensorEntity): @property def native_value(self) -> str | int | float | None: """Return the state of the sensor.""" + if not self._usrobj: + return None + if "current" in self._sensor: if "fitness" in self._sensor: return self._usrobj.current_sleep_fitness_score @@ -215,12 +221,12 @@ class EightUserSensor(EightSleepUserEntity, SensorEntity): def extra_state_attributes(self) -> dict[str, Any] | None: """Return device state attributes.""" attr = None - if "current" in self._sensor: + if "current" in self._sensor and self._usrobj: if "fitness" in self._sensor: attr = self._usrobj.current_fitness_values else: attr = self._usrobj.current_values - elif "last" in self._sensor: + elif "last" in self._sensor and self._usrobj: attr = self._usrobj.last_values if attr is None: From 8bb1a3fac413bbc3cb07b7922f8e7200446ba186 Mon Sep 17 00:00:00 2001 From: Chen-IL <18098431+Chen-IL@users.noreply.github.com> Date: Sat, 4 Dec 2021 11:29:26 +0200 Subject: [PATCH 0018/2644] Improve asuswrt code readability (#60975) --- homeassistant/components/asuswrt/router.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/asuswrt/router.py b/homeassistant/components/asuswrt/router.py index da314b12b65..67e592c71ef 100644 --- a/homeassistant/components/asuswrt/router.py +++ b/homeassistant/components/asuswrt/router.py @@ -362,14 +362,14 @@ class AsusWrtRouter: SENSORS_TYPE_COUNT: SENSORS_CONNECTED_DEVICE, SENSORS_TYPE_LOAD_AVG: SENSORS_LOAD_AVG, SENSORS_TYPE_RATES: SENSORS_RATES, - SENSORS_TYPE_TEMPERATURES: SENSORS_TEMPERATURES, } + sensors_types[ + SENSORS_TYPE_TEMPERATURES + ] = await self._get_available_temperature_sensors() for sensor_type, sensor_names in sensors_types.items(): - if sensor_type == SENSORS_TYPE_TEMPERATURES: - sensor_names = await self._get_available_temperature_sensors() - if not sensor_names: - continue + if not sensor_names: + continue coordinator = await self._sensors_data_handler.get_coordinator( sensor_type, sensor_type != SENSORS_TYPE_COUNT ) From c9bb688a79db23f0b264d9c7c8f93dd131f38640 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 4 Dec 2021 10:29:48 +0100 Subject: [PATCH 0019/2644] Revert metoffice weather daytime (#60978) --- homeassistant/components/metoffice/const.py | 2 -- homeassistant/components/metoffice/weather.py | 9 ++------- tests/components/metoffice/test_weather.py | 11 ----------- 3 files changed, 2 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/metoffice/const.py b/homeassistant/components/metoffice/const.py index d1f48eb4f2c..e413b102898 100644 --- a/homeassistant/components/metoffice/const.py +++ b/homeassistant/components/metoffice/const.py @@ -24,8 +24,6 @@ DOMAIN = "metoffice" DEFAULT_NAME = "Met Office" ATTRIBUTION = "Data provided by the Met Office" -ATTR_FORECAST_DAYTIME = "daytime" - DEFAULT_SCAN_INTERVAL = timedelta(minutes=15) METOFFICE_COORDINATES = "metoffice_coordinates" diff --git a/homeassistant/components/metoffice/weather.py b/homeassistant/components/metoffice/weather.py index d25df1d2654..79363db3667 100644 --- a/homeassistant/components/metoffice/weather.py +++ b/homeassistant/components/metoffice/weather.py @@ -15,7 +15,6 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import get_device_info from .const import ( - ATTR_FORECAST_DAYTIME, ATTRIBUTION, CONDITION_CLASSES, DEFAULT_NAME, @@ -47,7 +46,7 @@ async def async_setup_entry( ) -def _build_forecast_data(timestep, use_3hourly): +def _build_forecast_data(timestep): data = {} data[ATTR_FORECAST_TIME] = timestep.date.isoformat() if timestep.weather: @@ -60,9 +59,6 @@ def _build_forecast_data(timestep, use_3hourly): data[ATTR_FORECAST_WIND_BEARING] = timestep.wind_direction.value if timestep.wind_speed: data[ATTR_FORECAST_WIND_SPEED] = timestep.wind_speed.value - if not use_3hourly: - # if it's close to noon, mark as Day, otherwise as Night - data[ATTR_FORECAST_DAYTIME] = abs(timestep.date.hour - 12) < 6 return data @@ -86,7 +82,6 @@ class MetOfficeWeather(CoordinatorEntity, WeatherEntity): ) self._attr_name = f"{DEFAULT_NAME} {hass_data[METOFFICE_NAME]} {mode_label}" self._attr_unique_id = hass_data[METOFFICE_COORDINATES] - self._use_3hourly = use_3hourly if not use_3hourly: self._attr_unique_id = f"{self._attr_unique_id}_{MODE_DAILY}" @@ -160,7 +155,7 @@ class MetOfficeWeather(CoordinatorEntity, WeatherEntity): if self.coordinator.data.forecast is None: return None return [ - _build_forecast_data(timestep, self._use_3hourly) + _build_forecast_data(timestep) for timestep in self.coordinator.data.forecast ] diff --git a/tests/components/metoffice/test_weather.py b/tests/components/metoffice/test_weather.py index 1970217db5b..158e44ca15b 100644 --- a/tests/components/metoffice/test_weather.py +++ b/tests/components/metoffice/test_weather.py @@ -181,13 +181,6 @@ async def test_one_weather_site_running(hass, requests_mock, legacy_patchable_ti assert weather.attributes.get("forecast")[7]["temperature"] == 13 assert weather.attributes.get("forecast")[7]["wind_speed"] == 13 assert weather.attributes.get("forecast")[7]["wind_bearing"] == "SE" - assert weather.attributes.get("forecast")[7]["daytime"] is True - - # Check that night entry is correctly marked as Night - assert ( - weather.attributes.get("forecast")[6]["datetime"] == "2020-04-29T00:00:00+00:00" - ) - assert weather.attributes.get("forecast")[6]["daytime"] is False @patch( @@ -263,7 +256,6 @@ async def test_two_weather_sites_running(hass, requests_mock, legacy_patchable_t assert weather.attributes.get("forecast")[18]["temperature"] == 9 assert weather.attributes.get("forecast")[18]["wind_speed"] == 4 assert weather.attributes.get("forecast")[18]["wind_bearing"] == "NW" - assert "daytime" not in weather.attributes.get("forecast")[18] # Wavertree daily weather platform expected results weather = hass.states.get("weather.met_office_wavertree_daily") @@ -287,7 +279,6 @@ async def test_two_weather_sites_running(hass, requests_mock, legacy_patchable_t assert weather.attributes.get("forecast")[7]["temperature"] == 13 assert weather.attributes.get("forecast")[7]["wind_speed"] == 13 assert weather.attributes.get("forecast")[7]["wind_bearing"] == "SE" - assert weather.attributes.get("forecast")[7]["daytime"] is True # King's Lynn 3-hourly weather platform expected results weather = hass.states.get("weather.met_office_king_s_lynn_3_hourly") @@ -312,7 +303,6 @@ async def test_two_weather_sites_running(hass, requests_mock, legacy_patchable_t assert weather.attributes.get("forecast")[18]["temperature"] == 10 assert weather.attributes.get("forecast")[18]["wind_speed"] == 7 assert weather.attributes.get("forecast")[18]["wind_bearing"] == "SE" - assert "daytime" not in weather.attributes.get("forecast")[18] # King's Lynn daily weather platform expected results weather = hass.states.get("weather.met_office_king_s_lynn_daily") @@ -336,4 +326,3 @@ async def test_two_weather_sites_running(hass, requests_mock, legacy_patchable_t assert weather.attributes.get("forecast")[5]["temperature"] == 11 assert weather.attributes.get("forecast")[5]["wind_speed"] == 7 assert weather.attributes.get("forecast")[5]["wind_bearing"] == "ESE" - assert weather.attributes.get("forecast")[5]["daytime"] is True From ed6352a22b6421cc61791f729b270d930cf9faea Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 4 Dec 2021 12:33:34 +0100 Subject: [PATCH 0020/2644] Fix Xiaomi Miio providing strings as timestamps (#60979) --- .../components/xiaomi_miio/sensor.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 0d67014ced9..ccf55a04e17 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -49,6 +49,7 @@ from homeassistant.const import ( VOLUME_CUBIC_METERS, ) from homeassistant.core import callback +from homeassistant.util import dt as dt_util from . import VacuumCoordinatorDataAttributes from .const import ( @@ -689,14 +690,24 @@ class XiaomiGenericSensor(XiaomiCoordinatedMiioEntity, SensorEntity): def _determine_native_value(self): """Determine native value.""" if self.entity_description.parent_key is not None: - return self._extract_value_from_attribute( + native_value = self._extract_value_from_attribute( getattr(self.coordinator.data, self.entity_description.parent_key), self.entity_description.key, ) + else: + native_value = self._extract_value_from_attribute( + self.coordinator.data, self.entity_description.key + ) - return self._extract_value_from_attribute( - self.coordinator.data, self.entity_description.key - ) + if ( + self.device_class == DEVICE_CLASS_TIMESTAMP + and native_value is not None + and (native_datetime := dt_util.parse_datetime(str(native_value))) + is not None + ): + return native_datetime.astimezone(dt_util.UTC) + + return native_value class XiaomiAirQualityMonitor(XiaomiMiioEntity, SensorEntity): From 79cd281c482965734e36d8d299111249ba8347e2 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 4 Dec 2021 13:19:49 +0100 Subject: [PATCH 0021/2644] Use platform enum (5) [P-R] (#60943) --- homeassistant/components/p1_monitor/__init__.py | 5 ++--- homeassistant/components/philips_js/__init__.py | 3 ++- homeassistant/components/pi_hole/__init__.py | 7 ++++--- homeassistant/components/picnic/__init__.py | 4 ++-- homeassistant/components/plaato/const.py | 4 +++- homeassistant/components/plugwise/const.py | 10 ++++++++-- homeassistant/components/poolsense/__init__.py | 4 ++-- homeassistant/components/powerwall/__init__.py | 4 ++-- homeassistant/components/progettihwsw/__init__.py | 3 ++- homeassistant/components/prosegur/__init__.py | 4 ++-- homeassistant/components/ps4/__init__.py | 3 ++- homeassistant/components/pvpc_hourly_pricing/const.py | 4 +++- homeassistant/components/rachio/__init__.py | 4 ++-- homeassistant/components/rainforest_eagle/__init__.py | 3 ++- homeassistant/components/rainmachine/__init__.py | 3 ++- homeassistant/components/rdw/__init__.py | 5 ++--- homeassistant/components/recollect_waste/__init__.py | 3 ++- homeassistant/components/rfxtrx/__init__.py | 9 ++++++++- homeassistant/components/ridwell/__init__.py | 4 ++-- homeassistant/components/ring/__init__.py | 10 ++++++++-- .../components/rituals_perfume_genie/__init__.py | 9 ++++++++- homeassistant/components/roku/__init__.py | 6 ++---- homeassistant/components/roomba/const.py | 4 +++- homeassistant/components/rpi_power/__init__.py | 3 ++- homeassistant/components/ruckus_unleashed/const.py | 4 +++- 25 files changed, 80 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/p1_monitor/__init__.py b/homeassistant/components/p1_monitor/__init__.py index 5d649fa6d63..d87c521332e 100644 --- a/homeassistant/components/p1_monitor/__init__.py +++ b/homeassistant/components/p1_monitor/__init__.py @@ -5,9 +5,8 @@ from typing import TypedDict from p1monitor import P1Monitor, Phases, Settings, SmartMeter -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -22,7 +21,7 @@ from .const import ( SERVICE_SMARTMETER, ) -PLATFORMS = (SENSOR_DOMAIN,) +PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index 79999b95d12..fe3e46c70f1 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -16,6 +16,7 @@ from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_USERNAME, + Platform, ) from homeassistant.core import CALLBACK_TYPE, Context, HassJob, HomeAssistant, callback from homeassistant.helpers.debounce import Debouncer @@ -23,7 +24,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import CONF_ALLOW_NOTIFY, DOMAIN -PLATFORMS = ["media_player", "light", "remote"] +PLATFORMS = [Platform.MEDIA_PLAYER, Platform.LIGHT, Platform.REMOTE] LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index 930ded4aa42..3e18953af84 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -14,6 +14,7 @@ from homeassistant.const import ( CONF_NAME, CONF_SSL, CONF_VERIFY_SSL, + Platform, ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady @@ -150,11 +151,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @callback -def _async_platforms(entry: ConfigEntry) -> list[str]: +def _async_platforms(entry: ConfigEntry) -> list[Platform]: """Return platforms to be loaded / unloaded.""" - platforms = ["binary_sensor", "sensor"] + platforms = [Platform.BINARY_SENSOR, Platform.SENSOR] if not entry.data[CONF_STATISTICS_ONLY]: - platforms.append("switch") + platforms.append(Platform.SWITCH) return platforms diff --git a/homeassistant/components/picnic/__init__.py b/homeassistant/components/picnic/__init__.py index ce6a02a81b5..e34223a4799 100644 --- a/homeassistant/components/picnic/__init__.py +++ b/homeassistant/components/picnic/__init__.py @@ -3,13 +3,13 @@ from python_picnic_api import PicnicAPI from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.const import CONF_ACCESS_TOKEN, Platform from homeassistant.core import HomeAssistant from .const import CONF_API, CONF_COORDINATOR, CONF_COUNTRY_CODE, DOMAIN from .coordinator import PicnicUpdateCoordinator -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] def create_picnic_client(entry: ConfigEntry): diff --git a/homeassistant/components/plaato/const.py b/homeassistant/components/plaato/const.py index 2d8cf40c91e..c47b91a4adb 100644 --- a/homeassistant/components/plaato/const.py +++ b/homeassistant/components/plaato/const.py @@ -1,6 +1,8 @@ """Const for Plaato.""" from datetime import timedelta +from homeassistant.const import Platform + DOMAIN = "plaato" PLAATO_DEVICE_SENSORS = "sensors" PLAATO_DEVICE_ATTRS = "attrs" @@ -15,7 +17,7 @@ PLACEHOLDER_DOCS_URL = "docs_url" PLACEHOLDER_DEVICE_TYPE = "device_type" PLACEHOLDER_DEVICE_NAME = "device_name" DOCS_URL = "https://www.home-assistant.io/integrations/plaato/" -PLATFORMS = ["sensor", "binary_sensor"] +PLATFORMS = [Platform.SENSOR, Platform.BINARY_SENSOR] SENSOR_DATA = "sensor_data" COORDINATOR = "coordinator" DEVICE = "device" diff --git a/homeassistant/components/plugwise/const.py b/homeassistant/components/plugwise/const.py index 2c1867346c4..9c6823e22e4 100644 --- a/homeassistant/components/plugwise/const.py +++ b/homeassistant/components/plugwise/const.py @@ -1,4 +1,5 @@ """Constants for Plugwise component.""" +from homeassistant.const import Platform API = "api" ATTR_ILLUMINANCE = "illuminance" @@ -20,8 +21,13 @@ STRETCH_USERNAME = "stretch" UNDO_UPDATE_LISTENER = "undo_update_listener" UNIT_LUMEN = "lm" -PLATFORMS_GATEWAY = ["binary_sensor", "climate", "sensor", "switch"] -SENSOR_PLATFORMS = ["sensor", "switch"] +PLATFORMS_GATEWAY = [ + Platform.BINARY_SENSOR, + Platform.CLIMATE, + Platform.SENSOR, + Platform.SWITCH, +] +SENSOR_PLATFORMS = [Platform.SENSOR, Platform.SWITCH] ZEROCONF_MAP = { "smile": "P1", "smile_thermo": "Anna", diff --git a/homeassistant/components/poolsense/__init__.py b/homeassistant/components/poolsense/__init__.py index 7ca7751b6f0..31362e41668 100644 --- a/homeassistant/components/poolsense/__init__.py +++ b/homeassistant/components/poolsense/__init__.py @@ -7,7 +7,7 @@ from poolsense import PoolSense from poolsense.exceptions import PoolSenseError from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION, CONF_EMAIL, CONF_PASSWORD +from homeassistant.const import ATTR_ATTRIBUTION, CONF_EMAIL, CONF_PASSWORD, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client from homeassistant.helpers.entity import EntityDescription @@ -19,7 +19,7 @@ from homeassistant.helpers.update_coordinator import ( from .const import ATTRIBUTION, DOMAIN -PLATFORMS = ["sensor", "binary_sensor"] +PLATFORMS = [Platform.SENSOR, Platform.BINARY_SENSOR] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index 8b06b2a9c6d..fa7b33ff2ad 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -11,7 +11,7 @@ from tesla_powerwall import ( ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD +from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import entity_registry @@ -38,7 +38,7 @@ from .const import ( CONFIG_SCHEMA = cv.deprecated(DOMAIN) -PLATFORMS = ["binary_sensor", "sensor"] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/progettihwsw/__init__.py b/homeassistant/components/progettihwsw/__init__.py index e3555079a4d..1ebabd5bb08 100644 --- a/homeassistant/components/progettihwsw/__init__.py +++ b/homeassistant/components/progettihwsw/__init__.py @@ -5,11 +5,12 @@ from ProgettiHWSW.input import Input from ProgettiHWSW.relay import Relay from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from .const import DOMAIN -PLATFORMS = ["switch", "binary_sensor"] +PLATFORMS = [Platform.SWITCH, Platform.BINARY_SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/prosegur/__init__.py b/homeassistant/components/prosegur/__init__.py index 3e31a1142ce..b8023c7fadd 100644 --- a/homeassistant/components/prosegur/__init__.py +++ b/homeassistant/components/prosegur/__init__.py @@ -4,14 +4,14 @@ import logging from pyprosegur.auth import Auth from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client from .const import CONF_COUNTRY, DOMAIN -PLATFORMS = ["alarm_control_panel"] +PLATFORMS = [Platform.ALARM_CONTROL_PANEL] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/ps4/__init__.py b/homeassistant/components/ps4/__init__.py index bf5eafa7bbb..caab9e1dd26 100644 --- a/homeassistant/components/ps4/__init__.py +++ b/homeassistant/components/ps4/__init__.py @@ -17,6 +17,7 @@ from homeassistant.const import ( ATTR_LOCKED, CONF_REGION, CONF_TOKEN, + Platform, ) from homeassistant.core import HomeAssistant, split_entity_id from homeassistant.exceptions import HomeAssistantError @@ -45,7 +46,7 @@ PS4_COMMAND_SCHEMA = vol.Schema( } ) -PLATFORMS = ["media_player"] +PLATFORMS = [Platform.MEDIA_PLAYER] class PS4Data: diff --git a/homeassistant/components/pvpc_hourly_pricing/const.py b/homeassistant/components/pvpc_hourly_pricing/const.py index ad97124c330..186ee1171f3 100644 --- a/homeassistant/components/pvpc_hourly_pricing/const.py +++ b/homeassistant/components/pvpc_hourly_pricing/const.py @@ -1,6 +1,8 @@ """Constant values for pvpc_hourly_pricing.""" +from homeassistant.const import Platform + DOMAIN = "pvpc_hourly_pricing" -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] ATTR_POWER = "power" ATTR_POWER_P3 = "power_p3" ATTR_TARIFF = "tariff" diff --git a/homeassistant/components/rachio/__init__.py b/homeassistant/components/rachio/__init__.py index 2d1caf23c42..134a10795f2 100644 --- a/homeassistant/components/rachio/__init__.py +++ b/homeassistant/components/rachio/__init__.py @@ -6,7 +6,7 @@ from rachiopy import Rachio from requests.exceptions import ConnectTimeout from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY +from homeassistant.const import CONF_API_KEY, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv @@ -20,7 +20,7 @@ from .webhooks import ( _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["switch", "binary_sensor"] +PLATFORMS = [Platform.SWITCH, Platform.BINARY_SENSOR] CONFIG_SCHEMA = cv.deprecated(DOMAIN) diff --git a/homeassistant/components/rainforest_eagle/__init__.py b/homeassistant/components/rainforest_eagle/__init__.py index 44a5624267e..862c9850cb9 100644 --- a/homeassistant/components/rainforest_eagle/__init__.py +++ b/homeassistant/components/rainforest_eagle/__init__.py @@ -2,12 +2,13 @@ from __future__ import annotations from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from . import data from .const import DOMAIN -PLATFORMS = ("sensor",) +PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 9f7f014f13d..0e0dd726f75 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -18,6 +18,7 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_PORT, CONF_SSL, + Platform, ) from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ConfigEntryNotReady @@ -56,7 +57,7 @@ DEFAULT_UPDATE_INTERVAL = timedelta(seconds=15) CONFIG_SCHEMA = cv.deprecated(DOMAIN) -PLATFORMS = ["binary_sensor", "sensor", "switch"] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH] UPDATE_INTERVALS = { DATA_PROVISION_SETTINGS: timedelta(minutes=1), diff --git a/homeassistant/components/rdw/__init__.py b/homeassistant/components/rdw/__init__.py index bde83fffad3..ac2bed06310 100644 --- a/homeassistant/components/rdw/__init__.py +++ b/homeassistant/components/rdw/__init__.py @@ -3,16 +3,15 @@ from __future__ import annotations from vehicle import RDW, Vehicle -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import CONF_LICENSE_PLATE, DOMAIN, LOGGER, SCAN_INTERVAL -PLATFORMS = (BINARY_SENSOR_DOMAIN, SENSOR_DOMAIN) +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/recollect_waste/__init__.py b/homeassistant/components/recollect_waste/__init__.py index 25900345205..23ebf8aefbc 100644 --- a/homeassistant/components/recollect_waste/__init__.py +++ b/homeassistant/components/recollect_waste/__init__.py @@ -7,6 +7,7 @@ from aiorecollect.client import Client, PickupEvent from aiorecollect.errors import RecollectError from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -16,7 +17,7 @@ from .const import CONF_PLACE_ID, CONF_SERVICE_ID, DOMAIN, LOGGER DEFAULT_NAME = "recollect_waste" DEFAULT_UPDATE_INTERVAL = timedelta(days=1) -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index d7de0f5b7be..7fc743d021f 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -18,6 +18,7 @@ from homeassistant.const import ( CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP, + Platform, ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -60,7 +61,13 @@ def _bytearray_string(data): SERVICE_SEND_SCHEMA = vol.Schema({ATTR_EVENT: _bytearray_string}) -PLATFORMS = ["switch", "sensor", "light", "binary_sensor", "cover"] +PLATFORMS = [ + Platform.SWITCH, + Platform.SENSOR, + Platform.LIGHT, + Platform.BINARY_SENSOR, + Platform.COVER, +] async def async_setup_entry(hass, entry: config_entries.ConfigEntry): diff --git a/homeassistant/components/ridwell/__init__.py b/homeassistant/components/ridwell/__init__.py index 4aa5ea3e162..85d456271df 100644 --- a/homeassistant/components/ridwell/__init__.py +++ b/homeassistant/components/ridwell/__init__.py @@ -9,7 +9,7 @@ from aioridwell.client import RidwellAccount, RidwellPickupEvent from aioridwell.errors import InvalidCredentialsError, RidwellError from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client @@ -19,7 +19,7 @@ from .const import DATA_ACCOUNT, DATA_COORDINATOR, DOMAIN, LOGGER DEFAULT_UPDATE_INTERVAL = timedelta(hours=1) -PLATFORMS: list[str] = ["sensor"] +PLATFORMS: list[Platform] = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py index a8196b30302..db383b9228f 100644 --- a/homeassistant/components/ring/__init__.py +++ b/homeassistant/components/ring/__init__.py @@ -11,7 +11,7 @@ from oauthlib.oauth2 import AccessDeniedError import requests from ring_doorbell import Auth, Ring -from homeassistant.const import __version__ +from homeassistant.const import Platform, __version__ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.event import async_track_time_interval from homeassistant.util.async_ import run_callback_threadsafe @@ -26,7 +26,13 @@ NOTIFICATION_TITLE = "Ring Setup" DOMAIN = "ring" DEFAULT_ENTITY_NAMESPACE = "ring" -PLATFORMS = ("binary_sensor", "light", "sensor", "switch", "camera") +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.LIGHT, + Platform.SENSOR, + Platform.SWITCH, + Platform.CAMERA, +] async def async_setup(hass, config): diff --git a/homeassistant/components/rituals_perfume_genie/__init__.py b/homeassistant/components/rituals_perfume_genie/__init__.py index 8a9ed5d94a3..c45a762ca9a 100644 --- a/homeassistant/components/rituals_perfume_genie/__init__.py +++ b/homeassistant/components/rituals_perfume_genie/__init__.py @@ -6,6 +6,7 @@ import aiohttp from pyrituals import Account, Diffuser from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -13,7 +14,13 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ACCOUNT_HASH, COORDINATORS, DEVICES, DOMAIN -PLATFORMS = ["binary_sensor", "number", "select", "sensor", "switch"] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.NUMBER, + Platform.SELECT, + Platform.SENSOR, + Platform.SWITCH, +] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index f1c30ec2af7..b145864b726 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -5,10 +5,8 @@ import logging from rokuecp import RokuConnectionError, RokuError -from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN -from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv @@ -17,7 +15,7 @@ from .coordinator import RokuDataUpdateCoordinator CONFIG_SCHEMA = cv.deprecated(DOMAIN) -PLATFORMS = [MEDIA_PLAYER_DOMAIN, REMOTE_DOMAIN] +PLATFORMS = [Platform.MEDIA_PLAYER, Platform.REMOTE] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/roomba/const.py b/homeassistant/components/roomba/const.py index 2e59279cfdb..ae872e0540c 100644 --- a/homeassistant/components/roomba/const.py +++ b/homeassistant/components/roomba/const.py @@ -1,6 +1,8 @@ """The roomba constants.""" +from homeassistant.const import Platform + DOMAIN = "roomba" -PLATFORMS = ["sensor", "binary_sensor", "vacuum"] +PLATFORMS = [Platform.SENSOR, Platform.BINARY_SENSOR, Platform.VACUUM] CONF_CERT = "certificate" CONF_CONTINUOUS = "continuous" CONF_BLID = "blid" diff --git a/homeassistant/components/rpi_power/__init__.py b/homeassistant/components/rpi_power/__init__.py index 320043a0260..8647ab38a78 100644 --- a/homeassistant/components/rpi_power/__init__.py +++ b/homeassistant/components/rpi_power/__init__.py @@ -1,8 +1,9 @@ """The Raspberry Pi Power Supply Checker integration.""" from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant -PLATFORMS = ["binary_sensor"] +PLATFORMS = [Platform.BINARY_SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/ruckus_unleashed/const.py b/homeassistant/components/ruckus_unleashed/const.py index 77e1029143e..e6087be3fd2 100644 --- a/homeassistant/components/ruckus_unleashed/const.py +++ b/homeassistant/components/ruckus_unleashed/const.py @@ -1,6 +1,8 @@ """Constants for the Ruckus Unleashed integration.""" +from homeassistant.const import Platform + DOMAIN = "ruckus_unleashed" -PLATFORMS = ["device_tracker"] +PLATFORMS = [Platform.DEVICE_TRACKER] SCAN_INTERVAL = 180 MANUFACTURER = "Ruckus" From cd1b923e16c93665d01bff8872465c9a746c6120 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 4 Dec 2021 13:26:40 +0100 Subject: [PATCH 0022/2644] Use platform enum (2) [E-G] (#60933) --- homeassistant/components/ecobee/const.py | 9 ++++++++- homeassistant/components/econet/__init__.py | 9 +++++++-- homeassistant/components/efergy/__init__.py | 5 ++--- homeassistant/components/elgato/__init__.py | 6 ++---- homeassistant/components/elkm1/__init__.py | 13 +++++++------ homeassistant/components/emonitor/__init__.py | 4 ++-- homeassistant/components/enphase_envoy/const.py | 9 +++++++-- .../components/environment_canada/__init__.py | 4 ++-- homeassistant/components/epson/__init__.py | 5 ++--- .../components/evil_genius_labs/__init__.py | 3 ++- homeassistant/components/ezviz/__init__.py | 9 +++++---- homeassistant/components/faa_delays/__init__.py | 4 ++-- .../components/fireservicerota/__init__.py | 7 ++----- homeassistant/components/fjaraskupan/__init__.py | 9 ++++++++- homeassistant/components/flick_electric/__init__.py | 3 ++- homeassistant/components/flipr/__init__.py | 4 ++-- homeassistant/components/flo/__init__.py | 4 ++-- homeassistant/components/flume/const.py | 3 ++- homeassistant/components/flunearyou/__init__.py | 4 ++-- homeassistant/components/flux_led/__init__.py | 11 ++++++++--- homeassistant/components/forecast_solar/__init__.py | 4 ++-- homeassistant/components/forked_daapd/__init__.py | 4 ++-- homeassistant/components/foscam/__init__.py | 10 ++++++++-- homeassistant/components/freebox/const.py | 4 ++-- homeassistant/components/fritz/const.py | 9 ++++++++- homeassistant/components/fritzbox/const.py | 10 +++++++++- .../components/fritzbox_callmonitor/const.py | 3 ++- homeassistant/components/fronius/__init__.py | 4 ++-- .../components/garages_amsterdam/__init__.py | 3 ++- homeassistant/components/gdacs/const.py | 4 +++- homeassistant/components/geofency/__init__.py | 4 ++-- homeassistant/components/geonetnz_quakes/const.py | 4 +++- homeassistant/components/geonetnz_volcano/const.py | 4 +++- homeassistant/components/gios/__init__.py | 3 ++- homeassistant/components/glances/__init__.py | 3 ++- homeassistant/components/goalzero/__init__.py | 13 ++++++++----- homeassistant/components/gogogate2/__init__.py | 6 ++---- .../components/google_travel_time/__init__.py | 3 ++- homeassistant/components/gpslogger/__init__.py | 9 +++------ homeassistant/components/gree/__init__.py | 5 ++--- homeassistant/components/growatt_server/const.py | 4 +++- homeassistant/components/guardian/__init__.py | 3 ++- 42 files changed, 150 insertions(+), 91 deletions(-) diff --git a/homeassistant/components/ecobee/const.py b/homeassistant/components/ecobee/const.py index caf25690a9d..50dd606ad25 100644 --- a/homeassistant/components/ecobee/const.py +++ b/homeassistant/components/ecobee/const.py @@ -14,6 +14,7 @@ from homeassistant.components.weather import ( ATTR_CONDITION_SUNNY, ATTR_CONDITION_WINDY, ) +from homeassistant.const import Platform _LOGGER = logging.getLogger(__package__) @@ -37,7 +38,13 @@ ECOBEE_MODEL_TO_NAME = { "vulcanSmart": "ecobee4 Smart", } -PLATFORMS = ["binary_sensor", "climate", "humidifier", "sensor", "weather"] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.CLIMATE, + Platform.HUMIDIFIER, + Platform.SENSOR, + Platform.WEATHER, +] MANUFACTURER = "ecobee" diff --git a/homeassistant/components/econet/__init__.py b/homeassistant/components/econet/__init__.py index 8e39a6a6267..c3abea82d7f 100644 --- a/homeassistant/components/econet/__init__.py +++ b/homeassistant/components/econet/__init__.py @@ -12,7 +12,7 @@ from pyeconet.errors import ( PyeconetError, ) -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, TEMP_FAHRENHEIT +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, TEMP_FAHRENHEIT, Platform from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.dispatcher import dispatcher_send @@ -23,7 +23,12 @@ from .const import API_CLIENT, DOMAIN, EQUIPMENT _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["climate", "binary_sensor", "sensor", "water_heater"] +PLATFORMS = [ + Platform.CLIMATE, + Platform.BINARY_SENSOR, + Platform.SENSOR, + Platform.WATER_HEATER, +] PUSH_UPDATE = "econet.push_update" INTERVAL = timedelta(minutes=60) diff --git a/homeassistant/components/efergy/__init__.py b/homeassistant/components/efergy/__init__.py index dd6c6001259..372dbe77e75 100644 --- a/homeassistant/components/efergy/__init__.py +++ b/homeassistant/components/efergy/__init__.py @@ -3,9 +3,8 @@ from __future__ import annotations from pyefergy import Efergy, exceptions -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY +from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr @@ -14,7 +13,7 @@ from homeassistant.helpers.entity import DeviceInfo, Entity from .const import ATTRIBUTION, DATA_KEY_API, DEFAULT_NAME, DOMAIN -PLATFORMS = [SENSOR_DOMAIN] +PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/elgato/__init__.py b/homeassistant/components/elgato/__init__.py index c074b3303e7..6ef540365be 100644 --- a/homeassistant/components/elgato/__init__.py +++ b/homeassistant/components/elgato/__init__.py @@ -3,17 +3,15 @@ import logging from elgato import Elgato, ElgatoConnectionError -from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN -from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_PORT, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN -PLATFORMS = [BUTTON_DOMAIN, LIGHT_DOMAIN] +PLATFORMS = [Platform.BUTTON, Platform.LIGHT] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 07111282c3d..8714a41a9a7 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -22,6 +22,7 @@ from homeassistant.const import ( CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT, + Platform, ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError @@ -57,12 +58,12 @@ SYNC_TIMEOUT = 120 _LOGGER = logging.getLogger(__name__) PLATFORMS = [ - "alarm_control_panel", - "climate", - "light", - "scene", - "sensor", - "switch", + Platform.ALARM_CONTROL_PANEL, + Platform.CLIMATE, + Platform.LIGHT, + Platform.SCENE, + Platform.SENSOR, + Platform.SWITCH, ] SPEAK_SERVICE_SCHEMA = vol.Schema( diff --git a/homeassistant/components/emonitor/__init__.py b/homeassistant/components/emonitor/__init__.py index 1cff3c9c30e..3d03c7b8fe6 100644 --- a/homeassistant/components/emonitor/__init__.py +++ b/homeassistant/components/emonitor/__init__.py @@ -5,7 +5,7 @@ import logging from aioemonitor import Emonitor from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -16,7 +16,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_UPDATE_RATE = 60 -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/enphase_envoy/const.py b/homeassistant/components/enphase_envoy/const.py index ea67b5b633b..7e278d83b86 100644 --- a/homeassistant/components/enphase_envoy/const.py +++ b/homeassistant/components/enphase_envoy/const.py @@ -6,11 +6,16 @@ from homeassistant.components.sensor import ( STATE_CLASS_TOTAL_INCREASING, SensorEntityDescription, ) -from homeassistant.const import DEVICE_CLASS_ENERGY, ENERGY_WATT_HOUR, POWER_WATT +from homeassistant.const import ( + DEVICE_CLASS_ENERGY, + ENERGY_WATT_HOUR, + POWER_WATT, + Platform, +) DOMAIN = "enphase_envoy" -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] COORDINATOR = "coordinator" diff --git a/homeassistant/components/environment_canada/__init__.py b/homeassistant/components/environment_canada/__init__.py index 5e52d3631f6..90227ec997d 100644 --- a/homeassistant/components/environment_canada/__init__.py +++ b/homeassistant/components/environment_canada/__init__.py @@ -6,7 +6,7 @@ import xml.etree.ElementTree as et from env_canada import ECRadar, ECWeather, ec_exc from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, Platform from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import CONF_LANGUAGE, CONF_STATION, DOMAIN @@ -14,7 +14,7 @@ from .const import CONF_LANGUAGE, CONF_STATION, DOMAIN DEFAULT_RADAR_UPDATE_INTERVAL = timedelta(minutes=5) DEFAULT_WEATHER_UPDATE_INTERVAL = timedelta(minutes=5) -PLATFORMS = ["camera", "sensor", "weather"] +PLATFORMS = [Platform.CAMERA, Platform.SENSOR, Platform.WEATHER] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/epson/__init__.py b/homeassistant/components/epson/__init__.py index 036b8df7ca9..9710cb8d96a 100644 --- a/homeassistant/components/epson/__init__.py +++ b/homeassistant/components/epson/__init__.py @@ -7,16 +7,15 @@ from epson_projector.const import ( STATE_UNAVAILABLE as EPSON_STATE_UNAVAILABLE, ) -from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_PLATFORM from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN, HTTP from .exceptions import CannotConnect, PoweredOff -PLATFORMS = [MEDIA_PLAYER_PLATFORM] +PLATFORMS = [Platform.MEDIA_PLAYER] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/evil_genius_labs/__init__.py b/homeassistant/components/evil_genius_labs/__init__.py index 78445a42e7d..2fbd85938f0 100644 --- a/homeassistant/components/evil_genius_labs/__init__.py +++ b/homeassistant/components/evil_genius_labs/__init__.py @@ -9,6 +9,7 @@ from async_timeout import timeout import pyevilgenius from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import ( aiohttp_client, @@ -19,7 +20,7 @@ from homeassistant.helpers.entity import DeviceInfo from .const import DOMAIN -PLATFORMS = ["light"] +PLATFORMS = [Platform.LIGHT] UPDATE_INTERVAL = 10 diff --git a/homeassistant/components/ezviz/__init__.py b/homeassistant/components/ezviz/__init__.py index d2d98aa04cb..9d6f7864b84 100644 --- a/homeassistant/components/ezviz/__init__.py +++ b/homeassistant/components/ezviz/__init__.py @@ -11,6 +11,7 @@ from homeassistant.const import ( CONF_TYPE, CONF_URL, CONF_USERNAME, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -30,10 +31,10 @@ from .coordinator import EzvizDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) PLATFORMS = [ - "binary_sensor", - "camera", - "sensor", - "switch", + Platform.BINARY_SENSOR, + Platform.CAMERA, + Platform.SENSOR, + Platform.SWITCH, ] diff --git a/homeassistant/components/faa_delays/__init__.py b/homeassistant/components/faa_delays/__init__.py index 205fa016130..8bfcf60f30a 100644 --- a/homeassistant/components/faa_delays/__init__.py +++ b/homeassistant/components/faa_delays/__init__.py @@ -7,7 +7,7 @@ from async_timeout import timeout from faadelays import Airport from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ID +from homeassistant.const import CONF_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -16,7 +16,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["binary_sensor"] +PLATFORMS = [Platform.BINARY_SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/fireservicerota/__init__.py b/homeassistant/components/fireservicerota/__init__.py index aa10a16f088..4cdb7ec0c42 100644 --- a/homeassistant/components/fireservicerota/__init__.py +++ b/homeassistant/components/fireservicerota/__init__.py @@ -10,11 +10,8 @@ from pyfireservicerota import ( InvalidTokenError, ) -from homeassistant.components.binary_sensor import DOMAIN as BINARYSENSOR_DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_USERNAME +from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.dispatcher import dispatcher_send @@ -26,7 +23,7 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) _LOGGER = logging.getLogger(__name__) -PLATFORMS = [SENSOR_DOMAIN, BINARYSENSOR_DOMAIN, SWITCH_DOMAIN] +PLATFORMS = [Platform.SENSOR, Platform.BINARY_SENSOR, Platform.SWITCH] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/fjaraskupan/__init__.py b/homeassistant/components/fjaraskupan/__init__.py index babcdc6649a..64962a746f7 100644 --- a/homeassistant/components/fjaraskupan/__init__.py +++ b/homeassistant/components/fjaraskupan/__init__.py @@ -12,6 +12,7 @@ from bleak.backends.scanner import AdvertisementData from fjaraskupan import UUID_SERVICE, Device, State, device_filter from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -23,7 +24,13 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DISPATCH_DETECTION, DOMAIN -PLATFORMS = ["binary_sensor", "fan", "light", "number", "sensor"] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.FAN, + Platform.LIGHT, + Platform.NUMBER, + Platform.SENSOR, +] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/flick_electric/__init__.py b/homeassistant/components/flick_electric/__init__.py index ca634310659..8fca87a6814 100644 --- a/homeassistant/components/flick_electric/__init__.py +++ b/homeassistant/components/flick_electric/__init__.py @@ -13,6 +13,7 @@ from homeassistant.const import ( CONF_CLIENT_SECRET, CONF_PASSWORD, CONF_USERNAME, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client @@ -21,7 +22,7 @@ from .const import CONF_TOKEN_EXPIRES_IN, CONF_TOKEN_EXPIRY, DOMAIN CONF_ID_TOKEN = "id_token" -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/flipr/__init__.py b/homeassistant/components/flipr/__init__.py index 9280c77f95c..6294d56d850 100644 --- a/homeassistant/components/flipr/__init__.py +++ b/homeassistant/components/flipr/__init__.py @@ -5,7 +5,7 @@ import logging from flipr_api import FliprAPIRestClient from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION, CONF_EMAIL, CONF_PASSWORD +from homeassistant.const import ATTR_ATTRIBUTION, CONF_EMAIL, CONF_PASSWORD, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import ( @@ -20,7 +20,7 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(minutes=60) -PLATFORMS = ["binary_sensor", "sensor"] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/flo/__init__.py b/homeassistant/components/flo/__init__.py index 32802ba85d3..2dcca979acc 100644 --- a/homeassistant/components/flo/__init__.py +++ b/homeassistant/components/flo/__init__.py @@ -6,7 +6,7 @@ from aioflo import async_get_api from aioflo.errors import RequestError from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -16,7 +16,7 @@ from .device import FloDeviceDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["binary_sensor", "sensor", "switch"] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/flume/const.py b/homeassistant/components/flume/const.py index 5060bd96489..95236829bd9 100644 --- a/homeassistant/components/flume/const.py +++ b/homeassistant/components/flume/const.py @@ -2,10 +2,11 @@ from __future__ import annotations from homeassistant.components.sensor import SensorEntityDescription +from homeassistant.const import Platform DOMAIN = "flume" -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] DEFAULT_NAME = "Flume Sensor" diff --git a/homeassistant/components/flunearyou/__init__.py b/homeassistant/components/flunearyou/__init__.py index beb7bec2c2f..1c98f4dceae 100644 --- a/homeassistant/components/flunearyou/__init__.py +++ b/homeassistant/components/flunearyou/__init__.py @@ -10,7 +10,7 @@ from pyflunearyou import Client from pyflunearyou.errors import FluNearYouError from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -21,7 +21,7 @@ DEFAULT_UPDATE_INTERVAL = timedelta(minutes=30) CONFIG_SCHEMA = cv.deprecated(DOMAIN) -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/flux_led/__init__.py b/homeassistant/components/flux_led/__init__.py index 7ea86af2e9d..cfb39a9f8f1 100644 --- a/homeassistant/components/flux_led/__init__.py +++ b/homeassistant/components/flux_led/__init__.py @@ -14,7 +14,12 @@ from flux_led.scanner import FluxLEDDiscovery from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STARTED +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + EVENT_HOMEASSISTANT_STARTED, + Platform, +) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr @@ -37,8 +42,8 @@ from .const import ( _LOGGER = logging.getLogger(__name__) PLATFORMS_BY_TYPE: Final = { - DeviceType.Bulb: ["light", "number"], - DeviceType.Switch: ["switch"], + DeviceType.Bulb: [Platform.LIGHT, Platform.NUMBER], + DeviceType.Switch: [Platform.SWITCH], } DISCOVERY_INTERVAL: Final = timedelta(minutes=15) REQUEST_REFRESH_DELAY: Final = 1.5 diff --git a/homeassistant/components/forecast_solar/__init__.py b/homeassistant/components/forecast_solar/__init__.py index 1fd77b9797c..760ad04af98 100644 --- a/homeassistant/components/forecast_solar/__init__.py +++ b/homeassistant/components/forecast_solar/__init__.py @@ -7,7 +7,7 @@ import logging from forecast_solar import ForecastSolar from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -20,7 +20,7 @@ from .const import ( DOMAIN, ) -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/forked_daapd/__init__.py b/homeassistant/components/forked_daapd/__init__.py index fc67d78d5ed..ea2d678aab5 100644 --- a/homeassistant/components/forked_daapd/__init__.py +++ b/homeassistant/components/forked_daapd/__init__.py @@ -1,9 +1,9 @@ """The forked_daapd component.""" -from homeassistant.components.media_player import DOMAIN as MP_DOMAIN +from homeassistant.const import Platform from .const import DOMAIN, HASS_DATA_REMOVE_LISTENERS_KEY, HASS_DATA_UPDATER_KEY -PLATFORMS = [MP_DOMAIN] +PLATFORMS = [Platform.MEDIA_PLAYER] async def async_setup_entry(hass, entry): diff --git a/homeassistant/components/foscam/__init__.py b/homeassistant/components/foscam/__init__.py index a6714094b0c..2ebea0615f7 100644 --- a/homeassistant/components/foscam/__init__.py +++ b/homeassistant/components/foscam/__init__.py @@ -3,14 +3,20 @@ from libpyfoscam import FoscamCamera from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + Platform, +) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_registry import async_migrate_entries from .config_flow import DEFAULT_RTSP_PORT from .const import CONF_RTSP_PORT, DOMAIN, LOGGER, SERVICE_PTZ, SERVICE_PTZ_PRESET -PLATFORMS = ["camera"] +PLATFORMS = [Platform.CAMERA] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/freebox/const.py b/homeassistant/components/freebox/const.py index 7183bd029ff..77f36cf44de 100644 --- a/homeassistant/components/freebox/const.py +++ b/homeassistant/components/freebox/const.py @@ -4,7 +4,7 @@ from __future__ import annotations import socket from homeassistant.components.sensor import SensorEntityDescription -from homeassistant.const import DATA_RATE_KILOBYTES_PER_SECOND, PERCENTAGE +from homeassistant.const import DATA_RATE_KILOBYTES_PER_SECOND, PERCENTAGE, Platform DOMAIN = "freebox" SERVICE_REBOOT = "reboot" @@ -17,7 +17,7 @@ APP_DESC = { } API_VERSION = "v6" -PLATFORMS = ["device_tracker", "sensor", "switch"] +PLATFORMS = [Platform.DEVICE_TRACKER, Platform.SENSOR, Platform.SWITCH] DEFAULT_DEVICE_NAME = "Unknown device" diff --git a/homeassistant/components/fritz/const.py b/homeassistant/components/fritz/const.py index d2c26d7bee8..4786ac9097c 100644 --- a/homeassistant/components/fritz/const.py +++ b/homeassistant/components/fritz/const.py @@ -2,9 +2,16 @@ from typing import Literal +from homeassistant.const import Platform + DOMAIN = "fritz" -PLATFORMS = ["binary_sensor", "device_tracker", "sensor", "switch"] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.DEVICE_TRACKER, + Platform.SENSOR, + Platform.SWITCH, +] DATA_FRITZ = "fritz_data" diff --git a/homeassistant/components/fritzbox/const.py b/homeassistant/components/fritzbox/const.py index 9d537bec617..d4827fbb289 100644 --- a/homeassistant/components/fritzbox/const.py +++ b/homeassistant/components/fritzbox/const.py @@ -4,6 +4,8 @@ from __future__ import annotations import logging from typing import Final +from homeassistant.const import Platform + ATTR_STATE_BATTERY_LOW: Final = "battery_low" ATTR_STATE_DEVICE_LOCKED: Final = "device_locked" ATTR_STATE_HOLIDAY_MODE: Final = "holiday_mode" @@ -24,4 +26,10 @@ DOMAIN: Final = "fritzbox" LOGGER: Final[logging.Logger] = logging.getLogger(__package__) -PLATFORMS: Final[list[str]] = ["binary_sensor", "climate", "light", "switch", "sensor"] +PLATFORMS: Final[list[Platform]] = [ + Platform.BINARY_SENSOR, + Platform.CLIMATE, + Platform.LIGHT, + Platform.SWITCH, + Platform.SENSOR, +] diff --git a/homeassistant/components/fritzbox_callmonitor/const.py b/homeassistant/components/fritzbox_callmonitor/const.py index ba0f8d1d973..435bfdef87e 100644 --- a/homeassistant/components/fritzbox_callmonitor/const.py +++ b/homeassistant/components/fritzbox_callmonitor/const.py @@ -1,4 +1,5 @@ """Constants for the AVM Fritz!Box call monitor integration.""" +from homeassistant.const import Platform STATE_RINGING = "ringing" STATE_DIALING = "dialing" @@ -36,6 +37,6 @@ DEFAULT_NAME = "Phone" DOMAIN = "fritzbox_callmonitor" MANUFACTURER = "AVM" -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] UNDO_UPDATE_LISTENER = "undo_update_listener" FRITZBOX_PHONEBOOK = "fritzbox_phonebook" diff --git a/homeassistant/components/fronius/__init__.py b/homeassistant/components/fronius/__init__.py index cf648d3b613..b7699ea7747 100644 --- a/homeassistant/components/fronius/__init__.py +++ b/homeassistant/components/fronius/__init__.py @@ -9,7 +9,7 @@ from typing import TypeVar from pyfronius import Fronius, FroniusError from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_MODEL, ATTR_SW_VERSION, CONF_HOST +from homeassistant.const import ATTR_MODEL, ATTR_SW_VERSION, CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr @@ -28,7 +28,7 @@ from .coordinator import ( ) _LOGGER = logging.getLogger(__name__) -PLATFORMS: list[str] = ["sensor"] +PLATFORMS: list[Platform] = [Platform.SENSOR] FroniusCoordinatorType = TypeVar("FroniusCoordinatorType", bound=FroniusCoordinatorBase) diff --git a/homeassistant/components/garages_amsterdam/__init__.py b/homeassistant/components/garages_amsterdam/__init__.py index 2077dec741f..5e7fbade8de 100644 --- a/homeassistant/components/garages_amsterdam/__init__.py +++ b/homeassistant/components/garages_amsterdam/__init__.py @@ -6,13 +6,14 @@ import async_timeout import garages_amsterdam from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN -PLATFORMS = ["binary_sensor", "sensor"] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/gdacs/const.py b/homeassistant/components/gdacs/const.py index 5d5c83f013e..551c8be5810 100644 --- a/homeassistant/components/gdacs/const.py +++ b/homeassistant/components/gdacs/const.py @@ -3,9 +3,11 @@ from datetime import timedelta from aio_georss_gdacs.consts import EVENT_TYPE_MAP +from homeassistant.const import Platform + DOMAIN = "gdacs" -PLATFORMS = ("sensor", "geo_location") +PLATFORMS = [Platform.SENSOR, Platform.GEO_LOCATION] FEED = "feed" diff --git a/homeassistant/components/geofency/__init__.py b/homeassistant/components/geofency/__init__.py index 1e8e3eb1f04..1191b72ba3f 100644 --- a/homeassistant/components/geofency/__init__.py +++ b/homeassistant/components/geofency/__init__.py @@ -4,13 +4,13 @@ from http import HTTPStatus from aiohttp import web import voluptuous as vol -from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER from homeassistant.const import ( ATTR_LATITUDE, ATTR_LONGITUDE, ATTR_NAME, CONF_WEBHOOK_ID, STATE_NOT_HOME, + Platform, ) from homeassistant.helpers import config_entry_flow import homeassistant.helpers.config_validation as cv @@ -19,7 +19,7 @@ from homeassistant.util import slugify from .const import DOMAIN -PLATFORMS = [DEVICE_TRACKER] +PLATFORMS = [Platform.DEVICE_TRACKER] CONF_MOBILE_BEACONS = "mobile_beacons" diff --git a/homeassistant/components/geonetnz_quakes/const.py b/homeassistant/components/geonetnz_quakes/const.py index 43818b55f6f..f3303d551ce 100644 --- a/homeassistant/components/geonetnz_quakes/const.py +++ b/homeassistant/components/geonetnz_quakes/const.py @@ -1,9 +1,11 @@ """Define constants for the GeoNet NZ Quakes integration.""" from datetime import timedelta +from homeassistant.const import Platform + DOMAIN = "geonetnz_quakes" -PLATFORMS = ("sensor", "geo_location") +PLATFORMS = [Platform.SENSOR, Platform.GEO_LOCATION] CONF_MINIMUM_MAGNITUDE = "minimum_magnitude" CONF_MMI = "mmi" diff --git a/homeassistant/components/geonetnz_volcano/const.py b/homeassistant/components/geonetnz_volcano/const.py index b70d224a685..3a23084aa1f 100644 --- a/homeassistant/components/geonetnz_volcano/const.py +++ b/homeassistant/components/geonetnz_volcano/const.py @@ -1,6 +1,8 @@ """Define constants for the GeoNet NZ Volcano integration.""" from datetime import timedelta +from homeassistant.const import Platform + DOMAIN = "geonetnz_volcano" FEED = "feed" @@ -15,4 +17,4 @@ DEFAULT_ICON = "mdi:image-filter-hdr" DEFAULT_RADIUS = 50.0 DEFAULT_SCAN_INTERVAL = timedelta(minutes=5) -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] diff --git a/homeassistant/components/gios/__init__.py b/homeassistant/components/gios/__init__.py index 8457f62fd3f..8348fa6567b 100644 --- a/homeassistant/components/gios/__init__.py +++ b/homeassistant/components/gios/__init__.py @@ -11,6 +11,7 @@ from gios import ApiError, Gios, InvalidSensorsData, NoStationError from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -21,7 +22,7 @@ from .const import API_TIMEOUT, CONF_STATION_ID, DOMAIN, SCAN_INTERVAL _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/glances/__init__.py b/homeassistant/components/glances/__init__.py index 464b320dac0..a2f662c4999 100644 --- a/homeassistant/components/glances/__init__.py +++ b/homeassistant/components/glances/__init__.py @@ -15,6 +15,7 @@ from homeassistant.const import ( CONF_SSL, CONF_USERNAME, CONF_VERIFY_SSL, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -37,7 +38,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] GLANCES_SCHEMA = vol.All( vol.Schema( diff --git a/homeassistant/components/goalzero/__init__.py b/homeassistant/components/goalzero/__init__.py index e03aa25f8f9..6e527adb485 100644 --- a/homeassistant/components/goalzero/__init__.py +++ b/homeassistant/components/goalzero/__init__.py @@ -5,11 +5,14 @@ import logging from goalzero import Yeti, exceptions -from homeassistant.components.binary_sensor import DOMAIN as DOMAIN_BINARY_SENSOR -from homeassistant.components.sensor import DOMAIN as DOMAIN_SENSOR -from homeassistant.components.switch import DOMAIN as DOMAIN_SWITCH from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION, ATTR_MODEL, CONF_HOST, CONF_NAME +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_MODEL, + CONF_HOST, + CONF_NAME, + Platform, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr @@ -33,7 +36,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -PLATFORMS = [DOMAIN_BINARY_SENSOR, DOMAIN_SENSOR, DOMAIN_SWITCH] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/gogogate2/__init__.py b/homeassistant/components/gogogate2/__init__.py index f4ff18b0837..7dccd5551c7 100644 --- a/homeassistant/components/gogogate2/__init__.py +++ b/homeassistant/components/gogogate2/__init__.py @@ -1,15 +1,13 @@ """The gogogate2 component.""" -from homeassistant.components.cover import DOMAIN as COVER -from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_DEVICE +from homeassistant.const import CONF_DEVICE, Platform from homeassistant.core import HomeAssistant from .common import get_data_update_coordinator from .const import DEVICE_TYPE_GOGOGATE2 -PLATFORMS = [COVER, SENSOR] +PLATFORMS = [Platform.COVER, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/google_travel_time/__init__.py b/homeassistant/components/google_travel_time/__init__.py index 7ac88b84727..1bc1c285bee 100644 --- a/homeassistant/components/google_travel_time/__init__.py +++ b/homeassistant/components/google_travel_time/__init__.py @@ -2,13 +2,14 @@ import logging from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_registry import ( async_entries_for_config_entry, async_get, ) -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/gpslogger/__init__.py b/homeassistant/components/gpslogger/__init__.py index 715119448b5..ebbac36659f 100644 --- a/homeassistant/components/gpslogger/__init__.py +++ b/homeassistant/components/gpslogger/__init__.py @@ -4,11 +4,8 @@ from http import HTTPStatus from aiohttp import web import voluptuous as vol -from homeassistant.components.device_tracker import ( - ATTR_BATTERY, - DOMAIN as DEVICE_TRACKER, -) -from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, CONF_WEBHOOK_ID +from homeassistant.components.device_tracker import ATTR_BATTERY +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, CONF_WEBHOOK_ID, Platform from homeassistant.helpers import config_entry_flow import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -24,7 +21,7 @@ from .const import ( DOMAIN, ) -PLATFORMS = [DEVICE_TRACKER] +PLATFORMS = [Platform.DEVICE_TRACKER] TRACKER_UPDATE = f"{DOMAIN}_tracker_update" diff --git a/homeassistant/components/gree/__init__.py b/homeassistant/components/gree/__init__.py index 761c8e0ab78..fa51a48bb4f 100644 --- a/homeassistant/components/gree/__init__.py +++ b/homeassistant/components/gree/__init__.py @@ -2,9 +2,8 @@ from datetime import timedelta import logging -from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.event import async_track_time_interval @@ -20,7 +19,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -PLATFORMS = [CLIMATE_DOMAIN, SWITCH_DOMAIN] +PLATFORMS = [Platform.CLIMATE, Platform.SWITCH] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/growatt_server/const.py b/homeassistant/components/growatt_server/const.py index 5425e26c806..4fcc4887843 100644 --- a/homeassistant/components/growatt_server/const.py +++ b/homeassistant/components/growatt_server/const.py @@ -1,4 +1,6 @@ """Define constants for the Growatt Server component.""" +from homeassistant.const import Platform + CONF_PLANT_ID = "plant_id" DEFAULT_PLANT_ID = "0" @@ -15,6 +17,6 @@ DEFAULT_URL = SERVER_URLS[0] DOMAIN = "growatt_server" -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] LOGIN_INVALID_AUTH_CODE = "502" diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index 892080b9afe..7f0834faff5 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -16,6 +16,7 @@ from homeassistant.const import ( CONF_IP_ADDRESS, CONF_PORT, CONF_URL, + Platform, ) from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import config_validation as cv, device_registry as dr @@ -80,7 +81,7 @@ SERVICE_UPGRADE_FIRMWARE_SCHEMA = vol.Schema( ) -PLATFORMS = ["binary_sensor", "sensor", "switch"] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH] @callback From 216ecf3426adb551b2eb85d4560754c69e4db930 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 4 Dec 2021 13:31:34 +0100 Subject: [PATCH 0023/2644] Fix DSMR Reader providing strings as timestamps (#60988) --- homeassistant/components/dsmr_reader/definitions.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/dsmr_reader/definitions.py b/homeassistant/components/dsmr_reader/definitions.py index 1c719bc890b..4645aef9a7a 100644 --- a/homeassistant/components/dsmr_reader/definitions.py +++ b/homeassistant/components/dsmr_reader/definitions.py @@ -24,6 +24,7 @@ from homeassistant.const import ( POWER_KILO_WATT, VOLUME_CUBIC_METERS, ) +from homeassistant.util import dt as dt_util PRICE_EUR_KWH: Final = f"EUR/{ENERGY_KILO_WATT_HOUR}" PRICE_EUR_M3: Final = f"EUR/{VOLUME_CUBIC_METERS}" @@ -202,6 +203,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( name="Telegram timestamp", entity_registry_enabled_default=False, device_class=DEVICE_CLASS_TIMESTAMP, + state=dt_util.parse_datetime, ), DSMRReaderSensorEntityDescription( key="dsmr/consumption/gas/delivered", @@ -222,6 +224,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( name="Gas meter read", entity_registry_enabled_default=False, device_class=DEVICE_CLASS_TIMESTAMP, + state=dt_util.parse_datetime, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity1", From ffb4b4df96125e73f0744a2928faa09e0afc7764 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 4 Dec 2021 13:37:42 +0100 Subject: [PATCH 0024/2644] Only report deprecated device_state_attributes once (#60980) --- homeassistant/helpers/entity.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 86c8fa86af5..cd04f9db184 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -241,6 +241,9 @@ class Entity(ABC): # If we reported this entity is updated while disabled _disabled_reported = False + # If we reported this entity is using deprecated device_state_attributes + _deprecated_device_state_attributes_reported = False + # Protect for multiple updates _update_staged = False @@ -538,7 +541,10 @@ class Entity(ABC): extra_state_attributes = self.extra_state_attributes # Backwards compatibility for "device_state_attributes" deprecated in 2021.4 # Warning added in 2021.12, will be removed in 2022.4 - if self.device_state_attributes is not None: + if ( + self.device_state_attributes is not None + and not self._deprecated_device_state_attributes_reported + ): report_issue = self._suggest_report_issue() _LOGGER.warning( "Entity %s (%s) implements device_state_attributes. Please %s", @@ -546,6 +552,7 @@ class Entity(ABC): type(self), report_issue, ) + self._deprecated_device_state_attributes_reported = True if extra_state_attributes is None: extra_state_attributes = self.device_state_attributes attr.update(extra_state_attributes or {}) From f7193400d4a93b65df268c36df773b8798e2c948 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 4 Dec 2021 13:43:48 +0100 Subject: [PATCH 0025/2644] Use platform enum (6) [S] (#60944) --- .../components/samsungtv/__init__.py | 4 +-- .../components/screenlogic/__init__.py | 11 ++++++-- homeassistant/components/sense/__init__.py | 3 ++- homeassistant/components/sharkiq/const.py | 4 ++- homeassistant/components/shelly/__init__.py | 27 +++++++++++++------ homeassistant/components/sia/const.py | 7 ++--- .../components/simplisafe/__init__.py | 13 ++++----- homeassistant/components/sma/const.py | 3 ++- homeassistant/components/smappee/const.py | 4 ++- .../components/smart_meter_texas/__init__.py | 4 +-- homeassistant/components/smarthab/__init__.py | 4 +-- homeassistant/components/smartthings/const.py | 20 +++++++------- homeassistant/components/smarttub/__init__.py | 10 ++++++- homeassistant/components/smhi/__init__.py | 3 ++- homeassistant/components/sms/__init__.py | 4 +-- .../components/solaredge/__init__.py | 4 +-- homeassistant/components/solarlog/__init__.py | 4 +-- homeassistant/components/soma/__init__.py | 4 +-- homeassistant/components/somfy/__init__.py | 14 ++++++++-- .../components/somfy_mylink/const.py | 3 ++- homeassistant/components/sonarr/__init__.py | 3 ++- homeassistant/components/songpal/__init__.py | 4 +-- .../components/speedtestdotnet/const.py | 8 ++++-- homeassistant/components/spider/const.py | 3 ++- homeassistant/components/spotify/__init__.py | 10 ++++--- .../components/squeezebox/__init__.py | 4 +-- .../components/srp_energy/__init__.py | 4 +-- homeassistant/components/starline/const.py | 10 ++++++- .../components/stookalert/__init__.py | 4 +-- homeassistant/components/subaru/const.py | 3 ++- .../components/surepetcare/__init__.py | 3 ++- .../components/switchbot/__init__.py | 6 ++--- .../components/syncthing/__init__.py | 3 ++- homeassistant/components/syncthru/__init__.py | 6 ++--- .../components/synology_dsm/const.py | 3 ++- .../components/system_bridge/__init__.py | 3 ++- 36 files changed, 146 insertions(+), 81 deletions(-) diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index f55dc0639ba..4cbd661f113 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -9,7 +9,6 @@ import getmac import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.media_player.const import DOMAIN as MP_DOMAIN from homeassistant.config_entries import ConfigEntry, ConfigEntryNotReady from homeassistant.const import ( CONF_HOST, @@ -19,6 +18,7 @@ from homeassistant.const import ( CONF_PORT, CONF_TOKEN, EVENT_HOMEASSISTANT_STOP, + Platform, ) from homeassistant.core import Event, HomeAssistant, callback import homeassistant.helpers.config_validation as cv @@ -50,7 +50,7 @@ def ensure_unique_hosts(value: dict[Any, Any]) -> dict[Any, Any]: return value -PLATFORMS = [MP_DOMAIN] +PLATFORMS = [Platform.MEDIA_PLAYER] CONFIG_SCHEMA = vol.Schema( { diff --git a/homeassistant/components/screenlogic/__init__.py b/homeassistant/components/screenlogic/__init__.py index 72c1f162552..e88475ea5bb 100644 --- a/homeassistant/components/screenlogic/__init__.py +++ b/homeassistant/components/screenlogic/__init__.py @@ -14,7 +14,7 @@ from screenlogicpy.const import ( ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, CONF_SCAN_INTERVAL +from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, CONF_SCAN_INTERVAL, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr @@ -40,7 +40,14 @@ HEATER_COOLDOWN_DELAY = 6 # These seem to be constant across all controller models PRIMARY_CIRCUIT_IDS = [500, 505] # [Spa, Pool] -PLATFORMS = ["binary_sensor", "climate", "light", "number", "sensor", "switch"] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.CLIMATE, + Platform.LIGHT, + Platform.NUMBER, + Platform.SENSOR, + Platform.SWITCH, +] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/sense/__init__.py b/homeassistant/components/sense/__init__.py index 92a4e29108c..4f5153c3d1e 100644 --- a/homeassistant/components/sense/__init__.py +++ b/homeassistant/components/sense/__init__.py @@ -11,6 +11,7 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_TIMEOUT, EVENT_HOMEASSISTANT_STOP, + Platform, ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady @@ -33,7 +34,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["binary_sensor", "sensor"] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] class SenseDevicesData: diff --git a/homeassistant/components/sharkiq/const.py b/homeassistant/components/sharkiq/const.py index 8f4c56b65db..166e165bd70 100644 --- a/homeassistant/components/sharkiq/const.py +++ b/homeassistant/components/sharkiq/const.py @@ -2,10 +2,12 @@ from datetime import timedelta import logging +from homeassistant.const import Platform + _LOGGER = logging.getLogger(__package__) API_TIMEOUT = 20 -PLATFORMS = ["vacuum"] +PLATFORMS = [Platform.VACUUM] DOMAIN = "sharkiq" SHARK = "Shark" UPDATE_INTERVAL = timedelta(seconds=30) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 27f25211a96..8d78c148b51 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -19,6 +19,7 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, + Platform, ) from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady @@ -66,15 +67,25 @@ from .utils import ( ) BLOCK_PLATFORMS: Final = [ - "binary_sensor", - "button", - "cover", - "light", - "sensor", - "switch", + Platform.BINARY_SENSOR, + Platform.BUTTON, + Platform.COVER, + Platform.LIGHT, + Platform.SENSOR, + Platform.SWITCH, +] +BLOCK_SLEEPING_PLATFORMS: Final = [ + Platform.BINARY_SENSOR, + Platform.CLIMATE, + Platform.SENSOR, +] +RPC_PLATFORMS: Final = [ + Platform.BINARY_SENSOR, + Platform.BUTTON, + Platform.LIGHT, + Platform.SENSOR, + Platform.SWITCH, ] -BLOCK_SLEEPING_PLATFORMS: Final = ["binary_sensor", "climate", "sensor"] -RPC_PLATFORMS: Final = ["binary_sensor", "button", "light", "sensor", "switch"] _LOGGER: Final = logging.getLogger(__name__) COAP_SCHEMA: Final = vol.Schema( diff --git a/homeassistant/components/sia/const.py b/homeassistant/components/sia/const.py index 711c070b1ee..183b3422f78 100644 --- a/homeassistant/components/sia/const.py +++ b/homeassistant/components/sia/const.py @@ -1,10 +1,7 @@ """Constants for the sia integration.""" -from homeassistant.components.alarm_control_panel import ( - DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, -) -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.const import Platform -PLATFORMS = [ALARM_CONTROL_PANEL_DOMAIN, BINARY_SENSOR_DOMAIN] +PLATFORMS = [Platform.ALARM_CONTROL_PANEL, Platform.BINARY_SENSOR] DOMAIN = "sia" diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 250b3e6c3b1..925f51cfe87 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -51,6 +51,7 @@ from homeassistant.const import ( CONF_CODE, CONF_TOKEN, EVENT_HOMEASSISTANT_STOP, + Platform, ) from homeassistant.core import CoreState, Event, HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady @@ -118,12 +119,12 @@ DISPATCHER_TOPIC_WEBSOCKET_EVENT = "simplisafe_websocket_event_{0}" EVENT_SIMPLISAFE_EVENT = "SIMPLISAFE_EVENT" EVENT_SIMPLISAFE_NOTIFICATION = "SIMPLISAFE_NOTIFICATION" -PLATFORMS = ( - "alarm_control_panel", - "binary_sensor", - "lock", - "sensor", -) +PLATFORMS = [ + Platform.ALARM_CONTROL_PANEL, + Platform.BINARY_SENSOR, + Platform.LOCK, + Platform.SENSOR, +] VOLUME_MAP = { "high": Volume.HIGH, diff --git a/homeassistant/components/sma/const.py b/homeassistant/components/sma/const.py index 91173d95493..1cef8dd72d2 100644 --- a/homeassistant/components/sma/const.py +++ b/homeassistant/components/sma/const.py @@ -1,4 +1,5 @@ """Constants for the sma integration.""" +from homeassistant.const import Platform DOMAIN = "sma" @@ -8,7 +9,7 @@ PYSMA_REMOVE_LISTENER = "remove_listener" PYSMA_SENSORS = "pysma_sensors" PYSMA_DEVICE_INFO = "device_info" -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] CONF_CUSTOM = "custom" CONF_FACTOR = "factor" diff --git a/homeassistant/components/smappee/const.py b/homeassistant/components/smappee/const.py index 1abfc3a9b02..1e04a11f47a 100644 --- a/homeassistant/components/smappee/const.py +++ b/homeassistant/components/smappee/const.py @@ -2,6 +2,8 @@ from datetime import timedelta +from homeassistant.const import Platform + DOMAIN = "smappee" DATA_CLIENT = "smappee_data" @@ -12,7 +14,7 @@ CONF_TITLE = "title" ENV_CLOUD = "cloud" ENV_LOCAL = "local" -PLATFORMS = ["binary_sensor", "sensor", "switch"] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH] SUPPORTED_LOCAL_DEVICES = ("Smappee1", "Smappee2", "Smappee50") diff --git a/homeassistant/components/smart_meter_texas/__init__.py b/homeassistant/components/smart_meter_texas/__init__.py index 0d636c7558f..b3ee4f807ba 100644 --- a/homeassistant/components/smart_meter_texas/__init__.py +++ b/homeassistant/components/smart_meter_texas/__init__.py @@ -10,7 +10,7 @@ from smart_meter_texas.exceptions import ( ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client @@ -30,7 +30,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/smarthab/__init__.py b/homeassistant/components/smarthab/__init__.py index 7f8d5a38222..a4dc1a43f31 100644 --- a/homeassistant/components/smarthab/__init__.py +++ b/homeassistant/components/smarthab/__init__.py @@ -5,7 +5,7 @@ import pysmarthab import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv @@ -13,7 +13,7 @@ from homeassistant.helpers.typing import ConfigType DOMAIN = "smarthab" DATA_HUB = "hub" -PLATFORMS = ["light", "cover"] +PLATFORMS = [Platform.LIGHT, Platform.COVER] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/smartthings/const.py b/homeassistant/components/smartthings/const.py index a7aa9066dd2..1bd21cd73cd 100644 --- a/homeassistant/components/smartthings/const.py +++ b/homeassistant/components/smartthings/const.py @@ -2,6 +2,8 @@ from datetime import timedelta import re +from homeassistant.const import Platform + DOMAIN = "smartthings" APP_OAUTH_CLIENT_NAME = "Home Assistant" @@ -32,15 +34,15 @@ STORAGE_VERSION = 1 # Ordered 'specific to least-specific platform' in order for capabilities # to be drawn-down and represented by the most appropriate platform. PLATFORMS = [ - "climate", - "fan", - "light", - "lock", - "cover", - "switch", - "binary_sensor", - "sensor", - "scene", + Platform.CLIMATE, + Platform.FAN, + Platform.LIGHT, + Platform.LOCK, + Platform.COVER, + Platform.SWITCH, + Platform.BINARY_SENSOR, + Platform.SENSOR, + Platform.SCENE, ] IGNORED_CAPABILITIES = [ diff --git a/homeassistant/components/smarttub/__init__.py b/homeassistant/components/smarttub/__init__.py index 89ad9222e7a..4a8aa10a51d 100644 --- a/homeassistant/components/smarttub/__init__.py +++ b/homeassistant/components/smarttub/__init__.py @@ -1,8 +1,16 @@ """SmartTub integration.""" +from homeassistant.const import Platform + from .const import DOMAIN, SMARTTUB_CONTROLLER from .controller import SmartTubController -PLATFORMS = ["binary_sensor", "climate", "light", "sensor", "switch"] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.CLIMATE, + Platform.LIGHT, + Platform.SENSOR, + Platform.SWITCH, +] async def async_setup_entry(hass, entry): diff --git a/homeassistant/components/smhi/__init__.py b/homeassistant/components/smhi/__init__.py index 418c9ac32f1..9625d15f92a 100644 --- a/homeassistant/components/smhi/__init__.py +++ b/homeassistant/components/smhi/__init__.py @@ -1,8 +1,9 @@ """Support for the Swedish weather institute weather service.""" from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant -PLATFORMS = ["weather"] +PLATFORMS = [Platform.WEATHER] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/sms/__init__.py b/homeassistant/components/sms/__init__.py index 358f608be58..f987507ca1f 100644 --- a/homeassistant/components/sms/__init__.py +++ b/homeassistant/components/sms/__init__.py @@ -3,14 +3,14 @@ import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_DEVICE +from homeassistant.const import CONF_DEVICE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from .const import DOMAIN, SMS_GATEWAY from .gateway import create_sms_gateway -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] CONFIG_SCHEMA = vol.Schema( {DOMAIN: vol.Schema({vol.Required(CONF_DEVICE): cv.isdevice})}, diff --git a/homeassistant/components/solaredge/__init__.py b/homeassistant/components/solaredge/__init__.py index cb56817fe87..8b83891f4e3 100644 --- a/homeassistant/components/solaredge/__init__.py +++ b/homeassistant/components/solaredge/__init__.py @@ -5,7 +5,7 @@ from requests.exceptions import ConnectTimeout, HTTPError from solaredge import Solaredge from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY +from homeassistant.const import CONF_API_KEY, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv @@ -14,7 +14,7 @@ from .const import CONF_SITE_ID, DATA_API_CLIENT, DOMAIN, LOGGER CONFIG_SCHEMA = cv.deprecated(DOMAIN) -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/solarlog/__init__.py b/homeassistant/components/solarlog/__init__.py index 190898abb27..c0457b729f5 100644 --- a/homeassistant/components/solarlog/__init__.py +++ b/homeassistant/components/solarlog/__init__.py @@ -7,7 +7,7 @@ from requests.exceptions import HTTPError, Timeout from sunwatcher.solarlog.solarlog import SolarLog from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import update_coordinator @@ -15,7 +15,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/soma/__init__.py b/homeassistant/components/soma/__init__.py index 532e6204ad9..65171070537 100644 --- a/homeassistant/components/soma/__init__.py +++ b/homeassistant/components/soma/__init__.py @@ -5,7 +5,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_PORT, Platform from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import DeviceInfo, Entity @@ -26,7 +26,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -PLATFORMS = ["cover", "sensor"] +PLATFORMS = [Platform.COVER, Platform.SENSOR] async def async_setup(hass, config): diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py index a6bd320edd6..c021d408288 100644 --- a/homeassistant/components/somfy/__init__.py +++ b/homeassistant/components/somfy/__init__.py @@ -6,7 +6,12 @@ from pymfy.api.devices.category import Category import voluptuous as vol from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_OPTIMISTIC +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + CONF_OPTIMISTIC, + Platform, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import ( config_entry_oauth2_flow, @@ -39,7 +44,12 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -PLATFORMS = ["climate", "cover", "sensor", "switch"] +PLATFORMS = [ + Platform.CLIMATE, + Platform.COVER, + Platform.SENSOR, + Platform.SWITCH, +] async def async_setup(hass, config): diff --git a/homeassistant/components/somfy_mylink/const.py b/homeassistant/components/somfy_mylink/const.py index 4d4c4b58eae..bb9f2c5fd42 100644 --- a/homeassistant/components/somfy_mylink/const.py +++ b/homeassistant/components/somfy_mylink/const.py @@ -1,4 +1,5 @@ """Component for the Somfy MyLink device supporting the Synergy API.""" +from homeassistant.const import Platform CONF_SYSTEM_ID = "system_id" CONF_REVERSE = "reverse" @@ -12,6 +13,6 @@ DATA_SOMFY_MYLINK = "somfy_mylink_data" MYLINK_STATUS = "mylink_status" DOMAIN = "somfy_mylink" -PLATFORMS = ["cover"] +PLATFORMS = [Platform.COVER] MANUFACTURER = "Somfy" diff --git a/homeassistant/components/sonarr/__init__.py b/homeassistant/components/sonarr/__init__.py index b2cc13abc37..b574e68ae2f 100644 --- a/homeassistant/components/sonarr/__init__.py +++ b/homeassistant/components/sonarr/__init__.py @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_PORT, CONF_SSL, CONF_VERIFY_SSL, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady @@ -27,7 +28,7 @@ from .const import ( DOMAIN, ) -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] SCAN_INTERVAL = timedelta(seconds=30) diff --git a/homeassistant/components/songpal/__init__.py b/homeassistant/components/songpal/__init__.py index eb93dafb123..e4b7b4a0c0b 100644 --- a/homeassistant/components/songpal/__init__.py +++ b/homeassistant/components/songpal/__init__.py @@ -3,7 +3,7 @@ import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import ConfigType @@ -19,7 +19,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -PLATFORMS = ["media_player"] +PLATFORMS = [Platform.MEDIA_PLAYER] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: diff --git a/homeassistant/components/speedtestdotnet/const.py b/homeassistant/components/speedtestdotnet/const.py index 323d17cdd84..caed1f408ba 100644 --- a/homeassistant/components/speedtestdotnet/const.py +++ b/homeassistant/components/speedtestdotnet/const.py @@ -9,7 +9,11 @@ from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, SensorEntityDescription, ) -from homeassistant.const import DATA_RATE_MEGABITS_PER_SECOND, TIME_MILLISECONDS +from homeassistant.const import ( + DATA_RATE_MEGABITS_PER_SECOND, + TIME_MILLISECONDS, + Platform, +) DOMAIN: Final = "speedtestdotnet" @@ -65,4 +69,4 @@ ATTRIBUTION: Final = "Data retrieved from Speedtest.net by Ookla" ICON: Final = "mdi:speedometer" -PLATFORMS: Final = ["sensor"] +PLATFORMS: Final = [Platform.SENSOR] diff --git a/homeassistant/components/spider/const.py b/homeassistant/components/spider/const.py index b8621262ed5..503625fedd2 100644 --- a/homeassistant/components/spider/const.py +++ b/homeassistant/components/spider/const.py @@ -1,6 +1,7 @@ """Constants for the Spider integration.""" +from homeassistant.const import Platform DOMAIN = "spider" DEFAULT_SCAN_INTERVAL = 300 -PLATFORMS = ["climate", "switch", "sensor"] +PLATFORMS = [Platform.CLIMATE, Platform.SWITCH, Platform.SENSOR] diff --git a/homeassistant/components/spotify/__init__.py b/homeassistant/components/spotify/__init__.py index 8af58edd4b4..d251c35c3ec 100644 --- a/homeassistant/components/spotify/__init__.py +++ b/homeassistant/components/spotify/__init__.py @@ -4,9 +4,13 @@ import aiohttp from spotipy import Spotify, SpotifyException import voluptuous as vol -from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_CREDENTIALS, CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.const import ( + ATTR_CREDENTIALS, + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + Platform, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv @@ -37,7 +41,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -PLATFORMS = [MEDIA_PLAYER_DOMAIN] +PLATFORMS = [Platform.MEDIA_PLAYER] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: diff --git a/homeassistant/components/squeezebox/__init__.py b/homeassistant/components/squeezebox/__init__.py index 9bdc8ac9669..f7a20ba4aef 100644 --- a/homeassistant/components/squeezebox/__init__.py +++ b/homeassistant/components/squeezebox/__init__.py @@ -2,15 +2,15 @@ import logging -from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from .const import DISCOVERY_TASK, DOMAIN, PLAYER_DISCOVERY_UNSUB _LOGGER = logging.getLogger(__name__) -PLATFORMS = [MP_DOMAIN] +PLATFORMS = [Platform.MEDIA_PLAYER] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/srp_energy/__init__.py b/homeassistant/components/srp_energy/__init__.py index 76d344415d6..ac9cf693c10 100644 --- a/homeassistant/components/srp_energy/__init__.py +++ b/homeassistant/components/srp_energy/__init__.py @@ -4,7 +4,7 @@ import logging from srpenergy.client import SrpEnergyClient from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -13,7 +13,7 @@ from .const import SRP_ENERGY_DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/starline/const.py b/homeassistant/components/starline/const.py index 488a5cb9e0f..be9656e70c9 100644 --- a/homeassistant/components/starline/const.py +++ b/homeassistant/components/starline/const.py @@ -1,10 +1,18 @@ """StarLine constants.""" import logging +from homeassistant.const import Platform + _LOGGER = logging.getLogger(__package__) DOMAIN = "starline" -PLATFORMS = ["device_tracker", "binary_sensor", "sensor", "lock", "switch"] +PLATFORMS = [ + Platform.DEVICE_TRACKER, + Platform.BINARY_SENSOR, + Platform.SENSOR, + Platform.LOCK, + Platform.SWITCH, +] CONF_APP_ID = "app_id" CONF_APP_SECRET = "app_secret" diff --git a/homeassistant/components/stookalert/__init__.py b/homeassistant/components/stookalert/__init__.py index 8dfc208d945..ad800c20e95 100644 --- a/homeassistant/components/stookalert/__init__.py +++ b/homeassistant/components/stookalert/__init__.py @@ -3,13 +3,13 @@ from __future__ import annotations import stookalert -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from .const import CONF_PROVINCE, DOMAIN -PLATFORMS = (BINARY_SENSOR_DOMAIN,) +PLATFORMS = [Platform.BINARY_SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/subaru/const.py b/homeassistant/components/subaru/const.py index cada29edd3a..596923cbc06 100644 --- a/homeassistant/components/subaru/const.py +++ b/homeassistant/components/subaru/const.py @@ -1,4 +1,5 @@ """Constants for the Subaru integration.""" +from homeassistant.const import Platform DOMAIN = "subaru" FETCH_INTERVAL = 300 @@ -31,7 +32,7 @@ API_GEN_2 = "g2" MANUFACTURER = "Subaru Corp." PLATFORMS = [ - "sensor", + Platform.SENSOR, ] ICONS = { diff --git a/homeassistant/components/surepetcare/__init__.py b/homeassistant/components/surepetcare/__init__.py index adf3d07f79e..78988afa1cf 100644 --- a/homeassistant/components/surepetcare/__init__.py +++ b/homeassistant/components/surepetcare/__init__.py @@ -16,6 +16,7 @@ from homeassistant.const import ( CONF_SCAN_INTERVAL, CONF_TOKEN, CONF_USERNAME, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady @@ -41,7 +42,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["binary_sensor", "lock", "sensor"] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.LOCK, Platform.SENSOR] SCAN_INTERVAL = timedelta(minutes=3) CONFIG_SCHEMA = vol.Schema( diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index 421f6cab866..0059d655767 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -4,7 +4,7 @@ from asyncio import Lock import switchbot # pylint: disable=import-error from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_SENSOR_TYPE +from homeassistant.const import CONF_SENSOR_TYPE, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -27,8 +27,8 @@ from .const import ( from .coordinator import SwitchbotDataUpdateCoordinator PLATFORMS_BY_TYPE = { - ATTR_BOT: ["switch", "sensor"], - ATTR_CURTAIN: ["cover", "binary_sensor", "sensor"], + ATTR_BOT: [Platform.SWITCH, Platform.SENSOR], + ATTR_CURTAIN: [Platform.COVER, Platform.BINARY_SENSOR, Platform.SENSOR], } diff --git a/homeassistant/components/syncthing/__init__.py b/homeassistant/components/syncthing/__init__.py index d7cc671465a..ac9d5d32e92 100644 --- a/homeassistant/components/syncthing/__init__.py +++ b/homeassistant/components/syncthing/__init__.py @@ -10,6 +10,7 @@ from homeassistant.const import ( CONF_URL, CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -23,7 +24,7 @@ from .const import ( SERVER_UNAVAILABLE, ) -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/syncthru/__init__.py b/homeassistant/components/syncthru/__init__.py index da45350836c..a891bc50831 100644 --- a/homeassistant/components/syncthru/__init__.py +++ b/homeassistant/components/syncthru/__init__.py @@ -7,10 +7,8 @@ import logging import async_timeout from pysyncthru import ConnectionMode, SyncThru, SyncThruAPINotSupported -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_URL +from homeassistant.const import CONF_URL, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client, device_registry as dr from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -19,7 +17,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORMS = [BINARY_SENSOR_DOMAIN, SENSOR_DOMAIN] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index b1afe37b765..55f45fddabd 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -30,11 +30,12 @@ from homeassistant.const import ( ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, TEMP_CELSIUS, + Platform, ) from homeassistant.helpers.entity import EntityDescription DOMAIN = "synology_dsm" -PLATFORMS = ["binary_sensor", "camera", "sensor", "switch"] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.CAMERA, Platform.SENSOR, Platform.SWITCH] COORDINATOR_CAMERAS = "coordinator_cameras" COORDINATOR_CENTRAL = "coordinator_central" COORDINATOR_SWITCHES = "coordinator_switches" diff --git a/homeassistant/components/system_bridge/__init__.py b/homeassistant/components/system_bridge/__init__.py index cf50368c34c..53b0ead9728 100644 --- a/homeassistant/components/system_bridge/__init__.py +++ b/homeassistant/components/system_bridge/__init__.py @@ -20,6 +20,7 @@ from homeassistant.const import ( CONF_HOST, CONF_PATH, CONF_PORT, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ( @@ -40,7 +41,7 @@ from .coordinator import SystemBridgeDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["binary_sensor", "sensor"] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] CONF_ARGUMENTS = "arguments" CONF_BRIDGE = "bridge" From 66c73871111ca847620a9bd38ffd7330feac6891 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 4 Dec 2021 13:46:42 +0100 Subject: [PATCH 0026/2644] Fix str for device registry entry_type warnings caused by core (#60989) --- homeassistant/components/config/entity_registry.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 64cbfd7de1e..d42c5be08fc 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -10,7 +10,10 @@ from homeassistant.components.websocket_api.decorators import ( ) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.entity_registry import DISABLED_USER, async_get_registry +from homeassistant.helpers.entity_registry import ( + RegistryEntryDisabler, + async_get_registry, +) async def async_setup(hass): @@ -75,7 +78,12 @@ async def websocket_get_entity(hass, connection, msg): vol.Optional("name"): vol.Any(str, None), vol.Optional("new_entity_id"): str, # We only allow setting disabled_by user via API. - vol.Optional("disabled_by"): vol.Any(DISABLED_USER, None), + vol.Optional("disabled_by"): vol.Any( + None, + vol.All( + vol.Coerce(RegistryEntryDisabler), RegistryEntryDisabler.USER.value + ), + ), } ) async def websocket_update_entity(hass, connection, msg): From 73c880b6c2174f63336ccc92a305b82be234a999 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 4 Dec 2021 13:52:42 +0100 Subject: [PATCH 0027/2644] Fix typo in state_characteristic warning (#60990) --- homeassistant/components/statistics/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 44548c7d1a7..b06589f52c0 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -63,9 +63,9 @@ STAT_VARIANCE = "variance" STAT_DEFAULT = "default" DEPRECATION_WARNING = ( - "The configuration parameter 'state_characteristics' will become " + "The configuration parameter 'state_characteristic' will become " "mandatory in a future release of the statistics integration. " - "Please add 'state_characteristics: %s' to the configuration of " + "Please add 'state_characteristic: %s' to the configuration of " 'sensor "%s" to keep the current behavior. Read the documentation ' "for further details: " "https://www.home-assistant.io/integrations/statistics/" From b79b35abb506a0250d5f8fbcf172108e2090d99c Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 4 Dec 2021 14:10:01 +0100 Subject: [PATCH 0028/2644] Use platform enum (7) [T-Z] (#60948) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Joakim Sørensen Co-authored-by: Franck Nijhof --- homeassistant/components/tado/__init__.py | 9 ++++- .../components/tailscale/__init__.py | 5 +-- .../tesla_wall_connector/__init__.py | 4 +- homeassistant/components/tibber/__init__.py | 11 +++-- homeassistant/components/tile/__init__.py | 4 +- homeassistant/components/tolo/__init__.py | 16 ++++---- homeassistant/components/toon/__init__.py | 17 ++++---- .../components/totalconnect/__init__.py | 4 +- homeassistant/components/tplink/const.py | 4 +- homeassistant/components/traccar/__init__.py | 5 +-- homeassistant/components/tractive/__init__.py | 8 +++- homeassistant/components/tradfri/const.py | 13 +++++- .../trafikverket_weatherstation/const.py | 3 +- .../components/transmission/__init__.py | 3 +- homeassistant/components/tuya/const.py | 31 +++++++------- .../components/twentemilieu/__init__.py | 4 +- homeassistant/components/twinkly/__init__.py | 3 +- homeassistant/components/unifi/controller.py | 10 ++--- homeassistant/components/upb/__init__.py | 4 +- homeassistant/components/upcloud/__init__.py | 9 ++--- homeassistant/components/upnp/__init__.py | 3 +- homeassistant/components/uptimerobot/const.py | 4 +- homeassistant/components/velbus/__init__.py | 11 ++++- homeassistant/components/venstar/__init__.py | 3 +- homeassistant/components/verisure/__init__.py | 22 ++++------ homeassistant/components/vicare/const.py | 8 +++- homeassistant/components/vilfo/__init__.py | 4 +- homeassistant/components/vizio/__init__.py | 3 +- .../components/vlc_telnet/__init__.py | 4 +- homeassistant/components/volumio/__init__.py | 4 +- homeassistant/components/wallbox/__init__.py | 4 +- homeassistant/components/watttime/__init__.py | 3 +- .../components/waze_travel_time/__init__.py | 3 +- .../components/whirlpool/__init__.py | 3 +- homeassistant/components/wiffi/__init__.py | 4 +- homeassistant/components/wilight/__init__.py | 3 +- homeassistant/components/wolflink/__init__.py | 4 +- homeassistant/components/xbox/__init__.py | 9 ++++- .../components/xiaomi_aqara/__init__.py | 12 +++++- .../components/xiaomi_miio/__init__.py | 40 ++++++++++++------- .../components/yale_smart_alarm/const.py | 3 +- .../components/yamaha_musiccast/__init__.py | 4 +- homeassistant/components/yeelight/const.py | 4 +- homeassistant/components/youless/__init__.py | 4 +- homeassistant/components/zerproc/__init__.py | 3 +- 45 files changed, 200 insertions(+), 136 deletions(-) diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index 8bb9b72f117..6858f49799a 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -9,7 +9,7 @@ import requests.exceptions from homeassistant.components.climate.const import PRESET_AWAY, PRESET_HOME from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv @@ -31,7 +31,12 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["binary_sensor", "sensor", "climate", "water_heater"] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.CLIMATE, + Platform.SENSOR, + Platform.WATER_HEATER, +] MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=4) SCAN_INTERVAL = timedelta(minutes=5) diff --git a/homeassistant/components/tailscale/__init__.py b/homeassistant/components/tailscale/__init__.py index 72a0ef49fc0..b23eea7a06c 100644 --- a/homeassistant/components/tailscale/__init__.py +++ b/homeassistant/components/tailscale/__init__.py @@ -3,9 +3,8 @@ from __future__ import annotations from tailscale import Device as TailscaleDevice -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo, EntityDescription @@ -17,7 +16,7 @@ from homeassistant.helpers.update_coordinator import ( from .const import DOMAIN from .coordinator import TailscaleDataUpdateCoordinator -PLATFORMS = (BINARY_SENSOR_DOMAIN, SENSOR_DOMAIN) +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/tesla_wall_connector/__init__.py b/homeassistant/components/tesla_wall_connector/__init__.py index 78ae232d493..742b0a34b17 100644 --- a/homeassistant/components/tesla_wall_connector/__init__.py +++ b/homeassistant/components/tesla_wall_connector/__init__.py @@ -15,7 +15,7 @@ from tesla_wall_connector.exceptions import ( ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_SCAN_INTERVAL +from homeassistant.const import CONF_HOST, CONF_SCAN_INTERVAL, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -34,7 +34,7 @@ from .const import ( WALLCONNECTOR_DEVICE_NAME, ) -PLATFORMS: list[str] = ["binary_sensor", "sensor"] +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py index da94df55c88..3b71a40b093 100644 --- a/homeassistant/components/tibber/__init__.py +++ b/homeassistant/components/tibber/__init__.py @@ -5,7 +5,12 @@ import logging import aiohttp import tibber -from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CONF_NAME, + EVENT_HOMEASSISTANT_STOP, + Platform, +) from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -14,9 +19,7 @@ from homeassistant.util import dt as dt_util from .const import DATA_HASS_CONFIG, DOMAIN -PLATFORMS = [ - "sensor", -] +PLATFORMS = [Platform.SENSOR] CONFIG_SCHEMA = cv.deprecated(DOMAIN) diff --git a/homeassistant/components/tile/__init__.py b/homeassistant/components/tile/__init__.py index 75bf306a98b..388ffbff7fd 100644 --- a/homeassistant/components/tile/__init__.py +++ b/homeassistant/components/tile/__init__.py @@ -9,7 +9,7 @@ from pytile.errors import InvalidAuthError, SessionExpiredError, TileError from pytile.tile import Tile from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client @@ -19,7 +19,7 @@ from homeassistant.util.async_ import gather_with_concurrency from .const import DATA_COORDINATOR, DATA_TILE, DOMAIN, LOGGER -PLATFORMS = ["device_tracker"] +PLATFORMS = [Platform.DEVICE_TRACKER] DEVICE_TYPES = ["PHONE", "TILE"] DEFAULT_INIT_TASK_LIMIT = 2 diff --git a/homeassistant/components/tolo/__init__.py b/homeassistant/components/tolo/__init__.py index 4f41303c8f8..6e95089ccbe 100644 --- a/homeassistant/components/tolo/__init__.py +++ b/homeassistant/components/tolo/__init__.py @@ -11,7 +11,7 @@ from tololib.errors import ResponseTimedOutError from tololib.message_info import SettingsInfo, StatusInfo from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( @@ -23,13 +23,13 @@ from homeassistant.helpers.update_coordinator import ( from .const import DEFAULT_RETRY_COUNT, DEFAULT_RETRY_TIMEOUT, DOMAIN PLATFORMS = [ - "binary_sensor", - "button", - "climate", - "fan", - "light", - "select", - "sensor", + Platform.BINARY_SENSOR, + Platform.BUTTON, + Platform.CLIMATE, + Platform.FAN, + Platform.LIGHT, + Platform.SELECT, + Platform.SENSOR, ] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/toon/__init__.py b/homeassistant/components/toon/__init__.py index 372eeb47096..7d442130130 100644 --- a/homeassistant/components/toon/__init__.py +++ b/homeassistant/components/toon/__init__.py @@ -2,16 +2,13 @@ import voluptuous as vol -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_STARTED, + Platform, ) from homeassistant.core import CoreState, HomeAssistant from homeassistant.helpers import config_validation as cv, device_registry as dr @@ -25,12 +22,12 @@ from .const import CONF_AGREEMENT_ID, CONF_MIGRATE, DEFAULT_SCAN_INTERVAL, DOMAI from .coordinator import ToonDataUpdateCoordinator from .oauth2 import register_oauth2_implementations -PLATFORMS = { - BINARY_SENSOR_DOMAIN, - CLIMATE_DOMAIN, - SENSOR_DOMAIN, - SWITCH_DOMAIN, -} +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.CLIMATE, + Platform.SENSOR, + Platform.SWITCH, +] # Validation of the user's configuration CONFIG_SCHEMA = vol.Schema( diff --git a/homeassistant/components/totalconnect/__init__.py b/homeassistant/components/totalconnect/__init__.py index dcbc1592814..679f7afc3d3 100644 --- a/homeassistant/components/totalconnect/__init__.py +++ b/homeassistant/components/totalconnect/__init__.py @@ -7,7 +7,7 @@ from total_connect_client.client import TotalConnectClient from total_connect_client.exceptions import AuthenticationError, TotalConnectError from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed import homeassistant.helpers.config_validation as cv @@ -15,7 +15,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import CONF_USERCODES, DOMAIN -PLATFORMS = ["alarm_control_panel", "binary_sensor"] +PLATFORMS = [Platform.ALARM_CONTROL_PANEL, Platform.BINARY_SENSOR] CONFIG_SCHEMA = cv.deprecated(DOMAIN) SCAN_INTERVAL = timedelta(seconds=30) diff --git a/homeassistant/components/tplink/const.py b/homeassistant/components/tplink/const.py index 6d4fcbea75d..b1cd323a36a 100644 --- a/homeassistant/components/tplink/const.py +++ b/homeassistant/components/tplink/const.py @@ -3,6 +3,8 @@ from __future__ import annotations from typing import Final +from homeassistant.const import Platform + DOMAIN = "tplink" ATTR_CURRENT_A: Final = "current_a" @@ -17,4 +19,4 @@ CONF_STRIP: Final = "strip" CONF_SWITCH: Final = "switch" CONF_SENSOR: Final = "sensor" -PLATFORMS: Final = [CONF_LIGHT, CONF_SENSOR, CONF_SWITCH] +PLATFORMS: Final = [Platform.LIGHT, Platform.SENSOR, Platform.SWITCH] diff --git a/homeassistant/components/traccar/__init__.py b/homeassistant/components/traccar/__init__.py index e2605cdfd54..1d656d75c47 100644 --- a/homeassistant/components/traccar/__init__.py +++ b/homeassistant/components/traccar/__init__.py @@ -4,8 +4,7 @@ from http import HTTPStatus from aiohttp import web import voluptuous as vol -from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER -from homeassistant.const import ATTR_ID, CONF_WEBHOOK_ID +from homeassistant.const import ATTR_ID, CONF_WEBHOOK_ID, Platform from homeassistant.helpers import config_entry_flow import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -22,7 +21,7 @@ from .const import ( DOMAIN, ) -PLATFORMS = [DEVICE_TRACKER] +PLATFORMS = [Platform.DEVICE_TRACKER] TRACKER_UPDATE = f"{DOMAIN}_tracker_update" diff --git a/homeassistant/components/tractive/__init__.py b/homeassistant/components/tractive/__init__.py index 66a2afccb43..496ae3a85d0 100644 --- a/homeassistant/components/tractive/__init__.py +++ b/homeassistant/components/tractive/__init__.py @@ -15,6 +15,7 @@ from homeassistant.const import ( CONF_EMAIL, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP, + Platform, ) from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady @@ -38,7 +39,12 @@ from .const import ( TRACKER_POSITION_UPDATED, ) -PLATFORMS = ["binary_sensor", "device_tracker", "sensor", "switch"] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.DEVICE_TRACKER, + Platform.SENSOR, + Platform.SWITCH, +] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tradfri/const.py b/homeassistant/components/tradfri/const.py index abd3e8aff4a..b0c3955453c 100644 --- a/homeassistant/components/tradfri/const.py +++ b/homeassistant/components/tradfri/const.py @@ -1,6 +1,9 @@ """Consts used by Tradfri.""" from homeassistant.components.light import SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION -from homeassistant.const import CONF_HOST # noqa: F401 pylint: disable=unused-import +from homeassistant.const import ( # noqa: F401 pylint: disable=unused-import + CONF_HOST, + Platform, +) ATTR_AUTO = "Auto" ATTR_DIMMER = "dimmer" @@ -25,5 +28,11 @@ SIGNAL_GW = "tradfri.gw_status" KEY_SECURITY_CODE = "security_code" SUPPORTED_GROUP_FEATURES = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION SUPPORTED_LIGHT_FEATURES = SUPPORT_TRANSITION -PLATFORMS = ["cover", "fan", "light", "sensor", "switch"] +PLATFORMS = [ + Platform.COVER, + Platform.FAN, + Platform.LIGHT, + Platform.SENSOR, + Platform.SWITCH, +] TIMEOUT_API = 30 diff --git a/homeassistant/components/trafikverket_weatherstation/const.py b/homeassistant/components/trafikverket_weatherstation/const.py index bfda5782084..3c2cfc4d165 100644 --- a/homeassistant/components/trafikverket_weatherstation/const.py +++ b/homeassistant/components/trafikverket_weatherstation/const.py @@ -1,8 +1,9 @@ """Adds constants for Trafikverket Weather integration.""" +from homeassistant.const import Platform DOMAIN = "trafikverket_weatherstation" CONF_STATION = "station" -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] ATTRIBUTION = "Data provided by Trafikverket" ATTR_MEASURE_TIME = "measure_time" ATTR_ACTIVE = "active" diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index f40f0bd7d82..f514f751149 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -17,6 +17,7 @@ from homeassistant.const import ( CONF_PORT, CONF_SCAN_INTERVAL, CONF_USERNAME, + Platform, ) from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv @@ -95,7 +96,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -PLATFORMS = ["sensor", "switch"] +PLATFORMS = [Platform.SENSOR, Platform.SWITCH] async def async_setup(hass, config): diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index fa69b76695c..4fc2f37f106 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -35,6 +35,7 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS, + Platform, ) DOMAIN = "tuya" @@ -77,21 +78,21 @@ TUYA_SMART_APP = "tuyaSmart" SMARTLIFE_APP = "smartlife" PLATFORMS = [ - "binary_sensor", - "button", - "camera", - "climate", - "cover", - "fan", - "humidifier", - "light", - "number", - "scene", - "select", - "sensor", - "siren", - "switch", - "vacuum", + Platform.BINARY_SENSOR, + Platform.BUTTON, + Platform.CAMERA, + Platform.CLIMATE, + Platform.COVER, + Platform.FAN, + Platform.HUMIDIFIER, + Platform.LIGHT, + Platform.NUMBER, + Platform.SCENE, + Platform.SELECT, + Platform.SENSOR, + Platform.SIREN, + Platform.SWITCH, + Platform.VACUUM, ] diff --git a/homeassistant/components/twentemilieu/__init__.py b/homeassistant/components/twentemilieu/__init__.py index 60b7d07808b..576ab2068a7 100644 --- a/homeassistant/components/twentemilieu/__init__.py +++ b/homeassistant/components/twentemilieu/__init__.py @@ -7,7 +7,7 @@ from twentemilieu import TwenteMilieu, WasteType import voluptuous as vol from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ID +from homeassistant.const import CONF_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -20,7 +20,7 @@ SCAN_INTERVAL = timedelta(seconds=3600) SERVICE_UPDATE = "update" SERVICE_SCHEMA = vol.Schema({vol.Optional(CONF_ID): cv.string}) -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/twinkly/__init__.py b/homeassistant/components/twinkly/__init__.py index 3a9a2a8faa2..ee52d5473de 100644 --- a/homeassistant/components/twinkly/__init__.py +++ b/homeassistant/components/twinkly/__init__.py @@ -3,12 +3,13 @@ import twinkly_client from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_ENTRY_HOST, CONF_ENTRY_ID, DOMAIN -PLATFORMS = ["light"] +PLATFORMS = [Platform.LIGHT] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index 26463563db2..98eb377dab3 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -26,9 +26,6 @@ from aiounifi.events import ( from aiounifi.websocket import STATE_DISCONNECTED, STATE_RUNNING import async_timeout -from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.unifi.switch import BLOCK_SWITCH, POE_SWITCH from homeassistant.const import ( CONF_HOST, @@ -36,6 +33,7 @@ from homeassistant.const import ( CONF_PORT, CONF_USERNAME, CONF_VERIFY_SSL, + Platform, ) from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady @@ -75,7 +73,7 @@ from .errors import AuthenticationRequired, CannotConnect RETRY_TIMER = 15 CHECK_HEARTBEAT_INTERVAL = timedelta(seconds=1) -PLATFORMS = [TRACKER_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN] +PLATFORMS = [Platform.DEVICE_TRACKER, Platform.SENSOR, Platform.SWITCH] CLIENT_CONNECTED = ( WIRED_CLIENT_CONNECTED, @@ -337,9 +335,9 @@ class UniFiController: for entry in async_entries_for_config_entry( entity_registry, self.config_entry.entry_id ): - if entry.domain == TRACKER_DOMAIN: + if entry.domain == Platform.DEVICE_TRACKER: mac = entry.unique_id.split("-", 1)[0] - elif entry.domain == SWITCH_DOMAIN and ( + elif entry.domain == Platform.SWITCH and ( entry.unique_id.startswith(BLOCK_SWITCH) or entry.unique_id.startswith(POE_SWITCH) ): diff --git a/homeassistant/components/upb/__init__.py b/homeassistant/components/upb/__init__.py index 90d7c35cf64..acdf8c045a1 100644 --- a/homeassistant/components/upb/__init__.py +++ b/homeassistant/components/upb/__init__.py @@ -2,7 +2,7 @@ import upb_lib -from homeassistant.const import ATTR_COMMAND, CONF_FILE_PATH, CONF_HOST +from homeassistant.const import ATTR_COMMAND, CONF_FILE_PATH, CONF_HOST, Platform from homeassistant.core import callback from homeassistant.helpers.entity import DeviceInfo, Entity @@ -14,7 +14,7 @@ from .const import ( EVENT_UPB_SCENE_CHANGED, ) -PLATFORMS = ["light", "scene"] +PLATFORMS = [Platform.LIGHT, Platform.SCENE] async def async_setup_entry(hass, config_entry): diff --git a/homeassistant/components/upcloud/__init__.py b/homeassistant/components/upcloud/__init__.py index 2ecc6ec7522..bbce9cd5304 100644 --- a/homeassistant/components/upcloud/__init__.py +++ b/homeassistant/components/upcloud/__init__.py @@ -9,8 +9,6 @@ from typing import Any, Dict import requests.exceptions import upcloud_api -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_PASSWORD, @@ -19,6 +17,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_PROBLEM, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -48,7 +47,7 @@ DATA_UPCLOUD = "data_upcloud" DEFAULT_COMPONENT_NAME = "UpCloud {}" -CONFIG_ENTRY_DOMAINS = {BINARY_SENSOR_DOMAIN, SWITCH_DOMAIN} +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SWITCH] SIGNAL_UPDATE_UPCLOUD = "upcloud_update" @@ -157,7 +156,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DATA_UPCLOUD].coordinators[entry.data[CONF_USERNAME]] = coordinator # Forward entry setup - hass.config_entries.async_setup_platforms(entry, CONFIG_ENTRY_DOMAINS) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -165,7 +164,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload the config entry.""" unload_ok = await hass.config_entries.async_unload_platforms( - config_entry, CONFIG_ENTRY_DOMAINS + config_entry, PLATFORMS ) hass.data[DATA_UPCLOUD].coordinators.pop(config_entry.data[CONF_USERNAME]) diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index da0585df987..5180932080c 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -16,6 +16,7 @@ from homeassistant.components import ssdp from homeassistant.components.binary_sensor import BinarySensorEntityDescription from homeassistant.components.sensor import SensorEntityDescription from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr @@ -43,7 +44,7 @@ from .device import Device NOTIFICATION_ID = "upnp_notification" NOTIFICATION_TITLE = "UPnP/IGD Setup" -PLATFORMS = ["binary_sensor", "sensor"] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] CONFIG_SCHEMA = vol.Schema( vol.All( diff --git a/homeassistant/components/uptimerobot/const.py b/homeassistant/components/uptimerobot/const.py index 1c9bbe3e00b..7637a61c593 100644 --- a/homeassistant/components/uptimerobot/const.py +++ b/homeassistant/components/uptimerobot/const.py @@ -5,13 +5,15 @@ from datetime import timedelta from logging import Logger, getLogger from typing import Final +from homeassistant.const import Platform + LOGGER: Logger = getLogger(__package__) # The free plan is limited to 10 requests/minute COORDINATOR_UPDATE_INTERVAL: timedelta = timedelta(seconds=10) DOMAIN: Final = "uptimerobot" -PLATFORMS: Final = ["binary_sensor"] +PLATFORMS: Final = [Platform.BINARY_SENSOR] ATTRIBUTION: Final = "Data provided by UptimeRobot" diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py index 330a4315a25..d2aa9531467 100644 --- a/homeassistant/components/velbus/__init__.py +++ b/homeassistant/components/velbus/__init__.py @@ -8,7 +8,7 @@ from velbusaio.controller import Velbus import voluptuous as vol from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ADDRESS, CONF_PORT +from homeassistant.const import CONF_ADDRESS, CONF_PORT, Platform from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import device_registry import homeassistant.helpers.config_validation as cv @@ -26,7 +26,14 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["switch", "sensor", "binary_sensor", "cover", "climate", "light"] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.CLIMATE, + Platform.COVER, + Platform.LIGHT, + Platform.SENSOR, + Platform.SWITCH, +] async def velbus_connect_task( diff --git a/homeassistant/components/venstar/__init__.py b/homeassistant/components/venstar/__init__.py index 2ed6080f84f..be082d0ad85 100644 --- a/homeassistant/components/venstar/__init__.py +++ b/homeassistant/components/venstar/__init__.py @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_PIN, CONF_SSL, CONF_USERNAME, + Platform, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import update_coordinator @@ -19,7 +20,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import _LOGGER, DOMAIN, VENSTAR_TIMEOUT -PLATFORMS = ["binary_sensor", "climate", "sensor"] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.SENSOR] async def async_setup_entry(hass, config): diff --git a/homeassistant/components/verisure/__init__.py b/homeassistant/components/verisure/__init__.py index bea82a4785c..d42a6ad004b 100644 --- a/homeassistant/components/verisure/__init__.py +++ b/homeassistant/components/verisure/__init__.py @@ -4,16 +4,8 @@ from __future__ import annotations from contextlib import suppress import os -from homeassistant.components.alarm_control_panel import ( - DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, -) -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN -from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed import homeassistant.helpers.config_validation as cv @@ -23,12 +15,12 @@ from .const import DOMAIN from .coordinator import VerisureDataUpdateCoordinator PLATFORMS = [ - ALARM_CONTROL_PANEL_DOMAIN, - BINARY_SENSOR_DOMAIN, - CAMERA_DOMAIN, - LOCK_DOMAIN, - SENSOR_DOMAIN, - SWITCH_DOMAIN, + Platform.ALARM_CONTROL_PANEL, + Platform.BINARY_SENSOR, + Platform.CAMERA, + Platform.LOCK, + Platform.SENSOR, + Platform.SWITCH, ] CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) diff --git a/homeassistant/components/vicare/const.py b/homeassistant/components/vicare/const.py index db8b782399e..c5f78f31fc0 100644 --- a/homeassistant/components/vicare/const.py +++ b/homeassistant/components/vicare/const.py @@ -6,11 +6,17 @@ from homeassistant.const import ( DEVICE_CLASS_GAS, ENERGY_KILO_WATT_HOUR, VOLUME_CUBIC_METERS, + Platform, ) DOMAIN = "vicare" -PLATFORMS = ["climate", "sensor", "binary_sensor", "water_heater"] +PLATFORMS = [ + Platform.CLIMATE, + Platform.SENSOR, + Platform.BINARY_SENSOR, + Platform.WATER_HEATER, +] VICARE_DEVICE_CONFIG = "device_conf" VICARE_API = "api" diff --git a/homeassistant/components/vilfo/__init__.py b/homeassistant/components/vilfo/__init__.py index b2e460c85f2..ac3b8b87f3f 100644 --- a/homeassistant/components/vilfo/__init__.py +++ b/homeassistant/components/vilfo/__init__.py @@ -6,14 +6,14 @@ from vilfo import Client as VilfoClient from vilfo.exceptions import VilfoException from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.util import Throttle from .const import ATTR_BOOT_TIME, ATTR_LOAD, DOMAIN, ROUTER_DEFAULT_HOST -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) diff --git a/homeassistant/components/vizio/__init__.py b/homeassistant/components/vizio/__init__.py index aec6f38a1b1..e66a8f3a554 100644 --- a/homeassistant/components/vizio/__init__.py +++ b/homeassistant/components/vizio/__init__.py @@ -11,6 +11,7 @@ import voluptuous as vol from homeassistant.components.media_player import DEVICE_CLASS_TV from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry, ConfigEntryState +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -40,7 +41,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -PLATFORMS = ["media_player"] +PLATFORMS = [Platform.MEDIA_PLAYER] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: diff --git a/homeassistant/components/vlc_telnet/__init__.py b/homeassistant/components/vlc_telnet/__init__.py index 72bbf57ff94..9fe3b97ab31 100644 --- a/homeassistant/components/vlc_telnet/__init__.py +++ b/homeassistant/components/vlc_telnet/__init__.py @@ -3,13 +3,13 @@ from aiovlc.client import Client from aiovlc.exceptions import AuthError, ConnectError from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from .const import DATA_AVAILABLE, DATA_VLC, DOMAIN, LOGGER -PLATFORMS = ["media_player"] +PLATFORMS = [Platform.MEDIA_PLAYER] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/volumio/__init__.py b/homeassistant/components/volumio/__init__.py index 215cd2e1aff..8189aeef1f4 100644 --- a/homeassistant/components/volumio/__init__.py +++ b/homeassistant/components/volumio/__init__.py @@ -3,14 +3,14 @@ from pyvolumio import CannotConnectError, Volumio from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_PORT, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DATA_INFO, DATA_VOLUMIO, DOMAIN -PLATFORMS = ["media_player"] +PLATFORMS = [Platform.MEDIA_PLAYER] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/wallbox/__init__.py b/homeassistant/components/wallbox/__init__.py index c40d3fb37a8..103a293c39f 100644 --- a/homeassistant/components/wallbox/__init__.py +++ b/homeassistant/components/wallbox/__init__.py @@ -10,7 +10,7 @@ import requests from wallbox import Wallbox from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -19,7 +19,7 @@ from .const import CONF_DATA_KEY, CONF_MAX_CHARGING_CURRENT_KEY, CONF_STATION, D _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["sensor", "number"] +PLATFORMS = [Platform.SENSOR, Platform.NUMBER] UPDATE_INTERVAL = 30 diff --git a/homeassistant/components/watttime/__init__.py b/homeassistant/components/watttime/__init__.py index d33e6fceb2e..2f07658b923 100644 --- a/homeassistant/components/watttime/__init__.py +++ b/homeassistant/components/watttime/__init__.py @@ -13,6 +13,7 @@ from homeassistant.const import ( CONF_LONGITUDE, CONF_PASSWORD, CONF_USERNAME, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed @@ -23,7 +24,7 @@ from .const import DOMAIN, LOGGER DEFAULT_UPDATE_INTERVAL = timedelta(minutes=5) -PLATFORMS: list[str] = ["sensor"] +PLATFORMS: list[Platform] = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/waze_travel_time/__init__.py b/homeassistant/components/waze_travel_time/__init__.py index fa605d19c49..3b193d6c06b 100644 --- a/homeassistant/components/waze_travel_time/__init__.py +++ b/homeassistant/components/waze_travel_time/__init__.py @@ -1,12 +1,13 @@ """The waze_travel_time component.""" from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_registry import ( async_entries_for_config_entry, async_get, ) -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/whirlpool/__init__.py b/homeassistant/components/whirlpool/__init__.py index c53b9a59ef4..659b4602f0d 100644 --- a/homeassistant/components/whirlpool/__init__.py +++ b/homeassistant/components/whirlpool/__init__.py @@ -5,6 +5,7 @@ import aiohttp from whirlpool.auth import Auth from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -12,7 +13,7 @@ from .const import AUTH_INSTANCE_KEY, DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["climate"] +PLATFORMS = [Platform.CLIMATE] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/wiffi/__init__.py b/homeassistant/components/wiffi/__init__.py index a7f6b8a7b22..ab5bda8dd1d 100644 --- a/homeassistant/components/wiffi/__init__.py +++ b/homeassistant/components/wiffi/__init__.py @@ -6,7 +6,7 @@ import logging from wiffi import WiffiTcpServer from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PORT, CONF_TIMEOUT +from homeassistant.const import CONF_PORT, CONF_TIMEOUT, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry @@ -29,7 +29,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["sensor", "binary_sensor"] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/wilight/__init__.py b/homeassistant/components/wilight/__init__.py index c77814519a1..932ce1538bf 100644 --- a/homeassistant/components/wilight/__init__.py +++ b/homeassistant/components/wilight/__init__.py @@ -1,6 +1,7 @@ """The WiLight integration.""" from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.entity import DeviceInfo, Entity @@ -10,7 +11,7 @@ from .parent_device import WiLightParent DOMAIN = "wilight" # List the platforms that you want to support. -PLATFORMS = ["cover", "fan", "light"] +PLATFORMS = [Platform.COVER, Platform.FAN, Platform.LIGHT] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/wolflink/__init__.py b/homeassistant/components/wolflink/__init__.py index e15a5225475..19cafa89a13 100644 --- a/homeassistant/components/wolflink/__init__.py +++ b/homeassistant/components/wolflink/__init__.py @@ -7,7 +7,7 @@ from wolf_smartset.token_auth import InvalidAuth from wolf_smartset.wolf_client import FetchFailed, ParameterReadError, WolfClient from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -23,7 +23,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/xbox/__init__.py b/homeassistant/components/xbox/__init__.py index 2a09c465984..0466d0191cf 100644 --- a/homeassistant/components/xbox/__init__.py +++ b/homeassistant/components/xbox/__init__.py @@ -21,7 +21,7 @@ from xbox.webapi.api.provider.smartglass.models import ( ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import ( aiohttp_client, @@ -48,7 +48,12 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -PLATFORMS = ["media_player", "remote", "binary_sensor", "sensor"] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.MEDIA_PLAYER, + Platform.REMOTE, + Platform.SENSOR, +] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: diff --git a/homeassistant/components/xiaomi_aqara/__init__.py b/homeassistant/components/xiaomi_aqara/__init__.py index 7aff6ece0e1..ce3e5d72e0d 100644 --- a/homeassistant/components/xiaomi_aqara/__init__.py +++ b/homeassistant/components/xiaomi_aqara/__init__.py @@ -15,6 +15,7 @@ from homeassistant.const import ( CONF_PORT, CONF_PROTOCOL, EVENT_HOMEASSISTANT_STOP, + Platform, ) from homeassistant.core import callback from homeassistant.helpers import device_registry as dr @@ -36,8 +37,15 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -GATEWAY_PLATFORMS = ["binary_sensor", "sensor", "switch", "light", "cover", "lock"] -GATEWAY_PLATFORMS_NO_KEY = ["binary_sensor", "sensor"] +GATEWAY_PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.COVER, + Platform.LIGHT, + Platform.LOCK, + Platform.SENSOR, + Platform.SWITCH, +] +GATEWAY_PLATFORMS_NO_KEY = [Platform.BINARY_SENSOR, Platform.SENSOR] ATTR_GW_MAC = "gw_mac" ATTR_RINGTONE_ID = "ringtone_id" diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 62935f5e38b..009b068a521 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -33,7 +33,7 @@ from miio import ( from miio.gateway.gateway import GatewayException from homeassistant import config_entries, core -from homeassistant.const import CONF_HOST, CONF_TOKEN +from homeassistant.const import CONF_HOST, CONF_TOKEN, Platform from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -78,20 +78,32 @@ _LOGGER = logging.getLogger(__name__) POLLING_TIMEOUT_SEC = 10 UPDATE_INTERVAL = timedelta(seconds=15) -GATEWAY_PLATFORMS = ["alarm_control_panel", "light", "sensor", "switch"] -SWITCH_PLATFORMS = ["switch"] -FAN_PLATFORMS = ["binary_sensor", "fan", "number", "select", "sensor", "switch"] -HUMIDIFIER_PLATFORMS = [ - "binary_sensor", - "humidifier", - "number", - "select", - "sensor", - "switch", +GATEWAY_PLATFORMS = [ + Platform.ALARM_CONTROL_PANEL, + Platform.LIGHT, + Platform.SENSOR, + Platform.SWITCH, ] -LIGHT_PLATFORMS = ["light"] -VACUUM_PLATFORMS = ["binary_sensor", "sensor", "vacuum"] -AIR_MONITOR_PLATFORMS = ["air_quality", "sensor"] +SWITCH_PLATFORMS = [Platform.SWITCH] +FAN_PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.FAN, + Platform.NUMBER, + Platform.SELECT, + Platform.SENSOR, + Platform.SWITCH, +] +HUMIDIFIER_PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.HUMIDIFIER, + Platform.NUMBER, + Platform.SELECT, + Platform.SENSOR, + Platform.SWITCH, +] +LIGHT_PLATFORMS = [Platform.LIGHT] +VACUUM_PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.VACUUM] +AIR_MONITOR_PLATFORMS = [Platform.AIR_QUALITY, Platform.SENSOR] MODEL_TO_CLASS_MAP = { MODEL_FAN_1C: Fan1C, diff --git a/homeassistant/components/yale_smart_alarm/const.py b/homeassistant/components/yale_smart_alarm/const.py index cbb96579f4f..6bf61cea610 100644 --- a/homeassistant/components/yale_smart_alarm/const.py +++ b/homeassistant/components/yale_smart_alarm/const.py @@ -11,6 +11,7 @@ from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, + Platform, ) CONF_AREA_ID = "area_id" @@ -30,7 +31,7 @@ LOGGER = logging.getLogger(__package__) ATTR_ONLINE = "online" ATTR_STATUS = "status" -PLATFORMS = ["alarm_control_panel"] +PLATFORMS = [Platform.ALARM_CONTROL_PANEL] STATE_MAP = { YALE_STATE_DISARM: STATE_ALARM_DISARMED, diff --git a/homeassistant/components/yamaha_musiccast/__init__.py b/homeassistant/components/yamaha_musiccast/__init__.py index 194168b2eee..142af2d769b 100644 --- a/homeassistant/components/yamaha_musiccast/__init__.py +++ b/homeassistant/components/yamaha_musiccast/__init__.py @@ -10,7 +10,7 @@ from aiomusiccast.musiccast_device import MusicCastData, MusicCastDevice from homeassistant.components import ssdp from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac @@ -30,7 +30,7 @@ from .const import ( ENTITY_CATEGORY_MAPPING, ) -PLATFORMS = ["media_player", "number"] +PLATFORMS = [Platform.MEDIA_PLAYER, Platform.NUMBER] _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=60) diff --git a/homeassistant/components/yeelight/const.py b/homeassistant/components/yeelight/const.py index 2b494bf9ef2..28b5591dcbf 100644 --- a/homeassistant/components/yeelight/const.py +++ b/homeassistant/components/yeelight/const.py @@ -2,6 +2,8 @@ from datetime import timedelta +from homeassistant.const import Platform + DOMAIN = "yeelight" @@ -100,4 +102,4 @@ UPDATE_REQUEST_PROPERTIES = [ ] -PLATFORMS = ["binary_sensor", "light"] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.LIGHT] diff --git a/homeassistant/components/youless/__init__.py b/homeassistant/components/youless/__init__.py index 0980e451028..3339cdccd36 100644 --- a/homeassistant/components/youless/__init__.py +++ b/homeassistant/components/youless/__init__.py @@ -6,14 +6,14 @@ from urllib.error import URLError from youless_api import YoulessAPI from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zerproc/__init__.py b/homeassistant/components/zerproc/__init__.py index ec48fb90345..459024ec34c 100644 --- a/homeassistant/components/zerproc/__init__.py +++ b/homeassistant/components/zerproc/__init__.py @@ -1,11 +1,12 @@ """Zerproc lights integration.""" from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from .const import DATA_ADDRESSES, DATA_DISCOVERY_SUBSCRIPTION, DOMAIN -PLATFORMS = ["light"] +PLATFORMS = [Platform.LIGHT] async def async_setup(hass, config): From c44eaca533fcb67d9e98d82f2c7ac7f683a9baa6 Mon Sep 17 00:00:00 2001 From: infeeeee Date: Sat, 4 Dec 2021 14:47:49 +0100 Subject: [PATCH 0029/2644] Use state variable from mpchc (#59341) --- homeassistant/components/mpchc/media_player.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mpchc/media_player.py b/homeassistant/components/mpchc/media_player.py index 8964ea7ed6f..343644b2b1d 100644 --- a/homeassistant/components/mpchc/media_player.py +++ b/homeassistant/components/mpchc/media_player.py @@ -106,13 +106,13 @@ class MpcHcDevice(MediaPlayerEntity): @property def state(self): """Return the state of the device.""" - state = self._player_variables.get("statestring", None) + state = self._player_variables.get("state", None) if state is None: return STATE_OFF - if state == "playing": + if state == "2": return STATE_PLAYING - if state == "paused": + if state == "1": return STATE_PAUSED return STATE_IDLE From e36f9f684d0d6674982672fb6f7a31117aeb5b37 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Sat, 4 Dec 2021 09:13:05 -0500 Subject: [PATCH 0030/2644] Revert "Use language independent variable to read MPC-HC state" (#60993) --- homeassistant/components/mpchc/media_player.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mpchc/media_player.py b/homeassistant/components/mpchc/media_player.py index 343644b2b1d..8964ea7ed6f 100644 --- a/homeassistant/components/mpchc/media_player.py +++ b/homeassistant/components/mpchc/media_player.py @@ -106,13 +106,13 @@ class MpcHcDevice(MediaPlayerEntity): @property def state(self): """Return the state of the device.""" - state = self._player_variables.get("state", None) + state = self._player_variables.get("statestring", None) if state is None: return STATE_OFF - if state == "2": + if state == "playing": return STATE_PLAYING - if state == "1": + if state == "paused": return STATE_PAUSED return STATE_IDLE From e33774a61e7fcc88aff752dfa4618dd26a746872 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 4 Dec 2021 09:34:24 -0700 Subject: [PATCH 0031/2644] Add missing SimpliSafe service information (#60958) --- homeassistant/components/simplisafe/services.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/homeassistant/components/simplisafe/services.yaml b/homeassistant/components/simplisafe/services.yaml index 273aa02c300..bdd7939a209 100644 --- a/homeassistant/components/simplisafe/services.yaml +++ b/homeassistant/components/simplisafe/services.yaml @@ -1,4 +1,16 @@ # Describes the format for available SimpliSafe services +clear_notifications: + name: Clear notifications + description: Clear any active SimpliSafe notificiations + fields: + device_id: + name: System + description: The system to remove the PIN from + required: true + selector: + device: + integration: simplisafe + model: alarm_control_panel remove_pin: name: Remove PIN description: Remove a PIN by its label or value. From a5b0e21e08b41e5f1a473e0396d8519727a5cf6d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 4 Dec 2021 18:38:09 +0100 Subject: [PATCH 0032/2644] Fix translations for binary_sensor tampered device triggers (#60996) --- homeassistant/components/binary_sensor/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/binary_sensor/strings.json b/homeassistant/components/binary_sensor/strings.json index eb97b370105..e2167c24f8a 100644 --- a/homeassistant/components/binary_sensor/strings.json +++ b/homeassistant/components/binary_sensor/strings.json @@ -90,8 +90,8 @@ "no_smoke": "{entity_name} stopped detecting smoke", "sound": "{entity_name} started detecting sound", "no_sound": "{entity_name} stopped detecting sound", - "is_tampered": "{entity_name} started detecting tampering", - "is_not_tampered": "{entity_name} stopped detecting tampering", + "tampered": "{entity_name} started detecting tampering", + "not_tampered": "{entity_name} stopped detecting tampering", "update": "{entity_name} got an update available", "no_update": "{entity_name} became up-to-date", "vibration": "{entity_name} started detecting vibration", From 5fd4c374274cae0ecabec045cbae21d79d388241 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sat, 4 Dec 2021 13:50:55 -0500 Subject: [PATCH 0033/2644] Fix missing test assertion in ZHA siren test (#61009) --- tests/components/zha/test_siren.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/components/zha/test_siren.py b/tests/components/zha/test_siren.py index 9a10f55f25a..d9c173fd932 100644 --- a/tests/components/zha/test_siren.py +++ b/tests/components/zha/test_siren.py @@ -137,3 +137,5 @@ async def test_siren(hass, siren): now = dt_util.utcnow() + timedelta(seconds=15) async_fire_time_changed(hass, now) await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == STATE_OFF From affa3a6ada4ae4d8f002c97b957decba27c602d2 Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Sat, 4 Dec 2021 23:07:28 +0100 Subject: [PATCH 0034/2644] Add missing local_ip to KNX config flow and options flow (#61018) * Add missing local_ip to KNX config flow and options flow * Update strings --- homeassistant/components/knx/__init__.py | 1 + homeassistant/components/knx/config_flow.py | 8 +++ homeassistant/components/knx/strings.json | 6 +- .../components/knx/translations/en.json | 2 + tests/components/knx/test_config_flow.py | 56 +++++++++++++++++++ 5 files changed, 71 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 3e75c614f1e..ba6689a023d 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -390,6 +390,7 @@ class KNXModule: connection_type=ConnectionType.TUNNELING, gateway_ip=self.config[CONF_HOST], gateway_port=self.config[CONF_PORT], + local_ip=self.config.get(ConnectionSchema.CONF_KNX_LOCAL_IP), route_back=self.config.get(ConnectionSchema.CONF_KNX_ROUTE_BACK, False), auto_reconnect=True, ) diff --git a/homeassistant/components/knx/config_flow.py b/homeassistant/components/knx/config_flow.py index 3fcf4069624..30071752731 100644 --- a/homeassistant/components/knx/config_flow.py +++ b/homeassistant/components/knx/config_flow.py @@ -121,6 +121,9 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ConnectionSchema.CONF_KNX_ROUTE_BACK: user_input[ ConnectionSchema.CONF_KNX_ROUTE_BACK ], + ConnectionSchema.CONF_KNX_LOCAL_IP: user_input.get( + ConnectionSchema.CONF_KNX_LOCAL_IP + ), CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, }, ) @@ -134,6 +137,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): vol.Required( ConnectionSchema.CONF_KNX_ROUTE_BACK, default=False ): vol.Coerce(bool), + vol.Optional(ConnectionSchema.CONF_KNX_LOCAL_IP): str, } return self.async_show_form( @@ -243,6 +247,9 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): **DEFAULT_ENTRY_DATA, CONF_HOST: config[CONF_KNX_TUNNELING][CONF_HOST], CONF_PORT: config[CONF_KNX_TUNNELING][CONF_PORT], + ConnectionSchema.CONF_KNX_LOCAL_IP: config[CONF_KNX_TUNNELING].get( + ConnectionSchema.CONF_KNX_LOCAL_IP + ), ConnectionSchema.CONF_KNX_ROUTE_BACK: config[CONF_KNX_TUNNELING][ ConnectionSchema.CONF_KNX_ROUTE_BACK ], @@ -299,6 +306,7 @@ class KNXOptionsFlowHandler(OptionsFlow): vol.Required( CONF_PORT, default=self.current_config.get(CONF_PORT, 3671) ): cv.port, + vol.Optional(ConnectionSchema.CONF_KNX_LOCAL_IP): str, vol.Required( ConnectionSchema.CONF_KNX_ROUTE_BACK, default=self.current_config.get( diff --git a/homeassistant/components/knx/strings.json b/homeassistant/components/knx/strings.json index ff191f7a4ce..7f770c25427 100644 --- a/homeassistant/components/knx/strings.json +++ b/homeassistant/components/knx/strings.json @@ -19,7 +19,8 @@ "port": "[%key:common::config_flow::data::port%]", "host": "[%key:common::config_flow::data::host%]", "individual_address": "Individual address for the connection", - "route_back": "Route Back / NAT Mode" + "route_back": "Route Back / NAT Mode", + "local_ip": "Local IP (leave empty if unsure)" } }, "routing": { @@ -55,7 +56,8 @@ "data": { "port": "[%key:common::config_flow::data::port%]", "host": "[%key:common::config_flow::data::host%]", - "route_back": "Route Back / NAT Mode" + "route_back": "Route Back / NAT Mode", + "local_ip": "Local IP (leave empty if unsure)" } } } diff --git a/homeassistant/components/knx/translations/en.json b/homeassistant/components/knx/translations/en.json index 80890538fbc..5320f0cfb03 100644 --- a/homeassistant/components/knx/translations/en.json +++ b/homeassistant/components/knx/translations/en.json @@ -12,6 +12,7 @@ "data": { "host": "Host", "individual_address": "Individual address for the connection", + "local_ip": "Local IP (leave empty if unsure)", "port": "Port", "route_back": "Route Back / NAT Mode" }, @@ -54,6 +55,7 @@ "tunnel": { "data": { "host": "Host", + "local_ip": "Local IP (leave empty if unsure)", "port": "Port", "route_back": "Route Back / NAT Mode" } diff --git a/tests/components/knx/test_config_flow.py b/tests/components/knx/test_config_flow.py index 2b792044fe5..ff1fc362aa5 100644 --- a/tests/components/knx/test_config_flow.py +++ b/tests/components/knx/test_config_flow.py @@ -132,6 +132,57 @@ async def test_tunneling_setup(hass: HomeAssistant) -> None: CONF_PORT: 3675, CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", ConnectionSchema.CONF_KNX_ROUTE_BACK: False, + ConnectionSchema.CONF_KNX_LOCAL_IP: None, + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_tunneling_setup_for_local_ip(hass: HomeAssistant) -> None: + """Test tunneling if only one gateway is found.""" + gateway = _gateway_descriptor("192.168.0.2", 3675) + with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways: + gateways.return_value = [gateway] + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert not result["errors"] + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, + }, + ) + await hass.async_block_till_done() + assert result2["type"] == RESULT_TYPE_FORM + assert result2["step_id"] == "manual_tunnel" + assert not result2["errors"] + + with patch( + "homeassistant.components.knx.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_HOST: "192.168.0.2", + CONF_PORT: 3675, + ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", + }, + ) + await hass.async_block_till_done() + assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["title"] == "Tunneling @ 192.168.0.2" + assert result3["data"] == { + **DEFAULT_ENTRY_DATA, + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, + CONF_HOST: "192.168.0.2", + CONF_PORT: 3675, + CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", + ConnectionSchema.CONF_KNX_ROUTE_BACK: False, + ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", } assert len(mock_setup_entry.mock_calls) == 1 @@ -188,6 +239,7 @@ async def test_tunneling_setup_for_multiple_found_gateways(hass: HomeAssistant) CONF_PORT: 3675, CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", ConnectionSchema.CONF_KNX_ROUTE_BACK: False, + ConnectionSchema.CONF_KNX_LOCAL_IP: None, } assert len(mock_setup_entry.mock_calls) == 1 @@ -261,6 +313,7 @@ async def test_import_config_tunneling(hass: HomeAssistant) -> None: CONF_KNX_TUNNELING: { CONF_HOST: "192.168.1.1", CONF_PORT: 3675, + ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", ConnectionSchema.CONF_KNX_ROUTE_BACK: True, }, } @@ -284,6 +337,7 @@ async def test_import_config_tunneling(hass: HomeAssistant) -> None: ConnectionSchema.CONF_KNX_STATE_UPDATER: True, ConnectionSchema.CONF_KNX_RATE_LIMIT: 20, ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, + ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, } @@ -509,6 +563,7 @@ async def test_tunneling_options_flow( CONF_HOST: "192.168.1.1", CONF_PORT: 3675, ConnectionSchema.CONF_KNX_ROUTE_BACK: True, + ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", }, ) @@ -526,6 +581,7 @@ async def test_tunneling_options_flow( CONF_HOST: "192.168.1.1", CONF_PORT: 3675, ConnectionSchema.CONF_KNX_ROUTE_BACK: True, + ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", } From fa5f524fdb64fa92f8c651864a699fa94ae7a49b Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 5 Dec 2021 00:14:27 +0000 Subject: [PATCH 0035/2644] [ci skip] Translation update --- .../components/balboa/translations/it.json | 28 +++++++++ .../binary_sensor/translations/de.json | 2 + .../binary_sensor/translations/en.json | 2 + .../binary_sensor/translations/hu.json | 2 + .../binary_sensor/translations/it.json | 2 + .../components/brunt/translations/it.json | 29 +++++++++ .../components/deconz/translations/ja.json | 11 +++- .../components/fronius/translations/bg.json | 7 ++- .../components/fronius/translations/fi.json | 10 +++ .../components/fronius/translations/hu.json | 7 ++- .../components/fronius/translations/it.json | 25 ++++++++ .../components/fronius/translations/ja.json | 7 ++- .../components/fronius/translations/nl.json | 7 ++- .../components/fronius/translations/tr.json | 7 ++- .../fronius/translations/zh-Hant.json | 7 ++- .../components/hue/translations/it.json | 12 +++- .../components/knx/translations/it.json | 63 +++++++++++++++++++ .../components/konnected/translations/it.json | 1 + .../components/mill/translations/it.json | 16 ++++- .../components/nam/translations/it.json | 21 ++++++- .../components/nest/translations/it.json | 13 +++- .../components/tailscale/translations/it.json | 26 ++++++++ .../components/tailscale/translations/ja.json | 6 +- .../tesla_wall_connector/translations/it.json | 30 +++++++++ .../components/tolo/translations/it.json | 23 +++++++ .../tolo/translations/select.it.json | 8 +++ .../tractive/translations/sensor.hu.json | 10 +++ .../tractive/translations/sensor.it.json | 10 +++ .../translations/hu.json | 6 ++ .../translations/it.json | 23 +++++++ .../tuya/translations/select.it.json | 4 ++ .../components/unifi/translations/it.json | 14 ++--- .../wled/translations/select.it.json | 9 +++ .../yale_smart_alarm/translations/it.json | 3 +- .../components/zha/translations/ja.json | 2 +- 35 files changed, 430 insertions(+), 23 deletions(-) create mode 100644 homeassistant/components/balboa/translations/it.json create mode 100644 homeassistant/components/brunt/translations/it.json create mode 100644 homeassistant/components/fronius/translations/fi.json create mode 100644 homeassistant/components/fronius/translations/it.json create mode 100644 homeassistant/components/knx/translations/it.json create mode 100644 homeassistant/components/tailscale/translations/it.json create mode 100644 homeassistant/components/tesla_wall_connector/translations/it.json create mode 100644 homeassistant/components/tolo/translations/it.json create mode 100644 homeassistant/components/tolo/translations/select.it.json create mode 100644 homeassistant/components/tractive/translations/sensor.hu.json create mode 100644 homeassistant/components/tractive/translations/sensor.it.json create mode 100644 homeassistant/components/trafikverket_weatherstation/translations/it.json create mode 100644 homeassistant/components/wled/translations/select.it.json diff --git a/homeassistant/components/balboa/translations/it.json b/homeassistant/components/balboa/translations/it.json new file mode 100644 index 00000000000..f9be4a44187 --- /dev/null +++ b/homeassistant/components/balboa/translations/it.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "host": "Host" + }, + "title": "Connettiti al dispositivo Wi-Fi Balboa" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "sync_time": "Mantieni sincronizzato l'orario del tuo client Balboa Spa con Home Assistant" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/de.json b/homeassistant/components/binary_sensor/translations/de.json index f6a124b5a0c..c84a91e32a5 100644 --- a/homeassistant/components/binary_sensor/translations/de.json +++ b/homeassistant/components/binary_sensor/translations/de.json @@ -84,6 +84,7 @@ "not_powered": "{entity_name} nicht mit Strom versorgt", "not_present": "{entity_name} nicht anwesend", "not_running": "{entity_name} wird nicht mehr ausgef\u00fchrt", + "not_tampered": "{entity_name} hat aufgeh\u00f6rt, Manipulationen zu erkennen", "not_unsafe": "{entity_name} wurde sicher", "occupied": "{entity_name} wurde besch\u00e4ftigt / besetzt", "opened": "{entity_name} ge\u00f6ffnet", @@ -94,6 +95,7 @@ "running": "{entity_name} ausgef\u00fchrt", "smoke": "{entity_name} detektiert Rauch", "sound": "{entity_name} detektiert Ger\u00e4usche", + "tampered": "{entity_name} hat begonnen, Manipulationen zu erkennen", "turned_off": "{entity_name} ausgeschaltet", "turned_on": "{entity_name} eingeschaltet", "unsafe": "{entity_name} ist unsicher", diff --git a/homeassistant/components/binary_sensor/translations/en.json b/homeassistant/components/binary_sensor/translations/en.json index 80da967cf8b..a094b001c5a 100644 --- a/homeassistant/components/binary_sensor/translations/en.json +++ b/homeassistant/components/binary_sensor/translations/en.json @@ -84,6 +84,7 @@ "not_powered": "{entity_name} not powered", "not_present": "{entity_name} not present", "not_running": "{entity_name} is no longer running", + "not_tampered": "{entity_name} stopped detecting tampering", "not_unsafe": "{entity_name} became safe", "occupied": "{entity_name} became occupied", "opened": "{entity_name} opened", @@ -94,6 +95,7 @@ "running": "{entity_name} started running", "smoke": "{entity_name} started detecting smoke", "sound": "{entity_name} started detecting sound", + "tampered": "{entity_name} started detecting tampering", "turned_off": "{entity_name} turned off", "turned_on": "{entity_name} turned on", "unsafe": "{entity_name} became unsafe", diff --git a/homeassistant/components/binary_sensor/translations/hu.json b/homeassistant/components/binary_sensor/translations/hu.json index 90fdcce7575..633a4bae372 100644 --- a/homeassistant/components/binary_sensor/translations/hu.json +++ b/homeassistant/components/binary_sensor/translations/hu.json @@ -84,6 +84,7 @@ "not_powered": "{entity_name} m\u00e1r nincs fesz\u00fcts\u00e9g alatt", "not_present": "{entity_name} m\u00e1r nincs jelen", "not_running": "{entity_name} m\u00e1r nem fut", + "not_tampered": "{entity_name} nem \u00e9szlel t\u00f6bb\u00e9 a manipul\u00e1l\u00e1st", "not_unsafe": "{entity_name} biztons\u00e1gos lett", "occupied": "{entity_name} foglalt lett", "opened": "{entity_name} ki lett nyitva", @@ -94,6 +95,7 @@ "running": "{entity_name} elindult", "smoke": "{entity_name} f\u00fcst\u00f6t \u00e9rz\u00e9kel", "sound": "{entity_name} hangot \u00e9rz\u00e9kel", + "tampered": "{entity_name} manipul\u00e1l\u00e1st \u00e9szlelt", "turned_off": "{entity_name} ki lett kapcsolva", "turned_on": "{entity_name} be lett kapcsolva", "unsafe": "{entity_name} m\u00e1r nem biztons\u00e1gos", diff --git a/homeassistant/components/binary_sensor/translations/it.json b/homeassistant/components/binary_sensor/translations/it.json index f0de143b244..6f526a12681 100644 --- a/homeassistant/components/binary_sensor/translations/it.json +++ b/homeassistant/components/binary_sensor/translations/it.json @@ -84,6 +84,7 @@ "not_powered": "{entity_name} non \u00e8 alimentato", "not_present": "{entity_name} non \u00e8 presente", "not_running": "{entity_name} non \u00e8 pi\u00f9 in funzione", + "not_tampered": "{entity_name} ha smesso di rilevare manomissioni", "not_unsafe": "{entity_name} \u00e8 diventato sicuro", "occupied": "{entity_name} \u00e8 diventato occupato", "opened": "{entity_name} \u00e8 aperto", @@ -94,6 +95,7 @@ "running": "{entity_name} ha iniziato a funzionare", "smoke": "{entity_name} ha iniziato la rilevazione di fumo", "sound": "{entity_name} ha iniziato il rilevamento del suono", + "tampered": "{entity_name} ha iniziato a rilevare manomissioni", "turned_off": "{entity_name} disattivato", "turned_on": "{entity_name} attivato", "unsafe": "{entity_name} diventato non sicuro", diff --git a/homeassistant/components/brunt/translations/it.json b/homeassistant/components/brunt/translations/it.json new file mode 100644 index 00000000000..0a928af664d --- /dev/null +++ b/homeassistant/components/brunt/translations/it.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "Reinserisci la password per: {username}", + "title": "Autenticare nuovamente l'integrazione" + }, + "user": { + "data": { + "password": "Password", + "username": "Nome utente" + }, + "title": "Configura la tua integrazione Brunt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/translations/ja.json b/homeassistant/components/deconz/translations/ja.json index 4b33589ddfc..a0c7e7b9cd4 100644 --- a/homeassistant/components/deconz/translations/ja.json +++ b/homeassistant/components/deconz/translations/ja.json @@ -66,10 +66,13 @@ "remote_awakened": "\u30c7\u30d0\u30a4\u30b9\u304c\u76ee\u899a\u3081\u305f", "remote_button_double_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u3092\u30c0\u30d6\u30eb\u30af\u30ea\u30c3\u30af", "remote_button_long_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u3092\u62bc\u3057\u7d9a\u3051\u308b", + "remote_button_long_release": "\u9577\u62bc\u3057\u3059\u308b\u3068 \"{subtype}\" \u30dc\u30bf\u30f3\u304c\u30ea\u30ea\u30fc\u30b9\u3055\u308c\u308b", "remote_button_quadruple_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u30924\u56de(quadruple)\u30af\u30ea\u30c3\u30af", "remote_button_quintuple_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u30925\u56de(quintuple)\u30af\u30ea\u30c3\u30af", - "remote_button_rotated_fast": "\u30dc\u30bf\u30f3\u304c\u9ad8\u901f\u56de\u8ee2\u3059\u308b \"{subtype}\"", + "remote_button_rotated": "\u30dc\u30bf\u30f3\u304c\u56de\u8ee2\u3057\u305f \"{subtype}\"", + "remote_button_rotated_fast": "\u30dc\u30bf\u30f3\u304c\u9ad8\u901f\u56de\u8ee2\u3057\u305f \"{subtype}\"", "remote_button_short_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u304c\u62bc\u3055\u308c\u307e\u3057\u305f\u3002", + "remote_button_short_release": "\"{subtype}\" \u30dc\u30bf\u30f3\u304c\u30ea\u30ea\u30fc\u30b9\u3055\u308c\u307e\u3057\u305f", "remote_button_triple_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u30923\u56de\u30af\u30ea\u30c3\u30af", "remote_double_tap": "\u30c7\u30d0\u30a4\u30b9 \"{subtype}\" \u304c\u30c0\u30d6\u30eb\u30bf\u30c3\u30d7\u3055\u308c\u307e\u3057\u305f", "remote_double_tap_any_side": "\u30c7\u30d0\u30a4\u30b9\u306e\u3044\u305a\u308c\u304b\u306e\u9762\u3092\u30c0\u30d6\u30eb\u30bf\u30c3\u30d7\u3057\u305f", @@ -78,6 +81,12 @@ "remote_flip_90_degrees": "\u30c7\u30d0\u30a4\u30b9\u304c90\u5ea6\u53cd\u8ee2", "remote_gyro_activated": "\u30c7\u30d0\u30a4\u30b9\u304c\u63fa\u308c\u308b", "remote_moved_any_side": "\u30c7\u30d0\u30a4\u30b9\u304c\u4efb\u610f\u306e\u9762\u3092\u4e0a\u306b\u3057\u3066\u79fb\u52d5\u3057\u305f", + "remote_rotate_from_side_1": "\u30c7\u30d0\u30a4\u30b9\u304c\u3001\"side 1\" \u304b\u3089 \"{subtype} \"\u306b\u56de\u8ee2\u3057\u305f", + "remote_rotate_from_side_2": "\u30c7\u30d0\u30a4\u30b9\u304c\u3001\"side 2\" \u304b\u3089 \"{subtype} \"\u306b\u56de\u8ee2\u3057\u305f", + "remote_rotate_from_side_3": "\u30c7\u30d0\u30a4\u30b9\u304c\u3001\"side 3\" \u304b\u3089 \"{subtype} \"\u306b\u56de\u8ee2\u3057\u305f", + "remote_rotate_from_side_4": "\u30c7\u30d0\u30a4\u30b9\u304c\u3001\"side 4\" \u304b\u3089 \"{subtype} \"\u306b\u56de\u8ee2\u3057\u305f", + "remote_rotate_from_side_5": "\u30c7\u30d0\u30a4\u30b9\u304c\u3001\"side 5\" \u304b\u3089 \"{subtype} \"\u306b\u56de\u8ee2\u3057\u305f", + "remote_rotate_from_side_6": "\u30c7\u30d0\u30a4\u30b9\u304c\u3001\"side 6\" \u304b\u3089 \"{subtype} \"\u306b\u56de\u8ee2\u3057\u305f", "remote_turned_clockwise": "\u30c7\u30d0\u30a4\u30b9\u304c\u6642\u8a08\u56de\u308a\u306b", "remote_turned_counter_clockwise": "\u30c7\u30d0\u30a4\u30b9\u304c\u53cd\u6642\u8a08\u56de\u308a\u306b" } diff --git a/homeassistant/components/fronius/translations/bg.json b/homeassistant/components/fronius/translations/bg.json index cbf1e2ae7c9..4c388f2ad58 100644 --- a/homeassistant/components/fronius/translations/bg.json +++ b/homeassistant/components/fronius/translations/bg.json @@ -1,13 +1,18 @@ { "config": { "abort": { - "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "invalid_host": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0438\u043c\u0435 \u043d\u0430 \u0445\u043e\u0441\u0442 \u0438\u043b\u0438 IP \u0430\u0434\u0440\u0435\u0441" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, + "flow_title": "{device}", "step": { + "confirm_discovery": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u0435 {device} \u043a\u044a\u043c Home Assistant?" + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442" diff --git a/homeassistant/components/fronius/translations/fi.json b/homeassistant/components/fronius/translations/fi.json new file mode 100644 index 00000000000..c8dd1e939ce --- /dev/null +++ b/homeassistant/components/fronius/translations/fi.json @@ -0,0 +1,10 @@ +{ + "config": { + "flow_title": "{device}", + "step": { + "confirm_discovery": { + "description": "Haluatko lis\u00e4t\u00e4 laitteen {device} Home Assistantiin?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fronius/translations/hu.json b/homeassistant/components/fronius/translations/hu.json index fc461b66121..4307a00af0e 100644 --- a/homeassistant/components/fronius/translations/hu.json +++ b/homeassistant/components/fronius/translations/hu.json @@ -1,13 +1,18 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "invalid_host": "\u00c9rv\u00e9nytelen hosztn\u00e9v vagy IP-c\u00edm" }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, + "flow_title": "{device}", "step": { + "confirm_discovery": { + "description": "Szeretn\u00e9 hozz\u00e1adni Home Assistanthoz: {device}?" + }, "user": { "data": { "host": "C\u00edm" diff --git a/homeassistant/components/fronius/translations/it.json b/homeassistant/components/fronius/translations/it.json new file mode 100644 index 00000000000..271754bfc29 --- /dev/null +++ b/homeassistant/components/fronius/translations/it.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "invalid_host": "Nome host o indirizzo IP non valido" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "unknown": "Errore imprevisto" + }, + "flow_title": "{device}", + "step": { + "confirm_discovery": { + "description": "Vuoi aggiungere {device} a Home Assistant?" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Configurare l'indirizzo IP o il nome host locale del proprio dispositivo Fronius.", + "title": "Fronius SolarNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fronius/translations/ja.json b/homeassistant/components/fronius/translations/ja.json index f5d2a03874e..07c7b0c3ed4 100644 --- a/homeassistant/components/fronius/translations/ja.json +++ b/homeassistant/components/fronius/translations/ja.json @@ -1,13 +1,18 @@ { "config": { "abort": { - "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "invalid_host": "\u7121\u52b9\u306a\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, + "flow_title": "{device}", "step": { + "confirm_discovery": { + "description": "{device} \u3092Home Assistant\u306b\u8ffd\u52a0\u3057\u307e\u3059\u304b\uff1f" + }, "user": { "data": { "host": "\u30db\u30b9\u30c8" diff --git a/homeassistant/components/fronius/translations/nl.json b/homeassistant/components/fronius/translations/nl.json index a6aa710148b..d71683ad922 100644 --- a/homeassistant/components/fronius/translations/nl.json +++ b/homeassistant/components/fronius/translations/nl.json @@ -1,13 +1,18 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd", + "invalid_host": "Ongeldige hostnaam of IP-adres" }, "error": { "cannot_connect": "Kan geen verbinding maken", "unknown": "Onverwachte fout" }, + "flow_title": "{device}", "step": { + "confirm_discovery": { + "description": "Wilt u {device} toevoegen aan Home Assistant?" + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/fronius/translations/tr.json b/homeassistant/components/fronius/translations/tr.json index 4d5b9a173db..90cdbb00deb 100644 --- a/homeassistant/components/fronius/translations/tr.json +++ b/homeassistant/components/fronius/translations/tr.json @@ -1,13 +1,18 @@ { "config": { "abort": { - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "invalid_host": "Ge\u00e7ersiz ana bilgisayar ad\u0131 veya IP adresi" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", "unknown": "Beklenmeyen hata" }, + "flow_title": "{device}", "step": { + "confirm_discovery": { + "description": "Home Assistant'a {device} eklemek istiyor musunuz?" + }, "user": { "data": { "host": "Sunucu" diff --git a/homeassistant/components/fronius/translations/zh-Hant.json b/homeassistant/components/fronius/translations/zh-Hant.json index 18134514d38..f7e507f1835 100644 --- a/homeassistant/components/fronius/translations/zh-Hant.json +++ b/homeassistant/components/fronius/translations/zh-Hant.json @@ -1,13 +1,18 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "invalid_host": "\u7121\u6548\u4e3b\u6a5f\u540d\u7a31\u6216 IP \u4f4d\u5740" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, + "flow_title": "{device}", "step": { + "confirm_discovery": { + "description": "\u662f\u5426\u8981\u5c07 {device} \u65b0\u589e\u81f3 Home Assistant\uff1f" + }, "user": { "data": { "host": "\u4e3b\u6a5f\u7aef" diff --git a/homeassistant/components/hue/translations/it.json b/homeassistant/components/hue/translations/it.json index f49e64b9b68..29298f736c3 100644 --- a/homeassistant/components/hue/translations/it.json +++ b/homeassistant/components/hue/translations/it.json @@ -35,6 +35,10 @@ }, "device_automation": { "trigger_subtype": { + "1": "Primo pulsante", + "2": "Secondo pulsante", + "3": "Terzo pulsante", + "4": "Quarto pulsante", "button_1": "Primo pulsante", "button_2": "Secondo pulsante", "button_3": "Terzo pulsante", @@ -47,11 +51,16 @@ "turn_on": "Acceso" }, "trigger_type": { + "double_short_release": "Entrambi \"{subtype}\" rilasciati", + "initial_press": "Pulsante \"{subtype}\" premuto inizialmente", + "long_release": "Pulsante \"{subtype}\" rilasciato dopo una pressione prolungata", "remote_button_long_release": "Pulsante \"{subtype}\" rilasciato dopo una lunga pressione", "remote_button_short_press": "Pulsante \"{subtype}\" premuto", "remote_button_short_release": "Pulsante \"{subtype}\" rilasciato", "remote_double_button_long_press": "Entrambi i \"{subtype}\" rilasciati dopo una lunga pressione", - "remote_double_button_short_press": "Entrambi i \"{subtype}\" rilasciati" + "remote_double_button_short_press": "Entrambi i \"{subtype}\" rilasciati", + "repeat": "Pulsante \"{subtype}\" tenuto premuto", + "short_release": "Pulsante \"{subtype}\" rilasciato dopo una breve pressione" } }, "options": { @@ -59,6 +68,7 @@ "init": { "data": { "allow_hue_groups": "Consenti gruppi Hue", + "allow_hue_scenes": "Consenti scene Tonalit\u00e0", "allow_unreachable": "Consentire alle lampadine irraggiungibili di segnalare correttamente il loro stato" } } diff --git a/homeassistant/components/knx/translations/it.json b/homeassistant/components/knx/translations/it.json new file mode 100644 index 00000000000..0659b928523 --- /dev/null +++ b/homeassistant/components/knx/translations/it.json @@ -0,0 +1,63 @@ +{ + "config": { + "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "step": { + "manual_tunnel": { + "data": { + "host": "Host", + "individual_address": "Indirizzo individuale per la connessione", + "port": "Porta", + "route_back": "Torna indietro / Modalit\u00e0 NAT" + }, + "description": "Inserisci le informazioni di connessione del tuo dispositivo di tunneling." + }, + "routing": { + "data": { + "individual_address": "Indirizzo individuale per la connessione di routing", + "multicast_group": "Il gruppo multicast utilizzato per il routing", + "multicast_port": "La porta multicast usata per il routing" + }, + "description": "Si prega di configurare le opzioni di routing." + }, + "tunnel": { + "data": { + "gateway": "Connessione tunnel KNX" + }, + "description": "Seleziona un gateway dall'elenco." + }, + "type": { + "data": { + "connection_type": "Tipo di connessione KNX" + }, + "description": "Inserisci il tipo di connessione che dovremmo usare per la tua connessione KNX.\n AUTOMATICO - L'integrazione si occupa della connettivit\u00e0 al tuo Bus KNX eseguendo una scansione del gateway.\n TUNNELING - L'integrazione si collegher\u00e0 al bus KNX tramite tunneling.\n ROUTING - L'integrazione si collegher\u00e0 al bus KNX tramite routing." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "connection_type": "Tipo di connessione KNX", + "individual_address": "Indirizzo individuale predefinito", + "multicast_group": "Gruppo multicast utilizzato per il routing e il rilevamento", + "multicast_port": "Porta multicast utilizzata per il routing e il rilevamento", + "rate_limit": "Numero massimo di telegrammi in uscita al secondo", + "state_updater": "Abilita globalmente la lettura degli stati dal bus KNX" + } + }, + "tunnel": { + "data": { + "host": "Host", + "port": "Porta", + "route_back": "Torna indietro / Modalit\u00e0 NAT" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/translations/it.json b/homeassistant/components/konnected/translations/it.json index 6b41217dca4..138ccb9a7c5 100644 --- a/homeassistant/components/konnected/translations/it.json +++ b/homeassistant/components/konnected/translations/it.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "cannot_connect": "Impossibile connettersi", "not_konn_panel": "Non \u00e8 un dispositivo Konnected.io riconosciuto", "unknown": "Errore imprevisto" }, diff --git a/homeassistant/components/mill/translations/it.json b/homeassistant/components/mill/translations/it.json index 5dc8b6ec2f1..fa19ebc4cbd 100644 --- a/homeassistant/components/mill/translations/it.json +++ b/homeassistant/components/mill/translations/it.json @@ -7,11 +7,25 @@ "cannot_connect": "Impossibile connettersi" }, "step": { - "user": { + "cloud": { "data": { "password": "Password", "username": "Nome utente" } + }, + "local": { + "data": { + "ip_address": "Indirizzo IP" + }, + "description": "Indirizzo IP locale del dispositivo." + }, + "user": { + "data": { + "connection_type": "Seleziona il tipo di connessione", + "password": "Password", + "username": "Nome utente" + }, + "description": "Seleziona il tipo di connessione. Locale richiede riscaldatori di terza generazione" } } } diff --git a/homeassistant/components/nam/translations/it.json b/homeassistant/components/nam/translations/it.json index 9a208cbfd3c..e82a0db8ddf 100644 --- a/homeassistant/components/nam/translations/it.json +++ b/homeassistant/components/nam/translations/it.json @@ -2,17 +2,34 @@ "config": { "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", - "device_unsupported": "Il dispositivo non \u00e8 supportato." + "device_unsupported": "Il dispositivo non \u00e8 supportato.", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", + "reauth_unsuccessful": "La riautenticazione non \u00e8 andata a buon fine, rimuovere l'integrazione e configurarla di nuovo." }, "error": { "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", "unknown": "Errore imprevisto" }, - "flow_title": "{name}", + "flow_title": "{host}", "step": { "confirm_discovery": { "description": "Vuoi configurare Nettigo Air Monitor su {host} ?" }, + "credentials": { + "data": { + "password": "Password", + "username": "Nome utente" + }, + "description": "Inserisci il nome utente e la password." + }, + "reauth_confirm": { + "data": { + "password": "Password", + "username": "Nome utente" + }, + "description": "Inserisci il nome utente e la password corretti per l'host: {host}" + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/nest/translations/it.json b/homeassistant/components/nest/translations/it.json index 6227dae21db..24318d779a6 100644 --- a/homeassistant/components/nest/translations/it.json +++ b/homeassistant/components/nest/translations/it.json @@ -2,6 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", + "invalid_access_token": "Token di accesso non valido", "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.", "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})", "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", @@ -12,10 +13,13 @@ "default": "Autenticazione riuscita" }, "error": { + "bad_project_id": "Inserisci un ID di progetto Cloud valido (controlla Cloud Console)", "internal_error": "Errore interno nella convalida del codice", "invalid_pin": "Codice PIN non valido", + "subscriber_error": "Errore di abbonato sconosciuto, vedere i registri", "timeout": "Tempo scaduto per l'inserimento del codice di convalida", - "unknown": "Errore imprevisto" + "unknown": "Errore imprevisto", + "wrong_project_id": "Inserisci un ID di progetto Cloud valido (trovato ID di progetto di accesso al dispositivo)" }, "step": { "auth": { @@ -42,6 +46,13 @@ "pick_implementation": { "title": "Scegli il metodo di autenticazione" }, + "pubsub": { + "data": { + "cloud_project_id": "ID del progetto Google Cloud" + }, + "description": "Visita la [Cloud Console]({url}) per trovare il tuo ID di progetto Google Cloud.", + "title": "Configura Google Cloud" + }, "reauth_confirm": { "description": "L'integrazione di Nest deve autenticare nuovamente il tuo account", "title": "Autenticare nuovamente l'integrazione" diff --git a/homeassistant/components/tailscale/translations/it.json b/homeassistant/components/tailscale/translations/it.json new file mode 100644 index 00000000000..0dfe90b8f2e --- /dev/null +++ b/homeassistant/components/tailscale/translations/it.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Chiave API" + }, + "description": "I token API di Tailscale sono validi per 90 giorni. Puoi creare una nuova chiave API Tailscale su https://login.tailscale.com/admin/settings/authkeys." + }, + "user": { + "data": { + "api_key": "Chiave API", + "tailnet": "Tailnet" + }, + "description": "Per autenticarti con Tailscale dovrai creare una chiave API su https://login.tailscale.com/admin/settings/authkeys. \n\nUna Tailnet \u00e8 il nome della tua rete Tailscale. Puoi trovarlo nell'angolo in alto a sinistra nel pannello di amministrazione di Tailscale (accanto al logo Tailscale)." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tailscale/translations/ja.json b/homeassistant/components/tailscale/translations/ja.json index bed4a13e8d3..0bfa7bc3dd7 100644 --- a/homeassistant/components/tailscale/translations/ja.json +++ b/homeassistant/components/tailscale/translations/ja.json @@ -11,13 +11,15 @@ "reauth_confirm": { "data": { "api_key": "API\u30ad\u30fc" - } + }, + "description": "Tailscale API tokens\u306f\u300190\u65e5\u9593\u6709\u52b9\u3067\u3059\u3002https://login.tailscale.com/admin/settings/authkeys \u3067\u65b0\u3057\u3044Tailscale API\u30ad\u30fc\u3092\u4f5c\u6210\u3067\u304d\u307e\u3059\u3002" }, "user": { "data": { "api_key": "API\u30ad\u30fc", "tailnet": "Tailnet" - } + }, + "description": "Tailscale\u3067\u8a8d\u8a3c\u3059\u308b\u306b\u306f\u3001https://login.tailscale.com/admin/settings/authkeys \u3067\u3001API\u30ad\u30fc\u3092\u4f5c\u6210\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\nTailnet\u3068\u306f\u3001\u3042\u306a\u305f\u306eTailscale network\u306e\u540d\u524d\u3067\u3059\u3002\u3053\u306e\u30a2\u30a4\u30b3\u30f3\u306f\u3001Tailscale Admin Panel\u306e\u5de6\u4e0a\u9685(Tailscale\u30ed\u30b4\u306e\u6a2a)\u306b\u3042\u308a\u307e\u3059\u3002" } } } diff --git a/homeassistant/components/tesla_wall_connector/translations/it.json b/homeassistant/components/tesla_wall_connector/translations/it.json new file mode 100644 index 00000000000..5f79a0ee9e0 --- /dev/null +++ b/homeassistant/components/tesla_wall_connector/translations/it.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "unknown": "Errore imprevisto" + }, + "flow_title": "{serial_number} ({host})", + "step": { + "user": { + "data": { + "host": "Host" + }, + "title": "Configura Tesla Wall Connector" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Frequenza di aggiornamento" + }, + "title": "Configura le opzioni per Tesla Wall Connector" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/it.json b/homeassistant/components/tolo/translations/it.json new file mode 100644 index 00000000000..28220e7ab99 --- /dev/null +++ b/homeassistant/components/tolo/translations/it.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "no_devices_found": "Nessun dispositivo trovato sulla rete" + }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Vuoi iniziare la configurazione?" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Inserisci il nome host o l'indirizzo IP del tuo dispositivo TOLO Sauna." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/select.it.json b/homeassistant/components/tolo/translations/select.it.json new file mode 100644 index 00000000000..8b47b1ca904 --- /dev/null +++ b/homeassistant/components/tolo/translations/select.it.json @@ -0,0 +1,8 @@ +{ + "state": { + "tolo__lamp_mode": { + "automatic": "automatico", + "manual": "manuale" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/sensor.hu.json b/homeassistant/components/tractive/translations/sensor.hu.json new file mode 100644 index 00000000000..07fe8b9bb01 --- /dev/null +++ b/homeassistant/components/tractive/translations/sensor.hu.json @@ -0,0 +1,10 @@ +{ + "state": { + "tractive__tracker_state": { + "not_reporting": "Nem jelentkezik", + "operational": "\u00dczemk\u00e9pes", + "system_shutdown_user": "Rendszerle\u00e1ll\u00edt\u00e1s felhaszn\u00e1l\u00f3ja", + "system_startup": "Rendszerind\u00edt\u00e1s" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/sensor.it.json b/homeassistant/components/tractive/translations/sensor.it.json new file mode 100644 index 00000000000..153a7964ee8 --- /dev/null +++ b/homeassistant/components/tractive/translations/sensor.it.json @@ -0,0 +1,10 @@ +{ + "state": { + "tractive__tracker_state": { + "not_reporting": "Non segnalante", + "operational": "Operativo", + "system_shutdown_user": "Spegnimento del sistema utente", + "system_startup": "Avvio del sistema" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_weatherstation/translations/hu.json b/homeassistant/components/trafikverket_weatherstation/translations/hu.json index 27c8d290af4..c4f830b20fa 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/hu.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/hu.json @@ -3,6 +3,12 @@ "abort": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "invalid_station": "Nem tal\u00e1lhat\u00f3 a megadott nev\u0171 meteorol\u00f3giai \u00e1llom\u00e1s", + "more_stations": "T\u00f6bb meteorol\u00f3giai \u00e1llom\u00e1s tal\u00e1lhat\u00f3 a megadott n\u00e9vvel" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/trafikverket_weatherstation/translations/it.json b/homeassistant/components/trafikverket_weatherstation/translations/it.json new file mode 100644 index 00000000000..a073a528586 --- /dev/null +++ b/homeassistant/components/trafikverket_weatherstation/translations/it.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "invalid_station": "Impossibile trovare una stazione meteorologica con il nome specificato", + "more_stations": "Trovate pi\u00f9 stazioni meteorologiche con il nome specificato" + }, + "step": { + "user": { + "data": { + "api_key": "Chiave API", + "conditions": "Condizioni monitorate", + "name": "Nome utente", + "station": "Stazione" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/select.it.json b/homeassistant/components/tuya/translations/select.it.json index 74410c0a593..c42d3208b33 100644 --- a/homeassistant/components/tuya/translations/select.it.json +++ b/homeassistant/components/tuya/translations/select.it.json @@ -14,6 +14,10 @@ "0": "Bassa sensibilit\u00e0", "1": "Alta sensibilit\u00e0" }, + "tuya__fingerbot_mode": { + "click": "Spingere", + "switch": "Interruttore" + }, "tuya__ipc_work_mode": { "0": "Modalit\u00e0 a basso consumo", "1": "Modalit\u00e0 di lavoro continua" diff --git a/homeassistant/components/unifi/translations/it.json b/homeassistant/components/unifi/translations/it.json index 00672b65b4e..389b98a26d9 100644 --- a/homeassistant/components/unifi/translations/it.json +++ b/homeassistant/components/unifi/translations/it.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Il sito del Controller \u00e8 gi\u00e0 configurato", + "already_configured": "Il sito della rete UniFi \u00e8 gi\u00e0 configurato", "configuration_updated": "Configurazione aggiornata.", "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, @@ -21,7 +21,7 @@ "username": "Nome utente", "verify_ssl": "Verificare il certificato SSL" }, - "title": "Configura l'UniFi Controller" + "title": "Configura la rete UniFi" } } }, @@ -34,19 +34,19 @@ "poe_clients": "Consentire il controllo POE dei client" }, "description": "Configurare i controlli client \n\nCreare interruttori per i numeri di serie dei quali si desidera controllare l'accesso alla rete.", - "title": "Opzioni UniFi 2/3" + "title": "Opzioni di rete UniFi 2/3" }, "device_tracker": { "data": { "detection_time": "Tempo in secondi dall'ultima volta che viene visto fino a quando non \u00e8 considerato lontano", - "ignore_wired_bug": "Disattivare la logica dei bug cablati UniFi", + "ignore_wired_bug": "Disabilita la logica dei bug cablati di rete UniFi", "ssid_filter": "Selezionare gli SSID su cui tracciare i client wireless", "track_clients": "Traccia i client di rete", "track_devices": "Tracciare i dispositivi di rete (dispositivi Ubiquiti)", "track_wired_clients": "Includi i client di rete cablata" }, "description": "Configurare il tracciamento del dispositivo", - "title": "Opzioni UniFi 1/3" + "title": "Opzioni di rete UniFi 1/3" }, "init": { "data": { @@ -60,7 +60,7 @@ "track_clients": "Traccia i client di rete", "track_devices": "Tracciare i dispositivi di rete (dispositivi Ubiquiti)" }, - "description": "Configurare l'integrazione UniFi" + "description": "Configura l'integrazione della rete UniFi" }, "statistics_sensors": { "data": { @@ -68,7 +68,7 @@ "allow_uptime_sensors": "Sensori di tempo di funzionamento per i client di rete" }, "description": "Configurare i sensori delle statistiche", - "title": "Opzioni UniFi 3/3" + "title": "Opzioni di rete UniFi 3/3" } } } diff --git a/homeassistant/components/wled/translations/select.it.json b/homeassistant/components/wled/translations/select.it.json new file mode 100644 index 00000000000..8cd961b9c87 --- /dev/null +++ b/homeassistant/components/wled/translations/select.it.json @@ -0,0 +1,9 @@ +{ + "state": { + "wled__live_override": { + "0": "Spento", + "1": "Acceso", + "2": "Fino al riavvio del dispositivo" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yale_smart_alarm/translations/it.json b/homeassistant/components/yale_smart_alarm/translations/it.json index 2f510e46396..bc08163c1a2 100644 --- a/homeassistant/components/yale_smart_alarm/translations/it.json +++ b/homeassistant/components/yale_smart_alarm/translations/it.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "L'account \u00e8 gi\u00e0 configurato" + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "invalid_auth": "Autenticazione non valida" diff --git a/homeassistant/components/zha/translations/ja.json b/homeassistant/components/zha/translations/ja.json index 35e8933220a..38231524232 100644 --- a/homeassistant/components/zha/translations/ja.json +++ b/homeassistant/components/zha/translations/ja.json @@ -76,7 +76,7 @@ "device_flipped": "\u30c7\u30d0\u30a4\u30b9\u304c\u53cd\u8ee2\u3057\u307e\u3057\u305f \"{subtype}\"", "device_knocked": "\u30c7\u30d0\u30a4\u30b9\u304c\u30ce\u30c3\u30af\u3055\u308c\u307e\u3057\u305f \"{subtype}\"", "device_offline": "\u30c7\u30d0\u30a4\u30b9\u304c\u30aa\u30d5\u30e9\u30a4\u30f3", - "device_rotated": "\u30c7\u30d0\u30a4\u30b9\u304c\u56de\u8ee2\u3057\u307e\u3057\u305f \"{subtype}\"", + "device_rotated": "\u30c7\u30d0\u30a4\u30b9\u304c\u56de\u8ee2\u3057\u305f \"{subtype}\"", "device_shaken": "\u30c7\u30d0\u30a4\u30b9\u304c\u63fa\u308c\u308b", "device_slid": "\u30c7\u30d0\u30a4\u30b9 \u30b9\u30e9\u30a4\u30c9 \"{subtype}\"", "device_tilted": "\u30c7\u30d0\u30a4\u30b9\u304c\u50be\u3044\u3066\u3044\u308b", From 08003c5287c02fd285256bd8af2a92aa75482648 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 5 Dec 2021 00:39:18 -0800 Subject: [PATCH 0036/2644] Improve nest media source event timestamp display (#61027) Drop subsecond text from the nest media source event timestamp display, using a common date/time template string. --- homeassistant/components/nest/media_source.py | 4 ++-- tests/components/nest/test_media_source.py | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/nest/media_source.py b/homeassistant/components/nest/media_source.py index e21be20380a..af51c296e43 100644 --- a/homeassistant/components/nest/media_source.py +++ b/homeassistant/components/nest/media_source.py @@ -46,7 +46,7 @@ from homeassistant.components.nest.const import DATA_SUBSCRIBER, DOMAIN from homeassistant.components.nest.device_info import NestDeviceInfo from homeassistant.components.nest.events import MEDIA_SOURCE_EVENT_TITLE_MAP from homeassistant.core import HomeAssistant -import homeassistant.util.dt as dt_util +from homeassistant.helpers.template import DATE_STR_FORMAT _LOGGER = logging.getLogger(__name__) @@ -250,7 +250,7 @@ def _browse_event( media_content_type=MEDIA_TYPE_IMAGE, title=CLIP_TITLE_FORMAT.format( event_name=MEDIA_SOURCE_EVENT_TITLE_MAP.get(event.event_type, "Event"), - event_time=dt_util.as_local(event.timestamp), + event_time=event.timestamp.strftime(DATE_STR_FORMAT), ), can_play=True, can_expand=False, diff --git a/tests/components/nest/test_media_source.py b/tests/components/nest/test_media_source.py index 4cda781ebeb..67d6ba2f229 100644 --- a/tests/components/nest/test_media_source.py +++ b/tests/components/nest/test_media_source.py @@ -17,6 +17,7 @@ from homeassistant.components.media_player.errors import BrowseError from homeassistant.components.media_source import const from homeassistant.components.media_source.error import Unresolvable from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.template import DATE_STR_FORMAT import homeassistant.util.dt as dt_util from .common import async_setup_sdm_platform @@ -211,7 +212,7 @@ async def test_camera_event(hass, auth, hass_client): assert len(browse.children) == 1 assert browse.children[0].domain == DOMAIN assert browse.children[0].identifier == f"{device.id}/{event_id}" - event_timestamp_string = event_timestamp.isoformat(timespec="seconds", sep=" ") + event_timestamp_string = event_timestamp.strftime(DATE_STR_FORMAT) assert browse.children[0].title == f"Person @ {event_timestamp_string}" assert not browse.children[0].can_expand assert len(browse.children[0].children) == 0 @@ -291,7 +292,7 @@ async def test_event_order(hass, auth): assert len(browse.children) == 2 assert browse.children[0].domain == DOMAIN assert browse.children[0].identifier == f"{device.id}/{event_id2}" - event_timestamp_string = event_timestamp2.isoformat(timespec="seconds", sep=" ") + event_timestamp_string = event_timestamp2.strftime(DATE_STR_FORMAT) assert browse.children[0].title == f"Motion @ {event_timestamp_string}" assert not browse.children[0].can_expand @@ -299,7 +300,7 @@ async def test_event_order(hass, auth): assert browse.children[1].domain == DOMAIN assert browse.children[1].identifier == f"{device.id}/{event_id1}" - event_timestamp_string = event_timestamp1.isoformat(timespec="seconds", sep=" ") + event_timestamp_string = event_timestamp1.strftime(DATE_STR_FORMAT) assert browse.children[1].title == f"Person @ {event_timestamp_string}" assert not browse.children[1].can_expand @@ -435,7 +436,7 @@ async def test_camera_event_clip_preview(hass, auth, hass_client): assert len(browse.children) == 1 assert browse.children[0].domain == DOMAIN actual_event_id = browse.children[0].identifier - event_timestamp_string = event_timestamp.isoformat(timespec="seconds", sep=" ") + event_timestamp_string = event_timestamp.strftime(DATE_STR_FORMAT) assert browse.children[0].title == f"Event @ {event_timestamp_string}" assert not browse.children[0].can_expand assert len(browse.children[0].children) == 0 From e34d982bdbec748965413fb372d243b57701653d Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 5 Dec 2021 01:50:47 -0800 Subject: [PATCH 0037/2644] Bump nest to version 0.4.2 (#61036) --- homeassistant/components/nest/manifest.json | 2 +- homeassistant/components/nest/media_source.py | 11 ++++++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 94b2c338528..3a4f64877d2 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["ffmpeg", "http", "media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.0"], + "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.2"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/homeassistant/components/nest/media_source.py b/homeassistant/components/nest/media_source.py index af51c296e43..140489bd63a 100644 --- a/homeassistant/components/nest/media_source.py +++ b/homeassistant/components/nest/media_source.py @@ -137,7 +137,7 @@ class NestMediaSource(MediaSource): raise Unresolvable( "Unable to find device with identifier: %s" % item.identifier ) - events = _get_events(device) + events = await _get_events(device) if media_id.event_id not in events: raise Unresolvable( "Unable to find event with identifier: %s" % item.identifier @@ -180,7 +180,7 @@ class NestMediaSource(MediaSource): # Browse a specific device and return child events browse_device = _browse_device(media_id, device) browse_device.children = [] - events = _get_events(device) + events = await _get_events(device) for child_event in events.values(): event_id = MediaId(media_id.device_id, child_event.event_id) browse_device.children.append( @@ -189,7 +189,7 @@ class NestMediaSource(MediaSource): return browse_device # Browse a specific event - events = _get_events(device) + events = await _get_events(device) if not (event := events.get(media_id.event_id)): raise BrowseError( "Unable to find event with identiifer: %s" % item.identifier @@ -201,9 +201,10 @@ class NestMediaSource(MediaSource): return await get_media_source_devices(self.hass) -def _get_events(device: Device) -> Mapping[str, ImageEventBase]: +async def _get_events(device: Device) -> Mapping[str, ImageEventBase]: """Return relevant events for the specified device.""" - return OrderedDict({e.event_id: e for e in device.event_media_manager.events}) + events = await device.event_media_manager.async_events() + return OrderedDict({e.event_id: e for e in events}) def _browse_root() -> BrowseMediaSource: diff --git a/requirements_all.txt b/requirements_all.txt index 765b67088f2..0644f704fd4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -738,7 +738,7 @@ google-cloud-pubsub==2.1.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==0.4.0 +google-nest-sdm==0.4.2 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e8b2c62eeb4..09e78d41032 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -461,7 +461,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.1.0 # homeassistant.components.nest -google-nest-sdm==0.4.0 +google-nest-sdm==0.4.2 # homeassistant.components.google_travel_time googlemaps==2.5.1 From 380c1a4be92917e3933b0152326210ebaa13222f Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Sun, 5 Dec 2021 11:20:40 +0100 Subject: [PATCH 0038/2644] Fix BMW Connected Drive (#60938) * Bump bimmer_connected to 0.8.5 * Always update HA states after service execution * Fix BMW device tracker & vehicle_finder service * Add charging_end_time sensor * Fix pylint & pytest * Remove unneeded DEFAULT_OPTION * Revert adding charging_end_time & state_attributes * Don't delete option data for CONF_USE_LOCATION * Remove stale string Co-authored-by: rikroe --- .../components/bmw_connected_drive/__init__.py | 14 ++++++++------ .../components/bmw_connected_drive/config_flow.py | 6 +----- .../bmw_connected_drive/device_tracker.py | 4 ++-- .../components/bmw_connected_drive/manifest.json | 2 +- .../components/bmw_connected_drive/strings.json | 3 +-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../bmw_connected_drive/test_config_flow.py | 10 +++------- 8 files changed, 18 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index a520214ca6a..e681cac8223 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -35,7 +35,6 @@ from .const import ( CONF_ACCOUNT, CONF_ALLOWED_REGIONS, CONF_READ_ONLY, - CONF_USE_LOCATION, DATA_ENTRIES, DATA_HASS_CONFIG, ) @@ -65,7 +64,6 @@ SERVICE_SCHEMA = vol.Schema( DEFAULT_OPTIONS = { CONF_READ_ONLY: False, - CONF_USE_LOCATION: False, } PLATFORMS = [ @@ -215,13 +213,10 @@ def setup_account( password: str = entry.data[CONF_PASSWORD] region: str = entry.data[CONF_REGION] read_only: bool = entry.options[CONF_READ_ONLY] - use_location: bool = entry.options[CONF_USE_LOCATION] _LOGGER.debug("Adding new account %s", name) - pos = ( - (hass.config.latitude, hass.config.longitude) if use_location else (None, None) - ) + pos = (hass.config.latitude, hass.config.longitude) cd_account = BMWConnectedDriveAccount( username, password, region, name, read_only, *pos ) @@ -258,6 +253,13 @@ def setup_account( function_call = getattr(vehicle.remote_services, function_name) function_call() + if call.service in [ + "find_vehicle", + "activate_air_conditioning", + "deactivate_air_conditioning", + ]: + cd_account.update() + if not read_only: # register the remote services for service in _SERVICE_MAP: diff --git a/homeassistant/components/bmw_connected_drive/config_flow.py b/homeassistant/components/bmw_connected_drive/config_flow.py index 838c991edb3..3b07830c077 100644 --- a/homeassistant/components/bmw_connected_drive/config_flow.py +++ b/homeassistant/components/bmw_connected_drive/config_flow.py @@ -13,7 +13,7 @@ from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from . import DOMAIN -from .const import CONF_ALLOWED_REGIONS, CONF_READ_ONLY, CONF_USE_LOCATION +from .const import CONF_ALLOWED_REGIONS, CONF_READ_ONLY DATA_SCHEMA = vol.Schema( { @@ -115,10 +115,6 @@ class BMWConnectedDriveOptionsFlow(config_entries.OptionsFlow): CONF_READ_ONLY, default=self.config_entry.options.get(CONF_READ_ONLY, False), ): bool, - vol.Optional( - CONF_USE_LOCATION, - default=self.config_entry.options.get(CONF_USE_LOCATION, False), - ): bool, } ), ) diff --git a/homeassistant/components/bmw_connected_drive/device_tracker.py b/homeassistant/components/bmw_connected_drive/device_tracker.py index d17920fef0c..0ba2d5012a1 100644 --- a/homeassistant/components/bmw_connected_drive/device_tracker.py +++ b/homeassistant/components/bmw_connected_drive/device_tracker.py @@ -35,7 +35,7 @@ async def async_setup_entry( for vehicle in account.account.vehicles: entities.append(BMWDeviceTracker(account, vehicle)) - if not vehicle.status.is_vehicle_tracking_enabled: + if not vehicle.is_vehicle_tracking_enabled: _LOGGER.info( "Tracking is (currently) disabled for vehicle %s (%s), defaulting to unknown", vehicle.name, @@ -83,6 +83,6 @@ class BMWDeviceTracker(BMWConnectedDriveBaseEntity, TrackerEntity): self._attr_extra_state_attributes = self._attrs self._location = ( self._vehicle.status.gps_position - if self._vehicle.status.is_vehicle_tracking_enabled + if self._vehicle.is_vehicle_tracking_enabled else None ) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 95ea2061fb4..fc641548aff 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -2,7 +2,7 @@ "domain": "bmw_connected_drive", "name": "BMW Connected Drive", "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", - "requirements": ["bimmer_connected==0.8.2"], + "requirements": ["bimmer_connected==0.8.5"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/homeassistant/components/bmw_connected_drive/strings.json b/homeassistant/components/bmw_connected_drive/strings.json index c0c45b814a4..3e93cccb8c6 100644 --- a/homeassistant/components/bmw_connected_drive/strings.json +++ b/homeassistant/components/bmw_connected_drive/strings.json @@ -21,8 +21,7 @@ "step": { "account_options": { "data": { - "read_only": "Read-only (only sensors and notify, no execution of services, no lock)", - "use_location": "Use Home Assistant location for car location polls (required for non i3/i8 vehicles produced before 7/2014)" + "read_only": "Read-only (only sensors and notify, no execution of services, no lock)" } } } diff --git a/requirements_all.txt b/requirements_all.txt index 0644f704fd4..74af9b74a0c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -387,7 +387,7 @@ beautifulsoup4==4.10.0 bellows==0.29.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.8.2 +bimmer_connected==0.8.5 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 09e78d41032..2db8378dbfe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -257,7 +257,7 @@ base36==0.1.1 bellows==0.29.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.8.2 +bimmer_connected==0.8.5 # homeassistant.components.blebox blebox_uniapi==1.3.3 diff --git a/tests/components/bmw_connected_drive/test_config_flow.py b/tests/components/bmw_connected_drive/test_config_flow.py index 6a0bd210387..b0bc3ce292c 100644 --- a/tests/components/bmw_connected_drive/test_config_flow.py +++ b/tests/components/bmw_connected_drive/test_config_flow.py @@ -3,10 +3,7 @@ from unittest.mock import patch from homeassistant import config_entries, data_entry_flow from homeassistant.components.bmw_connected_drive.config_flow import DOMAIN -from homeassistant.components.bmw_connected_drive.const import ( - CONF_READ_ONLY, - CONF_USE_LOCATION, -) +from homeassistant.components.bmw_connected_drive.const import CONF_READ_ONLY from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME from tests.common import MockConfigEntry @@ -28,7 +25,7 @@ FIXTURE_CONFIG_ENTRY = { CONF_PASSWORD: FIXTURE_USER_INPUT[CONF_PASSWORD], CONF_REGION: FIXTURE_USER_INPUT[CONF_REGION], }, - "options": {CONF_READ_ONLY: False, CONF_USE_LOCATION: False}, + "options": {CONF_READ_ONLY: False}, "source": config_entries.SOURCE_USER, "unique_id": f"{FIXTURE_USER_INPUT[CONF_REGION]}-{FIXTURE_USER_INPUT[CONF_REGION]}", } @@ -137,14 +134,13 @@ async def test_options_flow_implementation(hass): result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={CONF_READ_ONLY: False, CONF_USE_LOCATION: False}, + user_input={CONF_READ_ONLY: False}, ) await hass.async_block_till_done() assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"] == { CONF_READ_ONLY: False, - CONF_USE_LOCATION: False, } assert len(mock_setup.mock_calls) == 1 From 21c09d1a3e30d00fce740f7a10ece9cbe713ed97 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 5 Dec 2021 12:19:14 +0100 Subject: [PATCH 0039/2644] Fix panasonic_viera tests (#60999) Don't modify global test state --- .../panasonic_viera/test_config_flow.py | 40 +++++++++---------- tests/components/panasonic_viera/test_init.py | 8 ++-- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/tests/components/panasonic_viera/test_config_flow.py b/tests/components/panasonic_viera/test_config_flow.py index dd7f629c29b..a968c513516 100644 --- a/tests/components/panasonic_viera/test_config_flow.py +++ b/tests/components/panasonic_viera/test_config_flow.py @@ -41,7 +41,7 @@ async def test_flow_non_encrypted(hass): ): result = await hass.config_entries.flow.async_configure( result["flow_id"], - MOCK_BASIC_DATA, + {**MOCK_BASIC_DATA}, ) assert result["type"] == "create_entry" @@ -65,7 +65,7 @@ async def test_flow_not_connected_error(hass): ): result = await hass.config_entries.flow.async_configure( result["flow_id"], - MOCK_BASIC_DATA, + {**MOCK_BASIC_DATA}, ) assert result["type"] == "form" @@ -89,7 +89,7 @@ async def test_flow_unknown_abort(hass): ): result = await hass.config_entries.flow.async_configure( result["flow_id"], - MOCK_BASIC_DATA, + {**MOCK_BASIC_DATA}, ) assert result["type"] == "abort" @@ -114,7 +114,7 @@ async def test_flow_encrypted_not_connected_pin_code_request(hass): ): result = await hass.config_entries.flow.async_configure( result["flow_id"], - MOCK_BASIC_DATA, + {**MOCK_BASIC_DATA}, ) assert result["type"] == "abort" @@ -139,7 +139,7 @@ async def test_flow_encrypted_unknown_pin_code_request(hass): ): result = await hass.config_entries.flow.async_configure( result["flow_id"], - MOCK_BASIC_DATA, + {**MOCK_BASIC_DATA}, ) assert result["type"] == "abort" @@ -168,7 +168,7 @@ async def test_flow_encrypted_valid_pin_code(hass): ): result = await hass.config_entries.flow.async_configure( result["flow_id"], - MOCK_BASIC_DATA, + {**MOCK_BASIC_DATA}, ) assert result["type"] == "form" @@ -206,7 +206,7 @@ async def test_flow_encrypted_invalid_pin_code_error(hass): ): result = await hass.config_entries.flow.async_configure( result["flow_id"], - MOCK_BASIC_DATA, + {**MOCK_BASIC_DATA}, ) assert result["type"] == "form" @@ -244,7 +244,7 @@ async def test_flow_encrypted_not_connected_abort(hass): ): result = await hass.config_entries.flow.async_configure( result["flow_id"], - MOCK_BASIC_DATA, + {**MOCK_BASIC_DATA}, ) assert result["type"] == "form" @@ -277,7 +277,7 @@ async def test_flow_encrypted_unknown_abort(hass): ): result = await hass.config_entries.flow.async_configure( result["flow_id"], - MOCK_BASIC_DATA, + {**MOCK_BASIC_DATA}, ) assert result["type"] == "form" @@ -304,7 +304,7 @@ async def test_flow_non_encrypted_already_configured_abort(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, - data=MOCK_BASIC_DATA, + data={**MOCK_BASIC_DATA}, ) assert result["type"] == "abort" @@ -323,7 +323,7 @@ async def test_flow_encrypted_already_configured_abort(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, - data=MOCK_BASIC_DATA, + data={**MOCK_BASIC_DATA}, ) assert result["type"] == "abort" @@ -342,7 +342,7 @@ async def test_imported_flow_non_encrypted(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, - data=MOCK_CONFIG_DATA, + data={**MOCK_CONFIG_DATA}, ) assert result["type"] == "create_entry" @@ -366,7 +366,7 @@ async def test_imported_flow_encrypted_valid_pin_code(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, - data=MOCK_CONFIG_DATA, + data={**MOCK_CONFIG_DATA}, ) assert result["type"] == "form" @@ -398,7 +398,7 @@ async def test_imported_flow_encrypted_invalid_pin_code_error(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, - data=MOCK_CONFIG_DATA, + data={**MOCK_CONFIG_DATA}, ) assert result["type"] == "form" @@ -430,7 +430,7 @@ async def test_imported_flow_encrypted_not_connected_abort(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, - data=MOCK_CONFIG_DATA, + data={**MOCK_CONFIG_DATA}, ) assert result["type"] == "form" @@ -457,7 +457,7 @@ async def test_imported_flow_encrypted_unknown_abort(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, - data=MOCK_CONFIG_DATA, + data={**MOCK_CONFIG_DATA}, ) assert result["type"] == "form" @@ -482,7 +482,7 @@ async def test_imported_flow_not_connected_error(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, - data=MOCK_CONFIG_DATA, + data={**MOCK_CONFIG_DATA}, ) assert result["type"] == "form" @@ -500,7 +500,7 @@ async def test_imported_flow_unknown_abort(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, - data=MOCK_CONFIG_DATA, + data={**MOCK_CONFIG_DATA}, ) assert result["type"] == "abort" @@ -519,7 +519,7 @@ async def test_imported_flow_non_encrypted_already_configured_abort(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, - data=MOCK_BASIC_DATA, + data={**MOCK_BASIC_DATA}, ) assert result["type"] == "abort" @@ -538,7 +538,7 @@ async def test_imported_flow_encrypted_already_configured_abort(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, - data=MOCK_BASIC_DATA, + data={**MOCK_BASIC_DATA}, ) assert result["type"] == "abort" diff --git a/tests/components/panasonic_viera/test_init.py b/tests/components/panasonic_viera/test_init.py index e3fc74133d3..b4e220c42fd 100644 --- a/tests/components/panasonic_viera/test_init.py +++ b/tests/components/panasonic_viera/test_init.py @@ -130,7 +130,7 @@ async def test_setup_entry_unencrypted_missing_device_info(hass, mock_remote): mock_entry = MockConfigEntry( domain=DOMAIN, unique_id=MOCK_CONFIG_DATA[CONF_HOST], - data=MOCK_CONFIG_DATA, + data={**MOCK_CONFIG_DATA}, ) mock_entry.add_to_hass(hass) @@ -156,7 +156,7 @@ async def test_setup_entry_unencrypted_missing_device_info_none(hass): mock_entry = MockConfigEntry( domain=DOMAIN, unique_id=MOCK_CONFIG_DATA[CONF_HOST], - data=MOCK_CONFIG_DATA, + data={**MOCK_CONFIG_DATA}, ) mock_entry.add_to_hass(hass) @@ -207,7 +207,9 @@ async def test_setup_config_flow_initiated(hass): async def test_setup_unload_entry(hass, mock_remote): """Test if config entry is unloaded.""" mock_entry = MockConfigEntry( - domain=DOMAIN, unique_id=MOCK_DEVICE_INFO[ATTR_UDN], data=MOCK_CONFIG_DATA + domain=DOMAIN, + unique_id=MOCK_DEVICE_INFO[ATTR_UDN], + data={**MOCK_CONFIG_DATA}, ) mock_entry.add_to_hass(hass) From cf371ea8dd74aecdebd38d2323ab49062a93a6e0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 5 Dec 2021 13:53:52 +0100 Subject: [PATCH 0040/2644] Remove deprecated base entity classes (#61006) * Remove deprecated base entity classes * Clean up tests --- .../alarm_control_panel/__init__.py | 12 ---------- .../components/binary_sensor/__init__.py | 14 +---------- homeassistant/components/climate/__init__.py | 12 ---------- homeassistant/components/cover/__init__.py | 12 ---------- homeassistant/components/light/__init__.py | 12 ---------- homeassistant/components/lock/__init__.py | 12 ---------- .../components/media_player/__init__.py | 14 +---------- homeassistant/components/remote/__init__.py | 12 ---------- homeassistant/components/switch/__init__.py | 12 ---------- homeassistant/components/vacuum/__init__.py | 23 ------------------- .../components/water_heater/__init__.py | 12 ---------- .../alarm_control_panel/test_init.py | 13 ----------- tests/components/binary_sensor/test_init.py | 10 -------- tests/components/climate/test_init.py | 19 --------------- tests/components/cover/test_init.py | 10 -------- tests/components/light/test_init.py | 10 -------- tests/components/lock/test_init.py | 12 ---------- tests/components/media_player/test_init.py | 10 -------- tests/components/remote/test_init.py | 10 -------- tests/components/switch/test_init.py | 10 -------- tests/components/vacuum/test_init.py | 18 --------------- tests/components/water_heater/test_init.py | 12 ---------- 22 files changed, 2 insertions(+), 279 deletions(-) delete mode 100644 tests/components/alarm_control_panel/test_init.py delete mode 100644 tests/components/lock/test_init.py delete mode 100644 tests/components/vacuum/test_init.py delete mode 100644 tests/components/water_heater/test_init.py diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index 50317e97f2b..082327fdec8 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -217,15 +217,3 @@ class AlarmControlPanelEntity(Entity): ATTR_CHANGED_BY: self.changed_by, ATTR_CODE_ARM_REQUIRED: self.code_arm_required, } - - -class AlarmControlPanel(AlarmControlPanelEntity): - """An abstract class for alarm control entities (for backwards compatibility).""" - - def __init_subclass__(cls, **kwargs: Any) -> None: - """Print deprecation warning.""" - super().__init_subclass__(**kwargs) # type: ignore[call-arg] - _LOGGER.warning( - "AlarmControlPanel is deprecated, modify %s to extend AlarmControlPanelEntity", - cls.__name__, - ) diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index 0604d5da586..e3c6ed707e2 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -4,7 +4,7 @@ from __future__ import annotations from dataclasses import dataclass from datetime import timedelta import logging -from typing import Any, final +from typing import final import voluptuous as vol @@ -203,15 +203,3 @@ class BinarySensorEntity(Entity): def state(self) -> StateType: """Return the state of the binary sensor.""" return STATE_ON if self.is_on else STATE_OFF - - -class BinarySensorDevice(BinarySensorEntity): - """Represent a binary sensor (for backwards compatibility).""" - - def __init_subclass__(cls, **kwargs: Any): - """Print deprecation warning.""" - super().__init_subclass__(**kwargs) # type: ignore[call-arg] - _LOGGER.warning( - "BinarySensorDevice is deprecated, modify %s to extend BinarySensorEntity", - cls.__name__, - ) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 98e37237792..ae1c434d342 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -584,15 +584,3 @@ async def async_service_temperature_set( kwargs[value] = temp await entity.async_set_temperature(**kwargs) - - -class ClimateDevice(ClimateEntity): - """Representation of a climate entity (for backwards compatibility).""" - - def __init_subclass__(cls, **kwargs): - """Print deprecation warning.""" - super().__init_subclass__(**kwargs) - _LOGGER.warning( - "ClimateDevice is deprecated, modify %s to extend ClimateEntity", - cls.__name__, - ) diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index e654cf1a0d0..506a93461fe 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -403,15 +403,3 @@ class CoverEntity(Entity): if self._cover_is_last_toggle_direction_open: return fns["close"] return fns["open"] - - -class CoverDevice(CoverEntity): - """Representation of a cover (for backwards compatibility).""" - - def __init_subclass__(cls, **kwargs): - """Print deprecation warning.""" - super().__init_subclass__(**kwargs) - _LOGGER.warning( - "CoverDevice is deprecated, modify %s to extend CoverEntity", - cls.__name__, - ) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index dcf4972706a..390c675886d 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -958,18 +958,6 @@ class LightEntity(ToggleEntity): return self._attr_supported_features -class Light(LightEntity): - """Representation of a light (for backwards compatibility).""" - - def __init_subclass__(cls, **kwargs): - """Print deprecation warning.""" - super().__init_subclass__(**kwargs) - _LOGGER.warning( - "Light is deprecated, modify %s to extend LightEntity", - cls.__name__, - ) - - def legacy_supported_features( supported_features: int, supported_color_modes: list[str] | None ) -> int: diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index 1f2e87fc864..60c7c91152f 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -179,15 +179,3 @@ class LockEntity(Entity): if (locked := self.is_locked) is None: return None return STATE_LOCKED if locked else STATE_UNLOCKED - - -class LockDevice(LockEntity): - """Representation of a lock (for backwards compatibility).""" - - def __init_subclass__(cls, **kwargs: Any): - """Print deprecation warning.""" - super().__init_subclass__(**kwargs) # type: ignore[call-arg] - _LOGGER.warning( - "LockDevice is deprecated, modify %s to extend LockEntity", - cls.__name__, - ) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index d1d51f525e4..443225e70a6 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -1147,7 +1147,7 @@ async def websocket_browse_media(hass, connection, msg): To use, media_player integrations can implement MediaPlayerEntity.async_browse_media() """ component = hass.data[DOMAIN] - player: MediaPlayerDevice | None = component.get_entity(msg["entity_id"]) + player: MediaPlayerEntity | None = component.get_entity(msg["entity_id"]) if player is None: connection.send_error(msg["id"], "entity_not_found", "Entity not found") @@ -1195,18 +1195,6 @@ async def websocket_browse_media(hass, connection, msg): connection.send_result(msg["id"], payload) -class MediaPlayerDevice(MediaPlayerEntity): - """ABC for media player devices (for backwards compatibility).""" - - def __init_subclass__(cls, **kwargs): - """Print deprecation warning.""" - super().__init_subclass__(**kwargs) - _LOGGER.warning( - "MediaPlayerDevice is deprecated, modify %s to extend MediaPlayerEntity", - cls.__name__, - ) - - class BrowseMedia: """Represent a browsable media file.""" diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index e67a4eda9a9..8b06827f0d9 100644 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -210,15 +210,3 @@ class RemoteEntity(ToggleEntity): await self.hass.async_add_executor_job( ft.partial(self.delete_command, **kwargs) ) - - -class RemoteDevice(RemoteEntity): - """Representation of a remote (for backwards compatibility).""" - - def __init_subclass__(cls, **kwargs): - """Print deprecation warning.""" - super().__init_subclass__(**kwargs) - _LOGGER.warning( - "RemoteDevice is deprecated, modify %s to extend RemoteEntity", - cls.__name__, - ) diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index ac29e99bb73..157a5cd40c7 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -140,15 +140,3 @@ class SwitchEntity(ToggleEntity): data[attr] = value return data - - -class SwitchDevice(SwitchEntity): - """Representation of a switch (for backwards compatibility).""" - - def __init_subclass__(cls, **kwargs: Any) -> None: - """Print deprecation warning.""" - super().__init_subclass__(**kwargs) # type: ignore[call-arg] - _LOGGER.warning( - "SwitchDevice is deprecated, modify %s to extend SwitchEntity", - cls.__name__, - ) diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index db186a464fa..fde36871185 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -341,17 +341,6 @@ class VacuumEntity(_BaseVacuum, ToggleEntity): """Not supported.""" -class VacuumDevice(VacuumEntity): - """Representation of a vacuum (for backwards compatibility).""" - - def __init_subclass__(cls, **kwargs): - """Print deprecation warning.""" - super().__init_subclass__(**kwargs) - _LOGGER.warning( - "VacuumDevice is deprecated, modify %s to extend VacuumEntity", cls.__name__ - ) - - @dataclass class StateVacuumEntityDescription(EntityDescription): """A class that describes vacuum entities.""" @@ -406,15 +395,3 @@ class StateVacuumEntity(_BaseVacuum): async def async_toggle(self, **kwargs): """Not supported.""" - - -class StateVacuumDevice(StateVacuumEntity): - """Representation of a vacuum (for backwards compatibility).""" - - def __init_subclass__(cls, **kwargs): - """Print deprecation warning.""" - super().__init_subclass__(**kwargs) - _LOGGER.warning( - "StateVacuumDevice is deprecated, modify %s to extend StateVacuumEntity", - cls.__name__, - ) diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py index 85d6a791d7f..0a9174fa6ea 100644 --- a/homeassistant/components/water_heater/__init__.py +++ b/homeassistant/components/water_heater/__init__.py @@ -350,15 +350,3 @@ async def async_service_temperature_set(entity, service): kwargs[value] = temp await entity.async_set_temperature(**kwargs) - - -class WaterHeaterDevice(WaterHeaterEntity): - """Representation of a water heater (for backwards compatibility).""" - - def __init_subclass__(cls, **kwargs): - """Print deprecation warning.""" - super().__init_subclass__(**kwargs) - _LOGGER.warning( - "WaterHeaterDevice is deprecated, modify %s to extend WaterHeaterEntity", - cls.__name__, - ) diff --git a/tests/components/alarm_control_panel/test_init.py b/tests/components/alarm_control_panel/test_init.py deleted file mode 100644 index 257d764468a..00000000000 --- a/tests/components/alarm_control_panel/test_init.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Tests for Alarm control panel.""" -from homeassistant.components import alarm_control_panel - - -def test_deprecated_base_class(caplog): - """Test deprecated base class.""" - - class CustomAlarm(alarm_control_panel.AlarmControlPanel): - def supported_features(self): - pass - - CustomAlarm() - assert "AlarmControlPanel is deprecated, modify CustomAlarm" in caplog.text diff --git a/tests/components/binary_sensor/test_init.py b/tests/components/binary_sensor/test_init.py index 0c574df1569..18b42136ea2 100644 --- a/tests/components/binary_sensor/test_init.py +++ b/tests/components/binary_sensor/test_init.py @@ -19,13 +19,3 @@ def test_state(): new=True, ): assert binary_sensor.BinarySensorEntity().state == STATE_ON - - -def test_deprecated_base_class(caplog): - """Test deprecated base class.""" - - class CustomBinarySensor(binary_sensor.BinarySensorDevice): - pass - - CustomBinarySensor() - assert "BinarySensorDevice is deprecated, modify CustomBinarySensor" in caplog.text diff --git a/tests/components/climate/test_init.py b/tests/components/climate/test_init.py index 9473e61a165..1109704e1c4 100644 --- a/tests/components/climate/test_init.py +++ b/tests/components/climate/test_init.py @@ -10,7 +10,6 @@ from homeassistant.components.climate import ( HVAC_MODE_HEAT, HVAC_MODE_OFF, SET_TEMPERATURE_SCHEMA, - ClimateDevice, ClimateEntity, ) @@ -93,21 +92,3 @@ async def test_sync_turn_off(hass): await climate.async_turn_off() assert climate.turn_off.called - - -def test_deprecated_base_class(caplog): - """Test deprecated base class.""" - - class CustomClimate(ClimateDevice): - """Custom climate entity class.""" - - @property - def hvac_mode(self): - pass - - @property - def hvac_modes(self): - pass - - CustomClimate() - assert "ClimateDevice is deprecated, modify CustomClimate" in caplog.text diff --git a/tests/components/cover/test_init.py b/tests/components/cover/test_init.py index b46c0417cd2..95d3053d03b 100644 --- a/tests/components/cover/test_init.py +++ b/tests/components/cover/test_init.py @@ -111,13 +111,3 @@ def is_closed(hass, ent): def is_closing(hass, ent): """Return if the cover is closed based on the statemachine.""" return hass.states.is_state(ent.entity_id, STATE_CLOSING) - - -def test_deprecated_base_class(caplog): - """Test deprecated base class.""" - - class CustomCover(cover.CoverDevice): - pass - - CustomCover() - assert "CoverDevice is deprecated, modify CustomCover" in caplog.text diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index d51a5b64861..1f7fe431f0e 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -898,16 +898,6 @@ async def test_light_brightness_pct_conversion(hass, enable_custom_integrations) assert data["brightness"] == 255 -def test_deprecated_base_class(caplog): - """Test deprecated base class.""" - - class CustomLight(light.Light): - pass - - CustomLight() - assert "Light is deprecated, modify CustomLight" in caplog.text - - async def test_profiles(hass): """Test profiles loading.""" profiles = orig_Profiles(hass) diff --git a/tests/components/lock/test_init.py b/tests/components/lock/test_init.py deleted file mode 100644 index a788b9fa917..00000000000 --- a/tests/components/lock/test_init.py +++ /dev/null @@ -1,12 +0,0 @@ -"""The tests for Lock.""" -from homeassistant.components import lock - - -def test_deprecated_base_class(caplog): - """Test deprecated base class.""" - - class CustomLock(lock.LockDevice): - pass - - CustomLock() - assert "LockDevice is deprecated, modify CustomLock" in caplog.text diff --git a/tests/components/media_player/test_init.py b/tests/components/media_player/test_init.py index 7bdf3722d5d..caa562fc5a0 100644 --- a/tests/components/media_player/test_init.py +++ b/tests/components/media_player/test_init.py @@ -119,16 +119,6 @@ async def test_get_async_get_browse_image(hass, hass_client_no_auth, hass_ws_cli assert content == b"image" -def test_deprecated_base_class(caplog): - """Test deprecated base class.""" - - class CustomMediaPlayer(media_player.MediaPlayerDevice): - pass - - CustomMediaPlayer() - assert "MediaPlayerDevice is deprecated, modify CustomMediaPlayer" in caplog.text - - async def test_media_browse(hass, hass_ws_client): """Test browsing media.""" await async_setup_component( diff --git a/tests/components/remote/test_init.py b/tests/components/remote/test_init.py index 2c2005a7fee..024990ee000 100644 --- a/tests/components/remote/test_init.py +++ b/tests/components/remote/test_init.py @@ -139,13 +139,3 @@ async def test_delete_command(hass): assert call.domain == remote.DOMAIN assert call.service == SERVICE_DELETE_COMMAND assert call.data[ATTR_ENTITY_ID] == ENTITY_ID - - -async def test_deprecated_base_class(caplog): - """Test deprecated base class.""" - - class CustomRemote(remote.RemoteDevice): - pass - - CustomRemote() - assert "RemoteDevice is deprecated, modify CustomRemote" in caplog.text diff --git a/tests/components/switch/test_init.py b/tests/components/switch/test_init.py index 44302ec311b..ed3d3c59da9 100644 --- a/tests/components/switch/test_init.py +++ b/tests/components/switch/test_init.py @@ -72,13 +72,3 @@ async def test_switch_context( assert state2 is not None assert state.state != state2.state assert state2.context.user_id == hass_admin_user.id - - -def test_deprecated_base_class(caplog): - """Test deprecated base class.""" - - class CustomSwitch(switch.SwitchDevice): - pass - - CustomSwitch() - assert "SwitchDevice is deprecated, modify CustomSwitch" in caplog.text diff --git a/tests/components/vacuum/test_init.py b/tests/components/vacuum/test_init.py deleted file mode 100644 index 9075b385f40..00000000000 --- a/tests/components/vacuum/test_init.py +++ /dev/null @@ -1,18 +0,0 @@ -"""The tests for Vacuum.""" -from homeassistant.components import vacuum - - -def test_deprecated_base_class(caplog): - """Test deprecated base class.""" - - class CustomVacuum(vacuum.VacuumDevice): - pass - - class CustomStateVacuum(vacuum.StateVacuumDevice): - pass - - CustomVacuum() - assert "VacuumDevice is deprecated, modify CustomVacuum" in caplog.text - - CustomStateVacuum() - assert "StateVacuumDevice is deprecated, modify CustomStateVacuum" in caplog.text diff --git a/tests/components/water_heater/test_init.py b/tests/components/water_heater/test_init.py deleted file mode 100644 index 967e8b03620..00000000000 --- a/tests/components/water_heater/test_init.py +++ /dev/null @@ -1,12 +0,0 @@ -"""Tests for Water heater.""" -from homeassistant.components import water_heater - - -def test_deprecated_base_class(caplog): - """Test deprecated base class.""" - - class CustomWaterHeater(water_heater.WaterHeaterDevice): - pass - - CustomWaterHeater() - assert "WaterHeaterDevice is deprecated, modify CustomWaterHeater" in caplog.text From 9f7b8d3009e820a21dfe9757f43c5a0ee50e1c5a Mon Sep 17 00:00:00 2001 From: Maximilian <43999966+DeerMaximum@users.noreply.github.com> Date: Sun, 5 Dec 2021 14:11:02 +0000 Subject: [PATCH 0041/2644] Add nina integration (#56647) * Added nina integration * Improvements implemented * Fixed lint errors * Added tests * Improvements implemented * Use client session from HA * Added custom coordinator * Fixed tests * Fix pylint errors * Library updated to 0.1.4 * Optimization of static attributes * Removed unused code * Switched to BinarySensorDeviceClass * Switched to Platform Enum * Improve repetition * Improve repetition * Fix corona filter * Removed intermediate variable Co-authored-by: Marvin Wichmann * Fix black formatting Co-authored-by: Marvin Wichmann --- CODEOWNERS | 1 + homeassistant/components/nina/__init__.py | 106 ++++++++ .../components/nina/binary_sensor.py | 94 +++++++ homeassistant/components/nina/config_flow.py | 138 ++++++++++ homeassistant/components/nina/const.py | 53 ++++ homeassistant/components/nina/manifest.json | 14 ++ homeassistant/components/nina/strings.json | 27 ++ .../components/nina/translations/en.json | 27 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/nina/__init__.py | 1 + .../nina/fixtures/sample_regions.json | 1 + .../nina/fixtures/sample_warnings.json | 44 ++++ tests/components/nina/test_binary_sensor.py | 235 ++++++++++++++++++ tests/components/nina/test_config_flow.py | 130 ++++++++++ tests/components/nina/test_init.py | 46 ++++ 17 files changed, 924 insertions(+) create mode 100644 homeassistant/components/nina/__init__.py create mode 100644 homeassistant/components/nina/binary_sensor.py create mode 100644 homeassistant/components/nina/config_flow.py create mode 100644 homeassistant/components/nina/const.py create mode 100644 homeassistant/components/nina/manifest.json create mode 100644 homeassistant/components/nina/strings.json create mode 100644 homeassistant/components/nina/translations/en.json create mode 100644 tests/components/nina/__init__.py create mode 100644 tests/components/nina/fixtures/sample_regions.json create mode 100644 tests/components/nina/fixtures/sample_warnings.json create mode 100644 tests/components/nina/test_binary_sensor.py create mode 100644 tests/components/nina/test_config_flow.py create mode 100644 tests/components/nina/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index 513115a9953..8ebf28d249e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -353,6 +353,7 @@ homeassistant/components/nextcloud/* @meichthys homeassistant/components/nfandroidtv/* @tkdrob homeassistant/components/nightscout/* @marciogranzotto homeassistant/components/nilu/* @hfurubotten +homeassistant/components/nina/* @DeerMaximum homeassistant/components/nissan_leaf/* @filcole homeassistant/components/nmap_tracker/* @bdraco homeassistant/components/nmbs/* @thibmaek diff --git a/homeassistant/components/nina/__init__.py b/homeassistant/components/nina/__init__.py new file mode 100644 index 00000000000..143699174ce --- /dev/null +++ b/homeassistant/components/nina/__init__.py @@ -0,0 +1,106 @@ +"""The Nina integration.""" +from __future__ import annotations + +from datetime import timedelta +from typing import Any + +from async_timeout import timeout +from pynina import ApiError, Nina, Warning as NinaWarning + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + _LOGGER, + ATTR_EXPIRES, + ATTR_HEADLINE, + ATTR_ID, + ATTR_SENT, + ATTR_START, + CONF_FILTER_CORONA, + CONF_REGIONS, + DOMAIN, + SCAN_INTERVAL, +) + +PLATFORMS: list[str] = [Platform.BINARY_SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up platform from a ConfigEntry.""" + + regions: dict[str, str] = entry.data[CONF_REGIONS] + + coordinator = NINADataUpdateCoordinator( + hass, regions, entry.data[CONF_FILTER_CORONA] + ) + + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +class NINADataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching NINA data API.""" + + def __init__( + self, hass: HomeAssistant, regions: dict[str, str], corona_filter: bool + ) -> None: + """Initialize.""" + self._regions: dict[str, str] = regions + self._nina: Nina = Nina(async_get_clientsession(hass)) + self.warnings: dict[str, Any] = {} + self.corona_filter: bool = corona_filter + + for region in regions.keys(): + self._nina.addRegion(region) + + update_interval: timedelta = SCAN_INTERVAL + + super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) + + async def _async_update_data(self) -> dict[str, Any]: + """Update data.""" + + try: + async with timeout(10): + await self._nina.update() + return self._parse_data() + except ApiError as err: + raise UpdateFailed(err) from err + + def _parse_data(self) -> dict[str, Any]: + """Parse warning data.""" + + return_data: dict[str, Any] = {} + + for ( + region_id + ) in self._nina.warnings: # pylint: disable=consider-using-dict-items + raw_warnings: list[NinaWarning] = self._nina.warnings[region_id] + + warnings_for_regions: list[Any] = [] + + for raw_warn in raw_warnings: + if "corona" in raw_warn.headline.lower() and self.corona_filter: + continue + + warn_obj: dict[str, Any] = { + ATTR_ID: raw_warn.id, + ATTR_HEADLINE: raw_warn.headline, + ATTR_SENT: raw_warn.sent or "", + ATTR_START: raw_warn.start or "", + ATTR_EXPIRES: raw_warn.expires or "", + } + warnings_for_regions.append(warn_obj) + + return_data[region_id] = warnings_for_regions + + return return_data diff --git a/homeassistant/components/nina/binary_sensor.py b/homeassistant/components/nina/binary_sensor.py new file mode 100644 index 00000000000..e0d01c1bfb4 --- /dev/null +++ b/homeassistant/components/nina/binary_sensor.py @@ -0,0 +1,94 @@ +"""NINA sensor platform.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import NINADataUpdateCoordinator +from .const import ( + ATTR_EXPIRES, + ATTR_HEADLINE, + ATTR_ID, + ATTR_SENT, + ATTR_START, + CONF_MESSAGE_SLOTS, + CONF_REGIONS, + DOMAIN, +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up entries.""" + + coordinator: NINADataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + + regions: dict[str, str] = config_entry.data[CONF_REGIONS] + message_slots: int = config_entry.data[CONF_MESSAGE_SLOTS] + + entities: list[NINAMessage] = [] + + for ent in coordinator.data: + for i in range(0, message_slots): + entities.append(NINAMessage(coordinator, ent, regions[ent], i + 1)) + + async_add_entities(entities) + + +class NINAMessage(CoordinatorEntity, BinarySensorEntity): + """Representation of an NINA warning.""" + + def __init__( + self, + coordinator: NINADataUpdateCoordinator, + region: str, + regionName: str, + slotID: int, + ) -> None: + """Initialize.""" + super().__init__(coordinator) + + self._region: str = region + self._region_name: str = regionName + self._slot_id: int = slotID + self._warning_index: int = slotID - 1 + + self._coordinator: NINADataUpdateCoordinator = coordinator + + self._attr_name: str = f"Warning: {self._region_name} {self._slot_id}" + self._attr_unique_id: str = f"{self._region}-{self._slot_id}" + self._attr_device_class: str = BinarySensorDeviceClass.SAFETY + + @property + def is_on(self) -> bool: + """Return the state of the sensor.""" + return len(self._coordinator.data[self._region]) > self._warning_index + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return extra attributes of the sensor.""" + if ( + not len(self._coordinator.data[self._region]) > self._warning_index + ) or not self.is_on: + return {} + + data: dict[str, Any] = self._coordinator.data[self._region][self._warning_index] + + return { + ATTR_HEADLINE: data[ATTR_HEADLINE], + ATTR_ID: data[ATTR_ID], + ATTR_SENT: data[ATTR_SENT], + ATTR_START: data[ATTR_START], + ATTR_EXPIRES: data[ATTR_EXPIRES], + } diff --git a/homeassistant/components/nina/config_flow.py b/homeassistant/components/nina/config_flow.py new file mode 100644 index 00000000000..cb3fc35bc45 --- /dev/null +++ b/homeassistant/components/nina/config_flow.py @@ -0,0 +1,138 @@ +"""Config flow for Nina integration.""" +from __future__ import annotations + +from typing import Any + +from pynina import ApiError, Nina +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.data_entry_flow import FlowResult +import homeassistant.helpers.config_validation as cv + +from .const import ( + _LOGGER, + CONF_FILTER_CORONA, + CONF_MESSAGE_SLOTS, + CONF_REGIONS, + CONST_REGION_MAPPING, + CONST_REGIONS, + DOMAIN, +) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for NINA.""" + + VERSION: int = 1 + + def __init__(self) -> None: + """Initialize.""" + super().__init__() + self._all_region_codes_sorted: dict[str, str] = {} + self.regions: dict[str, dict[str, Any]] = {} + + for name in CONST_REGIONS: + self.regions[name] = {} + + async def async_step_user( + self: ConfigFlow, + user_input: dict[str, Any] | None = None, + ) -> FlowResult: + """Handle the initial step.""" + errors: dict[str, Any] = {} + + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + has_error: bool = False + + if len(self._all_region_codes_sorted) == 0: + try: + nina: Nina = Nina() + + self._all_region_codes_sorted = self.swap_key_value( + await nina.getAllRegionalCodes() + ) + + self.split_regions() + + except ApiError as err: + _LOGGER.warning("NINA setup error: %s", err) + errors["base"] = "cannot_connect" + has_error = True + except Exception as err: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception: %s", err) + errors["base"] = "unknown" + return self.async_abort(reason="unknown") + + if user_input is not None and not has_error: + config: dict[str, Any] = user_input + + config[CONF_REGIONS] = [] + + for group in CONST_REGIONS: + if group_input := user_input.get(group): + config[CONF_REGIONS] += group_input + + if len(config[CONF_REGIONS]) > 0: + tmp: dict[str, Any] = {} + + for reg in config[CONF_REGIONS]: + tmp[self._all_region_codes_sorted[reg]] = reg.split("_", 1)[0] + + compact: dict[str, Any] = {} + + for key, val in tmp.items(): + if val in compact: + compact[val] = f"{compact[val]} + {key}" + break + compact[val] = key + + config[CONF_REGIONS] = compact + + return self.async_create_entry(title="NINA", data=config) + + errors["base"] = "no_selection" + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + **{ + vol.Optional(region): cv.multi_select(self.regions[region]) + for region in CONST_REGIONS + }, + vol.Required(CONF_MESSAGE_SLOTS, default=5): vol.All( + int, vol.Range(min=1, max=20) + ), + vol.Required(CONF_FILTER_CORONA, default=True): cv.boolean, + } + ), + errors=errors, + ) + + @staticmethod + def swap_key_value(dict_to_sort: dict[str, str]) -> dict[str, str]: + """Swap keys and values in dict.""" + all_region_codes_swaped: dict[str, str] = {} + + for key, value in dict_to_sort.items(): + if value not in all_region_codes_swaped: + all_region_codes_swaped[value] = key + else: + for i in range(len(dict_to_sort)): + tmp_value: str = value + "_" + str(i) + if tmp_value not in all_region_codes_swaped: + all_region_codes_swaped[tmp_value] = key + break + + return dict(sorted(all_region_codes_swaped.items(), key=lambda ele: ele[1])) + + def split_regions(self) -> None: + """Split regions alphabetical.""" + for index, name in self._all_region_codes_sorted.items(): + for region_name, grouping_letters in CONST_REGION_MAPPING.items(): + if name[0] in grouping_letters: + self.regions[region_name][index] = name + break diff --git a/homeassistant/components/nina/const.py b/homeassistant/components/nina/const.py new file mode 100644 index 00000000000..12df703a480 --- /dev/null +++ b/homeassistant/components/nina/const.py @@ -0,0 +1,53 @@ +"""Constants for the Nina integration.""" +from __future__ import annotations + +from datetime import timedelta +from logging import Logger, getLogger +from typing import Final + +_LOGGER: Logger = getLogger(__package__) + +SCAN_INTERVAL: timedelta = timedelta(minutes=5) + +DOMAIN: str = "nina" + +CONF_REGIONS: str = "regions" +CONF_MESSAGE_SLOTS: str = "slots" +CONF_FILTER_CORONA: str = "corona_filter" + +ATTR_HEADLINE: str = "Headline" +ATTR_ID: str = "ID" +ATTR_SENT: str = "Sent" +ATTR_START: str = "Start" +ATTR_EXPIRES: str = "Expires" + +CONST_LIST_A_TO_D: list[str] = ["A", "Ä", "B", "C", "D"] +CONST_LIST_E_TO_H: list[str] = ["E", "F", "G", "H"] +CONST_LIST_I_TO_L: list[str] = ["I", "J", "K", "L"] +CONST_LIST_M_TO_Q: list[str] = ["M", "N", "O", "Ö", "P", "Q"] +CONST_LIST_R_TO_U: list[str] = ["R", "S", "T", "U", "Ü"] +CONST_LIST_V_TO_Z: list[str] = ["V", "W", "X", "Y"] + +CONST_REGION_A_TO_D: Final = "_a_to_d" +CONST_REGION_E_TO_H: Final = "_e_to_h" +CONST_REGION_I_TO_L: Final = "_i_to_l" +CONST_REGION_M_TO_Q: Final = "_m_to_q" +CONST_REGION_R_TO_U: Final = "_r_to_u" +CONST_REGION_V_TO_Z: Final = "_v_to_z" +CONST_REGIONS: Final = [ + CONST_REGION_A_TO_D, + CONST_REGION_E_TO_H, + CONST_REGION_I_TO_L, + CONST_REGION_M_TO_Q, + CONST_REGION_R_TO_U, + CONST_REGION_V_TO_Z, +] + +CONST_REGION_MAPPING: dict[str, list[str]] = { + CONST_REGION_A_TO_D: CONST_LIST_A_TO_D, + CONST_REGION_E_TO_H: CONST_LIST_E_TO_H, + CONST_REGION_I_TO_L: CONST_LIST_I_TO_L, + CONST_REGION_M_TO_Q: CONST_LIST_M_TO_Q, + CONST_REGION_R_TO_U: CONST_LIST_R_TO_U, + CONST_REGION_V_TO_Z: CONST_LIST_V_TO_Z, +} diff --git a/homeassistant/components/nina/manifest.json b/homeassistant/components/nina/manifest.json new file mode 100644 index 00000000000..11b1b3e3fdd --- /dev/null +++ b/homeassistant/components/nina/manifest.json @@ -0,0 +1,14 @@ +{ + "domain": "nina", + "name": "NINA", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/nina", + "requirements": [ + "pynina==0.1.4" + ], + "dependencies": [], + "codeowners": [ + "@DeerMaximum" + ], + "iot_class": "cloud_polling" +} \ No newline at end of file diff --git a/homeassistant/components/nina/strings.json b/homeassistant/components/nina/strings.json new file mode 100644 index 00000000000..1d9bb83def3 --- /dev/null +++ b/homeassistant/components/nina/strings.json @@ -0,0 +1,27 @@ +{ + "config": { + "step":{ + "user": { + "title": "Select city/county", + "data" : { + "_a_to_d": "City/county (A-D)", + "_e_to_h": "City/county (E-H)", + "_i_to_l": "City/county (I-L)", + "_m_to_q": "City/county (M-Q)", + "_r_to_u": "City/county (R-U)", + "_v_to_z": "City/county (V-Z)", + "slots": "Maximum warnings per city/county", + "corona_filter": "Remove Corona Warnings" + } + } + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + }, + "error": { + "no_selection": "Please select at least one city/county", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nina/translations/en.json b/homeassistant/components/nina/translations/en.json new file mode 100644 index 00000000000..793cbf595f1 --- /dev/null +++ b/homeassistant/components/nina/translations/en.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "error": { + "cannot_connect": "Failed to connect", + "no_selection": "Please select at least one city/county", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "_a_to_d": "City/county (A-D)", + "_e_to_h": "City/county (E-H)", + "_i_to_l": "City/county (I-L)", + "_m_to_q": "City/county (M-Q)", + "_r_to_u": "City/county (R-U)", + "_v_to_z": "City/county (V-Z)", + "corona_filter": "Remove Corona Warnings", + "slots": "Maximum warnings per city/county" + }, + "title": "Select city/county" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index c2648ec04cd..b4ca5fb2d5b 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -202,6 +202,7 @@ FLOWS = [ "nexia", "nfandroidtv", "nightscout", + "nina", "nmap_tracker", "notion", "nuheat", diff --git a/requirements_all.txt b/requirements_all.txt index 74af9b74a0c..a45251c3cea 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1669,6 +1669,9 @@ pynetgear==0.7.0 # homeassistant.components.netio pynetio==0.1.9.1 +# homeassistant.components.nina +pynina==0.1.4 + # homeassistant.components.nuki pynuki==1.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2db8378dbfe..019059669a5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1021,6 +1021,9 @@ pymysensors==0.22.1 # homeassistant.components.netgear pynetgear==0.7.0 +# homeassistant.components.nina +pynina==0.1.4 + # homeassistant.components.nuki pynuki==1.4.1 diff --git a/tests/components/nina/__init__.py b/tests/components/nina/__init__.py new file mode 100644 index 00000000000..92697378293 --- /dev/null +++ b/tests/components/nina/__init__.py @@ -0,0 +1 @@ +"""Tests for the Nina integration.""" diff --git a/tests/components/nina/fixtures/sample_regions.json b/tests/components/nina/fixtures/sample_regions.json new file mode 100644 index 00000000000..b25106e8138 --- /dev/null +++ b/tests/components/nina/fixtures/sample_regions.json @@ -0,0 +1 @@ +{"metadaten":{"kennung":"urn:de:bund:destatis:bevoelkerungsstatistik:schluessel:rs_2021-07-31","kennungInhalt":"urn:de:bund:destatis:bevoelkerungsstatistik:schluessel:rs","version":"2021-07-31","nameKurz":"Regionalschlüssel","nameLang":"Gemeinden, dargestellt durch den Amtlichen Regionalschlüssel (ARS) des Statistischen Bundesamtes","nameTechnisch":"Regionalschluessel","herausgebernameLang":"Statistisches Bundesamt, Wiesbaden","herausgebernameKurz":"Destatis","beschreibung":"Diese Codeliste stellt alle Gemeinden Deutschlands durch den Amtlichen Regionalschlüssel (ARS) dar, wie im Gemeindeverzeichnis des Statistischen Bundesamtes enthalten. Darüber hinaus enthält die Codeliste für die Stadtstaaten Hamburg, Bremen und Berlin Einträge für Stadt-/Ortsteile bzw. Stadtbezirke. Diese Einträge sind mit einem entsprechenden Hinweis versehen.","versionBeschreibung":null,"aenderungZurVorversion":"Mehrere Aenderungen","handbuchVersion":"1.0","xoevHandbuch":false,"gueltigAb":1627682400000,"bezugsorte":[]},"spalten":[{"spaltennameLang":"SCHLUESSEL","spaltennameTechnisch":"SCHLUESSEL","datentyp":"string","codeSpalte":true,"verwendung":{"code":"REQUIRED"},"empfohleneCodeSpalte":true},{"spaltennameLang":"Bezeichnung","spaltennameTechnisch":"Bezeichnung","datentyp":"string","codeSpalte":false,"verwendung":{"code":"REQUIRED"},"empfohleneCodeSpalte":false},{"spaltennameLang":"Hinweis","spaltennameTechnisch":"Hinweis","datentyp":"string","codeSpalte":false,"verwendung":{"code":"OPTIONAL"},"empfohleneCodeSpalte":false}],"daten":[["010010000000","Flensburg, Stadt",null],["010020000000","Kiel, Landeshauptstadt",null],["010030000000","Lübeck, Hansestadt",null],["010040000000","Neumünster, Stadt",null],["010510011011","Brunsbüttel, Stadt",null],["010510044044","Heide, Stadt",null],["010515163003","Averlak",null],["010515163010","Brickeln",null],["010515163012","Buchholz",null],["010515163016","Burg (Dithmarschen)",null],["010515163022","Dingen",null],["010515163024","Eddelak",null],["010515163026","Eggstedt",null],["010515163032","Frestedt",null],["010515163037","Großenrade",null],["010515163051","Hochdonn",null],["010515163064","Kuden",null],["010515163089","Quickborn",null],["010515163097","Sankt Michaelisdonn",null],["010515163110","Süderhastedt",null],["010515166021","Diekhusen-Fahrstedt",null],["010515166034","Friedrichskoog",null],["010515166046","Helse",null],["010515166057","Kaiser-Wilhelm-Koog",null],["010515166062","Kronprinzenkoog",null],["010515166072","Marne, Stadt",null],["010515166073","Marnerdeich",null],["010515166076","Neufeld",null],["010515166077","Neufelderkoog",null],["010515166090","Ramhusen",null],["010515166103","Schmedeswurth",null],["010515166118","Trennewurth",null],["010515166119","Volsemenhusen",null],["010515169005","Barkenholm",null],["010515169008","Bergewöhrden",null],["010515169019","Dellstedt",null],["010515169020","Delve",null],["010515169023","Dörpling",null],["010515169030","Fedderingen",null],["010515169035","Gaushorn",null],["010515169036","Glüsing",null],["010515169038","Groven",null],["010515169047","Hemme",null],["010515169049","Hennstedt",null],["010515169052","Hövede",null],["010515169053","Hollingstedt",null],["010515169058","Karolinenkoog",null],["010515169060","Kleve",null],["010515169061","Krempel",null],["010515169065","Lehe",null],["010515169068","Linden",null],["010515169071","Lunden",null],["010515169080","Norderheistedt",null],["010515169088","Pahlen",null],["010515169092","Rehm-Flehde-Bargen",null],["010515169096","Sankt Annen",null],["010515169100","Schalkholz",null],["010515169102","Schlichting",null],["010515169114","Tellingstedt",null],["010515169117","Tielenhemme",null],["010515169120","Wallen",null],["010515169125","Welmbüttel",null],["010515169131","Westerborstel",null],["010515169133","Wiemerstedt",null],["010515169136","Wrohm",null],["010515169139","Süderdorf",null],["010515169141","Süderheistedt",null],["010515172048","Hemmingstedt",null],["010515172067","Lieth",null],["010515172069","Lohe-Rickelshof",null],["010515172075","Neuenkirchen",null],["010515172081","Norderwöhrden",null],["010515172082","Nordhastedt",null],["010515172087","Ostrohe",null],["010515172107","Stelle-Wittenwurth",null],["010515172113","Wöhrden",null],["010515172122","Weddingstedt",null],["010515172130","Wesseln",null],["010515175001","Albersdorf",null],["010515175002","Arkebek",null],["010515175004","Bargenstedt",null],["010515175006","Barlt",null],["010515175015","Bunsoh",null],["010515175017","Busenwurth",null],["010515175027","Elpersbüttel",null],["010515175028","Epenwöhrden",null],["010515175039","Gudendorf",null],["010515175054","Immenstedt",null],["010515175063","Krumstedt",null],["010515175074","Meldorf, Stadt",null],["010515175078","Nindorf",null],["010515175083","Odderade",null],["010515175085","Offenbüttel",null],["010515175086","Osterrade",null],["010515175098","Sarzbüttel",null],["010515175099","Schafstedt",null],["010515175104","Schrum",null],["010515175126","Wennbüttel",null],["010515175134","Windbergen",null],["010515175135","Wolmersdorf",null],["010515175137","Nordermeldorf",null],["010515175138","Tensbüttel-Röst",null],["010515178013","Büsum",null],["010515178014","Büsumer Deichhausen",null],["010515178033","Friedrichsgabekoog",null],["010515178043","Hedwigenkoog",null],["010515178045","Hellschen-Heringsand-Unterschaar",null],["010515178050","Hillgroven",null],["010515178079","Norddeich",null],["010515178084","Oesterdeichstrich",null],["010515178093","Reinsbüttel",null],["010515178105","Schülp",null],["010515178108","Strübbel",null],["010515178109","Süderdeich",null],["010515178121","Warwerort",null],["010515178127","Wesselburen, Stadt",null],["010515178128","Wesselburener Deichhausen",null],["010515178129","Wesselburenerkoog",null],["010515178132","Westerdeichstrich",null],["010515178140","Oesterwurth",null],["010530032032","Geesthacht, Stadt",null],["010530083083","Lauenburg/ Elbe, Stadt",null],["010530090090","Mölln, Stadt",null],["010530100100","Ratzeburg, Stadt",null],["010530116116","Schwarzenbek, Stadt",null],["010530129129","Wentorf bei Hamburg",null],["010535308008","Behlendorf",null],["010535308009","Berkenthin",null],["010535308011","Bliestorf",null],["010535308024","Düchelsdorf",null],["010535308034","Göldenitz",null],["010535308061","Kastorf",null],["010535308067","Klempau",null],["010535308075","Krummesse",null],["010535308094","Niendorf bei Berkenthin",null],["010535308103","Rondeshagen",null],["010535308120","Sierksrade",null],["010535313002","Alt-Mölln",null],["010535313005","Bälau",null],["010535313013","Borstorf",null],["010535313014","Breitenfelde",null],["010535313037","Grambek",null],["010535313056","Hornbek",null],["010535313084","Lehmrade",null],["010535313095","Niendorf/ Stecknitz",null],["010535313113","Schretstaken",null],["010535313125","Talkau",null],["010535313134","Woltersdorf",null],["010535318010","Besenthal",null],["010535318015","Bröthen",null],["010535318020","Büchen",null],["010535318029","Fitzen",null],["010535318035","Göttin",null],["010535318046","Gudow",null],["010535318048","Güster",null],["010535318064","Klein Pampau",null],["010535318080","Langenlehsten",null],["010535318092","Müssen",null],["010535318104","Roseburg",null],["010535318115","Schulendorf",null],["010535318119","Siebeneichen",null],["010535318126","Tramm",null],["010535318132","Witzeeze",null],["010535323003","Aumühle",null],["010535323012","Börnsen",null],["010535323023","Dassendorf",null],["010535323028","Escheburg",null],["010535323050","Hamwarde",null],["010535323053","Hohenhorn",null],["010535323072","Kröppelshagen-Fahrendorf",null],["010535323131","Wiershop",null],["010535323133","Wohltorf",null],["010535323135","Worth",null],["010535343006","Basedow",null],["010535343019","Buchhorst",null],["010535343022","Dalldorf",null],["010535343058","Juliusburg",null],["010535343073","Krüzen",null],["010535343074","Krukow",null],["010535343082","Lanze",null],["010535343087","Lütau",null],["010535343111","Schnakenbek",null],["010535343128","Wangelau",null],["010535358001","Albsfelde",null],["010535358004","Bäk",null],["010535358016","Brunsmark",null],["010535358018","Buchholz",null],["010535358026","Einhaus",null],["010535358030","Fredeburg",null],["010535358033","Giesensdorf",null],["010535358040","Groß Disnack",null],["010535358041","Groß Grönau",null],["010535358043","Groß Sarau",null],["010535358051","Harmsdorf",null],["010535358054","Hollenbek",null],["010535358057","Horst",null],["010535358062","Kittlitz",null],["010535358066","Klein Zecher",null],["010535358078","Kulpin",null],["010535358088","Mechow",null],["010535358093","Mustin",null],["010535358098","Pogeez",null],["010535358102","Römnitz",null],["010535358107","Salem",null],["010535358110","Schmilau",null],["010535358117","Seedorf",null],["010535358123","Sterley",null],["010535358136","Ziethen",null],["010535373007","Basthorst",null],["010535373017","Brunstorf",null],["010535373021","Dahmker",null],["010535373027","Elmenhorst",null],["010535373031","Fuhlenhagen",null],["010535373036","Grabau",null],["010535373042","Groß Pampau",null],["010535373045","Grove",null],["010535373047","Gülzow",null],["010535373049","Hamfelde",null],["010535373052","Havekost",null],["010535373059","Kankelau",null],["010535373060","Kasseburg",null],["010535373070","Köthel",null],["010535373071","Kollow",null],["010535373076","Kuddewörde",null],["010535373089","Möhnsen",null],["010535373091","Mühlenrade",null],["010535373106","Sahms",null],["010535391025","Duvensee",null],["010535391038","Grinau",null],["010535391039","Groß Boden",null],["010535391044","Groß Schenkenberg",null],["010535391068","Klinkrade",null],["010535391069","Koberg",null],["010535391077","Kühsen",null],["010535391079","Labenz",null],["010535391081","Lankau",null],["010535391085","Linau",null],["010535391086","Lüchow",null],["010535391096","Nusse",null],["010535391097","Panten",null],["010535391099","Poggensee",null],["010535391101","Ritzerau",null],["010535391108","Sandesneben",null],["010535391109","Schiphorst",null],["010535391112","Schönberg",null],["010535391114","Schürensöhlen",null],["010535391118","Siebenbäumen",null],["010535391121","Sirksfelde",null],["010535391122","Steinhorst",null],["010535391124","Stubben",null],["010535391127","Walksfelde",null],["010535391130","Wentorf (Amt Sandesneben)",null],["010539105105","Sachsenwald (Forstgutsbez.),gemfr.Geb.",null],["010540033033","Friedrichstadt, Stadt",null],["010540056056","Husum, Stadt",null],["010540108108","Reußenköge",null],["010540138138","Tönning, Stadt",null],["010540168168","Sylt",null],["010545417035","Garding, Kirchspiel",null],["010545417036","Garding, Stadt",null],["010545417040","Grothusenkoog",null],["010545417063","Katharinenheerd",null],["010545417072","Kotzenbüll",null],["010545417090","Norderfriedrichskoog",null],["010545417095","Oldenswort",null],["010545417100","Osterhever",null],["010545417104","Poppenbüll",null],["010545417113","Sankt Peter-Ording",null],["010545417134","Tating",null],["010545417135","Tetenbüll",null],["010545417140","Tümlauer Koog",null],["010545417145","Vollerwiek",null],["010545417148","Welt",null],["010545417150","Westerhever",null],["010545439046","Hörnum (Sylt)",null],["010545439061","Kampen (Sylt)",null],["010545439078","List auf Sylt",null],["010545439149","Wenningstedt-Braderup (Sylt)",null],["010545453003","Ahrenviöl",null],["010545453004","Ahrenviölfeld",null],["010545453011","Behrendorf",null],["010545453013","Bondelum",null],["010545453041","Haselund",null],["010545453057","Immenstedt",null],["010545453079","Löwenstedt",null],["010545453092","Norstedt",null],["010545453101","Oster-Ohrstedt",null],["010545453118","Schwesing",null],["010545453123","Sollwitt",null],["010545453144","Viöl",null],["010545453152","Wester-Ohrstedt",null],["010545459039","Gröde",null],["010545459050","Hallig Hooge",null],["010545459074","Langeneß",null],["010545459103","Pellworm",null],["010545488005","Alkersum",null],["010545488015","Borgsum",null],["010545488025","Dunsum",null],["010545488083","Midlum",null],["010545488085","Nebel",null],["010545488087","Nieblum",null],["010545488089","Norddorf auf Amrum",null],["010545488094","Oevenum",null],["010545488098","Oldsum",null],["010545488129","Süderende",null],["010545488143","Utersum",null],["010545488158","Witsum",null],["010545488160","Wittdün auf Amrum",null],["010545488163","Wrixum",null],["010545488164","Wyk auf Föhr, Stadt",null],["010545489001","Achtrup",null],["010545489009","Aventoft",null],["010545489016","Bosbüll",null],["010545489017","Braderup",null],["010545489018","Bramstedtlund",null],["010545489022","Dagebüll",null],["010545489027","Ellhöft",null],["010545489034","Friedrich-Wilhelm-Lübke-Koog",null],["010545489048","Holm",null],["010545489055","Humptrup",null],["010545489062","Karlum",null],["010545489065","Klanxbüll",null],["010545489068","Klixbüll",null],["010545489073","Ladelund",null],["010545489076","Leck",null],["010545489077","Lexgaard",null],["010545489086","Neukirchen",null],["010545489088","Niebüll, Stadt",null],["010545489109","Risum-Lindholm",null],["010545489110","Rodenäs",null],["010545489124","Sprakebüll",null],["010545489125","Stadum",null],["010545489126","Stedesand",null],["010545489131","Süderlügum",null],["010545489136","Tinningstedt",null],["010545489142","Uphusum",null],["010545489154","Westre",null],["010545489165","Galmsbüll",null],["010545489166","Emmelsbüll-Horsbüll",null],["010545489167","Enge-Sande",null],["010545492007","Arlewatt",null],["010545492023","Drage",null],["010545492026","Elisabeth-Sophien-Koog",null],["010545492032","Fresendelf",null],["010545492042","Hattstedt",null],["010545492043","Hattstedtermarsch",null],["010545492052","Horstedt",null],["010545492054","Hude",null],["010545492070","Koldenbüttel",null],["010545492084","Mildstedt",null],["010545492091","Nordstrand",null],["010545492096","Oldersbek",null],["010545492097","Olderup",null],["010545492099","Ostenfeld (Husum)",null],["010545492105","Ramstedt",null],["010545492106","Rantrum",null],["010545492116","Schwabstedt",null],["010545492119","Seeth",null],["010545492120","Simonsberg",null],["010545492130","Süderhöft",null],["010545492132","Südermarsch",null],["010545492141","Uelvesbüll",null],["010545492156","Winnert",null],["010545492157","Wisch",null],["010545492159","Wittbek",null],["010545492161","Witzwort",null],["010545492162","Wobbenbüll",null],["010545494002","Ahrenshöft",null],["010545494006","Almdorf",null],["010545494010","Bargum",null],["010545494012","Bohmstedt",null],["010545494014","Bordelum",null],["010545494019","Bredstedt, Stadt",null],["010545494020","Breklum",null],["010545494024","Drelsdorf",null],["010545494037","Goldebek",null],["010545494038","Goldelund",null],["010545494045","Högel",null],["010545494059","Joldelund",null],["010545494071","Kolkerheide",null],["010545494075","Langenhorn",null],["010545494080","Lütjenholm",null],["010545494093","Ockholm",null],["010545494121","Sönnebüll",null],["010545494128","Struckum",null],["010545494146","Vollstedt",null],["010550001001","Ahrensbök",null],["010550004004","Bad Schwartau, Stadt",null],["010550007007","Bosau",null],["010550010010","Dahme",null],["010550012012","Eutin, Stadt",null],["010550016016","Grömitz",null],["010550018018","Grube",null],["010550021021","Heiligenhafen, Stadt",null],["010550025025","Kellenhusen (Ostsee)",null],["010550028028","Malente",null],["010550032032","Neustadt in Holstein, Stadt",null],["010550033033","Oldenburg in Holstein, Stadt",null],["010550035035","Ratekau",null],["010550040040","Stockelsdorf",null],["010550041041","Süsel",null],["010550042042","Timmendorfer Strand",null],["010550044044","Scharbeutz",null],["010550046046","Fehmarn, Stadt",null],["010555543014","Göhl",null],["010555543015","Gremersdorf",null],["010555543017","Großenbrode",null],["010555543022","Heringsdorf",null],["010555543031","Neukirchen",null],["010555543043","Wangels",null],["010555546006","Beschendorf",null],["010555546011","Damlos",null],["010555546020","Harmsdorf",null],["010555546023","Kabelhorst",null],["010555546027","Lensahn",null],["010555546029","Manhagen",null],["010555546036","Riepsdorf",null],["010555591002","Altenkrempe",null],["010555591024","Kasseedorf",null],["010555591037","Schashagen",null],["010555591038","Schönwalde am Bungsberg",null],["010555591039","Sierksdorf",null],["010560002002","Barmstedt, Stadt",null],["010560005005","Bönningstedt",null],["010560015015","Elmshorn, Stadt",null],["010560018018","Halstenbek",null],["010560021021","Hasloh",null],["010560025025","Helgoland",null],["010560039039","Pinneberg, Stadt",null],["010560041041","Quickborn, Stadt",null],["010560043043","Rellingen",null],["010560044044","Schenefeld, Stadt",null],["010560048048","Tornesch, Stadt",null],["010560049049","Uetersen, Stadt",null],["010560050050","Wedel, Stadt",null],["010565616029","Klein Nordende",null],["010565616030","Klein Offenseth-Sparrieshoop",null],["010565616031","Kölln-Reisiek",null],["010565616033","Seester",null],["010565616042","Raa-Besenbek",null],["010565616045","Seestermühe",null],["010565616046","Seeth-Ekholt",null],["010565636006","Bokel",null],["010565636010","Brande-Hörnerkirchen",null],["010565636038","Osterhorn",null],["010565636051","Westerhorn",null],["010565660003","Bevern",null],["010565660004","Bilsen",null],["010565660008","Bokholt-Hanredder",null],["010565660011","Bullenkuhlen",null],["010565660014","Ellerhoop",null],["010565660017","Groß Offenseth-Aspern",null],["010565660022","Heede",null],["010565660026","Hemdingen",null],["010565660034","Langeln",null],["010565660035","Lutzhorn",null],["010565687009","Borstel-Hohenraden",null],["010565687013","Ellerbek",null],["010565687032","Kummerfeld",null],["010565687040","Prisdorf",null],["010565687047","Tangstedt",null],["010565690001","Appen",null],["010565690016","Groß Nordende",null],["010565690019","Haselau",null],["010565690020","Haseldorf",null],["010565690023","Heidgraben",null],["010565690024","Heist",null],["010565690027","Hetlingen",null],["010565690028","Holm",null],["010565690036","Moorrege",null],["010565690037","Neuendeich",null],["010570001001","Ascheberg (Holstein)",null],["010570008008","Bönebüttel",null],["010570009009","Bösdorf",null],["010570057057","Plön, Stadt",null],["010570062062","Preetz, Stadt",null],["010570091091","Schwentinental, Stadt",null],["010575727004","Behrensdorf (Ostsee)",null],["010575727007","Blekendorf",null],["010575727013","Dannau",null],["010575727021","Giekau",null],["010575727026","Helmstorf",null],["010575727027","Högsdorf",null],["010575727029","Hohenfelde",null],["010575727030","Hohwacht (Ostsee)",null],["010575727034","Kirchnüchel",null],["010575727035","Klamp",null],["010575727038","Kletkamp",null],["010575727048","Lütjenburg, Stadt",null],["010575727055","Panker",null],["010575727076","Schwartbuck",null],["010575727082","Tröndel",null],["010575739015","Dersau",null],["010575739017","Dörnick",null],["010575739022","Grebin",null],["010575739032","Kalübbe",null],["010575739045","Lebrade",null],["010575739053","Nehmten",null],["010575739065","Rantzau",null],["010575739067","Rathjensdorf",null],["010575739089","Wittmoldt",null],["010575747002","Barmissen",null],["010575747010","Boksee",null],["010575747011","Bothkamp",null],["010575747023","Großbarkau",null],["010575747031","Honigsee",null],["010575747033","Kirchbarkau",null],["010575747037","Klein Barkau",null],["010575747042","Kühren",null],["010575747046","Lehmkuhlen",null],["010575747047","Löptin",null],["010575747054","Nettelsee",null],["010575747058","Pohnsdorf",null],["010575747059","Postfeld",null],["010575747066","Rastorf",null],["010575747070","Schellhorn",null],["010575747084","Wahlstorf",null],["010575747086","Warnau",null],["010575755003","Barsbek",null],["010575755006","Bendfeld",null],["010575755012","Brodersdorf",null],["010575755018","Fahren",null],["010575755020","Fiefbergen",null],["010575755028","Höhndorf",null],["010575755039","Köhn",null],["010575755040","Krokau",null],["010575755041","Krummbek",null],["010575755043","Laboe",null],["010575755049","Lutterbek",null],["010575755056","Passade",null],["010575755060","Prasdorf",null],["010575755063","Probsteierhagen",null],["010575755073","Schönberg (Holstein)",null],["010575755078","Stakendorf",null],["010575755079","Stein",null],["010575755081","Stoltenberg",null],["010575755087","Wendtorf",null],["010575755088","Wisch",null],["010575775016","Dobersdorf",null],["010575775044","Lammershagen",null],["010575775050","Martensrade",null],["010575775052","Mucheln",null],["010575775072","Schlesen",null],["010575775077","Selent",null],["010575775090","Fargau-Pratjau",null],["010575782025","Heikendorf",null],["010575782051","Mönkeberg",null],["010575782074","Schönkirchen",null],["010575785005","Belau",null],["010575785024","Großharrie",null],["010575785068","Rendswühren",null],["010575785069","Ruhwinkel",null],["010575785071","Schillsdorf",null],["010575785080","Stolpe",null],["010575785083","Tasdorf",null],["010575785085","Wankendorf",null],["010580005005","Altenholz",null],["010580034034","Büdelsdorf, Stadt",null],["010580043043","Eckernförde, Stadt",null],["010580092092","Kronshagen",null],["010580135135","Rendsburg, Stadt",null],["010580169169","Wasbek",null],["010585803001","Achterwehr",null],["010585803028","Bredenbek",null],["010585803050","Felde",null],["010585803093","Krummwisch",null],["010585803104","Melsdorf",null],["010585803126","Ottendorf",null],["010585803130","Quarnbek",null],["010585803171","Westensee",null],["010585822037","Dänischenhagen",null],["010585822116","Noer",null],["010585822150","Schwedeneck",null],["010585822157","Strande",null],["010585824051","Felm",null],["010585824058","Gettorf",null],["010585824096","Lindau",null],["010585824110","Neudorf-Bornstein",null],["010585824112","Neuwittenbek",null],["010585824121","Osdorf",null],["010585824142","Schinkel",null],["010585824165","Tüttendorf",null],["010585830019","Böhnhusen",null],["010585830053","Flintbek",null],["010585830145","Schönhorst",null],["010585830160","Techelsdorf",null],["010585833003","Alt Duvenstedt",null],["010585833054","Fockbek",null],["010585833118","Nübbel",null],["010585833136","Rickert",null],["010585847010","Bargstall",null],["010585847029","Breiholz",null],["010585847036","Christiansholm",null],["010585847047","Elsdorf-Westermühlen",null],["010585847055","Friedrichsgraben",null],["010585847056","Friedrichsholm",null],["010585847070","Hamdorf",null],["010585847078","Hohn",null],["010585847089","Königshügel",null],["010585847097","Lohe-Föhrden",null],["010585847129","Prinzenmoor",null],["010585847154","Sophienhamm",null],["010585853031","Brinjahe",null],["010585853048","Embühren",null],["010585853068","Haale",null],["010585853071","Hamweddel",null],["010585853075","Hörsten",null],["010585853086","Jevenstedt",null],["010585853101","Luhnstedt",null],["010585853148","Schülp b. Rendsburg",null],["010585853155","Stafstedt",null],["010585853172","Westerrönfeld",null],["010585859018","Blumenthal",null],["010585859105","Mielkendorf",null],["010585859107","Molfsee",null],["010585859138","Rodenbek",null],["010585859139","Rumohr",null],["010585859141","Schierensee",null],["010585864011","Bargstedt",null],["010585864021","Bokel",null],["010585864023","Borgdorf-Seedorf",null],["010585864027","Brammer",null],["010585864038","Dätgen",null],["010585864045","Eisendorf",null],["010585864046","Ellerdorf",null],["010585864049","Emkendorf",null],["010585864059","Gnutz",null],["010585864065","Groß Vollstedt",null],["010585864091","Krogaspe",null],["010585864094","Langwedel",null],["010585864117","Nortorf, Stadt",null],["010585864120","Oldenhütten",null],["010585864147","Schülp b. Nortorf",null],["010585864163","Timmaspe",null],["010585864168","Warder",null],["010585888026","Bovenau",null],["010585888073","Haßmoor",null],["010585888122","Ostenfeld (Rendsburg)",null],["010585888124","Osterrönfeld",null],["010585888132","Rade b. Rendsburg",null],["010585888140","Schacht-Audorf",null],["010585888146","Schülldorf",null],["010585889016","Bissee",null],["010585889022","Bordesholm",null],["010585889033","Brügge",null],["010585889063","Grevenkrug",null],["010585889064","Groß Buchwald",null],["010585889076","Hoffeld",null],["010585889098","Loop",null],["010585889108","Mühbrook",null],["010585889109","Negenharrie",null],["010585889133","Reesdorf",null],["010585889143","Schmalstede",null],["010585889144","Schönbek",null],["010585889153","Sören",null],["010585889170","Wattenbek",null],["010585890008","Ascheffel",null],["010585890024","Borgstedt",null],["010585890030","Brekendorf",null],["010585890035","Bünsdorf",null],["010585890039","Damendorf",null],["010585890066","Groß Wittensee",null],["010585890069","Haby",null],["010585890080","Holtsee",null],["010585890081","Holzbunge",null],["010585890083","Hütten",null],["010585890088","Klein Wittensee",null],["010585890111","Neu Duvenstedt",null],["010585890123","Osterby",null],["010585890127","Owschlag",null],["010585890152","Sehestedt",null],["010585890175","Ahlefeld-Bistensee",null],["010585893004","Altenhof",null],["010585893012","Barkelsby",null],["010585893032","Brodersby",null],["010585893040","Damp",null],["010585893042","Dörphof",null],["010585893052","Fleckeby",null],["010585893057","Gammelby",null],["010585893067","Güby",null],["010585893082","Holzdorf",null],["010585893084","Hummelfeld",null],["010585893087","Karby",null],["010585893090","Kosel",null],["010585893099","Loose",null],["010585893102","Goosefeld",null],["010585893137","Rieseby",null],["010585893162","Thumby",null],["010585893166","Waabs",null],["010585893173","Windeby",null],["010585893174","Winnemark",null],["010585895007","Arpsdorf",null],["010585895009","Aukrug",null],["010585895013","Beldorf",null],["010585895014","Bendorf",null],["010585895015","Beringstedt",null],["010585895025","Bornholt",null],["010585895044","Ehndorf",null],["010585895061","Gokels",null],["010585895062","Grauel",null],["010585895072","Hanerau-Hademarschen",null],["010585895074","Heinkenborstel",null],["010585895077","Hohenwestedt",null],["010585895085","Jahrsdorf",null],["010585895100","Lütjenwestedt",null],["010585895103","Meezen",null],["010585895106","Mörel",null],["010585895113","Nienborstel",null],["010585895115","Nindorf",null],["010585895119","Oldenbüttel",null],["010585895125","Osterstedt",null],["010585895128","Padenstedt",null],["010585895131","Rade b. Hohenwestedt",null],["010585895134","Remmels",null],["010585895151","Seefeld",null],["010585895156","Steenfeld",null],["010585895158","Tackesdorf",null],["010585895159","Tappendorf",null],["010585895161","Thaden",null],["010585895164","Todenbüttel",null],["010585895167","Wapelfeld",null],["010590045045","Kappeln, Stadt",null],["010590075075","Schleswig, Stadt",null],["010590113113","Glücksburg (Ostsee), Stadt",null],["010590120120","Harrislee",null],["010590183183","Handewitt",null],["010595912107","Eggebek",null],["010595912128","Janneby",null],["010595912131","Jerrishoe",null],["010595912132","Jörl",null],["010595912138","Langstedt",null],["010595912162","Sollerup",null],["010595912169","Süderhackstedt",null],["010595912174","Wanderup",null],["010595915012","Borgwedel",null],["010595915018","Busdorf",null],["010595915019","Dannewerk",null],["010595915026","Fahrdorf",null],["010595915032","Geltorf",null],["010595915043","Jagel",null],["010595915056","Lottorf",null],["010595915078","Selk",null],["010595919101","Tastrup",null],["010595919103","Ausacker",null],["010595919116","Großsolt",null],["010595919126","Hürup",null],["010595919127","Husby",null],["010595919141","Maasbüll",null],["010595919182","Freienwill",null],["010595920002","Arnis, Stadt",null],["010595920034","Grödersby",null],["010595920067","Oersberg",null],["010595920068","Rabenkirchen-Faulück",null],["010595937106","Dollerup",null],["010595937118","Grundhof",null],["010595937137","Langballig",null],["010595937145","Munkbrarup",null],["010595937157","Ringsberg",null],["010595937176","Wees",null],["010595937178","Westerholz",null],["010595940159","Sieverstedt",null],["010595940171","Tarp",null],["010595940184","Oeversee",null],["010595949076","Schnarup-Thumby",null],["010595949161","Sörup",null],["010595949185","Mittelangeln",null],["010595952105","Böxlund",null],["010595952115","Großenwiehe",null],["010595952123","Hörup",null],["010595952124","Holt",null],["010595952129","Jardelund",null],["010595952143","Medelby",null],["010595952144","Meyn",null],["010595952149","Nordhackstedt",null],["010595952151","Osterby",null],["010595952158","Schafflund",null],["010595952173","Wallsbüll",null],["010595952177","Weesby",null],["010595952179","Lindewitt",null],["010595974006","Böel",null],["010595974055","Loit",null],["010595974060","Mohrkirch",null],["010595974063","Norderbrarup",null],["010595974065","Nottfeld",null],["010595974070","Rügge",null],["010595974072","Saustrup",null],["010595974074","Scheggerott",null],["010595974080","Steinfeld",null],["010595974083","Süderbrarup",null],["010595974094","Ulsnis",null],["010595974095","Wagersrott",null],["010595974187","Boren",null],["010595987008","Böklund",null],["010595987037","Havetoft",null],["010595987042","Idstedt",null],["010595987049","Klappholz",null],["010595987062","Neuberend",null],["010595987073","Schaalby",null],["010595987081","Stolk",null],["010595987082","Struxdorf",null],["010595987084","Süderfahrenstedt",null],["010595987086","Taarstedt",null],["010595987090","Tolk",null],["010595987093","Uelsby",null],["010595987097","Twedt",null],["010595987098","Nübel",null],["010595987189","Brodersby-Goltoft",null],["010595990102","Ahneby",null],["010595990109","Esgrus",null],["010595990112","Gelting",null],["010595990121","Hasselberg",null],["010595990136","Kronsgaard",null],["010595990142","Maasholm",null],["010595990147","Nieby",null],["010595990148","Niesgrau",null],["010595990152","Pommerby",null],["010595990154","Rabel",null],["010595990155","Rabenholz",null],["010595990163","Stangheck",null],["010595990164","Steinberg",null],["010595990167","Sterup",null],["010595990168","Stoltebüll",null],["010595990186","Steinbergkirche",null],["010595993010","Bollingstedt",null],["010595993023","Ellingstedt",null],["010595993039","Hollingstedt",null],["010595993041","Hüsby",null],["010595993044","Jübek",null],["010595993057","Lürschau",null],["010595993077","Schuby",null],["010595993079","Silberstedt",null],["010595993092","Treia",null],["010595996001","Alt Bennebek",null],["010595996005","Bergenhusen",null],["010595996009","Börm",null],["010595996020","Dörpstedt",null],["010595996024","Erfde",null],["010595996035","Groß Rheide",null],["010595996050","Klein Bennebek",null],["010595996051","Klein Rheide",null],["010595996053","Kropp",null],["010595996058","Meggerdorf",null],["010595996087","Tetenhusen",null],["010595996088","Tielen",null],["010595996096","Wohlde",null],["010595996188","Stapel",null],["010600004004","Bad Bramstedt, Stadt",null],["010600005005","Bad Segeberg, Stadt",null],["010600019019","Ellerau",null],["010600039039","Henstedt-Ulzburg",null],["010600044044","Kaltenkirchen, Stadt",null],["010600063063","Norderstedt, Stadt",null],["010600092092","Wahlstedt, Stadt",null],["010605005003","Armstedt",null],["010605005009","Bimöhlen",null],["010605005013","Borstel",null],["010605005021","Föhrden-Barl",null],["010605005023","Fuhlendorf",null],["010605005027","Großenaspe",null],["010605005031","Hagen",null],["010605005033","Hardebek",null],["010605005035","Hasenkrug",null],["010605005037","Heidmoor",null],["010605005040","Hitzhusen",null],["010605005056","Mönkloh",null],["010605005095","Weddelbrook",null],["010605005099","Wiemersdorf",null],["010605024012","Bornhöved",null],["010605024017","Damsdorf",null],["010605024026","Gönnebek",null],["010605024072","Schmalensee",null],["010605024080","Stocksee",null],["010605024086","Tarbek",null],["010605024087","Tensfeld",null],["010605024089","Trappenkamp",null],["010605034043","Itzstedt",null],["010605034046","Kayhude",null],["010605034058","Nahe",null],["010605034065","Oering",null],["010605034076","Seth",null],["010605034085","Sülfeld",null],["010605043002","Alveslohe",null],["010605043034","Hartenholm",null],["010605043036","Hasenmoor",null],["010605043054","Lentföhrden",null],["010605043064","Nützen",null],["010605043073","Schmalfeld",null],["010605048042","Hüttblek",null],["010605048045","Kattendorf",null],["010605048047","Kisdorf",null],["010605048066","Oersdorf",null],["010605048077","Sievershütten",null],["010605048082","Struvenhütten",null],["010605048084","Stuvenborn",null],["010605048094","Wakendorf II",null],["010605048100","Winsen",null],["010605053007","Bark",null],["010605053008","Bebensee",null],["010605053022","Fredesdorf",null],["010605053029","Groß Niendorf",null],["010605053041","Högersdorf",null],["010605053051","Kükels",null],["010605053053","Leezen",null],["010605053057","Mözen",null],["010605053062","Neversdorf",null],["010605053074","Schwissel",null],["010605053088","Todesfelde",null],["010605053101","Wittenborn",null],["010605063011","Boostedt",null],["010605063016","Daldorf",null],["010605063028","Groß Kummerfeld",null],["010605063038","Heidmühlen",null],["010605063052","Latendorf",null],["010605063068","Rickling",null],["010605086006","Bahrenhof",null],["010605086010","Blunk",null],["010605086015","Bühnsdorf",null],["010605086018","Dreggers",null],["010605086020","Fahrenkrug",null],["010605086024","Geschendorf",null],["010605086025","Glasau",null],["010605086030","Groß Rönnau",null],["010605086048","Klein Gladebrügge",null],["010605086049","Klein Rönnau",null],["010605086050","Krems II",null],["010605086059","Negernbötel",null],["010605086060","Nehms",null],["010605086061","Neuengörs",null],["010605086067","Pronstorf",null],["010605086069","Rohlstorf",null],["010605086070","Schackendorf",null],["010605086071","Schieren",null],["010605086075","Seedorf",null],["010605086079","Stipsdorf",null],["010605086081","Strukdorf",null],["010605086090","Travenhorst",null],["010605086091","Traventhal",null],["010605086093","Wakendorf I",null],["010605086096","Weede",null],["010605086097","Wensin",null],["010605086098","Westerrade",null],["010609014014","Buchholz (Forstgutsbez.),gemfr. Gebiet",null],["010610029029","Glückstadt, Stadt",null],["010610046046","Itzehoe, Stadt",null],["010610113113","Wilster, Stadt",null],["010615104005","Auufer",null],["010615104016","Breitenberg",null],["010615104017","Breitenburg",null],["010615104053","Kollmoor",null],["010615104058","Kronsmoor",null],["010615104061","Lägerdorf",null],["010615104068","Moordiek",null],["010615104072","Münsterdorf",null],["010615104079","Oelixdorf",null],["010615104109","Westermoor",null],["010615104115","Wittenbergen",null],["010615134004","Altenmoor",null],["010615134012","Blomesche Wildnis",null],["010615134015","Borsfleth",null],["010615134027","Engelbrechtsche Wildnis",null],["010615134037","Herzhorn",null],["010615134041","Hohenfelde",null],["010615134044","Horst (Holstein)",null],["010615134050","Kiebitzreihe",null],["010615134054","Krempdorf",null],["010615134074","Neuendorf b. Elmshorn",null],["010615134101","Sommerland",null],["010615134118","Kollmar",null],["010615138008","Bekdorf",null],["010615138010","Bekmünde",null],["010615138024","Drage",null],["010615138034","Heiligenstedten",null],["010615138035","Heiligenstedtenerkamp",null],["010615138039","Hodorf",null],["010615138040","Hohenaspe",null],["010615138045","Huje",null],["010615138047","Kaaks",null],["010615138052","Kleve",null],["010615138059","Krummendiek",null],["010615138065","Lohbarbek",null],["010615138067","Mehlbek",null],["010615138070","Moorhusen",null],["010615138082","Oldendorf",null],["010615138083","Ottenbüttel",null],["010615138084","Peissen",null],["010615138098","Schlotfeld",null],["010615138100","Silzen",null],["010615138114","Winseldorf",null],["010615153006","Bahrenfleth",null],["010615153022","Dägeling",null],["010615153026","Elskop",null],["010615153030","Grevenkop",null],["010615153055","Krempe, Stadt",null],["010615153056","Kremperheide",null],["010615153057","Krempermoor",null],["010615153073","Neuenbrook",null],["010615153092","Rethwisch",null],["010615153104","Süderau",null],["010615168001","Aasbüttel",null],["010615168003","Agethorst",null],["010615168011","Besdorf",null],["010615168013","Bokelrehm",null],["010615168014","Bokhorst",null],["010615168021","Christinenthal",null],["010615168031","Gribbohm",null],["010615168033","Hadenfeld",null],["010615168043","Holstenniendorf",null],["010615168048","Kaisborstel",null],["010615168066","Looft",null],["010615168076","Nienbüttel",null],["010615168078","Nutteln",null],["010615168081","Oldenborstel",null],["010615168085","Pöschendorf",null],["010615168087","Puls",null],["010615168091","Reher",null],["010615168097","Schenefeld",null],["010615168105","Vaale",null],["010615168106","Vaalermoor",null],["010615168107","Wacken",null],["010615168108","Warringholz",null],["010615179002","Aebtissinwisch",null],["010615179007","Beidenfleth",null],["010615179018","Brokdorf",null],["010615179020","Büttel",null],["010615179023","Dammfleth",null],["010615179025","Ecklak",null],["010615179060","Kudensee",null],["010615179062","Landrecht",null],["010615179063","Landscheide",null],["010615179077","Nortorf",null],["010615179095","Sankt Margarethen",null],["010615179102","Stördorf",null],["010615179110","Wewelsfleth",null],["010615179119","Neuendorf-Sachsenbande",null],["010615189019","Brokstedt",null],["010615189028","Fitzbek",null],["010615189036","Hennstedt",null],["010615189038","Hingstheide",null],["010615189042","Hohenlockstedt",null],["010615189049","Kellinghusen, Stadt",null],["010615189064","Lockstedt",null],["010615189071","Mühlenbarbek",null],["010615189080","Oeschebüttel",null],["010615189086","Poyenberg",null],["010615189088","Quarnstedt",null],["010615189089","Rade",null],["010615189093","Rosdorf",null],["010615189096","Sarlhusen",null],["010615189103","Störkathen",null],["010615189111","Wiedenborstel",null],["010615189112","Willenscharen",null],["010615189116","Wrist",null],["010615189117","Wulfsmoor",null],["010620001001","Ahrensburg, Stadt",null],["010620004004","Bad Oldesloe, Stadt",null],["010620006006","Bargteheide, Stadt",null],["010620009009","Barsbüttel",null],["010620018018","Glinde, Stadt",null],["010620023023","Großhansdorf",null],["010620053053","Oststeinbek",null],["010620060060","Reinbek, Stadt",null],["010620061061","Reinfeld (Holstein), Stadt",null],["010620076076","Tangstedt",null],["010620090090","Ammersbek",null],["010625207019","Grabau",null],["010625207046","Meddewade",null],["010625207050","Neritz",null],["010625207056","Pölitz",null],["010625207062","Rethwisch",null],["010625207065","Rümpel",null],["010625207089","Lasbek",null],["010625207091","Steinburg",null],["010625207092","Travenbrück",null],["010625218005","Bargfeld-Stegen",null],["010625218014","Delingsdorf",null],["010625218016","Elmenhorst",null],["010625218027","Hammoor",null],["010625218036","Jersbek",null],["010625218051","Nienwohld",null],["010625218078","Todendorf",null],["010625218081","Tremsbüttel",null],["010625244003","Badendorf",null],["010625244008","Barnitz",null],["010625244025","Hamberge",null],["010625244031","Heidekamp",null],["010625244032","Heilshoop",null],["010625244039","Klein Wesenberg",null],["010625244048","Mönkhagen",null],["010625244059","Rehhorst",null],["010625244083","Westerau",null],["010625244087","Zarpen",null],["010625244093","Feldhorst",null],["010625244094","Wesenberg",null],["010625262011","Braak",null],["010625262035","Hoisdorf",null],["010625262069","Siek",null],["010625262071","Stapelfeld",null],["010625262088","Brunsbek",null],["010625270020","Grande",null],["010625270021","Grönwohld",null],["010625270022","Großensee",null],["010625270026","Hamfelde",null],["010625270033","Hohenfelde",null],["010625270040","Köthel",null],["010625270045","Lütjensee",null],["010625270058","Rausdorf",null],["010625270082","Trittau",null],["010625270086","Witzhave",null],["020000000000","Hamburg, Freie und Hansestadt",null],["021010101101","Hamburg-Altstadt, OT 101","Stadt-/Ortsteil bzw. Stadtbezirk"],["021010102102","Hamburg-Altstadt, OT 102","Stadt-/Ortsteil bzw. Stadtbezirk"],["021020103103","HafenCity, OT 103","Stadt-/Ortsteil bzw. Stadtbezirk"],["021020104104","HafenCity, OT 104","Stadt-/Ortsteil bzw. Stadtbezirk"],["021030105105","Neustadt, OT 105","Stadt-/Ortsteil bzw. Stadtbezirk"],["021030106106","Neustadt, OT 106","Stadt-/Ortsteil bzw. Stadtbezirk"],["021030107107","Neustadt, OT 107","Stadt-/Ortsteil bzw. Stadtbezirk"],["021030108108","Neustadt, OT 108","Stadt-/Ortsteil bzw. Stadtbezirk"],["021040109109","St. Pauli, OT 109","Stadt-/Ortsteil bzw. Stadtbezirk"],["021040110110","St. Pauli, OT 110","Stadt-/Ortsteil bzw. Stadtbezirk"],["021040111111","St. Pauli, OT 111","Stadt-/Ortsteil bzw. Stadtbezirk"],["021040112112","St. Pauli, OT 112","Stadt-/Ortsteil bzw. Stadtbezirk"],["021050113113","St. Georg, OT 113","Stadt-/Ortsteil bzw. Stadtbezirk"],["021050114114","St. Georg, OT 114","Stadt-/Ortsteil bzw. Stadtbezirk"],["021060115115","Hammerbrook, OT 115","Stadt-/Ortsteil bzw. Stadtbezirk"],["021060116116","Hammerbrook, OT 116","Stadt-/Ortsteil bzw. Stadtbezirk"],["021060117117","Hammerbrook, OT 117","Stadt-/Ortsteil bzw. Stadtbezirk"],["021060118118","Hammerbrook, OT 118","Stadt-/Ortsteil bzw. Stadtbezirk"],["021070119119","Borgfelde, OT 119","Stadt-/Ortsteil bzw. Stadtbezirk"],["021070120120","Borgfelde, OT 120","Stadt-/Ortsteil bzw. Stadtbezirk"],["021080121121","Hamm, OT 121","Stadt-/Ortsteil bzw. Stadtbezirk"],["021080122122","Hamm, OT 122","Stadt-/Ortsteil bzw. Stadtbezirk"],["021080123123","Hamm, OT 123","Stadt-/Ortsteil bzw. Stadtbezirk"],["021080124124","Hamm, OT 124","Stadt-/Ortsteil bzw. Stadtbezirk"],["021080125125","Hamm, OT 125","Stadt-/Ortsteil bzw. Stadtbezirk"],["021080126126","Hamm, OT 126","Stadt-/Ortsteil bzw. Stadtbezirk"],["021080127127","Hamm, OT 127","Stadt-/Ortsteil bzw. Stadtbezirk"],["021110128128","Horn, OT 128","Stadt-/Ortsteil bzw. Stadtbezirk"],["021110129129","Horn, OT 129","Stadt-/Ortsteil bzw. Stadtbezirk"],["021120130130","Billstedt, OT 130","Stadt-/Ortsteil bzw. Stadtbezirk"],["021130131131","Billbrook, OT 131","Stadt-/Ortsteil bzw. Stadtbezirk"],["021140132132","Rothenburgsort, OT 132","Stadt-/Ortsteil bzw. Stadtbezirk"],["021140133133","Rothenburgsort, OT 133","Stadt-/Ortsteil bzw. Stadtbezirk"],["021150134134","Veddel, OT 134","Stadt-/Ortsteil bzw. Stadtbezirk"],["021160135135","Wilhelmsburg, OT 135","Stadt-/Ortsteil bzw. Stadtbezirk"],["021160136136","Wilhelmsburg, OT 136","Stadt-/Ortsteil bzw. Stadtbezirk"],["021160137137","Wilhelmsburg, OT 137","Stadt-/Ortsteil bzw. Stadtbezirk"],["021170138138","Kleiner Grasbrook, OT 138","Stadt-/Ortsteil bzw. Stadtbezirk"],["021180139139","Steinwerder, OT 139","Stadt-/Ortsteil bzw. Stadtbezirk"],["021190140140","Waltershof, OT 140","Stadt-/Ortsteil bzw. Stadtbezirk"],["021200141141","Finkenwerder, OT 141","Stadt-/Ortsteil bzw. Stadtbezirk"],["021210142142","Neuwerk, OT 142","Stadt-/Ortsteil bzw. Stadtbezirk"],["021220150150","Seeleute/Binnenschiffer, OT 150","Stadt-/Ortsteil bzw. Stadtbezirk"],["022010201201","Altona-Altstadt, OT 201","Stadt-/Ortsteil bzw. Stadtbezirk"],["022010202202","Altona-Altstadt, OT 202","Stadt-/Ortsteil bzw. Stadtbezirk"],["022010203203","Altona-Altstadt, OT 203","Stadt-/Ortsteil bzw. Stadtbezirk"],["022010204204","Altona-Altstadt, OT 204","Stadt-/Ortsteil bzw. Stadtbezirk"],["022010205205","Altona-Altstadt, OT 205","Stadt-/Ortsteil bzw. Stadtbezirk"],["022010206206","Altona-Altstadt, OT 206","Stadt-/Ortsteil bzw. Stadtbezirk"],["022020207207","Sternschanze, OT 207","Stadt-/Ortsteil bzw. Stadtbezirk"],["022030208208","Altona-Nord, OT 208","Stadt-/Ortsteil bzw. Stadtbezirk"],["022030209209","Altona-Nord, OT 209","Stadt-/Ortsteil bzw. Stadtbezirk"],["022030210210","Altona-Nord, OT 210","Stadt-/Ortsteil bzw. Stadtbezirk"],["022040211211","Ottensen, OT 211","Stadt-/Ortsteil bzw. Stadtbezirk"],["022040212212","Ottensen, OT 212","Stadt-/Ortsteil bzw. Stadtbezirk"],["022040213213","Ottensen, OT 213","Stadt-/Ortsteil bzw. Stadtbezirk"],["022040214214","Ottensen, OT 214","Stadt-/Ortsteil bzw. Stadtbezirk"],["022050215215","Bahrenfeld, OT 215","Stadt-/Ortsteil bzw. Stadtbezirk"],["022050216216","Bahrenfeld, OT 216","Stadt-/Ortsteil bzw. Stadtbezirk"],["022050217217","Bahrenfeld, OT 217","Stadt-/Ortsteil bzw. Stadtbezirk"],["022060218218","Groß Flottbek, OT 218","Stadt-/Ortsteil bzw. Stadtbezirk"],["022070219219","Othmarschen, OT 219","Stadt-/Ortsteil bzw. Stadtbezirk"],["022080220220","Lurup, OT 220","Stadt-/Ortsteil bzw. Stadtbezirk"],["022090221221","Osdorf, OT 221","Stadt-/Ortsteil bzw. Stadtbezirk"],["022100222222","Nienstedten, OT 222","Stadt-/Ortsteil bzw. Stadtbezirk"],["022110223223","Blankenese, OT 223","Stadt-/Ortsteil bzw. Stadtbezirk"],["022110224224","Blankenese, OT 224","Stadt-/Ortsteil bzw. Stadtbezirk"],["022120225225","Iserbrook, OT 225","Stadt-/Ortsteil bzw. Stadtbezirk"],["022130226226","Sülldorf, OT 226","Stadt-/Ortsteil bzw. Stadtbezirk"],["022140227227","Rissen, OT 227","Stadt-/Ortsteil bzw. Stadtbezirk"],["023010301301","Eimsbüttel, OT 301","Stadt-/Ortsteil bzw. Stadtbezirk"],["023010302302","Eimsbüttel, OT 302","Stadt-/Ortsteil bzw. Stadtbezirk"],["023010303303","Eimsbüttel, OT 303","Stadt-/Ortsteil bzw. Stadtbezirk"],["023010304304","Eimsbüttel, OT 304","Stadt-/Ortsteil bzw. Stadtbezirk"],["023010305305","Eimsbüttel, OT 305","Stadt-/Ortsteil bzw. Stadtbezirk"],["023010306306","Eimsbüttel, OT 306","Stadt-/Ortsteil bzw. Stadtbezirk"],["023010307307","Eimsbüttel, OT 307","Stadt-/Ortsteil bzw. Stadtbezirk"],["023010308308","Eimsbüttel, OT 308","Stadt-/Ortsteil bzw. Stadtbezirk"],["023010309309","Eimsbüttel, OT 309","Stadt-/Ortsteil bzw. Stadtbezirk"],["023010310310","Eimsbüttel, OT 310","Stadt-/Ortsteil bzw. Stadtbezirk"],["023020311311","Rotherbaum, OT 311","Stadt-/Ortsteil bzw. Stadtbezirk"],["023020312312","Rotherbaum, OT 312","Stadt-/Ortsteil bzw. Stadtbezirk"],["023030313313","Harvestehude, OT 313","Stadt-/Ortsteil bzw. Stadtbezirk"],["023030314314","Harvestehude, OT 314","Stadt-/Ortsteil bzw. Stadtbezirk"],["023040315315","Hoheluft-West, OT 315","Stadt-/Ortsteil bzw. Stadtbezirk"],["023040316316","Hoheluft-West, OT 316","Stadt-/Ortsteil bzw. Stadtbezirk"],["023050317317","Lokstedt, OT 317","Stadt-/Ortsteil bzw. Stadtbezirk"],["023060318318","Niendorf, OT 318","Stadt-/Ortsteil bzw. Stadtbezirk"],["023070319319","Schnelsen, OT 319","Stadt-/Ortsteil bzw. Stadtbezirk"],["023080320320","Eidelstedt, OT 320","Stadt-/Ortsteil bzw. Stadtbezirk"],["023090321321","Stellingen, OT 321","Stadt-/Ortsteil bzw. Stadtbezirk"],["024010401401","Hoheluft-Ost, OT 401","Stadt-/Ortsteil bzw. Stadtbezirk"],["024010402402","Hoheluft-Ost, OT 402","Stadt-/Ortsteil bzw. Stadtbezirk"],["024020403403","Eppendorf, OT 403","Stadt-/Ortsteil bzw. Stadtbezirk"],["024020404404","Eppendorf, OT 404","Stadt-/Ortsteil bzw. Stadtbezirk"],["024020405405","Eppendorf, OT 405","Stadt-/Ortsteil bzw. Stadtbezirk"],["024030406406","Gross Borstel, OT 406","Stadt-/Ortsteil bzw. Stadtbezirk"],["024040407407","Alsterdorf, OT 407","Stadt-/Ortsteil bzw. Stadtbezirk"],["024050408408","Winterhude, OT 408","Stadt-/Ortsteil bzw. Stadtbezirk"],["024050409409","Winterhude, OT 409","Stadt-/Ortsteil bzw. Stadtbezirk"],["024050410410","Winterhude, OT 410","Stadt-/Ortsteil bzw. Stadtbezirk"],["024050411411","Winterhude, OT 411","Stadt-/Ortsteil bzw. Stadtbezirk"],["024050412412","Winterhude, OT 412","Stadt-/Ortsteil bzw. Stadtbezirk"],["024050413413","Winterhude, OT 413","Stadt-/Ortsteil bzw. Stadtbezirk"],["024060414414","Uhlenhorst, OT 414","Stadt-/Ortsteil bzw. Stadtbezirk"],["024060415415","Uhlenhorst, OT 415","Stadt-/Ortsteil bzw. Stadtbezirk"],["024070416416","Hohenfelde, OT 416","Stadt-/Ortsteil bzw. Stadtbezirk"],["024070417417","Hohenfelde, OT 417","Stadt-/Ortsteil bzw. Stadtbezirk"],["024080418418","Barmbek-Süd, OT 418","Stadt-/Ortsteil bzw. Stadtbezirk"],["024080419419","Barmbek-Süd, OT 419","Stadt-/Ortsteil bzw. Stadtbezirk"],["024080420420","Barmbek-Süd, OT 420","Stadt-/Ortsteil bzw. Stadtbezirk"],["024080421421","Barmbek-Süd, OT 421","Stadt-/Ortsteil bzw. Stadtbezirk"],["024080422422","Barmbek-Süd, OT 422","Stadt-/Ortsteil bzw. Stadtbezirk"],["024080423423","Barmbek-Süd, OT 423","Stadt-/Ortsteil bzw. Stadtbezirk"],["024090424424","Dulsberg, OT 424","Stadt-/Ortsteil bzw. Stadtbezirk"],["024090425425","Dulsberg, OT 425","Stadt-/Ortsteil bzw. Stadtbezirk"],["024100426426","Barmbek-Nord, OT 426","Stadt-/Ortsteil bzw. Stadtbezirk"],["024100427427","Barmbek-Nord, OT 427","Stadt-/Ortsteil bzw. Stadtbezirk"],["024100428428","Barmbek-Nord, OT 428","Stadt-/Ortsteil bzw. Stadtbezirk"],["024100429429","Barmbek-Nord, OT 429","Stadt-/Ortsteil bzw. Stadtbezirk"],["024110430430","Ohlsdorf, OT 430","Stadt-/Ortsteil bzw. Stadtbezirk"],["024120431431","Fuhlsbüttel, OT 431","Stadt-/Ortsteil bzw. Stadtbezirk"],["024130432432","Langenhorn, OT 432","Stadt-/Ortsteil bzw. Stadtbezirk"],["025010501501","Eilbek, OT 501","Stadt-/Ortsteil bzw. Stadtbezirk"],["025010502502","Eilbek, OT 502","Stadt-/Ortsteil bzw. Stadtbezirk"],["025010503503","Eilbek, OT 503","Stadt-/Ortsteil bzw. Stadtbezirk"],["025010504504","Eilbek, OT 504","Stadt-/Ortsteil bzw. Stadtbezirk"],["025020505505","Wandsbek, OT 505","Stadt-/Ortsteil bzw. Stadtbezirk"],["025020506506","Wandsbek, OT 506","Stadt-/Ortsteil bzw. Stadtbezirk"],["025020507507","Wandsbek, OT 507","Stadt-/Ortsteil bzw. Stadtbezirk"],["025020508508","Wandsbek, OT 508","Stadt-/Ortsteil bzw. Stadtbezirk"],["025020509509","Wandsbek, OT 509","Stadt-/Ortsteil bzw. Stadtbezirk"],["025030510510","Marienthal, OT 510","Stadt-/Ortsteil bzw. Stadtbezirk"],["025030511511","Marienthal, OT 511","Stadt-/Ortsteil bzw. Stadtbezirk"],["025040512512","Jenfeld, OT 512","Stadt-/Ortsteil bzw. Stadtbezirk"],["025050513513","Tonndorf, OT 513","Stadt-/Ortsteil bzw. Stadtbezirk"],["025060514514","Farmsen-Berne, OT 514","Stadt-/Ortsteil bzw. Stadtbezirk"],["025070515515","Bramfeld, OT 515","Stadt-/Ortsteil bzw. Stadtbezirk"],["025080516516","Steilshoop, OT 516","Stadt-/Ortsteil bzw. Stadtbezirk"],["025090517517","Wellingsbüttel, OT 517","Stadt-/Ortsteil bzw. Stadtbezirk"],["025100518518","Sasel, OT 518","Stadt-/Ortsteil bzw. Stadtbezirk"],["025110519519","Poppenbüttel, OT 519","Stadt-/Ortsteil bzw. Stadtbezirk"],["025120520520","Hummelsbüttel, OT 520","Stadt-/Ortsteil bzw. Stadtbezirk"],["025130521521","Lemsahl-Mellingstedt, OT 521","Stadt-/Ortsteil bzw. Stadtbezirk"],["025140522522","Duvenstedt, OT 522","Stadt-/Ortsteil bzw. Stadtbezirk"],["025150523523","Wohldorf-Ohlstedt, OT 523","Stadt-/Ortsteil bzw. Stadtbezirk"],["025160524524","Bergstedt, OT 524","Stadt-/Ortsteil bzw. Stadtbezirk"],["025170525525","Volksdorf, OT 525","Stadt-/Ortsteil bzw. Stadtbezirk"],["025180526526","Rahlstedt, OT 526","Stadt-/Ortsteil bzw. Stadtbezirk"],["026010601601","Lohbrügge, OT 601","Stadt-/Ortsteil bzw. Stadtbezirk"],["026020602602","Bergedorf, OT 602","Stadt-/Ortsteil bzw. Stadtbezirk"],["026020603603","Bergedorf, OT 603","Stadt-/Ortsteil bzw. Stadtbezirk"],["026030604604","Curslack, OT 604","Stadt-/Ortsteil bzw. Stadtbezirk"],["026040605605","Altengamme, OT 605","Stadt-/Ortsteil bzw. Stadtbezirk"],["026050606606","Neuengamme, OT 606","Stadt-/Ortsteil bzw. Stadtbezirk"],["026060607607","Kirchwerder, OT 607","Stadt-/Ortsteil bzw. Stadtbezirk"],["026070608608","Ochsenwerder, OT 608","Stadt-/Ortsteil bzw. Stadtbezirk"],["026080609609","Reitbrook, OT 609","Stadt-/Ortsteil bzw. Stadtbezirk"],["026090610610","Allermöhe, OT 610","Stadt-/Ortsteil bzw. Stadtbezirk"],["026100611611","Billwerder, OT 611","Stadt-/Ortsteil bzw. Stadtbezirk"],["026110612612","Moorfleet, OT 612","Stadt-/Ortsteil bzw. Stadtbezirk"],["026120613613","Tatenberg, OT 613","Stadt-/Ortsteil bzw. Stadtbezirk"],["026130614614","Spadenland, OT 614","Stadt-/Ortsteil bzw. Stadtbezirk"],["026140615615","Neuallermöhe, OT 615","Stadt-/Ortsteil bzw. Stadtbezirk"],["027010701701","Harburg, OT 701","Stadt-/Ortsteil bzw. Stadtbezirk"],["027010702702","Harburg, OT 702","Stadt-/Ortsteil bzw. Stadtbezirk"],["027020703703","Neuland, OT 703","Stadt-/Ortsteil bzw. Stadtbezirk"],["027030704704","Gut Moor, OT 704","Stadt-/Ortsteil bzw. Stadtbezirk"],["027040705705","Wilstorf, OT 705","Stadt-/Ortsteil bzw. Stadtbezirk"],["027050706706","Rönneburg, OT 706","Stadt-/Ortsteil bzw. Stadtbezirk"],["027060707707","Langenbek, OT 707","Stadt-/Ortsteil bzw. Stadtbezirk"],["027070708708","Sinstorf, OT 708","Stadt-/Ortsteil bzw. Stadtbezirk"],["027080709709","Marmstorf, OT 709","Stadt-/Ortsteil bzw. Stadtbezirk"],["027090710710","Eissendorf, OT 710","Stadt-/Ortsteil bzw. Stadtbezirk"],["027100711711","Heimfeld, OT 711","Stadt-/Ortsteil bzw. Stadtbezirk"],["027110712712","Moorburg, OT 712","Stadt-/Ortsteil bzw. Stadtbezirk"],["027120713713","Altenwerder, OT 713","Stadt-/Ortsteil bzw. Stadtbezirk"],["027130714714","Hausbruch, OT 714","Stadt-/Ortsteil bzw. Stadtbezirk"],["027140715715","Neugraben-Fischbek, OT 715","Stadt-/Ortsteil bzw. Stadtbezirk"],["027150716716","Francop, OT 716","Stadt-/Ortsteil bzw. Stadtbezirk"],["027160717717","Neuenfelde, OT 717","Stadt-/Ortsteil bzw. Stadtbezirk"],["027170718718","Cranz, OT 718","Stadt-/Ortsteil bzw. Stadtbezirk"],["031010000000","Braunschweig, Stadt",null],["031020000000","Salzgitter, Stadt",null],["031030000000","Wolfsburg, Stadt",null],["031510009009","Gifhorn, Stadt",null],["031510025025","Sassenburg",null],["031510040040","Wittingen, Stadt",null],["031515401002","Barwedel",null],["031515401004","Bokensdorf",null],["031515401014","Jembke",null],["031515401020","Osloß",null],["031515401030","Tappenbeck",null],["031515401039","Weyhausen",null],["031515402003","Bergfeld",null],["031515402005","Brome, Flecken",null],["031515402008","Ehra-Lessien",null],["031515402021","Parsau",null],["031515402024","Rühen",null],["031515402031","Tiddische",null],["031515402032","Tülau",null],["031515403007","Dedelstorf",null],["031515403011","Hankensbüttel",null],["031515403019","Obernholz",null],["031515403028","Sprakensehl",null],["031515403029","Steinhorst",null],["031515404006","Calberlah",null],["031515404013","Isenbüttel",null],["031515404022","Ribbesbüttel",null],["031515404037","Wasbüttel",null],["031515405012","Hillerse",null],["031515405015","Leiferde",null],["031515405017","Meinersen",null],["031515405018","Müden (Aller)",null],["031515406001","Adenbüttel",null],["031515406016","Meine",null],["031515406023","Rötgesbüttel",null],["031515406027","Schwülper",null],["031515406034","Vordorf",null],["031515406041","Didderse",null],["031515407010","Groß Oesingen",null],["031515407026","Schönewörde",null],["031515407033","Ummern",null],["031515407035","Wagenhoff",null],["031515407036","Wahrenholz",null],["031515407038","Wesendorf",null],["031519501501","Giebel, gemfr. Gebiet",null],["031530002002","Bad Harzburg, Stadt",null],["031530007007","Langelsheim, Stadt",null],["031530008008","Liebenburg",null],["031530012012","Seesen, Stadt",null],["031530016016","Braunlage, Stadt",null],["031530017017","Goslar, Stadt",null],["031530018018","Clausthal-Zellerfeld, Berg- und Universitätsstadt",null],["031535401006","Hahausen",null],["031535401009","Lutter am Barenberge, Flecken",null],["031535401014","Wallmoden",null],["031539504504","Harz (Landkreis Goslar), gemfr. Gebiet",null],["031540013013","Königslutter am Elm, Stadt",null],["031540014014","Lehre",null],["031540019019","Schöningen, Stadt",null],["031540028028","Helmstedt, Stadt",null],["031545401008","Grasleben",null],["031545401015","Mariental",null],["031545401016","Querenhorst",null],["031545401018","Rennau",null],["031545402002","Beierstedt",null],["031545402006","Gevensleben",null],["031545402012","Jerxheim",null],["031545402027","Söllingen",null],["031545403005","Frellstedt",null],["031545403017","Räbke",null],["031545403021","Süpplingen",null],["031545403022","Süpplingenburg",null],["031545403025","Warberg",null],["031545403026","Wolsdorf",null],["031545404001","Bahrdorf",null],["031545404004","Danndorf",null],["031545404007","Grafhorst",null],["031545404009","Groß Twülpstedt",null],["031545404024","Velpke",null],["031549501501","Brunsleberfeld, gemfr. Gebiet",null],["031549502502","Helmstedt, gemfr. Gebiet",null],["031549503503","Königslutter, gemfr. Gebiet",null],["031549504504","Mariental, gemfr. Gebiet",null],["031549506506","Schöningen, gemfr. Gebiet",null],["031550001001","Bad Gandersheim, Stadt",null],["031550002002","Bodenfelde, Flecken",null],["031550003003","Dassel, Stadt",null],["031550005005","Hardegsen, Stadt",null],["031550006006","Kalefeld",null],["031550007007","Katlenburg-Lindau",null],["031550009009","Moringen, Stadt",null],["031550010010","Nörten-Hardenberg, Flecken",null],["031550011011","Northeim, Stadt",null],["031550012012","Uslar, Stadt",null],["031550013013","Einbeck, Stadt",null],["031559501501","Solling (Landkreis Northeim), gemfr. Geb.",null],["031570001001","Edemissen",null],["031570002002","Hohenhameln",null],["031570005005","Lengede",null],["031570006006","Peine, Stadt",null],["031570007007","Vechelde",null],["031570008008","Wendeburg",null],["031570009009","Ilsede",null],["031580006006","Cremlingen",null],["031580037037","Wolfenbüttel, Stadt",null],["031580039039","Schladen-Werla",null],["031585402002","Baddeckenstedt",null],["031585402004","Burgdorf",null],["031585402011","Elbe",null],["031585402016","Haverlah",null],["031585402018","Heere",null],["031585402028","Sehlde",null],["031585403005","Cramme",null],["031585403010","Dorstadt",null],["031585403014","Flöthe",null],["031585403019","Heiningen",null],["031585403023","Ohrum",null],["031585403038","Börßum",null],["031585406009","Dettum",null],["031585406012","Erkerode",null],["031585406013","Evessen",null],["031585406030","Sickte",null],["031585406033","Veltheim (Ohe)",null],["031585407007","Dahlum",null],["031585407008","Denkte",null],["031585407017","Hedeper",null],["031585407021","Kissenbrück",null],["031585407022","Kneitlingen",null],["031585407025","Roklum",null],["031585407027","Schöppenstedt, Stadt",null],["031585407031","Uehrde",null],["031585407032","Vahlberg",null],["031585407035","Winnigstedt",null],["031585407036","Wittmar",null],["031585407040","Remlingen-Semmenstedt",null],["031589501501","Am Großen Rhode, gemfr. Gebiet",null],["031589502502","Barnstorf-Warle, gemfr. Gebiet",null],["031589503503","Voigtsdahlum, gemfr. Gebiet",null],["031590001001","Adelebsen, Flecken",null],["031590002002","Bad Grund (Harz)",null],["031590003003","Bad Lauterberg im Harz, Stadt",null],["031590004004","Bad Sachsa, Stadt",null],["031590007007","Bovenden, Flecken",null],["031590010010","Duderstadt, Stadt",null],["031590013013","Friedland",null],["031590015015","Gleichen",null],["031590016016","Göttingen, Stadt",null],["031590017017","Hann. Münden, Stadt",null],["031590019019","Herzberg am Harz, Stadt",null],["031590026026","Osterode am Harz, Stadt",null],["031590029029","Rosdorf",null],["031590034034","Staufenberg",null],["031590036036","Walkenried",null],["031595401008","Bühren",null],["031595401009","Dransfeld, Stadt",null],["031595401021","Jühnde",null],["031595401024","Niemetal",null],["031595401031","Scheden",null],["031595402005","Bilshausen",null],["031595402006","Bodensee",null],["031595402014","Gieboldehausen, Flecken",null],["031595402022","Krebeck",null],["031595402025","Obernfeld",null],["031595402027","Rhumspringe",null],["031595402028","Rollshausen",null],["031595402030","Rüdershausen",null],["031595402037","Wollbrandshausen",null],["031595402038","Wollershausen",null],["031595403012","Elbingerode",null],["031595403018","Hattorf am Harz",null],["031595403020","Hörden am Harz",null],["031595403039","Wulften am Harz",null],["031595404011","Ebergötzen",null],["031595404023","Landolfshausen",null],["031595404032","Seeburg",null],["031595404033","Seulingen",null],["031595404035","Waake",null],["031599501501","Harz (Landkreis Göttingen), gemfr. Geb.",null],["032410001001","Hannover, Landeshauptstadt",null],["032410002002","Barsinghausen, Stadt",null],["032410003003","Burgdorf, Stadt",null],["032410004004","Burgwedel, Stadt",null],["032410005005","Garbsen, Stadt",null],["032410006006","Gehrden, Stadt",null],["032410007007","Hemmingen, Stadt",null],["032410008008","Isernhagen",null],["032410009009","Laatzen, Stadt",null],["032410010010","Langenhagen, Stadt",null],["032410011011","Lehrte, Stadt",null],["032410012012","Neustadt am Rübenberge, Stadt",null],["032410013013","Pattensen, Stadt",null],["032410014014","Ronnenberg, Stadt",null],["032410015015","Seelze, Stadt",null],["032410016016","Sehnde, Stadt",null],["032410017017","Springe, Stadt",null],["032410018018","Uetze",null],["032410019019","Wedemark",null],["032410020020","Wennigsen (Deister)",null],["032410021021","Wunstorf, Stadt",null],["032510007007","Bassum, Stadt",null],["032510012012","Diepholz, Stadt",null],["032510037037","Stuhr",null],["032510040040","Sulingen, Stadt",null],["032510041041","Syke, Stadt",null],["032510042042","Twistringen, Stadt",null],["032510044044","Wagenfeld",null],["032510047047","Weyhe",null],["032515401009","Brockum",null],["032515401020","Hüde",null],["032515401022","Lembruch",null],["032515401023","Lemförde, Flecken",null],["032515401025","Marl",null],["032515401029","Quernheim",null],["032515401036","Stemshorn",null],["032515402005","Barnstorf, Flecken",null],["032515402013","Drebber",null],["032515402014","Drentwede",null],["032515402017","Eydelstedt",null],["032515403002","Asendorf",null],["032515403026","Martfeld",null],["032515403033","Schwarme",null],["032515403049","Bruchhausen-Vilsen, Flecken",null],["032515404003","Bahrenborstel",null],["032515404004","Barenburg, Flecken",null],["032515404018","Freistatt",null],["032515404021","Kirchdorf",null],["032515404043","Varrel",null],["032515404045","Wehrbleck",null],["032515405006","Barver",null],["032515405011","Dickel",null],["032515405019","Hemsloh",null],["032515405030","Rehden",null],["032515405046","Wetschen",null],["032515406001","Affinghausen",null],["032515406015","Ehrenburg",null],["032515406028","Neuenkirchen",null],["032515406031","Scholen",null],["032515406032","Schwaförden",null],["032515406038","Sudwalde",null],["032515407008","Borstel",null],["032515407024","Maasen",null],["032515407027","Mellinghausen",null],["032515407034","Siedenburg, Flecken",null],["032515407035","Staffhorst",null],["032520001001","Aerzen, Flecken",null],["032520002002","Bad Münder am Deister, Stadt",null],["032520003003","Bad Pyrmont, Stadt",null],["032520004004","Coppenbrügge, Flecken",null],["032520005005","Emmerthal",null],["032520006006","Hameln, Stadt",null],["032520007007","Hessisch Oldendorf, Stadt",null],["032520008008","Salzhemmendorf, Flecken",null],["032540002002","Alfeld (Leine), Stadt",null],["032540003003","Algermissen",null],["032540005005","Bad Salzdetfurth, Stadt",null],["032540008008","Bockenem, Stadt",null],["032540011011","Diekholzen",null],["032540014014","Elze, Stadt",null],["032540017017","Giesen",null],["032540020020","Harsum",null],["032540021021","Hildesheim, Stadt",null],["032540022022","Holle",null],["032540026026","Nordstemmen",null],["032540028028","Sarstedt, Stadt",null],["032540029029","Schellerten",null],["032540032032","Söhlde",null],["032540042042","Freden (Leine)",null],["032540044044","Lamspringe",null],["032540045045","Sibbesse",null],["032545406013","Eime, Flecken",null],["032545406041","Duingen, Flecken",null],["032545406043","Gronau (Leine), Stadt",null],["032550008008","Delligsen, Flecken",null],["032550023023","Holzminden, Stadt",null],["032555401002","Bevern, Flecken",null],["032555401015","Golmbach",null],["032555401021","Holenberg",null],["032555401030","Negenborn",null],["032555403004","Boffzen",null],["032555403009","Derental",null],["032555403014","Fürstenberg",null],["032555403026","Lauenförde, Flecken",null],["032555408003","Bodenwerder, Münchhausenstadt",null],["032555408005","Brevörde",null],["032555408016","Halle",null],["032555408017","Hehlen",null],["032555408019","Heinsen",null],["032555408020","Heyen",null],["032555408025","Kirchbrak",null],["032555408031","Ottenstein, Flecken",null],["032555408032","Pegestorf",null],["032555408033","Polle, Flecken",null],["032555408035","Vahlbruch",null],["032555409001","Arholzen",null],["032555409007","Deensen",null],["032555409010","Dielmissen",null],["032555409012","Eimen",null],["032555409013","Eschershausen, Stadt",null],["032555409018","Heinade",null],["032555409022","Holzen",null],["032555409027","Lenne",null],["032555409028","Lüerdissen",null],["032555409034","Stadtoldendorf, Stadt",null],["032555409036","Wangelnstedt",null],["032559501501","Boffzen, gemfr. Gebiet",null],["032559502502","Eimen, gemfr. Gebiet",null],["032559503503","Eschershausen, gemfr. Gebiet",null],["032559504504","Grünenplan, gemfr. Gebiet",null],["032559505505","Holzminden, gemfr. Gebiet",null],["032559506506","Merxhausen, gemfr. Gebiet",null],["032559508508","Wenzen, gemfr. Gebiet",null],["032560022022","Nienburg (Weser), Stadt",null],["032560025025","Rehburg-Loccum, Stadt",null],["032560030030","Steyerberg, Flecken",null],["032565402005","Drakenburg, Flecken",null],["032565402011","Haßbergen",null],["032565402012","Heemsen",null],["032565402027","Rohrsen",null],["032565405002","Binnen",null],["032565405019","Liebenau, Flecken",null],["032565405023","Pennigsehl",null],["032565406001","Balge",null],["032565406021","Marklohe",null],["032565406036","Wietzen",null],["032565407020","Linsburg",null],["032565407026","Rodewald",null],["032565407029","Steimbke",null],["032565407031","Stöckse",null],["032565408004","Diepenau, Flecken",null],["032565408024","Raddestorf",null],["032565408033","Uchte, Flecken",null],["032565408034","Warmsen",null],["032565409003","Bücken, Flecken",null],["032565409007","Eystrup",null],["032565409008","Gandesbergen",null],["032565409009","Hämelhausen",null],["032565409010","Hassel (Weser)",null],["032565409013","Hilgermissen",null],["032565409014","Hoya, Stadt",null],["032565409015","Hoyerhagen",null],["032565409028","Schweringen",null],["032565409035","Warpe",null],["032565410006","Estorf",null],["032565410016","Husum",null],["032565410017","Landesbergen",null],["032565410018","Leese",null],["032565410032","Stolzenau",null],["032570003003","Auetal",null],["032570009009","Bückeburg, Stadt",null],["032570028028","Obernkirchen, Stadt",null],["032570031031","Rinteln, Stadt",null],["032570035035","Stadthagen, Stadt",null],["032575401001","Ahnsen",null],["032575401005","Bad Eilsen",null],["032575401008","Buchholz",null],["032575401012","Heeßen",null],["032575401022","Luhden",null],["032575402007","Beckedorf",null],["032575402015","Heuerßen",null],["032575402020","Lindhorst",null],["032575402021","Lüdersfeld",null],["032575403006","Bad Nenndorf, Stadt",null],["032575403011","Haste",null],["032575403016","Hohnhorst",null],["032575403036","Suthfeld",null],["032575404019","Lauenhagen",null],["032575404023","Meerbeck",null],["032575404025","Niedernwöhren",null],["032575404027","Nordsehl",null],["032575404030","Pollhagen",null],["032575404037","Wiedensahl, Flecken",null],["032575405013","Helpsen",null],["032575405014","Hespe",null],["032575405026","Nienstädt",null],["032575405034","Seggebruch",null],["032575406002","Apelern",null],["032575406017","Hülsede",null],["032575406018","Lauenau, Flecken",null],["032575406024","Messenkamp",null],["032575406029","Pohle",null],["032575406032","Rodenberg, Stadt",null],["032575407004","Auhagen",null],["032575407010","Hagenburg, Flecken",null],["032575407033","Sachsenhagen, Stadt",null],["032575407038","Wölpinghausen",null],["033510004004","Bergen, Stadt",null],["033510006006","Celle, Stadt",null],["033510010010","Faßberg",null],["033510012012","Hambühren",null],["033510023023","Wietze",null],["033510024024","Winsen (Aller)",null],["033510025025","Eschede",null],["033510026026","Südheide",null],["033515402005","Bröckel",null],["033515402007","Eicklingen",null],["033515402017","Langlingen",null],["033515402022","Wienhausen, Klostergemeinde",null],["033515403002","Ahnsbeck",null],["033515403003","Beedenbostel",null],["033515403008","Eldingen",null],["033515403015","Hohne",null],["033515403016","Lachendorf",null],["033515404001","Adelheidsdorf",null],["033515404018","Nienhagen",null],["033515404021","Wathlingen",null],["033519501501","Lohheide, gemfr. Bezirk",null],["033520011011","Cuxhaven, Stadt",null],["033520032032","Loxstedt",null],["033520050050","Schiffdorf",null],["033520059059","Beverstedt",null],["033520060060","Hagen im Bremischen",null],["033520061061","Wurster Nordseeküste",null],["033520062062","Geestland, Stadt",null],["033525404002","Armstorf",null],["033525404024","Hollnseth",null],["033525404029","Lamstedt",null],["033525404036","Mittelstenahe",null],["033525404052","Stinstedt",null],["033525407020","Hechthausen",null],["033525407022","Hemmoor, Stadt",null],["033525407044","Osten",null],["033525411004","Belum",null],["033525411008","Bülkau",null],["033525411025","Ihlienworth",null],["033525411038","Neuenkirchen",null],["033525411039","Neuhaus (Oste), Flecken",null],["033525411041","Nordleda",null],["033525411042","Oberndorf",null],["033525411043","Odisheim",null],["033525411045","Osterbruch",null],["033525411046","Otterndorf, Stadt",null],["033525411051","Steinau",null],["033525411055","Wanna",null],["033525411056","Wingst",null],["033525411063","Cadenberge",null],["033530005005","Buchholz in der Nordheide, Stadt",null],["033530026026","Neu Wulmstorf",null],["033530029029","Rosengarten",null],["033530031031","Seevetal",null],["033530032032","Stelle",null],["033530040040","Winsen (Luhe), Stadt",null],["033535401007","Drage",null],["033535401023","Marschacht",null],["033535401033","Tespe",null],["033535402002","Asendorf",null],["033535402004","Brackel",null],["033535402009","Egestorf",null],["033535402016","Hanstedt",null],["033535402024","Marxen",null],["033535402036","Undeloh",null],["033535403001","Appel",null],["033535403008","Drestedt",null],["033535403014","Halvesbostel",null],["033535403019","Hollenstedt",null],["033535403025","Moisburg",null],["033535403028","Regesbostel",null],["033535403039","Wenzendorf",null],["033535404003","Bendestorf",null],["033535404017","Harmstorf",null],["033535404020","Jesteburg",null],["033535405010","Eyendorf",null],["033535405011","Garlstorf",null],["033535405012","Garstedt",null],["033535405013","Gödenstorf",null],["033535405030","Salzhausen",null],["033535405034","Toppenstedt",null],["033535405037","Vierhöfen",null],["033535405042","Wulfsen",null],["033535406006","Dohren",null],["033535406015","Handeloh",null],["033535406018","Heidenau",null],["033535406021","Kakenstorf",null],["033535406022","Königsmoor",null],["033535406027","Otter",null],["033535406035","Tostedt",null],["033535406038","Welle",null],["033535406041","Wistedt",null],["033545403005","Gartow, Flecken",null],["033545403007","Gorleben",null],["033545403010","Höhbeck",null],["033545403020","Prezelle",null],["033545403021","Schnackenburg, Stadt",null],["033545406003","Damnatz",null],["033545406004","Dannenberg (Elbe), Stadt",null],["033545406006","Göhrde",null],["033545406008","Gusborn",null],["033545406009","Hitzacker (Elbe), Stadt",null],["033545406011","Jameln",null],["033545406012","Karwitz",null],["033545406014","Langendorf",null],["033545406019","Neu Darchau",null],["033545406027","Zernien",null],["033545407001","Bergen an der Dumme, Flecken",null],["033545407002","Clenze, Flecken",null],["033545407013","Küsten",null],["033545407015","Lemgow",null],["033545407016","Luckau (Wendland)",null],["033545407017","Lübbow",null],["033545407018","Lüchow (Wendland), Stadt",null],["033545407022","Schnega",null],["033545407023","Trebel",null],["033545407024","Waddeweitz",null],["033545407025","Woltersdorf",null],["033545407026","Wustrow (Wendland), Stadt",null],["033549501501","Gartow, gemfr. Gebiet",null],["033549502502","Göhrde, gemfr. Gebiet",null],["033550001001","Adendorf",null],["033550009009","Bleckede, Stadt",null],["033550022022","Lüneburg, Hansestadt",null],["033550049049","Amt Neuhaus",null],["033555401002","Amelinghausen",null],["033555401008","Betzendorf",null],["033555401027","Oldendorf (Luhe)",null],["033555401029","Rehlingen",null],["033555401034","Soderstorf",null],["033555402004","Bardowick, Flecken",null],["033555402007","Barum",null],["033555402017","Handorf",null],["033555402023","Mechtersen",null],["033555402028","Radbruch",null],["033555402039","Vögelsen",null],["033555402042","Wittorf",null],["033555403010","Boitze",null],["033555403012","Dahlem",null],["033555403013","Dahlenburg, Flecken",null],["033555403025","Nahrendorf",null],["033555403037","Tosterglope",null],["033555404020","Kirchgellersen",null],["033555404031","Reppenstedt",null],["033555404035","Südergellersen",null],["033555404041","Westergellersen",null],["033555405006","Barnstedt",null],["033555405014","Deutsch Evern",null],["033555405016","Embsen",null],["033555405024","Melbeck",null],["033555406005","Barendorf",null],["033555406026","Neetze",null],["033555406030","Reinstorf",null],["033555406036","Thomasburg",null],["033555406038","Vastorf",null],["033555406040","Wendisch Evern",null],["033555407003","Artlenburg, Flecken",null],["033555407011","Brietlingen",null],["033555407015","Echem",null],["033555407018","Hittbergen",null],["033555407019","Hohnstorf (Elbe)",null],["033555407021","Lüdersburg",null],["033555407032","Rullstorf",null],["033555407033","Scharnebeck",null],["033560002002","Grasberg",null],["033560005005","Lilienthal",null],["033560007007","Osterholz-Scharmbeck, Stadt",null],["033560008008","Ritterhude",null],["033560009009","Schwanewede",null],["033560011011","Worpswede",null],["033565401001","Axstedt",null],["033565401003","Hambergen",null],["033565401004","Holste",null],["033565401006","Lübberstedt",null],["033565401010","Vollersode",null],["033570008008","Bremervörde, Stadt",null],["033570016016","Gnarrenburg",null],["033570039039","Rotenburg (Wümme), Stadt",null],["033570041041","Scheeßel",null],["033570051051","Visselhövede, Stadt",null],["033575401006","Bothel",null],["033575401009","Brockel",null],["033575401024","Hemsbünde",null],["033575401025","Hemslingen",null],["033575401031","Kirchwalsede",null],["033575401054","Westerwalsede",null],["033575402015","Fintel",null],["033575402023","Helvesiek",null],["033575402033","Lauenbrück",null],["033575402046","Stemmen",null],["033575402049","Vahlde",null],["033575403002","Alfstedt",null],["033575403004","Basdahl",null],["033575403012","Ebersdorf",null],["033575403027","Hipstedt",null],["033575403035","Oerel",null],["033575404003","Anderlingen",null],["033575404011","Deinstedt",null],["033575404014","Farven",null],["033575404036","Ostereistedt",null],["033575404038","Rhade",null],["033575404040","Sandbostel",null],["033575404042","Seedorf",null],["033575404043","Selsingen",null],["033575405017","Groß Meckelsen",null],["033575405019","Hamersen",null],["033575405029","Kalbe",null],["033575405032","Klein Meckelsen",null],["033575405034","Lengenbostel",null],["033575405044","Sittensen",null],["033575405048","Tiste",null],["033575405050","Vierden",null],["033575405056","Wohnste",null],["033575406001","Ahausen",null],["033575406005","Bötersen",null],["033575406020","Hassendorf",null],["033575406022","Hellwege",null],["033575406028","Horstedt",null],["033575406037","Reeßum",null],["033575406045","Sottrum",null],["033575407007","Breddorf",null],["033575407010","Bülstedt",null],["033575407026","Hepstedt",null],["033575407030","Kirchtimke",null],["033575407047","Tarmstedt",null],["033575407052","Vorwerk",null],["033575407053","Westertimke",null],["033575407055","Wilstedt",null],["033575408013","Elsdorf",null],["033575408018","Gyhum",null],["033575408021","Heeslingen",null],["033575408057","Zeven, Stadt",null],["033580002002","Bispingen",null],["033580008008","Bad Fallingbostel, Stadt",null],["033580016016","Munster, Stadt",null],["033580017017","Neuenkirchen",null],["033580019019","Schneverdingen, Stadt",null],["033580021021","Soltau, Stadt",null],["033580023023","Wietzendorf",null],["033580024024","Walsrode, Stadt",null],["033585401001","Ahlden (Aller), Flecken",null],["033585401006","Eickeloh",null],["033585401011","Grethem",null],["033585401012","Hademstorf",null],["033585401014","Hodenhagen",null],["033585402003","Böhme",null],["033585402009","Frankenfeld",null],["033585402013","Häuslingen",null],["033585402018","Rethem (Aller), Stadt",null],["033585403005","Buchholz (Aller)",null],["033585403007","Essel",null],["033585403010","Gilten",null],["033585403015","Lindwedel",null],["033585403020","Schwarmstedt",null],["033589501501","Osterheide, gemfr. Bezirk",null],["033590010010","Buxtehude, Hansestadt",null],["033590013013","Drochtersen",null],["033590028028","Jork",null],["033590038038","Stade, Hansestadt",null],["033595401003","Apensen",null],["033595401006","Beckdorf",null],["033595401037","Sauensiek",null],["033595402011","Deinste",null],["033595402017","Fredenbeck",null],["033595402031","Kutenholz",null],["033595403002","Ahlerstedt",null],["033595403005","Bargstedt",null],["033595403008","Brest",null],["033595403023","Harsefeld, Flecken",null],["033595405001","Agathenburg",null],["033595405007","Bliedersdorf",null],["033595405012","Dollern",null],["033595405027","Horneburg, Flecken",null],["033595405034","Nottensdorf",null],["033595406020","Grünendeich",null],["033595406021","Guderhandviertel",null],["033595406026","Hollern-Twielenfleth",null],["033595406032","Mittelnkirchen",null],["033595406033","Neuenkirchen",null],["033595406039","Steinkirchen",null],["033595407004","Balje",null],["033595407018","Freiburg (Elbe), Flecken",null],["033595407030","Krummendeich",null],["033595407035","Oederquart",null],["033595407040","Wischhafen",null],["033595409009","Burweg",null],["033595409014","Düdenbüttel",null],["033595409015","Engelschoff",null],["033595409016","Estorf",null],["033595409019","Großenwörden",null],["033595409022","Hammah",null],["033595409024","Heinbockel",null],["033595409025","Himmelpforten",null],["033595409029","Kranenburg",null],["033595409036","Oldendorf",null],["033600004004","Bienenbüttel",null],["033600025025","Uelzen, Hansestadt",null],["033605404015","Oetzen",null],["033605404016","Rätzlingen",null],["033605404018","Rosche",null],["033605404022","Stoetze",null],["033605404024","Suhlendorf",null],["033605405007","Eimke",null],["033605405009","Gerdau",null],["033605405023","Suderburg",null],["033605407001","Altenmedingen",null],["033605407002","Bad Bevensen, Stadt",null],["033605407003","Barum",null],["033605407006","Ebstorf,Klosterflecken",null],["033605407008","Emmendorf",null],["033605407010","Hanstedt",null],["033605407011","Himbergen",null],["033605407012","Jelmstorf",null],["033605407014","Natendorf",null],["033605407017","Römstedt",null],["033605407019","Schwienau",null],["033605407026","Weste",null],["033605407029","Wriedel",null],["033605408005","Bad Bodenteich, Flecken",null],["033605408013","Lüder",null],["033605408020","Soltendieck",null],["033605408030","Wrestedt",null],["033610001001","Achim, Stadt",null],["033610003003","Dörverden",null],["033610005005","Kirchlinteln",null],["033610006006","Langwedel, Flecken",null],["033610008008","Ottersberg, Flecken",null],["033610009009","Oyten",null],["033610012012","Verden (Aller), Stadt",null],["033615401002","Blender",null],["033615401004","Emtinghausen",null],["033615401010","Riede",null],["033615401013","Thedinghausen",null],["034010000000","Delmenhorst, Stadt",null],["034020000000","Emden, Stadt",null],["034030000000","Oldenburg (Oldenburg), Stadt",null],["034040000000","Osnabrück, Stadt",null],["034050000000","Wilhelmshaven, Stadt",null],["034510001001","Apen",null],["034510002002","Bad Zwischenahn",null],["034510004004","Edewecht",null],["034510005005","Rastede",null],["034510007007","Westerstede, Stadt",null],["034510008008","Wiefelstede",null],["034520001001","Aurich, Stadt",null],["034520002002","Baltrum",null],["034520006006","Großefehn",null],["034520007007","Großheide",null],["034520011011","Hinte",null],["034520012012","Ihlow",null],["034520013013","Juist, Inselgemeinde",null],["034520014014","Krummhörn",null],["034520019019","Norden, Stadt",null],["034520020020","Norderney, Stadt",null],["034520023023","Südbrookmerland",null],["034520025025","Wiesmoor, Stadt",null],["034520027027","Dornum",null],["034525401015","Leezdorf",null],["034525401017","Marienhafe, Flecken",null],["034525401021","Osteel",null],["034525401022","Rechtsupweg",null],["034525401024","Upgant-Schott",null],["034525401026","Wirdum",null],["034525403003","Berumbur",null],["034525403008","Hage, Flecken",null],["034525403009","Hagermarsch",null],["034525403010","Halbemond",null],["034525403016","Lütetsburg",null],["034529501501","Nordseeinsel Memmert, gemfr. Gebiet",null],["034530001001","Barßel",null],["034530002002","Bösel",null],["034530003003","Cappeln (Oldenburg)",null],["034530004004","Cloppenburg, Stadt",null],["034530005005","Emstek",null],["034530006006","Essen (Oldenburg)",null],["034530007007","Friesoythe, Stadt",null],["034530008008","Garrel",null],["034530009009","Lastrup",null],["034530010010","Lindern (Oldenburg)",null],["034530011011","Löningen, Stadt",null],["034530012012","Molbergen",null],["034530013013","Saterland",null],["034540010010","Emsbüren",null],["034540014014","Geeste",null],["034540018018","Haren (Ems), Stadt",null],["034540019019","Haselünne, Stadt",null],["034540032032","Lingen (Ems), Stadt",null],["034540035035","Meppen, Stadt",null],["034540041041","Papenburg, Stadt",null],["034540044044","Rhede (Ems)",null],["034540045045","Salzbergen",null],["034540054054","Twist",null],["034545401007","Dersum",null],["034545401008","Dörpen",null],["034545401020","Heede",null],["034545401025","Kluse",null],["034545401030","Lehe",null],["034545401037","Neubörger",null],["034545401038","Neulehe",null],["034545401056","Walchum",null],["034545401060","Wippingen",null],["034545402001","Andervenne",null],["034545402003","Beesten",null],["034545402012","Freren, Stadt",null],["034545402036","Messingen",null],["034545402053","Thuine",null],["034545403009","Dohren",null],["034545403021","Herzlake",null],["034545403026","Lähden",null],["034545404013","Fresenburg",null],["034545404029","Lathen",null],["034545404039","Niederlangen",null],["034545404040","Oberlangen",null],["034545404043","Renkenberge",null],["034545404052","Sustrum",null],["034545405002","Bawinkel",null],["034545405015","Gersten",null],["034545405017","Handrup",null],["034545405028","Langen",null],["034545405031","Lengerich",null],["034545405059","Wettrup",null],["034545406004","Bockhorst",null],["034545406006","Breddenberg",null],["034545406011","Esterwegen",null],["034545406022","Hilkenbrook",null],["034545406051","Surwold",null],["034545407005","Börger",null],["034545407016","Groß Berßen",null],["034545407023","Hüven",null],["034545407024","Klein Berßen",null],["034545407047","Sögel",null],["034545407048","Spahnharrenstätte",null],["034545407050","Stavern",null],["034545407058","Werpeloh",null],["034545408034","Lünne",null],["034545408046","Schapen",null],["034545408049","Spelle",null],["034545409027","Lahn",null],["034545409033","Lorup",null],["034545409042","Rastdorf",null],["034545409055","Vrees",null],["034545409057","Werlte, Stadt",null],["034550007007","Jever, Stadt",null],["034550014014","Sande",null],["034550015015","Schortens, Stadt",null],["034550020020","Wangerland",null],["034550021021","Wangerooge, Nordseebad",null],["034550025025","Bockhorn",null],["034550026026","Varel, Stadt",null],["034550027027","Zetel",null],["034560001001","Bad Bentheim, Stadt",null],["034560015015","Nordhorn, Stadt",null],["034560025025","Wietmarschen",null],["034565401002","Emlichheim",null],["034565401009","Hoogstede",null],["034565401012","Laar",null],["034565401019","Ringe",null],["034565402004","Esche",null],["034565402005","Georgsdorf",null],["034565402013","Lage",null],["034565402014","Neuenhaus, Stadt",null],["034565402017","Osterwald",null],["034565403003","Engden",null],["034565403010","Isterberg",null],["034565403016","Ohne",null],["034565403018","Quendorf",null],["034565403020","Samern",null],["034565403027","Schüttorf, Stadt",null],["034565404006","Getelo",null],["034565404007","Gölenkamp",null],["034565404008","Halle",null],["034565404011","Itterbeck",null],["034565404023","Uelsen",null],["034565404024","Wielen",null],["034565404026","Wilsum",null],["034570002002","Borkum, Stadt",null],["034570012012","Jemgum",null],["034570013013","Leer (Ostfriesland), Stadt",null],["034570014014","Moormerland",null],["034570017017","Ostrhauderfehn",null],["034570018018","Rhauderfehn",null],["034570020020","Uplengen",null],["034570021021","Weener, Stadt",null],["034570022022","Westoverledingen",null],["034570024024","Bunde",null],["034575402003","Brinkum",null],["034575402009","Firrel",null],["034575402010","Hesel",null],["034575402011","Holtland",null],["034575402015","Neukamperfehn",null],["034575402019","Schwerinsdorf",null],["034575403006","Detern, Flecken",null],["034575403008","Filsum",null],["034575403016","Nortmoor",null],["034579501501","Insel Lütje Hörn, gemfr. Gebiet",null],["034580003003","Dötlingen",null],["034580005005","Ganderkesee",null],["034580007007","Großenkneten",null],["034580009009","Hatten",null],["034580010010","Hude (Oldb)",null],["034580013013","Wardenburg",null],["034580014014","Wildeshausen, Stadt",null],["034585401001","Beckeln",null],["034585401002","Colnrade",null],["034585401004","Dünsen",null],["034585401006","Groß Ippener",null],["034585401008","Harpstedt, Flecken",null],["034585401011","Kirchseelte",null],["034585401012","Prinzhöfte",null],["034585401015","Winkelsett",null],["034590003003","Bad Essen",null],["034590004004","Bad Iburg, Stadt",null],["034590005005","Bad Laer",null],["034590006006","Bad Rothenfelde",null],["034590008008","Belm",null],["034590012012","Bissendorf",null],["034590013013","Bohmte",null],["034590014014","Bramsche, Stadt",null],["034590015015","Dissen am Teutoburger Wald, Stadt",null],["034590019019","Georgsmarienhütte, Stadt",null],["034590020020","Hagen am Teutoburger Wald",null],["034590021021","Hasbergen",null],["034590022022","Hilter am Teutoburger Wald",null],["034590024024","Melle, Stadt",null],["034590029029","Ostercappeln",null],["034590033033","Wallenhorst",null],["034590034034","Glandorf",null],["034595401007","Badbergen",null],["034595401025","Menslage",null],["034595401028","Nortrup",null],["034595401030","Quakenbrück, Stadt",null],["034595402001","Alfhausen",null],["034595402002","Ankum",null],["034595402010","Bersenbrück, Stadt",null],["034595402016","Eggermühlen",null],["034595402018","Gehrde",null],["034595402023","Kettenkamp",null],["034595402031","Rieste",null],["034595403009","Berge",null],["034595403011","Bippen",null],["034595403017","Fürstenau, Stadt",null],["034595404026","Merzen",null],["034595404027","Neuenkirchen",null],["034595404032","Voltlage",null],["034600001001","Bakum",null],["034600002002","Damme, Stadt",null],["034600003003","Dinklage, Stadt",null],["034600004004","Goldenstedt",null],["034600005005","Holdorf",null],["034600006006","Lohne (Oldenburg), Stadt",null],["034600007007","Neuenkirchen-Vörden",null],["034600008008","Steinfeld (Oldenburg)",null],["034600009009","Vechta, Stadt",null],["034600010010","Visbek",null],["034610001001","Berne",null],["034610002002","Brake (Unterweser), Stadt",null],["034610003003","Butjadingen",null],["034610004004","Elsfleth, Stadt",null],["034610005005","Jade",null],["034610006006","Lemwerder",null],["034610007007","Nordenham, Stadt",null],["034610008008","Ovelgönne",null],["034610009009","Stadland",null],["034620005005","Friedeburg",null],["034620007007","Langeoog",null],["034620014014","Spiekeroog",null],["034620019019","Wittmund, Stadt",null],["034625401002","Dunum",null],["034625401003","Esens, Stadt",null],["034625401006","Holtgast",null],["034625401008","Moorweg",null],["034625401010","Neuharlingersiel",null],["034625401015","Stedesdorf",null],["034625401017","Werdum",null],["034625402001","Blomberg",null],["034625402004","Eversmeer",null],["034625402009","Nenndorf",null],["034625402011","Neuschoo",null],["034625402012","Ochtersum",null],["034625402013","Schweindorf",null],["034625402016","Utarp",null],["034625402018","Westerholt",null],["039019999999","Nds-Küstengewässer(Gemarkung Nordsee)",null],["040110000000","Bremen, Stadt",null],["040110111111","Altstadt","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110112112","Bahnhofsvorstadt","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110113113","Ostertor","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110122122","Industriehäfen","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110123123","Stadtbremisches Überseehafengebiet Bremerhaven","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110124124","Neustädter Hafen","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110125125","Hohentorshafen","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110211211","Alte Neustadt","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110212212","Hohentor","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110213213","Neustadt","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110214214","Südervorstadt","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110215215","Gartenstadt Süd","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110216216","Buntentor","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110217217","Neuenland","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110218218","Huckelriede","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110231231","Habenhausen","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110232232","Arsten","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110233233","Kattenturm","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110234234","Kattenesch","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110241241","Mittelshuchting","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110242242","Sodenmatt","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110243243","Kirchhuchting","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110244244","Grolland","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110251251","Woltmershausen","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110252252","Rablinghausen","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110261261","Seehausen","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110271271","Strom","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110311311","Steintor","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110312312","Fesenfeld","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110313313","Peterswerder","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110314314","Hulsberg","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110321321","Neu-Schwachhausen","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110322322","Bürgerpark","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110323323","Barkhof","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110324324","Riensberg","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110325325","Radio Bremen","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110326326","Schwachhausen","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110327327","Gete","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110331331","Gartenstadt Vahr","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110332332","Neue Vahr Nord","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110334334","Neue Vahr Südwest","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110335335","Neue Vahr Südost","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110341341","Horn","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110342342","Lehe","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110343343","Lehesterdeich","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110351351","Borgfeld","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110361361","Oberneuland","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110371371","Ellener Feld","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110372372","Ellenerbrok-Schevemoor","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110373373","Tenever","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110374374","Osterholz","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110375375","Blockdiek","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110381381","Sebaldsbrück","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110382382","Hastedt","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110383383","Hemelingen","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110384384","Arbergen","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110385385","Mahndorf","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110411411","Blockland","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110421421","Regensburger Straße","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110422422","Findorff-Bürgerweide","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110423423","Weidedamm","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110424424","In den Hufen","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110431431","Utbremen","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110432432","Steffensweg","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110433433","Westend","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110434434","Walle","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110435435","Osterfeuerberg","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110436436","Hohweg","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110437437","Überseestadt","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110441441","Lindenhof","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110442442","Gröpelingen","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110443443","Ohlenhof","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110444444","In den Wischen","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110445445","Oslebshausen","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110511511","Burg-Grambke","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110512512","Werderland","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110513513","Burgdamm","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110514514","Lesum","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110515515","St. Magnus","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110521521","Vegesack","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110522522","Grohn","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110523523","Schönebeck","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110524524","Aumund-Hammersbeck","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110525525","Fähr-Lobbendorf","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110531531","Blumenthal","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110532532","Rönnebeck","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110533533","Lüssum-Bockhorn","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110534534","Farge","Stadt-/Ortsteil bzw. Stadtbezirk"],["040110535535","Rekum","Stadt-/Ortsteil bzw. Stadtbezirk"],["040120000000","Bremerhaven, Stadt",null],["051110000000","Düsseldorf, Stadt",null],["051120000000","Duisburg, Stadt",null],["051130000000","Essen, Stadt",null],["051140000000","Krefeld, Stadt",null],["051160000000","Mönchengladbach, Stadt",null],["051170000000","Mülheim an der Ruhr, Stadt",null],["051190000000","Oberhausen, Stadt",null],["051200000000","Remscheid, Stadt",null],["051220000000","Solingen, Klingenstadt",null],["051240000000","Wuppertal, Stadt",null],["051540004004","Bedburg-Hau",null],["051540008008","Emmerich am Rhein, Stadt",null],["051540012012","Geldern, Stadt",null],["051540016016","Goch, Stadt",null],["051540020020","Issum",null],["051540024024","Kalkar, Stadt",null],["051540028028","Kerken",null],["051540032032","Kevelaer, Stadt",null],["051540036036","Kleve, Stadt",null],["051540040040","Kranenburg",null],["051540044044","Rees, Stadt",null],["051540048048","Rheurdt",null],["051540052052","Straelen, Stadt",null],["051540056056","Uedem",null],["051540060060","Wachtendonk",null],["051540064064","Weeze",null],["051580004004","Erkrath, Fundort des Neanderthalers, Stadt",null],["051580008008","Haan, Stadt",null],["051580012012","Heiligenhaus, Stadt",null],["051580016016","Hilden, Stadt",null],["051580020020","Langenfeld (Rheinland), Stadt",null],["051580024024","Mettmann, Stadt",null],["051580026026","Monheim am Rhein, Stadt",null],["051580028028","Ratingen, Stadt",null],["051580032032","Velbert, Stadt",null],["051580036036","Wülfrath, Stadt",null],["051620004004","Dormagen, Stadt",null],["051620008008","Grevenbroich, Stadt",null],["051620012012","Jüchen, Stadt",null],["051620016016","Kaarst, Stadt",null],["051620020020","Korschenbroich, Stadt",null],["051620022022","Meerbusch, Stadt",null],["051620024024","Neuss, Stadt",null],["051620028028","Rommerskirchen",null],["051660004004","Brüggen, Burggemeinde",null],["051660008008","Grefrath, Sport- und Freizeitgemeinde",null],["051660012012","Kempen, Stadt",null],["051660016016","Nettetal, Stadt",null],["051660020020","Niederkrüchten",null],["051660024024","Schwalmtal",null],["051660028028","Tönisvorst, Stadt",null],["051660032032","Viersen, Stadt",null],["051660036036","Willich, Stadt",null],["051700004004","Alpen",null],["051700008008","Dinslaken, Stadt",null],["051700012012","Hamminkeln, Stadt",null],["051700016016","Hünxe",null],["051700020020","Kamp-Lintfort, Stadt",null],["051700024024","Moers, Stadt",null],["051700028028","Neukirchen-Vluyn, Stadt",null],["051700032032","Rheinberg, Stadt",null],["051700036036","Schermbeck",null],["051700040040","Sonsbeck",null],["051700044044","Voerde (Niederrhein), Stadt",null],["051700048048","Wesel, Stadt",null],["051700052052","Xanten, Stadt",null],["053140000000","Bonn, Stadt",null],["053150000000","Köln, Stadt",null],["053160000000","Leverkusen, Stadt",null],["053340002002","Aachen, Stadt",null],["053340004004","Alsdorf, Stadt",null],["053340008008","Baesweiler, Stadt",null],["053340012012","Eschweiler, Stadt",null],["053340016016","Herzogenrath, Stadt",null],["053340020020","Monschau, Stadt",null],["053340024024","Roetgen, Tor zur Eifel",null],["053340028028","Simmerath",null],["053340032032","Stolberg (Rhld.), Kupferstadt",null],["053340036036","Würselen, Stadt",null],["053580004004","Aldenhoven",null],["053580008008","Düren, Stadt",null],["053580012012","Heimbach, Stadt",null],["053580016016","Hürtgenwald",null],["053580020020","Inden",null],["053580024024","Jülich, Stadt",null],["053580028028","Kreuzau",null],["053580032032","Langerwehe",null],["053580036036","Linnich, Stadt",null],["053580040040","Merzenich",null],["053580044044","Nideggen, Stadt",null],["053580048048","Niederzier",null],["053580052052","Nörvenich",null],["053580056056","Titz",null],["053580060060","Vettweiß",null],["053620004004","Bedburg, Stadt",null],["053620008008","Bergheim, Stadt",null],["053620012012","Brühl, Stadt",null],["053620016016","Elsdorf, Stadt",null],["053620020020","Erftstadt, Stadt",null],["053620024024","Frechen, Stadt",null],["053620028028","Hürth, Stadt",null],["053620032032","Kerpen, Kolpingstadt",null],["053620036036","Pulheim, Stadt",null],["053620040040","Wesseling, Stadt",null],["053660004004","Bad Münstereifel, Stadt",null],["053660008008","Blankenheim",null],["053660012012","Dahlem",null],["053660016016","Euskirchen, Stadt",null],["053660020020","Hellenthal",null],["053660024024","Kall",null],["053660028028","Mechernich, Stadt",null],["053660032032","Nettersheim",null],["053660036036","Schleiden, Stadt",null],["053660040040","Weilerswist",null],["053660044044","Zülpich, Stadt",null],["053700004004","Erkelenz, Stadt",null],["053700008008","Gangelt",null],["053700012012","Geilenkirchen, Stadt",null],["053700016016","Heinsberg, Stadt",null],["053700020020","Hückelhoven, Stadt",null],["053700024024","Selfkant",null],["053700028028","Übach-Palenberg, Stadt",null],["053700032032","Waldfeucht",null],["053700036036","Wassenberg, Stadt",null],["053700040040","Wegberg, Stadt",null],["053740004004","Bergneustadt, Stadt",null],["053740008008","Engelskirchen",null],["053740012012","Gummersbach, Stadt",null],["053740016016","Hückeswagen, Schloss-Stadt",null],["053740020020","Lindlar",null],["053740024024","Marienheide",null],["053740028028","Morsbach",null],["053740032032","Nümbrecht",null],["053740036036","Radevormwald, Stadt auf der Höhe",null],["053740040040","Reichshof",null],["053740044044","Waldbröl, Stadt",null],["053740048048","Wiehl, Stadt",null],["053740052052","Wipperfürth, Hansestadt",null],["053780004004","Bergisch Gladbach, Stadt",null],["053780008008","Burscheid, Stadt",null],["053780012012","Kürten",null],["053780016016","Leichlingen (Rheinland), Blütenstadt",null],["053780020020","Odenthal",null],["053780024024","Overath, Stadt",null],["053780028028","Rösrath, Stadt",null],["053780032032","Wermelskirchen, Stadt",null],["053820004004","Alfter",null],["053820008008","Bad Honnef, Stadt",null],["053820012012","Bornheim, Stadt",null],["053820016016","Eitorf",null],["053820020020","Hennef (Sieg), Stadt",null],["053820024024","Königswinter, Stadt",null],["053820028028","Lohmar, Stadt",null],["053820032032","Meckenheim, Stadt",null],["053820036036","Much",null],["053820040040","Neunkirchen-Seelscheid",null],["053820044044","Niederkassel, Stadt",null],["053820048048","Rheinbach, Stadt",null],["053820052052","Ruppichteroth",null],["053820056056","Sankt Augustin, Stadt",null],["053820060060","Siegburg, Stadt",null],["053820064064","Swisttal",null],["053820068068","Troisdorf, Stadt",null],["053820072072","Wachtberg",null],["053820076076","Windeck",null],["055120000000","Bottrop, Stadt",null],["055130000000","Gelsenkirchen, Stadt",null],["055150000000","Münster, Stadt",null],["055540004004","Ahaus, Stadt",null],["055540008008","Bocholt, Stadt",null],["055540012012","Borken, Stadt",null],["055540016016","Gescher, Glockenstadt",null],["055540020020","Gronau (Westf.), Stadt",null],["055540024024","Heek",null],["055540028028","Heiden",null],["055540032032","Isselburg, Stadt",null],["055540036036","Legden",null],["055540040040","Raesfeld",null],["055540044044","Reken",null],["055540048048","Rhede, Stadt",null],["055540052052","Schöppingen",null],["055540056056","Stadtlohn, Stadt",null],["055540060060","Südlohn",null],["055540064064","Velen, Stadt",null],["055540068068","Vreden, Stadt",null],["055580004004","Ascheberg",null],["055580008008","Billerbeck, Stadt",null],["055580012012","Coesfeld, Stadt",null],["055580016016","Dülmen, Stadt",null],["055580020020","Havixbeck",null],["055580024024","Lüdinghausen, Stadt",null],["055580028028","Nordkirchen",null],["055580032032","Nottuln",null],["055580036036","Olfen, Stadt",null],["055580040040","Rosendahl",null],["055580044044","Senden",null],["055620004004","Castrop-Rauxel, Stadt",null],["055620008008","Datteln, Stadt",null],["055620012012","Dorsten, Stadt",null],["055620014014","Gladbeck, Stadt",null],["055620016016","Haltern am See, Stadt",null],["055620020020","Herten, Stadt",null],["055620024024","Marl, Stadt",null],["055620028028","Oer-Erkenschwick, Stadt",null],["055620032032","Recklinghausen, Stadt",null],["055620036036","Waltrop, Stadt",null],["055660004004","Altenberge",null],["055660008008","Emsdetten, Stadt",null],["055660012012","Greven, Stadt",null],["055660016016","Hörstel, Stadt",null],["055660020020","Hopsten",null],["055660024024","Horstmar, Stadt der Burgmannshöfe",null],["055660028028","Ibbenbüren, Stadt",null],["055660032032","Ladbergen",null],["055660036036","Laer",null],["055660040040","Lengerich, Stadt",null],["055660044044","Lienen",null],["055660048048","Lotte",null],["055660052052","Metelen",null],["055660056056","Mettingen",null],["055660060060","Neuenkirchen",null],["055660064064","Nordwalde",null],["055660068068","Ochtrup, Stadt",null],["055660072072","Recke",null],["055660076076","Rheine, Stadt",null],["055660080080","Saerbeck, NRW-Klimakommune",null],["055660084084","Steinfurt, Stadt",null],["055660088088","Tecklenburg, Stadt",null],["055660092092","Westerkappeln",null],["055660096096","Wettringen",null],["055700004004","Ahlen, Stadt",null],["055700008008","Beckum, Stadt",null],["055700012012","Beelen",null],["055700016016","Drensteinfurt, Stadt",null],["055700020020","Ennigerloh, Stadt",null],["055700024024","Everswinkel",null],["055700028028","Oelde, Stadt",null],["055700032032","Ostbevern",null],["055700036036","Sassenberg, Stadt",null],["055700040040","Sendenhorst, Stadt",null],["055700044044","Telgte, Stadt",null],["055700048048","Wadersloh",null],["055700052052","Warendorf, Stadt",null],["057110000000","Bielefeld, Stadt",null],["057540004004","Borgholzhausen, Stadt",null],["057540008008","Gütersloh, Stadt",null],["057540012012","Halle (Westf.), Stadt",null],["057540016016","Harsewinkel, Die Mähdrescherstadt",null],["057540020020","Herzebrock-Clarholz",null],["057540024024","Langenberg",null],["057540028028","Rheda-Wiedenbrück, Stadt",null],["057540032032","Rietberg, Stadt",null],["057540036036","Schloß Holte-Stukenbrock, Stadt",null],["057540040040","Steinhagen",null],["057540044044","Verl, Stadt",null],["057540048048","Versmold, Stadt",null],["057540052052","Werther (Westf.), Stadt",null],["057580004004","Bünde, Stadt",null],["057580008008","Enger, Widukindstadt",null],["057580012012","Herford, Hansestadt",null],["057580016016","Hiddenhausen",null],["057580020020","Kirchlengern",null],["057580024024","Löhne, Stadt",null],["057580028028","Rödinghausen",null],["057580032032","Spenge, Stadt",null],["057580036036","Vlotho, Stadt",null],["057620004004","Bad Driburg, Stadt",null],["057620008008","Beverungen, Stadt",null],["057620012012","Borgentreich, Orgelstadt",null],["057620016016","Brakel, Stadt",null],["057620020020","Höxter, Stadt",null],["057620024024","Marienmünster, Stadt",null],["057620028028","Nieheim, Stadt",null],["057620032032","Steinheim, Stadt",null],["057620036036","Warburg, Hansestadt",null],["057620040040","Willebadessen, Stadt",null],["057660004004","Augustdorf",null],["057660008008","Bad Salzuflen, Stadt",null],["057660012012","Barntrup, Stadt",null],["057660016016","Blomberg, Stadt",null],["057660020020","Detmold, Stadt",null],["057660024024","Dörentrup",null],["057660028028","Extertal",null],["057660032032","Horn-Bad Meinberg, Stadt",null],["057660036036","Kalletal",null],["057660040040","Lage, Stadt",null],["057660044044","Lemgo, Stadt",null],["057660048048","Leopoldshöhe",null],["057660052052","Lügde, Stadt der Osterräder",null],["057660056056","Oerlinghausen, Stadt",null],["057660060060","Schieder-Schwalenberg, Stadt",null],["057660064064","Schlangen",null],["057700004004","Bad Oeynhausen, Stadt",null],["057700008008","Espelkamp, Stadt",null],["057700012012","Hille",null],["057700016016","Hüllhorst",null],["057700020020","Lübbecke, Stadt",null],["057700024024","Minden, Stadt",null],["057700028028","Petershagen, Stadt",null],["057700032032","Porta Westfalica, Stadt",null],["057700036036","Preußisch Oldendorf, Stadt",null],["057700040040","Rahden, Stadt",null],["057700044044","Stemwede",null],["057740004004","Altenbeken",null],["057740008008","Bad Lippspringe, Stadt",null],["057740012012","Borchen",null],["057740016016","Büren, Stadt",null],["057740020020","Delbrück, Stadt",null],["057740024024","Hövelhof, Sennegemeinde",null],["057740028028","Lichtenau, Stadt",null],["057740032032","Paderborn, Stadt",null],["057740036036","Salzkotten, Stadt",null],["057740040040","Bad Wünnenberg, Stadt",null],["059110000000","Bochum, Stadt",null],["059130000000","Dortmund, Stadt",null],["059140000000","Hagen, Stadt der FernUniversität",null],["059150000000","Hamm, Stadt",null],["059160000000","Herne, Stadt",null],["059540004004","Breckerfeld, Hansestadt",null],["059540008008","Ennepetal, Stadt der Kluterthöhle",null],["059540012012","Gevelsberg, Stadt",null],["059540016016","Hattingen, Stadt",null],["059540020020","Herdecke, Stadt",null],["059540024024","Schwelm, Stadt",null],["059540028028","Sprockhövel, Stadt",null],["059540032032","Wetter (Ruhr), Stadt",null],["059540036036","Witten, Stadt",null],["059580004004","Arnsberg, Stadt",null],["059580008008","Bestwig",null],["059580012012","Brilon, Stadt",null],["059580016016","Eslohe (Sauerland)",null],["059580020020","Hallenberg, Stadt",null],["059580024024","Marsberg, Stadt",null],["059580028028","Medebach, Hansestadt",null],["059580032032","Meschede, Kreis- und Hochschulstadt",null],["059580036036","Olsberg, Stadt",null],["059580040040","Schmallenberg, Stadt",null],["059580044044","Sundern (Sauerland), Stadt",null],["059580048048","Winterberg, Stadt",null],["059620004004","Altena, Stadt",null],["059620008008","Balve, Stadt",null],["059620012012","Halver, Stadt",null],["059620016016","Hemer, Stadt",null],["059620020020","Herscheid",null],["059620024024","Iserlohn, Stadt",null],["059620028028","Kierspe, Stadt",null],["059620032032","Lüdenscheid, Stadt",null],["059620036036","Meinerzhagen, Stadt",null],["059620040040","Menden (Sauerland), Stadt",null],["059620044044","Nachrodt-Wiblingwerde",null],["059620048048","Neuenrade, Stadt",null],["059620052052","Plettenberg, Stadt",null],["059620056056","Schalksmühle",null],["059620060060","Werdohl, Stadt",null],["059660004004","Attendorn, Hansestadt",null],["059660008008","Drolshagen, Stadt",null],["059660012012","Finnentrop",null],["059660016016","Kirchhundem",null],["059660020020","Lennestadt, Stadt",null],["059660024024","Olpe, Stadt",null],["059660028028","Wenden",null],["059700004004","Bad Berleburg, Stadt",null],["059700008008","Burbach",null],["059700012012","Erndtebrück",null],["059700016016","Freudenberg, Stadt",null],["059700020020","Hilchenbach, Stadt",null],["059700024024","Kreuztal, Stadt",null],["059700028028","Bad Laasphe, Stadt",null],["059700032032","Netphen, Stadt",null],["059700036036","Neunkirchen",null],["059700040040","Siegen, Universitätsstadt",null],["059700044044","Wilnsdorf",null],["059740004004","Anröchte",null],["059740008008","Bad Sassendorf",null],["059740012012","Ense",null],["059740016016","Erwitte, Stadt",null],["059740020020","Geseke, Stadt",null],["059740024024","Lippetal",null],["059740028028","Lippstadt, Stadt",null],["059740032032","Möhnesee",null],["059740036036","Rüthen, Stadt",null],["059740040040","Soest, Stadt",null],["059740044044","Warstein, Stadt",null],["059740048048","Welver",null],["059740052052","Werl, Stadt",null],["059740056056","Wickede (Ruhr)",null],["059780004004","Bergkamen, Stadt",null],["059780008008","Bönen",null],["059780012012","Fröndenberg/Ruhr, Stadt",null],["059780016016","Holzwickede",null],["059780020020","Kamen, Stadt",null],["059780024024","Lünen, Stadt",null],["059780028028","Schwerte, Hansestadt an der Ruhr",null],["059780032032","Selm, Stadt",null],["059780036036","Unna, Stadt",null],["059780040040","Werne, Stadt",null],["064110000000","Darmstadt, Wissenschaftsstadt",null],["064120000000","Frankfurt am Main, Stadt",null],["064130000000","Offenbach am Main, Stadt",null],["064140000000","Wiesbaden, Landeshauptstadt",null],["064310001001","Abtsteinach",null],["064310002002","Bensheim, Stadt",null],["064310003003","Biblis",null],["064310004004","Birkenau",null],["064310005005","Bürstadt, Stadt",null],["064310006006","Einhausen",null],["064310007007","Fürth",null],["064310008008","Gorxheimertal",null],["064310009009","Grasellenbach",null],["064310010010","Groß-Rohrheim",null],["064310011011","Heppenheim (Bergstraße), Kreisstadt",null],["064310012012","Hirschhorn (Neckar), Stadt",null],["064310013013","Lampertheim, Stadt",null],["064310014014","Lautertal (Odenwald)",null],["064310015015","Lindenfels, Stadt",null],["064310016016","Lorsch, Karolingerstadt",null],["064310017017","Mörlenbach",null],["064310018018","Neckarsteinach, Stadt",null],["064310019019","Rimbach",null],["064310020020","Viernheim, Stadt",null],["064310021021","Wald-Michelbach",null],["064310022022","Zwingenberg, Stadt",null],["064319200200","Michelbuch, gemfr. Gebiet",null],["064320001001","Alsbach-Hähnlein",null],["064320002002","Babenhausen, Stadt",null],["064320003003","Bickenbach",null],["064320004004","Dieburg, Stadt",null],["064320005005","Eppertshausen",null],["064320006006","Erzhausen",null],["064320007007","Fischbachtal",null],["064320008008","Griesheim, Stadt",null],["064320009009","Groß-Bieberau, Stadt",null],["064320010010","Groß-Umstadt, Stadt",null],["064320011011","Groß-Zimmern",null],["064320012012","Messel",null],["064320013013","Modautal",null],["064320014014","Mühltal",null],["064320015015","Münster (Hessen)",null],["064320016016","Ober-Ramstadt, Stadt",null],["064320017017","Otzberg",null],["064320018018","Pfungstadt, Stadt",null],["064320019019","Reinheim, Stadt",null],["064320020020","Roßdorf",null],["064320021021","Schaafheim",null],["064320022022","Seeheim-Jugenheim",null],["064320023023","Weiterstadt, Stadt",null],["064330001001","Biebesheim am Rhein",null],["064330002002","Bischofsheim",null],["064330003003","Büttelborn",null],["064330004004","Gernsheim, Schöfferstadt",null],["064330005005","Ginsheim-Gustavsburg, Stadt",null],["064330006006","Groß-Gerau, Stadt",null],["064330007007","Kelsterbach, Stadt",null],["064330008008","Mörfelden-Walldorf, Stadt",null],["064330009009","Nauheim",null],["064330010010","Raunheim, Stadt",null],["064330011011","Riedstadt, Büchnerstadt",null],["064330012012","Rüsselsheim am Main, Stadt",null],["064330013013","Stockstadt am Rhein",null],["064330014014","Trebur",null],["064340001001","Bad Homburg v. d. Höhe, Stadt",null],["064340002002","Friedrichsdorf, Stadt",null],["064340003003","Glashütten",null],["064340004004","Grävenwiesbach",null],["064340005005","Königstein im Taunus, Stadt",null],["064340006006","Kronberg im Taunus, Stadt",null],["064340007007","Neu-Anspach, Stadt",null],["064340008008","Oberursel (Taunus), Stadt",null],["064340009009","Schmitten",null],["064340010010","Steinbach (Taunus), Stadt",null],["064340011011","Usingen, Stadt",null],["064340012012","Wehrheim",null],["064340013013","Weilrod",null],["064350001001","Bad Orb, Stadt",null],["064350002002","Bad Soden-Salmünster, Stadt",null],["064350003003","Biebergemünd",null],["064350004004","Birstein",null],["064350005005","Brachttal",null],["064350006006","Bruchköbel, Stadt",null],["064350007007","Erlensee, Stadt",null],["064350008008","Flörsbachtal",null],["064350009009","Freigericht",null],["064350010010","Gelnhausen, Barbarossast., Krst.",null],["064350011011","Großkrotzenburg",null],["064350012012","Gründau",null],["064350013013","Hammersbach",null],["064350014014","Hanau, Brüder-Grimm-Stadt",null],["064350015015","Hasselroth",null],["064350016016","Jossgrund",null],["064350017017","Langenselbold, Stadt",null],["064350018018","Linsengericht",null],["064350019019","Maintal, Stadt",null],["064350020020","Neuberg",null],["064350021021","Nidderau, Stadt",null],["064350022022","Niederdorfelden",null],["064350023023","Rodenbach",null],["064350024024","Ronneburg",null],["064350025025","Schlüchtern, Stadt",null],["064350026026","Schöneck",null],["064350027027","Sinntal",null],["064350028028","Steinau an der Straße, Brüder-Grimm-Stadt",null],["064350029029","Wächtersbach, Stadt",null],["064359200200","Gutsbezirk Spessart, gemfr. Gebiet",null],["064360001001","Bad Soden am Taunus, Stadt",null],["064360002002","Eppstein, Stadt",null],["064360003003","Eschborn, Stadt",null],["064360004004","Flörsheim am Main, Stadt",null],["064360005005","Hattersheim am Main, Stadt",null],["064360006006","Hochheim am Main, Stadt",null],["064360007007","Hofheim am Taunus, Kreisstadt",null],["064360008008","Kelkheim (Taunus), Stadt",null],["064360009009","Kriftel",null],["064360010010","Liederbach am Taunus",null],["064360011011","Schwalbach am Taunus, Stadt",null],["064360012012","Sulzbach (Taunus)",null],["064370001001","Bad König, Stadt",null],["064370003003","Brensbach",null],["064370004004","Breuberg, Stadt",null],["064370005005","Brombachtal",null],["064370006006","Erbach, Kreisstadt",null],["064370007007","Fränkisch-Crumbach",null],["064370009009","Höchst i. Odw.",null],["064370010010","Lützelbach",null],["064370011011","Michelstadt, Stadt",null],["064370012012","Mossautal",null],["064370013013","Reichelsheim (Odenwald)",null],["064370016016","Oberzent, Stadt",null],["064380001001","Dietzenbach, Kreisstadt",null],["064380002002","Dreieich, Stadt",null],["064380003003","Egelsbach",null],["064380004004","Hainburg",null],["064380005005","Heusenstamm, Stadt",null],["064380006006","Langen (Hessen), Stadt",null],["064380007007","Mainhausen",null],["064380008008","Mühlheim am Main, Stadt",null],["064380009009","Neu-Isenburg, Stadt",null],["064380010010","Obertshausen, Stadt",null],["064380011011","Rodgau, Stadt",null],["064380012012","Rödermark, Stadt",null],["064380013013","Seligenstadt, Einhardstadt",null],["064390001001","Aarbergen",null],["064390002002","Bad Schwalbach, Kreisstadt",null],["064390003003","Eltville am Rhein, Stadt",null],["064390004004","Geisenheim, Hochschulstadt",null],["064390005005","Heidenrod",null],["064390006006","Hohenstein",null],["064390007007","Hünstetten",null],["064390008008","Idstein, Hochschulstadt",null],["064390009009","Kiedrich",null],["064390010010","Lorch, Stadt",null],["064390011011","Niedernhausen",null],["064390012012","Oestrich-Winkel, Stadt",null],["064390013013","Rüdesheim am Rhein, Stadt",null],["064390014014","Schlangenbad",null],["064390015015","Taunusstein, Stadt",null],["064390016016","Waldems",null],["064390017017","Walluf",null],["064400001001","Altenstadt",null],["064400002002","Bad Nauheim, Stadt",null],["064400003003","Bad Vilbel, Stadt",null],["064400004004","Büdingen, Stadt",null],["064400005005","Butzbach, Friedrich-Ludwig-Weidig-Stadt",null],["064400006006","Echzell",null],["064400007007","Florstadt, Stadt",null],["064400008008","Friedberg (Hessen), Kreisstadt",null],["064400009009","Gedern, Stadt",null],["064400010010","Glauburg",null],["064400011011","Hirzenhain",null],["064400012012","Karben, Stadt",null],["064400013013","Kefenrod",null],["064400014014","Limeshain",null],["064400015015","Münzenberg, Stadt",null],["064400016016","Nidda, Stadt",null],["064400017017","Niddatal, Stadt",null],["064400018018","Ober-Mörlen",null],["064400019019","Ortenberg, Stadt",null],["064400020020","Ranstadt",null],["064400021021","Reichelsheim (Wetterau), Stadt",null],["064400022022","Rockenberg",null],["064400023023","Rosbach v. d. Höhe, Stadt",null],["064400024024","Wölfersheim",null],["064400025025","Wöllstadt",null],["065310001001","Allendorf (Lumda), Stadt",null],["065310002002","Biebertal",null],["065310003003","Buseck",null],["065310004004","Fernwald",null],["065310005005","Gießen, Universitätsstadt",null],["065310006006","Grünberg, Stadt",null],["065310007007","Heuchelheim a. d. Lahn",null],["065310008008","Hungen, Stadt",null],["065310009009","Langgöns",null],["065310010010","Laubach, Stadt",null],["065310011011","Lich, Stadt",null],["065310012012","Linden, Stadt",null],["065310013013","Lollar, Stadt",null],["065310014014","Pohlheim, Stadt",null],["065310015015","Rabenau",null],["065310016016","Reiskirchen",null],["065310017017","Staufenberg, Stadt",null],["065310018018","Wettenberg",null],["065320001001","Aßlar, Stadt",null],["065320002002","Bischoffen",null],["065320003003","Braunfels, Stadt",null],["065320004004","Breitscheid",null],["065320005005","Dietzhölztal",null],["065320006006","Dillenburg, Oranienstadt",null],["065320007007","Driedorf",null],["065320008008","Ehringshausen",null],["065320009009","Eschenburg",null],["065320010010","Greifenstein",null],["065320011011","Haiger, Stadt",null],["065320012012","Herborn, Stadt",null],["065320013013","Hohenahr",null],["065320014014","Hüttenberg",null],["065320015015","Lahnau",null],["065320016016","Leun, Stadt",null],["065320017017","Mittenaar",null],["065320018018","Schöffengrund",null],["065320019019","Siegbach",null],["065320020020","Sinn",null],["065320021021","Solms, Stadt",null],["065320022022","Waldsolms",null],["065320023023","Wetzlar, Stadt",null],["065330001001","Beselich",null],["065330002002","Brechen",null],["065330003003","Bad Camberg, Stadt",null],["065330004004","Dornburg",null],["065330005005","Elbtal",null],["065330006006","Elz",null],["065330007007","Hadamar, Stadt",null],["065330008008","Hünfelden",null],["065330009009","Limburg a. d. Lahn, Kreisstadt",null],["065330010010","Löhnberg",null],["065330011011","Mengerskirchen, Marktflecken",null],["065330012012","Merenberg, Marktflecken",null],["065330013013","Runkel, Stadt",null],["065330014014","Selters (Taunus)",null],["065330015015","Villmar, Marktflecken",null],["065330016016","Waldbrunn (Westerwald)",null],["065330017017","Weilburg, Stadt",null],["065330018018","Weilmünster, Marktflecken",null],["065330019019","Weinbach",null],["065340001001","Amöneburg, Stadt",null],["065340002002","Angelburg",null],["065340003003","Bad Endbach",null],["065340004004","Biedenkopf, Stadt",null],["065340005005","Breidenbach",null],["065340006006","Cölbe",null],["065340007007","Dautphetal",null],["065340008008","Ebsdorfergrund",null],["065340009009","Fronhausen",null],["065340010010","Gladenbach, Stadt",null],["065340011011","Kirchhain, Stadt",null],["065340012012","Lahntal",null],["065340013013","Lohra",null],["065340014014","Marburg, Universitätsstadt",null],["065340015015","Münchhausen",null],["065340016016","Neustadt (Hessen), Stadt",null],["065340017017","Rauschenberg, Stadt",null],["065340018018","Stadtallendorf, Stadt",null],["065340019019","Steffenberg",null],["065340020020","Weimar (Lahn)",null],["065340021021","Wetter (Hessen), Stadt",null],["065340022022","Wohratal",null],["065350001001","Alsfeld, Stadt",null],["065350002002","Antrifttal",null],["065350003003","Feldatal",null],["065350004004","Freiensteinau",null],["065350005005","Gemünden (Felda)",null],["065350006006","Grebenau, Stadt",null],["065350007007","Grebenhain",null],["065350008008","Herbstein, Stadt",null],["065350009009","Homberg (Ohm), Stadt",null],["065350010010","Kirtorf, Stadt",null],["065350011011","Lauterbach (Hessen), Kreisstadt",null],["065350012012","Lautertal (Vogelsberg)",null],["065350013013","Mücke",null],["065350014014","Romrod, Stadt",null],["065350015015","Schlitz, Stadt",null],["065350016016","Schotten, Stadt",null],["065350017017","Schwalmtal",null],["065350018018","Ulrichstein, Stadt",null],["065350019019","Wartenberg",null],["066110000000","Kassel, documenta-Stadt",null],["066310001001","Bad Salzschlirf",null],["066310002002","Burghaun, Marktgemeinde",null],["066310003003","Dipperz",null],["066310004004","Ebersburg",null],["066310005005","Ehrenberg (Rhön)",null],["066310006006","Eichenzell",null],["066310007007","Eiterfeld, Marktgemeinde",null],["066310008008","Flieden",null],["066310009009","Fulda, Stadt",null],["066310010010","Gersfeld (Rhön), Stadt",null],["066310011011","Großenlüder",null],["066310012012","Hilders, Marktgemeinde",null],["066310013013","Hofbieber",null],["066310014014","Hosenfeld",null],["066310015015","Hünfeld, Konrad-Zuse-Stadt",null],["066310016016","Kalbach",null],["066310017017","Künzell",null],["066310018018","Neuhof",null],["066310019019","Nüsttal",null],["066310020020","Petersberg",null],["066310021021","Poppenhausen (Wasserkuppe)",null],["066310022022","Rasdorf, Point-Alpha-Gemeinde",null],["066310023023","Tann (Rhön), Stadt",null],["066320001001","Alheim",null],["066320002002","Bad Hersfeld, Kreisstadt",null],["066320003003","Bebra, Stadt",null],["066320004004","Breitenbach a. Herzberg",null],["066320005005","Cornberg",null],["066320006006","Friedewald",null],["066320007007","Hauneck",null],["066320008008","Haunetal",null],["066320009009","Heringen (Werra), Stadt",null],["066320010010","Hohenroda",null],["066320011011","Kirchheim",null],["066320012012","Ludwigsau",null],["066320013013","Nentershausen",null],["066320014014","Neuenstein",null],["066320015015","Niederaula, Marktgemeinde",null],["066320016016","Philippsthal (Werra), Marktgemeinde",null],["066320017017","Ronshausen",null],["066320018018","Rotenburg a. d. Fulda, Stadt",null],["066320019019","Schenklengsfeld",null],["066320020020","Wildeck",null],["066330001001","Ahnatal",null],["066330002002","Bad Karlshafen, Stadt",null],["066330003003","Baunatal, Stadt",null],["066330004004","Breuna",null],["066330005005","Calden",null],["066330006006","Bad Emstal",null],["066330007007","Espenau",null],["066330008008","Fuldabrück",null],["066330009009","Fuldatal",null],["066330010010","Grebenstein, Stadt",null],["066330011011","Habichtswald",null],["066330012012","Helsa",null],["066330013013","Hofgeismar, Stadt",null],["066330014014","Immenhausen, Stadt",null],["066330015015","Kaufungen",null],["066330016016","Liebenau, Stadt",null],["066330017017","Lohfelden",null],["066330018018","Naumburg, Stadt",null],["066330019019","Nieste",null],["066330020020","Niestetal",null],["066330022022","Reinhardshagen",null],["066330023023","Schauenburg",null],["066330024024","Söhrewald",null],["066330025025","Trendelburg, Stadt",null],["066330026026","Vellmar, Stadt",null],["066330028028","Wolfhagen, Hans-Staden-Stadt",null],["066330029029","Zierenberg, Stadt",null],["066330030030","Wesertal",null],["066339200200","Gutsbezirk Reinhardswald, gemfr. Gebiet",null],["066340001001","Borken (Hessen), Stadt",null],["066340002002","Edermünde",null],["066340003003","Felsberg, Stadt",null],["066340004004","Frielendorf, Marktflecken",null],["066340005005","Fritzlar, Dom- und Kaiserstadt",null],["066340006006","Gilserberg",null],["066340007007","Gudensberg, Stadt",null],["066340008008","Guxhagen",null],["066340009009","Homberg (Efze), Reformationsstadt, Kreisstadt",null],["066340010010","Jesberg",null],["066340011011","Knüllwald",null],["066340012012","Körle",null],["066340013013","Malsfeld",null],["066340014014","Melsungen, Stadt",null],["066340015015","Morschen",null],["066340016016","Neuental",null],["066340017017","Neukirchen, Stadt",null],["066340018018","Niedenstein, Stadt",null],["066340019019","Oberaula",null],["066340020020","Ottrau",null],["066340021021","Schrecksbach",null],["066340022022","Schwalmstadt, Konfirmationsstadt",null],["066340023023","Schwarzenborn, Stadt",null],["066340024024","Spangenberg, Liebenbachstadt",null],["066340025025","Wabern",null],["066340026026","Willingshausen",null],["066340027027","Bad Zwesten",null],["066350001001","Allendorf (Eder)",null],["066350002002","Bad Arolsen, Stadt",null],["066350003003","Bad Wildungen, Stadt",null],["066350004004","Battenberg (Eder), Stadt",null],["066350005005","Bromskirchen",null],["066350006006","Burgwald",null],["066350007007","Diemelsee",null],["066350008008","Diemelstadt, Stadt",null],["066350009009","Edertal, Nationalparkgemeinde",null],["066350010010","Frankenau, Nationalparkstadt",null],["066350011011","Frankenberg (Eder), Philipp-Soldan-Stadt",null],["066350012012","Gemünden (Wohra), Stadt",null],["066350013013","Haina (Kloster)",null],["066350014014","Hatzfeld (Eder), Stadt",null],["066350015015","Korbach, Hansestadt, Kreisstadt",null],["066350016016","Lichtenfels, Stadt",null],["066350017017","Rosenthal, Stadt",null],["066350018018","Twistetal",null],["066350019019","Vöhl, Nationalparkgemeinde",null],["066350020020","Volkmarsen, Stadt",null],["066350021021","Waldeck, Stadt",null],["066350022022","Willingen (Upland)",null],["066360001001","Bad Sooden-Allendorf, Stadt",null],["066360002002","Berkatal",null],["066360003003","Eschwege, Kreisstadt",null],["066360004004","Großalmerode, Stadt",null],["066360005005","Herleshausen",null],["066360006006","Hessisch Lichtenau, Stadt",null],["066360007007","Meinhard",null],["066360008008","Meißner",null],["066360009009","Neu-Eichenberg",null],["066360010010","Ringgau",null],["066360011011","Sontra, Stadt",null],["066360012012","Waldkappel, Stadt",null],["066360013013","Wanfried, Stadt",null],["066360014014","Wehretal",null],["066360015015","Weißenborn",null],["066360016016","Witzenhausen, Stadt",null],["066369200200","Gutsbezirk Kaufunger Wald, gemfr. Gebiet",null],["070009999999","Gemeinsames deutsch-luxemburgisches Hoheitsgebiet",null],["071110000000","Koblenz, Stadt",null],["071310007007","Bad Neuenahr-Ahrweiler, Stadt",null],["071310070070","Remagen, Stadt",null],["071310077077","Sinzig, Stadt",null],["071310090090","Grafschaft",null],["071315001001","Adenau, Stadt",null],["071315001004","Antweiler",null],["071315001005","Aremberg",null],["071315001008","Barweiler",null],["071315001009","Bauler",null],["071315001015","Dankerath",null],["071315001018","Dorsel",null],["071315001021","Eichenbach",null],["071315001022","Fuchshofen",null],["071315001026","Harscheid",null],["071315001028","Herschbroich",null],["071315001030","Hoffeld",null],["071315001032","Honerath",null],["071315001033","Hümmel",null],["071315001034","Insul",null],["071315001037","Kaltenborn",null],["071315001042","Kottenborn",null],["071315001044","Leimbach",null],["071315001050","Meuspath",null],["071315001051","Müllenbach",null],["071315001052","Müsch",null],["071315001058","Nürburg",null],["071315001062","Ohlenhard",null],["071315001065","Pomster",null],["071315001066","Quiddelbach",null],["071315001069","Reifferscheid",null],["071315001072","Rodder",null],["071315001074","Schuld",null],["071315001075","Senscheid",null],["071315001076","Sierscheid",null],["071315001079","Trierscheid",null],["071315001082","Wershofen",null],["071315001083","Wiesemscheid",null],["071315001084","Wimbach",null],["071315001085","Winnerath",null],["071315001086","Wirft",null],["071315001501","Dümpelfeld",null],["071315002002","Ahrbrück",null],["071315002003","Altenahr",null],["071315002011","Berg",null],["071315002017","Dernau",null],["071315002027","Heckenbach",null],["071315002029","Hönningen",null],["071315002036","Kalenborn",null],["071315002039","Kesseling",null],["071315002040","Kirchsahr",null],["071315002047","Lind",null],["071315002049","Mayschoß",null],["071315002068","Rech",null],["071315003006","Bad Breisig, Stadt",null],["071315003014","Brohl-Lützing",null],["071315003025","Gönnersdorf",null],["071315003081","Waldorf",null],["071315004016","Dedenbach",null],["071315004041","Königsfeld",null],["071315004054","Niederdürenbach",null],["071315004055","Niederzissen",null],["071315004059","Oberdürenbach",null],["071315004060","Oberzissen",null],["071315004073","Schalkenbach",null],["071315004201","Brenk",null],["071315004202","Burgbrohl",null],["071315004204","Galenberg",null],["071315004205","Glees",null],["071315004206","Hohenleimbach",null],["071315004208","Spessart",null],["071315004209","Wassenach",null],["071315004210","Wehr",null],["071315004211","Weibern",null],["071315004502","Kempenich",null],["071325003018","Daaden, Stadt",null],["071325003019","Derschen",null],["071325003026","Emmerzhausen",null],["071325003036","Friedewald",null],["071325003050","Herdorf, Stadt",null],["071325003068","Mauden",null],["071325003075","Niederdreisbach",null],["071325003079","Nisterberg",null],["071325003101","Schutzbach",null],["071325003113","Weitefeld",null],["071325006007","Birkenbeul",null],["071325006010","Bitzen",null],["071325006013","Breitscheidt",null],["071325006014","Bruchertseifen",null],["071325006028","Etzbach",null],["071325006034","Forst",null],["071325006038","Fürthen",null],["071325006044","Hamm (Sieg)",null],["071325006077","Niederirsen",null],["071325006091","Pracht",null],["071325006096","Roth",null],["071325006102","Seelbach bei Hamm (Sieg)",null],["071325007012","Brachbach",null],["071325007037","Friesenhagen",null],["071325007045","Harbach",null],["071325007063","Kirchen (Sieg), Stadt",null],["071325007072","Mudersbach",null],["071325007076","Niederfischbach",null],["071325008008","Birken-Honigsessen",null],["071325008011","Mittelhof",null],["071325008054","Hövels",null],["071325008080","Katzwinkel (Sieg)",null],["071325008105","Selbach (Sieg)",null],["071325008117","Wissen, Stadt",null],["071325009002","Alsdorf",null],["071325009006","Betzdorf, Stadt",null],["071325009020","Dickendorf",null],["071325009024","Elben",null],["071325009025","Elkenroth",null],["071325009030","Fensdorf",null],["071325009039","Gebhardshain",null],["071325009042","Grünebach",null],["071325009059","Kausen",null],["071325009066","Malberg",null],["071325009071","Molzhain",null],["071325009073","Nauroth",null],["071325009095","Rosenheim (Landkreis Altenkirchen)",null],["071325009098","Scheuerfeld",null],["071325009107","Steinebach/ Sieg",null],["071325009108","Steineroth",null],["071325009111","Wallmenroth",null],["071325010001","Almersbach",null],["071325010004","Bachenberg",null],["071325010005","Berzhausen",null],["071325010009","Birnbach",null],["071325010015","Bürdenbach",null],["071325010016","Burglahr",null],["071325010017","Busenhausen",null],["071325010022","Eichelhardt",null],["071325010023","Eichen",null],["071325010027","Ersfeld",null],["071325010029","Eulenberg",null],["071325010031","Fiersbach",null],["071325010032","Flammersfeld",null],["071325010033","Fluterschen",null],["071325010035","Forstmehren",null],["071325010040","Gieleroth",null],["071325010041","Giershausen",null],["071325010043","Güllesheim",null],["071325010046","Hasselbach",null],["071325010047","Helmenzen",null],["071325010048","Helmeroth",null],["071325010049","Hemmelzen",null],["071325010051","Heupelzen",null],["071325010052","Hilgenroth",null],["071325010053","Hirz-Maulsbach",null],["071325010055","Horhausen (Westerwald)",null],["071325010056","Idelberg",null],["071325010057","Ingelbach",null],["071325010058","Isert",null],["071325010060","Kescheid",null],["071325010061","Kettenhausen",null],["071325010062","Kircheib",null],["071325010064","Kraam",null],["071325010065","Krunkel",null],["071325010067","Mammelzen",null],["071325010069","Mehren",null],["071325010070","Michelbach (Westerwald)",null],["071325010078","Niedersteinebach",null],["071325010081","Obererbach (Westerwald)",null],["071325010082","Oberirsen",null],["071325010083","Oberlahr",null],["071325010085","Obersteinebach",null],["071325010086","Oberwambach",null],["071325010087","Ölsen",null],["071325010088","Orfgen",null],["071325010089","Peterslahr",null],["071325010090","Pleckhausen",null],["071325010092","Racksen",null],["071325010093","Reiferscheid",null],["071325010094","Rettersen",null],["071325010097","Rott",null],["071325010099","Schöneberg",null],["071325010100","Schürdt",null],["071325010103","Seelbach (Westerwald)",null],["071325010104","Seifen",null],["071325010106","Sörth",null],["071325010109","Stürzelbach",null],["071325010110","Volkerzen",null],["071325010112","Walterschen",null],["071325010114","Werkhausen",null],["071325010115","Weyerbusch",null],["071325010116","Willroth",null],["071325010118","Wölmersen",null],["071325010119","Ziegenhain",null],["071325010201","Berod bei Hachenburg",null],["071325010501","Altenkirchen (Westerwald), Stadt",null],["071325010502","Neitersen",null],["071330006006","Bad Kreuznach, Stadt",null],["071335001003","Altenbamberg",null],["071335001012","Biebelsheim",null],["071335001030","Feilbingert",null],["071335001031","Frei-Laubersheim",null],["071335001032","Fürfeld",null],["071335001037","Hackenheim",null],["071335001039","Hallgarten",null],["071335001045","Hochstätten",null],["071335001069","Neu-Bamberg",null],["071335001078","Pfaffen-Schwabenheim",null],["071335001080","Pleitersheim",null],["071335001104","Tiefenthal",null],["071335001106","Volxheim",null],["071335006002","Allenfeld",null],["071335006004","Argenschwang",null],["071335006013","Bockenau",null],["071335006014","Boos",null],["071335006015","Braunweiler",null],["071335006019","Burgsponheim",null],["071335006021","Dalberg",null],["071335006027","Duchroth",null],["071335006033","Gebroth",null],["071335006036","Gutenberg",null],["071335006040","Hargesheim",null],["071335006044","Hergenfeld",null],["071335006048","Hüffelsheim",null],["071335006061","Mandel",null],["071335006068","Münchwald",null],["071335006070","Niederhausen",null],["071335006071","Norheim",null],["071335006074","Oberhausen an der Nahe",null],["071335006075","Oberstreit",null],["071335006086","Roxheim",null],["071335006088","Sankt Katharinen",null],["071335006089","Schloßböckelheim",null],["071335006098","Sommerloch",null],["071335006099","Spabrücken",null],["071335006100","Spall",null],["071335006101","Sponheim",null],["071335006105","Traisen",null],["071335006107","Waldböckelheim",null],["071335006109","Wallhausen",null],["071335006112","Weinsheim",null],["071335006115","Winterbach",null],["071335006117","Rüdesheim",null],["071335009008","Bärenbach",null],["071335009010","Becherbach bei Kirn",null],["071335009016","Brauweiler",null],["071335009038","Hahnenbach",null],["071335009041","Heimweiler",null],["071335009042","Heinzenberg",null],["071335009043","Hennweiler",null],["071335009046","Hochstetten-Dhaun",null],["071335009047","Horbach",null],["071335009052","Kirn, Stadt",null],["071335009059","Limbach",null],["071335009063","Meckenbach",null],["071335009073","Oberhausen bei Kirn",null],["071335009077","Otzweiler",null],["071335009096","Simmertal",null],["071335009113","Weitersborn",null],["071335009201","Bruschied",null],["071335009202","Kellenbach",null],["071335009203","Königsau",null],["071335009204","Schneppenbach",null],["071335009205","Schwarzerden",null],["071335010001","Abtweiler",null],["071335010005","Auen",null],["071335010009","Bärweiler",null],["071335010011","Becherbach",null],["071335010017","Breitenheim",null],["071335010020","Callbach",null],["071335010022","Daubach",null],["071335010024","Desloch",null],["071335010049","Hundsbach",null],["071335010050","Ippenschied",null],["071335010051","Jeckenbach",null],["071335010053","Kirschroth",null],["071335010055","Langenthal",null],["071335010057","Lauschied",null],["071335010058","Lettweiler",null],["071335010060","Löllbach",null],["071335010062","Martinstein",null],["071335010064","Meddersheim",null],["071335010065","Meisenheim, Stadt",null],["071335010066","Merxheim",null],["071335010067","Monzingen",null],["071335010072","Nußbaum",null],["071335010076","Odernheim am Glan",null],["071335010081","Raumbach",null],["071335010082","Rehbach",null],["071335010083","Rehborn",null],["071335010084","Reiffelbach",null],["071335010090","Schmittweiler",null],["071335010092","Schweinschied",null],["071335010094","Seesbach",null],["071335010102","Staudernheim",null],["071335010111","Weiler bei Monzingen",null],["071335010116","Winterburg",null],["071335010501","Bad Sobernheim, Stadt",null],["071335011018","Bretzenheim",null],["071335011023","Daxweiler",null],["071335011025","Dörrebach",null],["071335011026","Dorsheim",null],["071335011028","Eckenroth",null],["071335011035","Guldental",null],["071335011054","Langenlonsheim",null],["071335011056","Laubenheim",null],["071335011085","Roth",null],["071335011087","Rümmelsheim",null],["071335011091","Schöneberg",null],["071335011093","Schweppenhausen",null],["071335011095","Seibersbach",null],["071335011103","Stromberg, Stadt",null],["071335011108","Waldlaubersheim",null],["071335011110","Warmsroth",null],["071335011114","Windesheim",null],["071340045045","Idar-Oberstein, Stadt",null],["071345001005","Baumholder, Stadt",null],["071345001007","Berglangenbach",null],["071345001008","Berschweiler bei Baumholder",null],["071345001021","Eckersweiler",null],["071345001026","Fohren-Linden",null],["071345001027","Frauenberg",null],["071345001033","Hahnweiler",null],["071345001036","Heimbach",null],["071345001051","Leitzweiler",null],["071345001054","Mettweiler",null],["071345001068","Reichenbach",null],["071345001073","Rohrbach",null],["071345001074","Rückweiler",null],["071345001075","Ruschberg",null],["071345002001","Abentheuer",null],["071345002002","Achtelsbach",null],["071345002010","Birkenfeld, Stadt",null],["071345002011","Börfink",null],["071345002015","Brücken",null],["071345002016","Buhlenberg",null],["071345002018","Dambach",null],["071345002020","Dienstweiler",null],["071345002022","Elchweiler",null],["071345002023","Ellenberg",null],["071345002024","Ellweiler",null],["071345002029","Gimbweiler",null],["071345002031","Gollenberg",null],["071345002034","Hattgenstein",null],["071345002042","Hoppstädten-Weiersbach",null],["071345002048","Kronweiler",null],["071345002050","Leisel",null],["071345002053","Meckenbach",null],["071345002057","Niederbrombach",null],["071345002058","Niederhambach",null],["071345002061","Nohen",null],["071345002062","Oberbrombach",null],["071345002063","Oberhambach",null],["071345002070","Rimsberg",null],["071345002071","Rinzenberg",null],["071345002072","Rötsweiler-Nockenthal",null],["071345002078","Schmißberg",null],["071345002080","Schwollen",null],["071345002084","Siesbach",null],["071345002085","Sonnenberg-Winnenberg",null],["071345002094","Wilzenberg-Hußweiler",null],["071345005003","Allenbach",null],["071345005004","Asbach",null],["071345005006","Bergen",null],["071345005009","Berschweiler bei Kirn",null],["071345005012","Bollenbach",null],["071345005013","Breitenthal",null],["071345005014","Bruchweiler",null],["071345005017","Bundenbach",null],["071345005019","Dickesbach",null],["071345005025","Fischbach",null],["071345005028","Gerach",null],["071345005030","Gösenroth",null],["071345005032","Griebelschied",null],["071345005035","Hausen",null],["071345005037","Hellertshausen",null],["071345005038","Herborn",null],["071345005039","Herrstein",null],["071345005040","Hettenrodt",null],["071345005041","Hintertiefenbach",null],["071345005043","Horbruch",null],["071345005044","Hottenbach",null],["071345005046","Kempfeld",null],["071345005047","Kirschweiler",null],["071345005049","Krummenau",null],["071345005052","Mackenrodt",null],["071345005055","Mittelreidenbach",null],["071345005056","Mörschied",null],["071345005059","Niederhosenbach",null],["071345005060","Niederwörresbach",null],["071345005064","Oberhosenbach",null],["071345005065","Oberkirn",null],["071345005066","Oberreidenbach",null],["071345005067","Oberwörresbach",null],["071345005069","Rhaunen",null],["071345005076","Schauren",null],["071345005077","Schmidthachenbach",null],["071345005079","Schwerbach",null],["071345005081","Sensweiler",null],["071345005082","Sien",null],["071345005083","Sienhachenbach",null],["071345005086","Sonnschied",null],["071345005087","Stipshausen",null],["071345005088","Sulzbach",null],["071345005089","Veitsrodt",null],["071345005090","Vollmersbach",null],["071345005091","Weiden",null],["071345005092","Weitersbach",null],["071345005093","Wickenrodt",null],["071345005095","Wirschweiler",null],["071345005502","Langweiler",null],["071355001007","Beilstein",null],["071355001012","Bremm",null],["071355001015","Briedern",null],["071355001017","Bruttig-Fankel",null],["071355001020","Cochem, Stadt",null],["071355001021","Dohr",null],["071355001024","Ediger-Eller",null],["071355001025","Ellenz-Poltersdorf",null],["071355001027","Ernst",null],["071355001029","Faid",null],["071355001036","Greimersburg",null],["071355001049","Klotten",null],["071355001053","Lieg",null],["071355001056","Lütz",null],["071355001060","Mesenich",null],["071355001065","Moselkern",null],["071355001066","Müden (Mosel)",null],["071355001069","Nehren",null],["071355001072","Pommern",null],["071355001079","Senheim",null],["071355001082","Treis-Karden",null],["071355001086","Valwig",null],["071355001090","Wirfus",null],["071355002009","Binningen",null],["071355002011","Brachtendorf",null],["071355002014","Brieden",null],["071355002016","Brohl",null],["071355002022","Dünfus",null],["071355002023","Düngenheim",null],["071355002026","Eppenberg",null],["071355002028","Eulgem",null],["071355002031","Forst (Eifel)",null],["071355002033","Gamlen",null],["071355002038","Hambuch",null],["071355002040","Hauroth",null],["071355002042","Illerich",null],["071355002043","Kaifenheim",null],["071355002044","Kail",null],["071355002045","Kaisersesch, Stadt",null],["071355002046","Kalenborn",null],["071355002051","Landkern",null],["071355002052","Laubach",null],["071355002058","Masburg",null],["071355002062","Möntenich",null],["071355002067","Müllenbach",null],["071355002075","Roes",null],["071355002084","Urmersbach",null],["071355002093","Zettingen",null],["071355002502","Leienkaul",null],["071355003002","Alflen",null],["071355003005","Auderath",null],["071355003008","Beuren",null],["071355003018","Büchel",null],["071355003030","Filz",null],["071355003034","Gevenich",null],["071355003035","Gillenbeuren",null],["071355003048","Kliding",null],["071355003057","Lutzerath",null],["071355003078","Schmitt",null],["071355003083","Ulmen, Stadt",null],["071355003085","Urschmitt",null],["071355003087","Wagenhausen",null],["071355003089","Weiler",null],["071355003091","Wollmerath",null],["071355003501","Bad Bertrich",null],["071355005001","Alf",null],["071355005003","Altlay",null],["071355005004","Altstrimmig",null],["071355005010","Blankenrath",null],["071355005013","Briedel",null],["071355005019","Bullay",null],["071355005032","Forst (Hunsrück)",null],["071355005037","Grenderich",null],["071355005039","Haserich",null],["071355005041","Hesweiler",null],["071355005054","Liesenich",null],["071355005061","Mittelstrimmig",null],["071355005064","Moritzheim",null],["071355005068","Neef",null],["071355005070","Panzweiler",null],["071355005071","Peterswald-Löffelscheid",null],["071355005073","Pünderich",null],["071355005074","Reidenhausen",null],["071355005076","Sankt Aldegund",null],["071355005077","Schauren",null],["071355005080","Sosberg",null],["071355005081","Tellig",null],["071355005088","Walhausen",null],["071355005092","Zell (Mosel), Stadt",null],["071370003003","Andernach, Stadt",null],["071370068068","Mayen, Stadt",null],["071370203203","Bendorf, Stadt",null],["071375001056","Kretz",null],["071375001057","Kruft",null],["071375001081","Nickenich",null],["071375001088","Plaidt",null],["071375001096","Saffig",null],["071375002023","Einig",null],["071375002027","Gappenach",null],["071375002029","Gering",null],["071375002030","Gierschnach",null],["071375002041","Kalt",null],["071375002048","Kerben",null],["071375002053","Kollig",null],["071375002065","Lonnig",null],["071375002070","Mertloch",null],["071375002080","Naunheim",null],["071375002086","Ochtendung",null],["071375002087","Pillig",null],["071375002089","Polch, Stadt",null],["071375002095","Rüber",null],["071375002102","Trimbs",null],["071375002112","Welling",null],["071375002114","Wierschem",null],["071375002501","Münstermaifeld, Stadt",null],["071375003001","Acht",null],["071375003004","Anschau",null],["071375003006","Arft",null],["071375003007","Baar",null],["071375003011","Bermel",null],["071375003014","Boos",null],["071375003019","Ditscheid",null],["071375003025","Ettringen",null],["071375003034","Hausten",null],["071375003035","Herresbach",null],["071375003036","Hirten",null],["071375003043","Kehrig",null],["071375003049","Kirchwald",null],["071375003055","Kottenheim",null],["071375003060","Langenfeld",null],["071375003061","Langscheid",null],["071375003063","Lind",null],["071375003066","Luxem",null],["071375003074","Monreal",null],["071375003077","Münk",null],["071375003079","Nachtsheim",null],["071375003092","Reudelsterz",null],["071375003097","Sankt Johann",null],["071375003099","Siebenbach",null],["071375003105","Virneburg",null],["071375003110","Weiler",null],["071375003113","Welschenbach",null],["071375004008","Bell",null],["071375004069","Mendig, Stadt",null],["071375004093","Rieden",null],["071375004101","Thür",null],["071375004106","Volkesfeld",null],["071375007218","Niederwerth",null],["071375007224","Urbar",null],["071375007226","Vallendar, Stadt",null],["071375007229","Weitersburg",null],["071375008202","Bassenheim",null],["071375008209","Kaltenengers",null],["071375008211","Kettig",null],["071375008216","Mülheim-Kärlich, Stadt",null],["071375008222","Sankt Sebastian",null],["071375008225","Urmitz",null],["071375008228","Weißenthurm, Stadt",null],["071375009201","Alken",null],["071375009204","Brey",null],["071375009205","Brodenbach",null],["071375009206","Burgen",null],["071375009207","Dieblich",null],["071375009208","Hatzenport",null],["071375009212","Kobern-Gondorf",null],["071375009214","Löf",null],["071375009215","Macken",null],["071375009217","Niederfell",null],["071375009219","Nörtershausen",null],["071375009220","Oberfell",null],["071375009221","Rhens, Stadt",null],["071375009223","Spay",null],["071375009227","Waldesch",null],["071375009230","Winningen",null],["071375009231","Wolken",null],["071375009504","Lehmen",null],["071380045045","Neuwied, Stadt",null],["071385001003","Asbach",null],["071385001044","Neustadt (Wied)",null],["071385001077","Windhagen",null],["071385001080","Buchholz (Westerwald)",null],["071385002004","Bad Hönningen, Stadt",null],["071385002024","Hammerstein",null],["071385002038","Leutesdorf",null],["071385002063","Rheinbrohl",null],["071385003012","Dierdorf, Stadt",null],["071385003023","Großmaischeid",null],["071385003031","Isenburg",null],["071385003034","Kleinmaischeid",null],["071385003069","Stebach",null],["071385003201","Marienhausen",null],["071385004009","Dattenberg",null],["071385004037","Leubsdorf",null],["071385004041","Linz am Rhein, Stadt",null],["071385004055","Ockenfels",null],["071385004068","Sankt Katharinen (Landkreis Neuwied)",null],["071385004075","Vettelschoß",null],["071385004501","Kasbach-Ohlenberg",null],["071385005011","Dernbach",null],["071385005013","Döttesfeld",null],["071385005014","Dürrholz",null],["071385005025","Hanroth",null],["071385005027","Harschbach",null],["071385005040","Linkenbach",null],["071385005048","Niederhofen",null],["071385005050","Niederwambach",null],["071385005052","Oberdreis",null],["071385005057","Puderbach",null],["071385005058","Ratzert",null],["071385005059","Raubach",null],["071385005064","Rodenbach bei Puderbach",null],["071385005070","Steimel",null],["071385005074","Urbach",null],["071385005078","Woldert",null],["071385007008","Bruchhausen",null],["071385007019","Erpel",null],["071385007062","Rheinbreitbach",null],["071385007073","Unkel, Stadt",null],["071385009002","Anhausen",null],["071385009005","Bonefeld",null],["071385009006","Breitscheid",null],["071385009007","Hausen (Wied)",null],["071385009010","Datzeroth",null],["071385009015","Ehlscheid",null],["071385009026","Hardert",null],["071385009030","Hümmerich",null],["071385009036","Kurtscheid",null],["071385009042","Meinborn",null],["071385009043","Melsbach",null],["071385009047","Niederbreitbach",null],["071385009053","Oberhonnefeld-Gierend",null],["071385009054","Oberraden",null],["071385009061","Rengsdorf",null],["071385009065","Roßbach",null],["071385009066","Rüscheid",null],["071385009071","Straßenhaus",null],["071385009072","Thalhausen",null],["071385009076","Waldbreitbach",null],["071400501501","Boppard, Stadt",null],["071405003001","Alterkülz",null],["071405003009","Bell (Hunsrück)",null],["071405003010","Beltheim",null],["071405003018","Braunshorn",null],["071405003021","Buch",null],["071405003042","Gödenroth",null],["071405003046","Hasselbach",null],["071405003055","Hollnich",null],["071405003064","Kastellaun, Stadt",null],["071405003073","Korweiler",null],["071405003095","Michelbach",null],["071405003131","Roth",null],["071405003147","Spesenroth",null],["071405003153","Uhler",null],["071405003202","Dommershausen",null],["071405003204","Mastershausen",null],["071405003502","Lahr",null],["071405003503","Mörsdorf",null],["071405003504","Zilshausen",null],["071405004006","Bärenbach",null],["071405004007","Belg",null],["071405004024","Büchenbeuren",null],["071405004028","Dickenschied",null],["071405004029","Dill",null],["071405004030","Dillendorf",null],["071405004040","Gehlweiler",null],["071405004041","Gemünden",null],["071405004044","Hahn",null],["071405004048","Hecken",null],["071405004049","Heinzenbach",null],["071405004050","Henau",null],["071405004053","Hirschfeld (Hunsrück)",null],["071405004062","Kappel",null],["071405004067","Kirchberg (Hunsrück), Stadt",null],["071405004071","Kludenbach",null],["071405004081","Laufersweiler",null],["071405004082","Lautzenhausen",null],["071405004086","Lindenschied",null],["071405004090","Maitzborn",null],["071405004094","Metzenhausen",null],["071405004105","Nieder Kostenz",null],["071405004107","Niedersohren",null],["071405004109","Niederweiler",null],["071405004111","Ober Kostenz",null],["071405004120","Raversbeuren",null],["071405004122","Reckershausen",null],["071405004128","Rödelhausen",null],["071405004129","Rödern",null],["071405004130","Rohrbach",null],["071405004135","Schlierschied",null],["071405004141","Schwarzen",null],["071405004145","Sohren",null],["071405004146","Sohrschied",null],["071405004151","Todenroth",null],["071405004154","Unzenberg",null],["071405004159","Wahlenau",null],["071405004163","Womrath",null],["071405004164","Woppenroth",null],["071405004165","Würrich",null],["071405008002","Altweidelbach",null],["071405008003","Argenthal",null],["071405008008","Belgweiler",null],["071405008011","Benzweiler",null],["071405008012","Bergenhausen",null],["071405008015","Biebern",null],["071405008020","Bubach",null],["071405008023","Budenbach",null],["071405008027","Dichtelbach",null],["071405008035","Ellern (Hunsrück)",null],["071405008037","Erbach",null],["071405008039","Fronhofen",null],["071405008056","Holzbach",null],["071405008058","Horn",null],["071405008065","Keidelheim",null],["071405008068","Kisselbach",null],["071405008070","Klosterkumbd",null],["071405008076","Külz (Hunsrück)",null],["071405008077","Kümbdchen",null],["071405008079","Laubach",null],["071405008085","Liebshausen",null],["071405008092","Mengerschied",null],["071405008096","Mörschbach",null],["071405008099","Mutterschied",null],["071405008100","Nannhausen",null],["071405008101","Neuerkirch",null],["071405008106","Niederkumbd",null],["071405008113","Ohlweiler",null],["071405008115","Oppertshausen",null],["071405008118","Pleizenhausen",null],["071405008119","Ravengiersburg",null],["071405008121","Rayerschied",null],["071405008123","Reich",null],["071405008125","Rheinböllen, Stadt",null],["071405008126","Riegenroth",null],["071405008127","Riesweiler",null],["071405008134","Sargenroth",null],["071405008138","Schnorbach",null],["071405008139","Schönborn",null],["071405008144","Simmern/ Hunsrück, Stadt",null],["071405008148","Steinbach",null],["071405008150","Tiefenbach",null],["071405008158","Wahlbach",null],["071405008166","Wüschheim",null],["071405009005","Badenhard",null],["071405009014","Bickenbach",null],["071405009016","Birkheim",null],["071405009025","Damscheid",null],["071405009031","Dörth",null],["071405009036","Emmelshausen, Stadt",null],["071405009043","Gondershausen",null],["071405009045","Halsenbach",null],["071405009047","Hausbay",null],["071405009060","Hungenroth",null],["071405009063","Karbach",null],["071405009075","Kratzenburg",null],["071405009080","Laudert",null],["071405009084","Leiningen",null],["071405009087","Lingerhahn",null],["071405009089","Maisborn",null],["071405009093","Mermuth",null],["071405009098","Mühlpfad",null],["071405009102","Ney",null],["071405009104","Niederburg",null],["071405009108","Niedert",null],["071405009110","Norath",null],["071405009112","Oberwesel, Stadt",null],["071405009116","Perscheid",null],["071405009117","Pfalzfeld",null],["071405009133","Sankt Goar, Stadt",null],["071405009140","Schwall",null],["071405009149","Thörlingen",null],["071405009155","Urbar",null],["071405009156","Utzenhain",null],["071405009161","Wiebelsheim",null],["071405009201","Beulich",null],["071405009205","Morshausen",null],["071410075075","Lahnstein, Stadt",null],["071415003002","Altendiez",null],["071415003005","Aull",null],["071415003014","Birlenbach",null],["071415003021","Charlottenberg",null],["071415003022","Cramberg",null],["071415003029","Diez, Stadt",null],["071415003030","Dörnberg",null],["071415003038","Eppenrod",null],["071415003045","Geilnau",null],["071415003049","Gückingen",null],["071415003052","Hambach",null],["071415003053","Heistenbach",null],["071415003057","Hirschberg",null],["071415003059","Holzappel",null],["071415003061","Holzheim",null],["071415003062","Horhausen",null],["071415003064","Isselbach",null],["071415003076","Langenscheid",null],["071415003077","Laurenburg",null],["071415003124","Scheidt",null],["071415003130","Steinsberg",null],["071415003133","Wasenbach",null],["071415003503","Balduinstein",null],["071415007009","Berg",null],["071415007012","Bettendorf",null],["071415007015","Bogel",null],["071415007019","Buch",null],["071415007035","Ehr",null],["071415007037","Endlichhofen",null],["071415007040","Eschbach",null],["071415007047","Gemmerich",null],["071415007055","Himmighofen",null],["071415007060","Holzhausen an der Haide",null],["071415007063","Hunzel",null],["071415007067","Kasdorf",null],["071415007070","Kehlbach",null],["071415007078","Lautert",null],["071415007080","Lipporn",null],["071415007084","Marienfels",null],["071415007085","Miehlen",null],["071415007092","Nastätten, Stadt",null],["071415007094","Niederbachheim",null],["071415007097","Niederwallmenach",null],["071415007100","Oberbachheim",null],["071415007104","Obertiefenbach",null],["071415007105","Oberwallmenach",null],["071415007107","Oelsberg",null],["071415007110","Hainau",null],["071415007116","Rettershain",null],["071415007120","Ruppertshofen",null],["071415007131","Strüth",null],["071415007134","Weidenbach",null],["071415007137","Welterod",null],["071415007140","Winterwerb",null],["071415007502","Diethardt",null],["071415009004","Auel",null],["071415009016","Bornich",null],["071415009023","Dachsenhausen",null],["071415009024","Dahlheim",null],["071415009031","Dörscheid",null],["071415009042","Filsen",null],["071415009066","Kamp-Bornhofen",null],["071415009069","Kaub, Stadt",null],["071415009072","Kestert",null],["071415009079","Lierschied",null],["071415009083","Lykershausen",null],["071415009099","Nochern",null],["071415009108","Osterspai",null],["071415009109","Patersberg",null],["071415009112","Prath",null],["071415009114","Reichenberg",null],["071415009115","Reitzenhain",null],["071415009121","Sankt Goarshausen, Loreleystadt, Stadt",null],["071415009122","Sauerthal",null],["071415009136","Weisel",null],["071415009138","Weyer",null],["071415009501","Braubach, Stadt",null],["071415010003","Attenhausen",null],["071415010006","Bad Ems, Stadt",null],["071415010008","Becheln",null],["071415010025","Dausenau",null],["071415010026","Dessighofen",null],["071415010027","Dienethal",null],["071415010033","Dornholzhausen",null],["071415010041","Fachbach",null],["071415010044","Frücht",null],["071415010046","Geisig",null],["071415010058","Hömberg",null],["071415010071","Kemmenau",null],["071415010082","Lollschied",null],["071415010086","Miellen",null],["071415010087","Misselberg",null],["071415010091","Nassau, Stadt",null],["071415010098","Nievern",null],["071415010103","Obernhof",null],["071415010106","Oberwies",null],["071415010111","Pohl",null],["071415010127","Schweighausen",null],["071415010128","Seelbach",null],["071415010129","Singhofen",null],["071415010132","Sulzbach",null],["071415010135","Weinähr",null],["071415010139","Winden",null],["071415010141","Zimmerschied",null],["071415010201","Arzbach",null],["071415011001","Allendorf",null],["071415011010","Berghausen",null],["071415011011","Berndroth",null],["071415011013","Biebrich",null],["071415011018","Bremberg",null],["071415011020","Burgschwalbach",null],["071415011032","Dörsdorf",null],["071415011034","Ebertshausen",null],["071415011036","Eisighofen",null],["071415011039","Ergeshausen",null],["071415011043","Flacht",null],["071415011050","Gutenacker",null],["071415011051","Hahnstätten",null],["071415011054","Herold",null],["071415011065","Kaltenholzhausen",null],["071415011068","Katzenelnbogen, Stadt",null],["071415011073","Klingelbach",null],["071415011074","Kördorf",null],["071415011081","Lohrheim",null],["071415011088","Mittelfischbach",null],["071415011089","Mudershausen",null],["071415011093","Netzbach",null],["071415011095","Niederneisen",null],["071415011096","Niedertiefenbach",null],["071415011101","Oberfischbach",null],["071415011102","Oberneisen",null],["071415011113","Reckenroth",null],["071415011117","Rettert",null],["071415011118","Roth",null],["071415011125","Schiesheim",null],["071415011126","Schönborn",null],["071435001206","Bad Marienberg (Westerwald), Stadt",null],["071435001211","Bölsberg",null],["071435001216","Dreisbach",null],["071435001222","Fehl-Ritzhausen",null],["071435001227","Großseifen",null],["071435001231","Hahn bei Marienberg",null],["071435001234","Hardt",null],["071435001243","Hof",null],["071435001248","Kirburg",null],["071435001253","Langenbach bei Kirburg",null],["071435001255","Lautzenbrücken",null],["071435001264","Mörlen",null],["071435001270","Neunkhausen",null],["071435001277","Nisterau",null],["071435001279","Nistertal",null],["071435001280","Norken",null],["071435001297","Stockhausen-Illfurth",null],["071435001300","Unnau",null],["071435002202","Alpenrod",null],["071435002204","Astert",null],["071435002205","Atzelgift",null],["071435002212","Borod",null],["071435002215","Dreifelden",null],["071435002223","Gehlert",null],["071435002225","Giesenhausen",null],["071435002229","Hachenburg, Stadt",null],["071435002235","Hattert",null],["071435002236","Heimborn",null],["071435002240","Heuzert",null],["071435002241","Höchstenbach",null],["071435002250","Kroppach",null],["071435002252","Kundert",null],["071435002257","Limbach",null],["071435002258","Linden",null],["071435002259","Lochum",null],["071435002260","Luckenbach",null],["071435002261","Marzhausen",null],["071435002262","Merkelbach",null],["071435002265","Mörsbach",null],["071435002267","Mudenbach",null],["071435002268","Mündersbach",null],["071435002269","Müschenbach",null],["071435002276","Nister",null],["071435002287","Roßbach",null],["071435002294","Steinebach an der Wied",null],["071435002296","Stein-Wingert",null],["071435002299","Streithausen",null],["071435002301","Wahlrod",null],["071435002306","Welkenbach",null],["071435002310","Wied",null],["071435002313","Winkelbach",null],["071435003030","Hilgert",null],["071435003031","Hillscheid",null],["071435003032","Höhr-Grenzhausen, Stadt",null],["071435003040","Kammerforst",null],["071435004005","Boden",null],["071435004008","Daubach",null],["071435004013","Eitelborn",null],["071435004020","Gackenbach",null],["071435004021","Girod",null],["071435004023","Görgeshausen",null],["071435004024","Großholbach",null],["071435004026","Heilberscheid",null],["071435004027","Heiligenroth",null],["071435004033","Holler",null],["071435004034","Horbach",null],["071435004036","Hübingen",null],["071435004039","Kadenbach",null],["071435004048","Montabaur, Stadt",null],["071435004051","Nentershausen",null],["071435004052","Neuhäusel",null],["071435004053","Niederelbert",null],["071435004054","Niedererbach",null],["071435004055","Nomborn",null],["071435004057","Oberelbert",null],["071435004065","Ruppach-Goldhausen",null],["071435004071","Simmern",null],["071435004072","Stahlhofen",null],["071435004077","Untershausen",null],["071435004079","Welschneudorf",null],["071435005001","Alsbach",null],["071435005006","Breitenau",null],["071435005007","Caan",null],["071435005009","Deesen",null],["071435005038","Hundsdorf",null],["071435005050","Nauort",null],["071435005059","Oberhaid",null],["071435005062","Ransbach-Baumbach, Stadt",null],["071435005068","Sessenbach",null],["071435005082","Wirscheid",null],["071435005084","Wittgert",null],["071435006214","Bretthausen",null],["071435006218","Elsoff (Westerwald)",null],["071435006237","Hellenhahn-Schellenberg",null],["071435006244","Homberg",null],["071435006245","Hüblingen",null],["071435006246","Irmtraut",null],["071435006256","Liebenscheid",null],["071435006271","Neunkirchen",null],["071435006272","Neustadt/ Westerwald",null],["071435006274","Niederroßbach",null],["071435006278","Nister-Möhrendorf",null],["071435006282","Oberrod",null],["071435006283","Oberroßbach",null],["071435006285","Rehe",null],["071435006286","Rennerod, Stadt",null],["071435006291","Salzburg",null],["071435006292","Seck",null],["071435006295","Stein-Neukirch",null],["071435006302","Waigandshain",null],["071435006303","Waldmühlen",null],["071435006309","Westernohe",null],["071435006311","Willingen",null],["071435006315","Zehnhausen bei Rennerod",null],["071435007015","Ellenhausen",null],["071435007018","Freilingen",null],["071435007019","Freirachdorf",null],["071435007022","Goddert",null],["071435007025","Hartenfels",null],["071435007029","Herschbach",null],["071435007041","Krümmel",null],["071435007044","Marienrachdorf",null],["071435007045","Maroth",null],["071435007046","Maxsain",null],["071435007056","Nordhofen",null],["071435007061","Quirnbach",null],["071435007064","Rückeroth",null],["071435007066","Schenkelberg",null],["071435007067","Selters (Westerwald), Stadt",null],["071435007069","Sessenhausen",null],["071435007075","Steinen",null],["071435007078","Vielbach",null],["071435007085","Wölferlingen",null],["071435007221","Ewighausen",null],["071435007305","Weidenhahn",null],["071435008011","Dreikirchen",null],["071435008037","Hundsangen",null],["071435008058","Obererbach",null],["071435008074","Steinefrenz",null],["071435008080","Weroth",null],["071435008203","Arnshöfen",null],["071435008208","Berod bei Wallmerod",null],["071435008210","Bilkheim",null],["071435008220","Ettinghausen",null],["071435008232","Hahn am See",null],["071435008239","Herschbach (Oberwesterwald)",null],["071435008251","Kuhnhöfen",null],["071435008263","Meudt",null],["071435008266","Molsberg",null],["071435008273","Niederahr",null],["071435008281","Oberahr",null],["071435008290","Salz",null],["071435008304","Wallmerod",null],["071435008316","Zehnhausen bei Wallmerod",null],["071435008501","Elbingen",null],["071435008502","Mähren",null],["071435009200","Ailertchen",null],["071435009207","Bellingen",null],["071435009209","Berzhahn",null],["071435009213","Brandscheid",null],["071435009219","Enspel",null],["071435009224","Gemünden",null],["071435009226","Girkenroth",null],["071435009228","Guckheim",null],["071435009230","Härtlingen",null],["071435009233","Halbs",null],["071435009238","Hergenroth",null],["071435009242","Höhn",null],["071435009247","Kaden",null],["071435009249","Kölbingen",null],["071435009254","Langenhahn",null],["071435009284","Pottum",null],["071435009288","Rotenhain",null],["071435009289","Rothenbach",null],["071435009293","Stahlhofen am Wiesensee",null],["071435009298","Stockum-Püschen",null],["071435009307","Weltersburg",null],["071435009308","Westerburg, Stadt",null],["071435009312","Willmenrod",null],["071435009314","Winnen",null],["071435010003","Bannberscheid",null],["071435010010","Dernbach (Westerwald)",null],["071435010012","Ebernhahn",null],["071435010028","Helferskirchen",null],["071435010042","Leuterod",null],["071435010047","Mogendorf",null],["071435010049","Moschheim",null],["071435010060","Ötzingen",null],["071435010070","Siershahn",null],["071435010073","Staudt",null],["071435010081","Wirges, Stadt",null],["071435010275","Niedersayn",null],["072110000000","Trier, Stadt",null],["072310134134","Wittlich, Stadt",null],["072310502502","Morbach",null],["072315001008","Bernkastel-Kues, Stadt",null],["072315001012","Brauneberg",null],["072315001016","Burgen",null],["072315001030","Erden",null],["072315001040","Gornhausen",null],["072315001041","Graach an der Mosel",null],["072315001056","Hochscheid",null],["072315001066","Kesten",null],["072315001070","Kleinich",null],["072315001071","Kommen",null],["072315001075","Lieser",null],["072315001076","Lösnich",null],["072315001077","Longkamp",null],["072315001081","Maring-Noviand",null],["072315001086","Minheim",null],["072315001087","Monzelfeld",null],["072315001090","Mülheim an der Mosel",null],["072315001092","Neumagen-Dhron",null],["072315001105","Piesport",null],["072315001125","Ürzig",null],["072315001126","Veldenz",null],["072315001133","Wintrich",null],["072315001136","Zeltingen-Rachtig",null],["072315006006","Berglicht",null],["072315006017","Burtscheid",null],["072315006018","Deuselbach",null],["072315006019","Dhronecken",null],["072315006032","Etgert",null],["072315006035","Gielert",null],["072315006042","Gräfendhron",null],["072315006054","Hilscheid",null],["072315006058","Horath",null],["072315006064","Immert",null],["072315006078","Lückenburg",null],["072315006079","Malborn",null],["072315006083","Merschbach",null],["072315006093","Neunkirchen",null],["072315006112","Rorodt",null],["072315006115","Schönberg",null],["072315006122","Talling",null],["072315006123","Thalfang",null],["072315006202","Breit",null],["072315006203","Büdlich",null],["072315006204","Heidenburg",null],["072315008001","Altrich",null],["072315008003","Arenrath",null],["072315008007","Bergweiler",null],["072315008009","Bettenfeld",null],["072315008010","Binsfeld",null],["072315008013","Bruch",null],["072315008021","Dierfeld",null],["072315008022","Dierscheid",null],["072315008023","Dodenburg",null],["072315008024","Dreis",null],["072315008025","Eckfeld",null],["072315008026","Eisenschmitt",null],["072315008031","Esch",null],["072315008036","Gipperath",null],["072315008037","Gladbach",null],["072315008044","Greimerath",null],["072315008046","Großlittgen",null],["072315008049","Hasborn",null],["072315008050","Heckenmünster",null],["072315008051","Heidweiler",null],["072315008053","Hetzerath",null],["072315008062","Hupperath",null],["072315008065","Karl",null],["072315008069","Klausen",null],["072315008074","Laufeld",null],["072315008080","Manderscheid, Stadt",null],["072315008082","Meerfeld",null],["072315008085","Minderlittgen",null],["072315008091","Musweiler",null],["072315008095","Niederöfflingen",null],["072315008096","Niederscheidweiler",null],["072315008100","Oberöfflingen",null],["072315008101","Oberscheidweiler",null],["072315008103","Osann-Monzel",null],["072315008104","Pantenburg",null],["072315008107","Platten",null],["072315008108","Plein",null],["072315008111","Rivenich",null],["072315008113","Salmtal",null],["072315008114","Schladt",null],["072315008116","Schwarzenborn",null],["072315008117","Sehlem",null],["072315008127","Wallscheid",null],["072315008503","Landscheid",null],["072315008504","Niersbach",null],["072315009004","Bausendorf",null],["072315009005","Bengel",null],["072315009014","Burg (Mosel)",null],["072315009020","Diefenbach",null],["072315009029","Enkirch",null],["072315009033","Flußbach",null],["072315009057","Hontheim",null],["072315009067","Kinderbeuern",null],["072315009068","Kinheim",null],["072315009072","Kröv",null],["072315009110","Reil",null],["072315009120","Starkenburg",null],["072315009124","Traben-Trarbach, Stadt",null],["072315009132","Willwerscheid",null],["072315009206","Lötzbeuren",null],["072315009501","Irmenach",null],["072320018018","Bitburg, Stadt",null],["072325001201","Arzfeld",null],["072325001211","Dackscheid",null],["072325001212","Dahnen",null],["072325001213","Daleiden",null],["072325001214","Dasburg",null],["072325001217","Eilscheid",null],["072325001220","Eschfeld",null],["072325001221","Euscheid",null],["072325001229","Großkampenberg",null],["072325001233","Hargarten",null],["072325001234","Harspelt",null],["072325001240","Herzfeld",null],["072325001245","Irrhausen",null],["072325001246","Jucken",null],["072325001247","Kesfeld",null],["072325001248","Kickeshausen",null],["072325001249","Kinzenburg",null],["072325001253","Krautscheid",null],["072325001254","Lambertsberg",null],["072325001255","Lascheid",null],["072325001258","Lauperath",null],["072325001259","Leidenborn",null],["072325001260","Lichtenborn",null],["072325001261","Lierfeld",null],["072325001262","Lünebach",null],["072325001263","Lützkampen",null],["072325001264","Manderscheid",null],["072325001267","Mauel",null],["072325001270","Merlscheid",null],["072325001277","Niederpierscheid",null],["072325001285","Oberpierscheid",null],["072325001287","Olmscheid",null],["072325001291","Pintesfeld",null],["072325001293","Plütscheid",null],["072325001294","Preischeid",null],["072325001297","Reiff",null],["072325001298","Reipeldingen",null],["072325001301","Roscheid",null],["072325001309","Sengerich",null],["072325001310","Sevenig (Our)",null],["072325001315","Strickscheid",null],["072325001322","Waxweiler",null],["072325001333","Üttfeld",null],["072325005001","Affler",null],["072325005002","Alsdorf",null],["072325005003","Altscheid",null],["072325005004","Ammeldingen an der Our",null],["072325005005","Ammeldingen bei Neuerburg",null],["072325005008","Bauler",null],["072325005011","Berkoth",null],["072325005012","Berscheid",null],["072325005016","Biesdorf",null],["072325005019","Bollendorf",null],["072325005022","Burg",null],["072325005025","Dauwelshausen",null],["072325005028","Echternacherbrück",null],["072325005031","Emmelbaum",null],["072325005033","Ernzen",null],["072325005037","Ferschweiler",null],["072325005038","Fischbach-Oberraden",null],["072325005040","Geichlingen",null],["072325005041","Gemünd",null],["072325005042","Gentingen",null],["072325005047","Heilbach",null],["072325005049","Herbstmühle",null],["072325005053","Holsthum",null],["072325005054","Hommerdingen",null],["072325005056","Hütten",null],["072325005059","Hüttingen bei Lahr",null],["072325005063","Irrel",null],["072325005064","Karlshausen",null],["072325005065","Kaschenbach",null],["072325005066","Keppeshausen",null],["072325005067","Körperich",null],["072325005068","Koxhausen",null],["072325005069","Kruchten",null],["072325005072","Lahr",null],["072325005073","Leimbach",null],["072325005078","Menningen",null],["072325005080","Mettendorf",null],["072325005082","Minden",null],["072325005084","Muxerath",null],["072325005085","Nasingen",null],["072325005088","Neuerburg, Stadt",null],["072325005089","Niedergeckler",null],["072325005090","Niederraden",null],["072325005093","Niederweis",null],["072325005094","Niehl",null],["072325005095","Nusbaum",null],["072325005096","Obergeckler",null],["072325005102","Utscheid",null],["072325005103","Peffingen",null],["072325005106","Plascheid",null],["072325005108","Prümzurlay",null],["072325005110","Rodershausen",null],["072325005112","Roth an der Our",null],["072325005114","Schankweiler",null],["072325005116","Scheitenkorb",null],["072325005117","Scheuern",null],["072325005121","Sevenig bei Neuerburg",null],["072325005122","Sinspelt",null],["072325005127","Übereisenbach",null],["072325005128","Uppershausen",null],["072325005130","Waldhof-Falkenstein",null],["072325005131","Wallendorf",null],["072325005132","Weidingen",null],["072325005138","Zweifelscheid",null],["072325005218","Eisenach",null],["072325005225","Gilzem",null],["072325006202","Auw bei Prüm",null],["072325006206","Bleialf",null],["072325006207","Brandscheid",null],["072325006208","Buchet",null],["072325006209","Büdesheim",null],["072325006216","Dingdorf",null],["072325006222","Feuerscheid",null],["072325006223","Fleringen",null],["072325006224","Giesdorf",null],["072325006226","Weinsheim",null],["072325006227","Gondenbrett",null],["072325006230","Großlangenfeld",null],["072325006231","Habscheid",null],["072325006236","Heckhuscheid",null],["072325006238","Heisdorf",null],["072325006250","Kleinlangenfeld",null],["072325006256","Lasel",null],["072325006265","Masthorn",null],["072325006266","Matzerath",null],["072325006271","Mützenich",null],["072325006272","Neuendorf",null],["072325006276","Niederlauch",null],["072325006279","Nimshuscheid",null],["072325006280","Nimsreuland",null],["072325006283","Oberlascheid",null],["072325006284","Oberlauch",null],["072325006288","Olzheim",null],["072325006290","Orlenbach",null],["072325006292","Pittenbach",null],["072325006295","Pronsfeld",null],["072325006296","Prüm, Stadt",null],["072325006300","Rommersheim",null],["072325006302","Roth bei Prüm",null],["072325006304","Schönecken",null],["072325006305","Schwirzheim",null],["072325006307","Seiwerath",null],["072325006308","Sellerich",null],["072325006318","Wallersheim",null],["072325006320","Watzerath",null],["072325006321","Wawern",null],["072325006327","Winringen",null],["072325006328","Winterscheid",null],["072325006329","Winterspelt",null],["072325006332","Hersdorf",null],["072325007006","Auw an der Kyll",null],["072325007010","Beilingen",null],["072325007050","Herforst",null],["072325007055","Hosten",null],["072325007104","Philippsheim",null],["072325007107","Preist",null],["072325007123","Speicher, Stadt",null],["072325007289","Orenhofen",null],["072325007311","Spangdahlem",null],["072325008007","Badem",null],["072325008009","Baustert",null],["072325008013","Bettingen",null],["072325008014","Bickendorf",null],["072325008015","Biersdorf am See",null],["072325008017","Birtlingen",null],["072325008020","Brecht",null],["072325008024","Dahlem",null],["072325008026","Dockendorf",null],["072325008027","Dudeldorf",null],["072325008029","Echtershausen",null],["072325008030","Ehlenz",null],["072325008032","Enzen",null],["072325008034","Eßlingen",null],["072325008035","Etteldorf",null],["072325008036","Feilsdorf",null],["072325008039","Fließem",null],["072325008043","Gindorf",null],["072325008044","Gondorf",null],["072325008045","Halsdorf",null],["072325008046","Hamm",null],["072325008048","Heilenbach",null],["072325008057","Hütterscheid",null],["072325008058","Hüttingen an der Kyll",null],["072325008060","Idenheim",null],["072325008061","Idesheim",null],["072325008062","Ingendorf",null],["072325008070","Kyllburg, Stadt",null],["072325008071","Kyllburgweiler",null],["072325008074","Ließem",null],["072325008075","Malberg",null],["072325008076","Malbergweich",null],["072325008077","Meckel",null],["072325008079","Messerich",null],["072325008081","Metterich",null],["072325008083","Mülbach",null],["072325008086","Nattenheim",null],["072325008087","Neidenbach",null],["072325008091","Niederstedem",null],["072325008092","Niederweiler",null],["072325008097","Oberstedem",null],["072325008098","Oberweiler",null],["072325008099","Oberweis",null],["072325008100","Olsdorf",null],["072325008101","Orsfeld",null],["072325008105","Pickließem",null],["072325008109","Rittersdorf",null],["072325008111","Röhl",null],["072325008113","Sankt Thomas",null],["072325008115","Scharfbillig",null],["072325008118","Schleid",null],["072325008119","Seffern",null],["072325008120","Sefferweich",null],["072325008124","Stockem",null],["072325008125","Sülm",null],["072325008126","Trimport",null],["072325008129","Usch",null],["072325008133","Wettlingen",null],["072325008134","Wiersdorf",null],["072325008135","Wilsecker",null],["072325008137","Wolsfeld",null],["072325008203","Balesfeld",null],["072325008210","Burbach",null],["072325008228","Gransdorf",null],["072325008273","Neuheilenbach",null],["072325008282","Oberkail",null],["072325008306","Seinsfeld",null],["072325008313","Steinborn",null],["072325008331","Zendscheid",null],["072325008501","Wißmannsdorf",null],["072325008502","Brimingen",null],["072335001006","Betteldorf",null],["072335001008","Bleckhausen",null],["072335001011","Brockscheid",null],["072335001014","Darscheid",null],["072335001016","Demerath",null],["072335001017","Deudesfeld",null],["072335001018","Dockweiler",null],["072335001020","Dreis-Brück",null],["072335001021","Ellscheid",null],["072335001025","Gefell",null],["072335001027","Gillenfeld",null],["072335001030","Hinterweiler",null],["072335001031","Hörscheid",null],["072335001034","Immerath",null],["072335001039","Kirchweiler",null],["072335001040","Kradenbach",null],["072335001042","Mehren",null],["072335001043","Meisburg",null],["072335001046","Mückeln",null],["072335001049","Nerdlen",null],["072335001052","Niederstadtfeld",null],["072335001055","Oberstadtfeld",null],["072335001061","Sarmersbach",null],["072335001062","Saxler",null],["072335001063","Schalkenmehren",null],["072335001064","Schönbach",null],["072335001065","Schutz",null],["072335001067","Steineberg",null],["072335001068","Steiningen",null],["072335001070","Strohn",null],["072335001071","Strotzbüsch",null],["072335001074","Udler",null],["072335001075","Üdersdorf",null],["072335001077","Utzerath",null],["072335001079","Wallenborn",null],["072335001081","Weidenbach",null],["072335001084","Winkel (Eifel)",null],["072335001501","Daun, Stadt",null],["072335004003","Beinhausen",null],["072335004010","Boxberg",null],["072335004032","Hörschhausen",null],["072335004037","Katzwinkel",null],["072335004048","Neichen",null],["072335004201","Arbach",null],["072335004202","Bereborn",null],["072335004203","Berenbach",null],["072335004205","Bodenbach",null],["072335004206","Bongard",null],["072335004207","Borler",null],["072335004208","Brücktal",null],["072335004210","Drees",null],["072335004212","Gelenberg",null],["072335004213","Gunderath",null],["072335004215","Höchstberg",null],["072335004216","Horperath",null],["072335004217","Kaperich",null],["072335004218","Kelberg",null],["072335004220","Kirsbach",null],["072335004221","Kötterichen",null],["072335004222","Kolverath",null],["072335004224","Lirstal",null],["072335004225","Mannebach",null],["072335004226","Mosbruch",null],["072335004228","Nitz",null],["072335004230","Oberelz",null],["072335004233","Reimerath",null],["072335004234","Retterath",null],["072335004236","Sassen",null],["072335004242","Uersfeld",null],["072335004243","Ueß",null],["072335004244","Welcherath",null],["072335006002","Basberg",null],["072335006004","Berlingen",null],["072335006005","Berndorf",null],["072335006007","Birgel",null],["072335006019","Dohm-Lammersdorf",null],["072335006022","Esch",null],["072335006023","Feusdorf",null],["072335006026","Gerolstein, Stadt",null],["072335006028","Gönnersdorf",null],["072335006029","Hillesheim, Stadt",null],["072335006033","Hohenfels-Essingen",null],["072335006035","Jünkerath",null],["072335006036","Kalenborn-Scheuern",null],["072335006038","Kerpen (Eifel)",null],["072335006041","Lissendorf",null],["072335006050","Neroth",null],["072335006053","Oberbettingen",null],["072335006054","Oberehe-Stroheich",null],["072335006056","Pelm",null],["072335006058","Rockeskyll",null],["072335006060","Salm",null],["072335006076","Üxheim",null],["072335006080","Walsdorf",null],["072335006083","Wiesbaum",null],["072335006204","Birresborn",null],["072335006209","Densborn",null],["072335006211","Duppach",null],["072335006214","Hallschlag",null],["072335006219","Kerschenbach",null],["072335006223","Kopp",null],["072335006227","Mürlenbach",null],["072335006229","Nohn",null],["072335006232","Ormont",null],["072335006235","Reuth",null],["072335006237","Scheid",null],["072335006239","Schüller",null],["072335006240","Stadtkyll",null],["072335006241","Steffeln",null],["072355001005","Bescheid",null],["072355001008","Beuren (Hochwald)",null],["072355001014","Damflos",null],["072355001030","Geisfeld",null],["072355001035","Grimburg",null],["072355001036","Gusenburg",null],["072355001045","Hermeskeil, Stadt",null],["072355001047","Hinzert-Pölert",null],["072355001092","Naurath (Wald)",null],["072355001093","Neuhütten",null],["072355001112","Rascheid",null],["072355001114","Reinsfeld",null],["072355001153","Züsch",null],["072355003055","Kanzem",null],["072355003068","Konz, Stadt",null],["072355003095","Nittel",null],["072355003096","Oberbillig",null],["072355003101","Onsdorf",null],["072355003106","Pellingen",null],["072355003132","Tawern",null],["072355003133","Temmels",null],["072355003143","Wasserliesch",null],["072355003144","Wawern",null],["072355003146","Wellen",null],["072355003148","Wiltingen",null],["072355004010","Bonerath",null],["072355004021","Farschweiler",null],["072355004037","Gusterath",null],["072355004038","Gutweiler",null],["072355004044","Herl",null],["072355004046","Hinzenburg",null],["072355004050","Holzerath",null],["072355004056","Kasel",null],["072355004070","Korlingen",null],["072355004080","Lorscheid",null],["072355004085","Mertesdorf",null],["072355004090","Morscheid",null],["072355004100","Ollmuth",null],["072355004103","Osburg",null],["072355004107","Pluwig",null],["072355004116","Riveris",null],["072355004124","Schöndorf",null],["072355004129","Sommerau",null],["072355004135","Thomm",null],["072355004141","Waldrach",null],["072355006004","Bekond",null],["072355006015","Detzem",null],["072355006019","Ensch",null],["072355006022","Fell",null],["072355006026","Föhren",null],["072355006060","Kenn",null],["072355006063","Klüsserath",null],["072355006067","Köwerich",null],["072355006074","Leiwen",null],["072355006077","Longen",null],["072355006078","Longuich",null],["072355006083","Mehring",null],["072355006091","Naurath (Eifel)",null],["072355006108","Pölich",null],["072355006115","Riol",null],["072355006120","Schleich",null],["072355006125","Schweich, Stadt",null],["072355006134","Thörnich",null],["072355006207","Trittenheim",null],["072355007001","Aach",null],["072355007027","Franzenheim",null],["072355007048","Hockweiler",null],["072355007051","Igel",null],["072355007069","Kordel",null],["072355007073","Langsur",null],["072355007094","Newel",null],["072355007111","Ralingen",null],["072355007137","Trierweiler",null],["072355007151","Zemmer",null],["072355007501","Welschbillig",null],["072355008002","Ayl",null],["072355008003","Baldringen",null],["072355008025","Fisch",null],["072355008028","Freudenburg",null],["072355008033","Greimerath",null],["072355008040","Heddert",null],["072355008043","Hentern",null],["072355008052","Irsch",null],["072355008057","Kastel-Staadt",null],["072355008058","Kell am See",null],["072355008062","Kirf",null],["072355008072","Lampaden",null],["072355008081","Mandern",null],["072355008082","Mannebach",null],["072355008098","Ockfen",null],["072355008104","Palzem",null],["072355008105","Paschel",null],["072355008118","Saarburg, Stadt",null],["072355008119","Schillingen",null],["072355008122","Schoden",null],["072355008123","Schömerich",null],["072355008126","Serrig",null],["072355008131","Taben-Rodt",null],["072355008136","Trassem",null],["072355008140","Vierherrenborn",null],["072355008142","Waldweiler",null],["072355008149","Wincheringen",null],["072355008152","Zerf",null],["072355008154","Merzkirchen",null],["073110000000","Frankenthal (Pfalz), Stadt",null],["073120000000","Kaiserslautern, Stadt",null],["073130000000","Landau in der Pfalz, Stadt",null],["073140000000","Ludwigshafen am Rhein, Stadt",null],["073150000000","Mainz, Stadt",null],["073160000000","Neustadt an der Weinstraße, Stadt",null],["073170000000","Pirmasens, Stadt",null],["073180000000","Speyer, Stadt",null],["073190000000","Worms, Stadt",null],["073200000000","Zweibrücken, Stadt",null],["073310003003","Alzey, Stadt",null],["073315001001","Albig",null],["073315001005","Bechenheim",null],["073315001007","Bechtolsheim",null],["073315001008","Bermersheim vor der Höhe",null],["073315001010","Biebelnheim",null],["073315001012","Bornheim",null],["073315001014","Dintesheim",null],["073315001020","Eppelsheim",null],["073315001021","Erbes-Büdesheim",null],["073315001022","Esselborn",null],["073315001024","Flomborn",null],["073315001025","Flonheim",null],["073315001026","Framersheim",null],["073315001027","Freimersheim",null],["073315001031","Gau-Heppenheim",null],["073315001032","Gau-Odernheim",null],["073315001042","Kettenheim",null],["073315001043","Lonsheim",null],["073315001044","Mauchenheim",null],["073315001050","Nack",null],["073315001051","Nieder-Wiesen",null],["073315001052","Ober-Flörsheim",null],["073315001053","Offenheim",null],["073315001067","Wahlheim",null],["073315002002","Alsheim",null],["073315002018","Eich",null],["073315002034","Gimbsheim",null],["073315002038","Hamm am Rhein",null],["073315002045","Mettenheim",null],["073315003023","Flörsheim-Dalsheim",null],["073315003041","Hohen-Sülzen",null],["073315003046","Mölsheim",null],["073315003047","Mörstadt",null],["073315003048","Monsheim",null],["073315003054","Offstein",null],["073315003066","Wachenheim",null],["073315005017","Eckelsheim",null],["073315005030","Gau-Bickelheim",null],["073315005035","Gumbsheim",null],["073315005060","Siefersheim",null],["073315005062","Stein-Bockenheim",null],["073315005070","Wendelsheim",null],["073315005072","Wöllstein",null],["073315005075","Wonsheim",null],["073315006004","Armsheim",null],["073315006019","Ensheim",null],["073315006029","Gabsheim",null],["073315006033","Gau-Weinheim",null],["073315006056","Partenheim",null],["073315006058","Saulheim",null],["073315006059","Schornsheim",null],["073315006061","Spiesheim",null],["073315006063","Sulzheim",null],["073315006064","Udenheim",null],["073315006065","Vendersheim",null],["073315006068","Wallertheim",null],["073315006073","Wörrstadt, Stadt",null],["073315007006","Bechtheim",null],["073315007009","Bermersheim",null],["073315007011","Hochborn",null],["073315007015","Dittelsheim-Heßloch",null],["073315007028","Frettenheim",null],["073315007036","Gundersheim",null],["073315007037","Gundheim",null],["073315007039","Hangen-Weisheim",null],["073315007049","Monzernheim",null],["073315007055","Osthofen, Stadt",null],["073315007071","Westhofen",null],["073320002002","Bad Dürkheim, Stadt",null],["073320024024","Grünstadt, Stadt",null],["073320025025","Haßloch",null],["073325001009","Deidesheim, Stadt",null],["073325001017","Forst an der Weinstraße",null],["073325001035","Meckenheim",null],["073325001039","Niederkirchen bei Deidesheim",null],["073325001043","Ruppertsberg",null],["073325002005","Bobenheim am Berg",null],["073325002008","Dackenheim",null],["073325002015","Erpolzheim",null],["073325002019","Freinsheim, Stadt",null],["073325002026","Herxheim am Berg",null],["073325002028","Kallstadt",null],["073325002049","Weisenheim am Berg",null],["073325002050","Weisenheim am Sand",null],["073325005014","Elmstein",null],["073325005016","Esthal",null],["073325005018","Frankeneck",null],["073325005032","Lambrecht (Pfalz), Stadt",null],["073325005034","Lindenberg",null],["073325005037","Neidenfels",null],["073325005048","Weidenthal",null],["073325006013","Ellerstadt",null],["073325006020","Friedelsheim",null],["073325006022","Gönnheim",null],["073325006046","Wachenheim an der Weinstraße, Stadt",null],["073325007001","Altleiningen",null],["073325007003","Battenberg (Pfalz)",null],["073325007004","Bissersheim",null],["073325007006","Bockenheim an der Weinstraße",null],["073325007007","Carlsberg",null],["073325007010","Dirmstein",null],["073325007012","Ebertsheim",null],["073325007021","Gerolsheim",null],["073325007023","Großkarlbach",null],["073325007027","Hettenleidelheim",null],["073325007029","Kindenheim",null],["073325007030","Kirchheim an der Weinstraße",null],["073325007031","Kleinkarlbach",null],["073325007033","Laumersheim",null],["073325007036","Mertesheim",null],["073325007038","Neuleiningen",null],["073325007040","Obersülzen",null],["073325007041","Obrigheim (Pfalz)",null],["073325007042","Quirnheim",null],["073325007044","Tiefenthal",null],["073325007047","Wattenheim",null],["073335002019","Eisenberg (Pfalz), Stadt",null],["073335002038","Kerzenheim",null],["073335002060","Ramsen",null],["073335003001","Albisheim (Pfrimm)",null],["073335003006","Biedesheim",null],["073335003012","Bubenheim",null],["073335003017","Dreisen",null],["073335003018","Einselthum",null],["073335003026","Göllheim",null],["073335003032","Immesheim",null],["073335003041","Lautersheim",null],["073335003058","Ottersheim",null],["073335003064","Rüssingen",null],["073335003074","Standenbühl",null],["073335003081","Weitersweiler",null],["073335003501","Zellertal",null],["073335004005","Bennhausen",null],["073335004007","Bischheim",null],["073335004010","Bolanden",null],["073335004013","Dannenfels",null],["073335004022","Gauersheim",null],["073335004031","Ilbesheim",null],["073335004035","Jakobsweiler",null],["073335004039","Kirchheimbolanden, Stadt",null],["073335004040","Kriegsfeld",null],["073335004045","Marnheim",null],["073335004046","Mörsfeld",null],["073335004047","Morschheim",null],["073335004056","Oberwiesen",null],["073335004057","Orbis",null],["073335004062","Rittersheim",null],["073335004076","Stetten",null],["073335006009","Börrstadt",null],["073335006011","Breunigweiler",null],["073335006020","Falkenstein",null],["073335006027","Gonbach",null],["073335006030","Höringen",null],["073335006033","Imsbach",null],["073335006042","Lohnsfeld",null],["073335006048","Münchweiler an der Alsenz",null],["073335006069","Schweisweiler",null],["073335006071","Sippersfeld",null],["073335006075","Steinbach am Donnersberg",null],["073335006080","Wartenberg-Rohrbach",null],["073335006503","Winnweiler",null],["073335007003","Alsenz",null],["073335007004","Bayerfeld-Steckweiler",null],["073335007008","Bisterschied",null],["073335007014","Dielkirchen",null],["073335007016","Dörrmoschel",null],["073335007021","Finkenbach-Gersweiler",null],["073335007023","Gaugrehweiler",null],["073335007024","Gehrweiler",null],["073335007025","Gerbach",null],["073335007028","Gundersweiler",null],["073335007034","Imsweiler",null],["073335007036","Kalkofen",null],["073335007037","Katzenbach",null],["073335007043","Mannweiler-Cölln",null],["073335007049","Münsterappel",null],["073335007050","Niederhausen an der Appel",null],["073335007051","Niedermoschel",null],["073335007053","Oberhausen an der Appel",null],["073335007054","Obermoschel, Stadt",null],["073335007055","Oberndorf",null],["073335007061","Ransweiler",null],["073335007065","Ruppertsecken",null],["073335007066","Sankt Alban",null],["073335007067","Schiersfeld",null],["073335007068","Schönborn",null],["073335007072","Sitters",null],["073335007073","Stahlberg",null],["073335007077","Teschenmoschel",null],["073335007078","Unkenbach",null],["073335007079","Waldgrehweiler",null],["073335007083","Winterborn",null],["073335007084","Würzweiler",null],["073335007201","Rathskirchen",null],["073335007202","Reichsthal",null],["073335007203","Seelen",null],["073335007502","Rockenhausen, Stadt",null],["073340007007","Germersheim, Stadt",null],["073340501501","Wörth am Rhein, Stadt",null],["073345001001","Bellheim",null],["073345001014","Knittelsheim",null],["073345001023","Ottersheim bei Landau",null],["073345001036","Zeiskam",null],["073345002002","Berg (Pfalz)",null],["073345002008","Hagenbach, Stadt",null],["073345002021","Neuburg am Rhein",null],["073345002027","Scheibenhardt",null],["073345003009","Hatzenbühl",null],["073345003012","Jockgrim",null],["073345003022","Neupotz",null],["073345003024","Rheinzabern",null],["073345004004","Erlenbach bei Kandel",null],["073345004005","Freckenfeld",null],["073345004013","Kandel, Stadt",null],["073345004020","Minfeld",null],["073345004030","Steinweiler",null],["073345004031","Vollmersweiler",null],["073345004034","Winden",null],["073345005006","Freisbach",null],["073345005017","Lingenfeld",null],["073345005018","Lustadt",null],["073345005028","Schwegenheim",null],["073345005032","Weingarten (Pfalz)",null],["073345005033","Westheim (Pfalz)",null],["073345006011","Hördt",null],["073345006015","Kuhardt",null],["073345006016","Leimersheim",null],["073345006025","Rülzheim",null],["073355001003","Bruchmühlbach-Miesau",null],["073355001011","Gerhardsbrunn",null],["073355001201","Lambsborn",null],["073355001202","Langwieden",null],["073355001203","Martinshöhe",null],["073355002004","Enkenbach-Alsenborn",null],["073355002007","Fischbach",null],["073355002010","Frankenstein",null],["073355002015","Hochspeyer",null],["073355002026","Mehlingen",null],["073355002028","Neuhemsbach",null],["073355002048","Waldleiningen",null],["073355002205","Sembach",null],["073355008016","Hütschenhausen",null],["073355008020","Kottweiler-Schwanden",null],["073355008030","Niedermohr",null],["073355008038","Ramstein-Miesenbach, Stadt",null],["073355008044","Steinwenden",null],["073355009005","Erzenhausen",null],["073355009006","Eulenbis",null],["073355009019","Kollweiler",null],["073355009024","Mackenbach",null],["073355009040","Rodenbach",null],["073355009043","Schwedelbach",null],["073355009049","Weilerbach",null],["073355009501","Reichenbach-Steegen",null],["073355010009","Frankelbach",null],["073355010013","Heiligenmoschel",null],["073355010014","Hirschhorn/ Pfalz",null],["073355010017","Katzweiler",null],["073355010025","Mehlbach",null],["073355010029","Niederkirchen",null],["073355010033","Olsbrücken",null],["073355010034","Otterbach",null],["073355010035","Otterberg, Stadt",null],["073355010041","Schallodenbach",null],["073355010042","Schneckenhausen",null],["073355010046","Sulzbachtal",null],["073355011002","Bann",null],["073355011012","Hauptstuhl",null],["073355011018","Kindsbach",null],["073355011021","Krickenbach",null],["073355011022","Landstuhl, Sickingenstadt, Stadt",null],["073355011023","Linden",null],["073355011027","Mittelbrunn",null],["073355011031","Oberarnbach",null],["073355011037","Queidersbach",null],["073355011045","Stelzenberg",null],["073355011047","Trippstadt",null],["073355011204","Schopp",null],["073365008001","Adenbach",null],["073365008005","Aschbach",null],["073365008012","Buborn",null],["073365008013","Cronenberg",null],["073365008014","Deimberg",null],["073365008019","Einöllen",null],["073365008023","Eßweiler",null],["073365008029","Ginsweiler",null],["073365008030","Glanbrücken",null],["073365008033","Grumbach",null],["073365008035","Hausweiler",null],["073365008036","Hefersweiler",null],["073365008038","Heinzenhausen",null],["073365008040","Herren-Sulzbach",null],["073365008042","Hinzweiler",null],["073365008043","Hohenöllen",null],["073365008044","Homberg",null],["073365008045","Hoppstädten",null],["073365008048","Jettenbach",null],["073365008049","Kappeln",null],["073365008050","Kirrweiler",null],["073365008053","Kreimbach-Kaulbach",null],["073365008057","Langweiler",null],["073365008058","Lauterecken, Stadt",null],["073365008060","Lohnweiler",null],["073365008061","Medard",null],["073365008062","Merzweiler",null],["073365008065","Nerzweiler",null],["073365008069","Nußbach",null],["073365008072","Oberweiler im Tal",null],["073365008073","Oberweiler-Tiefenbach",null],["073365008074","Odenbach",null],["073365008075","Offenbach-Hundheim",null],["073365008085","Reipoltskirchen",null],["073365008086","Relsberg",null],["073365008087","Rothselberg",null],["073365008090","Rutsweiler an der Lauter",null],["073365008095","Sankt Julian",null],["073365008100","Unterjeckenbach",null],["073365008104","Wiesweiler",null],["073365008105","Wolfstein, Stadt",null],["073365009004","Altenkirchen",null],["073365009008","Börsborn",null],["073365009010","Breitenbach",null],["073365009011","Brücken (Pfalz)",null],["073365009016","Dittweiler",null],["073365009017","Dunzweiler",null],["073365009027","Frohnhofen",null],["073365009031","Glan-Münchweiler",null],["073365009032","Gries",null],["073365009037","Henschtal",null],["073365009041","Herschweiler-Pettersheim",null],["073365009047","Hüffler",null],["073365009054","Krottelbach",null],["073365009056","Langenbach",null],["073365009064","Nanzdietschweiler",null],["073365009076","Ohmbach",null],["073365009082","Rehweiler",null],["073365009092","Schönenberg-Kübelberg",null],["073365009096","Steinbach am Glan",null],["073365009101","Wahnwegen",null],["073365009102","Waldmohr, Stadt",null],["073365009107","Matzenbach",null],["073365009501","Quirnbach/ Pfalz",null],["073365010002","Albessen",null],["073365010003","Altenglan",null],["073365010006","Blaubach",null],["073365010009","Bosenbach",null],["073365010015","Dennweiler-Frohnbach",null],["073365010018","Ehweiler",null],["073365010021","Elzweiler",null],["073365010022","Erdesbach",null],["073365010024","Etschberg",null],["073365010025","Föckelberg",null],["073365010034","Haschbach am Remigiusberg",null],["073365010039","Herchweiler",null],["073365010046","Horschbach",null],["073365010051","Körborn",null],["073365010052","Konken",null],["073365010055","Kusel, Stadt",null],["073365010066","Neunkirchen am Potzberg",null],["073365010067","Niederalben",null],["073365010068","Niederstaufenbach",null],["073365010070","Oberalben",null],["073365010071","Oberstaufenbach",null],["073365010077","Pfeffelbach",null],["073365010079","Rammelsbach",null],["073365010081","Rathsweiler",null],["073365010084","Reichweiler",null],["073365010088","Ruthweiler",null],["073365010089","Rutsweiler am Glan",null],["073365010091","Schellweiler",null],["073365010094","Selchenbach",null],["073365010097","Thallichtenberg",null],["073365010098","Theisbergstegen",null],["073365010099","Ulmet",null],["073365010103","Welchweiler",null],["073365010106","Bedesbach",null],["073375001001","Albersweiler",null],["073375001017","Dernbach",null],["073375001024","Eußerthal",null],["073375001033","Gossersweiler-Stein",null],["073375001054","Münchweiler am Klingbach",null],["073375001064","Ramberg",null],["073375001067","Rinnthal",null],["073375001074","Silz",null],["073375001078","Völkersweiler",null],["073375001080","Waldhambach",null],["073375001081","Waldrohrbach",null],["073375001083","Wernersberg",null],["073375001501","Annweiler am Trifels, Stadt",null],["073375002005","Bad Bergzabern, Stadt",null],["073375002006","Barbelroth",null],["073375002008","Birkenhördt",null],["073375002013","Böllenborn",null],["073375002018","Dierbach",null],["073375002019","Dörrenbach",null],["073375002029","Gleiszellen-Gleishorbach",null],["073375002037","Hergersweiler",null],["073375002045","Kapellen-Drusweiler",null],["073375002046","Kapsweyer",null],["073375002049","Klingenmünster",null],["073375002055","Niederhorbach",null],["073375002056","Niederotterbach",null],["073375002058","Oberhausen",null],["073375002059","Oberotterbach",null],["073375002060","Oberschlettenbach",null],["073375002062","Pleisweiler-Oberhofen",null],["073375002071","Schweigen-Rechtenbach",null],["073375002072","Schweighofen",null],["073375002076","Steinfeld",null],["073375002079","Vorderweidenthal",null],["073375003002","Altdorf",null],["073375003011","Böbingen",null],["073375003015","Burrweiler",null],["073375003020","Edenkoben, Stadt",null],["073375003021","Edesheim",null],["073375003025","Flemlingen",null],["073375003027","Freimersheim (Pfalz)",null],["073375003028","Gleisweiler",null],["073375003032","Gommersheim",null],["073375003035","Großfischlingen",null],["073375003036","Hainfeld",null],["073375003048","Kleinfischlingen",null],["073375003066","Rhodt unter Rietburg",null],["073375003069","Roschbach",null],["073375003077","Venningen",null],["073375003084","Weyher in der Pfalz",null],["073375004038","Herxheim bei Landau/ Pfalz",null],["073375004039","Herxheimweyher",null],["073375004044","Insheim",null],["073375004068","Rohrbach",null],["073375005007","Billigheim-Ingenheim",null],["073375005009","Birkweiler",null],["073375005012","Böchingen",null],["073375005022","Eschbach",null],["073375005026","Frankweiler",null],["073375005031","Göcklingen",null],["073375005040","Heuchelheim-Klingen",null],["073375005042","Ilbesheim bei Landau in der Pfalz",null],["073375005043","Impflingen",null],["073375005050","Knöringen",null],["073375005051","Leinsweiler",null],["073375005065","Ranschbach",null],["073375005073","Siebeldingen",null],["073375005082","Walsheim",null],["073375006047","Kirrweiler (Pfalz)",null],["073375006052","Maikammer",null],["073375006070","Sankt Martin",null],["073375007014","Bornheim",null],["073375007023","Essingen",null],["073375007041","Hochstadt (Pfalz)",null],["073375007061","Offenbach an der Queich",null],["073380004004","Bobenheim-Roxheim",null],["073380005005","Böhl-Iggelheim",null],["073380017017","Limburgerhof",null],["073380019019","Mutterstadt",null],["073380025025","Schifferstadt, Stadt",null],["073385001006","Dannstadt-Schauernheim",null],["073385001014","Hochdorf-Assenheim",null],["073385001022","Rödersheim-Gronau",null],["073385004003","Birkenheide",null],["073385004008","Fußgönheim",null],["073385004018","Maxdorf",null],["073385006002","Beindersheim",null],["073385006009","Großniedesheim",null],["073385006012","Heßheim",null],["073385006013","Heuchelheim bei Frankenthal",null],["073385006015","Kleinniedesheim",null],["073385006016","Lambsheim",null],["073385007007","Dudenhofen",null],["073385007010","Hanhofen",null],["073385007011","Harthausen",null],["073385007023","Römerberg",null],["073385008001","Altrip",null],["073385008020","Neuhofen",null],["073385008021","Otterstadt",null],["073385008026","Waldsee",null],["073390005005","Bingen am Rhein, Stadt",null],["073390009009","Budenheim",null],["073390030030","Ingelheim am Rhein, Stadt",null],["073395001003","Bacharach, Stadt",null],["073395001007","Breitscheid",null],["073395001036","Manubach",null],["073395001038","Münster-Sarmsheim",null],["073395001040","Niederheimbach",null],["073395001044","Oberdiebach",null],["073395001045","Oberheimbach",null],["073395001058","Trechtingshausen",null],["073395001062","Waldalgesheim",null],["073395001063","Weiler bei Bingen",null],["073395002006","Bodenheim",null],["073395002020","Gau-Bischofsheim",null],["073395002026","Harxheim",null],["073395002034","Lörzweiler",null],["073395002039","Nackenheim",null],["073395003001","Appenheim",null],["073395003008","Bubenheim",null],["073395003016","Engelstadt",null],["073395003019","Gau-Algesheim, Stadt",null],["073395003041","Nieder-Hilbersheim",null],["073395003046","Ober-Hilbersheim",null],["073395003048","Ockenheim",null],["073395003051","Schwabenheim an der Selz",null],["073395006017","Essenheim",null],["073395006031","Jugenheim in Rheinhessen",null],["073395006032","Klein-Winternheim",null],["073395006042","Nieder-Olm, Stadt",null],["073395006047","Ober-Olm",null],["073395006054","Sörgenloch",null],["073395006057","Stadecken-Elsheim",null],["073395006067","Zornheim",null],["073395007010","Dalheim",null],["073395007011","Dexheim",null],["073395007012","Dienheim",null],["073395007013","Dolgesheim",null],["073395007015","Eimsheim",null],["073395007018","Friesenheim",null],["073395007024","Guntersblum",null],["073395007025","Hahnheim",null],["073395007028","Hillesheim",null],["073395007033","Köngernheim",null],["073395007035","Ludwigshöhe",null],["073395007037","Mommenheim",null],["073395007043","Nierstein, Stadt",null],["073395007049","Oppenheim, Stadt",null],["073395007053","Selzen",null],["073395007059","Uelversheim",null],["073395007060","Undenheim",null],["073395007064","Weinolsheim",null],["073395007066","Wintersheim",null],["073395007201","Dorn-Dürkheim",null],["073395008002","Aspisheim",null],["073395008004","Badenheim",null],["073395008021","Gensingen",null],["073395008022","Grolsheim",null],["073395008029","Horrweiler",null],["073395008050","Sankt Johann",null],["073395008056","Sprendlingen",null],["073395008065","Welgesheim",null],["073395008068","Zotzenheim",null],["073395008202","Wolfsheim",null],["073405001001","Bobenthal",null],["073405001002","Busenberg",null],["073405001004","Dahn, Stadt",null],["073405001009","Erfweiler",null],["073405001010","Erlenbach bei Dahn",null],["073405001011","Fischbach bei Dahn",null],["073405001021","Hirschthal",null],["073405001029","Ludwigswinkel",null],["073405001033","Niederschlettenbach",null],["073405001034","Nothweiler",null],["073405001039","Rumbach",null],["073405001043","Schindhard",null],["073405001045","Schönau (Pfalz)",null],["073405001501","Bruchweiler-Bärenbach",null],["073405001502","Bundenthal",null],["073405002005","Darstein",null],["073405002006","Dimbach",null],["073405002014","Hauenstein",null],["073405002020","Hinterweidenthal",null],["073405002030","Lug",null],["073405002047","Schwanheim",null],["073405002049","Spirkelbach",null],["073405002057","Wilgartswiesen",null],["073405003008","Eppenbrunn",null],["073405003019","Hilst",null],["073405003026","Kröppen",null],["073405003028","Lemberg",null],["073405003036","Obersimten",null],["073405003040","Ruppertsweiler",null],["073405003048","Schweix",null],["073405003052","Trulben",null],["073405003053","Vinningen",null],["073405003205","Bottenbach",null],["073405004003","Clausen",null],["073405004007","Donsieders",null],["073405004027","Leimen",null],["073405004031","Merzalben",null],["073405004032","Münchweiler an der Rodalb",null],["073405004038","Rodalben, Stadt",null],["073405006012","Geiselberg",null],["073405006015","Heltersberg",null],["073405006016","Hermersberg",null],["073405006022","Höheinöd",null],["073405006025","Horbach",null],["073405006044","Schmalenberg",null],["073405006050","Steinalben",null],["073405006054","Waldfischbach-Burgalben",null],["073405008201","Althornbach",null],["073405008202","Battweiler",null],["073405008203","Bechhofen",null],["073405008206","Contwig",null],["073405008207","Dellfeld",null],["073405008208","Dietrichingen",null],["073405008209","Großbundenbach",null],["073405008210","Großsteinhausen",null],["073405008211","Hornbach, Stadt",null],["073405008212","Käshofen",null],["073405008213","Kleinbundenbach",null],["073405008214","Kleinsteinhausen",null],["073405008218","Mauschbach",null],["073405008221","Riedelberg",null],["073405008223","Rosenkopf",null],["073405008226","Walshausen",null],["073405008227","Wiesbach",null],["073405009017","Herschberg",null],["073405009018","Hettenhausen",null],["073405009023","Höheischweiler",null],["073405009024","Höhfröschen",null],["073405009035","Nünschweiler",null],["073405009037","Petersberg",null],["073405009041","Saalstadt",null],["073405009042","Schauerberg",null],["073405009051","Thaleischweiler-Fröschen",null],["073405009055","Weselberg",null],["073405009204","Biedershausen",null],["073405009215","Knopp-Labach",null],["073405009216","Krähenberg",null],["073405009217","Maßweiler",null],["073405009219","Obernheim-Kirchenarnbach",null],["073405009220","Reifenberg",null],["073405009222","Rieschweiler-Mühlbach",null],["073405009224","Schmitshausen",null],["073405009225","Wallhalben",null],["073405009228","Winterbach (Pfalz)",null],["081110000000","Stuttgart, Landeshauptstadt",null],["081150003003","Böblingen, Stadt",null],["081150028028","Leonberg, Stadt",null],["081150029029","Magstadt",null],["081150041041","Renningen, Stadt",null],["081150042042","Rutesheim, Stadt",null],["081150044044","Schönaich",null],["081150045045","Sindelfingen, Stadt",null],["081150050050","Weil der Stadt, Stadt",null],["081150051051","Weil im Schönbuch",null],["081150052052","Weissach",null],["081155001001","Aidlingen",null],["081155001054","Grafenau",null],["081155002013","Ehningen",null],["081155002015","Gärtringen",null],["081155003010","Deckenpfronn",null],["081155003021","Herrenberg, Stadt",null],["081155003037","Nufringen",null],["081155004002","Altdorf",null],["081155004022","Hildrizhausen",null],["081155004024","Holzgerlingen, Stadt",null],["081155005004","Bondorf",null],["081155005016","Gäufelden",null],["081155005034","Mötzingen",null],["081155005053","Jettingen",null],["081155006046","Steinenbronn",null],["081155006048","Waldenbuch, Stadt",null],["081160015015","Denkendorf",null],["081160019019","Esslingen am Neckar, Stadt",null],["081160047047","Neuhausen auf den Fildern",null],["081160072072","Wernau (Neckar), Stadt",null],["081160076076","Aichwald",null],["081160077077","Filderstadt, Stadt",null],["081160078078","Leinfelden-Echterdingen, Stadt",null],["081160080080","Ostfildern, Stadt",null],["081160081081","Aichtal, Stadt",null],["081165001016","Dettingen unter Teck",null],["081165001033","Kirchheim unter Teck, Stadt",null],["081165001048","Notzingen",null],["081165002018","Erkenbrechtsweiler",null],["081165002054","Owen, Stadt",null],["081165002079","Lenningen",null],["081165003005","Altdorf",null],["081165003006","Altenriet",null],["081165003008","Bempflingen",null],["081165003041","Neckartailfingen",null],["081165003042","Neckartenzlingen",null],["081165003063","Schlaitdorf",null],["081165004011","Beuren",null],["081165004036","Kohlberg",null],["081165004046","Neuffen, Stadt",null],["081165005020","Frickenhausen",null],["081165005022","Großbettlingen",null],["081165005049","Nürtingen, Stadt",null],["081165005050","Oberboihingen",null],["081165005068","Unterensingen",null],["081165005073","Wolfschlugen",null],["081165006004","Altbach",null],["081165006014","Deizisau",null],["081165006056","Plochingen, Stadt",null],["081165007007","Baltmannsweiler",null],["081165007027","Hochdorf",null],["081165007037","Lichtenwald",null],["081165007058","Reichenbach an der Fils",null],["081165008012","Bissingen an der Teck",null],["081165008029","Holzmaden",null],["081165008043","Neidlingen",null],["081165008053","Ohmden",null],["081165008070","Weilheim an der Teck, Stadt",null],["081165009035","Köngen",null],["081165009071","Wendlingen am Neckar, Stadt",null],["081170010010","Böhmenkirch",null],["081175001006","Bad Ditzenbach",null],["081175001014","Deggingen",null],["081175002018","Ebersbach an der Fils, Stadt",null],["081175002044","Schlierbach",null],["081175003019","Eislingen/Fils, Stadt",null],["081175003037","Ottenbach",null],["081175003042","Salach",null],["081175004007","Bad Überkingen",null],["081175004024","Geislingen an der Steige, Stadt",null],["081175004033","Kuchen",null],["081175005026","Göppingen, Stadt",null],["081175005043","Schlat",null],["081175005053","Wäschenbeuren",null],["081175005055","Wangen",null],["081175006015","Donzdorf, Stadt",null],["081175006025","Gingen an der Fils",null],["081175006049","Süßen, Stadt",null],["081175006061","Lauterstein, Stadt",null],["081175007016","Drackenstein",null],["081175007028","Gruibingen",null],["081175007031","Hohenstadt",null],["081175007035","Mühlhausen im Täle",null],["081175007058","Wiesensteig, Stadt",null],["081175008001","Adelberg",null],["081175008009","Birenbach",null],["081175008011","Börtlingen",null],["081175008038","Rechberghausen",null],["081175009002","Aichelberg",null],["081175009012","Bad Boll",null],["081175009017","Dürnau",null],["081175009023","Gammelshausen",null],["081175009029","Hattenhofen",null],["081175009060","Zell unter Aichelberg",null],["081175010003","Albershausen",null],["081175010051","Uhingen, Stadt",null],["081175011020","Eschenbach",null],["081175011030","Heiningen",null],["081180003003","Asperg, Stadt",null],["081180011011","Ditzingen, Stadt",null],["081180019019","Gerlingen, Stadt",null],["081180021021","Großbottwar, Stadt",null],["081180046046","Kornwestheim, Stadt",null],["081180048048","Ludwigsburg, Stadt",null],["081180050050","Markgröningen, Stadt",null],["081180051051","Möglingen",null],["081180060060","Oberstenfeld",null],["081180076076","Sachsenheim, Stadt",null],["081180080080","Korntal-Münchingen, Stadt",null],["081180081081","Remseck am Neckar, Stadt",null],["081185001007","Besigheim, Stadt",null],["081185001016","Freudental",null],["081185001018","Gemmrigheim",null],["081185001028","Hessigheim",null],["081185001047","Löchgau",null],["081185001053","Mundelsheim",null],["081185001074","Walheim",null],["081185002071","Tamm",null],["081185002077","Ingersheim",null],["081185002079","Bietigheim-Bissingen, Stadt",null],["081185003010","Bönnigheim, Stadt",null],["081185003015","Erligheim",null],["081185003040","Kirchheim am Neckar",null],["081185004063","Pleidelsheim",null],["081185004078","Freiberg am Neckar, Stadt",null],["081185005001","Affalterbach",null],["081185005006","Benningen am Neckar",null],["081185005014","Erdmannhausen",null],["081185005049","Marbach am Neckar, Stadt",null],["081185006027","Hemmingen",null],["081185006067","Schwieberdingen",null],["081185007054","Murr",null],["081185007070","Steinheim an der Murr, Stadt",null],["081185008012","Eberdingen",null],["081185008059","Oberriexingen, Stadt",null],["081185008068","Sersheim",null],["081185008073","Vaihingen an der Enz, Stadt",null],["081190001001","Alfdorf",null],["081190020020","Fellbach, Stadt",null],["081190041041","Korb",null],["081190044044","Murrhardt, Stadt",null],["081190061061","Rudersberg",null],["081190079079","Waiblingen, Stadt",null],["081190089089","Berglen",null],["081190090090","Remshalden",null],["081190091091","Weinstadt, Stadt",null],["081190093093","Kernen im Remstal",null],["081195001003","Allmersbach im Tal",null],["081195001004","Althütte",null],["081195001006","Auenwald",null],["081195001008","Backnang, Stadt",null],["081195001018","Burgstetten",null],["081195001038","Kirchberg an der Murr",null],["081195001053","Oppenweiler",null],["081195001083","Weissach im Tal",null],["081195001087","Aspach",null],["081195002055","Plüderhausen",null],["081195002076","Urbach",null],["081195003067","Schorndorf, Stadt",null],["081195003086","Winterbach",null],["081195004024","Großerlach",null],["081195004069","Spiegelberg",null],["081195004075","Sulzbach an der Murr",null],["081195005037","Kaisersbach",null],["081195005084","Welzheim, Stadt",null],["081195006042","Leutenbach",null],["081195006068","Schwaikheim",null],["081195006085","Winnenden, Stadt",null],["081210000000","Heilbronn, Universitätsstadt",null],["081250007007","Bad Wimpfen, Stadt",null],["081250039039","Gundelsheim, Stadt",null],["081250058058","Leingarten, Stadt",null],["081250068068","Neudenau, Stadt",null],["081250107107","Wüstenrot",null],["081255001005","Bad Friedrichshall, Stadt",null],["081255001078","Oedheim",null],["081255001079","Offenau",null],["081255002006","Bad Rappenau, Stadt",null],["081255002049","Kirchardt",null],["081255002087","Siegelsbach",null],["081255003013","Brackenheim, Stadt",null],["081255003017","Cleebronn",null],["081255004026","Eppingen, Stadt",null],["081255004034","Gemmingen",null],["081255004047","Ittlingen",null],["081255005030","Flein",null],["081255005094","Talheim",null],["081255006056","Lauffen am Neckar, Stadt",null],["081255006066","Neckarwestheim",null],["081255006074","Nordheim",null],["081255007048","Jagsthausen",null],["081255007063","Möckmühl, Stadt",null],["081255007084","Roigheim",null],["081255007103","Widdern, Stadt",null],["081255008027","Erlenbach",null],["081255008065","Neckarsulm, Stadt",null],["081255008096","Untereisesheim",null],["081255009069","Neuenstadt am Kocher, Stadt",null],["081255009111","Hardthausen am Kocher",null],["081255009113","Langenbrettach",null],["081255010038","Güglingen, Stadt",null],["081255010081","Pfaffenhofen",null],["081255010108","Zaberfeld",null],["081255011059","Löwenstein, Stadt",null],["081255011110","Obersulm",null],["081255012001","Abstatt",null],["081255012008","Beilstein, Stadt",null],["081255012046","Ilsfeld",null],["081255012098","Untergruppenbach",null],["081255013061","Massenbachhausen",null],["081255013086","Schwaigern, Stadt",null],["081255014021","Eberstadt",null],["081255014024","Ellhofen",null],["081255014057","Lehrensteinsfeld",null],["081255014102","Weinsberg, Stadt",null],["081260011011","Bretzfeld",null],["081260072072","Schöntal",null],["081265001047","Kupferzell",null],["081265001058","Neuenstein, Stadt",null],["081265001085","Waldenburg, Stadt",null],["081265002020","Dörzbach",null],["081265002045","Krautheim, Stadt",null],["081265002056","Mulfingen",null],["081265003039","Ingelfingen, Stadt",null],["081265003046","Künzelsau, Stadt",null],["081265004028","Forchtenberg, Stadt",null],["081265004060","Niedernhall, Stadt",null],["081265004086","Weißbach",null],["081265005066","Öhringen, Stadt",null],["081265005069","Pfedelbach",null],["081265005094","Zweiflingen",null],["081270008008","Blaufelden",null],["081270052052","Mainhardt",null],["081270075075","Schrozberg, Stadt",null],["081275001009","Braunsbach",null],["081275001086","Untermünkheim",null],["081275002014","Crailsheim, Stadt",null],["081275002073","Satteldorf",null],["081275002103","Frankenhardt",null],["081275002104","Stimpfach",null],["081275003101","Kreßberg",null],["081275003102","Fichtenau",null],["081275004032","Gerabronn, Stadt",null],["081275004047","Langenburg, Stadt",null],["081275005043","Ilshofen, Stadt",null],["081275005089","Vellberg, Stadt",null],["081275005099","Wolpertshausen",null],["081275006023","Fichtenberg",null],["081275006025","Gaildorf, Stadt",null],["081275006062","Oberrot",null],["081275006079","Sulzbach-Laufen",null],["081275007012","Bühlertann",null],["081275007013","Bühlerzell",null],["081275007063","Obersontheim",null],["081275008046","Kirchberg an der Jagst, Stadt",null],["081275008071","Rot am See",null],["081275008091","Wallhausen",null],["081275009056","Michelbach an der Bilz",null],["081275009059","Michelfeld",null],["081275009076","Schwäbisch Hall, Stadt",null],["081275009100","Rosengarten",null],["081280020020","Creglingen, Stadt",null],["081280039039","Freudenberg, Stadt",null],["081280064064","Külsheim, Stadt",null],["081280082082","Niederstetten, Stadt",null],["081280126126","Weikersheim, Stadt",null],["081280131131","Wertheim, Stadt",null],["081280139139","Lauda-Königshofen, Stadt",null],["081285001006","Assamstadt",null],["081285001007","Bad Mergentheim, Stadt",null],["081285001058","Igersheim",null],["081285002014","Boxberg, Stadt",null],["081285002138","Ahorn",null],["081285003047","Grünsfeld, Stadt",null],["081285003137","Wittighausen",null],["081285004045","Großrinderfeld",null],["081285004061","Königheim",null],["081285004115","Tauberbischofsheim, Stadt",null],["081285004128","Werbach",null],["081350010010","Dischingen",null],["081350015015","Gerstetten",null],["081350020020","Herbrechtingen, Stadt",null],["081350025025","Königsbronn",null],["081350032032","Steinheim am Albuch",null],["081355001016","Giengen an der Brenz, Stadt",null],["081355001021","Hermaringen",null],["081355002019","Heidenheim an der Brenz, Stadt",null],["081355002026","Nattheim",null],["081355003027","Niederstotzingen, Stadt",null],["081355003031","Sontheim an der Brenz",null],["081360002002","Abtsgmünd",null],["081360027027","Gschwend",null],["081360042042","Lorch, Stadt",null],["081360045045","Neresheim, Stadt",null],["081360050050","Oberkochen, Stadt",null],["081365001021","Essingen",null],["081365001033","Hüttlingen",null],["081365001088","Aalen, Stadt",null],["081365002010","Bopfingen, Stadt",null],["081365002037","Kirchheim am Ries",null],["081365002087","Riesbürg",null],["081365003003","Adelmannsfelden",null],["081365003018","Ellenberg",null],["081365003019","Ellwangen (Jagst), Stadt",null],["081365003035","Jagstzell",null],["081365003046","Neuler",null],["081365003060","Rosenberg",null],["081365003084","Wört",null],["081365003089","Rainau",null],["081365004038","Lauchheim, Stadt",null],["081365004082","Westhausen",null],["081365005020","Eschach",null],["081365005024","Göggingen",null],["081365005034","Iggingen",null],["081365005040","Leinzell",null],["081365005049","Obergröningen",null],["081365005062","Schechingen",null],["081365006007","Bartholomä",null],["081365006009","Böbingen an der Rems",null],["081365006028","Heubach, Stadt",null],["081365006029","Heuchlingen",null],["081365006043","Mögglingen",null],["081365007065","Schwäbisch Gmünd, Stadt",null],["081365007079","Waldstetten",null],["081365008015","Durlangen",null],["081365008044","Mutlangen",null],["081365008061","Ruppertshofen",null],["081365008066","Spraitbach",null],["081365008070","Täferrot",null],["081365009068","Stödtlen",null],["081365009071","Tannhausen",null],["081365009075","Unterschneidheim",null],["082110000000","Baden-Baden, Stadt",null],["082120000000","Karlsruhe, Stadt",null],["082150017017","Ettlingen, Stadt",null],["082150046046","Malsch",null],["082150047047","Marxzell",null],["082150064064","Östringen, Stadt",null],["082150084084","Ubstadt-Weiher",null],["082150089089","Walzbachtal",null],["082150090090","Weingarten (Baden)",null],["082150096096","Karlsbad",null],["082150097097","Kraichtal, Stadt",null],["082150101101","Pfinztal",null],["082150102102","Eggenstein-Leopoldshafen",null],["082150105105","Linkenheim-Hochstetten",null],["082150106106","Waghäusel, Stadt",null],["082150108108","Rheinstetten, Stadt",null],["082150109109","Stutensee, Stadt",null],["082150110110","Waldbronn",null],["082155001039","Kronau",null],["082155001100","Bad Schönborn",null],["082155002007","Bretten, Stadt",null],["082155002025","Gondelsheim",null],["082155003009","Bruchsal, Stadt",null],["082155003021","Forst",null],["082155003029","Hambrücken",null],["082155003103","Karlsdorf-Neuthard",null],["082155004099","Graben-Neudorf",null],["082155004111","Dettenheim",null],["082155005040","Kürnbach",null],["082155005059","Oberderdingen",null],["082155006066","Philippsburg, Stadt",null],["082155006107","Oberhausen-Rheinhausen",null],["082155007082","Sulzfeld",null],["082155007094","Zaisenhausen",null],["082160008008","Bühlertal",null],["082160013013","Forbach",null],["082160015015","Gaggenau, Stadt",null],["082165001006","Bischweier",null],["082165001024","Kuppenheim, Stadt",null],["082165002007","Bühl, Stadt",null],["082165002041","Ottersweier",null],["082165003002","Au am Rhein",null],["082165003005","Bietigheim",null],["082165003009","Durmersheim",null],["082165003012","Elchesheim-Illingen",null],["082165004017","Gernsbach, Stadt",null],["082165004029","Loffenau",null],["082165004059","Weisenbach",null],["082165005023","Iffezheim",null],["082165005033","Muggensturm",null],["082165005039","Ötigheim",null],["082165005043","Rastatt, Stadt",null],["082165005052","Steinmauern",null],["082165006028","Lichtenau, Stadt",null],["082165006063","Rheinmünster",null],["082165007022","Hügelsheim",null],["082165007049","Sinzheim",null],["082210000000","Heidelberg, Stadt",null],["082220000000","Mannheim, Universitätsstadt",null],["082250014014","Buchen (Odenwald), Stadt",null],["082250060060","Mudau",null],["082255001032","Hardheim",null],["082255001039","Höpfingen",null],["082255001109","Walldürn, Stadt",null],["082255002033","Haßmersheim",null],["082255002042","Hüffenhardt",null],["082255003002","Aglasterhausen",null],["082255003068","Neunkirchen",null],["082255003116","Schwarzach",null],["082255004024","Fahrenbach",null],["082255004052","Limbach",null],["082255005058","Mosbach, Stadt",null],["082255005067","Neckarzimmern",null],["082255005074","Obrigheim",null],["082255005117","Elztal",null],["082255006010","Binau",null],["082255006064","Neckargerach",null],["082255006113","Zwingenberg",null],["082255006118","Waldbrunn",null],["082255007075","Osterburken, Stadt",null],["082255007082","Rosenberg",null],["082255007114","Ravenstein, Stadt",null],["082255008009","Billigheim",null],["082255008115","Schefflenz",null],["082255009001","Adelsheim, Stadt",null],["082255009091","Seckach",null],["082260009009","Brühl",null],["082260012012","Dossenheim",null],["082260018018","Eppelheim, Stadt",null],["082260028028","Heddesheim",null],["082260036036","Ilvesheim",null],["082260037037","Ketsch",null],["082260038038","Ladenburg, Stadt",null],["082260041041","Leimen, Stadt",null],["082260060060","Nußloch",null],["082260062062","Oftersheim",null],["082260063063","Plankstadt",null],["082260076076","Sandhausen",null],["082260082082","Schriesheim, Stadt",null],["082260084084","Schwetzingen, Stadt",null],["082260095095","Walldorf, Stadt",null],["082260096096","Weinheim, Stadt",null],["082260103103","St. Leon-Rot",null],["082260105105","Edingen-Neckarhausen",null],["082260107107","Hirschberg an der Bergstraße",null],["082265001013","Eberbach, Stadt",null],["082265001081","Schönbrunn",null],["082265002020","Eschelbronn",null],["082265002048","Mauer",null],["082265002049","Meckesheim",null],["082265002086","Spechbach",null],["082265002104","Lobbach",null],["082265003031","Hemsbach, Stadt",null],["082265003040","Laudenbach",null],["082265004003","Altlußheim",null],["082265004032","Hockenheim, Stadt",null],["082265004059","Neulußheim",null],["082265004068","Reilingen",null],["082265005006","Bammental",null],["082265005022","Gaiberg",null],["082265005056","Neckargemünd, Stadt",null],["082265005097","Wiesenbach",null],["082265006046","Malsch",null],["082265006054","Mühlhausen",null],["082265006065","Rauenberg, Stadt",null],["082265007027","Heddesbach",null],["082265007029","Heiligkreuzsteinach",null],["082265007080","Schönau, Stadt",null],["082265007099","Wilhelmsfeld",null],["082265008085","Sinsheim, Stadt",null],["082265008101","Zuzenhausen",null],["082265008102","Angelbachtal",null],["082265009017","Epfenbach",null],["082265009055","Neckarbischofsheim, Stadt",null],["082265009058","Neidenstein",null],["082265009066","Reichartshausen",null],["082265009091","Waibstadt, Stadt",null],["082265009106","Helmstadt-Bargen",null],["082265010010","Dielheim",null],["082265010098","Wiesloch, Stadt",null],["082310000000","Pforzheim, Stadt",null],["082350065065","Schömberg",null],["082350080080","Wildberg, Stadt",null],["082355001006","Altensteig, Stadt",null],["082355001022","Egenhausen",null],["082355001066","Simmersfeld",null],["082355002007","Althengstett",null],["082355002029","Gechingen",null],["082355002057","Ostelsheim",null],["082355002067","Simmozheim",null],["082355003018","Dobel",null],["082355003033","Bad Herrenalb, Stadt",null],["082355004008","Bad Liebenzell, Stadt",null],["082355004073","Unterreichenbach",null],["082355005047","Neubulach, Stadt",null],["082355005050","Neuweiler",null],["082355005084","Bad Teinach-Zavelstein, Stadt",null],["082355006055","Oberreichenbach",null],["082355006085","Calw, Stadt",null],["082355007020","Ebhausen",null],["082355007032","Haiterbach, Stadt",null],["082355007046","Nagold, Stadt",null],["082355007060","Rohrdorf",null],["082355008025","Enzklösterle",null],["082355008035","Höfen an der Enz",null],["082355008079","Bad Wildbad, Stadt",null],["082360004004","Birkenfeld",null],["082360028028","Illingen",null],["082360030030","Ispringen",null],["082360033033","Knittlingen, Stadt",null],["082360046046","Niefern-Öschelbronn",null],["082360070070","Keltern",null],["082360071071","Remchingen",null],["082360072072","Straubenhardt",null],["082365001019","Friolzheim",null],["082365001025","Heimsheim, Stadt",null],["082365001039","Mönsheim",null],["082365001065","Wiernsheim",null],["082365001067","Wimsheim",null],["082365001068","Wurmberg",null],["082365002011","Eisingen",null],["082365002074","Kämpfelbach",null],["082365002076","Königsbach-Stein",null],["082365003038","Maulbronn, Stadt",null],["082365003061","Sternenfels",null],["082365004040","Mühlacker, Stadt",null],["082365004050","Ötisheim",null],["082365005013","Engelsbrand",null],["082365005043","Neuenbürg, Stadt",null],["082365006031","Kieselbronn",null],["082365006073","Neulingen",null],["082365006075","Ölbronn-Dürrn",null],["082365007044","Neuhausen",null],["082365007062","Tiefenbronn",null],["082370002002","Alpirsbach, Stadt",null],["082370004004","Baiersbronn",null],["082370045045","Loßburg",null],["082375001019","Dornstetten, Stadt",null],["082375001030","Glatten",null],["082375001061","Schopfloch",null],["082375001074","Waldachtal",null],["082375002028","Freudenstadt, Stadt",null],["082375002073","Seewald",null],["082375002075","Bad Rippoldsau-Schapbach",null],["082375003024","Empfingen",null],["082375003027","Eutingen im Gäu",null],["082375003040","Horb am Neckar, Stadt",null],["082375005032","Grömbach",null],["082375005054","Pfalzgrafenweiler",null],["082375005072","Wörnersberg",null],["083110000000","Freiburg im Breisgau, Stadt",null],["083150068068","Lenzkirch",null],["083150076076","Neuenburg am Rhein, Stadt",null],["083150133133","Vogtsburg im Kaiserstuhl, Stadt",null],["083155001006","Bad Krozingen, Stadt",null],["083155001048","Hartheim am Rhein",null],["083155002015","Breisach am Rhein, Stadt",null],["083155002059","Ihringen",null],["083155002072","Merdingen",null],["083155003020","Buchenbach",null],["083155003064","Kirchzarten",null],["083155003084","Oberried",null],["083155003109","Stegen",null],["083155004014","Bollschweil",null],["083155004131","Ehrenkirchen",null],["083155005047","Gundelfingen",null],["083155005051","Heuweiler",null],["083155006008","Ballrechten-Dottingen",null],["083155006033","Eschbach",null],["083155006050","Heitersheim, Stadt",null],["083155007003","Au",null],["083155007056","Horben",null],["083155007073","Merzhausen",null],["083155007107","Sölden",null],["083155007125","Wittnau",null],["083155008016","Breitnau",null],["083155008052","Hinterzarten",null],["083155009013","Bötzingen",null],["083155009030","Eichstetten am Kaiserstuhl",null],["083155009043","Gottenheim",null],["083155010039","Friedenweiler",null],["083155010070","Löffingen, Stadt",null],["083155011115","Umkirch",null],["083155011132","March",null],["083155012004","Auggen",null],["083155012007","Badenweiler",null],["083155012022","Buggingen",null],["083155012074","Müllheim, Stadt",null],["083155012111","Sulzburg, Stadt",null],["083155013041","Glottertal",null],["083155013094","St. Märgen",null],["083155013095","St. Peter",null],["083155014028","Ebringen",null],["083155014089","Pfaffenweiler",null],["083155014098","Schallstadt",null],["083155015037","Feldberg (Schwarzwald)",null],["083155015102","Schluchsee",null],["083155016108","Staufen im Breisgau, Stadt",null],["083155016130","Münstertal/Schwarzwald",null],["083155017031","Eisenbach (Hochschwarzwald)",null],["083155017113","Titisee-Neustadt, Stadt",null],["083165001009","Denzlingen",null],["083165001036","Reute",null],["083165001045","Vörstetten",null],["083165002003","Biederbach",null],["083165002010","Elzach, Stadt",null],["083165002055","Winden im Elztal",null],["083165003011","Emmendingen, Stadt",null],["083165003024","Malterdingen",null],["083165003039","Sexau",null],["083165003043","Teningen",null],["083165003054","Freiamt",null],["083165004017","Herbolzheim, Stadt",null],["083165004020","Kenzingen, Stadt",null],["083165004049","Weisweil",null],["083165004053","Rheinhausen",null],["083165005002","Bahlingen am Kaiserstuhl",null],["083165005012","Endingen am Kaiserstuhl, Stadt",null],["083165005013","Forchheim",null],["083165005037","Riegel am Kaiserstuhl",null],["083165005038","Sasbach am Kaiserstuhl",null],["083165005051","Wyhl am Kaiserstuhl",null],["083165006014","Gutach im Breisgau",null],["083165006042","Simonswald",null],["083165006056","Waldkirch, Stadt",null],["083170005005","Appenweier",null],["083170031031","Friesenheim",null],["083170051051","Hornberg, Stadt",null],["083170057057","Kehl, Stadt",null],["083170141141","Willstätt",null],["083170151151","Neuried",null],["083170153153","Rheinau, Stadt",null],["083175001001","Achern, Stadt",null],["083175001068","Lauf",null],["083175001116","Sasbach",null],["083175001118","Sasbachwalden",null],["083175002026","Ettenheim, Stadt",null],["083175002073","Mahlberg, Stadt",null],["083175002113","Ringsheim",null],["083175002114","Rust",null],["083175002152","Kappel-Grafenhausen",null],["083175003009","Berghaupten",null],["083175003034","Gengenbach, Stadt",null],["083175003097","Ohlsbach",null],["083175004029","Fischerbach",null],["083175004040","Haslach im Kinzigtal, Stadt",null],["083175004046","Hofstetten",null],["083175004078","Mühlenbach",null],["083175004129","Steinach",null],["083175005039","Gutach (Schwarzwaldbahn)",null],["083175005041","Hausach, Stadt",null],["083175006056","Kappelrodeck",null],["083175006102","Ottenhöfen im Schwarzwald",null],["083175006126","Seebach",null],["083175007059","Kippenheim",null],["083175007065","Lahr/Schwarzwald, Stadt",null],["083175008008","Bad Peterstal-Griesbach",null],["083175008098","Oppenau, Stadt",null],["083175009067","Lautenbach",null],["083175009089","Oberkirch, Stadt",null],["083175009110","Renchen, Stadt",null],["083175010021","Durbach",null],["083175010047","Hohberg",null],["083175010096","Offenburg, Stadt",null],["083175010100","Ortenberg",null],["083175010122","Schutterwald",null],["083175011121","Schuttertal",null],["083175011127","Seelbach",null],["083175012075","Meißenheim",null],["083175012150","Schwanau",null],["083175013093","Oberwolfach",null],["083175013145","Wolfach, Stadt",null],["083175014011","Biberach",null],["083175014085","Nordrach",null],["083175014088","Oberharmersbach",null],["083175014146","Zell am Harmersbach, Stadt",null],["083179971971","Rheinau, gemeindefreies Gebiet",null],["083250012012","Dornhan, Stadt",null],["083255001014","Dunningen",null],["083255001071","Eschbronn",null],["083255002015","Epfendorf",null],["083255002045","Oberndorf am Neckar, Stadt",null],["083255002070","Fluorn-Winzeln",null],["083255003011","Dietingen",null],["083255003049","Rottweil, Stadt",null],["083255003064","Wellendingen",null],["083255003069","Zimmern ob Rottweil",null],["083255003072","Deißlingen",null],["083255004050","Schenkenzell",null],["083255004051","Schiltach, Stadt",null],["083255005001","Aichhalden",null],["083255005024","Hardt",null],["083255005036","Lauterbach",null],["083255005053","Schramberg, Stadt",null],["083255006057","Sulz am Neckar, Stadt",null],["083255006061","Vöhringen",null],["083255007009","Bösingen",null],["083255007060","Villingendorf",null],["083260003003","Bad Dürrheim, Stadt",null],["083260005005","Blumberg, Stadt",null],["083260031031","Königsfeld im Schwarzwald",null],["083260052052","St. Georgen im Schwarzwald, Stadt",null],["083260068068","Vöhrenbach, Stadt",null],["083265001006","Bräunlingen, Stadt",null],["083265001012","Donaueschingen, Stadt",null],["083265001027","Hüfingen, Stadt",null],["083265002017","Furtwangen im Schwarzwald, Stadt",null],["083265002020","Gütenbach",null],["083265003054","Schönwald im Schwarzwald",null],["083265003055","Schonach im Schwarzwald",null],["083265003060","Triberg im Schwarzwald, Stadt",null],["083265004010","Dauchingen",null],["083265004037","Mönchweiler",null],["083265004041","Niedereschach",null],["083265004061","Tuningen",null],["083265004065","Unterkirnach",null],["083265004074","Villingen-Schwenningen, Stadt",null],["083265004075","Brigachtal",null],["083275001004","Bärenthal",null],["083275001008","Buchheim",null],["083275001016","Fridingen an der Donau, Stadt",null],["083275001027","Irndorf",null],["083275001030","Kolbingen",null],["083275001036","Mühlheim an der Donau, Stadt",null],["083275001041","Renquishausen",null],["083275002007","Bubsheim",null],["083275002009","Deilingen",null],["083275002013","Egesheim",null],["083275002019","Gosheim",null],["083275002029","Königsheim",null],["083275002040","Reichenbach am Heuberg",null],["083275002051","Wehingen",null],["083275003018","Geisingen, Stadt",null],["083275003025","Immendingen",null],["083275004002","Aldingen",null],["083275004005","Balgheim",null],["083275004006","Böttingen",null],["083275004010","Denkingen",null],["083275004011","Dürbheim",null],["083275004017","Frittlingen",null],["083275004023","Hausen ob Verena",null],["083275004033","Mahlstetten",null],["083275004046","Spaichingen, Stadt",null],["083275005012","Durchhausen",null],["083275005020","Gunningen",null],["083275005048","Talheim",null],["083275005049","Trossingen, Stadt",null],["083275006038","Neuhausen ob Eck",null],["083275006050","Tuttlingen, Stadt",null],["083275006054","Wurmlingen",null],["083275006055","Seitingen-Oberflacht",null],["083275006056","Rietheim-Weilheim",null],["083275006057","Emmingen-Liptingen",null],["083350035035","Hilzingen",null],["083350063063","Radolfzell am Bodensee, Stadt",null],["083350080080","Tengen, Stadt",null],["083355001001","Aach, Stadt",null],["083355001022","Engen, Stadt",null],["083355001097","Mühlhausen-Ehingen",null],["083355002015","Büsingen am Hochrhein",null],["083355002026","Gailingen am Hochrhein",null],["083355002028","Gottmadingen",null],["083355003025","Gaienhofen",null],["083355003055","Moos",null],["083355003061","Öhningen",null],["083355004002","Allensbach",null],["083355004043","Konstanz, Universitätsstadt",null],["083355004066","Reichenau",null],["083355005075","Singen (Hohentwiel), Stadt",null],["083355005077","Steißlingen",null],["083355005081","Volkertshausen",null],["083355005100","Rielasingen-Worblingen",null],["083355006021","Eigeltingen",null],["083355006057","Mühlingen",null],["083355006079","Stockach, Stadt",null],["083355006096","Hohenfels",null],["083355006098","Bodman-Ludwigshafen",null],["083355006099","Orsingen-Nenzingen",null],["083360014014","Efringen-Kirchen",null],["083360084084","Steinen",null],["083360087087","Todtnau, Stadt",null],["083360091091","Weil am Rhein, Stadt",null],["083360105105","Grenzach-Wyhlen",null],["083360107107","Kleines Wiesental",null],["083365001045","Kandern, Stadt",null],["083365001104","Malsburg-Marzell",null],["083365003043","Inzlingen",null],["083365003050","Lörrach, Stadt",null],["083365004069","Rheinfelden (Baden), Stadt",null],["083365004082","Schwörstadt",null],["083365005006","Bad Bellingen",null],["083365005078","Schliengen",null],["083365006004","Aitern",null],["083365006010","Böllen",null],["083365006025","Fröhnd",null],["083365006079","Schönau im Schwarzwald, Stadt",null],["083365006080","Schönenberg",null],["083365006089","Tunau",null],["083365006090","Utzenfeld",null],["083365006094","Wembach",null],["083365006096","Wieden",null],["083365007034","Hasel",null],["083365007036","Hausen im Wiesental",null],["083365007057","Maulburg",null],["083365007081","Schopfheim, Stadt",null],["083365008008","Binzen",null],["083365008019","Eimeldingen",null],["083365008024","Fischingen",null],["083365008073","Rümmingen",null],["083365008075","Schallbach",null],["083365008100","Wittlingen",null],["083365009103","Zell im Wiesental, Stadt",null],["083365009106","Häg-Ehrsberg",null],["083370002002","Albbruck",null],["083370038038","Görwihl",null],["083370062062","Klettgau",null],["083370066066","Laufenburg (Baden), Stadt",null],["083370106106","Stühlingen, Stadt",null],["083370116116","Wehr, Stadt",null],["083375001022","Bonndorf im Schwarzwald, Stadt",null],["083375001127","Wutach",null],["083375002030","Dettighofen",null],["083375002060","Jestetten",null],["083375002070","Lottstetten",null],["083375003053","Hohentengen am Hochrhein",null],["083375003125","Küssaberg",null],["083375004039","Grafenhausen",null],["083375004128","Ühlingen-Birkendorf",null],["083375005049","Herrischried",null],["083375005076","Murg",null],["083375005090","Rickenbach",null],["083375005096","Bad Säckingen, Stadt",null],["083375006013","Bernau im Schwarzwald",null],["083375006027","Dachsberg (Südschwarzwald)",null],["083375006045","Häusern",null],["083375006051","Höchenschwand",null],["083375006059","Ibach",null],["083375006097","St. Blasien, Stadt",null],["083375006108","Todtmoos",null],["083375007032","Dogern",null],["083375007065","Lauchringen",null],["083375007118","Weilheim",null],["083375007126","Waldshut-Tiengen, Stadt",null],["083375008123","Wutöschingen",null],["083375008124","Eggingen",null],["084150014014","Dettingen an der Erms",null],["084150019019","Eningen unter Achalm",null],["084150059059","Pfullingen, Stadt",null],["084150061061","Reutlingen, Stadt",null],["084150073073","Trochtelfingen, Stadt",null],["084150080080","Wannweil",null],["084150091091","Sonnenbühl",null],["084150092092","Lichtenstein",null],["084150093093","St. Johann",null],["084155001089","Engstingen",null],["084155001090","Hohenstein",null],["084155002029","Grafenberg",null],["084155002050","Metzingen, Stadt",null],["084155002062","Riederich",null],["084155003027","Gomadingen",null],["084155003048","Mehrstetten",null],["084155003053","Münsingen, Stadt",null],["084155004060","Pliezhausen",null],["084155004087","Walddorfhäslach",null],["084155005028","Grabenstetten",null],["084155005039","Hülben",null],["084155005078","Bad Urach, Stadt",null],["084155005088","Römerstein",null],["084155006034","Hayingen, Stadt",null],["084155006058","Pfronstetten",null],["084155006085","Zwiefalten",null],["084159971971","Gutsbezirk Münsingen, gemeindefreies Gebiet",null],["084160009009","Dettenhausen",null],["084160022022","Kirchentellinsfurt",null],["084160023023","Kusterdingen",null],["084160041041","Tübingen, Universitätsstadt",null],["084160048048","Ammerbuch",null],["084165001011","Dußlingen",null],["084165001015","Gomaringen",null],["084165001026","Nehren",null],["084165002006","Bodelshausen",null],["084165002025","Mössingen, Stadt",null],["084165002031","Ofterdingen",null],["084165003018","Hirrlingen",null],["084165003036","Rottenburg am Neckar, Stadt",null],["084165003049","Neustetten",null],["084165003050","Starzach",null],["084170013013","Burladingen, Stadt",null],["084170025025","Haigerloch, Stadt",null],["084170054054","Rosenfeld, Stadt",null],["084175001010","Bitz",null],["084175001079","Albstadt, Stadt",null],["084175002002","Balingen, Stadt",null],["084175002022","Geislingen, Stadt",null],["084175003008","Bisingen",null],["084175003023","Grosselfingen",null],["084175004031","Hechingen, Stadt",null],["084175004036","Jungingen",null],["084175004051","Rangendingen",null],["084175005044","Meßstetten, Stadt",null],["084175005045","Nusplingen",null],["084175005047","Obernheim",null],["084175006014","Dautmergen",null],["084175006015","Dormettingen",null],["084175006016","Dotternhausen",null],["084175006029","Hausen am Tann",null],["084175006052","Ratshausen",null],["084175006057","Schömberg, Stadt",null],["084175006071","Weilen unter den Rinnen",null],["084175006078","Zimmern unter der Burg",null],["084175007063","Straßberg",null],["084175007075","Winterlingen",null],["084210000000","Ulm, Universitätsstadt",null],["084250039039","Erbach, Stadt",null],["084250108108","Schelklingen, Stadt",null],["084250141141","Blaustein, Stadt",null],["084255001002","Allmendingen",null],["084255001004","Altheim",null],["084255002017","Berghülen",null],["084255002020","Blaubeuren, Stadt",null],["084255003028","Dietenheim, Stadt",null],["084255003066","Illerrieden",null],["084255003140","Balzheim",null],["084255004014","Beimerstetten",null],["084255004031","Dornstadt",null],["084255004135","Westerstetten",null],["084255005033","Ehingen (Donau), Stadt",null],["084255005050","Griesingen",null],["084255005088","Oberdischingen",null],["084255005093","Öpfingen",null],["084255006064","Hüttisheim",null],["084255006110","Schnürpflingen",null],["084255006137","Illerkirchberg",null],["084255006138","Staig",null],["084255007071","Laichingen, Stadt",null],["084255007079","Merklingen",null],["084255007084","Nellingen",null],["084255007134","Westerheim",null],["084255007139","Heroldstatt",null],["084255008005","Altheim (Alb)",null],["084255008011","Asselfingen",null],["084255008013","Ballendorf",null],["084255008019","Bernstadt",null],["084255008022","Börslingen",null],["084255008024","Breitingen",null],["084255008062","Holzkirch",null],["084255008072","Langenau, Stadt",null],["084255008083","Neenstetten",null],["084255008085","Nerenstetten",null],["084255008092","Öllingen",null],["084255008097","Rammingen",null],["084255008112","Setzingen",null],["084255008130","Weidenstetten",null],["084255009008","Amstetten",null],["084255009075","Lonsee",null],["084255010035","Emeringen",null],["084255010036","Emerkingen",null],["084255010052","Grundsheim",null],["084255010055","Hausen am Bussen",null],["084255010073","Lauterach",null],["084255010081","Munderkingen, Stadt",null],["084255010090","Obermarchtal",null],["084255010091","Oberstadion",null],["084255010098","Rechtenstein",null],["084255010104","Rottenacker",null],["084255010123","Untermarchtal",null],["084255010124","Unterstadion",null],["084255010125","Unterwachingen",null],["084260134134","Schemmerhofen",null],["084265001005","Alleshausen",null],["084265001006","Allmannsweiler",null],["084265001013","Bad Buchau, Stadt",null],["084265001020","Betzenweiler",null],["084265001036","Dürnau",null],["084265001064","Kanzach",null],["084265001078","Moosburg",null],["084265001090","Oggelshausen",null],["084265001109","Seekirch",null],["084265001118","Tiefenbach",null],["084265002014","Bad Schussenried, Stadt",null],["084265002062","Ingoldingen",null],["084265003011","Attenweiler",null],["084265003021","Biberach an der Riß, Stadt",null],["084265003038","Eberhardzell",null],["084265003058","Hochdorf",null],["084265003071","Maselheim",null],["084265003074","Mittelbiberach",null],["084265003120","Ummendorf",null],["084265003128","Warthausen",null],["084265004019","Berkheim",null],["084265004031","Dettingen an der Iller",null],["084265004044","Erolzheim",null],["084265004065","Kirchberg an der Iller",null],["084265004066","Kirchdorf an der Iller",null],["084265005001","Achstetten",null],["084265005028","Burgrieden",null],["084265005070","Laupheim, Stadt",null],["084265005073","Mietingen",null],["084265006043","Erlenmoos",null],["084265006087","Ochsenhausen, Stadt",null],["084265006113","Steinhausen an der Rottum",null],["084265006135","Gutenzell-Hürbel",null],["084265007008","Altheim",null],["084265007035","Dürmentingen",null],["084265007045","Ertingen",null],["084265007067","Langenenslingen",null],["084265007097","Riedlingen, Stadt",null],["084265007121","Unlingen",null],["084265007124","Uttenweiler",null],["084265008100","Rot an der Rot",null],["084265008117","Tannheim",null],["084265009108","Schwendi",null],["084265009125","Wain",null],["084350035035","Meckenbeuren",null],["084355001013","Eriskirch",null],["084355001029","Kressbronn am Bodensee",null],["084355001030","Langenargen",null],["084355002016","Friedrichshafen, Stadt",null],["084355002024","Immenstaad am Bodensee",null],["084355003005","Bermatingen",null],["084355003034","Markdorf, Stadt",null],["084355003045","Oberteuringen",null],["084355003067","Deggenhausertal",null],["084355004010","Daisendorf",null],["084355004018","Hagnau am Bodensee",null],["084355004036","Meersburg, Stadt",null],["084355004054","Stetten",null],["084355004066","Uhldingen-Mühlhofen",null],["084355005015","Frickingen",null],["084355005020","Heiligenberg",null],["084355005052","Salem",null],["084355006042","Neukirch",null],["084355006057","Tettnang, Stadt",null],["084355007047","Owingen",null],["084355007053","Sipplingen",null],["084355007059","Überlingen, Stadt",null],["084360008008","Aulendorf, Stadt",null],["084360010010","Bad Wurzach, Stadt",null],["084360049049","Isny im Allgäu, Stadt",null],["084360052052","Kißlegg",null],["084360094094","Argenbühl",null],["084365001005","Altshausen",null],["084365001019","Boms",null],["084365001024","Ebenweiler",null],["084365001027","Eichstegen",null],["084365001032","Fleischwangen",null],["084365001040","Guggenhausen",null],["084365001047","Hoßkirch",null],["084365001053","Königseggwald",null],["084365001067","Riedhausen",null],["084365001077","Unterwaldhausen",null],["084365001093","Ebersbach-Musbach",null],["084365002009","Bad Waldsee, Stadt",null],["084365002014","Bergatreute",null],["084365003018","Bodnegg",null],["084365003039","Grünkraut",null],["084365003069","Schlier",null],["084365003079","Waldburg",null],["084365004003","Aichstetten",null],["084365004004","Aitrach",null],["084365004055","Leutkirch im Allgäu, Stadt",null],["084365005011","Baienfurt",null],["084365005012","Baindt",null],["084365005013","Berg",null],["084365005064","Ravensburg, Stadt",null],["084365005082","Weingarten, Stadt",null],["084365006078","Vogt",null],["084365006085","Wolfegg",null],["084365007001","Achberg",null],["084365007006","Amtzell",null],["084365007081","Wangen im Allgäu, Stadt",null],["084365008083","Wilhelmsdorf",null],["084365008095","Horgenzell",null],["084365009087","Wolpertswende",null],["084365009096","Fronreute",null],["084370086086","Ostrach",null],["084375001031","Gammertingen, Stadt",null],["084375001047","Hettingen, Stadt",null],["084375001082","Neufra",null],["084375001114","Veringenstadt, Stadt",null],["084375002053","Hohentengen",null],["084375002076","Mengen, Stadt",null],["084375002101","Scheer, Stadt",null],["084375003072","Leibertingen",null],["084375003078","Meßkirch, Stadt",null],["084375003123","Sauldorf",null],["084375004056","Illmensee",null],["084375004088","Pfullendorf, Stadt",null],["084375004118","Wald",null],["084375004124","Herdwangen-Schönach",null],["084375005044","Herbertingen",null],["084375005100","Bad Saulgau, Stadt",null],["084375006005","Beuron",null],["084375006008","Bingen",null],["084375006059","Inzigkofen",null],["084375006065","Krauchenwies",null],["084375006104","Sigmaringen, Stadt",null],["084375006105","Sigmaringendorf",null],["084375007102","Schwenningen",null],["084375007107","Stetten am kalten Markt",null],["091610000000","Ingolstadt",null],["091620000000","München, Landeshauptstadt",null],["091630000000","Rosenheim",null],["091710111111","Altötting, St",null],["091710112112","Burghausen, St",null],["091710113113","Burgkirchen a.d.Alz",null],["091710117117","Garching a.d.Alz",null],["091710118118","Haiming",null],["091710125125","Neuötting, St",null],["091710127127","Pleiskirchen",null],["091710131131","Teising",null],["091710132132","Töging a.Inn, St",null],["091710133133","Tüßling, M",null],["091710137137","Winhöring",null],["091715101114","Emmerting",null],["091715101124","Mehring",null],["091715102116","Feichten a.d.Alz",null],["091715102119","Halsbach",null],["091715102122","Kirchweidach",null],["091715102134","Tyrlaching",null],["091715103123","Marktl, M",null],["091715103130","Stammham",null],["091715104115","Erlbach",null],["091715104126","Perach",null],["091715104129","Reischach",null],["091715106121","Kastl",null],["091715106135","Unterneukirchen",null],["091720111111","Ainring",null],["091720112112","Anger",null],["091720114114","Bad Reichenhall, GKSt",null],["091720115115","Bayerisch Gmain",null],["091720116116","Berchtesgaden, M",null],["091720117117","Bischofswiesen",null],["091720118118","Freilassing, St",null],["091720122122","Laufen, St",null],["091720124124","Marktschellenberg, M",null],["091720128128","Piding",null],["091720129129","Ramsau b.Berchtesgaden",null],["091720130130","Saaldorf-Surheim",null],["091720131131","Schneizlreuth",null],["091720132132","Schönau a.Königssee",null],["091720134134","Teisendorf, M",null],["091729452452","Eck",null],["091729454454","Schellenberger Forst",null],["091730111111","Bad Heilbrunn",null],["091730112112","Bad Tölz, St",null],["091730118118","Dietramszell",null],["091730120120","Egling",null],["091730123123","Eurasburg",null],["091730124124","Gaißach",null],["091730126126","Geretsried, St",null],["091730130130","Icking",null],["091730131131","Jachenau",null],["091730134134","Königsdorf",null],["091730135135","Lenggries",null],["091730137137","Münsing",null],["091730145145","Wackersberg",null],["091730147147","Wolfratshausen, St",null],["091735107113","Benediktbeuern",null],["091735107115","Bichl",null],["091735108133","Kochel a.See",null],["091735108142","Schlehdorf",null],["091735109127","Greiling",null],["091735109140","Reichersbeuern",null],["091735109141","Sachsenkam",null],["091739451451","Pupplinger Au",null],["091739452452","Wolfratshauser Forst",null],["091740111111","Altomünster, M",null],["091740113113","Bergkirchen",null],["091740115115","Dachau, GKSt",null],["091740118118","Erdweg",null],["091740121121","Haimhausen",null],["091740122122","Hebertshausen",null],["091740126126","Karlsfeld",null],["091740131131","Markt Indersdorf, M",null],["091740135135","Odelzhausen",null],["091740136136","Petershausen",null],["091740137137","Pfaffenhofen a.d.Glonn",null],["091740141141","Röhrmoos",null],["091740143143","Schwabhausen",null],["091740146146","Sulzemoos",null],["091740147147","Hilgertshausen-Tandern",null],["091740150150","Vierkirchen",null],["091740151151","Weichs",null],["091750111111","Anzing",null],["091750115115","Ebersberg, St",null],["091750118118","Forstinning",null],["091750122122","Grafing b.München, St",null],["091750123123","Hohenlinden",null],["091750124124","Kirchseeon, M",null],["091750127127","Markt Schwaben, M",null],["091750132132","Vaterstetten",null],["091750133133","Pliening",null],["091750135135","Poing",null],["091750137137","Steinhöring",null],["091750139139","Zorneding",null],["091755112112","Aßling",null],["091755112119","Frauenneuharting",null],["091755112136","Emmering",null],["091755114113","Baiern",null],["091755114114","Bruck",null],["091755114116","Egmating",null],["091755114121","Glonn, M",null],["091755114128","Moosach",null],["091755114131","Oberpframmern",null],["091759451451","Anzinger Forst",null],["091759452452","Ebersberger Forst",null],["091759453453","Eglhartinger Forst",null],["091760112112","Altmannstein, M",null],["091760114114","Beilngries, St",null],["091760118118","Buxheim",null],["091760120120","Denkendorf",null],["091760121121","Dollnstein, M",null],["091760123123","Eichstätt, GKSt",null],["091760126126","Gaimersheim, M",null],["091760129129","Großmehring",null],["091760131131","Hepberg",null],["091760132132","Hitzhofen",null],["091760137137","Kinding, M",null],["091760138138","Kipfenberg, M",null],["091760139139","Kösching, M",null],["091760143143","Lenting",null],["091760148148","Mörnsheim, M",null],["091760161161","Stammham",null],["091760164164","Titting, M",null],["091760166166","Wellheim, M",null],["091760167167","Wettstetten",null],["091765115155","Pollenfeld",null],["091765115160","Schernfeld",null],["091765115165","Walting",null],["091765116116","Böhmfeld",null],["091765116124","Eitensheim",null],["091765118111","Adelschlag",null],["091765118122","Egweil",null],["091765118149","Nassenfels, M",null],["091765119147","Mindelstetten",null],["091765119150","Oberdolling",null],["091765119153","Pförring, M",null],["091769451451","Haunstetter Forst",null],["091770113113","Bockhorn",null],["091770115115","Dorfen, St",null],["091770117117","Erding, GKSt",null],["091770118118","Finsing",null],["091770119119","Forstern",null],["091770120120","Fraunberg",null],["091770123123","Isen, M",null],["091770127127","Lengdorf",null],["091770130130","Moosinning",null],["091770137137","Sankt Wolfgang",null],["091770139139","Taufkirchen (Vils)",null],["091775120114","Buch a.Buchrain",null],["091775120135","Pastetten",null],["091775121142","Walpertskirchen",null],["091775121144","Wörth",null],["091775123116","Eitting",null],["091775123133","Oberding",null],["091775124131","Neuching",null],["091775124134","Ottenhofen",null],["091775125121","Hohenpolding",null],["091775125122","Inning a.Holz",null],["091775125124","Kirchberg",null],["091775125138","Steinkirchen",null],["091775126112","Berglern",null],["091775126126","Langenpreising",null],["091775126143","Wartenberg, M",null],["091780116116","Au i.d.Hallertau, M",null],["091780120120","Eching",null],["091780122122","Rudelzhausen",null],["091780123123","Fahrenzhausen",null],["091780124124","Freising, GKSt",null],["091780130130","Hallbergmoos",null],["091780133133","Hohenkammer",null],["091780136136","Kirchdorf a.d.Amper",null],["091780137137","Kranzberg",null],["091780138138","Langenbach",null],["091780140140","Marzling",null],["091780143143","Moosburg a.d.Isar, St",null],["091780144144","Nandlstadt, M",null],["091780145145","Neufahrn b.Freising",null],["091785127113","Allershausen",null],["091785127150","Paunzhausen",null],["091785129125","Gammelsdorf",null],["091785129132","Hörgertshausen",null],["091785129142","Mauern",null],["091785129155","Wang",null],["091785130115","Attenkirchen",null],["091785130129","Haag a.d.Amper",null],["091785130156","Wolfersdorf",null],["091785130157","Zolling",null],["091790113113","Alling",null],["091790117117","Egenhofen",null],["091790118118","Eichenau",null],["091790119119","Emmering",null],["091790121121","Fürstenfeldbruck, GKSt",null],["091790123123","Germering, GKSt",null],["091790126126","Gröbenzell",null],["091790134134","Maisach",null],["091790138138","Moorenweis",null],["091790142142","Olching, St",null],["091790145145","Puchheim, St",null],["091790149149","Türkenfeld",null],["091795131111","Adelshofen",null],["091795131114","Althegnenberg",null],["091795131128","Hattenhofen",null],["091795131130","Jesenwang",null],["091795131132","Landsberied",null],["091795131136","Mammendorf",null],["091795131137","Mittelstetten",null],["091795131140","Oberschweinbach",null],["091795132125","Grafrath",null],["091795132131","Kottgeisering",null],["091795132147","Schöngeising",null],["091800112112","Bad Kohlgrub",null],["091800116116","Farchant",null],["091800117117","Garmisch-Partenkirchen, M",null],["091800118118","Grainau",null],["091800122122","Krün",null],["091800123123","Mittenwald, M",null],["091800124124","Murnau a.Staffelsee, M",null],["091800125125","Oberammergau",null],["091800126126","Oberau",null],["091800134134","Uffing a.Staffelsee",null],["091800136136","Wallgau",null],["091805133113","Bad Bayersoien",null],["091805133129","Saulgrub",null],["091805135115","Ettal",null],["091805135135","Unterammergau",null],["091805136114","Eschenlohe",null],["091805136119","Großweil",null],["091805136127","Ohlstadt",null],["091805136131","Schwaigen",null],["091805137128","Riegsee",null],["091805137132","Seehausen a.Staffelsee",null],["091805137133","Spatzenhausen",null],["091809451451","Ettaler Forst",null],["091810113113","Denklingen",null],["091810114114","Dießen am Ammersee, M",null],["091810116116","Egling a.d.Paar",null],["091810122122","Geltendorf",null],["091810128128","Kaufering, M",null],["091810130130","Landsberg am Lech, GKSt",null],["091810132132","Penzing",null],["091810144144","Utting am Ammersee",null],["091810145145","Weil",null],["091815138121","Fuchstal",null],["091815138143","Unterdießen",null],["091815139126","Hurlach",null],["091815139127","Igling",null],["091815139131","Obermeitingen",null],["091815140134","Prittriching",null],["091815140138","Scheuring",null],["091815141124","Hofstetten",null],["091815141140","Schwifting",null],["091815141141","Pürgen",null],["091815142111","Apfeldorf",null],["091815142129","Kinsau",null],["091815142133","Vilgertshofen",null],["091815142135","Reichling",null],["091815142137","Rott",null],["091815142142","Thaining",null],["091815143115","Eching am Ammersee",null],["091815143123","Greifenberg",null],["091815143139","Schondorf am Ammersee",null],["091815144118","Eresing",null],["091815144120","Finning",null],["091815144146","Windach",null],["091819451451","Ammersee",null],["091820111111","Bad Wiessee",null],["091820112112","Bayrischzell",null],["091820114114","Fischbachau",null],["091820116116","Gmund a.Tegernsee",null],["091820119119","Hausham",null],["091820120120","Holzkirchen, M",null],["091820123123","Irschenberg",null],["091820124124","Kreuth",null],["091820125125","Miesbach, St",null],["091820127127","Otterfing",null],["091820129129","Rottach-Egern",null],["091820131131","Schliersee, M",null],["091820132132","Tegernsee, St",null],["091820133133","Valley",null],["091820134134","Waakirchen",null],["091820136136","Warngau",null],["091820137137","Weyarn",null],["091830112112","Ampfing",null],["091830113113","Aschau a.Inn",null],["091830114114","Buchbach, M",null],["091830119119","Haag i.OB, M",null],["091830127127","Mettenheim",null],["091830128128","Mühldorf a.Inn, St",null],["091830135135","Obertaufkirchen",null],["091830144144","Schwindegg",null],["091830148148","Waldkraiburg, St",null],["091835145120","Heldenstein",null],["091835145138","Rattenkirchen",null],["091835146118","Gars a.Inn, M",null],["091835146147","Unterreit",null],["091835147123","Kirchdorf",null],["091835147140","Reichertsheim",null],["091835148122","Jettenbach",null],["091835148124","Kraiburg a.Inn, M",null],["091835148145","Taufkirchen",null],["091835149115","Egglkofen",null],["091835149129","Neumarkt-Sankt Veit, St",null],["091835150125","Lohkirchen",null],["091835150132","Oberbergkirchen",null],["091835150143","Schönberg",null],["091835150151","Zangberg",null],["091835151134","Oberneukirchen",null],["091835151136","Polling",null],["091835152116","Erharting",null],["091835152130","Niederbergkirchen",null],["091835152131","Niedertaufkirchen",null],["091835183126","Maitenbeth",null],["091835183139","Rechtmehring",null],["091839451451","Mühldorfer Hart",null],["091840112112","Aschheim",null],["091840113113","Baierbrunn",null],["091840114114","Brunnthal",null],["091840118118","Feldkirchen",null],["091840119119","Garching b.München, St",null],["091840120120","Gräfelfing",null],["091840121121","Grasbrunn",null],["091840122122","Grünwald",null],["091840123123","Haar",null],["091840127127","Höhenkirchen-Siegertsbrunn",null],["091840129129","Hohenbrunn",null],["091840130130","Ismaning",null],["091840131131","Kirchheim b.München",null],["091840132132","Neuried",null],["091840134134","Oberhaching",null],["091840135135","Oberschleißheim",null],["091840136136","Ottobrunn",null],["091840137137","Aying",null],["091840138138","Planegg",null],["091840139139","Pullach i.Isartal",null],["091840140140","Putzbrunn",null],["091840141141","Sauerlach",null],["091840142142","Schäftlarn",null],["091840144144","Straßlach-Dingharting",null],["091840145145","Taufkirchen",null],["091840146146","Neubiberg",null],["091840147147","Unterföhring",null],["091840148148","Unterhaching",null],["091840149149","Unterschleißheim, St",null],["091849452452","Forstenrieder Park",null],["091849454454","Grünwalder Forst",null],["091849457457","Perlacher Forst",null],["091850113113","Aresing",null],["091850125125","Burgheim, M",null],["091850127127","Ehekirchen",null],["091850139139","Karlshuld",null],["091850140140","Karlskron",null],["091850149149","Neuburg a.d.Donau, GKSt",null],["091850150150","Oberhausen",null],["091850153153","Rennertshofen, M",null],["091850158158","Schrobenhausen, St",null],["091850163163","Königsmoos",null],["091850168168","Weichering",null],["091855154118","Bergheim",null],["091855154157","Rohrenfels",null],["091855155116","Berg im Gau",null],["091855155123","Brunnen",null],["091855155131","Gachenbach",null],["091855155143","Langenmosen",null],["091855155166","Waidhofen",null],["091860113113","Baar-Ebenhausen",null],["091860125125","Gerolsbach",null],["091860128128","Hohenwart, M",null],["091860132132","Jetzendorf",null],["091860137137","Manching, M",null],["091860139139","Münchsmünster",null],["091860143143","Pfaffenhofen a.d.Ilm, St",null],["091860146146","Reichertshausen",null],["091860149149","Rohrbach",null],["091860151151","Scheyern",null],["091860152152","Schweitenkirchen",null],["091860158158","Vohburg a.d.Donau, St",null],["091860162162","Wolnzach, M",null],["091865156116","Ernsgaden",null],["091865156122","Geisenfeld, St",null],["091865157126","Hettenshausen",null],["091865157130","Ilmmünster",null],["091865158144","Pörnbach",null],["091865158147","Reichertshofen, M",null],["091870113113","Amerang",null],["091870114114","Aschau i.Chiemgau",null],["091870116116","Babensham",null],["091870117117","Bad Aibling, St",null],["091870118118","Bernau a.Chiemsee",null],["091870120120","Brannenburg",null],["091870122122","Bruckmühl, M",null],["091870124124","Edling",null],["091870125125","Eggstätt",null],["091870126126","Eiselfing",null],["091870128128","Bad Endorf, M",null],["091870129129","Bad Feilnbach",null],["091870130130","Feldkirchen-Westerham",null],["091870131131","Flintsbach a.Inn",null],["091870132132","Frasdorf",null],["091870134134","Griesstätt",null],["091870137137","Großkarolinenfeld",null],["091870142142","Schechen",null],["091870148148","Kiefersfelden",null],["091870150150","Kolbermoor, St",null],["091870154154","Neubeuern, M",null],["091870156156","Nußdorf a.Inn",null],["091870157157","Oberaudorf",null],["091870162162","Prien a.Chiemsee, M",null],["091870163163","Prutting",null],["091870165165","Raubling",null],["091870167167","Riedering",null],["091870168168","Rimsting",null],["091870169169","Rohrdorf",null],["091870172172","Samerberg",null],["091870174174","Söchtenau",null],["091870176176","Soyen",null],["091870177177","Stephanskirchen",null],["091870179179","Tuntenhausen",null],["091870181181","Vogtareuth",null],["091870182182","Wasserburg a.Inn, St",null],["091875160121","Breitbrunn a.Chiemsee",null],["091875160123","Chiemsee",null],["091875160138","Gstadt a.Chiemsee",null],["091875162139","Halfing",null],["091875162145","Höslwang",null],["091875162173","Schonstett",null],["091875165164","Ramerberg",null],["091875165170","Rott a.Inn",null],["091875184159","Pfaffing",null],["091875184186","Albaching",null],["091879451451","Rotter Forst-Nord",null],["091879452452","Rotter Forst-Süd",null],["091880113113","Berg",null],["091880117117","Andechs",null],["091880118118","Feldafing",null],["091880120120","Gauting",null],["091880121121","Gilching",null],["091880124124","Herrsching a.Ammersee",null],["091880126126","Inning a.Ammersee",null],["091880127127","Krailling",null],["091880132132","Seefeld",null],["091880137137","Pöcking",null],["091880139139","Starnberg, St",null],["091880141141","Tutzing",null],["091880144144","Weßling",null],["091880145145","Wörthsee",null],["091889451451","Starnberger See",null],["091890111111","Altenmarkt a.d.Alz",null],["091890114114","Chieming",null],["091890115115","Engelsberg",null],["091890118118","Fridolfing",null],["091890119119","Grabenstätt",null],["091890120120","Grassau, M",null],["091890124124","Inzell",null],["091890127127","Kirchanschöring",null],["091890130130","Nußdorf",null],["091890134134","Palling",null],["091890135135","Petting",null],["091890139139","Reit im Winkl",null],["091890140140","Ruhpolding",null],["091890141141","Schleching",null],["091890142142","Schnaitsee",null],["091890143143","Seeon-Seebruck",null],["091890145145","Siegsdorf",null],["091890148148","Surberg",null],["091890149149","Tacherting",null],["091890152152","Tittmoning, St",null],["091890154154","Traunreut, St",null],["091890155155","Traunstein, GKSt",null],["091890157157","Trostberg, St",null],["091890159159","Übersee",null],["091890160160","Unterwössen",null],["091895166113","Bergen",null],["091895166161","Vachendorf",null],["091895169129","Marquartstein",null],["091895169146","Staudach-Egerndach",null],["091895170126","Kienberg",null],["091895170133","Obing",null],["091895170137","Pittenhart",null],["091895173150","Taching a.See",null],["091895173162","Waging a.See, M",null],["091895173165","Wonneberg",null],["091899451451","Chiemsee (See)",null],["091899452452","Waginger See",null],["091900115115","Bernried am Starnberger See",null],["091900130130","Hohenpeißenberg",null],["091900138138","Pähl",null],["091900139139","Peißenberg, M",null],["091900140140","Peiting, M",null],["091900141141","Penzberg, St",null],["091900142142","Polling",null],["091900144144","Raisting",null],["091900148148","Schongau, St",null],["091900157157","Weilheim i.OB, St",null],["091900158158","Wessobrunn",null],["091900159159","Wielenbach",null],["091905174111","Altenstadt",null],["091905174129","Hohenfurch",null],["091905174133","Ingenried",null],["091905174149","Schwabbruck",null],["091905174151","Schwabsoien",null],["091905175114","Bernbeuren",null],["091905175118","Burggen",null],["091905176113","Antdorf",null],["091905176126","Habach",null],["091905176136","Obersöchering",null],["091905176153","Sindelsdorf",null],["091905177120","Eberfing",null],["091905177121","Eglfing",null],["091905177131","Huglfing",null],["091905177135","Oberhausen",null],["091905178117","Böbing",null],["091905178145","Rottenbuch",null],["091905179132","Iffeldorf",null],["091905179152","Seeshaupt",null],["091905180143","Prem",null],["091905180154","Steingaden",null],["091905180160","Wildsteig",null],["092610000000","Landshut",null],["092620000000","Passau",null],["092630000000","Straubing",null],["092710111111","Aholming",null],["092710113113","Auerbach",null],["092710116116","Bernried",null],["092710119119","Deggendorf, GKSt",null],["092710122122","Grafling",null],["092710125125","Hengersberg, M",null],["092710127127","Iggensbach",null],["092710128128","Künzing",null],["092710132132","Metten, M",null],["092710138138","Niederalteich",null],["092710140140","Offenberg",null],["092710141141","Osterhofen, St",null],["092710146146","Plattling, St",null],["092710151151","Stephansposching",null],["092710153153","Winzer, M",null],["092715202123","Grattersdorf",null],["092715202126","Hunding",null],["092715202130","Lalling",null],["092715202148","Schaufling",null],["092715204139","Oberpöring",null],["092715204143","Otzing",null],["092715204152","Wallerfing",null],["092715205118","Buchhofen",null],["092715205135","Moos",null],["092715206114","Außernzell",null],["092715206149","Schöllnach, M",null],["092720118118","Freyung, St",null],["092720120120","Grafenau, St",null],["092720121121","Grainet",null],["092720122122","Haidmühle",null],["092720127127","Hohenau",null],["092720129129","Jandelsbrunn",null],["092720134134","Mauth",null],["092720136136","Neureichenau",null],["092720140140","Ringelai",null],["092720141141","Röhrnbach, M",null],["092720142142","Saldenburg",null],["092720143143","Sankt Oswald-Riedlhütte",null],["092720146146","Neuschönau",null],["092720149149","Spiegelau",null],["092720151151","Waldkirchen, St",null],["092725211116","Eppenschlag",null],["092725211128","Innernzell",null],["092725211145","Schöfweg",null],["092725211147","Schönberg, M",null],["092725212126","Hinterschmiding",null],["092725212139","Philippsreut",null],["092725213150","Thurmansbang",null],["092725213152","Zenting",null],["092725214119","Fürsteneck",null],["092725214138","Perlesreut, M",null],["092729451451","Annathaler Wald",null],["092729452452","Frauenberger u. Duschlberger Wald",null],["092729453453","Graineter Wald",null],["092729455455","Leopoldsreuter Wald",null],["092729456456","Mauther Forst",null],["092729457457","Philippsreuter Wald",null],["092729458458","Pleckensteiner Wald",null],["092729459459","Sankt Oswald",null],["092729460460","Schlichtenberger Wald",null],["092729461461","Schönbrunner Wald",null],["092729463463","Waldhäuserwald",null],["092730111111","Abensberg, St",null],["092730116116","Bad Abbach, M",null],["092730137137","Kelheim, St",null],["092730147147","Mainburg, St",null],["092730152152","Neustadt a.d.Donau, St",null],["092730159159","Painten, M",null],["092730164164","Riedenburg, St",null],["092730165165","Rohr i.NB, M",null],["092735215121","Essing, M",null],["092735215133","Ihrlerstein",null],["092735216166","Saal a.d.Donau",null],["092735216175","Teugn",null],["092735217125","Hausen",null],["092735217127","Herrngiersdorf",null],["092735217141","Langquaid, M",null],["092735218119","Biburg",null],["092735218139","Kirchdorf",null],["092735218172","Siegenburg, M",null],["092735218177","Train",null],["092735218181","Wildenberg",null],["092735219113","Aiglsbach",null],["092735219115","Attenhofen",null],["092735219163","Elsendorf",null],["092735219178","Volkenschwand",null],["092739451451","Dürnbucher Forst",null],["092739452452","Frauenforst",null],["092739453453","Hacklberg",null],["092739454454","Hienheimer Forst",null],["092740111111","Adlkofen",null],["092740113113","Altdorf, M",null],["092740120120","Bodenkirchen",null],["092740121121","Buch a.Erlbach",null],["092740124124","Eching",null],["092740126126","Ergolding, M",null],["092740128128","Essenbach, M",null],["092740134134","Geisenhausen, M",null],["092740141141","Hohenthann",null],["092740146146","Kumhausen",null],["092740153153","Neufahrn i.NB",null],["092740156156","Niederaichbach",null],["092740172172","Pfeffenhausen, M",null],["092740176176","Rottenburg a.d.Laaber, St",null],["092740182182","Tiefenbach",null],["092740184184","Vilsbiburg, St",null],["092740185185","Vilsheim",null],["092740194194","Bruckberg",null],["092745220119","Bayerbach b.Ergoldsbach",null],["092745220127","Ergoldsbach, M",null],["092745221132","Furth",null],["092745221165","Obersüßbach",null],["092745221187","Weihmichl",null],["092745222174","Postau",null],["092745222188","Weng",null],["092745222191","Wörth a.d.Isar",null],["092745223112","Aham",null],["092745223135","Gerzen",null],["092745223145","Kröning",null],["092745223179","Schalkham",null],["092745226114","Altfraunhofen",null],["092745226118","Baierbach",null],["092745227154","Neufraunhofen",null],["092745227183","Velden, M",null],["092745227193","Wurmsham",null],["092750111111","Aicha vorm Wald",null],["092750114114","Aldersbach",null],["092750116116","Bad Füssing",null],["092750118118","Breitenberg",null],["092750119119","Büchlberg",null],["092750120120","Eging a.See, M",null],["092750121121","Fürstenstein",null],["092750122122","Fürstenzell, M",null],["092750124124","Bad Griesbach i.Rottal, St",null],["092750125125","Haarbach",null],["092750126126","Hauzenberg, St",null],["092750127127","Hofkirchen, M",null],["092750128128","Hutthurm, M",null],["092750130130","Kirchham",null],["092750131131","Kößlarn, M",null],["092750133133","Neuburg a.Inn",null],["092750134134","Neuhaus a.Inn",null],["092750135135","Neukirchen vorm Wald",null],["092750137137","Obernzell, M",null],["092750138138","Ortenburg, M",null],["092750141141","Pocking, St",null],["092750144144","Ruderting",null],["092750145145","Ruhstorf a.d.Rott, M",null],["092750146146","Salzweg",null],["092750148148","Sonnen",null],["092750149149","Tettenweis",null],["092750150150","Thyrnau",null],["092750151151","Tiefenbach",null],["092750153153","Untergriesbach, M",null],["092750154154","Vilshofen an der Donau, St",null],["092750156156","Wegscheid, M",null],["092750159159","Windorf, M",null],["092755229152","Tittling, M",null],["092755229160","Witzmannsberg",null],["092755232112","Aidenbach, M",null],["092755232117","Beutelsbach",null],["092755234132","Malching",null],["092755234143","Rotthalmünster, M",null],["092760113113","Arnbruck",null],["092760115115","Bayerisch Eisenstein",null],["092760116116","Bischofsmais",null],["092760117117","Bodenmais, M",null],["092760118118","Böbrach",null],["092760120120","Drachselsried",null],["092760121121","Frauenau",null],["092760122122","Geiersthal",null],["092760126126","Kirchberg i.Wald",null],["092760127127","Kirchdorf i.Wald",null],["092760128128","Kollnburg",null],["092760129129","Langdorf",null],["092760130130","Lindberg",null],["092760134134","Patersdorf",null],["092760135135","Prackenbach",null],["092760138138","Regen, St",null],["092760139139","Rinchnach",null],["092760143143","Teisnach, M",null],["092760144144","Viechtach, St",null],["092760148148","Zwiesel, St",null],["092765238111","Achslach",null],["092765238123","Gotteszell",null],["092765238142","Ruhmannsfelden, M",null],["092765238146","Zachenberg",null],["092770111111","Arnstorf, M",null],["092770114114","Dietersburg",null],["092770116116","Eggenfelden, St",null],["092770117117","Egglham",null],["092770121121","Gangkofen, M",null],["092770124124","Hebertsfelden",null],["092770126126","Johanniskirchen",null],["092770127127","Julbach",null],["092770128128","Kirchdorf a.Inn",null],["092770134134","Mitterskirchen",null],["092770138138","Pfarrkirchen, St",null],["092770139139","Postmünster",null],["092770142142","Roßbach",null],["092770144144","Schönau",null],["092770145145","Simbach a.Inn, St",null],["092770149149","Triftern, M",null],["092770151151","Unterdietfurt",null],["092770152152","Wittibreut",null],["092770153153","Wurmannsquick, M",null],["092770154154","Zeilarn",null],["092775239119","Falkenberg",null],["092775239131","Malgersdorf",null],["092775239141","Rimbach",null],["092775240122","Geratskirchen",null],["092775240133","Massing, M",null],["092775241112","Bayerbach",null],["092775241113","Bad Birnbach, M",null],["092775243140","Reut",null],["092775243148","Tann, M",null],["092775244118","Ering",null],["092775244147","Stubenberg",null],["092780118118","Bogen, St",null],["092780121121","Feldkirchen",null],["092780123123","Geiselhöring, St",null],["092780129129","Haibach",null],["092780141141","Kirchroth",null],["092780143143","Konzell",null],["092780144144","Laberweinting",null],["092780146146","Leiblfing",null],["092780148148","Mallersdorf-Pfaffenberg, M",null],["092780167167","Oberschneiding",null],["092780170170","Parkstetten",null],["092780178178","Rattenberg",null],["092780184184","Sankt Englmar",null],["092780190190","Steinach",null],["092780197197","Wiesenfelden",null],["092785246147","Loitzendorf",null],["092785246179","Rattiszell",null],["092785246189","Stallwang",null],["092785248116","Ascha",null],["092785248120","Falkenfels",null],["092785248134","Haselbach",null],["092785248151","Mitterfels, M",null],["092785249139","Hunderdorf",null],["092785249154","Neukirchen",null],["092785249198","Windberg",null],["092785250112","Aholfing",null],["092785250117","Atting",null],["092785250172","Perkam",null],["092785250177","Rain",null],["092785252149","Mariaposching",null],["092785252159","Niederwinkling",null],["092785252171","Perasdorf",null],["092785252187","Schwarzach, M",null],["092785256113","Aiterhofen",null],["092785256182","Salching",null],["092785257140","Irlbach",null],["092785257192","Straßkirchen",null],["092790112112","Dingolfing, St",null],["092790113113","Eichendorf, M",null],["092790115115","Frontenhausen, M",null],["092790122122","Landau a.d.Isar, St",null],["092790124124","Loiching",null],["092790126126","Marklkofen",null],["092790127127","Mengkofen",null],["092790128128","Moosthenning",null],["092790130130","Niederviehbach",null],["092790132132","Pilsting, M",null],["092790134134","Reisbach, M",null],["092790135135","Simbach, M",null],["092790137137","Wallersdorf, M",null],["092795208116","Gottfrieding",null],["092795208125","Mamming",null],["093610000000","Amberg",null],["093620000000","Regensburg",null],["093630000000","Weiden i.d.OPf.",null],["093710111111","Ammerthal",null],["093710113113","Auerbach i.d.OPf., St",null],["093710118118","Ebermannsdorf",null],["093710119119","Edelsfeld",null],["093710120120","Ensdorf",null],["093710121121","Freihung, M",null],["093710122122","Freudenberg",null],["093710127127","Hirschau, St",null],["093710129129","Hohenburg, M",null],["093710132132","Kastl, M",null],["093710136136","Kümmersbruck",null],["093710144144","Poppenricht",null],["093710146146","Rieden, M",null],["093710148148","Schmidmühlen, M",null],["093710150150","Schnaittenbach, St",null],["093710151151","Sulzbach-Rosenberg, St",null],["093710154154","Ursensollen",null],["093710156156","Vilseck, St",null],["093715301123","Gebenbach",null],["093715301126","Hahnbach, M",null],["093715302128","Hirschbach",null],["093715302135","Königstein, M",null],["093715303140","Etzelwang",null],["093715303141","Neukirchen b.Sulzbach-Rosenberg",null],["093715303157","Weigendorf",null],["093715304116","Birgland",null],["093715304131","Illschwang",null],["093719452452","Eichen",null],["093720112112","Arnschwang",null],["093720113113","Arrach",null],["093720115115","Blaibach",null],["093720116116","Cham, St",null],["093720117117","Chamerau",null],["093720124124","Eschlkam, M",null],["093720126126","Furth im Wald, St",null],["093720130130","Grafenwiesen",null],["093720135135","Hohenwarth",null],["093720137137","Bad Kötzting, St",null],["093720138138","Lam, M",null],["093720143143","Miltach",null],["093720144144","Neukirchen b.Hl.Blut, M",null],["093720146146","Pemfling",null],["093720151151","Rimbach",null],["093720153153","Roding, St",null],["093720154154","Rötz, St",null],["093720155155","Runding",null],["093720157157","Schönthal",null],["093720158158","Schorndorf",null],["093720164164","Traitsching",null],["093720168168","Waffenbrunn",null],["093720171171","Waldmünchen, St",null],["093720175175","Willmering",null],["093720177177","Zandt",null],["093720178178","Lohberg",null],["093725308163","Tiefenbach",null],["093725308165","Treffelstein",null],["093725310147","Pösing",null],["093725310161","Stamsried, M",null],["093725312128","Gleißenberg",null],["093725312174","Weiding",null],["093725313149","Reichenbach",null],["093725313170","Walderbach",null],["093725317167","Zell",null],["093725317169","Wald",null],["093725318125","Falkenstein, M",null],["093725318142","Michelsneukirchen",null],["093725318150","Rettenbach",null],["093730112112","Berching, St",null],["093730113113","Berg b.Neumarkt i.d.OPf.",null],["093730115115","Breitenbrunn, M",null],["093730119119","Deining",null],["093730121121","Dietfurt a.d.Altmühl, St",null],["093730126126","Freystadt, St",null],["093730134134","Hohenfels, M",null],["093730140140","Lauterhofen, M",null],["093730143143","Lupburg, M",null],["093730146146","Mühlhausen",null],["093730147147","Neumarkt i.d.OPf., GKSt",null],["093730151151","Parsberg, St",null],["093730155155","Postbauer-Heng, M",null],["093730156156","Pyrbaum, M",null],["093730160160","Seubersdorf i.d.OPf.",null],["093730167167","Velburg, St",null],["093735321114","Berngau",null],["093735321153","Pilsach",null],["093735321159","Sengenthal",null],["093740111111","Altenstadt a.d.Waldnaab",null],["093740118118","Eslarn, M",null],["093740121121","Floß, M",null],["093740122122","Flossenbürg",null],["093740124124","Grafenwöhr, St",null],["093740133133","Luhe-Wildenau, M",null],["093740134134","Mantel, M",null],["093740137137","Moosbach, M",null],["093740139139","Neustadt a.d.Waldnaab, St",null],["093740162162","Vohenstrauß, St",null],["093740164164","Waidhaus, M",null],["093740165165","Waldthurn, M",null],["093740168168","Windischeschenbach, St",null],["093745323128","Kirchendemenreuth",null],["093745323144","Parkstein, M",null],["093745323150","Püchersreuth",null],["093745323158","Störnstein",null],["093745323160","Theisseil",null],["093745324148","Trabitz",null],["093745324149","Pressath, St",null],["093745324156","Schwarzenbach",null],["093745325119","Etzenricht",null],["093745325131","Kohlberg, M",null],["093745325166","Weiherhammer",null],["093745326129","Kirchenthumbach, M",null],["093745326155","Schlammersdorf",null],["093745326163","Vorbach",null],["093745327117","Eschenbach i.d.OPf., St",null],["093745327140","Neustadt am Kulm, St",null],["093745327157","Speinshart",null],["093745329127","Irchenrieth",null],["093745329146","Pirk",null],["093745329154","Schirmitz",null],["093745329170","Bechtsrieth",null],["093745330132","Leuchtenberg, M",null],["093745330159","Tännesberg, M",null],["093745331123","Georgenberg",null],["093745331147","Pleystein, St",null],["093749451451","Heinersreuther Forst",null],["093749452452","Manteler Forst",null],["093749458458","Speinsharter Forst",null],["093750117117","Barbing",null],["093750118118","Beratzhausen, M",null],["093750119119","Bernhardswald",null],["093750143143","Hagelstadt",null],["093750148148","Hemau, St",null],["093750161161","Köfering",null],["093750165165","Lappersdorf, M",null],["093750170170","Mintraching",null],["093750174174","Neutraubling, St",null],["093750175175","Nittendorf, M",null],["093750179179","Obertraubling",null],["093750180180","Pentling",null],["093750181181","Pettendorf",null],["093750183183","Pfatter",null],["093750190190","Regenstauf, M",null],["093750196196","Schierling, M",null],["093750199199","Sinzing",null],["093750204204","Tegernheim",null],["093750205205","Thalmassing",null],["093750208208","Wenzenbach",null],["093750209209","Wiesent",null],["093750213213","Zeitlarn",null],["093755332131","Duggendorf",null],["093755332153","Holzheim a.Forst",null],["093755332156","Kallmünz, M",null],["093755333122","Brunn",null],["093755333127","Deuerling",null],["093755333162","Laaber, M",null],["093755334184","Pielenhofen",null],["093755334211","Wolfsegg",null],["093755335114","Altenthann",null],["093755335116","Bach a.d.Donau",null],["093755335130","Donaustauf, M",null],["093755336120","Brennberg",null],["093755336210","Wörth a.d.Donau, St",null],["093755337113","Alteglofsheim",null],["093755337182","Pfakofen",null],["093755338115","Aufhausen",null],["093755338171","Mötzing",null],["093755338191","Riekofen",null],["093755338201","Sünching",null],["093759451451","Forstmühler Forst",null],["093759452452","Kreuther Forst",null],["093760116116","Bodenwöhr",null],["093760117117","Bruck i.d.OPf., M",null],["093760119119","Burglengenfeld, St",null],["093760125125","Fensterbach",null],["093760141141","Maxhütte-Haidhof, St",null],["093760147147","Neunburg vorm Wald, St",null],["093760149149","Nittenau, St",null],["093760150150","Wernberg-Köblitz, M",null],["093760151151","Oberviechtach, St",null],["093760159159","Schmidgaden",null],["093760161161","Schwandorf, GKSt",null],["093760170170","Teublitz, St",null],["093765339131","Gleiritsch",null],["093765339148","Niedermurach",null],["093765339171","Teunz",null],["093765339178","Winklarn, M",null],["093765341112","Altendorf",null],["093765341133","Guteneck",null],["093765341144","Nabburg, St",null],["093765342162","Schwarzach b.Nabburg",null],["093765342163","Schwarzenfeld, M",null],["093765342169","Stulln",null],["093765343153","Pfreimd, St",null],["093765343173","Trausnitz",null],["093765344160","Schönsee, St",null],["093765344167","Stadlern",null],["093765344176","Weiding",null],["093765345122","Dieterskirchen",null],["093765345146","Neukirchen-Balbini, M",null],["093765345164","Schwarzhofen, M",null],["093765345172","Thanstein",null],["093765346168","Steinberg am See",null],["093765346175","Wackersdorf",null],["093769455455","Wolferlohe",null],["093770112112","Bärnau, St",null],["093770116116","Erbendorf, St",null],["093770118118","Friedenfels",null],["093770119119","Fuchsmühl, M",null],["093770127127","Immenreuth",null],["093770131131","Konnersreuth, M",null],["093770133133","Kulmain",null],["093770139139","Mähring, M",null],["093770142142","Bad Neualbenreuth, M",null],["093770146146","Plößberg, M",null],["093770154154","Tirschenreuth, St",null],["093770157157","Waldershof, St",null],["093770158158","Waldsassen, St",null],["093775347137","Leonberg",null],["093775347141","Mitterteich, St",null],["093775347145","Pechbrunn",null],["093775348128","Kastl",null],["093775348129","Kemnath, St",null],["093775349113","Brand",null],["093775349115","Ebnath",null],["093775349143","Neusorg",null],["093775349148","Pullenreuth",null],["093775350132","Krummennaab",null],["093775350149","Reuth b.Erbendorf",null],["093775351117","Falkenberg, M",null],["093775351159","Wiesau, M",null],["094610000000","Bamberg",null],["094620000000","Bayreuth",null],["094630000000","Coburg",null],["094640000000","Hof",null],["094710111111","Altendorf",null],["094710117117","Bischberg",null],["094710119119","Breitengüßbach",null],["094710123123","Buttenheim, M",null],["094710131131","Frensdorf",null],["094710137137","Gundelsheim",null],["094710140140","Hallstadt, St",null],["094710142142","Heiligenstadt i.OFr., M",null],["094710145145","Hirschaid, M",null],["094710150150","Kemmern",null],["094710155155","Litzendorf",null],["094710159159","Memmelsdorf",null],["094710165165","Oberhaid",null],["094710169169","Pettstadt",null],["094710172172","Pommersfelden",null],["094710174174","Rattelsdorf, M",null],["094710185185","Scheßlitz, St",null],["094710191191","Stegaurach",null],["094710195195","Strullendorf",null],["094710207207","Viereth-Trunstadt",null],["094710208208","Walsdorf",null],["094710214214","Zapfendorf, M",null],["094710220220","Schlüsselfeld, St",null],["094715401115","Baunach, St",null],["094715401133","Gerach",null],["094715401152","Lauter",null],["094715401175","Reckendorf",null],["094715403151","Königsfeld",null],["094715403189","Stadelhofen",null],["094715403209","Wattendorf",null],["094715407122","Burgwindheim, M",null],["094715407128","Ebrach, M",null],["094715408120","Burgebrach, M",null],["094715408186","Schönbrunn i.Steigerwald",null],["094715445154","Lisberg",null],["094715445173","Priesendorf",null],["094719452452","Ebracher Forst",null],["094719453453","Eichwald",null],["094719454454","Geisberger Forst",null],["094719455455","Hauptsmoor",null],["094719456456","Koppenwinder Forst",null],["094719457457","Lindach",null],["094719459459","Semberg",null],["094719460460","Steinachsrangen",null],["094719461461","Winkelhofer Forst",null],["094719462462","Zückshuter Forst",null],["094720111111","Ahorntal",null],["094720116116","Bad Berneck i.Fichtelgebirge, St",null],["094720119119","Bindlach",null],["094720121121","Bischofsgrün",null],["094720131131","Eckersdorf",null],["094720138138","Fichtelberg",null],["094720139139","Gefrees, St",null],["094720143143","Goldkronach, St",null],["094720150150","Heinersreuth",null],["094720164164","Mehlmeisel",null],["094720175175","Pegnitz, St",null],["094720179179","Pottenstein, St",null],["094720190190","Speichersdorf",null],["094720197197","Waischenfeld, St",null],["094720198198","Warmensteinach",null],["094725412115","Aufseß",null],["094725412154","Hollfeld, St",null],["094725412176","Plankenfels",null],["094725413141","Glashütten",null],["094725413167","Mistelgau",null],["094725414140","Gesees",null],["094725414155","Hummeltal",null],["094725414166","Mistelbach",null],["094725415133","Emtmannsberg",null],["094725415156","Kirchenpingarten",null],["094725415188","Seybothenreuth",null],["094725415199","Weidenberg, M",null],["094725416127","Creußen, St",null],["094725416146","Haag",null],["094725416180","Prebitz",null],["094725416184","Schnabelwaid, M",null],["094725417118","Betzenstein, St",null],["094725417177","Plech, M",null],["094729451451","Bischofsgrüner Forst",null],["094729453453","Fichtelberg",null],["094729454454","Forst Neustädtlein a.Forst",null],["094729456456","Glashüttener Forst",null],["094729458458","Heinersreuther Forst",null],["094729463463","Neubauer Forst-Nord",null],["094729464464","Prüll",null],["094729468468","Veldensteinerforst",null],["094729469469","Waidacher Forst",null],["094729470470","Warmensteinacher Forst-Nord",null],["094730112112","Ahorn",null],["094730120120","Dörfles-Esbach",null],["094730121121","Ebersdorf b.Coburg",null],["094730132132","Großheirath",null],["094730138138","Itzgrund",null],["094730141141","Lautertal",null],["094730144144","Meeder",null],["094730151151","Neustadt b.Coburg, GKSt",null],["094730158158","Bad Rodach, St",null],["094730159159","Rödental, St",null],["094730165165","Seßlach, St",null],["094730166166","Sonnefeld",null],["094730170170","Untersiemau",null],["094730174174","Weidhausen b.Coburg",null],["094730175175","Weitramsdorf",null],["094735418134","Grub a.Forst",null],["094735418153","Niederfüllbach",null],["094739452452","Callenberger Forst-West",null],["094739453453","Gellnhausen",null],["094739454454","Köllnholz",null],["094740123123","Eggolsheim, M",null],["094740124124","Egloffstein, M",null],["094740126126","Forchheim, GKSt",null],["094740129129","Gößweinstein, M",null],["094740133133","Hallerndorf",null],["094740134134","Hausen",null],["094740135135","Heroldsbach",null],["094740140140","Igensdorf, M",null],["094740146146","Langensendelbach",null],["094740154154","Neunkirchen a.Brand, M",null],["094740156156","Obertrubach",null],["094740161161","Pretzfeld, M",null],["094740176176","Wiesenttal, M",null],["094745420121","Ebermannstadt, St",null],["094745420168","Unterleinleiter",null],["094745422145","Kunreuth",null],["094745422158","Pinzberg",null],["094745422175","Wiesenthau",null],["094745423143","Kirchehrenbach",null],["094745423147","Leutenbach",null],["094745423171","Weilersbach",null],["094745425122","Effeltrich",null],["094745425160","Poxdorf",null],["094745426119","Dormitz",null],["094745426137","Hetzles",null],["094745426144","Kleinsendelbach",null],["094745427132","Gräfenberg, St",null],["094745427138","Hiltpoltstein, M",null],["094745427173","Weißenohe",null],["094750112112","Bad Steben, M",null],["094750113113","Berg",null],["094750120120","Döhlau",null],["094750128128","Geroldsgrün",null],["094750136136","Helmbrechts, St",null],["094750141141","Köditz",null],["094750142142","Konradsreuth",null],["094750154154","Münchberg, St",null],["094750156156","Naila, St",null],["094750158158","Oberkotzau, M",null],["094750161161","Regnitzlosau",null],["094750162162","Rehau, St",null],["094750168168","Schwarzenbach a.d.Saale, St",null],["094750169169","Schwarzenbach a.Wald, St",null],["094750171171","Selbitz, St",null],["094750175175","Stammbach, M",null],["094750189189","Zell im Fichtelgebirge, M",null],["094755428137","Issigau",null],["094755428146","Lichtenberg, St",null],["094755430123","Feilitzsch",null],["094755430127","Gattendorf",null],["094755430181","Töpen",null],["094755430182","Trogen",null],["094755431145","Leupoldsgrün",null],["094755431165","Schauenstein, St",null],["094755432174","Sparneck, M",null],["094755432184","Weißdorf",null],["094759451451","Forst Schwarzenbach a.Wald",null],["094759452452","Gerlaser Forst",null],["094759453453","Geroldsgrüner Forst",null],["094759454454","Martinlamitzer Forst-Nord",null],["094760145145","Kronach, St",null],["094760146146","Küps, M",null],["094760152152","Ludwigsstadt, St",null],["094760159159","Nordhalben, M",null],["094760164164","Pressig, M",null],["094760175175","Steinbach a.Wald",null],["094760177177","Steinwiesen, M",null],["094760178178","Stockheim",null],["094760179179","Tettau, M",null],["094760183183","Marktrodach, M",null],["094760184184","Wallenfels, St",null],["094760185185","Weißenbrunn",null],["094760189189","Wilhelmsthal",null],["094765433166","Reichenbach",null],["094765433180","Teuschnitz, St",null],["094765433182","Tschirn",null],["094765434154","Mitwitz, M",null],["094765434171","Schneckenlohe",null],["094769451451","Birnbaum",null],["094769453453","Langenbacher Forst",null],["094770121121","Himmelkron",null],["094770128128","Kulmbach, GKSt",null],["094770136136","Mainleus, M",null],["094770139139","Marktschorgast, M",null],["094770142142","Neudrossenfeld",null],["094770143143","Neuenmarkt",null],["094770148148","Presseck, M",null],["094770157157","Thurnau, M",null],["094770163163","Wirsberg, M",null],["094775435151","Rugendorf",null],["094775435156","Stadtsteinach, St",null],["094775436117","Grafengehaig, M",null],["094775436138","Marktleugast, M",null],["094775437118","Guttenberg",null],["094775437129","Kupferberg, St",null],["094775437135","Ludwigschorgast, M",null],["094775437159","Untersteinach",null],["094775438124","Kasendorf, M",null],["094775438164","Wonsees, M",null],["094775439119","Harsdorf",null],["094775439127","Ködnitz",null],["094775439158","Trebgast",null],["094780111111","Altenkunstadt",null],["094780116116","Burgkunstadt, St",null],["094780120120","Ebensfeld, M",null],["094780139139","Lichtenfels, St",null],["094780145145","Michelau i.OFr.",null],["094780165165","Bad Staffelstein, St",null],["094780176176","Weismain, St",null],["094785441143","Marktgraitz, M",null],["094785441155","Redwitz a.d.Rodach",null],["094785446127","Hochstadt a.Main",null],["094785446144","Marktzeuln, M",null],["094789451451","Breitengüßbacher Forst",null],["094789453453","Neuensorger Forst",null],["094790112112","Arzberg, St",null],["094790129129","Kirchenlamitz, St",null],["094790135135","Marktleuthen, St",null],["094790136136","Marktredwitz, GKSt",null],["094790145145","Röslau",null],["094790150150","Schönwald, St",null],["094790152152","Selb, GKSt",null],["094790166166","Weißenstadt, St",null],["094790169169","Wunsiedel, St",null],["094795442126","Höchstädt i.Fichtelgebirge",null],["094795442158","Thiersheim, M",null],["094795442159","Thierstein, M",null],["094795443127","Hohenberg a.d.Eger, St",null],["094795443147","Schirnding, M",null],["094795444111","Bad Alexandersbad",null],["094795444138","Nagel",null],["094795444161","Tröstau",null],["094799453453","Kaiserhammer Forst-Ost",null],["094799455455","Martinlamitzer Forst-Süd",null],["094799456456","Meierhöfer Seite",null],["094799457457","Neubauer Forst-Süd",null],["094799459459","Tröstauer Forst-Ost",null],["094799460460","Tröstauer Forst-West",null],["094799461461","Vordorfer Forst",null],["094799462462","Weißenstadter Forst-Nord",null],["094799463463","Weißenstadter Forst-Süd",null],["095610000000","Ansbach",null],["095620000000","Erlangen",null],["095630000000","Fürth",null],["095640000000","Nürnberg",null],["095650000000","Schwabach",null],["095710113113","Arberg, M",null],["095710114114","Aurach",null],["095710115115","Bechhofen, M",null],["095710127127","Burgoberbach",null],["095710130130","Colmberg, M",null],["095710135135","Dietenhofen, M",null],["095710136136","Dinkelsbühl, GKSt",null],["095710139139","Dürrwangen, M",null],["095710145145","Feuchtwangen, St",null],["095710146146","Flachslanden, M",null],["095710165165","Heilsbronn, St",null],["095710166166","Herrieden, St",null],["095710170170","Langfurth",null],["095710171171","Lehrberg, M",null],["095710174174","Leutershausen, St",null],["095710175175","Lichtenau, M",null],["095710177177","Merkendorf, St",null],["095710180180","Neuendettelsau",null],["095710183183","Oberdachstetten",null],["095710190190","Petersaurach",null],["095710193193","Rothenburg ob der Tauber, GKSt",null],["095710196196","Sachsen b.Ansbach",null],["095710199199","Schnelldorf",null],["095710200200","Schopfloch, M",null],["095710214214","Wassertrüdingen, St",null],["095710226226","Windsbach, St",null],["095715501111","Adelshofen",null],["095715501152","Gebsattel",null],["095715501155","Geslau",null],["095715501169","Insingen",null],["095715501181","Neusitz",null],["095715501188","Ohrenbach",null],["095715501205","Steinsfeld",null],["095715501225","Windelsbach",null],["095715502125","Buch a.Wald",null],["095715502134","Diebach",null],["095715502137","Dombühl, M",null],["095715502198","Schillingsfürst, St",null],["095715502222","Wettringen",null],["095715502228","Wörnitz",null],["095715504122","Bruckberg",null],["095715504194","Rügland",null],["095715504217","Weihenzell",null],["095715506189","Ornbau, St",null],["095715506216","Weidenbach, M",null],["095715507128","Burk",null],["095715507132","Dentlein a.Forst, M",null],["095715507223","Wieseth",null],["095715508179","Mönchsroth",null],["095715508218","Weiltingen, M",null],["095715508224","Wilburgstetten",null],["095715509141","Ehingen",null],["095715509154","Gerolfingen",null],["095715509192","Röckingen",null],["095715509208","Unterschwaningen",null],["095715509227","Wittelshofen",null],["095715538178","Mitteleschenbach",null],["095715538229","Wolframs-Eschenbach, St",null],["095719451451","Unterer Wald",null],["095720111111","Adelsdorf",null],["095720115115","Baiersdorf, St",null],["095720119119","Bubenreuth",null],["095720121121","Eckental, M",null],["095720130130","Hemhofen",null],["095720131131","Heroldsberg, M",null],["095720132132","Herzogenaurach, St",null],["095720135135","Höchstadt a.d.Aisch, St",null],["095720137137","Kalchreuth",null],["095720142142","Möhrendorf",null],["095720149149","Röttenbach",null],["095720160160","Wachenroth, M",null],["095720164164","Weisendorf, M",null],["095725510126","Gremsdorf",null],["095725510139","Lonnerstadt, M",null],["095725510143","Mühlhausen, M",null],["095725510159","Vestenbergsgreuth, M",null],["095725512114","Aurachtal",null],["095725512147","Oberreichenbach",null],["095725514120","Buckenhof",null],["095725514141","Marloffstein",null],["095725514154","Spardorf",null],["095725514158","Uttenreuth",null],["095725539127","Großenseebach",null],["095725539133","Heßdorf",null],["095729451451","Birkach",null],["095729452452","Buckenhofer Forst",null],["095729453453","Dormitzer Forst",null],["095729454454","Erlenstegener Forst",null],["095729455455","Forst Tennenlohe",null],["095729456456","Geschaidt",null],["095729457457","Kalchreuther Forst",null],["095729458458","Kraftshofer Forst",null],["095729459459","Mark",null],["095729460460","Neunhofer Forst",null],["095730111111","Ammerndorf, M",null],["095730114114","Cadolzburg, M",null],["095730115115","Großhabersdorf",null],["095730120120","Langenzenn, St",null],["095730122122","Oberasbach, St",null],["095730124124","Puschendorf",null],["095730125125","Roßtal, M",null],["095730127127","Stein, St",null],["095730133133","Wilhermsdorf, M",null],["095730134134","Zirndorf, St",null],["095735517126","Seukendorf",null],["095735517130","Veitsbronn",null],["095735540123","Obermichelbach",null],["095735540129","Tuchenbach",null],["095740112112","Altdorf b.Nürnberg, St",null],["095740117117","Burgthann",null],["095740123123","Feucht, M",null],["095740132132","Hersbruck, St",null],["095740135135","Kirchensittenbach",null],["095740138138","Lauf a.d.Pegnitz, St",null],["095740139139","Leinburg",null],["095740140140","Neuhaus a.d.Pegnitz, M",null],["095740141141","Neunkirchen a.Sand",null],["095740146146","Ottensoos",null],["095740147147","Pommelsbrunn",null],["095740150150","Reichenschwand",null],["095740152152","Röthenbach a.d.Pegnitz, St",null],["095740154154","Rückersdorf",null],["095740155155","Schnaittach, M",null],["095740156156","Schwaig b.Nürnberg",null],["095740157157","Schwarzenbruck",null],["095740158158","Simmelsdorf",null],["095740164164","Winkelhaid",null],["095745527129","Hartenstein",null],["095745527160","Velden, St",null],["095745527161","Vorra",null],["095745528111","Alfeld",null],["095745528128","Happurg",null],["095745529120","Engelthal",null],["095745529131","Henfenfeld",null],["095745529145","Offenhausen",null],["095749451451","Behringersdorfer Forst",null],["095749452452","Brunn",null],["095749453453","Engelthaler Forst",null],["095749454454","Feuchter Forst",null],["095749455455","Fischbach",null],["095749456456","Forsthof",null],["095749457457","Günthersbühler Forst",null],["095749458458","Haimendorfer Forst",null],["095749460460","Laufamholzer Forst",null],["095749461461","Leinburg",null],["095749462462","Rückersdorfer Forst",null],["095749463463","Schönberg",null],["095749464464","Winkelhaid",null],["095749465465","Zerzabelshofer Forst",null],["095750112112","Bad Windsheim, St",null],["095750116116","Burghaslach, M",null],["095750119119","Dietersheim",null],["095750121121","Emskirchen, M",null],["095750135135","Ipsheim, M",null],["095750145145","Markt Erlbach, M",null],["095750153153","Neustadt a.d.Aisch, St",null],["095750156156","Obernzenn, M",null],["095755518138","Langenfeld",null],["095755518144","Markt Bibart, M",null],["095755518147","Markt Taschendorf, M",null],["095755518157","Oberscheinfeld, M",null],["095755518161","Scheinfeld, St",null],["095755518165","Sugenheim, M",null],["095755519122","Ergersheim",null],["095755519127","Gollhofen",null],["095755519130","Hemmersheim",null],["095755519134","Ippesheim, M",null],["095755519146","Markt Nordheim, M",null],["095755519155","Oberickelsheim",null],["095755519163","Simmershofen",null],["095755519168","Uffenheim, St",null],["095755519179","Weigenheim",null],["095755520129","Hagenbüchach",null],["095755520181","Wilhelmsdorf",null],["095755521113","Baudenbach, M",null],["095755521118","Diespeck",null],["095755521128","Gutenstetten",null],["095755521150","Münchsteinach",null],["095755522117","Dachsbach, M",null],["095755522125","Gerhardshofen",null],["095755522167","Uehlfeld, M",null],["095755524115","Burgbernheim, St",null],["095755524124","Gallmersgarten",null],["095755524133","Illesheim",null],["095755524143","Marktbergel, M",null],["095755525152","Neuhof a.d.Zenn, M",null],["095755525166","Trautskirchen",null],["095759451451","Osing",null],["095760111111","Abenberg, St",null],["095760113113","Allersberg, M",null],["095760117117","Büchenbach",null],["095760121121","Georgensgmünd",null],["095760122122","Greding, St",null],["095760126126","Heideck, St",null],["095760127127","Hilpoltstein, St",null],["095760128128","Kammerstein",null],["095760132132","Schwanstetten, M",null],["095760137137","Rednitzhembach",null],["095760141141","Röttenbach",null],["095760142142","Rohr",null],["095760143143","Roth, St",null],["095760147147","Spalt, St",null],["095760148148","Thalmässing, M",null],["095760151151","Wendelstein, M",null],["095769451451","Abenberger Wald",null],["095769452452","Dechenwald",null],["095769453453","Forst Kleinschwarzenlohe",null],["095769454454","Heidenberg",null],["095769455455","Soos",null],["095770114114","Muhr a.See",null],["095770136136","Gunzenhausen, St",null],["095770148148","Langenaltheim",null],["095770158158","Pappenheim, St",null],["095770161161","Pleinfeld, M",null],["095770162162","Polsingen",null],["095770168168","Solnhofen",null],["095770173173","Treuchtlingen, St",null],["095770177177","Weißenburg i.Bay., GKSt",null],["095775532111","Absberg, M",null],["095775532138","Haundorf",null],["095775532159","Pfofeld",null],["095775532172","Theilenhofen",null],["095775533113","Alesheim",null],["095775533122","Dittenheim",null],["095775533149","Markt Berolzheim, M",null],["095775533150","Meinheim",null],["095775534125","Ellingen, St",null],["095775534127","Ettenstatt",null],["095775534141","Höttingen",null],["095775535115","Bergen",null],["095775535120","Burgsalach",null],["095775535151","Nennslingen, M",null],["095775535163","Raitenbuch",null],["095775536133","Gnotzheim, M",null],["095775536140","Heidenheim, M",null],["095775536179","Westheim",null],["096610000000","Aschaffenburg",null],["096620000000","Schweinfurt",null],["096630000000","Würzburg",null],["096710111111","Alzenau, St",null],["096710112112","Bessenbach",null],["096710114114","Karlstein a.Main",null],["096710119119","Geiselbach",null],["096710120120","Glattbach",null],["096710121121","Goldbach, M",null],["096710122122","Großostheim, M",null],["096710124124","Haibach",null],["096710130130","Hösbach, M",null],["096710133133","Johannesberg",null],["096710134134","Kahl a.Main",null],["096710136136","Kleinostheim",null],["096710139139","Laufach",null],["096710140140","Mainaschaff",null],["096710143143","Mömbris, M",null],["096710148148","Rothenbuch",null],["096710150150","Sailauf",null],["096710155155","Stockstadt a.Main, M",null],["096710156156","Waldaschaff",null],["096710157157","Weibersbrunn",null],["096715602126","Heigenbrücken",null],["096715602128","Heinrichsthal",null],["096715603127","Heimbuchenthal",null],["096715603141","Mespelbrunn",null],["096715603160","Dammbach",null],["096715604113","Blankenbach",null],["096715604135","Kleinkahl",null],["096715604138","Krombach",null],["096715604152","Schöllkrippen, M",null],["096715604153","Sommerkahl",null],["096715604159","Westerngrund",null],["096715604162","Wiesen",null],["096719451451","Forst Hain i.Spessart",null],["096719453453","Heinrichsthaler Forst",null],["096719456456","Rohrbrunner Forst",null],["096719457457","Rothenbucher Forst",null],["096719458458","Sailaufer Forst",null],["096719459459","Schöllkrippener Forst",null],["096719460460","Waldaschaffer Forst",null],["096719461461","Wiesener Forst",null],["096720112112","Bad Bocklet, M",null],["096720113113","Bad Brückenau, St",null],["096720114114","Bad Kissingen, GKSt",null],["096720117117","Burkardroth, M",null],["096720127127","Hammelburg, St",null],["096720134134","Motten",null],["096720135135","Münnerstadt, St",null],["096720136136","Nüdlingen",null],["096720139139","Oberthulba, M",null],["096720140140","Oerlenbach",null],["096720161161","Wartmannsroth",null],["096720163163","Wildflecken, M",null],["096720166166","Zeitlofs, M",null],["096725606126","Geroda, M",null],["096725606138","Oberleichtersbach",null],["096725606145","Riedenberg",null],["096725606149","Schondra, M",null],["096725607121","Elfershausen, M",null],["096725607124","Fuchsstadt",null],["096725608111","Aura a.d.Saale",null],["096725608122","Euerdorf, M",null],["096725608142","Ramsthal",null],["096725608155","Sulzthal, M",null],["096725609131","Maßbach, M",null],["096725609143","Rannungen",null],["096725609157","Thundorf i.UFr.",null],["096729451451","Dreistelzer Forst",null],["096729454454","Forst Detter-Süd",null],["096729455455","Geiersnest-Ost",null],["096729456456","Geiersnest-West",null],["096729457457","Großer Auersberg",null],["096729458458","Kälberberg",null],["096729461461","Mottener Forst-Süd",null],["096729462462","Neuwirtshauser Forst",null],["096729463463","Omerz u. Roter Berg",null],["096729464464","Römershager Forst-Nord",null],["096729465465","Römershager Forst-Ost",null],["096729466466","Roßbacher Forst",null],["096729468468","Waldfensterer Forst",null],["096730114114","Bad Neustadt a.d.Saale, St",null],["096730116116","Bastheim",null],["096730117117","Bischofsheim i.d.Rhön, St",null],["096730141141","Bad Königshofen i.Grabfeld, St",null],["096730149149","Oberelsbach, M",null],["096730162162","Sandberg",null],["096735633130","Hendungen",null],["096735633142","Mellrichstadt, St",null],["096735633151","Oberstreu",null],["096735633170","Stockheim",null],["096735634113","Aubstadt",null],["096735634126","Großbardorf",null],["096735634131","Herbstadt",null],["096735634134","Höchheim",null],["096735634172","Sulzdorf a.d.Lederhecke",null],["096735634173","Sulzfeld",null],["096735634174","Trappstadt, M",null],["096735635135","Hohenroth",null],["096735635146","Niederlauer",null],["096735635156","Rödelmaier",null],["096735635161","Salz",null],["096735635163","Schönau a.d.Brend",null],["096735635171","Strahlungen",null],["096735635186","Burglauer",null],["096735637123","Fladungen, St",null],["096735637129","Hausen",null],["096735637147","Nordheim v.d.Rhön",null],["096735638133","Heustreu",null],["096735638136","Hollstadt",null],["096735638175","Unsleben",null],["096735638183","Wollbach",null],["096735639153","Ostheim v.d.Rhön, St",null],["096735639167","Sondheim v.d.Rhön",null],["096735639182","Willmars",null],["096735640127","Großeibstadt",null],["096735640160","Saal a.d.Saale, M",null],["096735640184","Wülfershausen a.d.Saale",null],["096739451451","Bundorfer Forst",null],["096739452452","Burgwallbacher Forst",null],["096739453453","Forst Schmalwasser-Nord",null],["096739454454","Forst Schmalwasser-Süd",null],["096739455455","Mellrichstadter Forst",null],["096739456456","Steinacher Forst r.d.Saale",null],["096739457457","Sulzfelder Forst",null],["096739458458","Weigler",null],["096740133133","Eltmann, St",null],["096740147147","Haßfurt, St",null],["096740159159","Oberaurach",null],["096740163163","Knetzgau",null],["096740164164","Königsberg i.Bay., St",null],["096740171171","Maroldsweisach, M",null],["096740187187","Rauhenebrach",null],["096740195195","Sand a.Main",null],["096740210210","Untermerzbach",null],["096740221221","Zeil a.Main, St",null],["096745610118","Breitbrunn",null],["096745610129","Ebelsbach",null],["096745610160","Kirchlauter",null],["096745610201","Stettfeld",null],["096745611130","Ebern, St",null],["096745611184","Pfarrweisach",null],["096745611190","Rentweinsdorf, M",null],["096745612111","Aidhausen",null],["096745612120","Bundorf",null],["096745612121","Burgpreppach, M",null],["096745612149","Hofheim i.UFr., St",null],["096745612153","Riedbach",null],["096745612223","Ermershausen",null],["096745613139","Gädheim",null],["096745613180","Theres",null],["096745613219","Wonfurt",null],["096750117117","Dettelbach, St",null],["096750127127","Geiselwind, M",null],["096750141141","Kitzingen, GKSt",null],["096750144144","Mainbernheim, St",null],["096750158158","Prichsenstadt, St",null],["096750165165","Schwarzach a.Main, M",null],["096755614111","Abtswind, M",null],["096755614116","Castell",null],["096755614162","Rüdenhausen, M",null],["096755614178","Wiesentheid, M",null],["096755615131","Großlangheim, M",null],["096755615142","Kleinlangheim, M",null],["096755615177","Wiesenbronn",null],["096755616139","Iphofen, St",null],["096755616148","Markt Einersheim, M",null],["096755616161","Rödelsee",null],["096755616179","Willanzheim, M",null],["096755617112","Albertshofen",null],["096755617113","Biebelried",null],["096755617114","Buchbrunn",null],["096755617146","Mainstockheim",null],["096755617170","Sulzfeld a.Main",null],["096755618147","Marktbreit, St",null],["096755618149","Marktsteft, St",null],["096755618150","Martinsheim",null],["096755618156","Obernbreit, M",null],["096755618166","Segnitz",null],["096755618167","Seinsheim, M",null],["096755619155","Nordheim a.Main",null],["096755619169","Sommerach",null],["096755619174","Volkach, St",null],["096760112112","Amorbach, St",null],["096760117117","Collenberg",null],["096760118118","Dorfprozelten",null],["096760119119","Eichenbühl",null],["096760121121","Elsenfeld, M",null],["096760122122","Erlenbach a.Main, St",null],["096760123123","Eschau, M",null],["096760124124","Faulbach",null],["096760125125","Großheubach, M",null],["096760126126","Großwallstadt",null],["096760131131","Kirchzell, M",null],["096760134134","Klingenberg a.Main, St",null],["096760136136","Leidersbach",null],["096760139139","Miltenberg, St",null],["096760140140","Mömlingen",null],["096760144144","Niedernberg",null],["096760145145","Obernburg a.Main, St",null],["096760156156","Schneeberg, M",null],["096760160160","Sulzbach a.Main, M",null],["096760165165","Weilbach, M",null],["096760169169","Wörth a.Main, St",null],["096765626116","Bürgstadt, M",null],["096765626143","Neunkirchen",null],["096765627132","Kleinheubach, M",null],["096765627135","Laudenbach",null],["096765627153","Rüdenau",null],["096765630128","Hausen",null],["096765630133","Kleinwallstadt, M",null],["096765631141","Mönchberg, M",null],["096765631151","Röllbach",null],["096765632111","Altenbuch",null],["096765632158","Stadtprozelten, St",null],["096769452452","Forstwald",null],["096769455455","Hohe Wart",null],["096770114114","Arnstein, St",null],["096770127127","Eußenheim",null],["096770129129","Frammersbach, M",null],["096770131131","Gemünden a.Main, St",null],["096770148148","Karlstadt, St",null],["096770154154","Triefenstein, M",null],["096770155155","Lohr a.Main, St",null],["096770157157","Marktheidenfeld, St",null],["096770177177","Rieneck, St",null],["096775620137","Hasloch",null],["096775620151","Kreuzwertheim, M",null],["096775620182","Schollbrunn",null],["096775621119","Birkenfeld",null],["096775621120","Bischbrunn",null],["096775621125","Erlenbach b.Marktheidenfeld",null],["096775621126","Esselbach",null],["096775621135","Hafenlohr",null],["096775621146","Karbach, M",null],["096775621178","Roden",null],["096775621181","Rothenfels, St",null],["096775621193","Urspringen",null],["096775622116","Aura i.Sinngrund",null],["096775622122","Burgsinn, M",null],["096775622128","Fellen",null],["096775622159","Mittelsinn",null],["096775622169","Obersinn, M",null],["096775623132","Gössenheim",null],["096775623133","Gräfendorf",null],["096775623149","Karsbach",null],["096775624164","Neuendorf",null],["096775624166","Neustadt a.Main",null],["096775624172","Rechtenbach",null],["096775624186","Steinfeld",null],["096775625142","Himmelstadt",null],["096775625175","Retzstadt",null],["096775625189","Thüngen, M",null],["096775625203","Zellingen, M",null],["096775656165","Neuhütten",null],["096775656170","Partenstein",null],["096775656200","Wiesthal",null],["096779452452","Burgjoß",null],["096779453453","Forst Aura",null],["096779454454","Forst Lohrerstraße",null],["096779455455","Frammersbacher Forst",null],["096779456456","Fürstl. Löwenstein'scher Park",null],["096779457457","Haurain",null],["096779458458","Herrnwald",null],["096779459459","Langenprozeltener Forst",null],["096779461461","Partensteiner Forst",null],["096779463463","Ruppertshüttener Forst",null],["096780115115","Bergrheinfeld",null],["096780123123","Dittelbrunn",null],["096780128128","Euerbach",null],["096780132132","Geldersheim",null],["096780135135","Gochsheim",null],["096780136136","Grafenrheinfeld",null],["096780138138","Grettstadt",null],["096780150150","Kolitzheim",null],["096780160160","Niederwerrn",null],["096780168168","Poppenhausen",null],["096780170170","Röthlein",null],["096780174174","Schonungen",null],["096780176176","Schwebheim",null],["096780178178","Sennfeld",null],["096780181181","Stadtlauringen, M",null],["096780186186","Üchtelhausen",null],["096780190190","Waigolshausen",null],["096780192192","Wasserlosen",null],["096780193193","Werneck, M",null],["096785642122","Dingolshausen",null],["096785642124","Donnersdorf",null],["096785642130","Frankenwinheim",null],["096785642134","Gerolzhofen, St",null],["096785642153","Lülsfeld",null],["096785642157","Michelau i.Steigerwald",null],["096785642164","Oberschwarzach, M",null],["096785642183","Sulzheim",null],["096785643175","Schwanfeld",null],["096785643196","Wipfeld",null],["096789451451","Bürgerwald",null],["096789452452","Geiersberg",null],["096789453453","Hundelshausen",null],["096789454454","Nonnenkloster",null],["096789455455","Stollbergerforst",null],["096789456456","Vollburg",null],["096789457457","Wustvieler Forst",null],["096790126126","Eisingen",null],["096790134134","Gaukönigshofen",null],["096790136136","Gerbrunn",null],["096790142142","Güntersleben",null],["096790143143","Hausen b.Würzburg",null],["096790147147","Höchberg, M",null],["096790155155","Kleinrinderfeld",null],["096790156156","Kürnach",null],["096790164164","Neubrunn, M",null],["096790170170","Ochsenfurt, St",null],["096790175175","Randersacker, M",null],["096790176176","Reichenberg, M",null],["096790180180","Rimpar, M",null],["096790185185","Rottendorf",null],["096790193193","Theilheim",null],["096790194194","Thüngersheim",null],["096790200200","Leinach",null],["096790201201","Unterpleichfeld",null],["096790202202","Veitshöchheim",null],["096790204204","Waldbrunn",null],["096790205205","Waldbüttelbrunn",null],["096790209209","Zell a.Main, M",null],["096795644114","Aub, St",null],["096795644135","Gelchsheim, M",null],["096795644188","Sonderhofen",null],["096795645117","Bergtheim",null],["096795645169","Oberpleichfeld",null],["096795646124","Eibelstadt, St",null],["096795646131","Frickenhausen a.Main, M",null],["096795646187","Sommerhausen, M",null],["096795646206","Winterhausen, M",null],["096795647130","Estenfeld",null],["096795647167","Eisenheim, M",null],["096795647174","Prosselsheim",null],["096795648122","Bütthard, M",null],["096795648138","Giebelstadt, M",null],["096795649144","Helmstadt, M",null],["096795649149","Holzkirchen",null],["096795649177","Remlingen, M",null],["096795649196","Uettingen",null],["096795650137","Geroldshausen",null],["096795650153","Kirchheim",null],["096795651154","Kist",null],["096795651165","Altertheim",null],["096795652128","Erlabrunn",null],["096795652161","Margetshöchheim",null],["096795654118","Bieberehren",null],["096795654179","Riedenheim",null],["096795654182","Röttingen, St",null],["096795654192","Tauberrettersheim",null],["096795655141","Greußenheim",null],["096795655146","Hettstadt",null],["096799451451","Gramschatzer Wald",null],["096799452452","Guttenberger Wald",null],["096799453453","Irtenberger Wald",null],["097610000000","Augsburg",null],["097620000000","Kaufbeuren",null],["097630000000","Kempten (Allgäu)",null],["097640000000","Memmingen",null],["097710112112","Affing",null],["097710113113","Aichach, St",null],["097710130130","Friedberg, St",null],["097710140140","Hollenbach",null],["097710141141","Inchenhofen, M",null],["097710142142","Kissing",null],["097710145145","Merching",null],["097710158158","Rehling",null],["097710160160","Ried",null],["097715701114","Aindling, M",null],["097715701155","Petersdorf",null],["097715701169","Todtenweis",null],["097715703144","Kühbach, M",null],["097715703162","Schiltberg",null],["097715704111","Adelzhausen",null],["097715704122","Dasing",null],["097715704129","Eurasburg",null],["097715704149","Obergriesbach",null],["097715704165","Sielenbach",null],["097715705146","Mering, M",null],["097715705163","Schmiechen",null],["097715705168","Steindorf",null],["097715771156","Pöttmes, M",null],["097715771176","Baar (Schwaben)",null],["097720111111","Adelsried",null],["097720115115","Altenmünster",null],["097720117117","Aystetten",null],["097720121121","Biberbach, M",null],["097720125125","Bobingen, St",null],["097720130130","Diedorf, M",null],["097720131131","Dinkelscherben, M",null],["097720141141","Fischach, M",null],["097720145145","Gablingen",null],["097720147147","Gersthofen, St",null],["097720149149","Graben",null],["097720159159","Horgau",null],["097720163163","Königsbrunn, St",null],["097720167167","Kutzenhausen",null],["097720171171","Langweid a.Lech",null],["097720177177","Meitingen, M",null],["097720184184","Neusäß, St",null],["097720200200","Schwabmünchen, St",null],["097720202202","Stadtbergen, St",null],["097720207207","Thierhaupten, M",null],["097720215215","Wehringen",null],["097720223223","Zusmarshausen, M",null],["097725706114","Allmannshofen",null],["097725706134","Ehingen",null],["097725706136","Ellgau",null],["097725706166","Kühlenthal",null],["097725706185","Nordendorf",null],["097725706217","Westendorf",null],["097725707126","Bonstetten",null],["097725707137","Emersacker",null],["097725707156","Heretsried",null],["097725707216","Welden, M",null],["097725708148","Gessertshausen",null],["097725708211","Ustersbach",null],["097725709168","Langenneufnach",null],["097725709178","Mickhausen",null],["097725709179","Mittelneufnach",null],["097725709197","Scherstetten",null],["097725709214","Walkertshofen",null],["097725710151","Großaitingen",null],["097725710160","Kleinaitingen",null],["097725710186","Oberottmarshausen",null],["097725711162","Klosterlechfeld",null],["097725711209","Untermeitingen",null],["097725712157","Hiltenfingen",null],["097725712170","Langerringen",null],["097729451451","Schmellerforst",null],["097730117117","Bissingen, M",null],["097730122122","Buttenwiesen",null],["097730125125","Dillingen a.d.Donau, GKSt",null],["097730144144","Lauingen (Donau), St",null],["097735713113","Bächingen a.d.Brenz",null],["097735713136","Gundelfingen a.d.Donau, St",null],["097735713137","Haunsheim",null],["097735713153","Medlingen",null],["097735714112","Bachhagel",null],["097735714170","Syrgenstein",null],["097735714187","Zöschingen",null],["097735715147","Mödingen",null],["097735715183","Wittislingen, M",null],["097735715186","Ziertheim",null],["097735716119","Blindheim",null],["097735716139","Höchstädt a.d.Donau, St",null],["097735716146","Lutzingen",null],["097735716150","Finningen",null],["097735716164","Schwenningen",null],["097735718116","Binswangen",null],["097735718143","Laugna",null],["097735718179","Villenbach",null],["097735718182","Wertingen, St",null],["097735718188","Zusamaltheim",null],["097735719111","Aislingen, M",null],["097735719133","Glött",null],["097735719140","Holzheim",null],["097740116116","Ursberg",null],["097740119119","Bibertal",null],["097740121121","Burgau, St",null],["097740122122","Burtenbach, M",null],["097740135135","Günzburg, GKSt",null],["097740144144","Jettingen-Scheppach, M",null],["097740145145","Kammeltal",null],["097740150150","Krumbach (Schwaben), St",null],["097740155155","Leipheim, St",null],["097740162162","Neuburg a.d.Kammel, M",null],["097745727136","Gundremmingen",null],["097745727171","Offingen, M",null],["097745727174","Rettenbach",null],["097745728127","Dürrlauingen",null],["097745728140","Haldenwang",null],["097745728151","Landensberg",null],["097745728178","Röfingen",null],["097745728196","Winterbach",null],["097745729118","Bubesheim",null],["097745729148","Kötz",null],["097745730133","Ellzee",null],["097745730143","Ichenhausen, St",null],["097745730191","Waldstetten, M",null],["097745731111","Aletshausen",null],["097745731117","Breitenthal",null],["097745731124","Deisenhausen",null],["097745731129","Ebershausen",null],["097745731189","Wiesenbach",null],["097745731192","Waltenhausen",null],["097745732115","Balzhausen",null],["097745732160","Münsterhausen, M",null],["097745732185","Thannhausen, St",null],["097745733166","Aichen",null],["097745733198","Ziemetshausen, M",null],["097749451451","Ebershauser-Nattenhauser Wald",null],["097749452452","Winzerwald",null],["097750115115","Bellenberg",null],["097750129129","Illertissen, St",null],["097750134134","Nersingen",null],["097750135135","Neu-Ulm, GKSt",null],["097750139139","Elchingen",null],["097750149149","Roggenburg",null],["097750152152","Senden, St",null],["097750162162","Vöhringen, St",null],["097750164164","Weißenhorn, St",null],["097755739126","Holzheim",null],["097755739143","Pfaffenhofen a.d.Roth, M",null],["097755740111","Altenstadt, M",null],["097755740132","Kellmünz a.d.Iller, M",null],["097755740142","Osterberg",null],["097755741118","Buch, M",null],["097755741141","Oberroth",null],["097755741161","Unterroth",null],["097759451451","Auwald",null],["097759452452","Oberroggenburger Wald",null],["097759454454","Stoffenrieder Forst",null],["097759455455","Unterroggenburger Wald",null],["097760111111","Bodolz",null],["097760114114","Heimenkirch, M",null],["097760116116","Lindau (Bodensee), GKSt",null],["097760117117","Lindenberg i.Allgäu, St",null],["097760120120","Nonnenhorn",null],["097760122122","Opfenbach",null],["097760125125","Scheidegg, M",null],["097760128128","Wasserburg (Bodensee)",null],["097760129129","Weiler-Simmerberg, M",null],["097760131131","Hergatz",null],["097765735115","Hergensweiler",null],["097765735126","Sigmarszell",null],["097765735130","Weißensberg",null],["097765737112","Gestratz",null],["097765737113","Grünenbach",null],["097765737118","Maierhöfen",null],["097765737124","Röthenbach (Allgäu)",null],["097765738121","Oberreute",null],["097765738127","Stiefenhofen",null],["097770129129","Füssen, St",null],["097770130130","Germaringen",null],["097770147147","Lechbruck am See",null],["097770151151","Marktoberdorf, St",null],["097770152152","Mauerstetten",null],["097770153153","Nesselwang, M",null],["097770159159","Pfronten",null],["097770165165","Ronsberg, M",null],["097770169169","Schwangau",null],["097770173173","Halblech",null],["097775748121","Buchloe, St",null],["097775748140","Jengen",null],["097775748145","Lamerdingen",null],["097775748177","Waal, M",null],["097775749139","Irsee, M",null],["097775749158","Pforzen",null],["097775749164","Rieden",null],["097775751141","Kaltental, M",null],["097775751155","Oberostendorf",null],["097775751157","Osterzell",null],["097775751172","Stöttwang",null],["097775751182","Westendorf",null],["097775752111","Aitrang",null],["097775752112","Biessenhofen",null],["097775752118","Bidingen",null],["097775752167","Ruderatshofen",null],["097775753114","Baisweil",null],["097775753124","Eggenthal",null],["097775753128","Friesenried",null],["097775754138","Günzach",null],["097775754154","Obergünzburg, M",null],["097775754176","Untrasried",null],["097775755131","Görisried",null],["097775755144","Kraftisried",null],["097775755175","Unterthingau, M",null],["097775756125","Eisenberg",null],["097775756135","Hopferau",null],["097775756149","Lengenwang",null],["097775756168","Rückholz",null],["097775756170","Seeg",null],["097775756179","Wald",null],["097775770163","Rieden am Forggensee",null],["097775770166","Roßhaupten",null],["097775772171","Stötten a.Auerberg",null],["097775772183","Rettenbach a.Auerberg",null],["097780116116","Bad Wörishofen, St",null],["097780123123","Buxheim",null],["097780137137","Ettringen",null],["097780168168","Markt Rettenbach, M",null],["097780169169","Markt Wald, M",null],["097780173173","Mindelheim, St",null],["097780196196","Sontheim",null],["097780204204","Tussenhausen, M",null],["097785757119","Böhen",null],["097785757149","Hawangen",null],["097785757186","Ottobeuren, M",null],["097785758115","Babenhausen, M",null],["097785758130","Egg a.d.Günz",null],["097785758157","Kirchhaslach",null],["097785758184","Oberschönegg",null],["097785758217","Winterrieden",null],["097785758221","Kettershausen",null],["097785759121","Breitenbrunn",null],["097785759183","Oberrieden",null],["097785759187","Pfaffenhausen, M",null],["097785759190","Salgen",null],["097785760134","Eppishausen",null],["097785760158","Kirchheim i.Schw., M",null],["097785761120","Boos",null],["097785761139","Fellheim",null],["097785761150","Heimertingen",null],["097785761177","Niederrieden",null],["097785761188","Pleß",null],["097785762136","Erkheim, M",null],["097785762163","Lauben",null],["097785762180","Kammlach",null],["097785762214","Westerheim",null],["097785764111","Amberg",null],["097785764203","Türkheim, M",null],["097785764209","Rammingen",null],["097785764216","Wiedergeltingen",null],["097785765118","Benningen",null],["097785765151","Holzgünz",null],["097785765162","Lachen",null],["097785765171","Memmingerberg",null],["097785765202","Trunkelsberg",null],["097785765205","Ungerhausen",null],["097785766113","Apfeltrach",null],["097785766127","Dirlewang, M",null],["097785766199","Stetten",null],["097785766207","Unteregg",null],["097785767161","Kronburg",null],["097785767164","Lautrach",null],["097785767165","Legau, M",null],["097785768144","Bad Grönenbach, M",null],["097785768218","Wolfertschwenden",null],["097785768219","Woringen",null],["097789451451","Ungerhauser Wald",null],["097790115115","Asbach-Bäumenheim",null],["097790131131","Donauwörth, GKSt",null],["097790147147","Fremdingen",null],["097790155155","Harburg (Schwaben), St",null],["097790169169","Kaisheim, M",null],["097790178178","Marxheim",null],["097790181181","Mertingen",null],["097790185185","Möttingen",null],["097790194194","Nördlingen, GKSt",null],["097790196196","Oberndorf a.Lech",null],["097790218218","Tapfheim",null],["097795720176","Maihingen",null],["097795720177","Marktoffingen",null],["097795720224","Wallerstein, M",null],["097795721117","Auhausen",null],["097795721138","Ehingen a.Ries",null],["097795721154","Hainsfarth",null],["097795721180","Megesheim",null],["097795721188","Munningen",null],["097795721197","Oettingen i.Bay., St",null],["097795722111","Alerheim",null],["097795722112","Amerdingen",null],["097795722130","Deiningen",null],["097795722136","Ederheim",null],["097795722146","Forheim",null],["097795722162","Hohenaltheim",null],["097795722184","Mönchsdeggingen",null],["097795722203","Reimlingen",null],["097795722226","Wechingen",null],["097795723148","Fünfstetten",null],["097795723167","Huisheim",null],["097795723198","Otting",null],["097795723228","Wemding, St",null],["097795723231","Wolferstadt",null],["097795724126","Buchdorf",null],["097795724129","Daiting",null],["097795724186","Monheim, St",null],["097795724206","Rögling",null],["097795724217","Tagmersheim",null],["097795725149","Genderkingen",null],["097795725163","Holzheim",null],["097795725187","Münster",null],["097795725192","Niederschönenfeld",null],["097795725201","Rain, St",null],["097799452452","Dornstadt-Linkersbaindt",null],["097799453453","Esterholz",null],["097800112112","Altusried, M",null],["097800114114","Betzigau",null],["097800115115","Blaichach",null],["097800117117","Buchenberg, M",null],["097800118118","Burgberg i.Allgäu",null],["097800119119","Dietmannsried, M",null],["097800120120","Durach",null],["097800122122","Haldenwang",null],["097800123123","Bad Hindelang, M",null],["097800124124","Immenstadt i.Allgäu, St",null],["097800125125","Lauben",null],["097800128128","Oy-Mittelberg",null],["097800132132","Oberstaufen, M",null],["097800133133","Oberstdorf, M",null],["097800137137","Rettenberg",null],["097800139139","Sonthofen, St",null],["097800140140","Sulzberg, M",null],["097800143143","Waltenhofen",null],["097800145145","Wertach, M",null],["097800146146","Wiggensbach, M",null],["097800147147","Wildpoldsried",null],["097805742113","Balderschwang",null],["097805742116","Bolsterlang",null],["097805742121","Fischen i.Allgäu",null],["097805742131","Obermaiselstein",null],["097805742134","Ofterschwang",null],["097805745127","Missen-Wilhams",null],["097805745144","Weitnau, M",null],["097809451451","Kempter Wald",null],["100410100100","Saarbrücken, Landeshauptstadt",null],["100410511511","Friedrichsthal, Stadt",null],["100410512512","Großrosseln",null],["100410513513","Heusweiler",null],["100410514514","Kleinblittersdorf",null],["100410515515","Püttlingen, Stadt",null],["100410516516","Quierschied",null],["100410517517","Riegelsberg",null],["100410518518","Sulzbach/ Saar, Stadt",null],["100410519519","Völklingen, Stadt",null],["100420111111","Beckingen",null],["100420112112","Losheim am See",null],["100420113113","Merzig, Kreisstadt",null],["100420114114","Mettlach",null],["100420115115","Perl",null],["100420116116","Wadern, Stadt",null],["100420117117","Weiskirchen",null],["100429999999","Deutsch-luxemburgisches Hoheitsgebiet",null],["100430111111","Eppelborn",null],["100430112112","Illingen",null],["100430113113","Merchweiler",null],["100430114114","Neunkirchen, Kreisstadt",null],["100430115115","Ottweiler, Stadt",null],["100430116116","Schiffweiler",null],["100430117117","Spiesen-Elversberg",null],["100440111111","Dillingen/ Saar, Stadt",null],["100440112112","Lebach, Stadt",null],["100440113113","Nalbach",null],["100440114114","Rehlingen-Siersburg",null],["100440115115","Saarlouis, Kreisstadt",null],["100440116116","Saarwellingen",null],["100440117117","Schmelz",null],["100440118118","Schwalbach",null],["100440119119","Überherrn",null],["100440120120","Wadgassen",null],["100440121121","Wallerfangen",null],["100440122122","Bous",null],["100440123123","Ensdorf",null],["100450111111","Bexbach, Stadt",null],["100450112112","Blieskastel, Stadt",null],["100450113113","Gersheim",null],["100450114114","Homburg, Kreisstadt",null],["100450115115","Kirkel",null],["100450116116","Mandelbachtal",null],["100450117117","St. Ingbert, Stadt",null],["100460111111","Freisen",null],["100460112112","Marpingen",null],["100460113113","Namborn",null],["100460114114","Nohfelden",null],["100460115115","Nonnweiler",null],["100460116116","Oberthal",null],["100460117117","St. Wendel, Kreisstadt",null],["100460118118","Tholey",null],["110000000000","Berlin, Stadt",null],["110010001001","Mitte","Stadt-/Ortsteil bzw. Stadtbezirk"],["110020002002","Friedrichshain-Kreuzberg","Stadt-/Ortsteil bzw. Stadtbezirk"],["110030003003","Pankow","Stadt-/Ortsteil bzw. Stadtbezirk"],["110040004004","Charlottenburg-Wilmersdorf","Stadt-/Ortsteil bzw. Stadtbezirk"],["110050005005","Spandau","Stadt-/Ortsteil bzw. Stadtbezirk"],["110060006006","Steglitz-Zehlendorf","Stadt-/Ortsteil bzw. Stadtbezirk"],["110070007007","Tempelhof-Schöneberg","Stadt-/Ortsteil bzw. Stadtbezirk"],["110080008008","Neukölln","Stadt-/Ortsteil bzw. Stadtbezirk"],["110090009009","Treptow-Köpenick","Stadt-/Ortsteil bzw. Stadtbezirk"],["110100010010","Marzahn-Hellersdorf","Stadt-/Ortsteil bzw. Stadtbezirk"],["110110011011","Lichtenberg","Stadt-/Ortsteil bzw. Stadtbezirk"],["110120012012","Reinickendorf","Stadt-/Ortsteil bzw. Stadtbezirk"],["120510000000","Brandenburg an der Havel, Stadt",null],["120520000000","Cottbus/Chóśebuz, Stadt",null],["120530000000","Frankfurt (Oder), Stadt",null],["120540000000","Potsdam, Stadt",null],["120600005005","Ahrensfelde",null],["120600020020","Bernau bei Berlin, Stadt",null],["120600052052","Eberswalde, Stadt",null],["120600181181","Panketal",null],["120600198198","Schorfheide",null],["120600269269","Wandlitz",null],["120600280280","Werneuchen, Stadt",null],["120605003024","Biesenthal, Stadt",null],["120605003034","Breydin",null],["120605003154","Marienwerder",null],["120605003161","Melchow",null],["120605003192","Rüdnitz",null],["120605003250","Sydower Fließ",null],["120605006012","Althüttendorf",null],["120605006068","Friedrichswalde",null],["120605006100","Joachimsthal, Stadt",null],["120605006296","Ziethen",null],["120605011036","Britz",null],["120605011045","Chorin",null],["120605011092","Hohenfinow",null],["120605011128","Liepe",null],["120605011149","Lunow-Stolzenhagen",null],["120605011172","Niederfinow",null],["120605011176","Oderberg, Stadt",null],["120605011185","Parsteinsee",null],["120610020020","Bestensee",null],["120610112112","Eichwalde",null],["120610217217","Heidesee",null],["120610219219","Heideblick",null],["120610260260","Königs Wusterhausen, Stadt",null],["120610316316","Lübben (Spreewald) / Lubin (Błota), Stadt",null],["120610320320","Luckau, Stadt",null],["120610329329","Märkische Heide/Markojska Góla",null],["120610332332","Mittenwalde, Stadt",null],["120610433433","Schönefeld",null],["120610444444","Schulzendorf",null],["120610540540","Wildau, Stadt",null],["120610572572","Zeuthen",null],["120615108192","Groß Köris",null],["120615108216","Halbe",null],["120615108328","Märkisch Buchholz, Stadt",null],["120615108344","Münchehofe",null],["120615108448","Schwerin",null],["120615108492","Teupitz, Stadt",null],["120615113005","Alt Zauche-Wußwerk/Stara Niwa-Wózwjerch",null],["120615113061","Byhleguhre-Byhlen/Beła Góra-Bělin",null],["120615113224","Jamlitz",null],["120615113308","Lieberose, Stadt",null],["120615113352","Neu Zauche/Nowa Niwa",null],["120615113450","Schwielochsee/Gójacki Jazor",null],["120615113470","Spreewaldheide/Błośańska Góla",null],["120615113476","Straupitz (Spreewald)/Tšupc (Błota)",null],["120615114017","Bersteland",null],["120615114097","Drahnsdorf",null],["120615114164","Golßen, Stadt",null],["120615114244","Kasel-Golzig",null],["120615114265","Krausnick-Groß Wasserburg",null],["120615114405","Rietzneuendorf-Staakow",null],["120615114428","Schlepzig/Słopišća",null],["120615114435","Schönwald",null],["120615114471","Steinreich",null],["120615114510","Unterspreewald",null],["120620092092","Doberlug-Kirchhain, Stadt",null],["120620124124","Elsterwerda, Stadt",null],["120620140140","Finsterwalde, Stadt",null],["120620224224","Herzberg (Elster), Stadt",null],["120620410410","Röderland",null],["120620461461","Schönewalde, Stadt",null],["120620469469","Sonnewalde, Stadt",null],["120625031024","Bad Liebenwerda, Stadt",null],["120625031128","Falkenberg/Elster, Stadt",null],["120625031341","Mühlberg/Elbe, Stadt",null],["120625031500","Uebigau-Wahrenbrück, Stadt",null],["120625202219","Heideland",null],["120625202417","Rückersdorf",null],["120625202440","Schilda",null],["120625202453","Schönborn",null],["120625202492","Tröbitz",null],["120625205088","Crinitz",null],["120625205293","Lichterfeld-Schacksdorf",null],["120625205333","Massen-Niederlausitz",null],["120625205425","Sallgast",null],["120625207177","Gorden-Staupitz",null],["120625207240","Hohenleipisch",null],["120625207372","Plessa",null],["120625207464","Schraden",null],["120625209134","Fichtwald",null],["120625209237","Hohenbucko",null],["120625209282","Kremitzaue",null],["120625209289","Lebusa",null],["120625209445","Schlieben, Stadt",null],["120625211196","Gröden",null],["120625211208","Großthiemig",null],["120625211232","Hirschfeld",null],["120625211336","Merzdorf",null],["120630036036","Brieselang",null],["120630056056","Dallgow-Döberitz",null],["120630080080","Falkensee, Stadt",null],["120630148148","Ketzin/Havel, Stadt",null],["120630189189","Milower Land",null],["120630208208","Nauen, Stadt",null],["120630244244","Premnitz, Stadt",null],["120630252252","Rathenow, Stadt",null],["120630273273","Schönwalde-Glien",null],["120630357357","Wustermark",null],["120635302088","Friesack, Stadt",null],["120635302142","Wiesenaue",null],["120635302202","Mühlenberge",null],["120635302228","Paulinenaue",null],["120635302240","Pessin",null],["120635302256","Retzow",null],["120635306165","Kotzen",null],["120635306186","Märkisch Luch",null],["120635306212","Nennhausen",null],["120635306293","Stechow-Ferchesar",null],["120635309094","Gollenberg",null],["120635309112","Großderschau",null],["120635309134","Havelaue",null],["120635309161","Kleßen-Görne",null],["120635309260","Rhinow, Stadt",null],["120635309274","Seeblick",null],["120640029029","Altlandsberg, Stadt",null],["120640044044","Bad Freienwalde (Oder), Stadt",null],["120640136136","Fredersdorf-Vogelsdorf",null],["120640227227","Hoppegarten",null],["120640274274","Letschin",null],["120640317317","Müncheberg, Stadt",null],["120640336336","Neuenhagen bei Berlin",null],["120640380380","Petershagen/Eggersdorf",null],["120640428428","Rüdersdorf bei Berlin",null],["120640448448","Seelow, Stadt",null],["120640472472","Strausberg, Stadt",null],["120640512512","Wriezen, Stadt",null],["120645403053","Beiersdorf-Freudenberg",null],["120645403125","Falkenberg",null],["120645403205","Heckelberg-Brunow",null],["120645403222","Höhenland",null],["120645404009","Alt Tucheband",null],["120645404057","Bleyen-Genschmar",null],["120645404172","Golzow",null],["120645404266","Küstriner Vorland",null],["120645404538","Zechin",null],["120645406268","Lebus, Stadt",null],["120645406388","Podelzig",null],["120645406420","Reitwein",null],["120645406480","Treplin",null],["120645406539","Zeschdorf",null],["120645408084","Buckow (Märkische Schweiz), Stadt",null],["120645408153","Garzau-Garzin",null],["120645408370","Oberbarnim",null],["120645408408","Rehfelde",null],["120645408484","Waldsieversdorf",null],["120645410190","Gusow-Platkow",null],["120645410303","Märkische Höhe",null],["120645410340","Neuhardenberg",null],["120645412128","Falkenhagen (Mark)",null],["120645412130","Fichtenhöhe",null],["120645412288","Lietzen",null],["120645412290","Lindendorf",null],["120645412482","Vierlinden",null],["120645414061","Bliesdorf",null],["120645414349","Neulewin",null],["120645414365","Neutrebbin",null],["120645414371","Oderaue",null],["120645414393","Prötzel",null],["120645414417","Reichenow-Möglin",null],["120650036036","Birkenwerder",null],["120650084084","Fürstenberg/Havel, Stadt",null],["120650096096","Glienicke/Nordbahn",null],["120650136136","Hennigsdorf, Stadt",null],["120650144144","Hohen Neuendorf, Stadt",null],["120650165165","Kremmen, Stadt",null],["120650180180","Leegebruch",null],["120650193193","Liebenwalde, Stadt",null],["120650198198","Löwenberger Land",null],["120650225225","Mühlenbecker Land",null],["120650251251","Oberkrämer",null],["120650256256","Oranienburg, Stadt",null],["120650332332","Velten, Stadt",null],["120650356356","Zehdenick, Stadt",null],["120655502100","Gransee, Stadt",null],["120655502117","Großwoltersdorf",null],["120655502276","Schönermark",null],["120655502301","Sonnenberg",null],["120655502310","Stechlin",null],["120660052052","Calau/Kalawa, Stadt",null],["120660112112","Großräschen/Rań, Stadt",null],["120660176176","Lauchhammer, Stadt",null],["120660196196","Lübbenau/Spreewald / Lubnjow/Błota, Stadt",null],["120660285285","Schipkau",null],["120660296296","Schwarzheide, Stadt",null],["120660304304","Senftenberg/Zły Komorow, Stadt",null],["120660320320","Vetschau/Spreewald / Wětošow/Błota, Stadt",null],["120665601008","Altdöbern",null],["120665601041","Bronkow",null],["120665601202","Luckaitztal",null],["120665601226","Neu-Seeland/Nowa Jazorina",null],["120665601228","Neupetershain/Nowe Wiki",null],["120665606064","Frauendorf",null],["120665606104","Großkmehlen",null],["120665606168","Kroppen",null],["120665606188","Lindenau",null],["120665606240","Ortrand, Stadt",null],["120665606316","Tettau",null],["120665607116","Grünewald",null],["120665607120","Guteborn",null],["120665607124","Hermsdorf",null],["120665607132","Hohenbocka",null],["120665607272","Ruhland, Stadt",null],["120665607292","Schwarzbach",null],["120670036036","Beeskow, Stadt",null],["120670120120","Eisenhüttenstadt, Stadt",null],["120670124124","Erkner, Stadt",null],["120670137137","Friedland, Stadt",null],["120670144144","Fürstenwalde/Spree, Stadt",null],["120670201201","Grünheide (Mark)",null],["120670426426","Rietz-Neuendorf",null],["120670440440","Schöneiche bei Berlin",null],["120670481481","Storkow (Mark), Stadt",null],["120670493493","Tauche",null],["120670544544","Woltersdorf",null],["120675701076","Brieskow-Finkenheerd",null],["120675701180","Groß Lindow",null],["120675701508","Vogelsang",null],["120675701528","Wiesenau",null],["120675701552","Ziltendorf",null],["120675705292","Lawitz",null],["120675705338","Neißemünde",null],["120675705357","Neuzelle",null],["120675706040","Berkenbrück",null],["120675706072","Briesen (Mark)",null],["120675706237","Jacobsdorf",null],["120675706473","Steinhöfel",null],["120675707024","Bad Saarow",null],["120675707112","Diensdorf-Radlow",null],["120675707288","Langewahl",null],["120675707413","Reichenwalde",null],["120675707520","Wendisch Rietz",null],["120675708205","Grunow-Dammendorf",null],["120675708324","Mixdorf",null],["120675708336","Müllrose, Stadt",null],["120675708397","Ragow-Merz",null],["120675708438","Schlaubetal",null],["120675708458","Siehdichum",null],["120675709173","Gosen-Neu Zittau",null],["120675709408","Rauen",null],["120675709469","Spreenhagen",null],["120680117117","Fehrbellin",null],["120680181181","Heiligengrabe",null],["120680264264","Kyritz, Stadt",null],["120680320320","Neuruppin, Stadt",null],["120680353353","Rheinsberg, Stadt",null],["120680468468","Wittstock/Dosse, Stadt",null],["120680477477","Wusterhausen/Dosse",null],["120685804188","Herzberg (Mark)",null],["120685804280","Lindow (Mark), Stadt",null],["120685804372","Rüthnick",null],["120685804437","Vielitzsee",null],["120685805052","Breddin",null],["120685805109","Dreetz",null],["120685805324","Neustadt (Dosse), Stadt",null],["120685805409","Sieversdorf-Hohenofen",null],["120685805417","Stüdenitz-Schönermark",null],["120685805501","Zernitz-Lohm",null],["120685807072","Dabergotz",null],["120685807306","Märkisch Linden",null],["120685807413","Storbeck-Frankendorf",null],["120685807425","Temnitzquell",null],["120685807426","Temnitztal",null],["120685807452","Walsleben",null],["120690017017","Beelitz, Stadt",null],["120690020020","Bad Belzig, Stadt",null],["120690249249","Groß Kreutz (Havel)",null],["120690304304","Kleinmachnow",null],["120690306306","Kloster Lehnin",null],["120690397397","Michendorf",null],["120690454454","Nuthetal",null],["120690590590","Schwielowsee",null],["120690596596","Seddiner See",null],["120690604604","Stahnsdorf",null],["120690616616","Teltow, Stadt",null],["120690632632","Treuenbrietzen, Stadt",null],["120690656656","Werder (Havel), Stadt",null],["120690665665","Wiesenburg/Mark",null],["120695902018","Beetzsee",null],["120695902019","Beetzseeheide",null],["120695902270","Havelsee, Stadt",null],["120695902460","Päwesin",null],["120695902541","Roskow",null],["120695904052","Borkheide",null],["120695904056","Borkwalde",null],["120695904076","Brück, Stadt",null],["120695904216","Golzow",null],["120695904345","Linthe",null],["120695904470","Planebruch",null],["120695910402","Mühlenfließ",null],["120695910448","Niemegk, Stadt",null],["120695910474","Planetal",null],["120695910485","Rabenstein/Fläming",null],["120695917028","Bensdorf",null],["120695917537","Rosenau",null],["120695917688","Wusterwitz",null],["120695918089","Buckautal",null],["120695918224","Görzke",null],["120695918232","Gräben",null],["120695918648","Wenzlow",null],["120695918680","Wollin",null],["120695918696","Ziesar, Stadt",null],["120700125125","Groß Pankow (Prignitz)",null],["120700149149","Gumtow",null],["120700173173","Karstädt",null],["120700296296","Perleberg, Stadt",null],["120700302302","Plattenburg",null],["120700316316","Pritzwalk, Stadt",null],["120700424424","Wittenberge, Stadt",null],["120705001008","Bad Wilsnack, Stadt",null],["120705001052","Breese",null],["120705001241","Legde/Quitzöbel",null],["120705001348","Rühstädt",null],["120705001416","Weisen",null],["120705005060","Cumlosen",null],["120705005236","Lanz",null],["120705005244","Lenzen (Elbe), Stadt",null],["120705005246","Lenzerwische",null],["120705006096","Gerdshagen",null],["120705006153","Halenbeck-Rohlsdorf",null],["120705006222","Kümmernitztal",null],["120705006266","Marienfließ",null],["120705006280","Meyenburg, Stadt",null],["120705009028","Berge",null],["120705009145","Gülitz-Reetz",null],["120705009300","Pirow",null],["120705009325","Putlitz, Stadt",null],["120705009393","Triglitz",null],["120710057057","Drebkau/Drjowk, Stadt",null],["120710076076","Forst (Lausitz)/Baršć (Łužyca), Stadt",null],["120710160160","Guben, Stadt",null],["120710244244","Kolkwitz/Gołkojce",null],["120710301301","Neuhausen/Spree / Kopańce/Sprjewja",null],["120710337337","Schenkendöbern/Derbno",null],["120710372372","Spremberg/Grodk, Stadt",null],["120710408408","Welzow/Wjelcej, Stadt",null],["120715101028","Briesen/Brjazyna",null],["120715101032","Burg (Spreewald)/Bórkowy (Błota)",null],["120715101041","Dissen-Striesow/Dešno-Strjažow",null],["120715101164","Guhrow/Góry",null],["120715101341","Schmogrow-Fehrow/Smogorjow-Prjawoz",null],["120715101412","Werben/Wjerbno",null],["120715102044","Döbern/Derbno, Stadt",null],["120715102074","Felixsee/Feliksowy Jazor",null],["120715102153","Groß Schacksdorf-Simmersdorf",null],["120715102189","Jämlitz-Klein Düben",null],["120715102294","Neiße-Malxetal/Dolina Nysa-Małksa",null],["120715102392","Tschernitz/Cersk",null],["120715102414","Wiesengrund/Łukojce",null],["120715107052","Drachhausen/Hochoza",null],["120715107060","Drehnow/Drjenow",null],["120715107176","Heinersbrück/Móst",null],["120715107193","Jänschwalde/Janšojce",null],["120715107304","Peitz/Picnjo, Stadt",null],["120715107384","Tauer/Turjej",null],["120715107386","Teichland/Gatojce",null],["120715107401","Turnow-Preilack/Turnow-Pśiłuk",null],["120720002002","Am Mellensee",null],["120720014014","Baruth/Mark, Stadt",null],["120720017017","Blankenfelde-Mahlow",null],["120720120120","Großbeeren",null],["120720169169","Jüterbog, Stadt",null],["120720232232","Luckenwalde, Stadt",null],["120720240240","Ludwigsfelde, Stadt",null],["120720297297","Niedergörsdorf",null],["120720312312","Nuthe-Urstromtal",null],["120720340340","Rangsdorf",null],["120720426426","Trebbin, Stadt",null],["120720477477","Zossen, Stadt",null],["120725204053","Dahme/Mark, Stadt",null],["120725204055","Dahmetal",null],["120725204157","Ihlow",null],["120725204298","Niederer Fläming",null],["120730008008","Angermünde, Stadt",null],["120730069069","Boitzenburger Land",null],["120730384384","Lychen, Stadt",null],["120730429429","Nordwestuckermark",null],["120730452452","Prenzlau, Stadt",null],["120730532532","Schwedt/Oder, Stadt",null],["120730572572","Templin, Stadt",null],["120730579579","Uckerland",null],["120735303085","Brüssow, Stadt",null],["120735303093","Carmzow-Wallmow",null],["120735303216","Göritz",null],["120735303490","Schenkenberg",null],["120735303520","Schönfeld",null],["120735304097","Casekow",null],["120735304189","Gartz (Oder), Stadt",null],["120735304309","Hohenselchow-Groß Pinnow",null],["120735304393","Mescherin",null],["120735304565","Tantow",null],["120735305157","Flieth-Stegelitz",null],["120735305201","Gerswalde",null],["120735305396","Milmersdorf",null],["120735305404","Mittenwalde",null],["120735305569","Temmen-Ringenwalde",null],["120735306225","Gramzow",null],["120735306261","Grünow",null],["120735306430","Oberuckersee",null],["120735306458","Randowtal",null],["120735306578","Uckerfelde",null],["120735306645","Zichow",null],["120735310032","Berkholz-Meyenburg",null],["120735310386","Mark Landin",null],["120735310440","Pinnow",null],["120735310603","Passow",null],["130009999999","Küstengewässer einschl. Anteil am Festlandsockel",null],["130030000000","Rostock, Hanse- und Universitätsstadt",null],["130040000000","Schwerin, Landeshauptstadt",null],["130710027027","Dargun, Stadt",null],["130710029029","Demmin, Hansestadt",null],["130710033033","Feldberger Seenlandschaft",null],["130710107107","Neubrandenburg, Vier-Tore-Stadt",null],["130710110110","Neustrelitz, Residenzstadt",null],["130710156156","Waren (Müritz), Stadt",null],["130715151008","Beggerow",null],["130715151014","Borrentin",null],["130715151064","Hohenbollentin",null],["130715151065","Hohenmocker",null],["130715151072","Kentzlin",null],["130715151076","Kletzin",null],["130715151089","Lindenberg",null],["130715151096","Meesiger",null],["130715151112","Nossendorf",null],["130715151128","Sarow",null],["130715151131","Schönfeld",null],["130715151136","Siedenbrünzow",null],["130715151139","Sommersdorf",null],["130715151148","Utzedel",null],["130715151150","Verchen",null],["130715151157","Warrenzin",null],["130715152028","Datzetal",null],["130715152035","Friedland, Stadt",null],["130715152037","Galenbeck",null],["130715153007","Basedow",null],["130715153032","Faulenrost",null],["130715153039","Gielow",null],["130715153084","Kummerow",null],["130715153092","Malchin, Stadt",null],["130715153109","Neukalen, Peenestadt",null],["130715154001","Alt Schwerin",null],["130715154036","Fünfseen",null],["130715154043","Göhren-Lebbin",null],["130715154093","Malchow, Inselstadt",null],["130715154113","Nossentiner Hütte",null],["130715154114","Penkow",null],["130715154138","Silz",null],["130715154155","Walow",null],["130715154171","Zislow",null],["130715155099","Mirow, Stadt",null],["130715155119","Priepert",null],["130715155159","Wesenberg, Stadt",null],["130715155167","Wustrow",null],["130715156011","Blankensee",null],["130715156012","Blumenholz",null],["130715156025","Carpin",null],["130715156042","Godendorf",null],["130715156058","Grünow",null],["130715156066","Hohenzieritz",null],["130715156075","Klein Vielen",null],["130715156080","Kratzeburg",null],["130715156100","Möllenbeck",null],["130715156147","Userin",null],["130715156162","Wokuhl-Dabelow",null],["130715157009","Beseritz",null],["130715157010","Blankenhof",null],["130715157019","Brunn",null],["130715157104","Neddemin",null],["130715157108","Neuenkirchen",null],["130715157111","Neverin",null],["130715157140","Sponholz",null],["130715157141","Staven",null],["130715157145","Trollenhagen",null],["130715157161","Woggersin",null],["130715157166","Wulkenzin",null],["130715157170","Zirzow",null],["130715158005","Ankershagen, Schliemanngemeinde",null],["130715158101","Möllenhagen",null],["130715158115","Penzlin, Stadt",null],["130715158173","Kuckssee",null],["130715159003","Altenhof",null],["130715159013","Bollewick",null],["130715159020","Buchholz",null],["130715159023","Bütow",null],["130715159034","Fincken",null],["130715159045","Gotthun",null],["130715159053","Groß Kelle",null],["130715159073","Kieve",null],["130715159087","Lärz",null],["130715159088","Leizen",null],["130715159097","Melz",null],["130715159118","Priborn",null],["130715159122","Rechlin",null],["130715159124","Röbel/Müritz, Stadt",null],["130715159133","Schwarz",null],["130715159137","Sietow",null],["130715159143","Stuer",null],["130715159175","Eldetal",null],["130715159176","Südmüritz",null],["130715160047","Grabowhöfe",null],["130715160056","Groß Plasten",null],["130715160063","Hohen Wangelin",null],["130715160069","Jabel",null],["130715160071","Kargow",null],["130715160077","Klink",null],["130715160078","Klocksin",null],["130715160103","Moltzow",null],["130715160144","Torgelow am See",null],["130715160154","Vollrathsruhe",null],["130715160172","Peenehagen",null],["130715160174","Schloen-Dratow",null],["130715161021","Burg Stargard, Stadt",null],["130715161026","Cölpin",null],["130715161055","Groß Nemerow",null],["130715161067","Holldorf",null],["130715161090","Lindetal",null],["130715161117","Pragsdorf",null],["130715162015","Bredenfelde",null],["130715162018","Briggow",null],["130715162048","Grammentin",null],["130715162060","Gülzow",null],["130715162068","Ivenack",null],["130715162070","Jürgenstorf",null],["130715162074","Kittendorf",null],["130715162079","Knorrendorf",null],["130715162102","Mölln",null],["130715162123","Ritzerow",null],["130715162127","Rosenow",null],["130715162142","Stavenhagen, Reuterstadt, Stadt",null],["130715162169","Zettemin",null],["130715163002","Altenhagen",null],["130715163004","Altentreptow, Stadt",null],["130715163006","Bartow",null],["130715163016","Breesen",null],["130715163017","Breest",null],["130715163022","Burow",null],["130715163041","Gnevkow",null],["130715163044","Golchen",null],["130715163049","Grapzow",null],["130715163050","Grischow",null],["130715163057","Groß Teetzleben",null],["130715163059","Gültz",null],["130715163081","Kriesow",null],["130715163120","Pripsleben",null],["130715163125","Röckwitz",null],["130715163135","Siedenbollentin",null],["130715163146","Tützpatz",null],["130715163158","Werder",null],["130715163160","Wildberg",null],["130715163163","Wolde",null],["130715164054","Groß Miltzow",null],["130715164083","Kublank",null],["130715164105","Neetzka",null],["130715164130","Schönbeck",null],["130715164132","Schönhausen",null],["130715164153","Voigtsdorf",null],["130715164164","Woldegk, Windmühlenstadt",null],["130720006006","Bad Doberan, Stadt",null],["130720029029","Dummerstorf",null],["130720036036","Graal-Müritz, Ostseeheilbad",null],["130720043043","Güstrow, Barlachstadt",null],["130720058058","Kröpelin, Stadt",null],["130720060060","Kühlungsborn, Ostseebad, Stadt",null],["130720074074","Neubukow, Stadt",null],["130720091091","Sanitz",null],["130720093093","Satow",null],["130720106106","Teterow, Bergringstadt",null],["130725251001","Admannshagen-Bargeshagen",null],["130725251007","Bartenshagen-Parkentin",null],["130725251017","Börgerende-Rethwisch",null],["130725251047","Hohenfelde",null],["130725251075","Nienhagen, Ostseebad",null],["130725251083","Reddelich",null],["130725251086","Retschow",null],["130725251099","Steffenshagen",null],["130725251117","Wittenbeck",null],["130725252009","Baumgarten",null],["130725252013","Bernitt",null],["130725252020","Bützow, Stadt",null],["130725252028","Dreetz",null],["130725252050","Jürgenshagen",null],["130725252053","Klein Belitz",null],["130725252078","Penzin",null],["130725252089","Rühn",null],["130725252101","Steinhagen",null],["130725252104","Tarnow",null],["130725252114","Warnow",null],["130725252120","Zepelin",null],["130725253019","Broderstorf",null],["130725253081","Poppendorf",null],["130725253087","Roggentin",null],["130725253108","Thulendorf",null],["130725254004","Altkalen",null],["130725254010","Behren-Lübchin",null],["130725254031","Finkenthal",null],["130725254035","Gnoien, Warbelstadt",null],["130725254111","Walkendorf",null],["130725255033","Glasewitz",null],["130725255039","Groß Schwiesow",null],["130725255042","Gülzow-Prüzen",null],["130725255044","Gutow",null],["130725255055","Klein Upahl",null],["130725255061","Kuhs",null],["130725255067","Lohmen",null],["130725255069","Lüssow",null],["130725255071","Mistorf",null],["130725255073","Mühl Rosin",null],["130725255079","Plaaz",null],["130725255084","Reimershagen",null],["130725255092","Sarmstorf",null],["130725255119","Zehna",null],["130725256026","Dobbin-Linstow",null],["130725256048","Hoppenrade",null],["130725256056","Krakow am See, Stadt",null],["130725256059","Kuchelmiß",null],["130725256063","Lalendorf",null],["130725257027","Dolgen am See",null],["130725257046","Hohen Sprenz",null],["130725257062","Laage, Stadt",null],["130725257112","Wardow",null],["130725258003","Alt Sührkow",null],["130725258023","Dahmen",null],["130725258024","Dalkendorf",null],["130725258038","Groß Roge",null],["130725258040","Groß Wokern",null],["130725258041","Groß Wüstenfelde",null],["130725258045","Hohen Demzin",null],["130725258049","Jördenstorf",null],["130725258066","Lelkendorf",null],["130725258082","Prebberede",null],["130725258094","Schorssow",null],["130725258096","Schwasdorf",null],["130725258103","Sukow-Levitzow",null],["130725258109","Thürkow",null],["130725258113","Warnkenhagen",null],["130725259002","Alt Bukow",null],["130725259005","Am Salzhaff",null],["130725259008","Bastorf",null],["130725259014","Biendorf",null],["130725259022","Carinerland",null],["130725259085","Rerik, Ostseebad, Stadt",null],["130725260012","Bentwisch",null],["130725260015","Blankenhagen",null],["130725260032","Gelbensande",null],["130725260072","Mönchhagen",null],["130725260088","Rövershagen",null],["130725261011","Benitz",null],["130725261018","Bröbberow",null],["130725261051","Kassow",null],["130725261090","Rukieten",null],["130725261095","Schwaan, Stadt",null],["130725261110","Vorbeck",null],["130725261116","Wiendorf",null],["130725262021","Cammin",null],["130725262034","Gnewitz",null],["130725262037","Grammow",null],["130725262076","Nustrow",null],["130725262097","Selpin",null],["130725262102","Stubbendorf",null],["130725262105","Tessin, Stadt",null],["130725262107","Thelkow",null],["130725262118","Zarnewanz",null],["130725263030","Elmenhorst/Lichtenhagen",null],["130725263057","Kritzmow",null],["130725263064","Lambrechtshagen",null],["130725263077","Papendorf",null],["130725263080","Pölchow",null],["130725263098","Stäbelow",null],["130725263121","Ziesendorf",null],["130730011011","Binz, Ostseebad",null],["130730035035","Grimmen, Stadt",null],["130730055055","Marlow, Stadt",null],["130730070070","Putbus, Stadt",null],["130730080080","Sassnitz, Stadt",null],["130730088088","Stralsund, Hansestadt",null],["130730089089","Süderholz",null],["130730105105","Zingst, Ostseeheilbad",null],["130735351005","Altenpleen",null],["130735351037","Groß Mohrdorf",null],["130735351044","Klausdorf",null],["130735351046","Kramerhof",null],["130735351066","Preetz",null],["130735351068","Prohn",null],["130735352009","Barth, Stadt",null],["130735352018","Divitz-Spoldershagen",null],["130735352025","Fuhlendorf",null],["130735352042","Karnin",null],["130735352043","Kenz-Küstrow",null],["130735352051","Löbnitz",null],["130735352053","Lüdershagen",null],["130735352069","Pruchten",null],["130735352077","Saal",null],["130735352094","Trinwillershagen",null],["130735353010","Bergen auf Rügen, Stadt",null],["130735353014","Buschvitz",null],["130735353027","Garz/Rügen, Stadt",null],["130735353038","Gustow",null],["130735353049","Lietzow",null],["130735353063","Parchtitz",null],["130735353064","Patzig",null],["130735353065","Poseritz",null],["130735353072","Ralswiek",null],["130735353074","Rappin",null],["130735353083","Sehlen",null],["130735354002","Ahrenshoop, Ostseebad",null],["130735354012","Born a. Darß",null],["130735354017","Dierhagen, Ostseebad",null],["130735354067","Prerow, Ostseebad",null],["130735354100","Wieck a. Darß",null],["130735354103","Wustrow, Ostseebad",null],["130735355024","Franzburg, Stadt",null],["130735355029","Glewitz",null],["130735355034","Gremersdorf-Buchholz",null],["130735355057","Millienhagen-Oebelitz",null],["130735355062","Papenhagen",null],["130735355076","Richtenberg, Stadt",null],["130735355086","Splietsdorf",null],["130735355096","Velgast",null],["130735355097","Weitenhagen",null],["130735355098","Wendisch Baggendorf",null],["130735356023","Elmenhorst",null],["130735356090","Sundhagen",null],["130735356102","Wittenhagen",null],["130735357006","Baabe, Ostseebad",null],["130735357031","Göhren, Ostseebad",null],["130735357048","Lancken-Granitz",null],["130735357084","Sellin, Ostseebad",null],["130735357106","Zirkow",null],["130735357107","Mönchgut, Ostseebad",null],["130735358036","Groß Kordshagen",null],["130735358041","Jakobsdorf",null],["130735358054","Lüssow",null],["130735358060","Niepars",null],["130735358061","Pantelitz",null],["130735358087","Steinhagen",null],["130735358099","Wendorf",null],["130735358104","Zarrendorf",null],["130735359004","Altenkirchen",null],["130735359013","Breege",null],["130735359019","Dranske",null],["130735359030","Glowe",null],["130735359052","Lohme",null],["130735359071","Putgarten",null],["130735359078","Sagard",null],["130735359101","Wiek",null],["130735360007","Bad Sülze, Stadt",null],["130735360015","Dettmannsdorf",null],["130735360016","Deyelsdorf",null],["130735360020","Drechow",null],["130735360022","Eixen",null],["130735360032","Grammendorf",null],["130735360033","Gransebieth",null],["130735360039","Hugoldsdorf",null],["130735360050","Lindholz",null],["130735360093","Tribsees, Stadt",null],["130735361001","Ahrenshagen-Daskow",null],["130735361075","Ribnitz-Damgarten, Bernsteinstadt",null],["130735361082","Schlemmin",null],["130735361085","Semlow",null],["130735362003","Altefähr",null],["130735362021","Dreschvitz",null],["130735362028","Gingst",null],["130735362040","Insel Hiddensee, Seebad",null],["130735362045","Kluis",null],["130735362059","Neuenkirchen",null],["130735362073","Rambin",null],["130735362079","Samtens",null],["130735362081","Schaprode",null],["130735362092","Trent",null],["130735362095","Ummanz",null],["130740026026","Grevesmühlen, Stadt",null],["130740035035","Insel Poel, Ostseebad",null],["130740087087","Wismar, Hansestadt",null],["130745451002","Bad Kleinen",null],["130745451003","Barnekow",null],["130745451008","Bobitz",null],["130745451019","Dorf Mecklenburg",null],["130745451030","Groß Stieten",null],["130745451031","Hohen Viecheln",null],["130745451047","Lübow",null],["130745451053","Metelsdorf",null],["130745451082","Ventschow",null],["130745452020","Dragun",null],["130745452021","Gadebusch, Stadt",null],["130745452040","Kneese",null],["130745452043","Krembz",null],["130745452054","Mühlen Eichsen",null],["130745452068","Roggendorf",null],["130745452070","Rögnitz",null],["130745452081","Veelböken",null],["130745453005","Bernstorf",null],["130745453022","Gägelow",null],["130745453069","Roggenstorf",null],["130745453071","Rüting",null],["130745453077","Testorf-Steinfort",null],["130745453079","Upahl",null],["130745453085","Warnow",null],["130745453093","Stepenitztal",null],["130745454010","Boltenhagen, Ostseebad",null],["130745454016","Damshagen",null],["130745454032","Hohenkirchen",null],["130745454037","Kalkhorst",null],["130745454039","Klütz, Stadt",null],["130745454089","Zierow",null],["130745455001","Alt Meteln",null],["130745455012","Brüsewitz",null],["130745455014","Cramonshagen",null],["130745455015","Dalberg-Wendelstorf",null],["130745455024","Gottesgabe",null],["130745455025","Grambow",null],["130745455038","Klein Trebbow",null],["130745455048","Lübstorf",null],["130745455050","Lützow",null],["130745455061","Perlin",null],["130745455062","Pingelshagen",null],["130745455064","Pokrent",null],["130745455072","Schildetal",null],["130745455075","Seehof",null],["130745455088","Zickhusen",null],["130745456004","Benz",null],["130745456007","Blowatz",null],["130745456009","Boiensdorf",null],["130745456034","Hornstorf",null],["130745456044","Krusenhagen",null],["130745456056","Neuburg",null],["130745457006","Bibow",null],["130745457023","Glasin",null],["130745457036","Jesendorf",null],["130745457046","Lübberstorf",null],["130745457057","Neukloster, Stadt",null],["130745457060","Passee",null],["130745457084","Warin, Stadt",null],["130745457090","Zurow",null],["130745457091","Züsow",null],["130745458013","Carlow",null],["130745458018","Dechow",null],["130745458028","Groß Molzahn",null],["130745458033","Holdorf",null],["130745458042","Königsfeld",null],["130745458065","Rehna, Stadt",null],["130745458066","Rieps",null],["130745458073","Schlagsdorf",null],["130745458078","Thandorf",null],["130745458080","Utecht",null],["130745458092","Wedendorfersee",null],["130745459017","Dassow, Stadt",null],["130745459027","Grieben",null],["130745459049","Lüdersdorf",null],["130745459052","Menzendorf",null],["130745459067","Roduchelstorf",null],["130745459074","Schönberg, Stadt",null],["130745459076","Selmsdorf",null],["130745459094","Siemz-Niendorf",null],["130750005005","Anklam, Hansestadt",null],["130750039039","Greifswald, Universitäts- und Hansestadt",null],["130750049049","Heringsdorf, Ostseebad",null],["130750105105","Pasewalk, Stadt",null],["130750130130","Strasburg (Uckermark), Stadt",null],["130750136136","Ueckermünde, Seebad , Stadt",null],["130755551021","Buggenhagen",null],["130755551072","Krummin",null],["130755551074","Lassan, Stadt",null],["130755551087","Lütow",null],["130755551124","Sauzin",null],["130755551144","Wolgast, Stadt",null],["130755551147","Zemitz",null],["130755552001","Ahlbeck",null],["130755552003","Altwarp",null],["130755552031","Eggesin, Stadt",null],["130755552037","Grambin",null],["130755552051","Hintersee",null],["130755552075","Leopoldshagen",null],["130755552078","Liepgarten",null],["130755552084","Lübs",null],["130755552085","Luckow",null],["130755552089","Meiersberg",null],["130755552093","Mönkebude",null],["130755552139","Vogelsang-Warsin",null],["130755553007","Bargischow",null],["130755553013","Blesewitz",null],["130755553015","Boldekow",null],["130755553020","Bugewitz",null],["130755553022","Butzow",null],["130755553029","Ducherow",null],["130755553053","Iven",null],["130755553068","Krien",null],["130755553073","Krusenfelde",null],["130755553088","Medow",null],["130755553098","Neu Kosenow",null],["130755553101","Neuenkirchen",null],["130755553110","Postlow",null],["130755553116","Rossin",null],["130755553122","Sarnow",null],["130755553127","Spantekow",null],["130755553128","Stolpe an der Peene",null],["130755553155","Neetzow-Liepen",null],["130755554002","Alt Tellin",null],["130755554009","Bentzin",null],["130755554023","Daberkow",null],["130755554054","Jarmen, Stadt",null],["130755554070","Kruckow",null],["130755554134","Tutow",null],["130755554140","Völschow",null],["130755555008","Behrenhoff",null],["130755555025","Dargelin",null],["130755555027","Dersekow",null],["130755555050","Hinrichshagen",null],["130755555076","Levenhagen",null],["130755555091","Mesekenhagen",null],["130755555102","Neuenkirchen",null],["130755555141","Wackerow",null],["130755555142","Weitenhagen",null],["130755556011","Bergholz",null],["130755556012","Blankensee",null],["130755556016","Boock",null],["130755556035","Glasow",null],["130755556038","Grambow",null],["130755556067","Krackow",null],["130755556079","Löcknitz",null],["130755556095","Nadrensee",null],["130755556107","Penkun, Stadt",null],["130755556108","Plöwen",null],["130755556113","Ramin",null],["130755556117","Rossow",null],["130755556119","Rothenklempenow",null],["130755557018","Brünzow",null],["130755557046","Hanshagen",null],["130755557059","Katzow",null],["130755557060","Kemnitz",null],["130755557069","Kröslin",null],["130755557081","Loissin",null],["130755557083","Lubmin, Seebad",null],["130755557097","Neu Boltenhagen",null],["130755557120","Rubenow",null],["130755557146","Wusterhusen",null],["130755558036","Görmin",null],["130755558082","Loitz, Stadt",null],["130755558123","Sassen-Trantow",null],["130755559004","Altwigshagen",null],["130755559033","Ferdinandshof",null],["130755559045","Hammer a.d. Uecker",null],["130755559048","Heinrichswalde",null],["130755559118","Rothemühl",null],["130755559131","Torgelow, Stadt",null],["130755559143","Wilhelmsburg",null],["130755560017","Brietzig",null],["130755560032","Fahrenwalde",null],["130755560042","Groß Luckow",null],["130755560055","Jatznick",null],["130755560063","Koblentz",null],["130755560071","Krugsdorf",null],["130755560103","Nieden",null],["130755560104","Papendorf",null],["130755560109","Polzow",null],["130755560115","Rollwitz",null],["130755560126","Schönwalde",null],["130755560138","Viereck",null],["130755560149","Zerrenthin",null],["130755561058","Karlshagen, Ostseebad",null],["130755561092","Mölschow",null],["130755561106","Peenemünde",null],["130755561133","Trassenheide, Ostseebad",null],["130755561151","Zinnowitz, Ostseebad",null],["130755562010","Benz",null],["130755562026","Dargen",null],["130755562034","Garz",null],["130755562056","Kamminke",null],["130755562065","Korswandt",null],["130755562066","Koserow, Ostseebad",null],["130755562080","Loddin, Seebad",null],["130755562090","Mellenthin",null],["130755562111","Pudagla",null],["130755562114","Rankwitz",null],["130755562129","Stolpe auf Usedom",null],["130755562135","Ückeritz, Seebad",null],["130755562137","Usedom, Stadt",null],["130755562148","Zempin, Seebad",null],["130755562152","Zirchow",null],["130755563006","Bandelin",null],["130755563040","Gribow",null],["130755563041","Groß Kiesow",null],["130755563043","Groß Polzin",null],["130755563044","Gützkow, Stadt",null],["130755563061","Klein Bünzow",null],["130755563094","Murchin",null],["130755563121","Rubkow",null],["130755563125","Schmatzin",null],["130755563145","Wrangelsburg",null],["130755563150","Ziethen",null],["130755563154","Züssow",null],["130755563156","Karlsburg",null],["130760014014","Boizenburg/ Elbe, Stadt",null],["130760060060","Hagenow, Stadt",null],["130760088088","Lübtheen, Stadt",null],["130760090090","Ludwigslust, Stadt",null],["130760108108","Parchim, Stadt",null],["130765652009","Bengerstorf",null],["130765652010","Besitz",null],["130765652016","Brahlstorf",null],["130765652030","Dersenow",null],["130765652054","Gresse",null],["130765652055","Greven",null],["130765652102","Neu Gülze",null],["130765652106","Nostorf",null],["130765652122","Schwanheide",null],["130765652136","Teldau",null],["130765652138","Tessin b. Boizenburg",null],["130765654034","Dömitz, Stadt",null],["130765654053","Grebs-Niendorf",null],["130765654067","Karenz",null],["130765654093","Malk Göhren",null],["130765654094","Malliß",null],["130765654103","Neu Kaliß",null],["130765654143","Vielank",null],["130765655040","Gallin-Kuppentin",null],["130765655051","Granzin",null],["130765655075","Kreien",null],["130765655077","Kritzow",null],["130765655089","Lübz, Stadt",null],["130765655109","Passow",null],["130765655125","Siggelkow",null],["130765655151","Werder",null],["130765655165","Gehlsbach",null],["130765655168","Ruhner Berge",null],["130765656032","Dobbertin",null],["130765656048","Goldberg, Stadt",null],["130765656096","Mestlin",null],["130765656104","Neu Poserin",null],["130765656135","Techentin",null],["130765657003","Balow",null],["130765657021","Brunow",null],["130765657027","Dambeck",null],["130765657037","Eldena",null],["130765657049","Gorlosen",null],["130765657050","Grabow, Stadt",null],["130765657069","Karstädt",null],["130765657076","Kremmin",null],["130765657097","Milow",null],["130765657098","Möllenbeck",null],["130765657100","Muchow",null],["130765657115","Prislich",null],["130765657161","Zierzow",null],["130765658002","Alt Zachun",null],["130765658004","Bandenitz",null],["130765658008","Belsch",null],["130765658013","Bobzin",null],["130765658019","Bresegard bei Picher",null],["130765658041","Gammelin",null],["130765658057","Groß Krams",null],["130765658064","Hoort",null],["130765658065","Hülseburg",null],["130765658070","Kirch Jesar",null],["130765658079","Kuhstorf",null],["130765658099","Moraas",null],["130765658110","Pätow-Steegen",null],["130765658111","Picher",null],["130765658116","Pritzier",null],["130765658119","Redefin",null],["130765658131","Strohkirchen",null],["130765658169","Toddin",null],["130765658145","Warlitz",null],["130765659001","Alt Krenzlin",null],["130765659018","Bresegard bei Eldena",null],["130765659046","Göhlen",null],["130765659058","Groß Laasch",null],["130765659086","Lübesse",null],["130765659087","Lüblow",null],["130765659118","Rastow",null],["130765659134","Sülstorf",null],["130765659141","Uelitz",null],["130765659146","Warlow",null],["130765659156","Wöbbelin",null],["130765660012","Blievenstorf",null],["130765660017","Brenz",null],["130765660105","Neustadt-Glewe, Stadt",null],["130765662035","Domsühl",null],["130765662056","Groß Godems",null],["130765662068","Karrenzin",null],["130765662085","Lewitzrand",null],["130765662120","Rom",null],["130765662126","Spornitz",null],["130765662129","Stolpe",null],["130765662160","Ziegendorf",null],["130765662162","Zölkow",null],["130765662164","Obere Warnow",null],["130765663006","Barkhagen",null],["130765663114","Plau am See, Stadt",null],["130765663166","Ganzlin",null],["130765664011","Blankenberg",null],["130765664015","Borkow",null],["130765664020","Brüel, Stadt",null],["130765664026","Dabel",null],["130765664062","Hohen Pritz",null],["130765664072","Kobrow",null],["130765664078","Kuhlen-Wendorf",null],["130765664101","Mustin",null],["130765664128","Sternberg, Stadt",null],["130765664148","Weitendorf",null],["130765664155","Witzin",null],["130765664167","Kloster Tempzin",null],["130765665036","Dümmer",null],["130765665063","Holthusen",null],["130765665071","Klein Rogahn",null],["130765665107","Pampow",null],["130765665121","Schossin",null],["130765665130","Stralendorf",null],["130765665147","Warsow",null],["130765665154","Wittenförden",null],["130765665163","Zülow",null],["130765666152","Wittenburg, Stadt",null],["130765666153","Wittendörp",null],["130765667039","Gallin",null],["130765667073","Kogel",null],["130765667092","Lüttow-Valluhn",null],["130765667142","Vellahn",null],["130765667159","Zarrentin am Schaalsee, Stadt",null],["130765668005","Banzkow",null],["130765668007","Barnin",null],["130765668023","Bülow",null],["130765668024","Cambs",null],["130765668025","Crivitz, Stadt",null],["130765668029","Demen",null],["130765668033","Dobin am See",null],["130765668038","Friedrichsruhe",null],["130765668044","Gneven",null],["130765668080","Langen Brütz",null],["130765668082","Leezen",null],["130765668112","Pinnow",null],["130765668113","Plate",null],["130765668117","Raben Steinfeld",null],["130765668133","Sukow",null],["130765668140","Tramm",null],["130765668158","Zapel",null],["145110000000","Chemnitz, Stadt",null],["145210010010","Amtsberg",null],["145210020020","Annaberg-Buchholz, Stadt",null],["145210035035","Aue-Bad Schlema, Stadt",null],["145210110110","Breitenbrunn/Erzgeb.",null],["145210130130","Crottendorf",null],["145210150150","Drebach",null],["145210160160","Ehrenfriedersdorf, Stadt",null],["145210170170","Eibenstock, Stadt",null],["145210200200","Gelenau/Erzgeb.",null],["145210240240","Großolbersdorf",null],["145210250250","Großrückerswalde",null],["145210260260","Grünhain-Beierfeld, Stadt",null],["145210290290","Hohndorf",null],["145210310310","Jahnsdorf/Erzgeb.",null],["145210320320","Johanngeorgenstadt, Stadt",null],["145210330330","Jöhstadt, Stadt",null],["145210355355","Lauter-Bernsbach, Stadt",null],["145210370370","Lößnitz, Stadt",null],["145210390390","Marienberg, Stadt",null],["145210400400","Mildenau",null],["145210410410","Neukirchen/Erzgeb.",null],["145210440440","Oberwiesenthal, Kurort, Stadt",null],["145210450450","Oelsnitz/Erzgeb., Stadt",null],["145210460460","Olbernhau, Stadt",null],["145210495495","Pockau-Lengefeld, Stadt",null],["145210500500","Raschau-Markersbach",null],["145210530530","Schneeberg, Stadt",null],["145210540540","Schönheide",null],["145210550550","Schwarzenberg/Erzgeb., Stadt",null],["145210560560","Sehmatal",null],["145210600600","Stützengrün",null],["145210620620","Thalheim/Erzgeb., Stadt",null],["145210630630","Thermalbad Wiesenbad",null],["145210640640","Thum, Stadt",null],["145210670670","Wolkenstein, Stadt",null],["145215101060","Bärenstein",null],["145215101340","Königswalde",null],["145215103040","Auerbach",null],["145215103120","Burkhardtsdorf",null],["145215103230","Gornsdorf",null],["145215110210","Geyer, Stadt",null],["145215110610","Tannenberg",null],["145215115380","Lugau/Erzgeb., Stadt",null],["145215115430","Niederwürschnitz",null],["145215130510","Scheibenberg, Stadt",null],["145215130520","Schlettau, Stadt",null],["145215132140","Deutschneudorf",null],["145215132280","Heidersdorf",null],["145215132570","Seiffen/Erzgeb., Kurort",null],["145215133420","Niederdorf",null],["145215133590","Stollberg/Erzgeb., Stadt",null],["145215138220","Gornau/Erzgeb.",null],["145215138690","Zschopau, Stadt",null],["145215139080","Bockau",null],["145215139700","Zschorlau",null],["145215140180","Elterlein, Stadt",null],["145215140710","Zwönitz, Stadt",null],["145215405090","Börnichen/Erzgeb.",null],["145215405270","Grünhainichen",null],["145220020020","Augustusburg, Stadt",null],["145220035035","Bobritzsch-Hilbersdorf",null],["145220050050","Brand-Erbisdorf, Stadt",null],["145220070070","Claußnitz",null],["145220080080","Döbeln, Stadt",null],["145220110110","Eppendorf",null],["145220120120","Erlau",null],["145220140140","Flöha, Stadt",null],["145220150150","Frankenberg/Sa., Stadt",null],["145220170170","Frauenstein, Stadt",null],["145220180180","Freiberg, Stadt, Universitätsstadt",null],["145220190190","Geringswalde, Stadt",null],["145220200200","Großhartmannsdorf",null],["145220210210","Großschirma, Stadt",null],["145220220220","Großweitzschen",null],["145220230230","Hainichen, Stadt",null],["145220240240","Halsbrücke",null],["145220250250","Hartha, Stadt",null],["145220260260","Hartmannsdorf",null],["145220290290","Königshain-Wiederau",null],["145220300300","Kriebstein",null],["145220310310","Leisnig, Stadt",null],["145220320320","Leubsdorf",null],["145220330330","Lichtenau",null],["145220350350","Lunzenau, Stadt",null],["145220390390","Mulda/Sa.",null],["145220400400","Neuhausen/Erzgeb.",null],["145220420420","Niederwiesa",null],["145220430430","Oberschöna",null],["145220440440","Oederan, Stadt",null],["145220460460","Penig, Stadt",null],["145220470470","Rechenberg-Bienenmühle",null],["145220480480","Reinsberg",null],["145220500500","Rossau",null],["145220510510","Roßwein, Stadt",null],["145220540540","Striegistal",null],["145220570570","Waldheim, Stadt",null],["145220580580","Wechselburg",null],["145225102060","Burgstädt, Stadt",null],["145225102380","Mühlau",null],["145225102550","Taura",null],["145225113340","Lichtenberg/Erzgeb.",null],["145225113590","Weißenborn/Erzgeb.",null],["145225119010","Altmittweida",null],["145225119360","Mittweida, Stadt, Hochschulstadt",null],["145225123450","Ostrau",null],["145225123620","Zschaitz-Ottewig",null],["145225126280","Königsfeld",null],["145225126490","Rochlitz, Stadt",null],["145225126530","Seelitz",null],["145225126600","Zettlitz",null],["145225129090","Dorfchemnitz",null],["145225129520","Sayda, Stadt",null],["145230010010","Adorf/Vogtl., Stadt",null],["145230020020","Auerbach/Vogtl., Stadt",null],["145230030030","Bad Brambach",null],["145230040040","Bad Elster, Stadt",null],["145230090090","Ellefeld",null],["145230100100","Elsterberg, Stadt",null],["145230160160","Klingenthal, Stadt",null],["145230170170","Lengenfeld, Stadt",null],["145230200200","Markneukirchen, Stadt",null],["145230245245","Muldenhammer",null],["145230280280","Neumark",null],["145230310310","Pausa-Mühltroff, Stadt",null],["145230320320","Plauen, Stadt",null],["145230330330","Pöhl",null],["145230360360","Rodewisch, Stadt",null],["145230365365","Rosenbach/Vogtl.",null],["145230380380","Steinberg",null],["145230450450","Weischlitz",null],["145235107120","Falkenstein/Vogtl., Stadt",null],["145235107130","Grünbach",null],["145235107290","Neustadt/Vogtl.",null],["145235120190","Limbach",null],["145235120260","Netzschkau, Stadt",null],["145235122060","Bösenbrunn",null],["145235122080","Eichigt",null],["145235122300","Oelsnitz/Vogtl., Stadt",null],["145235122440","Triebel/Vogtl.",null],["145235125150","Heinsdorfergrund",null],["145235125340","Reichenbach im Vogtland, Stadt",null],["145235131230","Mühlental",null],["145235131370","Schöneck/Vogtl., Stadt",null],["145235134270","Neuensalz",null],["145235134430","Treuen, Stadt",null],["145235402050","Bergen",null],["145235402410","Theuma",null],["145235402420","Tirpersdorf",null],["145235402460","Werda",null],["145240020020","Callenberg",null],["145240060060","Fraureuth",null],["145240070070","Gersdorf",null],["145240080080","Glauchau, Stadt",null],["145240090090","Hartenstein, Stadt",null],["145240120120","Hohenstein-Ernstthal, Stadt",null],["145240140140","Langenbernsdorf",null],["145240150150","Langenweißbach",null],["145240170170","Lichtentanne",null],["145240200200","Mülsen",null],["145240210210","Neukirchen/Pleiße",null],["145240230230","Oberlungwitz, Stadt",null],["145240250250","Reinsdorf",null],["145240300300","Werdau, Stadt",null],["145240310310","Wildenfels, Stadt",null],["145240320320","Wilkau-Haßlau, Stadt",null],["145240330330","Zwickau, Stadt",null],["145245104030","Crimmitschau, Stadt",null],["145245104050","Dennheritz",null],["145245111040","Crinitzberg",null],["145245111100","Hartmannsdorf b. Kirchberg",null],["145245111110","Hirschfeld",null],["145245111130","Kirchberg, Stadt",null],["145245114180","Limbach-Oberfrohna, Stadt",null],["145245114220","Niederfrohna",null],["145245118190","Meerane, Stadt",null],["145245118270","Schönberg",null],["145245128010","Bernsdorf",null],["145245128160","Lichtenstein/Sa., Stadt",null],["145245128280","St. Egidien",null],["145245135240","Oberwiera",null],["145245135260","Remse",null],["145245135290","Waldenburg, Stadt",null],["146120000000","Dresden, Stadt",null],["146250010010","Arnsdorf",null],["146250020020","Bautzen / Budyšin, Stadt",null],["146250030030","Bernsdorf, Stadt",null],["146250060060","Burkau",null],["146250090090","Cunewalde",null],["146250100100","Demitz-Thumitz",null],["146250110110","Doberschau-Gaußig / Dobruša-Huska",null],["146250120120","Elsterheide / Halštrowska Hola",null],["146250130130","Elstra, Stadt",null],["146250150150","Göda / Hodźij",null],["146250160160","Großdubrau / Wulka Dubrawa",null],["146250200200","Großröhrsdorf, Stadt",null],["146250220220","Haselbachtal",null],["146250230230","Hochkirch / Bukecy",null],["146250240240","Hoyerswerda / Wojerecy, Stadt",null],["146250250250","Kamenz / Kamjenc, Stadt",null],["146250280280","Königswartha / Rakecy",null],["146250290290","Kubschütz / Kubšicy",null],["146250310310","Lauta, Stadt",null],["146250330330","Lohsa / Łaz",null],["146250340340","Malschwitz / Malešecy",null],["146250380380","Neukirch/Lausitz",null],["146250420420","Oßling",null],["146250430430","Ottendorf-Okrilla",null],["146250480480","Radeberg, Stadt",null],["146250490490","Radibor / Radwor",null],["146250525525","Schirgiswalde-Kirschau, Stadt",null],["146250530530","Schmölln-Putzkau",null],["146250550550","Schwepnitz",null],["146250560560","Sohland a. d. Spree",null],["146250570570","Spreetal / Sprjewiny Doł",null],["146250590590","Steinigtwolmsdorf",null],["146250600600","Wachau",null],["146250610610","Weißenberg / Wóspork, Stadt",null],["146250630630","Wilthen, Stadt",null],["146250640640","Wittichenau / Kulow, Stadt",null],["146255207040","Bischofswerda, Stadt",null],["146255207510","Rammenau",null],["146255211140","Frankenthal",null],["146255211170","Großharthau",null],["146255212190","Großpostwitz/O.L. / Budestecy",null],["146255212390","Obergurig / Hornja Hórka",null],["146255218270","Königsbrück, Stadt",null],["146255218300","Laußnitz",null],["146255218370","Neukirch",null],["146255223360","Neschwitz / Njeswačidło",null],["146255223460","Puschwitz / Bóšicy",null],["146255231180","Großnaundorf",null],["146255231320","Lichtenberg",null],["146255231410","Ohorn",null],["146255231450","Pulsnitz, Stadt",null],["146255231580","Steina",null],["146255501080","Crostwitz / Chrósćicy",null],["146255501350","Nebelschütz / Njebjelčicy",null],["146255501440","Panschwitz-Kuckau / Pančicy-Kukow",null],["146255501470","Räckelwitz / Worklecy",null],["146255501500","Ralbitz-Rosenthal / Ralbicy-Róžant",null],["146260060060","Boxberg/O.L. / Hamor",null],["146260085085","Ebersbach-Neugersdorf, Stadt",null],["146260110110","Görlitz, Stadt",null],["146260180180","Herrnhut, Stadt",null],["146260245245","Kottmar",null],["146260250250","Krauschwitz i.d. O.L. / Krušwica",null],["146260280280","Leutersdorf",null],["146260300300","Markersdorf",null],["146260310310","Mittelherwigsdorf",null],["146260370370","Niesky, Stadt",null],["146260390390","Oderwitz",null],["146260420420","Ostritz, Stadt",null],["146260530530","Seifhennersdorf, Stadt",null],["146260610610","Zittau, Stadt",null],["146265203010","Bad Muskau / Mužakow, Stadt",null],["146265203100","Gablenz / Jabłońc",null],["146265206030","Bernstadt a. d. Eigen, Stadt",null],["146265206500","Schönau-Berzdorf a. d. Eigen",null],["146265214140","Großschönau",null],["146265214170","Hainewalde",null],["146265220150","Großschweidnitz",null],["146265220270","Lawalde",null],["146265220290","Löbau, Stadt",null],["146265220470","Rosenbach",null],["146265224070","Dürrhennersdorf",null],["146265224350","Neusalza-Spremberg, Stadt",null],["146265224510","Schönbach",null],["146265227050","Bertsdorf-Hörnitz",null],["146265227210","Jonsdorf, Kurort",null],["146265227400","Olbersdorf",null],["146265227430","Oybin",null],["146265228020","Beiersdorf",null],["146265228410","Oppach",null],["146265232240","Königshain",null],["146265232450","Reichenbach/O.L., Stadt",null],["146265232570","Vierkirchen",null],["146265233260","Kreba-Neudorf / Chrjebja-Nowa Wjes",null],["146265233460","Rietschen / Rěčicy",null],["146265235160","Hähnichen",null],["146265235480","Rothenburg/O.L., Stadt",null],["146265237120","Groß Düben / Dźěwin",null],["146265237490","Schleife / Slepo",null],["146265237560","Trebendorf / Trjebin",null],["146265242590","Weißkeißel / Wuskidź",null],["146265242600","Weißwasser/O.L., Stadt / Běła Woda",null],["146265502190","Hohendubrau / Wysoka Dubrawa",null],["146265502320","Mücka / Mikow",null],["146265502440","Quitzdorf am See",null],["146265502580","Waldhufen",null],["146265503200","Horka",null],["146265503230","Kodersdorf",null],["146265503330","Neißeaue",null],["146265503520","Schöpstal",null],["146270010010","Coswig, Stadt",null],["146270020020","Diera-Zehren",null],["146270030030","Ebersbach",null],["146270050050","Gröditz, Stadt",null],["146270060060","Großenhain, Stadt",null],["146270070070","Hirschstein",null],["146270080080","Käbschütztal",null],["146270100100","Klipphausen",null],["146270130130","Lommatzsch, Stadt",null],["146270140140","Meißen, Stadt",null],["146270150150","Moritzburg",null],["146270170170","Niederau",null],["146270180180","Nossen, Stadt",null],["146270200200","Priestewitz",null],["146270210210","Radebeul, Stadt",null],["146270220220","Radeburg, Stadt",null],["146270230230","Riesa, Stadt",null],["146270260260","Stauchitz",null],["146270270270","Strehla, Stadt",null],["146270290290","Thiendorf",null],["146270310310","Weinböhla",null],["146270360360","Zeithain",null],["146275225040","Glaubitz",null],["146275225190","Nünchritz",null],["146275234240","Röderaue",null],["146275234340","Wülknitz",null],["146275238110","Lampertswalde",null],["146275238250","Schönfeld",null],["146280050050","Bannewitz",null],["146280060060","Dippoldiswalde, Stadt",null],["146280100100","Dürrröhrsdorf-Dittersbach",null],["146280110110","Freital, Stadt",null],["146280130130","Glashütte, Stadt",null],["146280160160","Heidenau, Stadt",null],["146280190190","Hohnstein, Stadt",null],["146280220220","Kreischa",null],["146280260260","Neustadt in Sachsen, Stadt",null],["146280300300","Rabenau, Stadt",null],["146280360360","Sebnitz, Stadt",null],["146280380380","Stolpen, Stadt",null],["146280410410","Wilsdruff, Stadt",null],["146285201010","Altenberg, Stadt",null],["146285201170","Hermsdorf/Erzgeb.",null],["146285202020","Bad Gottleuba-Berggießhübel, Stadt",null],["146285202040","Bahretal",null],["146285202230","Liebstadt, Stadt",null],["146285204030","Bad Schandau, Stadt",null],["146285204320","Rathmannsdorf",null],["146285204330","Reinhardtsdorf-Schöna",null],["146285209080","Dohna, Stadt",null],["146285209250","Müglitztal",null],["146285219140","Gohrisch",null],["146285219210","Königstein/Sächs. Schw., Stadt",null],["146285219310","Rathen, Kurort",null],["146285219340","Rosenthal-Bielatal",null],["146285219390","Struppen",null],["146285221240","Lohmen",null],["146285221370","Stadt Wehlen, Stadt",null],["146285229070","Dohma",null],["146285229270","Pirna, Stadt",null],["146285230150","Hartmannsdorf-Reichenau",null],["146285230205","Klingenberg",null],["146285240090","Dorfhain",null],["146285240400","Tharandt, Stadt",null],["147130000000","Leipzig, Stadt",null],["147290030030","Bennewitz",null],["147290040040","Böhlen, Stadt",null],["147290050050","Borna, Stadt",null],["147290060060","Borsdorf",null],["147290070070","Brandis, Stadt",null],["147290080080","Colditz, Stadt",null],["147290140140","Frohburg, Stadt",null],["147290150150","Geithain, Stadt",null],["147290160160","Grimma, Stadt",null],["147290170170","Groitzsch, Stadt",null],["147290190190","Großpösna",null],["147290220220","Kitzscher, Stadt",null],["147290245245","Lossatal",null],["147290250250","Machern",null],["147290260260","Markkleeberg, Stadt",null],["147290270270","Markranstädt, Stadt",null],["147290320320","Neukieritzsch",null],["147290360360","Regis-Breitingen, Stadt",null],["147290370370","Rötha, Stadt",null],["147290380380","Thallwitz",null],["147290400400","Trebsen/Mulde, Stadt",null],["147290410410","Wurzen, Stadt",null],["147290430430","Zwenkau, Stadt",null],["147295301010","Bad Lausick, Stadt",null],["147295301330","Otterwisch",null],["147295307020","Belgershain",null],["147295307300","Naunhof, Stadt",null],["147295307340","Parthenstein",null],["147295308100","Elstertrebnitz",null],["147295308350","Pegau, Stadt",null],["147300020020","Bad Düben, Stadt",null],["147300045045","Belgern-Schildau, Stadt",null],["147300050050","Cavertitz",null],["147300060060","Dahlen, Stadt",null],["147300070070","Delitzsch, Stadt",null],["147300080080","Doberschütz",null],["147300110110","Eilenburg, Stadt",null],["147300160160","Laußig",null],["147300170170","Liebschützberg",null],["147300180180","Löbnitz",null],["147300190190","Mockrehna",null],["147300200200","Mügeln, Stadt",null],["147300210210","Naundorf",null],["147300230230","Oschatz, Stadt",null],["147300250250","Rackwitz",null],["147300270270","Schkeuditz, Stadt",null],["147300300300","Taucha, Stadt",null],["147300330330","Wermsdorf",null],["147300340340","Wiedemar",null],["147305302010","Arzberg",null],["147305302030","Beilrode",null],["147305303090","Dommitzsch, Stadt",null],["147305303120","Elsnig",null],["147305303320","Trossin",null],["147305306150","Krostitz",null],["147305306280","Schönwölkau",null],["147305311100","Dreiheide",null],["147305311310","Torgau, Stadt",null],["147305601140","Jesewitz",null],["147305601360","Zschepplin",null],["150010000000","Dessau-Roßlau, Stadt",null],["150020000000","Halle (Saale), Stadt",null],["150030000000","Magdeburg, Landeshauptstadt",null],["150810030030","Arendsee (Altmark), Stadt",null],["150810135135","Gardelegen, Hansestadt",null],["150810240240","Kalbe (Milde), Stadt",null],["150810280280","Klötze, Stadt",null],["150810455455","Salzwedel, Hansestadt",null],["150815051026","Apenburg-Winterfeld, Flecken",null],["150815051045","Beetzendorf",null],["150815051095","Dähre",null],["150815051105","Diesdorf, Flecken",null],["150815051225","Jübar",null],["150815051290","Kuhfelde",null],["150815051440","Rohrberg",null],["150815051545","Wallstawe",null],["150820005005","Aken (Elbe), Stadt",null],["150820015015","Bitterfeld-Wolfen, Stadt",null],["150820180180","Köthen (Anhalt), Stadt",null],["150820241241","Muldestausee",null],["150820256256","Osternienburger Land",null],["150820301301","Raguhn-Jeßnitz, Stadt",null],["150820340340","Sandersdorf-Brehna, Stadt",null],["150820377377","Südliches Anhalt, Stadt",null],["150820430430","Zerbst/Anhalt, Stadt",null],["150820440440","Zörbig, Stadt",null],["150830040040","Barleben",null],["150830270270","Haldensleben, Stadt",null],["150830298298","Hohe Börde",null],["150830390390","Niedere Börde",null],["150830411411","Oebisfelde-Weferlingen, Stadt",null],["150830415415","Oschersleben (Bode), Stadt",null],["150830490490","Sülzetal",null],["150830531531","Wanzleben-Börde, Stadt",null],["150830565565","Wolmirstedt, Stadt",null],["150835051030","Angern",null],["150835051120","Burgstall",null],["150835051130","Colbitz",null],["150835051361","Loitsche-Heinrichsberg",null],["150835051440","Rogätz",null],["150835051557","Westheide",null],["150835051580","Zielitz",null],["150835052020","Altenhausen",null],["150835052060","Beendorf",null],["150835052115","Bülstringen",null],["150835052125","Calvörde",null],["150835052205","Erxleben",null],["150835052230","Flechtingen",null],["150835052323","Ingersleben",null],["150835053190","Eilsleben",null],["150835053275","Harbke",null],["150835053320","Hötensleben",null],["150835053485","Sommersdorf",null],["150835053505","Ummendorf",null],["150835053515","Völpke",null],["150835053535","Wefensleben",null],["150835054025","Am Großen Bruch",null],["150835054035","Ausleben",null],["150835054245","Gröningen, Stadt",null],["150835054355","Kroppenstedt, Stadt",null],["150840130130","Elsteraue",null],["150840235235","Hohenmölsen, Stadt",null],["150840315315","Lützen, Stadt",null],["150840355355","Naumburg (Saale), Stadt",null],["150840490490","Teuchern, Stadt",null],["150840550550","Weißenfels, Stadt",null],["150840590590","Zeitz, Stadt",null],["150845051012","An der Poststraße",null],["150845051015","Bad Bibra, Stadt",null],["150845051125","Eckartsberga, Stadt",null],["150845051132","Finne",null],["150845051133","Finneland",null],["150845051246","Kaiserpfalz",null],["150845051282","Lanitz-Hassel-Tal",null],["150845052115","Droyßig",null],["150845052207","Gutenborn",null],["150845052275","Kretzschau",null],["150845052442","Schnaudertal",null],["150845052565","Wetterzeube",null],["150845053025","Balgstädt",null],["150845053135","Freyburg (Unstrut), Stadt",null],["150845053150","Gleina",null],["150845053170","Goseck",null],["150845053250","Karsdorf",null],["150845053285","Laucha an der Unstrut, Stadt",null],["150845053360","Nebra (Unstrut), Stadt",null],["150845054013","Meineweh",null],["150845054335","Mertendorf",null],["150845054341","Molauer Land",null],["150845054375","Osterfeld, Stadt",null],["150845054445","Schönburg",null],["150845054470","Stößen, Stadt",null],["150845054560","Wethau",null],["150850040040","Ballenstedt, Stadt",null],["150850055055","Blankenburg (Harz), Stadt",null],["150850110110","Falkenstein/Harz, Stadt",null],["150850135135","Halberstadt, Stadt",null],["150850145145","Harzgerode, Stadt",null],["150850185185","Huy",null],["150850190190","Ilsenburg (Harz), Stadt",null],["150850227227","Nordharz",null],["150850228228","Oberharz am Brocken, Stadt",null],["150850230230","Osterwieck, Stadt",null],["150850235235","Quedlinburg, Welterbestadt",null],["150850330330","Thale, Stadt",null],["150850370370","Wernigerode, Stadt",null],["150855051090","Ditfurt",null],["150855051125","Groß Quenstedt",null],["150855051140","Harsleben",null],["150855051160","Hedersleben",null],["150855051285","Schwanebeck, Stadt",null],["150855051287","Selke-Aue",null],["150855051365","Wegeleben, Stadt",null],["150860005005","Biederitz",null],["150860015015","Burg, Stadt",null],["150860035035","Elbe-Parey",null],["150860040040","Genthin, Stadt",null],["150860055055","Gommern, Stadt",null],["150860080080","Jerichow, Stadt",null],["150860140140","Möckern, Stadt",null],["150860145145","Möser",null],["150870015015","Allstedt, Stadt",null],["150870031031","Arnstein, Stadt",null],["150870130130","Eisleben, Lutherstadt",null],["150870165165","Gerbstedt, Stadt",null],["150870220220","Hettstedt, Stadt",null],["150870275275","Mansfeld, Stadt",null],["150870370370","Sangerhausen, Stadt",null],["150870386386","Seegebiet Mansfelder Land",null],["150870412412","Südharz",null],["150875051055","Berga",null],["150875051101","Brücken-Hackpfüffel",null],["150875051125","Edersleben",null],["150875051250","Kelbra (Kyffhäuser), Stadt",null],["150875051440","Wallhausen",null],["150875052010","Ahlsdorf",null],["150875052045","Benndorf",null],["150875052070","Blankenheim",null],["150875052075","Bornstedt",null],["150875052205","Helbra",null],["150875052210","Hergisdorf",null],["150875052260","Klostermansfeld",null],["150875052470","Wimmelburg",null],["150880020020","Bad Dürrenberg, Solestadt",null],["150880025025","Bad Lauchstädt, Goethestadt",null],["150880065065","Braunsbedra, Stadt",null],["150880150150","Kabelsketal",null],["150880195195","Landsberg, Stadt",null],["150880205205","Leuna, Stadt",null],["150880216216","Wettin-Löbejün, Stadt",null],["150880220220","Merseburg, Stadt",null],["150880235235","Mücheln (Geiseltal), Stadt",null],["150880295295","Petersberg",null],["150880305305","Querfurt, Stadt",null],["150880319319","Salzatal",null],["150880330330","Schkopau",null],["150880365365","Teutschenthal",null],["150885051030","Barnstädt",null],["150885051100","Farnstädt",null],["150885051250","Nemsdorf-Göhrendorf",null],["150885051265","Obhausen",null],["150885051340","Schraplau, Stadt",null],["150885051355","Steigra",null],["150890015015","Aschersleben, Stadt",null],["150890026026","Barby, Stadt",null],["150890030030","Bernburg (Saale), Stadt",null],["150890042042","Bördeland",null],["150890055055","Calbe (Saale), Stadt",null],["150890175175","Hecklingen, Stadt",null],["150890195195","Könnern, Stadt",null],["150890235235","Nienburg (Saale), Stadt",null],["150890305305","Schönebeck (Elbe), Stadt",null],["150890307307","Seeland, Stadt",null],["150890310310","Staßfurt, Stadt",null],["150895051041","Bördeaue",null],["150895051043","Börde-Hakel",null],["150895051045","Borne",null],["150895051075","Egeln, Stadt",null],["150895051365","Wolmirsleben",null],["150895052005","Alsleben (Saale), Stadt",null],["150895052130","Giersleben",null],["150895052165","Güsten, Stadt",null],["150895052185","Ilberstedt",null],["150895052245","Plötzkau",null],["150900070070","Bismark (Altmark), Stadt",null],["150900225225","Havelberg, Hansestadt",null],["150900415415","Osterburg (Altmark), Hansestadt",null],["150900535535","Stendal, Hansestadt",null],["150900546546","Tangerhütte, Stadt",null],["150900550550","Tangermünde, Stadt",null],["150905051010","Arneburg, Stadt",null],["150905051135","Eichstedt (Altmark)",null],["150905051180","Goldbeck",null],["150905051220","Hassel",null],["150905051245","Hohenberg-Krusemark",null],["150905051270","Iden",null],["150905051435","Rochau",null],["150905051610","Werben (Elbe), Hansestadt",null],["150905052285","Kamern",null],["150905052310","Klietz",null],["150905052445","Sandau (Elbe), Stadt",null],["150905052485","Schollene",null],["150905052500","Schönhausen (Elbe)",null],["150905052631","Wust-Fischbeck",null],["150905053003","Aland",null],["150905053007","Altmärkische Höhe",null],["150905053008","Altmärkische Wische",null],["150905053520","Seehausen (Altmark), Hansestadt",null],["150905053635","Zehrental",null],["150910010010","Annaburg, Stadt",null],["150910020020","Bad Schmiedeberg, Stadt",null],["150910060060","Coswig (Anhalt), Stadt",null],["150910110110","Gräfenhainichen, Stadt",null],["150910145145","Jessen (Elster), Stadt",null],["150910160160","Kemberg, Stadt",null],["150910241241","Oranienbaum-Wörlitz, Stadt",null],["150910375375","Wittenberg, Lutherstadt",null],["150910391391","Zahna-Elster, Stadt",null],["160510000000","Erfurt, Stadt",null],["160520000000","Gera, Stadt",null],["160530000000","Jena, Stadt",null],["160540000000","Suhl, Stadt",null],["160550000000","Weimar, Stadt",null],["160610045045","Heilbad Heiligenstadt, Stadt",null],["160610074074","Niederorschel",null],["160610115115","Leinefelde-Worbis, Stadt",null],["160610116116","Am Ohmberg",null],["160610117117","Sonnenstein",null],["160610118118","Dingelstädt, Stadt",null],["160615001003","Berlingerode",null],["160615001015","Brehme",null],["160615001026","Ecklingerode",null],["160615001031","Ferna",null],["160615001094","Tastungen",null],["160615001103","Wehnde",null],["160615001114","Teistungen",null],["160615006017","Breitenworbis",null],["160615006019","Buhla",null],["160615006037","Gernrode",null],["160615006044","Haynrode",null],["160615006058","Kirchworbis",null],["160615008001","Arenshausen",null],["160615008014","Bornhagen",null],["160615008021","Burgwalde",null],["160615008032","Freienhagen",null],["160615008033","Fretterode",null],["160615008036","Gerbershausen",null],["160615008048","Hohengandern",null],["160615008057","Kirchgandern",null],["160615008066","Lindewerra",null],["160615008069","Marth",null],["160615008078","Rohrberg",null],["160615008082","Rustenfelde",null],["160615008083","Schachtebich",null],["160615008102","Wahlhausen",null],["160615009012","Bodenrode-Westhausen",null],["160615009034","Geisleden",null],["160615009039","Glasehausen",null],["160615009047","Heuthen",null],["160615009049","Hohes Kreuz",null],["160615009076","Reinholterode",null],["160615009089","Steinbach",null],["160615009107","Wingerode",null],["160615012002","Asbach-Sickenberg",null],["160615012007","Birkenfelde",null],["160615012024","Dietzenrode/Vatterode",null],["160615012028","Eichstruth",null],["160615012065","Lenterode",null],["160615012067","Lutter",null],["160615012068","Mackenrode",null],["160615012077","Röhrig",null],["160615012084","Schönhagen",null],["160615012091","Steinheuterode",null],["160615012096","Thalwenden",null],["160615012097","Uder",null],["160615012111","Wüstheuterode",null],["160615013018","Büttstedt",null],["160615013027","Effelder",null],["160615013041","Großbartloff",null],["160615013063","Küllstedt",null],["160615013101","Wachstedt",null],["160615014023","Dieterode",null],["160615014035","Geismar",null],["160615014056","Kella",null],["160615014062","Krombach",null],["160615014075","Pfaffschwende",null],["160615014085","Schwobfeld",null],["160615014086","Sickerode",null],["160615014098","Volkerode",null],["160615014105","Wiesenfeld",null],["160615014113","Schimberg",null],["160620005005","Ellrich, Stadt",null],["160620041041","Nordhausen, Stadt",null],["160620049049","Sollstedt",null],["160620062062","Hohenstein",null],["160620063063","Werther",null],["160620065065","Harztor",null],["160625053008","Görsbach",null],["160625053054","Urbach",null],["160625053064","Heringen/Helme, Stadt",null],["160625054009","Großlohra",null],["160625054024","Kehmstedt",null],["160625054026","Kleinfurra",null],["160625054033","Lipprechterode",null],["160625054037","Niedergebra",null],["160625054066","Bleicherode, Stadt",null],["160630004004","Barchfeld-Immelborn",null],["160630076076","Treffurt, Stadt",null],["160630078078","Unterbreizbach",null],["160630082082","Vacha, Stadt",null],["160630092092","Wutha-Farnroda",null],["160630097097","Gerstungen",null],["160630098098","Hörselberg-Hainich",null],["160630099099","Bad Liebenstein, Stadt",null],["160630101101","Krayenberggemeinde",null],["160630103103","Werra-Suhl-Tal, Stadt",null],["160630105105","Eisenach, Stadt",null],["160635006006","Berka v. d. Hainich",null],["160635006008","Bischofroda",null],["160635006028","Frankenroda",null],["160635006037","Hallungen",null],["160635006046","Krauthausen",null],["160635006049","Lauterbach",null],["160635006058","Nazza",null],["160635006104","Amt Creuzburg, Stadt",null],["160635051003","Bad Salzungen, Stadt",null],["160635051051","Leimbach",null],["160635056011","Buttlar",null],["160635056032","Geisa, Stadt",null],["160635056033","Gerstengrund",null],["160635056068","Schleid",null],["160635057066","Ruhla, Stadt",null],["160635057071","Seebach",null],["160635059015","Dermbach",null],["160635059023","Empfertshausen",null],["160635059062","Oechsen",null],["160635059084","Weilar",null],["160635059086","Wiesenthal",null],["160640003003","Bad Langensalza, Stadt",null],["160640014014","Dünwald",null],["160640046046","Mühlhausen/Thüringen, Stadt",null],["160640071071","Unstruttal",null],["160640072072","Menteroda",null],["160640073073","Anrode",null],["160645001004","Bad Tennstedt, Stadt",null],["160645001005","Ballhausen",null],["160645001007","Blankenburg",null],["160645001009","Bruchstedt",null],["160645001021","Haussömmern",null],["160645001027","Hornsömmern",null],["160645001033","Kirchheilingen",null],["160645001038","Kutzleben",null],["160645001045","Mittelsömmern",null],["160645001061","Sundhausen",null],["160645001062","Tottleben",null],["160645001064","Urleben",null],["160645051019","Großvargula",null],["160645051022","Herbsleben",null],["160645052055","Rodeberg",null],["160645052074","Südeichsfeld",null],["160645053032","Kammerforst",null],["160645053053","Oppershausen",null],["160645053075","Vogtei",null],["160645054058","Schönstedt",null],["160645054076","Unstrut-Hainich",null],["160645055037","Körner",null],["160645055043","Marolterode",null],["160645055077","Nottertal-Heilinger Höhen, Stadt",null],["160650003003","Bad Frankenhausen/Kyffhäuser, Stadt",null],["160650032032","Helbedündorf",null],["160650067067","Sondershausen, Stadt",null],["160650085085","Kyffhäuserland",null],["160650087087","Roßleben-Wiehe, Stadt",null],["160650089089","Greußen, Stadt",null],["160655002012","Clingen, Stadt",null],["160655002048","Niederbösa",null],["160655002051","Oberbösa",null],["160655002074","Topfstedt",null],["160655002075","Trebra",null],["160655002077","Wasserthaleben",null],["160655002079","Westgreußen",null],["160655052001","Abtsbessingen",null],["160655052005","Bellstedt",null],["160655052014","Ebeleben, Stadt",null],["160655052018","Freienbessingen",null],["160655052038","Holzsußra",null],["160655052058","Rockstedt",null],["160655055008","Borxleben",null],["160655055019","Gehofen",null],["160655055042","Kalbsrieth",null],["160655055046","Mönchpfiffel-Nikolausrieth",null],["160655055056","Reinsdorf",null],["160655055086","Artern, Stadt",null],["160655056016","Etzleben",null],["160655056052","Oberheldrungen",null],["160655056088","An der Schmücke, Stadt",null],["160660023023","Floh-Seligenthal",null],["160660047047","Oberhof, Stadt",null],["160660063063","Schmalkalden, Kurort, Stadt",null],["160660069069","Steinbach-Hallenberg, Kurort, Stadt",null],["160660074074","Brotterode-Trusetal, Stadt",null],["160660092092","Zella-Mehlis, Stadt",null],["160660093093","Rhönblick",null],["160660094094","Grabfeld",null],["160665005012","Birx",null],["160665005019","Erbenhausen",null],["160665005024","Frankenheim/Rhön",null],["160665005052","Oberweid",null],["160665005095","Kaltennordheim, Stadt",null],["160665013025","Friedelshausen",null],["160665013041","Mehmels",null],["160665013064","Schwallungen",null],["160665013086","Wasungen, Stadt",null],["160665014005","Belrieth",null],["160665014015","Christes",null],["160665014016","Dillstädt",null],["160665014017","Einhausen",null],["160665014018","Ellingshausen",null],["160665014038","Kühndorf",null],["160665014039","Leutersdorf",null],["160665014045","Neubrunn",null],["160665014049","Obermaßfeld-Grimmenthal",null],["160665014057","Ritschenhausen",null],["160665014058","Rohr",null],["160665014065","Schwarza",null],["160665014079","Utendorf",null],["160665014081","Vachdorf",null],["160665050042","Meiningen, Stadt",null],["160665050056","Rippershausen",null],["160665050073","Sülzfeld",null],["160665050076","Untermaßfeld",null],["160665051013","Breitungen/Werra",null],["160665051022","Fambach",null],["160665051059","Rosa",null],["160665051061","Roßdorf",null],["160670019019","Friedrichroda, Stadt",null],["160670029029","Gotha, Stadt",null],["160670064064","Bad Tabarz",null],["160670065065","Tambach-Dietharz/Thür. Wald, Stadt",null],["160670072072","Waltershausen, Stadt",null],["160670087087","Nesse-Apfelstädt",null],["160670088088","Hörsel",null],["160675007004","Bienstädt",null],["160675007016","Eschenbergen",null],["160675007022","Friemar",null],["160675007047","Molschleben",null],["160675007052","Nottleben",null],["160675007055","Pferdingsleben",null],["160675007068","Tröchtelborn",null],["160675007071","Tüttleben",null],["160675007082","Zimmernsupra",null],["160675012009","Dachwig",null],["160675012011","Döllstädt",null],["160675012026","Gierstädt",null],["160675012033","Großfahner",null],["160675012067","Tonna",null],["160675050044","Luisenthal",null],["160675050053","Ohrdruf, Stadt",null],["160675052059","Schwabhausen",null],["160675052089","Drei Gleichen",null],["160675053063","Sonneborn",null],["160675053091","Nessetal",null],["160675054013","Emleben",null],["160675054036","Herrenhof",null],["160675054092","Georgenthal",null],["160680034034","Kölleda, Stadt",null],["160680051051","Sömmerda, Stadt",null],["160680058058","Weißensee, Stadt",null],["160680063063","Buttstädt",null],["160685002002","Andisleben",null],["160685002014","Gebesee, Stadt",null],["160685002045","Ringleben",null],["160685002057","Walschleben",null],["160685005005","Büchel",null],["160685005015","Griefstedt",null],["160685005022","Günstedt",null],["160685005043","Riethgen",null],["160685005064","Kindelbrück",null],["160685006019","Großneuhausen",null],["160685006033","Kleinneuhausen",null],["160685006041","Ostramondra",null],["160685006042","Rastenberg, Stadt",null],["160685009013","Gangloffsömmern",null],["160685009025","Haßleben",null],["160685009044","Riethnordhausen",null],["160685009049","Schwerstedt",null],["160685009053","Straußfurt",null],["160685009059","Werningshausen",null],["160685009062","Wundersleben",null],["160685012001","Alperstedt",null],["160685012007","Eckstedt",null],["160685012017","Großmölsen",null],["160685012021","Großrudestedt",null],["160685012032","Kleinmölsen",null],["160685012036","Markvippach",null],["160685012037","Nöda",null],["160685012039","Ollendorf",null],["160685012048","Schloßvippach",null],["160685012052","Sprötau",null],["160685012055","Udestedt",null],["160685012056","Vogelsberg",null],["160685050009","Elxleben",null],["160685050061","Witterda",null],["160690012012","Eisfeld, Stadt",null],["160690024024","Hildburghausen, Stadt",null],["160690042042","Schleusegrund",null],["160690043043","Schleusingen, Stadt",null],["160690053053","Veilsdorf",null],["160690061061","Masserberg",null],["160690062062","Römhild, Stadt",null],["160695002001","Ahlstädt",null],["160695002003","Beinerstadt",null],["160695002004","Bischofrod",null],["160695002008","Dingsleben",null],["160695002009","Ehrenberg",null],["160695002011","Eichenberg",null],["160695002016","Grimmelshausen",null],["160695002017","Grub",null],["160695002021","Henfstädt",null],["160695002025","Kloster Veßra",null],["160695002026","Lengfeld",null],["160695002028","Marisfeld",null],["160695002035","Oberstadt",null],["160695002037","Reurieth",null],["160695002044","Schmeheim",null],["160695002047","St.Bernhard",null],["160695002051","Themar, Stadt",null],["160695004041","Schlechtsart",null],["160695004046","Schweickershausen",null],["160695004049","Straufhain",null],["160695004052","Ummerstadt, Stadt",null],["160695004056","Westhausen",null],["160695004063","Heldburg, Stadt",null],["160695051006","Brünn/Thür.",null],["160695051058","Auengrund",null],["160700004004","Arnstadt, Stadt",null],["160700028028","Amt Wachsenburg",null],["160700029029","Ilmenau, Stadt",null],["160700048048","Stadtilm, Stadt",null],["160700057057","Geratal",null],["160700058058","Großbreitenbach, Stadt",null],["160705002011","Elgersburg",null],["160705002034","Martinroda",null],["160705002043","Plaue, Stadt",null],["160705009001","Alkersleben",null],["160705009006","Bösleben-Wüllersleben",null],["160705009008","Dornheim",null],["160705009012","Elleben",null],["160705009013","Elxleben",null],["160705009041","Osthausen-Wülfershausen",null],["160705009054","Witzleben",null],["160710001001","Apolda, Stadt",null],["160710003003","Bad Berka, Stadt",null],["160710008008","Blankenhain, Stadt",null],["160710101101","Ilmtal-Weinstraße",null],["160710103103","Grammetal",null],["160715007032","Hohenfelden",null],["160715007043","Klettbach",null],["160715007046","Kranichfeld, Stadt",null],["160715007059","Nauendorf",null],["160715007079","Rittersdorf",null],["160715007087","Tonndorf",null],["160715008009","Buchfart",null],["160715008013","Döbritschen",null],["160715008019","Frankendorf",null],["160715008025","Großschwabhausen",null],["160715008027","Hammerstedt",null],["160715008031","Hetschburg",null],["160715008037","Kapellendorf",null],["160715008038","Kiliansroda",null],["160715008042","Kleinschwabhausen",null],["160715008049","Lehnstedt",null],["160715008053","Magdala, Stadt",null],["160715008055","Mechelroda",null],["160715008056","Mellingen",null],["160715008071","Oettern",null],["160715008089","Umpferstedt",null],["160715008093","Vollersroda",null],["160715008095","Wiegendorf",null],["160715051004","Bad Sulza, Stadt",null],["160715051015","Eberstedt",null],["160715051022","Großheringen",null],["160715051064","Niedertrebra",null],["160715051069","Obertrebra",null],["160715051077","Rannstedt",null],["160715051083","Schmiedehausen",null],["160715053005","Ballstedt",null],["160715053017","Ettersburg",null],["160715053061","Neumark, Stadt",null],["160715053102","Am Ettersberg",null],["160720011011","Lauscha, Stadt",null],["160720015015","Schalkau, Stadt",null],["160720018018","Sonneberg, Stadt",null],["160720019019","Steinach, Stadt",null],["160720023023","Frankenblick",null],["160720024024","Föritztal",null],["160725051006","Goldisthal",null],["160725051013","Neuhaus am Rennweg, Stadt",null],["160730005005","Bad Blankenburg, Stadt",null],["160730076076","Rudolstadt, Stadt",null],["160730077077","Saalfeld/Saale, Stadt",null],["160730106106","Leutenberg, Stadt",null],["160730109109","Uhlstädt-Kirchhasel",null],["160730111111","Unterwellenborn",null],["160735005028","Gräfenthal, Stadt",null],["160735005046","Lehesten, Stadt",null],["160735005067","Probstzella",null],["160735012013","Cursdorf",null],["160735012014","Deesbach",null],["160735012017","Döschnitz",null],["160735012037","Katzhütte",null],["160735012055","Meura",null],["160735012074","Rohrbach",null],["160735012082","Schwarzburg",null],["160735012084","Sitzendorf",null],["160735012094","Unterweißbach",null],["160735012113","Schwarzatal, Stadt",null],["160735051002","Altenbeuthen",null],["160735051035","Hohenwarte",null],["160735051038","Kaulsdorf",null],["160735051107","Drognitz",null],["160735054001","Allendorf",null],["160735054006","Bechstedt",null],["160735054112","Königsee, Stadt",null],["160740044044","Kahla, Stadt",null],["160745005012","Crossen an der Elster",null],["160745005038","Hartmannsdorf",null],["160745005039","Heideland",null],["160745005072","Rauda",null],["160745005092","Silbitz",null],["160745005106","Walpernhain",null],["160745005116","Schkölen, Stadt",null],["160745007007","Bremsnitz",null],["160745007017","Eineborn",null],["160745007022","Geisenhain",null],["160745007024","Gneus",null],["160745007029","Großbockedra",null],["160745007045","Karlsdorf",null],["160745007046","Kleinbockedra",null],["160745007047","Kleinebersdorf",null],["160745007053","Lippersdorf-Erdmannsdorf",null],["160745007056","Meusebach",null],["160745007064","Oberbodnitz",null],["160745007066","Ottendorf",null],["160745007071","Rattelsdorf",null],["160745007074","Rausdorf",null],["160745007077","Renthendorf",null],["160745007097","Tautendorf",null],["160745007101","Tissa",null],["160745007102","Trockenborn-Wolfersdorf",null],["160745007103","Tröbnitz",null],["160745007104","Unterbodnitz",null],["160745007107","Waltersdorf",null],["160745007108","Weißbach",null],["160745011002","Altenberga",null],["160745011004","Bibra",null],["160745011008","Bucha",null],["160745011016","Eichenberg",null],["160745011021","Freienorla",null],["160745011031","Großeutersdorf",null],["160745011033","Großpürschütz",null],["160745011034","Gumperda",null],["160745011042","Hummelshain",null],["160745011048","Kleineutersdorf",null],["160745011049","Laasdorf",null],["160745011052","Lindig",null],["160745011057","Milda",null],["160745011065","Orlamünde, Stadt",null],["160745011076","Reinstädt",null],["160745011079","Rothenstein",null],["160745011087","Schöps",null],["160745011089","Seitenroda",null],["160745011095","Sulza",null],["160745011114","Zöllnitz",null],["160745014041","Hermsdorf, Stadt",null],["160745014059","Mörsdorf",null],["160745014075","Reichenbach",null],["160745014084","Schleifreisen",null],["160745014093","St.Gangloff",null],["160745015011","Dornburg-Camburg, Stadt",null],["160745015019","Frauenprießnitz",null],["160745015026","Golmsdorf",null],["160745015032","Großlöbichau",null],["160745015036","Hainichen",null],["160745015043","Jenalöbnitz",null],["160745015051","Lehesten",null],["160745015054","Löberschütz",null],["160745015063","Neuengönna",null],["160745015096","Tautenburg",null],["160745015099","Thierschneck",null],["160745015112","Wichmar",null],["160745015113","Zimmern",null],["160745050058","Möckern",null],["160745050081","Ruttersdorf-Lotschen",null],["160745050094","Stadtroda, Stadt",null],["160745051009","Bürgel, Stadt",null],["160745051028","Graitschen b. Bürgel",null],["160745051061","Nausnitz",null],["160745051068","Poxdorf",null],["160745052018","Eisenberg, Stadt",null],["160745052025","Gösen",null],["160745052037","Hainspitz",null],["160745052055","Mertendorf",null],["160745052067","Petersberg",null],["160745052073","Rauschwitz",null],["160745053001","Albersdorf",null],["160745053003","Bad Klosterlausnitz",null],["160745053005","Bobeck",null],["160745053082","Scheiditz",null],["160745053085","Schlöben",null],["160745053086","Schöngleina",null],["160745053091","Serba",null],["160745053098","Tautenhain",null],["160745053105","Waldeck",null],["160745053109","Weißenborn",null],["160750046046","Hirschberg, Stadt",null],["160750062062","Bad Lobenstein, Stadt",null],["160750085085","Pößneck, Stadt",null],["160750098098","Schleiz, Stadt",null],["160750131131","Gefell, Stadt",null],["160750132132","Tanna, Stadt",null],["160750133133","Wurzbach, Stadt",null],["160750134134","Remptendorf",null],["160750135135","Saalburg-Ebersdorf, Stadt",null],["160750136136","Rosenthal am Rennsteig",null],["160755004014","Dittersdorf",null],["160755004033","Görkwitz",null],["160755004034","Göschitz",null],["160755004048","Kirschkau",null],["160755004063","Löhma",null],["160755004068","Moßbach",null],["160755004072","Neundorf (bei Schleiz)",null],["160755004076","Oettersdorf",null],["160755004083","Plothen",null],["160755004084","Pörmitz",null],["160755004109","Tegau",null],["160755004119","Volkmannsdorf",null],["160755005006","Bodelwitz",null],["160755005016","Döbritz",null],["160755005031","Gertewitz",null],["160755005039","Grobengereuth",null],["160755005054","Langenorla",null],["160755005056","Lausnitz b. Neustadt an der Orla",null],["160755005074","Nimritz",null],["160755005075","Oberoppurg",null],["160755005077","Oppurg",null],["160755005087","Quaschwitz",null],["160755005105","Solkwitz",null],["160755005121","Weira",null],["160755005124","Wernburg",null],["160755011019","Dreitzsch",null],["160755011029","Geroda",null],["160755011057","Lemnitz",null],["160755011065","Miesitz",null],["160755011066","Mittelpöllnitz",null],["160755011093","Rosendorf",null],["160755011099","Schmieritz",null],["160755011114","Tömmelsdorf",null],["160755011116","Triptis, Stadt",null],["160755013023","Eßbach",null],["160755013035","Gössitz",null],["160755013047","Keila",null],["160755013069","Moxa",null],["160755013079","Paska",null],["160755013081","Peuschen",null],["160755013088","Ranis, Stadt",null],["160755013101","Schmorda",null],["160755013102","Schöndorf",null],["160755013103","Seisla",null],["160755013125","Wilhelmsdorf",null],["160755013127","Ziegenrück, Stadt",null],["160755013129","Krölpa",null],["160755050051","Kospoda",null],["160755050073","Neustadt an der Orla, Stadt",null],["160760004004","Berga/Elster, Stadt",null],["160760022022","Greiz, Stadt",null],["160760061061","Ronneburg, Stadt",null],["160760088088","Harth-Pöllnitz",null],["160760089089","Kraftsdorf",null],["160760092092","Auma-Weidatal, Stadt",null],["160760093093","Mohlsdorf-Teichwolframsdorf",null],["160765004009","Braunichswalde",null],["160765004017","Endschütz",null],["160765004019","Gauern",null],["160765004027","Hilbersdorf",null],["160765004034","Kauern",null],["160765004043","Linda b. Weida",null],["160765004055","Paitzdorf",null],["160765004062","Rückersdorf",null],["160765004069","Seelingstädt",null],["160765004074","Teichwitz",null],["160765004084","Wünschendorf/Elster",null],["160765006007","Bocka",null],["160765006033","Hundhaupten",null],["160765006042","Lederhose",null],["160765006044","Lindenkreuz",null],["160765006049","Münchenbernsdorf, Stadt",null],["160765006064","Saara",null],["160765006068","Schwarzbach",null],["160765006086","Zedlitz",null],["160765008006","Bethenhausen",null],["160765008008","Brahmenau",null],["160765008023","Großenstein",null],["160765008028","Hirschfeld",null],["160765008036","Korbußen",null],["160765008058","Pölzig",null],["160765008059","Reichstädt",null],["160765008067","Schwaara",null],["160765051003","Bad Köstritz, Stadt",null],["160765051012","Caaschwitz",null],["160765051026","Hartmannsdorf",null],["160765053014","Crimla",null],["160765053079","Weida, Stadt",null],["160765054041","Langenwolschendorf",null],["160765054081","Weißendorf",null],["160765054087","Zeulenroda-Triebes, Stadt",null],["160765056029","Hohenleuben, Stadt",null],["160765056038","Kühdorf",null],["160765056039","Langenwetzendorf",null],["160770001001","Altenburg, Stadt",null],["160770028028","Lucka, Stadt",null],["160770032032","Meuselwitz, Stadt",null],["160775004005","Fockendorf",null],["160775004007","Gerstenberg",null],["160775004015","Haselbach",null],["160775004048","Treben",null],["160775004052","Windischleuba",null],["160775005008","Göhren",null],["160775005009","Göllnitz",null],["160775005022","Kriebitzsch",null],["160775005027","Lödla",null],["160775005031","Mehna",null],["160775005034","Monstab",null],["160775005042","Rositz",null],["160775005044","Starkenberg",null],["160775009016","Heukewalde",null],["160775009018","Jonaswalde",null],["160775009026","Löbichau",null],["160775009041","Posterstein",null],["160775009047","Thonhausen",null],["160775009049","Vollmershain",null],["160775050012","Gößnitz, Stadt",null],["160775050017","Heyersdorf",null],["160775050039","Ponitz",null],["160775051011","Göpfersdorf",null],["160775051023","Langenleuba-Niederhain",null],["160775051036","Nobitz",null],["160775052003","Dobitschen",null],["160775052043","Schmölln, Stadt",null]]} \ No newline at end of file diff --git a/tests/components/nina/fixtures/sample_warnings.json b/tests/components/nina/fixtures/sample_warnings.json new file mode 100644 index 00000000000..d53fecffa63 --- /dev/null +++ b/tests/components/nina/fixtures/sample_warnings.json @@ -0,0 +1,44 @@ +[ + { + "id": "mow.DE-BW-S-SE018-20211102-18-001", + "payload": { + "version": 1, + "type": "ALERT", + "id": "mow.DE-BW-S-SE018-20211102-18-001", + "hash": "cae97b1c11bde900017305f681904ad5a6e8fd1c841241ced524b83eaa3522f4", + "data": { + "headline": "Corona-Verordnung des Landes: Warnstufe durch Landesgesundheitsamt ausgerufen", + "provider": "MOWAS", + "severity": "Minor", + "msgType": "Update", + "transKeys": {"event": "BBK-EVC-040"}, + "area": {"type": "ZGEM", "data": "9956+1102,100001"} + } + }, + "i18nTitle": { + "de": "Corona-Verordnung des Landes: Warnstufe durch Landesgesundheitsamt ausgerufen" + }, + "sent": "2021-11-02T20:07:16+01:00" + }, + { + "id": "mow.DE-NW-BN-SE030-20201014-30-000", + "payload": { + "version": 1, + "type": "ALERT", + "id": "mow.DE-NW-BN-SE030-20201014-30-000", + "hash": "551db820a43be7e4f39283e1dfb71b212cd520c3ee478d44f43519e9c48fde4c", + "data": { + "headline": "Ausfall Notruf 112", + "provider": "MOWAS", + "severity": "Minor", + "msgType": "Update", + "transKeys": {"event": "BBK-EVC-040"}, + "area": {"type": "ZGEM", "data": "1+11057,100001"} + } + }, + "i18nTitle": {"de": "Ausfall Notruf 112"}, + "start": "2021-11-01T05:20:00+01:00", + "sent": "2021-10-11T05:20:00+01:00", + "expires": "3021-11-22T05:19:00+01:00" + } +] \ No newline at end of file diff --git a/tests/components/nina/test_binary_sensor.py b/tests/components/nina/test_binary_sensor.py new file mode 100644 index 00000000000..8016a255d46 --- /dev/null +++ b/tests/components/nina/test_binary_sensor.py @@ -0,0 +1,235 @@ +"""Test the Nina binary sensor.""" +import json +from typing import Any, Dict +from unittest.mock import patch + +from pynina import ApiError + +from homeassistant.components.binary_sensor import BinarySensorDeviceClass +from homeassistant.components.nina.const import ( + ATTR_EXPIRES, + ATTR_HEADLINE, + ATTR_ID, + ATTR_SENT, + ATTR_START, + DOMAIN, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry, load_fixture + +ENTRY_DATA: Dict[str, Any] = { + "slots": 5, + "corona_filter": True, + "regions": {"083350000000": "Aach, Stadt"}, +} + +ENTRY_DATA_NO_CORONA: Dict[str, Any] = { + "slots": 5, + "corona_filter": False, + "regions": {"083350000000": "Aach, Stadt"}, +} + + +async def test_sensors(hass: HomeAssistant) -> None: + """Test the creation and values of the NINA sensors.""" + + dummy_response: Dict[str, Any] = json.loads( + load_fixture("sample_warnings.json", "nina") + ) + + with patch( + "pynina.baseApi.BaseAPI._makeRequest", + return_value=dummy_response, + ): + + conf_entry: MockConfigEntry = MockConfigEntry( + domain=DOMAIN, title="NINA", data=ENTRY_DATA + ) + + entity_registry: er = er.async_get(hass) + conf_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(conf_entry.entry_id) + await hass.async_block_till_done() + + assert conf_entry.state == ConfigEntryState.LOADED + + state_w1 = hass.states.get("binary_sensor.warning_aach_stadt_1") + entry_w1 = entity_registry.async_get("binary_sensor.warning_aach_stadt_1") + + assert state_w1.state == STATE_ON + assert state_w1.attributes.get(ATTR_HEADLINE) == "Ausfall Notruf 112" + assert state_w1.attributes.get(ATTR_ID) == "mow.DE-NW-BN-SE030-20201014-30-000" + assert state_w1.attributes.get(ATTR_SENT) == "2021-10-11T05:20:00+01:00" + assert state_w1.attributes.get(ATTR_START) == "2021-11-01T05:20:00+01:00" + assert state_w1.attributes.get(ATTR_EXPIRES) == "3021-11-22T05:19:00+01:00" + + assert entry_w1.unique_id == "083350000000-1" + assert state_w1.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY + + state_w2 = hass.states.get("binary_sensor.warning_aach_stadt_2") + entry_w2 = entity_registry.async_get("binary_sensor.warning_aach_stadt_2") + + assert state_w2.state == STATE_OFF + assert state_w2.attributes.get(ATTR_HEADLINE) is None + assert state_w2.attributes.get(ATTR_ID) is None + assert state_w2.attributes.get(ATTR_SENT) is None + assert state_w2.attributes.get(ATTR_START) is None + assert state_w2.attributes.get(ATTR_EXPIRES) is None + + assert entry_w2.unique_id == "083350000000-2" + assert state_w2.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY + + state_w3 = hass.states.get("binary_sensor.warning_aach_stadt_3") + entry_w3 = entity_registry.async_get("binary_sensor.warning_aach_stadt_3") + + assert state_w3.state == STATE_OFF + assert state_w3.attributes.get(ATTR_HEADLINE) is None + assert state_w3.attributes.get(ATTR_ID) is None + assert state_w3.attributes.get(ATTR_SENT) is None + assert state_w3.attributes.get(ATTR_START) is None + assert state_w3.attributes.get(ATTR_EXPIRES) is None + + assert entry_w3.unique_id == "083350000000-3" + assert state_w3.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY + + state_w4 = hass.states.get("binary_sensor.warning_aach_stadt_4") + entry_w4 = entity_registry.async_get("binary_sensor.warning_aach_stadt_4") + + assert state_w4.state == STATE_OFF + assert state_w4.attributes.get(ATTR_HEADLINE) is None + assert state_w4.attributes.get(ATTR_ID) is None + assert state_w4.attributes.get(ATTR_SENT) is None + assert state_w4.attributes.get(ATTR_START) is None + assert state_w4.attributes.get(ATTR_EXPIRES) is None + + assert entry_w4.unique_id == "083350000000-4" + assert state_w4.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY + + state_w5 = hass.states.get("binary_sensor.warning_aach_stadt_5") + entry_w5 = entity_registry.async_get("binary_sensor.warning_aach_stadt_5") + + assert state_w5.state == STATE_OFF + assert state_w5.attributes.get(ATTR_HEADLINE) is None + assert state_w5.attributes.get(ATTR_ID) is None + assert state_w5.attributes.get(ATTR_SENT) is None + assert state_w5.attributes.get(ATTR_START) is None + assert state_w5.attributes.get(ATTR_EXPIRES) is None + + assert entry_w5.unique_id == "083350000000-5" + assert state_w5.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY + + +async def test_sensors_without_corona_filter(hass: HomeAssistant) -> None: + """Test the creation and values of the NINA sensors without the corona filter.""" + + dummy_response: Dict[str, Any] = json.loads( + load_fixture("nina/sample_warnings.json") + ) + + with patch( + "pynina.baseApi.BaseAPI._makeRequest", + return_value=dummy_response, + ): + + conf_entry: MockConfigEntry = MockConfigEntry( + domain=DOMAIN, title="NINA", data=ENTRY_DATA_NO_CORONA + ) + + entity_registry: er = er.async_get(hass) + conf_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(conf_entry.entry_id) + await hass.async_block_till_done() + + assert conf_entry.state == ConfigEntryState.LOADED + + state_w1 = hass.states.get("binary_sensor.warning_aach_stadt_1") + entry_w1 = entity_registry.async_get("binary_sensor.warning_aach_stadt_1") + + assert state_w1.state == STATE_ON + assert ( + state_w1.attributes.get(ATTR_HEADLINE) + == "Corona-Verordnung des Landes: Warnstufe durch Landesgesundheitsamt ausgerufen" + ) + assert state_w1.attributes.get(ATTR_ID) == "mow.DE-BW-S-SE018-20211102-18-001" + assert state_w1.attributes.get(ATTR_SENT) == "2021-11-02T20:07:16+01:00" + assert state_w1.attributes.get(ATTR_START) == "" + assert state_w1.attributes.get(ATTR_EXPIRES) == "" + + assert entry_w1.unique_id == "083350000000-1" + assert state_w1.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY + + state_w2 = hass.states.get("binary_sensor.warning_aach_stadt_2") + entry_w2 = entity_registry.async_get("binary_sensor.warning_aach_stadt_2") + + assert state_w2.state == STATE_ON + assert state_w2.attributes.get(ATTR_HEADLINE) == "Ausfall Notruf 112" + assert state_w2.attributes.get(ATTR_ID) == "mow.DE-NW-BN-SE030-20201014-30-000" + assert state_w2.attributes.get(ATTR_SENT) == "2021-10-11T05:20:00+01:00" + assert state_w2.attributes.get(ATTR_START) == "2021-11-01T05:20:00+01:00" + assert state_w2.attributes.get(ATTR_EXPIRES) == "3021-11-22T05:19:00+01:00" + + assert entry_w2.unique_id == "083350000000-2" + assert state_w2.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY + + state_w3 = hass.states.get("binary_sensor.warning_aach_stadt_3") + entry_w3 = entity_registry.async_get("binary_sensor.warning_aach_stadt_3") + + assert state_w3.state == STATE_OFF + assert state_w3.attributes.get(ATTR_HEADLINE) is None + assert state_w3.attributes.get(ATTR_ID) is None + assert state_w3.attributes.get(ATTR_SENT) is None + assert state_w3.attributes.get(ATTR_START) is None + assert state_w3.attributes.get(ATTR_EXPIRES) is None + + assert entry_w3.unique_id == "083350000000-3" + assert state_w3.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY + + state_w4 = hass.states.get("binary_sensor.warning_aach_stadt_4") + entry_w4 = entity_registry.async_get("binary_sensor.warning_aach_stadt_4") + + assert state_w4.state == STATE_OFF + assert state_w4.attributes.get(ATTR_HEADLINE) is None + assert state_w4.attributes.get(ATTR_ID) is None + assert state_w4.attributes.get(ATTR_SENT) is None + assert state_w4.attributes.get(ATTR_START) is None + assert state_w4.attributes.get(ATTR_EXPIRES) is None + + assert entry_w4.unique_id == "083350000000-4" + assert state_w4.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY + + state_w5 = hass.states.get("binary_sensor.warning_aach_stadt_5") + entry_w5 = entity_registry.async_get("binary_sensor.warning_aach_stadt_5") + + assert state_w5.state == STATE_OFF + assert state_w5.attributes.get(ATTR_HEADLINE) is None + assert state_w5.attributes.get(ATTR_ID) is None + assert state_w5.attributes.get(ATTR_SENT) is None + assert state_w5.attributes.get(ATTR_START) is None + assert state_w5.attributes.get(ATTR_EXPIRES) is None + + assert entry_w5.unique_id == "083350000000-5" + assert state_w5.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY + + +async def test_sensors_connection_error(hass: HomeAssistant) -> None: + """Test the creation and values of the NINA sensors with no connected.""" + with patch( + "pynina.baseApi.BaseAPI._makeRequest", + side_effect=ApiError("Could not connect to Api"), + ): + conf_entry: MockConfigEntry = MockConfigEntry( + domain=DOMAIN, title="NINA", data=ENTRY_DATA + ) + + conf_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(conf_entry.entry_id) + await hass.async_block_till_done() + + assert conf_entry.state == ConfigEntryState.SETUP_RETRY diff --git a/tests/components/nina/test_config_flow.py b/tests/components/nina/test_config_flow.py new file mode 100644 index 00000000000..052c1adeb38 --- /dev/null +++ b/tests/components/nina/test_config_flow.py @@ -0,0 +1,130 @@ +"""Test the Nina config flow.""" +from __future__ import annotations + +import json +from typing import Any +from unittest.mock import patch + +from pynina import ApiError + +from homeassistant import data_entry_flow +from homeassistant.components.nina.const import ( + CONF_FILTER_CORONA, + CONF_MESSAGE_SLOTS, + CONST_REGION_A_TO_D, + CONST_REGION_E_TO_H, + CONST_REGION_I_TO_L, + CONST_REGION_M_TO_Q, + CONST_REGION_R_TO_U, + CONST_REGION_V_TO_Z, + DOMAIN, +) +from homeassistant.config_entries import SOURCE_USER +from homeassistant.core import HomeAssistant + +from tests.common import load_fixture + +DUMMY_DATA: dict[str, Any] = { + CONF_MESSAGE_SLOTS: 5, + CONST_REGION_A_TO_D: ["095760000000_0", "095760000000_1"], + CONST_REGION_E_TO_H: ["010610000000_0", "010610000000_1"], + CONST_REGION_I_TO_L: ["071320000000_0", "071320000000_1"], + CONST_REGION_M_TO_Q: ["071380000000_0", "071380000000_1"], + CONST_REGION_R_TO_U: ["072320000000_0", "072320000000_1"], + CONST_REGION_V_TO_Z: ["081270000000_0", "081270000000_1"], + CONF_FILTER_CORONA: True, +} + +DUMMY_RESPONSE: dict[str, Any] = json.loads(load_fixture("sample_regions.json", "nina")) + + +async def test_show_set_form(hass: HomeAssistant) -> None: + """Test that the setup form is served.""" + with patch( + "pynina.baseApi.BaseAPI._makeRequest", + return_value=DUMMY_RESPONSE, + ): + + result: dict[str, Any] = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + +async def test_step_user_connection_error(hass: HomeAssistant) -> None: + """Test starting a flow by user but no connection.""" + with patch( + "pynina.baseApi.BaseAPI._makeRequest", + side_effect=ApiError("Could not connect to Api"), + ): + + result: dict[str, Any] = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=DUMMY_DATA + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_step_user_unexpected_exception(hass: HomeAssistant) -> None: + """Test starting a flow by user but with an unexpected exception.""" + with patch( + "pynina.baseApi.BaseAPI._makeRequest", + side_effect=Exception("DUMMY"), + ): + + result: dict[str, Any] = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=DUMMY_DATA + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + + +async def test_step_user(hass: HomeAssistant) -> None: + """Test starting a flow by user with valid values.""" + with patch( + "pynina.baseApi.BaseAPI._makeRequest", + return_value=DUMMY_RESPONSE, + ): + + result: dict[str, Any] = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=DUMMY_DATA + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "NINA" + + +async def test_step_user_no_selection(hass: HomeAssistant) -> None: + """Test starting a flow by user with no selection.""" + with patch( + "pynina.baseApi.BaseAPI._makeRequest", + return_value=DUMMY_RESPONSE, + ): + + result: dict[str, Any] = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "no_selection"} + + +async def test_step_user_already_configured(hass: HomeAssistant) -> None: + """Test starting a flow by user but it was already configured.""" + with patch( + "pynina.baseApi.BaseAPI._makeRequest", + return_value=DUMMY_RESPONSE, + ): + result: dict[str, Any] = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=DUMMY_DATA + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=DUMMY_DATA + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT diff --git a/tests/components/nina/test_init.py b/tests/components/nina/test_init.py new file mode 100644 index 00000000000..4246c014748 --- /dev/null +++ b/tests/components/nina/test_init.py @@ -0,0 +1,46 @@ +"""Test the Nina init file.""" +import json +from typing import Any, Dict +from unittest.mock import patch + +from homeassistant.components.nina.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry, load_fixture + +ENTRY_DATA: Dict[str, Any] = { + "slots": 5, + "corona_filter": True, + "regions": {"083350000000": "Aach, Stadt"}, +} + + +async def init_integration(hass) -> MockConfigEntry: + """Set up the NINA integration in Home Assistant.""" + + dummy_response: Dict[str, Any] = json.loads( + load_fixture("sample_warnings.json", "nina") + ) + + with patch( + "pynina.baseApi.BaseAPI._makeRequest", + return_value=dummy_response, + ): + + entry: MockConfigEntry = MockConfigEntry( + domain=DOMAIN, title="NINA", data=ENTRY_DATA + ) + entry.add_to_hass(hass) + + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + return entry + + +async def test_config_entry_not_ready(hass: HomeAssistant) -> None: + """Test the configuration entry.""" + entry: MockConfigEntry = await init_integration(hass) + + assert entry.state == ConfigEntryState.LOADED From f5d7adc0182a764d1c39e17d7b2a544b8b12c5a7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Dec 2021 06:09:18 -1000 Subject: [PATCH 0042/2644] Fix lutron caseta discovery with newer firmwares (#61029) --- homeassistant/components/lutron_caseta/config_flow.py | 2 +- tests/components/lutron_caseta/test_config_flow.py | 6 +++--- tests/components/lutron_caseta/test_device_trigger.py | 8 -------- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/lutron_caseta/config_flow.py b/homeassistant/components/lutron_caseta/config_flow.py index d75fc77c66e..b198d5ddbee 100644 --- a/homeassistant/components/lutron_caseta/config_flow.py +++ b/homeassistant/components/lutron_caseta/config_flow.py @@ -68,7 +68,7 @@ class LutronCasetaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle a flow initialized by zeroconf discovery.""" hostname = discovery_info.hostname - if hostname is None or not hostname.startswith("lutron-"): + if hostname is None or not hostname.lower().startswith("lutron-"): return self.async_abort(reason="not_lutron_device") self.lutron_id = hostname.split("-")[1].replace(".local.", "") diff --git a/tests/components/lutron_caseta/test_config_flow.py b/tests/components/lutron_caseta/test_config_flow.py index e22d759c1b3..2b947c36982 100644 --- a/tests/components/lutron_caseta/test_config_flow.py +++ b/tests/components/lutron_caseta/test_config_flow.py @@ -427,7 +427,7 @@ async def test_zeroconf_host_already_configured(hass, tmpdir): context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="1.1.1.1", - hostname="lutron-abc.local.", + hostname="LuTrOn-abc.local.", name="mock_name", port=None, properties={}, @@ -454,7 +454,7 @@ async def test_zeroconf_lutron_id_already_configured(hass): context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="1.1.1.1", - hostname="lutron-abc.local.", + hostname="LuTrOn-abc.local.", name="mock_name", port=None, properties={}, @@ -504,7 +504,7 @@ async def test_zeroconf(hass, source, tmpdir): context={"source": source}, data=zeroconf.ZeroconfServiceInfo( host="1.1.1.1", - hostname="lutron-abc.local.", + hostname="LuTrOn-abc.local.", name="mock_name", port=None, properties={}, diff --git a/tests/components/lutron_caseta/test_device_trigger.py b/tests/components/lutron_caseta/test_device_trigger.py index 32d6eb3dc5f..23faa929574 100644 --- a/tests/components/lutron_caseta/test_device_trigger.py +++ b/tests/components/lutron_caseta/test_device_trigger.py @@ -335,11 +335,3 @@ async def test_validate_trigger_invalid_triggers(hass, device_reg): ] }, ) - - assert ( - len(entity_ids := hass.states.async_entity_ids("persistent_notification")) == 1 - ) - assert ( - "The following integrations and platforms could not be set up" - in hass.states.get(entity_ids[0]).attributes["message"] - ) From 5efb88f3f127d6a67a3e2df3b0b0db20ad5276d6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 5 Dec 2021 17:09:37 +0100 Subject: [PATCH 0043/2644] Remove deprecated YAML configuration from DSMR (#61008) --- homeassistant/components/dsmr/config_flow.py | 26 --- homeassistant/components/dsmr/sensor.py | 44 +--- tests/components/dsmr/test_config_flow.py | 227 ------------------- tests/components/dsmr/test_sensor.py | 41 ---- 4 files changed, 3 insertions(+), 335 deletions(-) diff --git a/homeassistant/components/dsmr/config_flow.py b/homeassistant/components/dsmr/config_flow.py index 9670aab21cf..587d51d13c7 100644 --- a/homeassistant/components/dsmr/config_flow.py +++ b/homeassistant/components/dsmr/config_flow.py @@ -19,7 +19,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.typing import ConfigType from .const import ( CONF_DSMR_VERSION, @@ -303,31 +302,6 @@ class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return data - async def async_step_import(self, import_config: ConfigType) -> FlowResult: - """Handle the initial step.""" - host = import_config.get(CONF_HOST) - port = import_config[CONF_PORT] - - status = self._abort_if_host_port_configured(port, host, import_config) - if status is not None: - return status - - try: - info = await _validate_dsmr_connection(self.hass, import_config) - except CannotConnect: - return self.async_abort(reason="cannot_connect") - except CannotCommunicate: - return self.async_abort(reason="cannot_communicate") - - name = f"{host}:{port}" if host is not None else port - data = {**import_config, **info} - - if info[CONF_SERIAL_ID]: - await self.async_set_unique_id(info[CONF_SERIAL_ID]) - self._abort_if_unique_id_configured(data) - - return self.async_create_entry(title=name, data=data) - class DSMROptionFlowHandler(config_entries.OptionsFlow): """Handle options.""" diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index c016a25ad55..94ae2864905 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -6,16 +6,14 @@ from asyncio import CancelledError from contextlib import suppress from datetime import timedelta from functools import partial -from typing import Any from dsmr_parser import obis_references as obis_ref from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader from dsmr_parser.objects import DSMRObject import serial -import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_PORT, @@ -23,10 +21,9 @@ from homeassistant.const import ( VOLUME_CUBIC_METERS, ) from homeassistant.core import CoreState, HomeAssistant, callback -from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, EventType, StateType +from homeassistant.helpers.typing import EventType, StateType from homeassistant.util import Throttle from .const import ( @@ -37,55 +34,20 @@ from .const import ( CONF_SERIAL_ID_GAS, CONF_TIME_BETWEEN_UPDATE, DATA_TASK, - DEFAULT_DSMR_VERSION, - DEFAULT_PORT, DEFAULT_PRECISION, DEFAULT_RECONNECT_INTERVAL, DEFAULT_TIME_BETWEEN_UPDATE, DEVICE_NAME_ENERGY, DEVICE_NAME_GAS, DOMAIN, - DSMR_VERSIONS, LOGGER, SENSORS, ) from .models import DSMRSensorEntityDescription -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.string, - vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_DSMR_VERSION, default=DEFAULT_DSMR_VERSION): vol.All( - cv.string, vol.In(DSMR_VERSIONS) - ), - vol.Optional(CONF_RECONNECT_INTERVAL, default=DEFAULT_RECONNECT_INTERVAL): int, - vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): vol.Coerce(int), - } -) - UNIT_CONVERSION = {"m3": VOLUME_CUBIC_METERS} -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: dict[str, Any] | None = None, -) -> None: - """Import the platform into a config entry.""" - LOGGER.warning( - "Configuration of the DSMR platform in YAML is deprecated and will be " - "removed in Home Assistant 2021.9; Your existing configuration " - "has been imported into the UI automatically and can be safely removed " - "from your configuration.yaml file" - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config - ) - ) - - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: diff --git a/tests/components/dsmr/test_config_flow.py b/tests/components/dsmr/test_config_flow.py index 9a2d1fe8481..02c27369f09 100644 --- a/tests/components/dsmr/test_config_flow.py +++ b/tests/components/dsmr/test_config_flow.py @@ -1,5 +1,4 @@ """Test the DSMR config flow.""" -import asyncio from itertools import chain, repeat import os from unittest.mock import DEFAULT, AsyncMock, MagicMock, patch, sentinel @@ -225,188 +224,6 @@ async def test_setup_serial_wrong_telegram( assert result["errors"] == {"base": "cannot_communicate"} -async def test_import_usb(hass, dsmr_connection_send_validate_fixture): - """Test we can import.""" - - entry_data = { - "port": "/dev/ttyUSB0", - "dsmr_version": "2.2", - "precision": 4, - "reconnect_interval": 30, - } - - with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=entry_data, - ) - - assert result["type"] == "create_entry" - assert result["title"] == "/dev/ttyUSB0" - assert result["data"] == {**entry_data, **SERIAL_DATA} - - -async def test_import_usb_failed_connection( - hass, dsmr_connection_send_validate_fixture -): - """Test we can import.""" - (connection_factory, transport, protocol) = dsmr_connection_send_validate_fixture - - entry_data = { - "port": "/dev/ttyUSB0", - "dsmr_version": "2.2", - "precision": 4, - "reconnect_interval": 30, - } - - # override the mock to have it fail the first time and succeed after - first_fail_connection_factory = AsyncMock( - return_value=(transport, protocol), - side_effect=chain([serial.serialutil.SerialException], repeat(DEFAULT)), - ) - - with patch( - "homeassistant.components.dsmr.async_setup_entry", return_value=True - ), patch( - "homeassistant.components.dsmr.config_flow.create_dsmr_reader", - first_fail_connection_factory, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=entry_data, - ) - - assert result["type"] == "abort" - assert result["reason"] == "cannot_connect" - - -async def test_import_usb_no_data(hass, dsmr_connection_send_validate_fixture): - """Test we can import.""" - (connection_factory, transport, protocol) = dsmr_connection_send_validate_fixture - - entry_data = { - "port": "/dev/ttyUSB0", - "dsmr_version": "2.2", - "precision": 4, - "reconnect_interval": 30, - } - - # override the mock to have it fail the first time and succeed after - wait_closed = AsyncMock( - return_value=(transport, protocol), - side_effect=chain([asyncio.TimeoutError], repeat(DEFAULT)), - ) - - protocol.wait_closed = wait_closed - - with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=entry_data, - ) - - assert result["type"] == "abort" - assert result["reason"] == "cannot_communicate" - - -async def test_import_usb_wrong_telegram(hass, dsmr_connection_send_validate_fixture): - """Test we can import.""" - (connection_factory, transport, protocol) = dsmr_connection_send_validate_fixture - - entry_data = { - "port": "/dev/ttyUSB0", - "dsmr_version": "2.2", - "precision": 4, - "reconnect_interval": 30, - } - - protocol.telegram = {} - - with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=entry_data, - ) - - assert result["type"] == "abort" - assert result["reason"] == "cannot_communicate" - - -async def test_import_network(hass, dsmr_connection_send_validate_fixture): - """Test we can import from network.""" - - entry_data = { - "host": "localhost", - "port": "1234", - "dsmr_version": "2.2", - "precision": 4, - "reconnect_interval": 30, - } - - with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=entry_data, - ) - - assert result["type"] == "create_entry" - assert result["title"] == "localhost:1234" - assert result["data"] == {**entry_data, **SERIAL_DATA} - - -async def test_import_update(hass, dsmr_connection_send_validate_fixture): - """Test we can import.""" - - entry_data = { - "port": "/dev/ttyUSB0", - "dsmr_version": "2.2", - "precision": 4, - "reconnect_interval": 30, - } - - entry = MockConfigEntry( - domain=DOMAIN, - data=entry_data, - unique_id="/dev/ttyUSB0", - ) - entry.add_to_hass(hass) - - with patch( - "homeassistant.components.dsmr.async_setup_entry", return_value=True - ), patch("homeassistant.components.dsmr.async_unload_entry", return_value=True): - await hass.config_entries.async_setup(entry.entry_id) - - await hass.async_block_till_done() - - new_entry_data = { - "port": "/dev/ttyUSB0", - "dsmr_version": "2.2", - "precision": 3, - "reconnect_interval": 30, - } - - with patch( - "homeassistant.components.dsmr.async_setup_entry", return_value=True - ), patch("homeassistant.components.dsmr.async_unload_entry", return_value=True): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=new_entry_data, - ) - - await hass.async_block_till_done() - - assert result["type"] == "abort" - assert result["reason"] == "already_configured" - - assert entry.data["precision"] == 3 - - async def test_options_flow(hass): """Test options flow.""" @@ -446,50 +263,6 @@ async def test_options_flow(hass): assert entry.options == {"time_between_update": 15} -async def test_import_luxembourg(hass, dsmr_connection_send_validate_fixture): - """Test we can import.""" - - entry_data = { - "port": "/dev/ttyUSB0", - "dsmr_version": "5L", - "precision": 4, - "reconnect_interval": 30, - } - - with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=entry_data, - ) - - assert result["type"] == "create_entry" - assert result["title"] == "/dev/ttyUSB0" - assert result["data"] == {**entry_data, **SERIAL_DATA} - - -async def test_import_sweden(hass, dsmr_connection_send_validate_fixture): - """Test we can import.""" - - entry_data = { - "port": "/dev/ttyUSB0", - "dsmr_version": "5S", - "precision": 4, - "reconnect_interval": 30, - } - - with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=entry_data, - ) - - assert result["type"] == "create_entry" - assert result["title"] == "/dev/ttyUSB0" - assert result["data"] == {**entry_data, **SERIAL_DATA_SWEDEN} - - def test_get_serial_by_id_no_dir(): """Test serial by id conversion if there's no /dev/serial/by-id.""" p1 = patch("os.path.isdir", MagicMock(return_value=False)) diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 19ac6dc5d1c..b0b1f9c1183 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -12,10 +12,8 @@ from itertools import chain, repeat from unittest.mock import DEFAULT, MagicMock from homeassistant import config_entries -from homeassistant.components.dsmr.const import DOMAIN from homeassistant.components.sensor import ( ATTR_STATE_CLASS, - DOMAIN as SENSOR_DOMAIN, SensorDeviceClass, SensorStateClass, ) @@ -28,49 +26,10 @@ from homeassistant.const import ( VOLUME_CUBIC_METERS, ) from homeassistant.helpers import entity_registry as er -from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, patch -async def test_setup_platform(hass, dsmr_connection_fixture): - """Test setup of platform.""" - async_add_entities = MagicMock() - - entry_data = { - "platform": DOMAIN, - "port": "/dev/ttyUSB0", - "dsmr_version": "2.2", - "precision": 4, - "reconnect_interval": 30, - } - - serial_data = {"serial_id": "1234", "serial_id_gas": "5678"} - - with patch( - "homeassistant.components.dsmr.async_setup_entry", return_value=True - ), patch( - "homeassistant.components.dsmr.config_flow._validate_dsmr_connection", - return_value=serial_data, - ): - assert await async_setup_component( - hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: entry_data} - ) - await hass.async_block_till_done() - - assert not async_add_entities.called - - # Check config entry - conf_entries = hass.config_entries.async_entries(DOMAIN) - - assert len(conf_entries) == 1 - - entry = conf_entries[0] - - assert entry.state == config_entries.ConfigEntryState.LOADED - assert entry.data == {**entry_data, **serial_data} - - async def test_default_setup(hass, dsmr_connection_fixture): """Test the default setup.""" (connection_factory, transport, protocol) = dsmr_connection_fixture From bf1cacf4b27ce44af040fb12f9c4533c2eb5f3b1 Mon Sep 17 00:00:00 2001 From: Tim Rightnour <6556271+garbled1@users.noreply.github.com> Date: Sun, 5 Dec 2021 09:22:13 -0700 Subject: [PATCH 0044/2644] Address late review of Balboa (#61004) * Initial fixes from review of balboa climate * Minor fixes from review --- homeassistant/components/balboa/__init__.py | 8 ++++---- homeassistant/components/balboa/binary_sensor.py | 6 ++---- homeassistant/components/balboa/climate.py | 10 ++++------ homeassistant/components/balboa/config_flow.py | 8 +++++--- homeassistant/components/balboa/entity.py | 2 +- tests/components/balboa/test_climate.py | 5 ++--- 6 files changed, 18 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/balboa/__init__.py b/homeassistant/components/balboa/__init__.py index 0922218aa5c..731f7b2c2d1 100644 --- a/homeassistant/components/balboa/__init__.py +++ b/homeassistant/components/balboa/__init__.py @@ -1,6 +1,6 @@ """The Balboa Spa Client integration.""" import asyncio -from datetime import timedelta +from datetime import datetime, timedelta import time from pybalboa import BalboaSpaWifi @@ -52,7 +52,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: spa.new_data_cb = _async_balboa_update_cb _LOGGER.debug("Starting listener and monitor tasks") - hass.loop.create_task(spa.listen()) + asyncio.create_task(spa.listen()) await spa.spa_configured() asyncio.create_task(spa.check_connection_status()) @@ -92,11 +92,11 @@ async def async_setup_time_sync(hass: HomeAssistant, entry: ConfigEntry) -> None _LOGGER.debug("Setting up daily time sync") spa = hass.data[DOMAIN][entry.entry_id] - async def sync_time(): + async def sync_time(now: datetime): _LOGGER.debug("Syncing time with Home Assistant") await spa.set_time(time.strptime(str(dt_util.now()), "%Y-%m-%d %H:%M:%S.%f%z")) - await sync_time() + await sync_time(dt_util.utcnow()) entry.async_on_unload( async_track_time_interval(hass, sync_time, SYNC_TIME_INTERVAL) ) diff --git a/homeassistant/components/balboa/binary_sensor.py b/homeassistant/components/balboa/binary_sensor.py index 133ed2da9f4..e00537439a1 100644 --- a/homeassistant/components/balboa/binary_sensor.py +++ b/homeassistant/components/balboa/binary_sensor.py @@ -18,11 +18,9 @@ FILTER_STATES = [ async def async_setup_entry(hass, entry, async_add_entities): """Set up the spa's binary sensors.""" spa = hass.data[DOMAIN][entry.entry_id] - entities = [ - BalboaSpaFilter(hass, entry, spa, FILTER, index) for index in range(1, 3) - ] + entities = [BalboaSpaFilter(entry, spa, FILTER, index) for index in range(1, 3)] if spa.have_circ_pump(): - entities.append(BalboaSpaCircPump(hass, entry, spa, CIRC_PUMP)) + entities.append(BalboaSpaCircPump(entry, spa, CIRC_PUMP)) async_add_entities(entities) diff --git a/homeassistant/components/balboa/climate.py b/homeassistant/components/balboa/climate.py index 567c65d6388..c99448a77de 100644 --- a/homeassistant/components/balboa/climate.py +++ b/homeassistant/components/balboa/climate.py @@ -25,7 +25,6 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, ) -from homeassistant.exceptions import HomeAssistantError from .const import CLIMATE, CLIMATE_SUPPORTED_FANSTATES, CLIMATE_SUPPORTED_MODES, DOMAIN from .entity import BalboaEntity @@ -36,7 +35,6 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities( [ BalboaSpaClimate( - hass, entry, hass.data[DOMAIN][entry.entry_id], CLIMATE, @@ -52,9 +50,9 @@ class BalboaSpaClimate(BalboaEntity, ClimateEntity): _attr_fan_modes = CLIMATE_SUPPORTED_FANSTATES _attr_hvac_modes = CLIMATE_SUPPORTED_MODES - def __init__(self, hass, entry, client, devtype, num=None): + def __init__(self, entry, client, devtype, num=None): """Initialize the climate entity.""" - super().__init__(hass, entry, client, devtype, num) + super().__init__(entry, client, devtype, num) self._balboa_to_ha_blower_map = { self._client.BLOWER_OFF: FAN_OFF, self._client.BLOWER_LOW: FAN_LOW, @@ -137,7 +135,7 @@ class BalboaSpaClimate(BalboaEntity, ClimateEntity): modelist = self._client.get_heatmode_stringlist() self._async_validate_mode_or_raise(preset_mode) if preset_mode not in modelist: - raise HomeAssistantError(f"{preset_mode} is not a valid preset mode") + raise ValueError(f"{preset_mode} is not a valid preset mode") await self._client.change_heatmode(modelist.index(preset_mode)) async def async_set_fan_mode(self, fan_mode): @@ -147,7 +145,7 @@ class BalboaSpaClimate(BalboaEntity, ClimateEntity): def _async_validate_mode_or_raise(self, mode): """Check that the mode can be set.""" if mode == self._client.HEATMODE_RNR: - raise HomeAssistantError(f"{mode} can only be reported but not set") + raise ValueError(f"{mode} can only be reported but not set") async def async_set_hvac_mode(self, hvac_mode): """Set new target hvac mode. diff --git a/homeassistant/components/balboa/config_flow.py b/homeassistant/components/balboa/config_flow.py index 1c91376d76e..42895e5ccd6 100644 --- a/homeassistant/components/balboa/config_flow.py +++ b/homeassistant/components/balboa/config_flow.py @@ -1,4 +1,6 @@ """Config flow for Balboa Spa Client integration.""" +import asyncio + from pybalboa import BalboaSpaWifi import voluptuous as vol @@ -26,15 +28,15 @@ async def validate_input(hass: core.HomeAssistant, data): await spa.send_mod_ident_req() await spa.send_panel_req(0, 1) - hass.loop.create_task(spa.listen()) + asyncio.create_task(spa.listen()) await spa.spa_configured() - macaddr = format_mac(spa.get_macaddr()) + mac_addr = format_mac(spa.get_macaddr()) model = spa.get_model_name() await spa.disconnect() - return {"title": model, "formatted_mac": macaddr} + return {"title": model, "formatted_mac": mac_addr} class BalboaSpaClientFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/balboa/entity.py b/homeassistant/components/balboa/entity.py index 016beadac5c..44f06350243 100644 --- a/homeassistant/components/balboa/entity.py +++ b/homeassistant/components/balboa/entity.py @@ -19,7 +19,7 @@ class BalboaEntity(Entity): _attr_should_poll = False - def __init__(self, hass, entry, client, devtype, num=None): + def __init__(self, entry, client, devtype, num=None): """Initialize the spa entity.""" self._client = client self._device_name = self._client.get_model_name() diff --git a/tests/components/balboa/test_climate.py b/tests/components/balboa/test_climate.py index 53eb0307beb..2363c35efaa 100644 --- a/tests/components/balboa/test_climate.py +++ b/tests/components/balboa/test_climate.py @@ -28,7 +28,6 @@ from homeassistant.components.climate.const import ( ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_FAHRENHEIT from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.setup import async_setup_component @@ -154,7 +153,7 @@ async def test_spa_hvac_modes(hass: HomeAssistant): assert [HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF] == modes assert state.state == HVAC_SETTINGS[heat_mode] - with pytest.raises(HomeAssistantError): + with pytest.raises(ValueError): await _patch_spa_heatmode(hass, config_entry, 2) @@ -198,7 +197,7 @@ async def test_spa_preset_modes(hass: HomeAssistant): with patch( "homeassistant.components.balboa.BalboaSpaWifi.get_heatmode", return_value=2, - ), pytest.raises(HomeAssistantError): + ), pytest.raises(ValueError): await common.async_set_preset_mode(hass, 2, ENTITY_CLIMATE) From dd4ede09c8449fc6ad8a92f6c3e8f2cafc7b6463 Mon Sep 17 00:00:00 2001 From: Tim Rightnour <6556271+garbled1@users.noreply.github.com> Date: Sun, 5 Dec 2021 09:23:22 -0700 Subject: [PATCH 0045/2644] Add Venstar runtimes and battery sensors (#60414) * Venstar: Add runtimes and battery sensors * Address review - replace classes with lambda functions * Clean up patch usage, make temperature_unit it's own function * Remove double define of entities --- homeassistant/components/venstar/__init__.py | 22 ++- homeassistant/components/venstar/const.py | 1 + homeassistant/components/venstar/sensor.py | 127 ++++++++++++------ tests/components/venstar/__init__.py | 4 + .../venstar/fixtures/colortouch_runtimes.json | 1 + .../venstar/fixtures/t2k_runtimes.json | 1 + tests/components/venstar/test_climate.py | 4 +- tests/components/venstar/test_init.py | 9 +- 8 files changed, 124 insertions(+), 45 deletions(-) create mode 100644 tests/components/venstar/fixtures/colortouch_runtimes.json create mode 100644 tests/components/venstar/fixtures/t2k_runtimes.json diff --git a/homeassistant/components/venstar/__init__.py b/homeassistant/components/venstar/__init__.py index be082d0ad85..9bd42c33203 100644 --- a/homeassistant/components/venstar/__init__.py +++ b/homeassistant/components/venstar/__init__.py @@ -1,4 +1,6 @@ """The venstar component.""" +from __future__ import annotations + import asyncio from datetime import timedelta @@ -18,7 +20,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import update_coordinator from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import _LOGGER, DOMAIN, VENSTAR_TIMEOUT +from .const import _LOGGER, DOMAIN, VENSTAR_SLEEP, VENSTAR_TIMEOUT PLATFORMS = [Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.SENSOR] @@ -78,6 +80,7 @@ class VenstarDataUpdateCoordinator(update_coordinator.DataUpdateCoordinator): update_interval=timedelta(seconds=60), ) self.client = venstar_connection + self.runtimes: list[dict[str, int]] = [] async def _async_update_data(self) -> None: """Update the state.""" @@ -89,7 +92,7 @@ class VenstarDataUpdateCoordinator(update_coordinator.DataUpdateCoordinator): ) from ex # older venstars sometimes cannot handle rapid sequential connections - await asyncio.sleep(1) + await asyncio.sleep(VENSTAR_SLEEP) try: await self.hass.async_add_executor_job(self.client.update_sensors) @@ -99,7 +102,7 @@ class VenstarDataUpdateCoordinator(update_coordinator.DataUpdateCoordinator): ) from ex # older venstars sometimes cannot handle rapid sequential connections - await asyncio.sleep(1) + await asyncio.sleep(VENSTAR_SLEEP) try: await self.hass.async_add_executor_job(self.client.update_alerts) @@ -107,6 +110,19 @@ class VenstarDataUpdateCoordinator(update_coordinator.DataUpdateCoordinator): raise update_coordinator.UpdateFailed( f"Exception during Venstar alert update: {ex}" ) from ex + + # older venstars sometimes cannot handle rapid sequential connections + await asyncio.sleep(VENSTAR_SLEEP) + + try: + self.runtimes = await self.hass.async_add_executor_job( + self.client.get_runtimes + ) + except (OSError, RequestException) as ex: + raise update_coordinator.UpdateFailed( + f"Exception during Venstar runtime update: {ex}" + ) from ex + return None diff --git a/homeassistant/components/venstar/const.py b/homeassistant/components/venstar/const.py index 999e08384dd..ec672afd714 100644 --- a/homeassistant/components/venstar/const.py +++ b/homeassistant/components/venstar/const.py @@ -25,5 +25,6 @@ HOLD_MODE_OFF = "off" HOLD_MODE_TEMPERATURE = "temperature" VENSTAR_TIMEOUT = 5 +VENSTAR_SLEEP = 1.0 _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/venstar/sensor.py b/homeassistant/components/venstar/sensor.py index dc13269f7df..d7b806927ae 100644 --- a/homeassistant/components/venstar/sensor.py +++ b/homeassistant/components/venstar/sensor.py @@ -1,9 +1,12 @@ """Representation of Venstar sensors.""" from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass +from typing import Any from homeassistant.components.sensor import ( + DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, @@ -11,19 +14,51 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.const import PERCENTAGE, TEMP_CELSIUS, TEMP_FAHRENHEIT, TIME_MINUTES from homeassistant.helpers.entity import Entity from . import VenstarDataUpdateCoordinator, VenstarEntity from .const import DOMAIN +RUNTIME_HEAT1 = "heat1" +RUNTIME_HEAT2 = "heat2" +RUNTIME_COOL1 = "cool1" +RUNTIME_COOL2 = "cool2" +RUNTIME_AUX1 = "aux1" +RUNTIME_AUX2 = "aux2" +RUNTIME_FC = "fc" +RUNTIME_OV = "ov" + +RUNTIME_DEVICES = [ + RUNTIME_HEAT1, + RUNTIME_HEAT2, + RUNTIME_COOL1, + RUNTIME_COOL2, + RUNTIME_AUX1, + RUNTIME_AUX2, + RUNTIME_FC, + RUNTIME_OV, +] + +RUNTIME_ATTRIBUTES = { + RUNTIME_HEAT1: "Heating Stage 1", + RUNTIME_HEAT2: "Heating Stage 2", + RUNTIME_COOL1: "Cooling Stage 1", + RUNTIME_COOL2: "Cooling Stage 2", + RUNTIME_AUX1: "Aux Stage 1", + RUNTIME_AUX2: "Aux Stage 2", + RUNTIME_FC: "Free Cooling", + RUNTIME_OV: "Override", +} + @dataclass class VenstarSensorTypeMixin: """Mixin for sensor required keys.""" - cls: type[VenstarSensor] - stype: str + value_fn: Callable[[Any, Any], Any] + name_fn: Callable[[Any, Any], str] + uom_fn: Callable[[Any], str] @dataclass @@ -40,21 +75,34 @@ async def async_setup_entry(hass, config_entry, async_add_entities) -> None: if not sensors: return - entities = [] - for sensor_name in sensors: entities.extend( [ - description.cls(coordinator, config_entry, description, sensor_name) + VenstarSensor(coordinator, config_entry, description, sensor_name) for description in SENSOR_ENTITIES - if coordinator.client.get_sensor(sensor_name, description.stype) + if coordinator.client.get_sensor(sensor_name, description.key) is not None ] ) + runtimes = coordinator.runtimes[-1] + for sensor_name in runtimes: + if sensor_name in RUNTIME_DEVICES: + entities.append( + VenstarSensor(coordinator, config_entry, RUNTIME_ENTITY, sensor_name) + ) + async_add_entities(entities) +def temperature_unit(coordinator: VenstarDataUpdateCoordinator) -> str: + """Return the correct unit for temperature.""" + unit = TEMP_CELSIUS + if coordinator.client.tempunits == coordinator.client.TEMPUNITS_F: + unit = TEMP_FAHRENHEIT + return unit + + class VenstarSensor(VenstarEntity, SensorEntity): """Base class for a Venstar sensor.""" @@ -78,58 +126,59 @@ class VenstarSensor(VenstarEntity, SensorEntity): """Return the unique id.""" return f"{self._config.entry_id}_{self.sensor_name.replace(' ', '_')}_{self.entity_description.key}" - -class VenstarHumiditySensor(VenstarSensor): - """Represent a Venstar humidity sensor.""" - @property def name(self): """Return the name of the device.""" - return f"{self._client.name} {self.sensor_name} Humidity" + return self.entity_description.name_fn(self.coordinator, self.sensor_name) @property def native_value(self) -> int: """Return state of the sensor.""" - return self._client.get_sensor(self.sensor_name, "hum") - - -class VenstarTemperatureSensor(VenstarSensor): - """Represent a Venstar temperature sensor.""" - - @property - def name(self): - """Return the name of the device.""" - return ( - f"{self._client.name} {self.sensor_name.replace(' Temp', '')} Temperature" - ) + return self.entity_description.value_fn(self.coordinator, self.sensor_name) @property def native_unit_of_measurement(self) -> str: """Return unit of measurement the value is expressed in.""" - if self._client.tempunits == self._client.TEMPUNITS_F: - return TEMP_FAHRENHEIT - return TEMP_CELSIUS - - @property - def native_value(self) -> float: - """Return state of the sensor.""" - return round(float(self._client.get_sensor(self.sensor_name, "temp")), 1) + return self.entity_description.uom_fn(self.coordinator) SENSOR_ENTITIES: tuple[VenstarSensorEntityDescription, ...] = ( VenstarSensorEntityDescription( - key="humidity", + key="hum", device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, - native_unit_of_measurement=PERCENTAGE, - cls=VenstarHumiditySensor, - stype="hum", + uom_fn=lambda coordinator: PERCENTAGE, + value_fn=lambda coordinator, sensor_name: coordinator.client.get_sensor( + sensor_name, "hum" + ), + name_fn=lambda coordinator, sensor_name: f"{coordinator.client.name} {sensor_name} Humidity", ), VenstarSensorEntityDescription( - key="temperature", + key="temp", device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, - cls=VenstarTemperatureSensor, - stype="temp", + uom_fn=temperature_unit, + value_fn=lambda coordinator, sensor_name: round( + float(coordinator.client.get_sensor(sensor_name, "temp")), 1 + ), + name_fn=lambda coordinator, sensor_name: f"{coordinator.client.name} {sensor_name.replace(' Temp', '')} Temperature", + ), + VenstarSensorEntityDescription( + key="battery", + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, + uom_fn=lambda coordinator: PERCENTAGE, + value_fn=lambda coordinator, sensor_name: coordinator.client.get_sensor( + sensor_name, "battery" + ), + name_fn=lambda coordinator, sensor_name: f"{coordinator.client.name} {sensor_name} Battery", ), ) + +RUNTIME_ENTITY = VenstarSensorEntityDescription( + key="runtime", + state_class=STATE_CLASS_MEASUREMENT, + uom_fn=lambda coordinator: TIME_MINUTES, + value_fn=lambda coordinator, sensor_name: coordinator.runtimes[-1][sensor_name], + name_fn=lambda coordinator, sensor_name: f"{coordinator.client.name} {RUNTIME_ATTRIBUTES[sensor_name]} Runtime", +) diff --git a/tests/components/venstar/__init__.py b/tests/components/venstar/__init__.py index 326aeeeb0e2..c7b4815c5bb 100644 --- a/tests/components/venstar/__init__.py +++ b/tests/components/venstar/__init__.py @@ -60,3 +60,7 @@ class VenstarColorTouchMock: def update_alerts(self): """Mock update_alerts.""" return True + + def get_runtimes(self): + """Mock get runtimes.""" + return {} diff --git a/tests/components/venstar/fixtures/colortouch_runtimes.json b/tests/components/venstar/fixtures/colortouch_runtimes.json new file mode 100644 index 00000000000..2ec323755c2 --- /dev/null +++ b/tests/components/venstar/fixtures/colortouch_runtimes.json @@ -0,0 +1 @@ +{"runtimes":[{"ts":1637452800,"heat1":0,"heat2":0,"cool1":156,"cool2":0,"aux1":0,"aux2":0,"fc":0},{"ts":1637539200,"heat1":0,"heat2":0,"cool1":216,"cool2":0,"aux1":0,"aux2":0,"fc":0},{"ts":1637625600,"heat1":0,"heat2":0,"cool1":234,"cool2":0,"aux1":0,"aux2":0,"fc":0},{"ts":1637712000,"heat1":0,"heat2":0,"cool1":225,"cool2":0,"aux1":0,"aux2":0,"fc":0},{"ts":1637798400,"heat1":0,"heat2":0,"cool1":153,"cool2":0,"aux1":0,"aux2":0,"fc":0},{"ts":1637884800,"heat1":0,"heat2":0,"cool1":94,"cool2":0,"aux1":0,"aux2":0,"fc":0},{"ts":1637921499,"heat1":0,"heat2":0,"cool1":12,"cool2":0,"aux1":0,"aux2":0,"fc":0}]} \ No newline at end of file diff --git a/tests/components/venstar/fixtures/t2k_runtimes.json b/tests/components/venstar/fixtures/t2k_runtimes.json new file mode 100644 index 00000000000..bea2697a387 --- /dev/null +++ b/tests/components/venstar/fixtures/t2k_runtimes.json @@ -0,0 +1 @@ +{"runtimes":[{"ts":1637452800,"heat1":0,"heat2":0,"cool1":156,"cool2":0,"aux1":0,"aux2":0,"fc":0},{"ts":1637539200,"heat1":0,"heat2":0,"cool1":216,"cool2":0,"aux1":0,"aux2":0,"fc":0},{"ts":1637625600,"heat1":0,"heat2":0,"cool1":234,"cool2":0,"aux1":0,"aux2":0,"fc":0},{"ts":1637712000,"heat1":0,"heat2":0,"cool1":225,"cool2":0,"aux1":0,"aux2":0,"fc":0},{"ts":1637798400,"heat1":0,"heat2":0,"cool1":153,"cool2":0,"aux1":0,"aux2":0,"fc":0},{"ts":1637884800,"heat1":0,"heat2":0,"cool1":94,"cool2":0,"aux1":0,"aux2":0,"fc":0},{"ts":1637921489,"heat1":0,"heat2":0,"cool1":12,"cool2":0,"aux1":0,"aux2":0,"fc":0}]} \ No newline at end of file diff --git a/tests/components/venstar/test_climate.py b/tests/components/venstar/test_climate.py index babd946073b..fe1e6141c98 100644 --- a/tests/components/venstar/test_climate.py +++ b/tests/components/venstar/test_climate.py @@ -20,7 +20,7 @@ EXPECTED_BASE_SUPPORTED_FEATURES = ( async def test_colortouch(hass): """Test interfacing with a venstar colortouch with attached humidifier.""" - with patch("homeassistant.components.onewire.sensor.asyncio.sleep"): + with patch("homeassistant.components.venstar.VENSTAR_SLEEP", new=0): await async_init_integration(hass) state = hass.states.get("climate.colortouch") @@ -56,7 +56,7 @@ async def test_colortouch(hass): async def test_t2000(hass): """Test interfacing with a venstar T2000 presently turned off.""" - with patch("homeassistant.components.onewire.sensor.asyncio.sleep"): + with patch("homeassistant.components.venstar.VENSTAR_SLEEP", new=0): await async_init_integration(hass) state = hass.states.get("climate.t2000") diff --git a/tests/components/venstar/test_init.py b/tests/components/venstar/test_init.py index b245f4eef6d..696f20ed105 100644 --- a/tests/components/venstar/test_init.py +++ b/tests/components/venstar/test_init.py @@ -37,7 +37,11 @@ async def test_setup_entry(hass: HomeAssistant): "homeassistant.components.venstar.VenstarColorTouch.update_alerts", new=VenstarColorTouchMock.update_alerts, ), patch( - "homeassistant.components.onewire.sensor.asyncio.sleep" + "homeassistant.components.venstar.VenstarColorTouch.get_runtimes", + new=VenstarColorTouchMock.get_runtimes, + ), patch( + "homeassistant.components.venstar.VENSTAR_SLEEP", + new=0, ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -72,6 +76,9 @@ async def test_setup_entry_exception(hass: HomeAssistant): ), patch( "homeassistant.components.venstar.VenstarColorTouch.update_alerts", new=VenstarColorTouchMock.update_alerts, + ), patch( + "homeassistant.components.venstar.VenstarColorTouch.get_runtimes", + new=VenstarColorTouchMock.get_runtimes, ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() From 1a842d65ce824f5fc37ffb50d24f449755a7ad58 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 5 Dec 2021 09:11:44 -0800 Subject: [PATCH 0046/2644] Remove unnecessary explicit use of OrderedDict in nest media source (#61054) Address follow up PR comments from #60073 --- homeassistant/components/nest/media_source.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/nest/media_source.py b/homeassistant/components/nest/media_source.py index 140489bd63a..4f6cd8147d9 100644 --- a/homeassistant/components/nest/media_source.py +++ b/homeassistant/components/nest/media_source.py @@ -18,7 +18,6 @@ https://developers.google.com/nest/device-access/api/camera#handle_camera_events from __future__ import annotations -from collections import OrderedDict from collections.abc import Mapping from dataclasses import dataclass import logging @@ -204,7 +203,7 @@ class NestMediaSource(MediaSource): async def _get_events(device: Device) -> Mapping[str, ImageEventBase]: """Return relevant events for the specified device.""" events = await device.event_media_manager.async_events() - return OrderedDict({e.event_id: e for e in events}) + return {e.event_id: e for e in events} def _browse_root() -> BrowseMediaSource: From 5fdcbbe0e1111475712da44e727de7c433a83030 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 5 Dec 2021 09:45:40 -0800 Subject: [PATCH 0047/2644] Fetch media for events for rendering in the nest media player (#61056) --- homeassistant/components/nest/__init__.py | 8 ++++++++ tests/components/nest/common.py | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index af3757d31da..fb39188710c 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -86,6 +86,11 @@ PLATFORMS = ["sensor", "camera", "climate"] WEB_AUTH_DOMAIN = DOMAIN INSTALLED_AUTH_DOMAIN = f"{DOMAIN}.installed" +# Fetch media for events with an in memory cache. The largest media items +# are mp4 clips at ~90kb each, so this totals a few MB per camera. +# Note: Media for events can only be published within 30 seconds of the event +EVENT_MEDIA_CACHE_SIZE = 64 + class WebAuth(config_entry_oauth2_flow.LocalOAuth2Implementation): """OAuth implementation using OAuth for web applications.""" @@ -206,6 +211,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: subscriber = await api.new_subscriber(hass, entry) if not subscriber: return False + # Keep media for last N events in memory + subscriber.cache_policy.event_cache_size = EVENT_MEDIA_CACHE_SIZE + subscriber.cache_policy.fetch = True callback = SignalUpdateCallback(hass) subscriber.set_update_callback(callback.async_handle_event) diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index eb44b19d540..a0ba813ab28 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -6,6 +6,7 @@ from unittest.mock import patch from google_nest_sdm.device_manager import DeviceManager from google_nest_sdm.event import EventMessage +from google_nest_sdm.event_media import CachePolicy from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber from homeassistant.components.nest import DOMAIN @@ -98,6 +99,11 @@ class FakeSubscriber(GoogleNestSubscriber): """Return the fake device manager.""" return self._device_manager + @property + def cache_policy(self) -> CachePolicy: + """Return the cache policy.""" + return self._device_manager.cache_policy + def stop_async(self): """No-op to stop the subscriber.""" return None From 86e8034ea0eb30a622c9cd50503d47ead569478a Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sun, 5 Dec 2021 18:46:05 +0100 Subject: [PATCH 0048/2644] Add guard for empty mac address in Hue integration (#61037) --- homeassistant/components/hue/migration.py | 7 +++---- homeassistant/components/hue/v2/device.py | 3 ++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/hue/migration.py b/homeassistant/components/hue/migration.py index 5396e646ce1..2da13ce8bf2 100644 --- a/homeassistant/components/hue/migration.py +++ b/homeassistant/components/hue/migration.py @@ -95,13 +95,12 @@ async def handle_v2_migration(hass: core.HomeAssistant, entry: ConfigEntry) -> N # handle entities attached to device for hue_dev in api.devices: zigbee = api.devices.get_zigbee_connectivity(hue_dev.id) - if not zigbee: - # not a zigbee device + if not zigbee or not zigbee.mac_address: + # not a zigbee device or invalid mac continue - mac = zigbee.mac_address # get/update existing device by V1 identifier (mac address) # the device will now have both the old and the new identifier - identifiers = {(DOMAIN, hue_dev.id), (DOMAIN, mac)} + identifiers = {(DOMAIN, hue_dev.id), (DOMAIN, zigbee.mac_address)} hass_dev = dev_reg.async_get_or_create( config_entry_id=entry.entry_id, identifiers=identifiers ) diff --git a/homeassistant/components/hue/v2/device.py b/homeassistant/components/hue/v2/device.py index 1608743cc48..64bdcc7a4f2 100644 --- a/homeassistant/components/hue/v2/device.py +++ b/homeassistant/components/hue/v2/device.py @@ -49,7 +49,8 @@ async def async_setup_devices(bridge: "HueBridge"): params[ATTR_IDENTIFIERS].add((DOMAIN, api.config.bridge_id)) else: params[ATTR_VIA_DEVICE] = (DOMAIN, api.config.bridge_device.id) - if zigbee := dev_controller.get_zigbee_connectivity(hue_device.id): + zigbee = dev_controller.get_zigbee_connectivity(hue_device.id) + if zigbee and zigbee.mac_address: params[ATTR_CONNECTIONS] = { (device_registry.CONNECTION_NETWORK_MAC, zigbee.mac_address) } From 3eeb8556795245ce7aadc6b64f5e395f2cc7211f Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sun, 5 Dec 2021 18:46:48 +0100 Subject: [PATCH 0049/2644] Fix Hue config flow (#61028) --- homeassistant/components/hue/config_flow.py | 34 +++++++++++---------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 9d4bc87889d..a77a2ff101a 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -216,16 +216,17 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if not url.hostname: return self.async_abort(reason="not_hue_bridge") - bridge = await self._get_bridge( + # abort if we already have exactly this bridge id/host + # reload the integration if the host got updated + bridge_id = normalize_bridge_id(discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL]) + await self.async_set_unique_id(bridge_id) + self._abort_if_unique_id_configured( + updates={CONF_HOST: url.hostname}, reload_on_update=True + ) + + self.bridge = await self._get_bridge( url.hostname, discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL] ) - - await self.async_set_unique_id(bridge.id) - self._abort_if_unique_id_configured( - updates={CONF_HOST: bridge.host}, reload_on_update=False - ) - - self.bridge = bridge return await self.async_step_link() async def async_step_zeroconf( @@ -236,17 +237,18 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): This flow is triggered by the Zeroconf component. It will check if the host is already configured and delegate to the import step if not. """ - bridge = await self._get_bridge( - discovery_info.host, - discovery_info.properties["bridgeid"], - ) - - await self.async_set_unique_id(bridge.id) + # abort if we already have exactly this bridge id/host + # reload the integration if the host got updated + bridge_id = normalize_bridge_id(discovery_info.properties["bridgeid"]) + await self.async_set_unique_id(bridge_id) self._abort_if_unique_id_configured( - updates={CONF_HOST: bridge.host}, reload_on_update=False + updates={CONF_HOST: discovery_info.host}, reload_on_update=True ) - self.bridge = bridge + # we need to query the other capabilities too + self.bridge = await self._get_bridge( + discovery_info.host, discovery_info.properties["bridgeid"] + ) return await self.async_step_link() async def async_step_homekit( From 11e2f516814964cd20352f3ce3418cb12c981ee7 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sun, 5 Dec 2021 18:47:24 +0100 Subject: [PATCH 0050/2644] Fix Hue migration (#61030) --- homeassistant/components/hue/migration.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/hue/migration.py b/homeassistant/components/hue/migration.py index 2da13ce8bf2..408ba3fc8e0 100644 --- a/homeassistant/components/hue/migration.py +++ b/homeassistant/components/hue/migration.py @@ -166,6 +166,9 @@ async def handle_v2_migration(hass: core.HomeAssistant, entry: ConfigEntry) -> N continue v1_id = f"/groups/{ent.unique_id}" hue_group = api.groups.room.get_by_v1_id(v1_id) + if hue_group is None or hue_group.grouped_light is None: + # try again with zone + hue_group = api.groups.zone.get_by_v1_id(v1_id) if hue_group is None or hue_group.grouped_light is None: # this may happen if we're looking at some orphaned entity LOGGER.warning( From b98bc64604ed89e11b86f92e1f160273d72b1e41 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sun, 5 Dec 2021 18:47:44 +0100 Subject: [PATCH 0051/2644] Disable options flow for Hue V2 bridges (#61045) --- homeassistant/components/hue/config_flow.py | 11 +++++------ tests/components/hue/test_config_flow.py | 10 +++------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index a77a2ff101a..ceb5a9a1a8e 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -12,7 +12,7 @@ import async_timeout import slugify as unicode_slug import voluptuous as vol -from homeassistant import config_entries +from homeassistant import config_entries, data_entry_flow from homeassistant.components import ssdp, zeroconf from homeassistant.const import CONF_API_KEY, CONF_HOST from homeassistant.core import callback @@ -48,7 +48,10 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): config_entry: config_entries.ConfigEntry, ) -> HueOptionsFlowHandler: """Get the options flow for this handler.""" - return HueOptionsFlowHandler(config_entry) + if config_entry.data.get(CONF_API_VERSION, 1) == 1: + # Options for Hue are only applicable to V1 bridges. + return HueOptionsFlowHandler(config_entry) + raise data_entry_flow.UnknownHandler def __init__(self) -> None: """Initialize the Hue flow.""" @@ -292,10 +295,6 @@ class HueOptionsFlowHandler(config_entries.OptionsFlow): if user_input is not None: return self.async_create_entry(title="", data=user_input) - if self.config_entry.data.get(CONF_API_VERSION, 1) > 1: - # Options for Hue are only applicable to V1 bridges. - return self.async_show_form(step_id="init") - return self.async_show_form( step_id="init", data_schema=vol.Schema( diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index 80e1a8909b9..65d3dd696d6 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -7,7 +7,7 @@ from aiohue.errors import LinkButtonNotPressed import pytest import voluptuous as vol -from homeassistant import config_entries +from homeassistant import config_entries, data_entry_flow from homeassistant.components import ssdp, zeroconf from homeassistant.components.hue import config_flow, const from homeassistant.components.hue.errors import CannotConnect @@ -706,12 +706,8 @@ async def test_options_flow_v2(hass): ) entry.add_to_hass(hass) - result = await hass.config_entries.options.async_init(entry.entry_id) - - assert result["type"] == "form" - assert result["step_id"] == "init" - # V2 bridge does not have config options - assert result["data_schema"] is None + with pytest.raises(data_entry_flow.UnknownHandler): + await hass.config_entries.options.async_init(entry.entry_id) async def test_bridge_zeroconf(hass, aioclient_mock): From b5e3050a2339ce71b2a344e4486c403300ac74fa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Dec 2021 07:48:03 -1000 Subject: [PATCH 0052/2644] Update flux_led models database to fix turn on for newer models (#61005) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 1f7c84e73d6..dc60a46d68c 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.25.13"], + "requirements": ["flux_led==0.25.16"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index a45251c3cea..b65d183a7d6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.25.13 +flux_led==0.25.16 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 019059669a5..b997f4cb571 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.25.13 +flux_led==0.25.16 # homeassistant.components.homekit fnvhash==0.1.0 From dc5377485b202b8cccfbc928a65c1f20b43b5598 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Sun, 5 Dec 2021 18:48:25 +0100 Subject: [PATCH 0053/2644] Use STATE_DOCKED for emptying the bin for xiaomi_miio.vacuum (#60513) --- homeassistant/components/xiaomi_miio/vacuum.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index 60d557837fb..2362fcf8996 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -92,6 +92,7 @@ STATE_CODE_TO_STATE = { 16: STATE_CLEANING, # "Going to target" 17: STATE_CLEANING, # "Zoned cleaning" 18: STATE_CLEANING, # "Segment cleaning" + 22: STATE_DOCKED, # "Emptying the bin" on s7+ 100: STATE_DOCKED, # "Charging complete" 101: STATE_ERROR, # "Device offline" } From 95f00985934e4515a6b0eef37fa7408dc361963f Mon Sep 17 00:00:00 2001 From: david reid Date: Sun, 5 Dec 2021 17:50:15 +0000 Subject: [PATCH 0054/2644] Catch ConnectionResetError (#60987) --- homeassistant/components/hassio/ingress.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 620c69f543d..6935bbdc7da 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -255,3 +255,5 @@ async def _websocket_forward(ws_from, ws_to): await ws_to.close(code=ws_to.close_code, message=msg.extra) except RuntimeError: _LOGGER.debug("Ingress Websocket runtime error") + except ConnectionResetError: + _LOGGER.debug("Ingress Websocket Connection Reset") From a4ffa631653cc6cc32e825ea6f8de75db0a1b89b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 5 Dec 2021 18:51:57 +0100 Subject: [PATCH 0055/2644] Handle unknown/unavailable state for mobile_app (#60974) --- homeassistant/components/mobile_app/entity.py | 13 ++++++++++++- homeassistant/components/mobile_app/sensor.py | 7 +++++-- tests/components/mobile_app/test_sensor.py | 14 ++++++++++++-- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mobile_app/entity.py b/homeassistant/components/mobile_app/entity.py index 03b6d95c2a2..0c26533b7cb 100644 --- a/homeassistant/components/mobile_app/entity.py +++ b/homeassistant/components/mobile_app/entity.py @@ -1,6 +1,12 @@ """A entity class for mobile_app.""" from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ICON, CONF_NAME, CONF_UNIQUE_ID, CONF_WEBHOOK_ID +from homeassistant.const import ( + ATTR_ICON, + CONF_NAME, + CONF_UNIQUE_ID, + CONF_WEBHOOK_ID, + STATE_UNAVAILABLE, +) from homeassistant.core import callback from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -101,6 +107,11 @@ class MobileAppEntity(RestoreEntity): """Return device registry information for this entity.""" return device_info(self._registration) + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._config.get(ATTR_SENSOR_STATE) != STATE_UNAVAILABLE + @callback def _handle_update(self, data): """Handle async event updates.""" diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py index b58beef96ba..0631f8f72aa 100644 --- a/homeassistant/components/mobile_app/sensor.py +++ b/homeassistant/components/mobile_app/sensor.py @@ -8,6 +8,7 @@ from homeassistant.const import ( CONF_WEBHOOK_ID, DEVICE_CLASS_DATE, DEVICE_CLASS_TIMESTAMP, + STATE_UNKNOWN, ) from homeassistant.core import callback from homeassistant.helpers import entity_registry as er @@ -88,9 +89,11 @@ class MobileAppSensor(MobileAppEntity, SensorEntity): @property def native_value(self): """Return the state of the sensor.""" + if (state := self._config[ATTR_SENSOR_STATE]) in (None, STATE_UNKNOWN): + return None + if ( - (state := self._config[ATTR_SENSOR_STATE]) is not None - and self.device_class + self.device_class in ( DEVICE_CLASS_DATE, DEVICE_CLASS_TIMESTAMP, diff --git a/tests/components/mobile_app/test_sensor.py b/tests/components/mobile_app/test_sensor.py index cfd9efa34c2..295e37ee7d9 100644 --- a/tests/components/mobile_app/test_sensor.py +++ b/tests/components/mobile_app/test_sensor.py @@ -4,7 +4,7 @@ from http import HTTPStatus import pytest from homeassistant.components.sensor import DEVICE_CLASS_DATE, DEVICE_CLASS_TIMESTAMP -from homeassistant.const import PERCENTAGE, STATE_UNKNOWN +from homeassistant.const import PERCENTAGE, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -89,7 +89,7 @@ async def test_sensor(hass, create_registrations, webhook_client): await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() unloaded_entity = hass.states.get("sensor.test_1_battery_state") - assert unloaded_entity.state == "unavailable" + assert unloaded_entity.state == STATE_UNAVAILABLE await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -295,6 +295,16 @@ async def test_update_sensor_no_state(hass, create_registrations, webhook_client "2021-11-18 20:25:00+01:00", "2021-11-18T19:25:00+00:00", ), + ( + DEVICE_CLASS_TIMESTAMP, + "unavailable", + STATE_UNAVAILABLE, + ), + ( + DEVICE_CLASS_TIMESTAMP, + "unknown", + STATE_UNKNOWN, + ), ], ) async def test_sensor_datetime( From 4144699814602e0c116bf801c2f8f26ba25edca6 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 5 Dec 2021 11:38:27 -0700 Subject: [PATCH 0056/2644] Fix mispelling in SimpliSafe service description (#61058) --- homeassistant/components/simplisafe/services.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/simplisafe/services.yaml b/homeassistant/components/simplisafe/services.yaml index bdd7939a209..3d0965b4b0b 100644 --- a/homeassistant/components/simplisafe/services.yaml +++ b/homeassistant/components/simplisafe/services.yaml @@ -1,7 +1,7 @@ # Describes the format for available SimpliSafe services clear_notifications: name: Clear notifications - description: Clear any active SimpliSafe notificiations + description: Clear any active SimpliSafe notifications fields: device_id: name: System From 5bd113986796893ab9f7c2acbc6a3443379666cd Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 5 Dec 2021 13:02:37 -0800 Subject: [PATCH 0057/2644] Fix regression in nest event media player with multiple devices (#61064) --- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nest/common.py | 36 +++----- tests/components/nest/test_config_flow_sdm.py | 12 +-- tests/components/nest/test_media_source.py | 82 ++++++++++++++++++- 6 files changed, 97 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 3a4f64877d2..a82f8395733 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["ffmpeg", "http", "media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.2"], + "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.3"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index b65d183a7d6..63bbc61ff84 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -738,7 +738,7 @@ google-cloud-pubsub==2.1.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==0.4.2 +google-nest-sdm==0.4.3 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b997f4cb571..18841a1f537 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -461,7 +461,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.1.0 # homeassistant.components.nest -google-nest-sdm==0.4.2 +google-nest-sdm==0.4.3 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index a0ba813ab28..35183a441a5 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -53,31 +53,12 @@ def create_config_entry(hass, token_expiration_time=None) -> MockConfigEntry: return config_entry -class FakeDeviceManager(DeviceManager): - """Fake DeviceManager that can supply a list of devices and structures.""" - - def __init__(self, devices: dict, structures: dict): - """Initialize FakeDeviceManager.""" - super().__init__() - self._devices = devices - - @property - def structures(self) -> dict: - """Override structures with fake result.""" - return self._structures - - @property - def devices(self) -> dict: - """Override devices with fake result.""" - return self._devices - - class FakeSubscriber(GoogleNestSubscriber): """Fake subscriber that supplies a FakeDeviceManager.""" - def __init__(self, device_manager: FakeDeviceManager): + def __init__(self): """Initialize Fake Subscriber.""" - self._device_manager = device_manager + self._device_manager = DeviceManager() def set_update_callback(self, callback: Callable[[EventMessage], Awaitable[None]]): """Capture the callback set by Home Assistant.""" @@ -121,8 +102,14 @@ async def async_setup_sdm_platform( """Set up the platform and prerequisites.""" if with_config: create_config_entry(hass) - device_manager = FakeDeviceManager(devices=devices, structures=structures) - subscriber = FakeSubscriber(device_manager) + subscriber = FakeSubscriber() + device_manager = await subscriber.async_get_device_manager() + if devices: + for device in devices.values(): + device_manager.add_device(device) + if structures: + for structure in structures.values(): + device_manager.add_structure(structure) with patch( "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation" ), patch("homeassistant.components.nest.PLATFORMS", [platform]), patch( @@ -131,4 +118,7 @@ async def async_setup_sdm_platform( ): assert await async_setup_component(hass, DOMAIN, CONFIG) await hass.async_block_till_done() + # Disabled to reduce setup burden, and enabled manually by tests that + # need to exercise this + subscriber.cache_policy.fetch = False return subscriber diff --git a/tests/components/nest/test_config_flow_sdm.py b/tests/components/nest/test_config_flow_sdm.py index 5d6987f94f7..d4af62cb255 100644 --- a/tests/components/nest/test_config_flow_sdm.py +++ b/tests/components/nest/test_config_flow_sdm.py @@ -17,7 +17,7 @@ from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow -from .common import FakeDeviceManager, FakeSubscriber, MockConfigEntry +from .common import FakeSubscriber, MockConfigEntry CLIENT_ID = "1234" CLIENT_SECRET = "5678" @@ -43,15 +43,9 @@ APP_REDIRECT_URL = "urn:ietf:wg:oauth:2.0:oob" @pytest.fixture -def device_manager() -> FakeDeviceManager: - """Create FakeDeviceManager.""" - return FakeDeviceManager(devices={}, structures={}) - - -@pytest.fixture -def subscriber(device_manager: FakeDeviceManager) -> FakeSubscriber: +def subscriber() -> FakeSubscriber: """Create FakeSubscriber.""" - return FakeSubscriber(device_manager) + return FakeSubscriber() def get_config_entry(hass): diff --git a/tests/components/nest/test_media_source.py b/tests/components/nest/test_media_source.py index 67d6ba2f229..82c87579525 100644 --- a/tests/components/nest/test_media_source.py +++ b/tests/components/nest/test_media_source.py @@ -81,7 +81,7 @@ async def async_setup_devices(hass, auth, device_type, traits={}, events=[]): return subscriber -def create_event(event_id, event_type, timestamp=None): +def create_event(event_id, event_type, timestamp=None, device_id=None): """Create an EventMessage for a single event type.""" if not timestamp: timestamp = dt_util.now() @@ -91,17 +91,19 @@ def create_event(event_id, event_type, timestamp=None): "eventId": event_id, }, } - return create_event_message(event_id, event_data, timestamp) + return create_event_message(event_id, event_data, timestamp, device_id=device_id) -def create_event_message(event_id, event_data, timestamp): +def create_event_message(event_id, event_data, timestamp, device_id=None): """Create an EventMessage for a single event type.""" + if device_id is None: + device_id = DEVICE_ID return EventMessage( { "eventId": f"{event_id}-{timestamp}", "timestamp": timestamp.isoformat(timespec="seconds"), "resourceUpdate": { - "name": DEVICE_ID, + "name": device_id, "events": event_data, }, }, @@ -568,3 +570,75 @@ async def test_media_permission_unauthorized(hass, auth, hass_client, hass_admin assert response.status == HTTPStatus.UNAUTHORIZED, ( "Response not matched: %s" % response ) + + +async def test_multiple_devices(hass, auth, hass_client): + """Test events received for multiple devices.""" + device_id1 = f"{DEVICE_ID}-1" + device_id2 = f"{DEVICE_ID}-2" + + devices = { + device_id1: Device.MakeDevice( + { + "name": device_id1, + "type": CAMERA_DEVICE_TYPE, + "traits": CAMERA_TRAITS, + }, + auth=auth, + ), + device_id2: Device.MakeDevice( + { + "name": device_id2, + "type": CAMERA_DEVICE_TYPE, + "traits": CAMERA_TRAITS, + }, + auth=auth, + ), + } + subscriber = await async_setup_sdm_platform(hass, PLATFORM, devices=devices) + + device_registry = dr.async_get(hass) + device1 = device_registry.async_get_device({(DOMAIN, device_id1)}) + assert device1 + device2 = device_registry.async_get_device({(DOMAIN, device_id2)}) + assert device2 + + # Very no events have been received yet + browse = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/{device1.id}" + ) + assert len(browse.children) == 0 + browse = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/{device2.id}" + ) + assert len(browse.children) == 0 + + # Send events for device #1 + for i in range(0, 5): + await subscriber.async_receive_event( + create_event(f"event-id-{i}", PERSON_EVENT, device_id=device_id1) + ) + + browse = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/{device1.id}" + ) + assert len(browse.children) == 5 + browse = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/{device2.id}" + ) + assert len(browse.children) == 0 + + # Send events for device #2 + for i in range(0, 3): + await subscriber.async_receive_event( + create_event(f"other-id-{i}", PERSON_EVENT, device_id=device_id2) + ) + + browse = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/{device1.id}" + ) + assert len(browse.children) == 5 + browse = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/{device2.id}" + ) + assert len(browse.children) == 3 From ab75efda9ad09177e79d490ea0bb45f07fc5ec44 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 5 Dec 2021 13:30:02 -0800 Subject: [PATCH 0058/2644] Add debug logging for pip install command (#61057) --- homeassistant/util/package.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index 609d09e4f55..a0b5c2832ad 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -93,6 +93,7 @@ def install_package( # Workaround for incompatible prefix setting # See http://stackoverflow.com/a/4495175 args += ["--prefix="] + _LOGGER.debug("Running pip command: args=%s", args) with Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env) as process: _, stderr = process.communicate() if process.returncode != 0: From ac263acb1cc9253ff6b0c3e267ecf96e207d647f Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sun, 5 Dec 2021 23:29:39 +0100 Subject: [PATCH 0059/2644] Don't use ConfigEntry update listener for Fronius (#61017) * disable `async_setup_entry` in config_flow tests * don't use config_entry update listener * add `Final` to constants * assert that an updated entry causes a reload (unload) --- homeassistant/components/fronius/__init__.py | 13 ++------ .../components/fronius/config_flow.py | 11 +++---- homeassistant/components/fronius/sensor.py | 10 +++--- tests/components/fronius/test_config_flow.py | 33 ++++++++++++++----- 4 files changed, 37 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/fronius/__init__.py b/homeassistant/components/fronius/__init__.py index b7699ea7747..23e595b71ce 100644 --- a/homeassistant/components/fronius/__init__.py +++ b/homeassistant/components/fronius/__init__.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from collections.abc import Callable import logging -from typing import TypeVar +from typing import Final, TypeVar from pyfronius import Fronius, FroniusError @@ -27,8 +27,8 @@ from .coordinator import ( FroniusStorageUpdateCoordinator, ) -_LOGGER = logging.getLogger(__name__) -PLATFORMS: list[Platform] = [Platform.SENSOR] +_LOGGER: Final = logging.getLogger(__name__) +PLATFORMS: Final = [Platform.SENSOR] FroniusCoordinatorType = TypeVar("FroniusCoordinatorType", bound=FroniusCoordinatorBase) @@ -42,8 +42,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = solar_net hass.config_entries.async_setup_platforms(entry, PLATFORMS) - # reload on config_entry update - entry.async_on_unload(entry.add_update_listener(async_update_entry)) return True @@ -58,11 +56,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -async def async_update_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Update a given config entry.""" - await hass.config_entries.async_reload(entry.entry_id) - - class FroniusSolarNet: """The FroniusSolarNet class routes received values to sensor entities.""" diff --git a/homeassistant/components/fronius/config_flow.py b/homeassistant/components/fronius/config_flow.py index 86654f00c36..ea590767359 100644 --- a/homeassistant/components/fronius/config_flow.py +++ b/homeassistant/components/fronius/config_flow.py @@ -18,7 +18,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN, FroniusConfigEntryData -_LOGGER = logging.getLogger(__name__) +_LOGGER: Final = logging.getLogger(__name__) DHCP_REQUEST_DELAY: Final = 60 @@ -102,9 +102,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "unknown" else: await self.async_set_unique_id(unique_id, raise_on_progress=False) - self._abort_if_unique_id_configured( - updates=dict(info), reload_on_update=False - ) + self._abort_if_unique_id_configured(updates=dict(info)) + return self.async_create_entry(title=create_title(info), data=info) return self.async_show_form( @@ -132,9 +131,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="invalid_host") await self.async_set_unique_id(unique_id, raise_on_progress=False) - self._abort_if_unique_id_configured( - updates=dict(self.info), reload_on_update=False - ) + self._abort_if_unique_id_configured(updates=dict(self.info)) return await self.async_step_confirm_discovery() diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index b2c6ecbb820..348ef91cd7d 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Final import voluptuous as vol @@ -48,11 +48,11 @@ if TYPE_CHECKING: FroniusStorageUpdateCoordinator, ) -_LOGGER = logging.getLogger(__name__) +_LOGGER: Final = logging.getLogger(__name__) -ELECTRIC_CHARGE_AMPERE_HOURS = "Ah" -ENERGY_VOLT_AMPERE_REACTIVE_HOUR = "varh" -POWER_VOLT_AMPERE_REACTIVE = "var" +ELECTRIC_CHARGE_AMPERE_HOURS: Final = "Ah" +ENERGY_VOLT_AMPERE_REACTIVE_HOUR: Final = "varh" +POWER_VOLT_AMPERE_REACTIVE: Final = "var" PLATFORM_SCHEMA = vol.All( PLATFORM_SCHEMA.extend( diff --git a/tests/components/fronius/test_config_flow.py b/tests/components/fronius/test_config_flow.py index 427c8e4a163..c6f2f69ce5f 100644 --- a/tests/components/fronius/test_config_flow.py +++ b/tests/components/fronius/test_config_flow.py @@ -2,6 +2,7 @@ from unittest.mock import patch from pyfronius import FroniusError +import pytest from homeassistant import config_entries from homeassistant.components.dhcp import DhcpServiceInfo @@ -20,6 +21,17 @@ from . import MOCK_HOST, mock_responses from tests.common import MockConfigEntry + +@pytest.fixture(autouse=True) +def no_setup(): + """Disable setting up the whole integration in config_flow tests.""" + with patch( + "homeassistant.components.fronius.async_setup_entry", + return_value=True, + ): + yield + + INVERTER_INFO_RETURN_VALUE = { "inverters": [ { @@ -172,7 +184,7 @@ async def test_form_unexpected(hass: HomeAssistant) -> None: assert result2["errors"] == {"base": "unknown"} -async def test_form_already_existing(hass): +async def test_form_already_existing(hass: HomeAssistant) -> None: """Test existing entry.""" MockConfigEntry( domain=DOMAIN, @@ -224,17 +236,22 @@ async def test_form_updates_host(hass, aioclient_mock): ) mock_responses(aioclient_mock, host=new_host) - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "host": new_host, - }, - ) - await hass.async_block_till_done() + with patch( + "homeassistant.components.fronius.async_unload_entry", + return_value=True, + ) as mock_unload_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": new_host, + }, + ) + await hass.async_block_till_done() assert result2["type"] == RESULT_TYPE_ABORT assert result2["reason"] == "already_configured" + mock_unload_entry.assert_called_with(hass, entry) entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 assert entries[0].data == { From 5e90e178aaa5ba63f5291b8804043f1b4a7d97b2 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sun, 5 Dec 2021 18:24:20 -0500 Subject: [PATCH 0060/2644] Prevent ZHA coordinator from showing unavailable (#61068) --- homeassistant/components/zha/core/device.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 82e2b85173e..e8b38fb9699 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -341,6 +341,9 @@ class ZHADevice(LogMixin): ) async def _check_available(self, *_): + # don't flip the availability state of the coordinator + if self.is_coordinator: + return if self.last_seen is None: self.update_available(False) return From ecdb18eb0a45cd6de90b81921926f2d0e6e9ca57 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 6 Dec 2021 00:13:37 +0000 Subject: [PATCH 0061/2644] [ci skip] Translation update --- .../components/auth/translations/fi.json | 9 +++++++ .../binary_sensor/translations/ja.json | 2 ++ .../binary_sensor/translations/ru.json | 4 ++- .../binary_sensor/translations/zh-Hant.json | 2 ++ .../components/cover/translations/ja.json | 2 +- .../components/elkm1/translations/ja.json | 1 + .../components/hangouts/translations/fi.json | 5 ++++ .../homematicip_cloud/translations/fi.json | 11 ++++++-- .../homematicip_cloud/translations/ja.json | 2 +- .../components/ifttt/translations/fi.json | 3 ++- .../components/knx/translations/de.json | 2 ++ .../components/knx/translations/hu.json | 2 ++ .../components/knx/translations/it.json | 2 ++ .../components/knx/translations/ja.json | 2 ++ .../components/knx/translations/ru.json | 2 ++ .../components/knx/translations/zh-Hant.json | 2 ++ .../components/mqtt/translations/fi.json | 6 +++-- .../components/nest/translations/fi.json | 1 + .../components/nina/translations/de.json | 27 +++++++++++++++++++ .../components/nina/translations/es.json | 9 +++++++ .../components/nina/translations/hu.json | 27 +++++++++++++++++++ .../components/nina/translations/ja.json | 27 +++++++++++++++++++ .../components/nina/translations/ru.json | 27 +++++++++++++++++++ .../components/ps4/translations/ja.json | 2 +- .../smartthings/translations/ja.json | 1 + .../components/tradfri/translations/fi.json | 5 +++- .../wolflink/translations/sensor.ja.json | 2 ++ .../components/zha/translations/ja.json | 7 +++++ 28 files changed, 184 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/nina/translations/de.json create mode 100644 homeassistant/components/nina/translations/es.json create mode 100644 homeassistant/components/nina/translations/hu.json create mode 100644 homeassistant/components/nina/translations/ja.json create mode 100644 homeassistant/components/nina/translations/ru.json diff --git a/homeassistant/components/auth/translations/fi.json b/homeassistant/components/auth/translations/fi.json index ca174d81e6e..83aeeb4538c 100644 --- a/homeassistant/components/auth/translations/fi.json +++ b/homeassistant/components/auth/translations/fi.json @@ -1,10 +1,16 @@ { "mfa_setup": { "notify": { + "abort": { + "no_available_service": "Ilmoituspalveluita ei ole saatavilla." + }, "error": { "invalid_code": "Virheellinen koodi. Yrit\u00e4 uudelleen." }, "step": { + "init": { + "description": "Valitse jokin ilmoituspalveluista:" + }, "setup": { "title": "Varmista asetukset" } @@ -12,6 +18,9 @@ "title": "Ilmoita kertaluonteinen salasana" }, "totp": { + "error": { + "invalid_code": "Virheellinen koodi, yrit\u00e4 uudelleen. Jos saat t\u00e4m\u00e4n virheen jatkuvasti, varmista, ett\u00e4 Home Assistant -j\u00e4rjestelm\u00e4si kello on ajassa." + }, "step": { "init": { "title": "M\u00e4\u00e4rit\u00e4 kaksivaiheinen todennus TOTP:n avulla" diff --git a/homeassistant/components/binary_sensor/translations/ja.json b/homeassistant/components/binary_sensor/translations/ja.json index 979d2cf966a..1a4b14b2c68 100644 --- a/homeassistant/components/binary_sensor/translations/ja.json +++ b/homeassistant/components/binary_sensor/translations/ja.json @@ -84,6 +84,7 @@ "not_powered": "{entity_name} \u306f\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u307e\u305b\u3093", "not_present": "{entity_name} \u304c\u5b58\u5728\u3057\u307e\u305b\u3093", "not_running": "{entity_name} \u306f\u3082\u3046\u5b9f\u884c\u3055\u308c\u3066\u3044\u306a\u3044", + "not_tampered": "{entity_name} \u304c\u6539\u7ac4(tampering)\u306e\u691c\u51fa\u3092\u505c\u6b62\u3057\u307e\u3057\u305f", "not_unsafe": "{entity_name} \u304c\u5b89\u5168\u306b\u306a\u308a\u307e\u3057\u305f", "occupied": "{entity_name} \u304c\u5360\u6709\u3055\u308c\u307e\u3057\u305f", "opened": "{entity_name} \u304c\u958b\u304b\u308c\u307e\u3057\u305f", @@ -94,6 +95,7 @@ "running": "{entity_name} \u306e\u5b9f\u884c\u3092\u958b\u59cb", "smoke": "{entity_name} \u304c\u7159\u306e\u691c\u51fa\u3092\u958b\u59cb\u3057\u307e\u3057\u305f", "sound": "{entity_name} \u304c\u97f3\u306e\u691c\u51fa\u3092\u958b\u59cb\u3057\u307e\u3057\u305f", + "tampered": "{entity_name} \u304c\u6539\u7ac4(tampering)\u306e\u691c\u51fa\u3092\u958b\u59cb\u3057\u307e\u3057\u305f", "turned_off": "{entity_name} \u30aa\u30d5\u306b\u306a\u308a\u307e\u3057\u305f", "turned_on": "{entity_name} \u30aa\u30f3\u306b\u306a\u3063\u3066\u3044\u307e\u3059", "unsafe": "{entity_name} \u306f\u5b89\u5168\u3067\u306f\u306a\u304f\u306a\u308a\u307e\u3057\u305f", diff --git a/homeassistant/components/binary_sensor/translations/ru.json b/homeassistant/components/binary_sensor/translations/ru.json index bb8ec9fdadb..6a30d031c70 100644 --- a/homeassistant/components/binary_sensor/translations/ru.json +++ b/homeassistant/components/binary_sensor/translations/ru.json @@ -84,6 +84,7 @@ "not_powered": "{entity_name} \u043d\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043d\u0430\u043b\u0438\u0447\u0438\u0435 \u043f\u0438\u0442\u0430\u043d\u0438\u044f", "not_present": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", "not_running": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c", + "not_tampered": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u043d\u0438\u043a\u043d\u043e\u0432\u0435\u043d\u0438\u0435", "not_unsafe": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u044c", "occupied": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", "opened": "{entity_name} \u043e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", @@ -94,6 +95,7 @@ "running": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c", "smoke": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0434\u044b\u043c", "sound": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0437\u0432\u0443\u043a", + "tampered": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u043d\u0438\u043a\u043d\u043e\u0432\u0435\u043d\u0438\u0435", "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", "unsafe": "{entity_name} \u043d\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u044c", @@ -208,7 +210,7 @@ "on": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d" }, "update": { - "off": "\u041e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u043d\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f", + "off": "\u041d\u0435\u0442 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0439", "on": "\u0414\u043e\u0441\u0442\u0443\u043f\u043d\u043e \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435" }, "vibration": { diff --git a/homeassistant/components/binary_sensor/translations/zh-Hant.json b/homeassistant/components/binary_sensor/translations/zh-Hant.json index d9705225361..05a3f288f2a 100644 --- a/homeassistant/components/binary_sensor/translations/zh-Hant.json +++ b/homeassistant/components/binary_sensor/translations/zh-Hant.json @@ -84,6 +84,7 @@ "not_powered": "{entity_name}\u672a\u901a\u96fb", "not_present": "{entity_name}\u672a\u51fa\u73fe", "not_running": "{entity_name} \u4e0d\u518d\u57f7\u884c", + "not_tampered": "{entity_name}\u5df2\u505c\u6b62\u5075\u6e2c\u6e1b\u5f31", "not_unsafe": "{entity_name}\u5df2\u5b89\u5168", "occupied": "{entity_name}\u8b8a\u6210\u6709\u4eba", "opened": "{entity_name}\u5df2\u958b\u555f", @@ -94,6 +95,7 @@ "running": "{entity_name} \u958b\u59cb\u57f7\u884c", "smoke": "{entity_name}\u5df2\u5075\u6e2c\u5230\u7159\u9727", "sound": "{entity_name}\u5df2\u5075\u6e2c\u5230\u8072\u97f3", + "tampered": "{entity_name}\u5df2\u5075\u6e2c\u5230\u6e1b\u5f31", "turned_off": "{entity_name}\u5df2\u95dc\u9589", "turned_on": "{entity_name}\u5df2\u958b\u555f", "unsafe": "{entity_name}\u5df2\u4e0d\u5b89\u5168", diff --git a/homeassistant/components/cover/translations/ja.json b/homeassistant/components/cover/translations/ja.json index 2b2f8cdf284..59967ebe6f3 100644 --- a/homeassistant/components/cover/translations/ja.json +++ b/homeassistant/components/cover/translations/ja.json @@ -31,7 +31,7 @@ "closed": "\u30af\u30ed\u30fc\u30ba\u30c9", "closing": "\u9589\u3058\u3066\u3044\u307e\u3059", "open": "\u30aa\u30fc\u30d7\u30f3", - "opening": "\u6249(Opening)", + "opening": "\u30aa\u30fc\u30d7\u30cb\u30f3\u30b0", "stopped": "\u505c\u6b62" } }, diff --git a/homeassistant/components/elkm1/translations/ja.json b/homeassistant/components/elkm1/translations/ja.json index 4dc86431964..a2dea3c10cf 100644 --- a/homeassistant/components/elkm1/translations/ja.json +++ b/homeassistant/components/elkm1/translations/ja.json @@ -19,6 +19,7 @@ "temperature_unit": "ElkM1\u304c\u4f7f\u7528\u3059\u308b\u6e29\u5ea6\u5358\u4f4d\u3002", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, + "description": "\u30a2\u30c9\u30ec\u30b9\u6587\u5b57\u5217\u306f\u3001 '\u30bb\u30ad\u30e5\u30a2 '\u304a\u3088\u3073 '\u975e\u30bb\u30ad\u30e5\u30a2 '\u306e\u5834\u5408\u306f\u3001'address[:port]'\u306e\u5f62\u5f0f\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u4f8b: '192.168.1.1'\u3002\u30dd\u30fc\u30c8\u306f\u30aa\u30d7\u30b7\u30e7\u30f3\u3067\u3001\u30c7\u30d5\u30a9\u30eb\u30c8\u306f'\u975e\u30bb\u30ad\u30e5\u30a2'\u306e\u5834\u5408\u306f\u30012101 \u3067'\u30bb\u30ad\u30e5\u30a2'\u306e\u5834\u5408\u306f\u30012601 \u3067\u3059\u3002\u30b7\u30ea\u30a2\u30eb \u30d7\u30ed\u30c8\u30b3\u30eb\u306e\u5834\u5408\u3001\u30a2\u30c9\u30ec\u30b9\u306f\u3001'tty[:baud]' \u306e\u5f62\u5f0f\u3067\u306a\u3051\u308c\u3070\u306a\u308a\u307e\u305b\u3093\u3002\u4f8b: '/dev/ttyS1'\u3002\u30dc\u30fc\u306f\u30aa\u30d7\u30b7\u30e7\u30f3\u3067\u3001\u30c7\u30d5\u30a9\u30eb\u30c8\u306f115200\u3067\u3059\u3002", "title": "Elk-M1 Control\u306b\u63a5\u7d9a" } } diff --git a/homeassistant/components/hangouts/translations/fi.json b/homeassistant/components/hangouts/translations/fi.json index 05b394c69f4..e93642a952d 100644 --- a/homeassistant/components/hangouts/translations/fi.json +++ b/homeassistant/components/hangouts/translations/fi.json @@ -3,6 +3,11 @@ "abort": { "unknown": "Tapahtui tuntematon virhe." }, + "error": { + "invalid_2fa": "Virheellinen kaksitekij\u00e4todennus, yrit\u00e4 uudelleen.", + "invalid_2fa_method": "Virheellinen 2FA-menetelm\u00e4 (tarkista puhelimessa).", + "invalid_login": "Virheellinen kirjautuminen, yrit\u00e4 uudelleen." + }, "step": { "2fa": { "data": { diff --git a/homeassistant/components/homematicip_cloud/translations/fi.json b/homeassistant/components/homematicip_cloud/translations/fi.json index 6a46955cddb..70e6fbe6b3c 100644 --- a/homeassistant/components/homematicip_cloud/translations/fi.json +++ b/homeassistant/components/homematicip_cloud/translations/fi.json @@ -7,13 +7,20 @@ }, "error": { "invalid_sgtin_or_pin": "Virheellinen PIN-koodi, yrit\u00e4 uudelleen.", - "press_the_button": "Paina sinist\u00e4 painiketta." + "press_the_button": "Paina sinist\u00e4 painiketta.", + "register_failed": "Rekister\u00f6inti ep\u00e4onnistui, yrit\u00e4 uudelleen.", + "timeout_button": "Sinisen painikkeen painalluksen aikakatkaisu, yrit\u00e4 uudelleen." }, "step": { "init": { "data": { "pin": "PIN-koodi" - } + }, + "title": "Valitse HomematicIP-tukiasema" + }, + "link": { + "description": "Rekister\u00f6i HomematicIP Home Assistantiin painamalla tukiaseman sinist\u00e4 painiketta ja l\u00e4hetyspainiketta. \n\n ![Painikkeen sijainti sillalla](/static/images/config_flows/config_homematicip_cloud.png)", + "title": "Linkit\u00e4 tukiasema" } } } diff --git a/homeassistant/components/homematicip_cloud/translations/ja.json b/homeassistant/components/homematicip_cloud/translations/ja.json index f68e51c7893..cbaf3c122a1 100644 --- a/homeassistant/components/homematicip_cloud/translations/ja.json +++ b/homeassistant/components/homematicip_cloud/translations/ja.json @@ -15,7 +15,7 @@ "init": { "data": { "hapid": "\u30a2\u30af\u30bb\u30b9\u30dd\u30a4\u30f3\u30c8ID (SGTIN)", - "name": "\u540d\u524d(\u30aa\u30d7\u30b7\u30e7\u30f3\u3002\u5168\u30c7\u30d0\u30a4\u30b9\u306e\u540d\u524d\u306e\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u3068\u3057\u3066\u4f7f\u7528)", + "name": "\u540d\u524d(\u30aa\u30d7\u30b7\u30e7\u30f3\u3001\u5168\u30c7\u30d0\u30a4\u30b9\u306e\u540d\u524d\u306e\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u3068\u3057\u3066\u4f7f\u7528)", "pin": "PIN\u30b3\u30fc\u30c9" }, "title": "HomematicIP Access point\u3092\u9078\u629e" diff --git a/homeassistant/components/ifttt/translations/fi.json b/homeassistant/components/ifttt/translations/fi.json index e9f6e13cc75..332926d8b49 100644 --- a/homeassistant/components/ifttt/translations/fi.json +++ b/homeassistant/components/ifttt/translations/fi.json @@ -2,7 +2,8 @@ "config": { "step": { "user": { - "description": "Haluatko varmasti m\u00e4\u00e4ritt\u00e4\u00e4 IFTTT:n?" + "description": "Haluatko varmasti m\u00e4\u00e4ritt\u00e4\u00e4 IFTTT:n?", + "title": "M\u00e4\u00e4rit\u00e4 IFTTT Webhook -sovelma" } } } diff --git a/homeassistant/components/knx/translations/de.json b/homeassistant/components/knx/translations/de.json index 4d26412bdae..41afdfb14ac 100644 --- a/homeassistant/components/knx/translations/de.json +++ b/homeassistant/components/knx/translations/de.json @@ -12,6 +12,7 @@ "data": { "host": "Host", "individual_address": "Individuelle Adresse f\u00fcr die Verbindung", + "local_ip": "Lokale IP (leer lassen, wenn unsicher)", "port": "Port", "route_back": "Route Back / NAT-Modus" }, @@ -54,6 +55,7 @@ "tunnel": { "data": { "host": "Host", + "local_ip": "Lokale IP (leer lassen, wenn unsicher)", "port": "Port", "route_back": "Route Back / NAT-Modus" } diff --git a/homeassistant/components/knx/translations/hu.json b/homeassistant/components/knx/translations/hu.json index 9d2e4d5f858..592c9a50a1c 100644 --- a/homeassistant/components/knx/translations/hu.json +++ b/homeassistant/components/knx/translations/hu.json @@ -12,6 +12,7 @@ "data": { "host": "C\u00edm", "individual_address": "A kapcsolat egy\u00e9ni c\u00edme", + "local_ip": "Helyi IP c\u00edm (hagyja \u00fcresen, ha nem biztos benne)", "port": "Port", "route_back": "\u00datvonal (route) vissza / NAT m\u00f3d" }, @@ -54,6 +55,7 @@ "tunnel": { "data": { "host": "C\u00edm", + "local_ip": "Helyi IP c\u00edm (hagyja \u00fcresen, ha nem biztos benne)", "port": "Port", "route_back": "\u00datvonal (route) vissza / NAT m\u00f3d" } diff --git a/homeassistant/components/knx/translations/it.json b/homeassistant/components/knx/translations/it.json index 0659b928523..ec4e71e9c46 100644 --- a/homeassistant/components/knx/translations/it.json +++ b/homeassistant/components/knx/translations/it.json @@ -12,6 +12,7 @@ "data": { "host": "Host", "individual_address": "Indirizzo individuale per la connessione", + "local_ip": "IP locale (lasciare vuoto se non si \u00e8 sicuri)", "port": "Porta", "route_back": "Torna indietro / Modalit\u00e0 NAT" }, @@ -54,6 +55,7 @@ "tunnel": { "data": { "host": "Host", + "local_ip": "IP locale (lasciare vuoto se non si \u00e8 sicuri)", "port": "Porta", "route_back": "Torna indietro / Modalit\u00e0 NAT" } diff --git a/homeassistant/components/knx/translations/ja.json b/homeassistant/components/knx/translations/ja.json index 23614ebcbc2..515d338cde7 100644 --- a/homeassistant/components/knx/translations/ja.json +++ b/homeassistant/components/knx/translations/ja.json @@ -12,6 +12,7 @@ "data": { "host": "\u30db\u30b9\u30c8", "individual_address": "\u63a5\u7d9a\u7528\u306e\u500b\u5225\u30a2\u30c9\u30ec\u30b9", + "local_ip": "\u30ed\u30fc\u30ab\u30ebIP(\u4e0d\u660e\u306a\u5834\u5408\u306f\u7a7a\u306e\u307e\u307e\u306b\u3057\u3066\u304f\u3060\u3055\u3044)", "port": "\u30dd\u30fc\u30c8", "route_back": "\u30eb\u30fc\u30c8\u30d0\u30c3\u30af / NAT\u30e2\u30fc\u30c9" }, @@ -54,6 +55,7 @@ "tunnel": { "data": { "host": "\u30db\u30b9\u30c8", + "local_ip": "\u30ed\u30fc\u30ab\u30ebIP(\u4e0d\u660e\u306a\u5834\u5408\u306f\u7a7a\u306e\u307e\u307e\u306b\u3057\u3066\u304f\u3060\u3055\u3044)", "port": "\u30dd\u30fc\u30c8", "route_back": "\u30eb\u30fc\u30c8\u30d0\u30c3\u30af / NAT\u30e2\u30fc\u30c9" } diff --git a/homeassistant/components/knx/translations/ru.json b/homeassistant/components/knx/translations/ru.json index 93c15c33415..35c58bc2667 100644 --- a/homeassistant/components/knx/translations/ru.json +++ b/homeassistant/components/knx/translations/ru.json @@ -12,6 +12,7 @@ "data": { "host": "\u0425\u043e\u0441\u0442", "individual_address": "\u0418\u043d\u0434\u0438\u0432\u0438\u0434\u0443\u0430\u043b\u044c\u043d\u044b\u0439 \u0430\u0434\u0440\u0435\u0441 \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f", + "local_ip": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441 (\u0435\u0441\u043b\u0438 \u043d\u0435 \u0437\u043d\u0430\u0435\u0442\u0435, \u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u043e\u043b\u0435 \u043f\u0443\u0441\u0442\u044b\u043c)", "port": "\u041f\u043e\u0440\u0442", "route_back": "\u041e\u0431\u0440\u0430\u0442\u043d\u044b\u0439 \u043c\u0430\u0440\u0448\u0440\u0443\u0442 / \u0440\u0435\u0436\u0438\u043c NAT" }, @@ -54,6 +55,7 @@ "tunnel": { "data": { "host": "\u0425\u043e\u0441\u0442", + "local_ip": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441 (\u0435\u0441\u043b\u0438 \u043d\u0435 \u0437\u043d\u0430\u0435\u0442\u0435, \u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u043e\u043b\u0435 \u043f\u0443\u0441\u0442\u044b\u043c)", "port": "\u041f\u043e\u0440\u0442", "route_back": "\u041e\u0431\u0440\u0430\u0442\u043d\u044b\u0439 \u043c\u0430\u0440\u0448\u0440\u0443\u0442 / \u0440\u0435\u0436\u0438\u043c NAT" } diff --git a/homeassistant/components/knx/translations/zh-Hant.json b/homeassistant/components/knx/translations/zh-Hant.json index c8185f1a867..4cf9a43a4c9 100644 --- a/homeassistant/components/knx/translations/zh-Hant.json +++ b/homeassistant/components/knx/translations/zh-Hant.json @@ -12,6 +12,7 @@ "data": { "host": "\u4e3b\u6a5f\u7aef", "individual_address": "\u9023\u7dda\u500b\u5225\u4f4d\u5740", + "local_ip": "\u672c\u5730\u7aef IP\uff08\u5047\u5982\u4e0d\u78ba\u5b9a\uff0c\u4fdd\u7559\u7a7a\u767d\uff09", "port": "\u901a\u8a0a\u57e0", "route_back": "\u8def\u7531\u8fd4\u56de / NAT \u6a21\u5f0f" }, @@ -54,6 +55,7 @@ "tunnel": { "data": { "host": "\u4e3b\u6a5f\u7aef", + "local_ip": "\u672c\u5730\u7aef IP\uff08\u5047\u5982\u4e0d\u78ba\u5b9a\uff0c\u4fdd\u7559\u7a7a\u767d\uff09", "port": "\u901a\u8a0a\u57e0", "route_back": "\u8def\u7531\u8fd4\u56de / NAT \u6a21\u5f0f" } diff --git a/homeassistant/components/mqtt/translations/fi.json b/homeassistant/components/mqtt/translations/fi.json index bc974dfd7d9..62bb5b0f48e 100644 --- a/homeassistant/components/mqtt/translations/fi.json +++ b/homeassistant/components/mqtt/translations/fi.json @@ -11,12 +11,14 @@ "password": "Salasana", "port": "Portti", "username": "K\u00e4ytt\u00e4j\u00e4tunnus" - } + }, + "description": "Sy\u00f6t\u00e4 MQTT-v\u00e4litt\u00e4j\u00e4n yhteystiedot." }, "hassio_confirm": { "data": { "discovery": "Ota etsint\u00e4 k\u00e4ytt\u00f6\u00f6n" - } + }, + "title": "MQTT-v\u00e4litt\u00e4j\u00e4 Home Assistant -lis\u00e4osan kautta" } } } diff --git a/homeassistant/components/nest/translations/fi.json b/homeassistant/components/nest/translations/fi.json index e4235ee096e..f420d1eca4e 100644 --- a/homeassistant/components/nest/translations/fi.json +++ b/homeassistant/components/nest/translations/fi.json @@ -1,6 +1,7 @@ { "config": { "error": { + "internal_error": "Sis\u00e4inen virhe koodin vahvistamisessa", "unknown": "Odottamaton virhe" }, "step": { diff --git a/homeassistant/components/nina/translations/de.json b/homeassistant/components/nina/translations/de.json new file mode 100644 index 00000000000..1e7e1a3e70e --- /dev/null +++ b/homeassistant/components/nina/translations/de.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "no_selection": "Bitte w\u00e4hle mindestens eine Stadt/einen Landkreis aus", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "_a_to_d": "Stadt/Landkreis (A-D)", + "_e_to_h": "Stadt/Landkreis (E-H)", + "_i_to_l": "Stadt/Landkreis (I-L)", + "_m_to_q": "Stadt/Landkreis (M-Q)", + "_r_to_u": "Stadt/Landkreis (R-U)", + "_v_to_z": "Stadt/Landkreis (V-Z)", + "corona_filter": "Corona-Warnungen entfernen", + "slots": "Maximale Warnungen pro Stadt/Landkreis" + }, + "title": "Stadt/Landkreis ausw\u00e4hlen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nina/translations/es.json b/homeassistant/components/nina/translations/es.json new file mode 100644 index 00000000000..e74ba8dd092 --- /dev/null +++ b/homeassistant/components/nina/translations/es.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Seleccionar ciudad/pa\u00eds" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nina/translations/hu.json b/homeassistant/components/nina/translations/hu.json new file mode 100644 index 00000000000..24a0d59cbae --- /dev/null +++ b/homeassistant/components/nina/translations/hu.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "no_selection": "K\u00e9rem, v\u00e1lasszon legal\u00e1bb egy v\u00e1rost/megy\u00e9t", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "_a_to_d": "V\u00e1ros/megye (A-D)", + "_e_to_h": "V\u00e1ros/megye (E-H)", + "_i_to_l": "V\u00e1ros/megye (I-L)", + "_m_to_q": "V\u00e1ros/megye (M-Q)", + "_r_to_u": "V\u00e1ros/megye (R-U)", + "_v_to_z": "V\u00e1ros/megye (V-Z)", + "corona_filter": "Corona figyelmeztet\u00e9sek bez\u00e1r\u00e1sa", + "slots": "A figyelmeztet\u00e9sek maxim\u00e1lis sz\u00e1ma v\u00e1rosonk\u00e9nt/megy\u00e9nk\u00e9nt" + }, + "title": "V\u00e1lasszon v\u00e1rost/megy\u00e9t" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nina/translations/ja.json b/homeassistant/components/nina/translations/ja.json new file mode 100644 index 00000000000..7c765025ae8 --- /dev/null +++ b/homeassistant/components/nina/translations/ja.json @@ -0,0 +1,27 @@ +{ + "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": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "no_selection": "\u5c11\u306a\u304f\u3068\u30821\u3064\u306e\u5e02\u533a\u753a\u6751/\u90e1\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "user": { + "data": { + "_a_to_d": "City/county (A-D)", + "_e_to_h": "City/county (E-H)", + "_i_to_l": "City/county (I-L)", + "_m_to_q": "City/county (M-Q)", + "_r_to_u": "City/county (R-U)", + "_v_to_z": "City/county (V-Z)", + "corona_filter": "\u30b3\u30ed\u30ca\u8b66\u544a\u3092\u524a\u9664", + "slots": "1\u5e02\u533a\u753a\u6751/\u90e1\u3042\u305f\u308a\u306e\u6700\u5927\u8b66\u544a\u6570" + }, + "title": "\u5e02\u533a\u753a\u6751/\u7fa4\u3092\u9078\u629e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nina/translations/ru.json b/homeassistant/components/nina/translations/ru.json new file mode 100644 index 00000000000..114f9d44040 --- /dev/null +++ b/homeassistant/components/nina/translations/ru.json @@ -0,0 +1,27 @@ +{ + "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": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "no_selection": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0445\u043e\u0442\u044f \u0431\u044b \u043e\u0434\u0438\u043d \u0433\u043e\u0440\u043e\u0434/\u043e\u043a\u0440\u0443\u0433", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "_a_to_d": "\u0413\u043e\u0440\u043e\u0434/\u043e\u043a\u0440\u0443\u0433 (A-D)", + "_e_to_h": "\u0413\u043e\u0440\u043e\u0434/\u043e\u043a\u0440\u0443\u0433 (E-H)", + "_i_to_l": "\u0413\u043e\u0440\u043e\u0434/\u043e\u043a\u0440\u0443\u0433 (I-L)", + "_m_to_q": "\u0413\u043e\u0440\u043e\u0434/\u043e\u043a\u0440\u0443\u0433 (M-Q)", + "_r_to_u": "\u0413\u043e\u0440\u043e\u0434/\u043e\u043a\u0440\u0443\u0433 (R-U)", + "_v_to_z": "\u0413\u043e\u0440\u043e\u0434/\u043e\u043a\u0440\u0443\u0433 (V-Z)", + "corona_filter": "\u0418\u0441\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u044f \u043e \u043a\u043e\u0440\u043e\u043d\u0430\u0432\u0438\u0440\u0443\u0441\u0435", + "slots": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u0439 \u043d\u0430 \u0433\u043e\u0440\u043e\u0434/\u043e\u043a\u0440\u0443\u0433" + }, + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0433\u043e\u0440\u043e\u0434/\u043e\u043a\u0440\u0443\u0433" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/translations/ja.json b/homeassistant/components/ps4/translations/ja.json index 041ba2e121e..b8dee70a844 100644 --- a/homeassistant/components/ps4/translations/ja.json +++ b/homeassistant/components/ps4/translations/ja.json @@ -25,7 +25,7 @@ "name": "\u540d\u524d", "region": "\u30ea\u30fc\u30b8\u30e7\u30f3" }, - "description": "PlayStation4\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u307e\u3059\u3002 PIN\u30b3\u30fc\u30c9(PIN CodePIN Code)\u306e\u5834\u5408\u306f\u3001PlayStation4\u672c\u4f53\u306e '\u8a2d\u5b9a' \u306b\u79fb\u52d5\u3057\u307e\u3059\u3002\u6b21\u306b\u3001'\u30e2\u30d0\u30a4\u30eb\u30a2\u30d7\u30ea\u63a5\u7d9a\u8a2d\u5b9a' \u306b\u79fb\u52d5\u3057\u3066\u3001'\u30c7\u30d0\u30a4\u30b9\u306e\u8ffd\u52a0' \u3092\u9078\u629e\u3057\u307e\u3059\u3002\u8868\u793a\u3055\u308c\u305f PIN\u30b3\u30fc\u30c9(PIN CodePIN Code) \u3092\u5165\u529b\u3057\u307e\u3059\u3002\u8a73\u7d30\u306f\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]\uff08(https://www.home-assistant.io/components/ps4/) \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "description": "PlayStation4\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u307e\u3059\u3002 PIN\u30b3\u30fc\u30c9(PIN CodePIN Code)\u306e\u5834\u5408\u306f\u3001PlayStation4\u672c\u4f53\u306e '\u8a2d\u5b9a' \u306b\u79fb\u52d5\u3057\u307e\u3059\u3002\u6b21\u306b\u3001'\u30e2\u30d0\u30a4\u30eb\u30a2\u30d7\u30ea\u63a5\u7d9a\u8a2d\u5b9a' \u306b\u79fb\u52d5\u3057\u3066\u3001'\u30c7\u30d0\u30a4\u30b9\u306e\u8ffd\u52a0' \u3092\u9078\u629e\u3057\u307e\u3059\u3002\u8868\u793a\u3055\u308c\u305f PIN\u30b3\u30fc\u30c9(PIN CodePIN Code) \u3092\u5165\u529b\u3057\u307e\u3059\u3002\u8a73\u7d30\u306f\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8](https://www.home-assistant.io/components/ps4/) \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "Play Station 4" }, "mode": { diff --git a/homeassistant/components/smartthings/translations/ja.json b/homeassistant/components/smartthings/translations/ja.json index 4d1b8a15cfc..29149174ae9 100644 --- a/homeassistant/components/smartthings/translations/ja.json +++ b/homeassistant/components/smartthings/translations/ja.json @@ -19,6 +19,7 @@ "data": { "access_token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3" }, + "description": "[\u624b\u9806]({component_url})\u3054\u3068\u306b\u4f5c\u6210\u3055\u308c\u305f\u3001SmartThings[\u500b\u4eba\u7528\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3]({token_url})\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002 \u3053\u308c\u306f\u3001SmartThings account\u5185\u306bHome Assistant\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u4f5c\u6210\u3059\u308b\u305f\u3081\u306b\u4f7f\u7528\u3055\u308c\u307e\u3059\u3002", "title": "\u30d1\u30fc\u30bd\u30ca\u30eb \u30a2\u30af\u30bb\u30b9 \u30c8\u30fc\u30af\u30f3\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044" }, "select_location": { diff --git a/homeassistant/components/tradfri/translations/fi.json b/homeassistant/components/tradfri/translations/fi.json index 4946d88778f..63b3b2adddf 100644 --- a/homeassistant/components/tradfri/translations/fi.json +++ b/homeassistant/components/tradfri/translations/fi.json @@ -4,7 +4,9 @@ "already_configured": "Silta on jo m\u00e4\u00e4ritetty" }, "error": { - "cannot_connect": "Yhdist\u00e4minen ep\u00e4onnistui" + "cannot_connect": "Yhdist\u00e4minen ep\u00e4onnistui", + "invalid_key": "Rekister\u00f6inti annetulla avaimella ep\u00e4onnistui. Jos t\u00e4m\u00e4 toistuu, yrit\u00e4 k\u00e4ynnist\u00e4\u00e4 yhdysk\u00e4yt\u00e4v\u00e4 uudelleen.", + "timeout": "Aikakatkaisu koodin vahvistamisessa." }, "step": { "auth": { @@ -12,6 +14,7 @@ "host": "Palvelin", "security_code": "Turvakoodi" }, + "description": "L\u00f6yd\u00e4t suojakoodin yhdysk\u00e4yt\u00e4v\u00e4si takaosasta.", "title": "Kirjoita suojakoodi" } } diff --git a/homeassistant/components/wolflink/translations/sensor.ja.json b/homeassistant/components/wolflink/translations/sensor.ja.json index 0b9fa47c9ea..519e2062e5b 100644 --- a/homeassistant/components/wolflink/translations/sensor.ja.json +++ b/homeassistant/components/wolflink/translations/sensor.ja.json @@ -11,6 +11,8 @@ "at_frostschutz": "OT\u971c\u9632\u6b62", "aus": "\u7121\u52b9", "auto": "\u30aa\u30fc\u30c8", + "auto_off_cool": "AutoOffCool", + "auto_on_cool": "AutoOnCool", "automatik_aus": "\u81ea\u52d5\u30aa\u30d5", "automatik_ein": "\u81ea\u52d5\u30aa\u30f3", "bereit_keine_ladung": "\u6e96\u5099\u5b8c\u4e86\u3001\u8aad\u307f\u8fbc\u307f\u4e2d\u3067\u306f\u306a\u3044", diff --git a/homeassistant/components/zha/translations/ja.json b/homeassistant/components/zha/translations/ja.json index 38231524232..7095ef8813d 100644 --- a/homeassistant/components/zha/translations/ja.json +++ b/homeassistant/components/zha/translations/ja.json @@ -66,6 +66,13 @@ "close": "\u30af\u30ed\u30fc\u30ba", "dim_down": "\u8584\u6697\u304f\u3059\u308b", "dim_up": "\u5fae\u304b\u306b\u660e\u308b\u304f\u3059\u308b", + "face_1": "\u30d5\u30a7\u30a4\u30b91\u304c\u30a2\u30af\u30c6\u30a3\u30d6\u306b\u306a\u3063\u3066\u3044\u308b", + "face_2": "\u30d5\u30a7\u30a4\u30b92\u304c\u30a2\u30af\u30c6\u30a3\u30d6\u306b\u306a\u3063\u3066\u3044\u308b", + "face_3": "\u30d5\u30a7\u30a4\u30b93\u304c\u30a2\u30af\u30c6\u30a3\u30d6\u306b\u306a\u3063\u3066\u3044\u308b", + "face_4": "\u30d5\u30a7\u30a4\u30b94\u304c\u30a2\u30af\u30c6\u30a3\u30d6\u306b\u306a\u3063\u3066\u3044\u308b", + "face_5": "\u30d5\u30a7\u30a4\u30b95\u304c\u30a2\u30af\u30c6\u30a3\u30d6\u306b\u306a\u3063\u3066\u3044\u308b", + "face_6": "\u30d5\u30a7\u30a4\u30b96\u304c\u30a2\u30af\u30c6\u30a3\u30d6\u306b\u306a\u3063\u3066\u3044\u308b", + "face_any": "\u4efb\u610f/\u6307\u5b9a\u3055\u308c\u305f\u30d5\u30a7\u30a4\u30b9\u304c\u30a2\u30af\u30c6\u30a3\u30d6\u306b\u306a\u3063\u3066\u3044\u308b", "left": "\u5de6", "open": "\u30aa\u30fc\u30d7\u30f3", "right": "\u53f3", From 4e9fd56b8c1f84d29f0d4d07cf100d135ae46176 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sun, 5 Dec 2021 20:36:05 -0500 Subject: [PATCH 0062/2644] Add 3157100-E model to Centralite thermostat (#61073) --- homeassistant/components/zha/climate.py | 2 +- homeassistant/components/zha/sensor.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index 69c1ce35849..9ef7e8fdebc 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -599,7 +599,7 @@ class ZenWithinThermostat(Thermostat): channel_names=CHANNEL_THERMOSTAT, aux_channels=CHANNEL_FAN, manufacturers="Centralite", - models="3157100", + models={"3157100", "3157100-E"}, stop_on_match=True, ) class CentralitePearl(ZenWithinThermostat): diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 304a3d155f5..567d2a6065e 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -61,6 +61,7 @@ from .core import discovery from .core.const import ( CHANNEL_ANALOG_INPUT, CHANNEL_ELECTRICAL_MEASUREMENT, + CHANNEL_FAN, CHANNEL_HUMIDITY, CHANNEL_ILLUMINANCE, CHANNEL_LEAF_WETNESS, @@ -636,6 +637,13 @@ class ThermostatHVACAction(Sensor, id_suffix="hvac_action"): self.async_write_ha_state() +@MULTI_MATCH( + channel_names=CHANNEL_THERMOSTAT, + aux_channels=CHANNEL_FAN, + manufacturers="Centralite", + models={"3157100", "3157100-E"}, + stop_on_match=True, +) @MULTI_MATCH( channel_names=CHANNEL_THERMOSTAT, manufacturers="Zen Within", From fb5cc13061656b631deee8e78bde7f7c995c109f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 6 Dec 2021 02:51:45 +0100 Subject: [PATCH 0063/2644] Adjust yeelight ssdp_listener tests (#61065) --- tests/components/yeelight/__init__.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/tests/components/yeelight/__init__.py b/tests/components/yeelight/__init__.py index b6bf0b10d67..b48cfc5402a 100644 --- a/tests/components/yeelight/__init__.py +++ b/tests/components/yeelight/__init__.py @@ -157,7 +157,7 @@ def _mocked_bulb(cannot_connect=False): return bulb -def _patched_ssdp_listener(info, *args, **kwargs): +def _patched_ssdp_listener(info: ssdp.SsdpHeaders, *args, **kwargs): listener = SsdpSearchListener(*args, **kwargs) async def _async_callback(*_): @@ -181,12 +181,7 @@ def _patch_discovery(no_device=False, capabilities=None): def _generate_fake_ssdp_listener(*args, **kwargs): info = None if not no_device: - info = ssdp.SsdpServiceInfo( - ssdp_usn="", - ssdp_st=scanner.SSDP_ST, - upnp={}, - ssdp_headers=capabilities or CAPABILITIES, - ) + info = capabilities or CAPABILITIES return _patched_ssdp_listener(info, *args, **kwargs) return patch( From ef326c0ce9960cb099b22c5ceebda6ac6ea28cab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Dec 2021 15:56:35 -1000 Subject: [PATCH 0064/2644] Bump flux_led to 0.25.17 to fix missing push messages on 0xA3 models (#61070) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index dc60a46d68c..71d8fd350b7 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.25.16"], + "requirements": ["flux_led==0.25.17"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 63bbc61ff84..e399a734ab8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.25.16 +flux_led==0.25.17 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 18841a1f537..075dd7e61cb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.25.16 +flux_led==0.25.17 # homeassistant.components.homekit fnvhash==0.1.0 From a7e129a952ea7c7fac22e3a8e2400b0c26b35f21 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Mon, 6 Dec 2021 04:02:46 +0100 Subject: [PATCH 0065/2644] Add Aseko Pool Live integration (#56299) --- .coveragerc | 3 + .strict-typing | 1 + CODEOWNERS | 1 + .../components/aseko_pool_live/__init__.py | 77 +++++++++++++++++ .../components/aseko_pool_live/config_flow.py | 81 +++++++++++++++++ .../components/aseko_pool_live/const.py | 3 + .../components/aseko_pool_live/entity.py | 29 +++++++ .../components/aseko_pool_live/manifest.json | 11 +++ .../components/aseko_pool_live/sensor.py | 72 ++++++++++++++++ .../components/aseko_pool_live/strings.json | 20 +++++ .../aseko_pool_live/translations/en.json | 20 +++++ homeassistant/generated/config_flows.py | 1 + mypy.ini | 11 +++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/aseko_pool_live/__init__.py | 1 + .../aseko_pool_live/test_config_flow.py | 86 +++++++++++++++++++ 17 files changed, 423 insertions(+) create mode 100644 homeassistant/components/aseko_pool_live/__init__.py create mode 100644 homeassistant/components/aseko_pool_live/config_flow.py create mode 100644 homeassistant/components/aseko_pool_live/const.py create mode 100644 homeassistant/components/aseko_pool_live/entity.py create mode 100644 homeassistant/components/aseko_pool_live/manifest.json create mode 100644 homeassistant/components/aseko_pool_live/sensor.py create mode 100644 homeassistant/components/aseko_pool_live/strings.json create mode 100644 homeassistant/components/aseko_pool_live/translations/en.json create mode 100644 tests/components/aseko_pool_live/__init__.py create mode 100644 tests/components/aseko_pool_live/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index fc033c77369..92a2638dee5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -73,6 +73,9 @@ omit = homeassistant/components/arris_tg2492lg/* homeassistant/components/aruba/device_tracker.py homeassistant/components/arwn/sensor.py + homeassistant/components/aseko_pool_live/__init__.py + homeassistant/components/aseko_pool_live/entity.py + homeassistant/components/aseko_pool_live/sensor.py homeassistant/components/asterisk_cdr/mailbox.py homeassistant/components/asterisk_mbox/* homeassistant/components/asuswrt/__init__.py diff --git a/.strict-typing b/.strict-typing index caaef80fe38..5546086a456 100644 --- a/.strict-typing +++ b/.strict-typing @@ -17,6 +17,7 @@ homeassistant.components.ambee.* homeassistant.components.ambient_station.* homeassistant.components.amcrest.* homeassistant.components.ampio.* +homeassistant.components.aseko_pool_live.* homeassistant.components.automation.* homeassistant.components.binary_sensor.* homeassistant.components.bluetooth_tracker.* diff --git a/CODEOWNERS b/CODEOWNERS index 8ebf28d249e..a349954bf65 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -52,6 +52,7 @@ homeassistant/components/arcam_fmj/* @elupus homeassistant/components/arduino/* @fabaff homeassistant/components/arest/* @fabaff homeassistant/components/arris_tg2492lg/* @vanbalken +homeassistant/components/aseko_pool_live/* @milanmeu homeassistant/components/asuswrt/* @kennedyshead @ollo69 homeassistant/components/atag/* @MatsNL homeassistant/components/aten_pe/* @mtdcr diff --git a/homeassistant/components/aseko_pool_live/__init__.py b/homeassistant/components/aseko_pool_live/__init__.py new file mode 100644 index 00000000000..7c7e8cc009f --- /dev/null +++ b/homeassistant/components/aseko_pool_live/__init__.py @@ -0,0 +1,77 @@ +"""The Aseko Pool Live integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging +from typing import Dict + +from aioaseko import APIUnavailable, MobileAccount, Unit, Variable + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS: list[str] = ["sensor"] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Aseko Pool Live from a config entry.""" + account = MobileAccount( + async_get_clientsession(hass), access_token=entry.data[CONF_ACCESS_TOKEN] + ) + + try: + units = await account.get_units() + except APIUnavailable as err: + raise ConfigEntryNotReady from err + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = [] + + for unit in units: + coordinator = AsekoDataUpdateCoordinator(hass, unit) + await coordinator.async_config_entry_first_refresh() + hass.data[DOMAIN][entry.entry_id].append((unit, coordinator)) + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +class AsekoDataUpdateCoordinator(DataUpdateCoordinator[Dict[str, Variable]]): + """Class to manage fetching Aseko unit data from single endpoint.""" + + def __init__(self, hass: HomeAssistant, unit: Unit) -> None: + """Initialize global Aseko unit data updater.""" + self._unit = unit + + if self._unit.name: + name = self._unit.name + else: + name = f"{self._unit.type}-{self._unit.serial_number}" + + super().__init__( + hass, + _LOGGER, + name=name, + update_interval=timedelta(minutes=2), + ) + + async def _async_update_data(self) -> dict[str, Variable]: + """Fetch unit data.""" + await self._unit.get_state() + return {variable.type: variable for variable in self._unit.variables} diff --git a/homeassistant/components/aseko_pool_live/config_flow.py b/homeassistant/components/aseko_pool_live/config_flow.py new file mode 100644 index 00000000000..c8f96db3bc8 --- /dev/null +++ b/homeassistant/components/aseko_pool_live/config_flow.py @@ -0,0 +1,81 @@ +"""Config flow for Aseko Pool Live integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from aioaseko import APIUnavailable, InvalidAuthCredentials, MobileAccount, WebAccount +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CONF_EMAIL, + CONF_PASSWORD, + CONF_UNIQUE_ID, +) +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Aseko Pool Live.""" + + VERSION = 1 + + async def get_account_info(self, email: str, password: str) -> dict: + """Get account info from the mobile API and the web API.""" + session = async_get_clientsession(self.hass) + + web_account = WebAccount(session, email, password) + web_account_info = await web_account.login() + + mobile_account = MobileAccount(session, email, password) + await mobile_account.login() + + return { + CONF_ACCESS_TOKEN: mobile_account.access_token, + CONF_EMAIL: web_account_info.email, + CONF_UNIQUE_ID: web_account_info.user_id, + } + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + errors = {} + if user_input is not None: + try: + info = await self.get_account_info( + user_input[CONF_EMAIL], user_input[CONF_PASSWORD] + ) + except APIUnavailable: + errors["base"] = "cannot_connect" + except InvalidAuthCredentials: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id(info[CONF_UNIQUE_ID]) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=info[CONF_EMAIL], + data={CONF_ACCESS_TOKEN: info[CONF_ACCESS_TOKEN]}, + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_EMAIL): str, + vol.Required(CONF_PASSWORD): str, + } + ), + errors=errors, + ) diff --git a/homeassistant/components/aseko_pool_live/const.py b/homeassistant/components/aseko_pool_live/const.py new file mode 100644 index 00000000000..41701e09754 --- /dev/null +++ b/homeassistant/components/aseko_pool_live/const.py @@ -0,0 +1,3 @@ +"""Constants for the Aseko Pool Live integration.""" + +DOMAIN = "aseko_pool_live" diff --git a/homeassistant/components/aseko_pool_live/entity.py b/homeassistant/components/aseko_pool_live/entity.py new file mode 100644 index 00000000000..963bb536671 --- /dev/null +++ b/homeassistant/components/aseko_pool_live/entity.py @@ -0,0 +1,29 @@ +"""Aseko entity.""" +from aioaseko import Unit + +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import AsekoDataUpdateCoordinator +from .const import DOMAIN + + +class AsekoEntity(CoordinatorEntity): + """Representation of an aseko entity.""" + + coordinator: AsekoDataUpdateCoordinator + + def __init__(self, unit: Unit, coordinator: AsekoDataUpdateCoordinator) -> None: + """Initialize the aseko entity.""" + super().__init__(coordinator) + self._unit = unit + + self._device_model = f"ASIN AQUA {self._unit.type}" + self._device_name = self._unit.name if self._unit.name else self._device_model + + self._attr_device_info = DeviceInfo( + name=self._device_name, + identifiers={(DOMAIN, str(self._unit.serial_number))}, + manufacturer="Aseko", + model=self._device_model, + ) diff --git a/homeassistant/components/aseko_pool_live/manifest.json b/homeassistant/components/aseko_pool_live/manifest.json new file mode 100644 index 00000000000..f6323b49354 --- /dev/null +++ b/homeassistant/components/aseko_pool_live/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "aseko_pool_live", + "name": "Aseko Pool Live", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/aseko_pool_live", + "requirements": ["aioaseko==0.0.1"], + "codeowners": [ + "@milanmeu" + ], + "iot_class": "cloud_polling" +} \ No newline at end of file diff --git a/homeassistant/components/aseko_pool_live/sensor.py b/homeassistant/components/aseko_pool_live/sensor.py new file mode 100644 index 00000000000..41036b582a7 --- /dev/null +++ b/homeassistant/components/aseko_pool_live/sensor.py @@ -0,0 +1,72 @@ +"""Support for Aseko Pool Live sensors.""" +from __future__ import annotations + +from aioaseko import Unit, Variable + +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import DEVICE_CLASS_TEMPERATURE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import AsekoDataUpdateCoordinator +from .const import DOMAIN +from .entity import AsekoEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Aseko Pool Live sensors.""" + data: list[tuple[Unit, AsekoDataUpdateCoordinator]] = hass.data[DOMAIN][ + config_entry.entry_id + ] + entities = [] + for unit, coordinator in data: + for variable in unit.variables: + entities.append(VariableSensorEntity(unit, variable, coordinator)) + async_add_entities(entities) + + +class VariableSensorEntity(AsekoEntity, SensorEntity): + """Representation of a unit variable sensor entity.""" + + attr_state_class = STATE_CLASS_MEASUREMENT + + def __init__( + self, unit: Unit, variable: Variable, coordinator: AsekoDataUpdateCoordinator + ) -> None: + """Initialize the variable sensor.""" + super().__init__(unit, coordinator) + self._variable = variable + + variable_name = { + "Air temp.": "Air Temperature", + "Cl free": "Free Chlorine", + "Water temp.": "Water Temperature", + }.get(self._variable.name, self._variable.name) + + self._attr_name = f"{self._device_name} {variable_name}" + self._attr_unique_id = f"{self._unit.serial_number}{self._variable.type}" + self._attr_native_unit_of_measurement = self._variable.unit + + self._attr_icon = { + "clf": "mdi:flask", + "ph": "mdi:ph", + "rx": "mdi:test-tube", + "waterLevel": "mdi:waves", + "waterTemp": "mdi:coolant-temperature", + }.get(self._variable.type) + + self._attr_device_class = { + "airTemp": DEVICE_CLASS_TEMPERATURE, + "waterTemp": DEVICE_CLASS_TEMPERATURE, + }.get(self._variable.type) + + @property + def native_value(self) -> int | None: + """Return the state of the sensor.""" + variable = self.coordinator.data[self._variable.type] + return variable.current_value diff --git a/homeassistant/components/aseko_pool_live/strings.json b/homeassistant/components/aseko_pool_live/strings.json new file mode 100644 index 00000000000..4c3813220b6 --- /dev/null +++ b/homeassistant/components/aseko_pool_live/strings.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "data": { + "email": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aseko_pool_live/translations/en.json b/homeassistant/components/aseko_pool_live/translations/en.json new file mode 100644 index 00000000000..399b4650695 --- /dev/null +++ b/homeassistant/components/aseko_pool_live/translations/en.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Account is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "Password" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index b4ca5fb2d5b..596cdf03fb7 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -27,6 +27,7 @@ FLOWS = [ "ambient_station", "apple_tv", "arcam_fmj", + "aseko_pool_live", "asuswrt", "atag", "august", diff --git a/mypy.ini b/mypy.ini index 7346cc83ba9..47450bab8ed 100644 --- a/mypy.ini +++ b/mypy.ini @@ -198,6 +198,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.aseko_pool_live.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.automation.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index e399a734ab8..9c40c80c07d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -135,6 +135,9 @@ aio_georss_gdacs==0.5 # homeassistant.components.ambient_station aioambient==2021.11.0 +# homeassistant.components.aseko_pool_live +aioaseko==0.0.1 + # homeassistant.components.asuswrt aioasuswrt==1.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 075dd7e61cb..eeb755b74ab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -86,6 +86,9 @@ aio_georss_gdacs==0.5 # homeassistant.components.ambient_station aioambient==2021.11.0 +# homeassistant.components.aseko_pool_live +aioaseko==0.0.1 + # homeassistant.components.asuswrt aioasuswrt==1.4.0 diff --git a/tests/components/aseko_pool_live/__init__.py b/tests/components/aseko_pool_live/__init__.py new file mode 100644 index 00000000000..6a63e0e585f --- /dev/null +++ b/tests/components/aseko_pool_live/__init__.py @@ -0,0 +1 @@ +"""Tests for the Aseko Pool Live integration.""" diff --git a/tests/components/aseko_pool_live/test_config_flow.py b/tests/components/aseko_pool_live/test_config_flow.py new file mode 100644 index 00000000000..5ab85c61a8b --- /dev/null +++ b/tests/components/aseko_pool_live/test_config_flow.py @@ -0,0 +1,86 @@ +"""Test the Aseko Pool Live config flow.""" +from unittest.mock import AsyncMock, patch + +from aioaseko import AccountInfo, APIUnavailable, InvalidAuthCredentials +import pytest + +from homeassistant import config_entries, setup +from homeassistant.components.aseko_pool_live.const import DOMAIN +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_EMAIL, CONF_PASSWORD +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM + + +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.aseko_pool_live.config_flow.WebAccount.login", + return_value=AccountInfo("aseko@example.com", "a_user_id", "any_language"), + ), patch( + "homeassistant.components.aseko_pool_live.config_flow.MobileAccount", + ) as mock_mobile_account, patch( + "homeassistant.components.aseko_pool_live.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + mobile_account = mock_mobile_account.return_value + mobile_account.login = AsyncMock() + mobile_account.access_token = "any_access_token" + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_EMAIL: "aseko@example.com", + CONF_PASSWORD: "passw0rd", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "aseko@example.com" + assert result2["data"] == {CONF_ACCESS_TOKEN: "any_access_token"} + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + "error_web, error_mobile, reason", + [ + (APIUnavailable, None, "cannot_connect"), + (InvalidAuthCredentials, None, "invalid_auth"), + (Exception, None, "unknown"), + (None, APIUnavailable, "cannot_connect"), + (None, InvalidAuthCredentials, "invalid_auth"), + (None, Exception, "unknown"), + ], +) +async def test_get_account_info_exceptions( + hass: HomeAssistant, error_web: Exception, error_mobile: Exception, reason: str +) -> None: + """Test we handle config flow exceptions.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.aseko_pool_live.config_flow.WebAccount.login", + return_value=AccountInfo("aseko@example.com", "a_user_id", "any_language"), + side_effect=error_web, + ), patch( + "homeassistant.components.aseko_pool_live.config_flow.MobileAccount.login", + side_effect=error_mobile, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_EMAIL: "aseko@example.com", + CONF_PASSWORD: "passw0rd", + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": reason} From e0cb33a0a14845262217263db551c98ae4a63a67 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 6 Dec 2021 04:06:35 +0100 Subject: [PATCH 0066/2644] Use platform enum (4) [M-O] (#60940) --- homeassistant/components/mazda/__init__.py | 4 ++-- homeassistant/components/melcloud/__init__.py | 4 ++-- homeassistant/components/met/__init__.py | 3 ++- .../components/met_eireann/__init__.py | 4 ++-- homeassistant/components/meteo_france/const.py | 3 ++- homeassistant/components/meteoclimatic/const.py | 3 ++- homeassistant/components/metoffice/__init__.py | 10 ++++++++-- homeassistant/components/mikrotik/const.py | 3 ++- homeassistant/components/mill/__init__.py | 4 ++-- .../components/minecraft_server/__init__.py | 4 ++-- homeassistant/components/mobile_app/__init__.py | 4 ++-- .../components/modem_callerid/__init__.py | 5 ++--- .../components/modern_forms/__init__.py | 17 ++++++----------- homeassistant/components/monoprice/__init__.py | 4 ++-- homeassistant/components/motion_blinds/const.py | 4 +++- homeassistant/components/mullvad/__init__.py | 3 ++- homeassistant/components/mutesync/__init__.py | 3 ++- homeassistant/components/myq/const.py | 3 ++- homeassistant/components/nam/__init__.py | 4 ++-- homeassistant/components/nanoleaf/__init__.py | 4 ++-- homeassistant/components/neato/__init__.py | 4 ++-- homeassistant/components/nest/__init__.py | 3 ++- homeassistant/components/netatmo/const.py | 14 ++++++++------ homeassistant/components/netgear/const.py | 4 +++- homeassistant/components/nexia/const.py | 3 ++- homeassistant/components/nightscout/__init__.py | 4 ++-- homeassistant/components/nmap_tracker/const.py | 4 +++- homeassistant/components/notion/__init__.py | 4 ++-- homeassistant/components/nuheat/const.py | 3 ++- homeassistant/components/nuki/__init__.py | 10 ++++++++-- homeassistant/components/nut/const.py | 3 ++- homeassistant/components/nws/__init__.py | 4 ++-- homeassistant/components/nzbget/__init__.py | 3 ++- homeassistant/components/octoprint/__init__.py | 3 ++- homeassistant/components/omnilogic/__init__.py | 4 ++-- homeassistant/components/ondilo_ico/__init__.py | 3 ++- homeassistant/components/onvif/__init__.py | 5 +++-- homeassistant/components/opengarage/__init__.py | 4 ++-- .../components/opentherm_gw/__init__.py | 6 ++---- homeassistant/components/openuv/__init__.py | 3 ++- .../components/openweathermap/const.py | 3 ++- homeassistant/components/ovo_energy/__init__.py | 4 ++-- homeassistant/components/owntracks/__init__.py | 3 ++- 43 files changed, 112 insertions(+), 82 deletions(-) diff --git a/homeassistant/components/mazda/__init__.py b/homeassistant/components/mazda/__init__.py index d165220aa8e..5650e07daca 100644 --- a/homeassistant/components/mazda/__init__.py +++ b/homeassistant/components/mazda/__init__.py @@ -14,7 +14,7 @@ from pymazda import ( import voluptuous as vol from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ( ConfigEntryAuthFailed, @@ -34,7 +34,7 @@ from .const import DATA_CLIENT, DATA_COORDINATOR, DATA_VEHICLES, DOMAIN, SERVICE _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["device_tracker", "lock", "sensor"] +PLATFORMS = [Platform.DEVICE_TRACKER, Platform.LOCK, Platform.SENSOR] async def with_timeout(task, timeout_seconds=10): diff --git a/homeassistant/components/melcloud/__init__.py b/homeassistant/components/melcloud/__init__.py index af34498aba2..417fa1641f9 100644 --- a/homeassistant/components/melcloud/__init__.py +++ b/homeassistant/components/melcloud/__init__.py @@ -12,7 +12,7 @@ from pymelcloud import Device, get_devices import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_TOKEN, CONF_USERNAME +from homeassistant.const import CONF_TOKEN, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv @@ -27,7 +27,7 @@ _LOGGER = logging.getLogger(__name__) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) -PLATFORMS = ["climate", "sensor", "water_heater"] +PLATFORMS = [Platform.CLIMATE, Platform.SENSOR, Platform.WATER_HEATER] CONF_LANGUAGE = "language" CONFIG_SCHEMA = vol.Schema( diff --git a/homeassistant/components/met/__init__.py b/homeassistant/components/met/__init__.py index 47573a76151..009beea2f78 100644 --- a/homeassistant/components/met/__init__.py +++ b/homeassistant/components/met/__init__.py @@ -18,6 +18,7 @@ from homeassistant.const import ( EVENT_CORE_CONFIG_UPDATE, LENGTH_FEET, LENGTH_METERS, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -34,7 +35,7 @@ from .const import ( URL = "https://aa015h6buqvih86i1.api.met.no/weatherapi/locationforecast/2.0/complete" -PLATFORMS = ["weather"] +PLATFORMS = [Platform.WEATHER] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/met_eireann/__init__.py b/homeassistant/components/met_eireann/__init__.py index c70f436009d..68ecaa9f05a 100644 --- a/homeassistant/components/met_eireann/__init__.py +++ b/homeassistant/components/met_eireann/__init__.py @@ -4,7 +4,7 @@ import logging import meteireann -from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, Platform from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed import homeassistant.util.dt as dt_util @@ -15,7 +15,7 @@ _LOGGER = logging.getLogger(__name__) UPDATE_INTERVAL = timedelta(minutes=60) -PLATFORMS = ["weather"] +PLATFORMS = [Platform.WEATHER] async def async_setup_entry(hass, config_entry): diff --git a/homeassistant/components/meteo_france/const.py b/homeassistant/components/meteo_france/const.py index b84f3ae14fa..2967e5e5bec 100644 --- a/homeassistant/components/meteo_france/const.py +++ b/homeassistant/components/meteo_france/const.py @@ -34,10 +34,11 @@ from homeassistant.const import ( SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, UV_INDEX, + Platform, ) DOMAIN = "meteo_france" -PLATFORMS = ["sensor", "weather"] +PLATFORMS = [Platform.SENSOR, Platform.WEATHER] COORDINATOR_FORECAST = "coordinator_forecast" COORDINATOR_RAIN = "coordinator_rain" COORDINATOR_ALERT = "coordinator_alert" diff --git a/homeassistant/components/meteoclimatic/const.py b/homeassistant/components/meteoclimatic/const.py index f4e51a6fb10..f5883cc3856 100644 --- a/homeassistant/components/meteoclimatic/const.py +++ b/homeassistant/components/meteoclimatic/const.py @@ -33,10 +33,11 @@ from homeassistant.const import ( PRESSURE_HPA, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, + Platform, ) DOMAIN = "meteoclimatic" -PLATFORMS = ["sensor", "weather"] +PLATFORMS = [Platform.SENSOR, Platform.WEATHER] ATTRIBUTION = "Data provided by Meteoclimatic" MODEL = "Meteoclimatic RSS feed" MANUFACTURER = "Meteoclimatic" diff --git a/homeassistant/components/metoffice/__init__.py b/homeassistant/components/metoffice/__init__.py index 5537af31540..3d10fdef378 100644 --- a/homeassistant/components/metoffice/__init__.py +++ b/homeassistant/components/metoffice/__init__.py @@ -6,7 +6,13 @@ import logging import datapoint from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.const import ( + CONF_API_KEY, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_NAME, + Platform, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.device_registry import DeviceEntryType @@ -28,7 +34,7 @@ from .helpers import fetch_data, fetch_site _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["sensor", "weather"] +PLATFORMS = [Platform.SENSOR, Platform.WEATHER] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/mikrotik/const.py b/homeassistant/components/mikrotik/const.py index 1fbe0af5c1b..b328c10a602 100644 --- a/homeassistant/components/mikrotik/const.py +++ b/homeassistant/components/mikrotik/const.py @@ -1,4 +1,5 @@ """Constants used in the Mikrotik components.""" +from homeassistant.const import Platform DOMAIN = "mikrotik" DEFAULT_NAME = "Mikrotik" @@ -37,7 +38,7 @@ MIKROTIK_SERVICES = { IS_CAPSMAN: "/caps-man/interface/print", } -PLATFORMS = ["device_tracker"] +PLATFORMS = [Platform.DEVICE_TRACKER] ATTR_DEVICE_TRACKER = [ "comment", diff --git a/homeassistant/components/mill/__init__.py b/homeassistant/components/mill/__init__.py index c087fe0d853..299adb949e3 100644 --- a/homeassistant/components/mill/__init__.py +++ b/homeassistant/components/mill/__init__.py @@ -7,7 +7,7 @@ import logging from mill import Mill from mill_local import Mill as MillLocal -from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -17,7 +17,7 @@ from .const import CLOUD, CONNECTION_TYPE, DOMAIN, LOCAL _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["climate", "sensor"] +PLATFORMS = [Platform.CLIMATE, Platform.SENSOR] class MillDataUpdateCoordinator(DataUpdateCoordinator): diff --git a/homeassistant/components/minecraft_server/__init__.py b/homeassistant/components/minecraft_server/__init__.py index 4876f6ea1fb..d8f42454498 100644 --- a/homeassistant/components/minecraft_server/__init__.py +++ b/homeassistant/components/minecraft_server/__init__.py @@ -7,7 +7,7 @@ import logging from mcstatus.server import MinecraftServer as MCStatus from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -20,7 +20,7 @@ from homeassistant.helpers.typing import ConfigType from . import helpers from .const import DOMAIN, MANUFACTURER, SCAN_INTERVAL, SIGNAL_NAME_PREFIX -PLATFORMS = ["binary_sensor", "sensor"] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index a83931bab23..097c4feb383 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -6,7 +6,7 @@ from homeassistant.components.webhook import ( async_register as webhook_register, async_unregister as webhook_unregister, ) -from homeassistant.const import ATTR_DEVICE_ID, CONF_WEBHOOK_ID +from homeassistant.const import ATTR_DEVICE_ID, CONF_WEBHOOK_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, discovery from homeassistant.helpers.typing import ConfigType @@ -31,7 +31,7 @@ from .helpers import savable_state from .http_api import RegistrationsView from .webhook import handle_webhook -PLATFORMS = "sensor", "binary_sensor", "device_tracker" +PLATFORMS = [Platform.SENSOR, Platform.BINARY_SENSOR, Platform.DEVICE_TRACKER] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: diff --git a/homeassistant/components/modem_callerid/__init__.py b/homeassistant/components/modem_callerid/__init__.py index afa79f1d210..d66be29f8b7 100644 --- a/homeassistant/components/modem_callerid/__init__.py +++ b/homeassistant/components/modem_callerid/__init__.py @@ -1,15 +1,14 @@ """The Modem Caller ID integration.""" from phone_modem import PhoneModem -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_DEVICE +from homeassistant.const import CONF_DEVICE, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from .const import DATA_KEY_API, DOMAIN, EXCEPTIONS -PLATFORMS = [SENSOR_DOMAIN] +PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/modern_forms/__init__.py b/homeassistant/components/modern_forms/__init__.py index 46ab0877bbb..dfd6a9a8807 100644 --- a/homeassistant/components/modern_forms/__init__.py +++ b/homeassistant/components/modern_forms/__init__.py @@ -11,13 +11,8 @@ from aiomodernforms import ( ) from aiomodernforms.models import Device as ModernFormsDeviceState -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.fan import DOMAIN as FAN_DOMAIN -from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import DeviceInfo @@ -31,11 +26,11 @@ from .const import DOMAIN SCAN_INTERVAL = timedelta(seconds=5) PLATFORMS = [ - BINARY_SENSOR_DOMAIN, - LIGHT_DOMAIN, - FAN_DOMAIN, - SENSOR_DOMAIN, - SWITCH_DOMAIN, + Platform.BINARY_SENSOR, + Platform.LIGHT, + Platform.FAN, + Platform.SENSOR, + Platform.SWITCH, ] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/monoprice/__init__.py b/homeassistant/components/monoprice/__init__.py index 90f1e2976ef..e018ef94f7d 100644 --- a/homeassistant/components/monoprice/__init__.py +++ b/homeassistant/components/monoprice/__init__.py @@ -5,7 +5,7 @@ from pymonoprice import get_monoprice from serial import SerialException from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PORT +from homeassistant.const import CONF_PORT, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -17,7 +17,7 @@ from .const import ( UNDO_UPDATE_LISTENER, ) -PLATFORMS = ["media_player"] +PLATFORMS = [Platform.MEDIA_PLAYER] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/motion_blinds/const.py b/homeassistant/components/motion_blinds/const.py index fca6b694fad..01f74c4ef4d 100644 --- a/homeassistant/components/motion_blinds/const.py +++ b/homeassistant/components/motion_blinds/const.py @@ -1,9 +1,11 @@ """Constants for the Motion Blinds component.""" +from homeassistant.const import Platform + DOMAIN = "motion_blinds" MANUFACTURER = "Motion Blinds, Coulisse B.V." DEFAULT_GATEWAY_NAME = "Motion Blinds Gateway" -PLATFORMS = ["cover", "sensor"] +PLATFORMS = [Platform.COVER, Platform.SENSOR] CONF_WAIT_FOR_PUSH = "wait_for_push" CONF_INTERFACE = "interface" diff --git a/homeassistant/components/mullvad/__init__.py b/homeassistant/components/mullvad/__init__.py index fe02983633b..386108fb0ff 100644 --- a/homeassistant/components/mullvad/__init__.py +++ b/homeassistant/components/mullvad/__init__.py @@ -6,12 +6,13 @@ import async_timeout from mullvad_api import MullvadAPI from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import update_coordinator from .const import DOMAIN -PLATFORMS = ["binary_sensor"] +PLATFORMS = [Platform.BINARY_SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: dict) -> bool: diff --git a/homeassistant/components/mutesync/__init__.py b/homeassistant/components/mutesync/__init__.py index af14725a3b4..50e485d96d2 100644 --- a/homeassistant/components/mutesync/__init__.py +++ b/homeassistant/components/mutesync/__init__.py @@ -7,12 +7,13 @@ import async_timeout import mutesync from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import update_coordinator from .const import DOMAIN, UPDATE_INTERVAL_IN_MEETING, UPDATE_INTERVAL_NOT_IN_MEETING -PLATFORMS = ["binary_sensor"] +PLATFORMS = [Platform.BINARY_SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/myq/const.py b/homeassistant/components/myq/const.py index 9f3a434ae37..3c7b5ba373a 100644 --- a/homeassistant/components/myq/const.py +++ b/homeassistant/components/myq/const.py @@ -14,11 +14,12 @@ from homeassistant.const import ( STATE_ON, STATE_OPEN, STATE_OPENING, + Platform, ) DOMAIN = "myq" -PLATFORMS = ["cover", "binary_sensor", "light"] +PLATFORMS = [Platform.COVER, Platform.BINARY_SENSOR, Platform.LIGHT] MYQ_TO_HASS = { MYQ_COVER_STATE_CLOSED: STATE_CLOSED, diff --git a/homeassistant/components/nam/__init__.py b/homeassistant/components/nam/__init__.py index 094c286b931..f9e625bfe16 100644 --- a/homeassistant/components/nam/__init__.py +++ b/homeassistant/components/nam/__init__.py @@ -18,7 +18,7 @@ from nettigo_air_monitor import ( from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -42,7 +42,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["button", "sensor"] +PLATFORMS = [Platform.BUTTON, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/nanoleaf/__init__.py b/homeassistant/components/nanoleaf/__init__.py index 3ed82ec2146..4560f340416 100644 --- a/homeassistant/components/nanoleaf/__init__.py +++ b/homeassistant/components/nanoleaf/__init__.py @@ -7,7 +7,7 @@ from dataclasses import dataclass from aionanoleaf import EffectsEvent, InvalidToken, Nanoleaf, StateEvent, Unavailable from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_TOKEN +from homeassistant.const import CONF_HOST, CONF_TOKEN, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -15,7 +15,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import DOMAIN -PLATFORMS = ["button", "light"] +PLATFORMS = [Platform.BUTTON, Platform.LIGHT] @dataclass diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index 6310e81cdd0..c1e193b7406 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -7,7 +7,7 @@ from pybotvac.exceptions import NeatoException import voluptuous as vol from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_TOKEN +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_TOKEN, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv @@ -32,7 +32,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -PLATFORMS = ["camera", "vacuum", "switch", "sensor"] +PLATFORMS = [Platform.CAMERA, Platform.VACUUM, Platform.SWITCH, Platform.SENSOR] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index fb39188710c..382edb80ca5 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -24,6 +24,7 @@ from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_SENSORS, CONF_STRUCTURE, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ( @@ -82,7 +83,7 @@ CONFIG_SCHEMA = vol.Schema( ) # Platforms for SDM API -PLATFORMS = ["sensor", "camera", "climate"] +PLATFORMS = [Platform.SENSOR, Platform.CAMERA, Platform.CLIMATE] WEB_AUTH_DOMAIN = DOMAIN INSTALLED_AUTH_DOMAIN = f"{DOMAIN}.installed" diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index a642d59ff1e..5fda8759540 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -1,9 +1,5 @@ """Constants used by the Netatmo component.""" -from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN -from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN -from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN -from homeassistant.components.select import DOMAIN as SELECT_DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import Platform API = "api" @@ -11,7 +7,13 @@ DOMAIN = "netatmo" MANUFACTURER = "Netatmo" DEFAULT_ATTRIBUTION = f"Data provided by {MANUFACTURER}" -PLATFORMS = [CAMERA_DOMAIN, CLIMATE_DOMAIN, LIGHT_DOMAIN, SELECT_DOMAIN, SENSOR_DOMAIN] +PLATFORMS = [ + Platform.CAMERA, + Platform.CLIMATE, + Platform.LIGHT, + Platform.SELECT, + Platform.SENSOR, +] NETATMO_SCOPES = [ "access_camera", diff --git a/homeassistant/components/netgear/const.py b/homeassistant/components/netgear/const.py index cba2d7ff875..44d14379eb5 100644 --- a/homeassistant/components/netgear/const.py +++ b/homeassistant/components/netgear/const.py @@ -1,9 +1,11 @@ """Netgear component constants.""" from datetime import timedelta +from homeassistant.const import Platform + DOMAIN = "netgear" -PLATFORMS = ["device_tracker", "sensor"] +PLATFORMS = [Platform.DEVICE_TRACKER, Platform.SENSOR] CONF_CONSIDER_HOME = "consider_home" diff --git a/homeassistant/components/nexia/const.py b/homeassistant/components/nexia/const.py index ada75ed580f..2c6b5195bf8 100644 --- a/homeassistant/components/nexia/const.py +++ b/homeassistant/components/nexia/const.py @@ -1,6 +1,7 @@ """Nexia constants.""" +from homeassistant.const import Platform -PLATFORMS = ["sensor", "binary_sensor", "climate", "scene"] +PLATFORMS = [Platform.SENSOR, Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.SCENE] ATTRIBUTION = "Data provided by mynexia.com" diff --git a/homeassistant/components/nightscout/__init__.py b/homeassistant/components/nightscout/__init__.py index 28906ce6e88..8e0c6c2b791 100644 --- a/homeassistant/components/nightscout/__init__.py +++ b/homeassistant/components/nightscout/__init__.py @@ -5,7 +5,7 @@ from aiohttp import ClientError from py_nightscout import Api as NightscoutAPI from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_URL +from homeassistant.const import CONF_API_KEY, CONF_URL, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr @@ -14,7 +14,7 @@ from homeassistant.helpers.entity import SLOW_UPDATE_WARNING from .const import DOMAIN -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] _API_TIMEOUT = SLOW_UPDATE_WARNING - 1 diff --git a/homeassistant/components/nmap_tracker/const.py b/homeassistant/components/nmap_tracker/const.py index e25368b22cc..c7c1342036e 100644 --- a/homeassistant/components/nmap_tracker/const.py +++ b/homeassistant/components/nmap_tracker/const.py @@ -1,9 +1,11 @@ """The Nmap Tracker integration.""" from typing import Final +from homeassistant.const import Platform + DOMAIN: Final = "nmap_tracker" -PLATFORMS: Final = ["device_tracker"] +PLATFORMS: Final = [Platform.DEVICE_TRACKER] NMAP_TRACKED_DEVICES: Final = "nmap_tracked_devices" diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index 4aa8447eb9a..0e841e12bbe 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -9,7 +9,7 @@ from aionotion import async_get_client from aionotion.errors import InvalidCredentialsError, NotionError from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import ( @@ -26,7 +26,7 @@ from homeassistant.helpers.update_coordinator import ( from .const import DOMAIN, LOGGER -PLATFORMS = ["binary_sensor", "sensor"] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] ATTR_SYSTEM_MODE = "system_mode" ATTR_SYSTEM_NAME = "system_name" diff --git a/homeassistant/components/nuheat/const.py b/homeassistant/components/nuheat/const.py index 9c4fc368226..619d4a11e2a 100644 --- a/homeassistant/components/nuheat/const.py +++ b/homeassistant/components/nuheat/const.py @@ -1,8 +1,9 @@ """Constants for NuHeat thermostats.""" +from homeassistant.const import Platform DOMAIN = "nuheat" -PLATFORMS = ["climate"] +PLATFORMS = [Platform.CLIMATE] CONF_SERIAL_NUMBER = "serial_number" diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index 99def8d4117..a7393666204 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -10,7 +10,13 @@ from requests.exceptions import RequestException from homeassistant import exceptions from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import CONF_HOST, CONF_PLATFORM, CONF_PORT, CONF_TOKEN +from homeassistant.const import ( + CONF_HOST, + CONF_PLATFORM, + CONF_PORT, + CONF_TOKEN, + Platform, +) from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -30,7 +36,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["binary_sensor", "lock"] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.LOCK] UPDATE_INTERVAL = timedelta(seconds=30) diff --git a/homeassistant/components/nut/const.py b/homeassistant/components/nut/const.py index 23ef55f3f09..ffb21a545bb 100644 --- a/homeassistant/components/nut/const.py +++ b/homeassistant/components/nut/const.py @@ -18,12 +18,13 @@ from homeassistant.const import ( POWER_WATT, TEMP_CELSIUS, TIME_SECONDS, + Platform, ) from homeassistant.helpers.entity import EntityCategory DOMAIN = "nut" -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] UNDO_UPDATE_LISTENER = "undo_update_listener" diff --git a/homeassistant/components/nws/__init__.py b/homeassistant/components/nws/__init__.py index 02327336035..3be70c95b7a 100644 --- a/homeassistant/components/nws/__init__.py +++ b/homeassistant/components/nws/__init__.py @@ -8,7 +8,7 @@ import logging from pynws import SimpleNWS from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import debounce from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -29,7 +29,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["sensor", "weather"] +PLATFORMS = [Platform.SENSOR, Platform.WEATHER] DEFAULT_SCAN_INTERVAL = datetime.timedelta(minutes=10) FAILED_SCAN_INTERVAL = datetime.timedelta(minutes=1) diff --git a/homeassistant/components/nzbget/__init__.py b/homeassistant/components/nzbget/__init__.py index ebb3a7e4e66..40ad666aaf4 100644 --- a/homeassistant/components/nzbget/__init__.py +++ b/homeassistant/components/nzbget/__init__.py @@ -10,6 +10,7 @@ from homeassistant.const import ( CONF_SCAN_INTERVAL, CONF_SSL, CONF_USERNAME, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv @@ -32,7 +33,7 @@ from .const import ( ) from .coordinator import NZBGetDataUpdateCoordinator -PLATFORMS = ["sensor", "switch"] +PLATFORMS = [Platform.SENSOR, Platform.SWITCH] CONFIG_SCHEMA = vol.Schema( vol.All( diff --git a/homeassistant/components/octoprint/__init__.py b/homeassistant/components/octoprint/__init__.py index 706f54ac708..102da9240a9 100644 --- a/homeassistant/components/octoprint/__init__.py +++ b/homeassistant/components/octoprint/__init__.py @@ -18,6 +18,7 @@ from homeassistant.const import ( CONF_PORT, CONF_SENSORS, CONF_SSL, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -49,7 +50,7 @@ def ensure_valid_path(value): return value -PLATFORMS = ["binary_sensor", "sensor"] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] DEFAULT_NAME = "OctoPrint" CONF_NUMBER_OF_TOOLS = "number_of_tools" CONF_BED = "bed" diff --git a/homeassistant/components/omnilogic/__init__.py b/homeassistant/components/omnilogic/__init__.py index ee43456285b..8a55eff6bb0 100644 --- a/homeassistant/components/omnilogic/__init__.py +++ b/homeassistant/components/omnilogic/__init__.py @@ -4,7 +4,7 @@ import logging from omnilogic import LoginException, OmniLogic, OmniLogicException from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client @@ -20,7 +20,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["sensor", "switch"] +PLATFORMS = [Platform.SENSOR, Platform.SWITCH] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/ondilo_ico/__init__.py b/homeassistant/components/ondilo_ico/__init__.py index 2d00af2a78b..e827b32f48a 100644 --- a/homeassistant/components/ondilo_ico/__init__.py +++ b/homeassistant/components/ondilo_ico/__init__.py @@ -1,6 +1,7 @@ """The Ondilo ICO integration.""" from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow @@ -8,7 +9,7 @@ from . import api, config_flow from .const import DOMAIN from .oauth_impl import OndiloOauth2Implementation -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/onvif/__init__.py b/homeassistant/components/onvif/__init__.py index 658ee7502f4..f6a6eabeb68 100644 --- a/homeassistant/components/onvif/__init__.py +++ b/homeassistant/components/onvif/__init__.py @@ -12,6 +12,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -82,10 +83,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.unique_id] = device - platforms = ["camera"] + platforms = [Platform.CAMERA] if device.capabilities.events: - platforms += ["binary_sensor", "sensor"] + platforms += [Platform.BINARY_SENSOR, Platform.SENSOR] hass.config_entries.async_setup_platforms(entry, platforms) diff --git a/homeassistant/components/opengarage/__init__.py b/homeassistant/components/opengarage/__init__.py index a8c8d50ce63..76ffcc42bd1 100644 --- a/homeassistant/components/opengarage/__init__.py +++ b/homeassistant/components/opengarage/__init__.py @@ -7,7 +7,7 @@ import logging import opengarage from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PORT, CONF_VERIFY_SSL +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_VERIFY_SSL, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import update_coordinator from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -16,7 +16,7 @@ from .const import CONF_DEVICE_KEY, DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["binary_sensor", "cover", "sensor"] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.COVER, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index f54a0e783cc..962379729db 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -6,9 +6,6 @@ import pyotgw import pyotgw.vars as gw_vars import voluptuous as vol -from homeassistant.components.binary_sensor import DOMAIN as COMP_BINARY_SENSOR -from homeassistant.components.climate import DOMAIN as COMP_CLIMATE -from homeassistant.components.sensor import DOMAIN as COMP_SENSOR from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_DATE, @@ -23,6 +20,7 @@ from homeassistant.const import ( PRECISION_HALVES, PRECISION_TENTHS, PRECISION_WHOLE, + Platform, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import ( @@ -80,7 +78,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -PLATFORMS = [COMP_BINARY_SENSOR, COMP_CLIMATE, COMP_SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.SENSOR] async def options_updated(hass, entry): diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index 77755fdca21..a60414e2872 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -15,6 +15,7 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_SENSORS, + Platform, ) from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ConfigEntryNotReady @@ -44,7 +45,7 @@ NOTIFICATION_TITLE = "OpenUV Component Setup" TOPIC_UPDATE = f"{DOMAIN}_data_update" -PLATFORMS = ["binary_sensor", "sensor"] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index 74aa767e44d..08568b02a17 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -40,6 +40,7 @@ from homeassistant.const import ( SPEED_METERS_PER_SECOND, TEMP_CELSIUS, UV_INDEX, + Platform, ) DOMAIN = "openweathermap" @@ -70,7 +71,7 @@ ATTR_API_UV_INDEX = "uv_index" ATTR_API_WEATHER_CODE = "weather_code" ATTR_API_FORECAST = "forecast" UPDATE_LISTENER = "update_listener" -PLATFORMS = ["sensor", "weather"] +PLATFORMS = [Platform.SENSOR, Platform.WEATHER] FORECAST_MODE_HOURLY = "hourly" FORECAST_MODE_DAILY = "daily" diff --git a/homeassistant/components/ovo_energy/__init__.py b/homeassistant/components/ovo_energy/__init__.py index 2b69ca0ade7..fceff685d6f 100644 --- a/homeassistant/components/ovo_energy/__init__.py +++ b/homeassistant/components/ovo_energy/__init__.py @@ -10,7 +10,7 @@ from ovoenergy import OVODailyUsage from ovoenergy.ovoenergy import OVOEnergy from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.device_registry import DeviceEntryType @@ -26,7 +26,7 @@ from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py index 24c7cc74d52..3cae9505ee8 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -14,6 +14,7 @@ from homeassistant.const import ( ATTR_LATITUDE, ATTR_LONGITUDE, CONF_WEBHOOK_ID, + Platform, ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -32,7 +33,7 @@ CONF_MQTT_TOPIC = "mqtt_topic" CONF_REGION_MAPPING = "region_mapping" CONF_EVENTS_ONLY = "events_only" BEACON_DEV_ID = "beacon" -PLATFORMS = ["device_tracker"] +PLATFORMS = [Platform.DEVICE_TRACKER] DEFAULT_OWNTRACKS_TOPIC = "owntracks/#" From 40b99135e5619b2d2f47748288803e41d60c8577 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 6 Dec 2021 04:10:07 +0100 Subject: [PATCH 0067/2644] Use platform enum (3) [H-L] (#60937) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/habitica/__init__.py | 3 +- homeassistant/components/harmony/const.py | 4 +- homeassistant/components/hassio/__init__.py | 3 +- homeassistant/components/heos/__init__.py | 11 ++--- .../components/hisense_aehw4a1/__init__.py | 4 +- homeassistant/components/hlk_sw16/__init__.py | 4 +- .../components/home_connect/__init__.py | 4 +- .../components/homematicip_cloud/const.py | 27 ++++------- .../components/honeywell/__init__.py | 4 +- .../components/huawei_lte/__init__.py | 25 ++++------ homeassistant/components/hue/bridge.py | 12 +++-- .../components/huisbaasje/__init__.py | 4 +- .../hunterdouglas_powerview/__init__.py | 4 +- .../components/hvv_departures/__init__.py | 6 +-- homeassistant/components/ialarm/__init__.py | 4 +- homeassistant/components/icloud/const.py | 3 +- homeassistant/components/iotawatt/__init__.py | 3 +- homeassistant/components/ipma/__init__.py | 4 +- homeassistant/components/ipp/__init__.py | 11 +++-- homeassistant/components/iqvia/__init__.py | 3 +- .../islamic_prayer_times/__init__.py | 3 +- homeassistant/components/isy994/const.py | 46 +++++++++++-------- homeassistant/components/izone/__init__.py | 4 +- homeassistant/components/juicenet/__init__.py | 4 +- .../components/keenetic_ndms2/__init__.py | 8 ++-- homeassistant/components/kmtronic/__init__.py | 4 +- homeassistant/components/kodi/__init__.py | 3 +- .../components/konnected/__init__.py | 3 +- .../components/kostal_plenticore/__init__.py | 3 +- homeassistant/components/kraken/__init__.py | 4 +- homeassistant/components/kulersky/__init__.py | 3 +- homeassistant/components/lcn/const.py | 11 ++++- homeassistant/components/lifx/__init__.py | 4 +- homeassistant/components/litejet/const.py | 3 +- .../components/litterrobot/__init__.py | 14 +++--- homeassistant/components/local_ip/const.py | 3 +- homeassistant/components/locative/__init__.py | 6 +-- .../components/logi_circle/__init__.py | 3 +- homeassistant/components/lookin/const.py | 7 ++- .../components/luftdaten/__init__.py | 3 +- .../components/lutron_caseta/__init__.py | 11 ++++- homeassistant/components/lyric/__init__.py | 4 +- 42 files changed, 165 insertions(+), 132 deletions(-) diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py index a08932d9c1b..af67178185d 100644 --- a/homeassistant/components/habitica/__init__.py +++ b/homeassistant/components/habitica/__init__.py @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_NAME, CONF_SENSORS, CONF_URL, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv @@ -73,7 +74,7 @@ INSTANCE_LIST_SCHEMA = vol.All( ) CONFIG_SCHEMA = vol.Schema({DOMAIN: INSTANCE_LIST_SCHEMA}, extra=vol.ALLOW_EXTRA) -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] SERVICE_API_CALL_SCHEMA = vol.Schema( { diff --git a/homeassistant/components/harmony/const.py b/homeassistant/components/harmony/const.py index c8e15ed0b0f..8df5b3d578c 100644 --- a/homeassistant/components/harmony/const.py +++ b/homeassistant/components/harmony/const.py @@ -1,8 +1,10 @@ """Constants for the Harmony component.""" +from homeassistant.const import Platform + DOMAIN = "harmony" SERVICE_SYNC = "sync" SERVICE_CHANGE_CHANNEL = "change_channel" -PLATFORMS = ["remote", "switch", "select"] +PLATFORMS = [Platform.REMOTE, Platform.SELECT, Platform.SWITCH] UNIQUE_ID = "unique_id" ACTIVITY_POWER_OFF = "PowerOff" HARMONY_OPTIONS_UPDATE = "harmony_options_update" diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 7991c50563c..614ea928828 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -22,6 +22,7 @@ from homeassistant.const import ( EVENT_CORE_CONFIG_UPDATE, SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP, + Platform, ) from homeassistant.core import DOMAIN as HASS_DOMAIN, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError @@ -68,7 +69,7 @@ _LOGGER = logging.getLogger(__name__) STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 -PLATFORMS = ["binary_sensor", "sensor"] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] CONF_FRONTEND_REPO = "development_repo" diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py index 2dbab5e9409..bbe611c1db9 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -8,9 +8,8 @@ import logging from pyheos import Heos, HeosError, const as heos_const import voluptuous as vol -from homeassistant.components.media_player.const import DOMAIN as MEDIA_PLAYER_DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError import homeassistant.helpers.config_validation as cv @@ -35,7 +34,7 @@ from .const import ( SIGNAL_HEOS_UPDATED, ) -PLATFORMS = [MEDIA_PLAYER_DOMAIN] +PLATFORMS = [Platform.MEDIA_PLAYER] CONFIG_SCHEMA = vol.Schema( vol.All( @@ -130,7 +129,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_CONTROLLER_MANAGER: controller_manager, DATA_GROUP_MANAGER: group_manager, DATA_SOURCE_MANAGER: source_manager, - MEDIA_PLAYER_DOMAIN: players, + Platform.MEDIA_PLAYER: players, # Maps player_id to entity_id. Populated by the individual HeosMediaPlayer entities. DATA_ENTITY_ID_MAP: {}, } @@ -228,7 +227,7 @@ class ControllerManager: ) # update entity registry entity_id = self._entity_registry.async_get_entity_id( - MEDIA_PLAYER_DOMAIN, DOMAIN, str(old_id) + Platform.MEDIA_PLAYER, DOMAIN, str(old_id) ) if entity_id: self._entity_registry.async_update_entity( @@ -355,7 +354,7 @@ class GroupManager: # Avoid calling async_update_groups when `DATA_ENTITY_ID_MAP` has not been # fully populated yet. This may only happen during early startup. if ( - len(self._hass.data[DOMAIN][MEDIA_PLAYER_DOMAIN]) + len(self._hass.data[DOMAIN][Platform.MEDIA_PLAYER]) <= len(self._hass.data[DOMAIN][DATA_ENTITY_ID_MAP]) and not self._initialized ): diff --git a/homeassistant/components/hisense_aehw4a1/__init__.py b/homeassistant/components/hisense_aehw4a1/__init__.py index 1134ac4181d..bc38d1df53f 100644 --- a/homeassistant/components/hisense_aehw4a1/__init__.py +++ b/homeassistant/components/hisense_aehw4a1/__init__.py @@ -8,14 +8,14 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN -from homeassistant.const import CONF_IP_ADDRESS +from homeassistant.const import CONF_IP_ADDRESS, Platform import homeassistant.helpers.config_validation as cv from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORMS = [CLIMATE_DOMAIN] +PLATFORMS = [Platform.CLIMATE] def coerce_ip(value): diff --git a/homeassistant/components/hlk_sw16/__init__.py b/homeassistant/components/hlk_sw16/__init__.py index e36af7676ed..9fae14f8d1a 100644 --- a/homeassistant/components/hlk_sw16/__init__.py +++ b/homeassistant/components/hlk_sw16/__init__.py @@ -5,7 +5,7 @@ from hlk_sw16 import create_hlk_sw16_connection import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_SWITCHES +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_SWITCHES, Platform from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( @@ -24,7 +24,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["switch"] +PLATFORMS = [Platform.SWITCH] DATA_DEVICE_REGISTER = "hlk_sw16_device_register" DATA_DEVICE_LISTENER = "hlk_sw16_device_listener" diff --git a/homeassistant/components/home_connect/__init__.py b/homeassistant/components/home_connect/__init__.py index 1fc446af401..26448893438 100644 --- a/homeassistant/components/home_connect/__init__.py +++ b/homeassistant/components/home_connect/__init__.py @@ -7,7 +7,7 @@ from requests import HTTPError import voluptuous as vol from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv from homeassistant.helpers.typing import ConfigType @@ -32,7 +32,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -PLATFORMS = ["binary_sensor", "light", "sensor", "switch"] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.LIGHT, Platform.SENSOR, Platform.SWITCH] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: diff --git a/homeassistant/components/homematicip_cloud/const.py b/homeassistant/components/homematicip_cloud/const.py index 31ccb8b9bc7..a0f1c84015f 100644 --- a/homeassistant/components/homematicip_cloud/const.py +++ b/homeassistant/components/homematicip_cloud/const.py @@ -1,30 +1,21 @@ """Constants for the HomematicIP Cloud component.""" import logging -from homeassistant.components.alarm_control_panel import ( - DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, -) -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN -from homeassistant.components.cover import DOMAIN as COVER_DOMAIN -from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN +from homeassistant.const import Platform _LOGGER = logging.getLogger(".") DOMAIN = "homematicip_cloud" PLATFORMS = [ - ALARM_CONTROL_PANEL_DOMAIN, - BINARY_SENSOR_DOMAIN, - CLIMATE_DOMAIN, - COVER_DOMAIN, - LIGHT_DOMAIN, - SENSOR_DOMAIN, - SWITCH_DOMAIN, - WEATHER_DOMAIN, + Platform.ALARM_CONTROL_PANEL, + Platform.BINARY_SENSOR, + Platform.CLIMATE, + Platform.COVER, + Platform.LIGHT, + Platform.SENSOR, + Platform.SWITCH, + Platform.WEATHER, ] CONF_ACCESSPOINT = "accesspoint" diff --git a/homeassistant/components/honeywell/__init__.py b/homeassistant/components/honeywell/__init__.py index c61e4fc18eb..485562f8b5d 100644 --- a/homeassistant/components/honeywell/__init__.py +++ b/homeassistant/components/honeywell/__init__.py @@ -4,7 +4,7 @@ from datetime import timedelta import somecomfort -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.util import Throttle @@ -12,7 +12,7 @@ from .const import _LOGGER, CONF_DEV_ID, CONF_LOC_ID, DOMAIN UPDATE_LOOP_SLEEP_TIME = 5 MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=300) -PLATFORMS = ["climate"] +PLATFORMS = [Platform.CLIMATE] async def async_setup_entry(hass, config): diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index f63b84254fc..4f4451330f6 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -22,13 +22,7 @@ from requests.exceptions import Timeout from url_normalize import url_normalize import voluptuous as vol -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.device_tracker.const import ( - DOMAIN as DEVICE_TRACKER_DOMAIN, -) from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( ATTR_MODEL, @@ -40,6 +34,7 @@ from homeassistant.const import ( CONF_URL, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, + Platform, ) from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import ConfigEntryNotReady @@ -123,12 +118,12 @@ CONFIG_SCHEMA = vol.Schema( SERVICE_SCHEMA = vol.Schema({vol.Optional(CONF_URL): cv.url}) -CONFIG_ENTRY_PLATFORMS = ( - BINARY_SENSOR_DOMAIN, - DEVICE_TRACKER_DOMAIN, - SENSOR_DOMAIN, - SWITCH_DOMAIN, -) +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.DEVICE_TRACKER, + Platform.SENSOR, + Platform.SWITCH, +] @attr.s @@ -455,7 +450,7 @@ async def async_setup_entry( # noqa: C901 ) # Forward config entry setup to platforms - hass.config_entries.async_setup_platforms(entry, CONFIG_ENTRY_PLATFORMS) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) # Notify doesn't support config entry setup yet, load with discovery for now await discovery.async_load_platform( @@ -495,9 +490,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> """Unload config entry.""" # Forward config entry unload to platforms - await hass.config_entries.async_unload_platforms( - config_entry, CONFIG_ENTRY_PLATFORMS - ) + await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) # Forget about the router and invoke its cleanup router = hass.data[DOMAIN].routers.pop(config_entry.unique_id) diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 5005f858a58..cda639bcf6c 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -14,7 +14,7 @@ import async_timeout from homeassistant import core from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_HOST +from homeassistant.const import CONF_API_KEY, CONF_HOST, Platform from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client @@ -26,8 +26,14 @@ from .v2.hue_event import async_setup_hue_events # How long should we sleep if the hub is busy HUB_BUSY_SLEEP = 0.5 -PLATFORMS_v1 = ["light", "binary_sensor", "sensor"] -PLATFORMS_v2 = ["light", "binary_sensor", "sensor", "scene", "switch"] +PLATFORMS_v1 = [Platform.BINARY_SENSOR, Platform.LIGHT, Platform.SENSOR] +PLATFORMS_v2 = [ + Platform.BINARY_SENSOR, + Platform.LIGHT, + Platform.SCENE, + Platform.SENSOR, + Platform.SWITCH, +] class HueBridge: diff --git a/homeassistant/components/huisbaasje/__init__.py b/homeassistant/components/huisbaasje/__init__.py index 6c8df713344..f2b4ef8d4ef 100644 --- a/homeassistant/components/huisbaasje/__init__.py +++ b/homeassistant/components/huisbaasje/__init__.py @@ -6,7 +6,7 @@ import async_timeout from huisbaasje import Huisbaasje, HuisbaasjeException from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -23,7 +23,7 @@ from .const import ( SOURCE_TYPES, ) -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hunterdouglas_powerview/__init__.py b/homeassistant/components/hunterdouglas_powerview/__init__.py index a9c620a4baa..304d7e13cc1 100644 --- a/homeassistant/components/hunterdouglas_powerview/__init__.py +++ b/homeassistant/components/hunterdouglas_powerview/__init__.py @@ -13,7 +13,7 @@ from aiopvapi.userdata import UserData import async_timeout from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -55,7 +55,7 @@ PARALLEL_UPDATES = 1 CONFIG_SCHEMA = cv.deprecated(DOMAIN) -PLATFORMS = ["cover", "scene", "sensor"] +PLATFORMS = [Platform.COVER, Platform.SCENE, Platform.SENSOR] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hvv_departures/__init__.py b/homeassistant/components/hvv_departures/__init__.py index 09b9d516bad..3c090249bc0 100644 --- a/homeassistant/components/hvv_departures/__init__.py +++ b/homeassistant/components/hvv_departures/__init__.py @@ -1,16 +1,14 @@ """The HVV integration.""" -from homeassistant.components.binary_sensor import DOMAIN as DOMAIN_BINARY_SENSOR -from homeassistant.components.sensor import DOMAIN as DOMAIN_SENSOR from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client from .const import DOMAIN from .hub import GTIHub -PLATFORMS = [DOMAIN_SENSOR, DOMAIN_BINARY_SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/ialarm/__init__.py b/homeassistant/components/ialarm/__init__.py index af8fd0695e4..db9aa000066 100644 --- a/homeassistant/components/ialarm/__init__.py +++ b/homeassistant/components/ialarm/__init__.py @@ -7,14 +7,14 @@ from pyialarm import IAlarm from homeassistant.components.alarm_control_panel import SCAN_INTERVAL from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_PORT, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DATA_COORDINATOR, DOMAIN, IALARM_TO_HASS -PLATFORMS = ["alarm_control_panel"] +PLATFORMS = [Platform.ALARM_CONTROL_PANEL] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/icloud/const.py b/homeassistant/components/icloud/const.py index 58c62f8a868..231f2cc1d0a 100644 --- a/homeassistant/components/icloud/const.py +++ b/homeassistant/components/icloud/const.py @@ -1,4 +1,5 @@ """iCloud component constants.""" +from homeassistant.const import Platform DOMAIN = "icloud" @@ -14,7 +15,7 @@ DEFAULT_GPS_ACCURACY_THRESHOLD = 500 # meters STORAGE_KEY = DOMAIN STORAGE_VERSION = 2 -PLATFORMS = ["device_tracker", "sensor"] +PLATFORMS = [Platform.DEVICE_TRACKER, Platform.SENSOR] # pyicloud.AppleDevice status DEVICE_BATTERY_LEVEL = "batteryLevel" diff --git a/homeassistant/components/iotawatt/__init__.py b/homeassistant/components/iotawatt/__init__.py index 7987004e594..55fc701cffd 100644 --- a/homeassistant/components/iotawatt/__init__.py +++ b/homeassistant/components/iotawatt/__init__.py @@ -1,11 +1,12 @@ """The iotawatt integration.""" from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from .const import DOMAIN from .coordinator import IotawattUpdater -PLATFORMS = ("sensor",) +PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/ipma/__init__.py b/homeassistant/components/ipma/__init__.py index 1a26d375653..f24f8104bd8 100644 --- a/homeassistant/components/ipma/__init__.py +++ b/homeassistant/components/ipma/__init__.py @@ -1,10 +1,12 @@ """Component for the Portuguese weather service - IPMA.""" +from homeassistant.const import Platform + from .config_flow import IpmaFlowHandler # noqa: F401 from .const import DOMAIN # noqa: F401 DEFAULT_NAME = "ipma" -PLATFORMS = ["weather"] +PLATFORMS = [Platform.WEATHER] async def async_setup_entry(hass, entry): diff --git a/homeassistant/components/ipp/__init__.py b/homeassistant/components/ipp/__init__.py index d5930ac5f56..23ee5adc0e4 100644 --- a/homeassistant/components/ipp/__init__.py +++ b/homeassistant/components/ipp/__init__.py @@ -1,15 +1,20 @@ """The Internet Printing Protocol (IPP) integration.""" from __future__ import annotations -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SSL, CONF_VERIFY_SSL +from homeassistant.const import ( + CONF_HOST, + CONF_PORT, + CONF_SSL, + CONF_VERIFY_SSL, + Platform, +) from homeassistant.core import HomeAssistant from .const import CONF_BASE_PATH, DOMAIN from .coordinator import IPPDataUpdateCoordinator -PLATFORMS = [SENSOR_DOMAIN] +PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index b0f7086cad7..56d90874eba 100644 --- a/homeassistant/components/iqvia/__init__.py +++ b/homeassistant/components/iqvia/__init__.py @@ -11,6 +11,7 @@ from pyiqvia import Client from pyiqvia.errors import IQVIAError from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client @@ -37,7 +38,7 @@ from .const import ( DEFAULT_ATTRIBUTION = "Data provided by IQVIA™" DEFAULT_SCAN_INTERVAL = timedelta(minutes=30) -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/islamic_prayer_times/__init__.py b/homeassistant/components/islamic_prayer_times/__init__.py index 8375b9d4c12..a55838970fe 100644 --- a/homeassistant/components/islamic_prayer_times/__init__.py +++ b/homeassistant/components/islamic_prayer_times/__init__.py @@ -7,6 +7,7 @@ from requests.exceptions import ConnectionError as ConnError import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import Platform from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -23,7 +24,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] CONFIG_SCHEMA = vol.Schema( vol.All( diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index 8e634006ec2..0a7b02c1dac 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -15,14 +15,12 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_SMOKE, DEVICE_CLASS_SOUND, DEVICE_CLASS_VIBRATION, - DOMAIN as BINARY_SENSOR, ) from homeassistant.components.climate.const import ( CURRENT_HVAC_COOL, CURRENT_HVAC_FAN, CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, - DOMAIN as CLIMATE, FAN_AUTO, FAN_HIGH, FAN_MEDIUM, @@ -37,12 +35,6 @@ from homeassistant.components.climate.const import ( PRESET_AWAY, PRESET_BOOST, ) -from homeassistant.components.cover import DOMAIN as COVER -from homeassistant.components.fan import DOMAIN as FAN -from homeassistant.components.light import DOMAIN as LIGHT -from homeassistant.components.lock import DOMAIN as LOCK -from homeassistant.components.sensor import DOMAIN as SENSOR -from homeassistant.components.switch import DOMAIN as SWITCH from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, CURRENCY_CENT, @@ -109,6 +101,7 @@ from homeassistant.const import ( VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR, VOLUME_GALLONS, VOLUME_LITERS, + Platform, ) _LOGGER = logging.getLogger(__package__) @@ -133,14 +126,29 @@ DEFAULT_VAR_SENSOR_STRING = "HA." KEY_ACTIONS = "actions" KEY_STATUS = "status" -PLATFORMS = [BINARY_SENSOR, SENSOR, LOCK, FAN, COVER, LIGHT, SWITCH, CLIMATE] -PROGRAM_PLATFORMS = [BINARY_SENSOR, LOCK, FAN, COVER, SWITCH] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.CLIMATE, + Platform.COVER, + Platform.FAN, + Platform.LIGHT, + Platform.LOCK, + Platform.SENSOR, + Platform.SWITCH, +] +PROGRAM_PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.COVER, + Platform.FAN, + Platform.LOCK, + Platform.SWITCH, +] SUPPORTED_BIN_SENS_CLASSES = ["moisture", "opening", "motion", "climate"] # ISY Scenes are more like Switches than Home Assistant Scenes # (they can turn off, and report their state) -ISY_GROUP_PLATFORM = SWITCH +ISY_GROUP_PLATFORM = Platform.SWITCH ISY994_ISY = "isy" ISY994_NODES = "isy994_nodes" @@ -211,7 +219,7 @@ UOM_PERCENTAGE = "51" # Insteon Types: https://www.universal-devices.com/developers/wsdk/5.0.4/1_fam.xml # Z-Wave Categories: https://www.universal-devices.com/developers/wsdk/5.0.4/4_fam.xml NODE_FILTERS = { - BINARY_SENSOR: { + Platform.BINARY_SENSOR: { FILTER_UOM: [UOM_ON_OFF], FILTER_STATES: [], FILTER_NODE_DEF_ID: [ @@ -231,7 +239,7 @@ NODE_FILTERS = { ], # Does a startswith() match; include the dot FILTER_ZWAVE_CAT: (["104", "112", "138"] + list(map(str, range(148, 180)))), }, - SENSOR: { + Platform.SENSOR: { # This is just a more-readable way of including MOST uoms between 1-100 # (Remember that range() is non-inclusive of the stop value) FILTER_UOM: ( @@ -255,28 +263,28 @@ NODE_FILTERS = { FILTER_INSTEON_TYPE: ["0.16.", "0.17.", "0.18.", "9.0.", "9.7."], FILTER_ZWAVE_CAT: (["118", "143"] + list(map(str, range(180, 186)))), }, - LOCK: { + Platform.LOCK: { FILTER_UOM: ["11"], FILTER_STATES: ["locked", "unlocked"], FILTER_NODE_DEF_ID: ["DoorLock"], FILTER_INSTEON_TYPE: [TYPE_CATEGORY_LOCK, "4.64."], FILTER_ZWAVE_CAT: ["111"], }, - FAN: { + Platform.FAN: { FILTER_UOM: [], FILTER_STATES: ["off", "low", "med", "high"], FILTER_NODE_DEF_ID: ["FanLincMotor"], FILTER_INSTEON_TYPE: ["1.46."], FILTER_ZWAVE_CAT: [], }, - COVER: { + Platform.COVER: { FILTER_UOM: [UOM_BARRIER], FILTER_STATES: ["open", "closed", "closing", "opening", "stopped"], FILTER_NODE_DEF_ID: ["DimmerMotorSwitch_ADV"], FILTER_INSTEON_TYPE: [TYPE_CATEGORY_COVER], FILTER_ZWAVE_CAT: [], }, - LIGHT: { + Platform.LIGHT: { FILTER_UOM: ["51"], FILTER_STATES: ["on", "off", "%"], FILTER_NODE_DEF_ID: [ @@ -293,7 +301,7 @@ NODE_FILTERS = { FILTER_INSTEON_TYPE: [TYPE_CATEGORY_DIMMABLE], FILTER_ZWAVE_CAT: ["109", "119"], }, - SWITCH: { + Platform.SWITCH: { FILTER_UOM: ["78"], FILTER_STATES: ["on", "off"], FILTER_NODE_DEF_ID: [ @@ -323,7 +331,7 @@ NODE_FILTERS = { ], FILTER_ZWAVE_CAT: ["121", "122", "123", "137", "141", "147"], }, - CLIMATE: { + Platform.CLIMATE: { FILTER_UOM: [UOM_ON_OFF], FILTER_STATES: ["heating", "cooling", "idle", "fan_only", "off"], FILTER_NODE_DEF_ID: ["TempLinc", "Thermostat"], diff --git a/homeassistant/components/izone/__init__.py b/homeassistant/components/izone/__init__.py index 2ac66963638..d49de8c0e55 100644 --- a/homeassistant/components/izone/__init__.py +++ b/homeassistant/components/izone/__init__.py @@ -2,7 +2,7 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_EXCLUDE +from homeassistant.const import CONF_EXCLUDE, Platform from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType @@ -10,7 +10,7 @@ from homeassistant.helpers.typing import ConfigType from .const import DATA_CONFIG, IZONE from .discovery import async_start_discovery_service, async_stop_discovery_service -PLATFORMS = ["climate"] +PLATFORMS = [Platform.CLIMATE] CONFIG_SCHEMA = vol.Schema( { diff --git a/homeassistant/components/juicenet/__init__.py b/homeassistant/components/juicenet/__init__.py index 41097589244..92d8de8bc0a 100644 --- a/homeassistant/components/juicenet/__init__.py +++ b/homeassistant/components/juicenet/__init__.py @@ -7,7 +7,7 @@ from pyjuicenet import Api, TokenError import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.const import CONF_ACCESS_TOKEN, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv @@ -20,7 +20,7 @@ from .device import JuiceNetApi _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["sensor", "switch"] +PLATFORMS = [Platform.SENSOR, Platform.SWITCH] CONFIG_SCHEMA = vol.Schema( vol.All( diff --git a/homeassistant/components/keenetic_ndms2/__init__.py b/homeassistant/components/keenetic_ndms2/__init__.py index 2af7434d2e4..94a249df209 100644 --- a/homeassistant/components/keenetic_ndms2/__init__.py +++ b/homeassistant/components/keenetic_ndms2/__init__.py @@ -3,10 +3,8 @@ from __future__ import annotations import logging -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_SCAN_INTERVAL +from homeassistant.const import CONF_HOST, CONF_SCAN_INTERVAL, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry, entity_registry @@ -25,7 +23,7 @@ from .const import ( ) from .router import KeeneticRouter -PLATFORMS = [BINARY_SENSOR_DOMAIN, DEVICE_TRACKER_DOMAIN] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.DEVICE_TRACKER] _LOGGER = logging.getLogger(__name__) @@ -80,7 +78,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> for entity_entry in list(ent_reg.entities.values()): if ( entity_entry.config_entry_id == config_entry.entry_id - and entity_entry.domain == DEVICE_TRACKER_DOMAIN + and entity_entry.domain == Platform.DEVICE_TRACKER ): mac = entity_entry.unique_id.partition("_")[0] if mac not in keep_devices: diff --git a/homeassistant/components/kmtronic/__init__.py b/homeassistant/components/kmtronic/__init__.py index 72c0c772aec..a7637879fc1 100644 --- a/homeassistant/components/kmtronic/__init__.py +++ b/homeassistant/components/kmtronic/__init__.py @@ -8,14 +8,14 @@ from pykmtronic.auth import Auth from pykmtronic.hub import KMTronicHubAPI from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DATA_COORDINATOR, DATA_HUB, DOMAIN, MANUFACTURER, UPDATE_LISTENER -PLATFORMS = ["switch"] +PLATFORMS = [Platform.SWITCH] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/kodi/__init__.py b/homeassistant/components/kodi/__init__.py index 7c7740e8fe8..f5e58f3c6a1 100644 --- a/homeassistant/components/kodi/__init__.py +++ b/homeassistant/components/kodi/__init__.py @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_SSL, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -25,7 +26,7 @@ from .const import ( ) _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["media_player"] +PLATFORMS = [Platform.MEDIA_PLAYER] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/konnected/__init__.py b/homeassistant/components/konnected/__init__.py index 43537154e41..f018d1a5133 100644 --- a/homeassistant/components/konnected/__init__.py +++ b/homeassistant/components/konnected/__init__.py @@ -31,6 +31,7 @@ from homeassistant.const import ( CONF_ZONE, STATE_OFF, STATE_ON, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv @@ -216,7 +217,7 @@ CONFIG_SCHEMA = vol.Schema( ) YAML_CONFIGS = "yaml_configs" -PLATFORMS = ["binary_sensor", "sensor", "switch"] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: diff --git a/homeassistant/components/kostal_plenticore/__init__.py b/homeassistant/components/kostal_plenticore/__init__.py index f5c973cc499..a42ad0a64ff 100644 --- a/homeassistant/components/kostal_plenticore/__init__.py +++ b/homeassistant/components/kostal_plenticore/__init__.py @@ -4,6 +4,7 @@ import logging from kostal.plenticore import PlenticoreApiException from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from .const import DOMAIN @@ -11,7 +12,7 @@ from .helper import Plenticore _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["select", "sensor", "switch"] +PLATFORMS = [Platform.SELECT, Platform.SENSOR, Platform.SWITCH] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/kraken/__init__.py b/homeassistant/components/kraken/__init__.py index 5b1fd2626e3..bf01f27673d 100644 --- a/homeassistant/components/kraken/__init__.py +++ b/homeassistant/components/kraken/__init__.py @@ -10,7 +10,7 @@ import krakenex import pykrakenapi from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_SCAN_INTERVAL +from homeassistant.const import CONF_SCAN_INTERVAL, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -27,7 +27,7 @@ from .utils import get_tradable_asset_pairs CALL_RATE_LIMIT_SLEEP = 1 -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/kulersky/__init__.py b/homeassistant/components/kulersky/__init__.py index 0ddac1850d7..39c0d0a5b84 100644 --- a/homeassistant/components/kulersky/__init__.py +++ b/homeassistant/components/kulersky/__init__.py @@ -1,11 +1,12 @@ """Kuler Sky lights integration.""" from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from .const import DATA_ADDRESSES, DATA_DISCOVERY_SUBSCRIPTION, DOMAIN -PLATFORMS = ["light"] +PLATFORMS = [Platform.LIGHT] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/lcn/const.py b/homeassistant/components/lcn/const.py index faef86dc70a..f2059827e84 100644 --- a/homeassistant/components/lcn/const.py +++ b/homeassistant/components/lcn/const.py @@ -8,9 +8,18 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, TEMP_KELVIN, + Platform, ) -PLATFORMS = ["binary_sensor", "climate", "cover", "light", "scene", "sensor", "switch"] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.CLIMATE, + Platform.COVER, + Platform.LIGHT, + Platform.SCENE, + Platform.SENSOR, + Platform.SWITCH, +] DOMAIN = "lcn" DATA_LCN = "lcn" diff --git a/homeassistant/components/lifx/__init__.py b/homeassistant/components/lifx/__init__.py index b0b67450b5e..6d967ea9c69 100644 --- a/homeassistant/components/lifx/__init__.py +++ b/homeassistant/components/lifx/__init__.py @@ -3,7 +3,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN -from homeassistant.const import CONF_PORT +from homeassistant.const import CONF_PORT, Platform import homeassistant.helpers.config_validation as cv from .const import DOMAIN @@ -26,7 +26,7 @@ CONFIG_SCHEMA = vol.Schema( DATA_LIFX_MANAGER = "lifx_manager" -PLATFORMS = [LIGHT_DOMAIN] +PLATFORMS = [Platform.LIGHT] async def async_setup(hass, config): diff --git a/homeassistant/components/litejet/const.py b/homeassistant/components/litejet/const.py index 82521092106..baeadd7f4a9 100644 --- a/homeassistant/components/litejet/const.py +++ b/homeassistant/components/litejet/const.py @@ -1,10 +1,11 @@ """LiteJet constants.""" +from homeassistant.const import Platform DOMAIN = "litejet" CONF_EXCLUDE_NAMES = "exclude_names" CONF_INCLUDE_SWITCHES = "include_switches" -PLATFORMS = ["light", "switch", "scene"] +PLATFORMS = [Platform.LIGHT, Platform.SCENE, Platform.SWITCH] CONF_DEFAULT_TRANSITION = "default_transition" diff --git a/homeassistant/components/litterrobot/__init__.py b/homeassistant/components/litterrobot/__init__.py index d972ecc79d9..90c432010d7 100644 --- a/homeassistant/components/litterrobot/__init__.py +++ b/homeassistant/components/litterrobot/__init__.py @@ -2,19 +2,21 @@ from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException -from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN -from homeassistant.components.select import DOMAIN as SELECT_DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.components.vacuum import DOMAIN as VACUUM_DOMAIN from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from .const import DOMAIN from .hub import LitterRobotHub -PLATFORMS = [BUTTON_DOMAIN, SELECT_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN, VACUUM_DOMAIN] +PLATFORMS = [ + Platform.BUTTON, + Platform.SELECT, + Platform.SENSOR, + Platform.SWITCH, + Platform.VACUUM, +] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/local_ip/const.py b/homeassistant/components/local_ip/const.py index 0bac6d874d1..b079ec87663 100644 --- a/homeassistant/components/local_ip/const.py +++ b/homeassistant/components/local_ip/const.py @@ -1,6 +1,7 @@ """Local IP constants.""" +from homeassistant.const import Platform DOMAIN = "local_ip" -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] SENSOR = "address" diff --git a/homeassistant/components/locative/__init__.py b/homeassistant/components/locative/__init__.py index 548b0a2d1fe..ec61cbbcdc7 100644 --- a/homeassistant/components/locative/__init__.py +++ b/homeassistant/components/locative/__init__.py @@ -7,13 +7,13 @@ import logging from aiohttp import web import voluptuous as vol -from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER from homeassistant.const import ( ATTR_ID, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_WEBHOOK_ID, STATE_NOT_HOME, + Platform, ) from homeassistant.helpers import config_entry_flow import homeassistant.helpers.config_validation as cv @@ -24,7 +24,7 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "locative" TRACKER_UPDATE = f"{DOMAIN}_tracker_update" -PLATFORMS = [DEVICE_TRACKER] +PLATFORMS = [Platform.DEVICE_TRACKER] ATTR_DEVICE_ID = "device" ATTR_TRIGGER = "trigger" @@ -82,7 +82,7 @@ async def handle_webhook(hass, webhook_id, request): return web.Response(text=f"Setting location to {location_name}") if direction == "exit": - current_state = hass.states.get(f"{DEVICE_TRACKER}.{device}") + current_state = hass.states.get(f"{Platform.DEVICE_TRACKER}.{device}") if current_state is None or current_state.state == location_name: location_name = STATE_NOT_HOME diff --git a/homeassistant/components/logi_circle/__init__.py b/homeassistant/components/logi_circle/__init__.py index 45b34928a30..01034a3a5ca 100644 --- a/homeassistant/components/logi_circle/__init__.py +++ b/homeassistant/components/logi_circle/__init__.py @@ -18,6 +18,7 @@ from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_SENSORS, EVENT_HOMEASSISTANT_STOP, + Platform, ) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -48,7 +49,7 @@ SERVICE_LIVESTREAM_RECORD = "livestream_record" ATTR_VALUE = "value" ATTR_DURATION = "duration" -PLATFORMS = ["camera", "sensor"] +PLATFORMS = [Platform.CAMERA, Platform.SENSOR] SENSOR_KEYS = [desc.key for desc in SENSOR_TYPES] diff --git a/homeassistant/components/lookin/const.py b/homeassistant/components/lookin/const.py index 1605a169592..d9b1141aa97 100644 --- a/homeassistant/components/lookin/const.py +++ b/homeassistant/components/lookin/const.py @@ -3,8 +3,7 @@ from __future__ import annotations from typing import Final +from homeassistant.const import Platform + DOMAIN: Final = "lookin" -PLATFORMS: Final = [ - "sensor", - "climate", -] +PLATFORMS: Final = [Platform.CLIMATE, Platform.SENSOR] diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py index f8a67fff2f3..63b9965243e 100644 --- a/homeassistant/components/luftdaten/__init__.py +++ b/homeassistant/components/luftdaten/__init__.py @@ -21,6 +21,7 @@ from homeassistant.const import ( PERCENTAGE, PRESSURE_PA, TEMP_CELSIUS, + Platform, ) from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady @@ -38,7 +39,7 @@ DATA_LUFTDATEN_CLIENT = "data_luftdaten_client" DATA_LUFTDATEN_LISTENER = "data_luftdaten_listener" DEFAULT_ATTRIBUTION = "Data provided by luftdaten.info" -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] SENSOR_HUMIDITY = "humidity" SENSOR_PM10 = "P1" diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index e1a93385d31..3c74e378336 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -12,7 +12,7 @@ from pylutron_caseta.smartbridge import Smartbridge import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, Platform from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr @@ -63,7 +63,14 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -PLATFORMS = ["light", "switch", "cover", "scene", "fan", "binary_sensor"] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.COVER, + Platform.FAN, + Platform.LIGHT, + Platform.SCENE, + Platform.SWITCH, +] async def async_setup(hass, base_config): diff --git a/homeassistant/components/lyric/__init__.py b/homeassistant/components/lyric/__init__.py index a9d5cbdec7d..623abb54c4b 100644 --- a/homeassistant/components/lyric/__init__.py +++ b/homeassistant/components/lyric/__init__.py @@ -13,7 +13,7 @@ import async_timeout import voluptuous as vol from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import ( @@ -48,7 +48,7 @@ CONFIG_SCHEMA = vol.Schema( _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["climate", "sensor"] +PLATFORMS = [Platform.CLIMATE, Platform.SENSOR] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: From 52d6b83da88f00032abe7cd6bc26c8de6303a4b5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Dec 2021 18:41:09 -1000 Subject: [PATCH 0068/2644] Abort flux_led discovery if another device gets the ip (#61074) - If the dhcp reservation expired for the device that was at the ip and a new flux_led device appears we would discover it because the unique_id did not match --- .../components/flux_led/config_flow.py | 5 ++-- tests/components/flux_led/test_config_flow.py | 30 +++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/flux_led/config_flow.py b/homeassistant/components/flux_led/config_flow.py index ab39e5b8ace..87d07bba2b1 100644 --- a/homeassistant/components/flux_led/config_flow.py +++ b/homeassistant/components/flux_led/config_flow.py @@ -115,8 +115,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(mac) self._abort_if_unique_id_configured(updates={CONF_HOST: host}) for entry in self._async_current_entries(include_ignore=False): - if entry.data[CONF_HOST] == host and not entry.unique_id: - async_update_entry_from_discovery(self.hass, entry, device) + if entry.data[CONF_HOST] == host: + if not entry.unique_id: + async_update_entry_from_discovery(self.hass, entry, device) return self.async_abort(reason="already_configured") self.context[CONF_HOST] = host for progress in self._async_in_progress(): diff --git a/tests/components/flux_led/test_config_flow.py b/tests/components/flux_led/test_config_flow.py index af84d3561f7..4c956358818 100644 --- a/tests/components/flux_led/test_config_flow.py +++ b/tests/components/flux_led/test_config_flow.py @@ -40,6 +40,8 @@ from . import ( from tests.common import MockConfigEntry +MAC_ADDRESS_DIFFERENT = "ff:bb:ff:dd:ee:ff" + async def test_discovery(hass: HomeAssistant): """Test setting up discovery.""" @@ -472,6 +474,34 @@ async def test_discovered_by_dhcp_or_discovery_adds_missing_unique_id( assert config_entry.unique_id == MAC_ADDRESS +@pytest.mark.parametrize( + "source, data", + [ + (config_entries.SOURCE_DHCP, DHCP_DISCOVERY), + (config_entries.SOURCE_DISCOVERY, FLUX_DISCOVERY), + ], +) +async def test_discovered_by_dhcp_or_discovery_mac_address_mismatch_host_already_configured( + hass, source, data +): + """Test we abort if the host is already configured but the mac does not match.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=MAC_ADDRESS_DIFFERENT + ) + config_entry.add_to_hass(hass) + + with _patch_discovery(), _patch_wifibulb(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": source}, data=data + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + assert config_entry.unique_id == MAC_ADDRESS_DIFFERENT + + async def test_options(hass: HomeAssistant): """Test options flow.""" config_entry = MockConfigEntry( From 40a57b3b019681e7d94803db323412287979fd7a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Dec 2021 21:48:17 -1000 Subject: [PATCH 0069/2644] Bump enphase_envoy to 0.20.1 (#61082) --- homeassistant/components/enphase_envoy/manifest.json | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 9e948eaf842..d7ad10ca062 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -3,7 +3,7 @@ "name": "Enphase Envoy", "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "requirements": [ - "envoy_reader==0.20.0" + "envoy_reader==0.20.1" ], "codeowners": [ "@gtdiehl" @@ -15,4 +15,4 @@ } ], "iot_class": "local_polling" -} \ No newline at end of file +} diff --git a/requirements_all.txt b/requirements_all.txt index 9c40c80c07d..501ab2f4819 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -609,7 +609,7 @@ env_canada==0.5.18 # envirophat==0.0.6 # homeassistant.components.enphase_envoy -envoy_reader==0.20.0 +envoy_reader==0.20.1 # homeassistant.components.season ephem==3.7.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eeb755b74ab..a0ba7da5d24 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -381,7 +381,7 @@ enocean==0.50 env_canada==0.5.18 # homeassistant.components.enphase_envoy -envoy_reader==0.20.0 +envoy_reader==0.20.1 # homeassistant.components.season ephem==3.7.7.0 From 742623ee81a66d98da2b11bf2b8ec18996583896 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Dec 2021 21:50:45 -1000 Subject: [PATCH 0070/2644] Provide a hint on which username to use for enphase_envoy (#61084) --- homeassistant/components/enphase_envoy/strings.json | 1 + homeassistant/components/enphase_envoy/translations/en.json | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/enphase_envoy/strings.json b/homeassistant/components/enphase_envoy/strings.json index b42f6bfb50f..822ee14fc9e 100644 --- a/homeassistant/components/enphase_envoy/strings.json +++ b/homeassistant/components/enphase_envoy/strings.json @@ -3,6 +3,7 @@ "flow_title": "{serial} ({host})", "step": { "user": { + "description": "For newer models, enter username `envoy` without a password. For older models, enter username `installer` without a password. For all other models, enter a valid username and password.", "data": { "host": "[%key:common::config_flow::data::host%]", "username": "[%key:common::config_flow::data::username%]", diff --git a/homeassistant/components/enphase_envoy/translations/en.json b/homeassistant/components/enphase_envoy/translations/en.json index 2cdb75a6b53..5d4617ed9fa 100644 --- a/homeassistant/components/enphase_envoy/translations/en.json +++ b/homeassistant/components/enphase_envoy/translations/en.json @@ -16,7 +16,8 @@ "host": "Host", "password": "Password", "username": "Username" - } + }, + "description": "For newer models, enter username `envoy` without a password. For older models, enter username `installer` without a password. For all other models, enter a valid username and password." } } } From 2f0695e4089cf2c451fa746c89bf53e33231c69f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Dec 2021 21:53:53 -1000 Subject: [PATCH 0071/2644] Fix missing unique id in enphase_envoy (#61083) --- .../components/enphase_envoy/__init__.py | 8 ++ .../components/enphase_envoy/config_flow.py | 53 ++++++++--- .../enphase_envoy/test_config_flow.py | 88 +++++++++++++++++++ 3 files changed, 136 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/enphase_envoy/__init__.py b/homeassistant/components/enphase_envoy/__init__.py index 69c488169a6..7b3765bd25c 100644 --- a/homeassistant/components/enphase_envoy/__init__.py +++ b/homeassistant/components/enphase_envoy/__init__.py @@ -75,6 +75,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: envoy_reader.get_inverters = False await coordinator.async_config_entry_first_refresh() + if not entry.unique_id: + try: + serial = await envoy_reader.get_full_serial_number() + except httpx.HTTPError: + pass + else: + hass.config_entries.async_update_entry(entry, unique_id=serial) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { COORDINATOR: coordinator, NAME: name, diff --git a/homeassistant/components/enphase_envoy/config_flow.py b/homeassistant/components/enphase_envoy/config_flow.py index 0b163e331d6..d1e0febe2e6 100644 --- a/homeassistant/components/enphase_envoy/config_flow.py +++ b/homeassistant/components/enphase_envoy/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Enphase Envoy integration.""" from __future__ import annotations +import contextlib import logging from typing import Any @@ -31,7 +32,7 @@ ENVOY = "Envoy" CONF_SERIAL = "serial" -async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> EnvoyReader: """Validate the user input allows us to connect.""" envoy_reader = EnvoyReader( data[CONF_HOST], @@ -48,6 +49,8 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, except (RuntimeError, httpx.HTTPError) as err: raise CannotConnect from err + return envoy_reader + class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Enphase Envoy.""" @@ -59,7 +62,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.ip_address = None self.name = None self.username = None - self.serial = None self._reauth_entry = None @callback @@ -104,8 +106,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle a flow initialized by zeroconf discovery.""" - self.serial = discovery_info.properties["serialnum"] - await self.async_set_unique_id(self.serial) + serial = discovery_info.properties["serialnum"] + await self.async_set_unique_id(serial) self.ip_address = discovery_info.host self._abort_if_unique_id_configured({CONF_HOST: self.ip_address}) for entry in self._async_current_entries(include_ignore=False): @@ -114,9 +116,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): and CONF_HOST in entry.data and entry.data[CONF_HOST] == self.ip_address ): - title = f"{ENVOY} {self.serial}" if entry.title == ENVOY else ENVOY + title = f"{ENVOY} {serial}" if entry.title == ENVOY else ENVOY self.hass.config_entries.async_update_entry( - entry, title=title, unique_id=self.serial + entry, title=title, unique_id=serial ) self.hass.async_create_task( self.hass.config_entries.async_reload(entry.entry_id) @@ -132,6 +134,24 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return await self.async_step_user() + def _async_envoy_name(self) -> str: + """Return the name of the envoy.""" + if self.name: + return self.name + if self.unique_id: + return f"{ENVOY} {self.unique_id}" + return ENVOY + + async def _async_set_unique_id_from_envoy(self, envoy_reader: EnvoyReader) -> bool: + """Set the unique id by fetching it from the envoy.""" + serial = None + with contextlib.suppress(httpx.HTTPError): + serial = await envoy_reader.get_full_serial_number() + if serial: + await self.async_set_unique_id(serial) + return True + return False + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -145,7 +165,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ): return self.async_abort(reason="already_configured") try: - await validate_input(self.hass, user_input) + envoy_reader = await validate_input(self.hass, user_input) except CannotConnect: errors["base"] = "cannot_connect" except InvalidAuth: @@ -155,21 +175,28 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "unknown" else: data = user_input.copy() - if self.serial: - data[CONF_NAME] = f"{ENVOY} {self.serial}" - else: - data[CONF_NAME] = self.name or ENVOY + data[CONF_NAME] = self._async_envoy_name() + if self._reauth_entry: self.hass.config_entries.async_update_entry( self._reauth_entry, data=data, ) return self.async_abort(reason="reauth_successful") + + if not self.unique_id and await self._async_set_unique_id_from_envoy( + envoy_reader + ): + data[CONF_NAME] = self._async_envoy_name() + + if self.unique_id: + self._abort_if_unique_id_configured({CONF_HOST: data[CONF_HOST]}) + return self.async_create_entry(title=data[CONF_NAME], data=data) - if self.serial: + if self.unique_id: self.context["title_placeholders"] = { - CONF_SERIAL: self.serial, + CONF_SERIAL: self.unique_id, CONF_HOST: self.ip_address, } return self.async_show_form( diff --git a/tests/components/enphase_envoy/test_config_flow.py b/tests/components/enphase_envoy/test_config_flow.py index fc9a7de188e..41a49a7b245 100644 --- a/tests/components/enphase_envoy/test_config_flow.py +++ b/tests/components/enphase_envoy/test_config_flow.py @@ -23,6 +23,91 @@ async def test_form(hass: HomeAssistant) -> None: with patch( "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.getData", return_value=True, + ), patch( + "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.get_full_serial_number", + return_value="1234", + ), patch( + "homeassistant.components.enphase_envoy.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "Envoy 1234" + assert result2["data"] == { + "host": "1.1.1.1", + "name": "Envoy 1234", + "username": "test-username", + "password": "test-password", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_no_serial_number(hass: HomeAssistant) -> None: + """Test user setup without a serial number.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.getData", + return_value=True, + ), patch( + "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.get_full_serial_number", + return_value=None, + ), patch( + "homeassistant.components.enphase_envoy.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "Envoy" + assert result2["data"] == { + "host": "1.1.1.1", + "name": "Envoy", + "username": "test-username", + "password": "test-password", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_fetching_serial_fails(hass: HomeAssistant) -> None: + """Test user setup without a serial number.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.getData", + return_value=True, + ), patch( + "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.get_full_serial_number", + side_effect=httpx.HTTPStatusError( + "any", request=MagicMock(), response=MagicMock() + ), ), patch( "homeassistant.components.enphase_envoy.async_setup_entry", return_value=True, @@ -125,6 +210,9 @@ async def test_import(hass: HomeAssistant) -> None: with patch( "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.getData", return_value=True, + ), patch( + "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.get_full_serial_number", + return_value="1234", ), patch( "homeassistant.components.enphase_envoy.async_setup_entry", return_value=True, From 0ae6969aa483beca563a078005fec5ea094f6d20 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 6 Dec 2021 00:55:52 -0700 Subject: [PATCH 0072/2644] Deprecate `system_id` parameter in SimpliSafe service calls (#61076) --- .../components/simplisafe/__init__.py | 138 ++++++++++++------ 1 file changed, 90 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 925f51cfe87..a3a80c0d107 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -145,57 +145,86 @@ SERVICES = ( SERVICE_NAME_SET_SYSTEM_PROPERTIES, ) - -SERVICE_REMOVE_PIN_SCHEMA = vol.Schema( - { - vol.Required(ATTR_DEVICE_ID): cv.string, - vol.Required(ATTR_PIN_LABEL_OR_VALUE): cv.string, - } +SERVICE_CLEAR_NOTIFICATIONS_SCHEMA = vol.All( + cv.deprecated(ATTR_SYSTEM_ID), + vol.Schema( + { + vol.Optional(ATTR_DEVICE_ID): cv.string, + vol.Optional(ATTR_SYSTEM_ID): cv.string, + } + ), + cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_SYSTEM_ID), ) -SERVICE_SET_PIN_SCHEMA = vol.Schema( - { - vol.Required(ATTR_DEVICE_ID): cv.string, - vol.Required(ATTR_PIN_LABEL): cv.string, - vol.Required(ATTR_PIN_VALUE): cv.string, - } +SERVICE_REMOVE_PIN_SCHEMA = vol.All( + cv.deprecated(ATTR_SYSTEM_ID), + vol.Schema( + { + vol.Optional(ATTR_DEVICE_ID): cv.string, + vol.Optional(ATTR_SYSTEM_ID): cv.string, + vol.Required(ATTR_PIN_LABEL_OR_VALUE): cv.string, + } + ), + cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_SYSTEM_ID), ) -SERVICE_SET_SYSTEM_PROPERTIES_SCHEMA = vol.Schema( - { - vol.Required(ATTR_DEVICE_ID): cv.string, - vol.Optional(ATTR_ALARM_DURATION): vol.All( - cv.time_period, - lambda value: value.total_seconds(), - vol.Range(min=MIN_ALARM_DURATION, max=MAX_ALARM_DURATION), - ), - vol.Optional(ATTR_ALARM_VOLUME): vol.All(vol.In(VOLUME_MAP), VOLUME_MAP.get), - vol.Optional(ATTR_CHIME_VOLUME): vol.All(vol.In(VOLUME_MAP), VOLUME_MAP.get), - vol.Optional(ATTR_ENTRY_DELAY_AWAY): vol.All( - cv.time_period, - lambda value: value.total_seconds(), - vol.Range(min=MIN_ENTRY_DELAY_AWAY, max=MAX_ENTRY_DELAY_AWAY), - ), - vol.Optional(ATTR_ENTRY_DELAY_HOME): vol.All( - cv.time_period, - lambda value: value.total_seconds(), - vol.Range(max=MAX_ENTRY_DELAY_HOME), - ), - vol.Optional(ATTR_EXIT_DELAY_AWAY): vol.All( - cv.time_period, - lambda value: value.total_seconds(), - vol.Range(min=MIN_EXIT_DELAY_AWAY, max=MAX_EXIT_DELAY_AWAY), - ), - vol.Optional(ATTR_EXIT_DELAY_HOME): vol.All( - cv.time_period, - lambda value: value.total_seconds(), - vol.Range(max=MAX_EXIT_DELAY_HOME), - ), - vol.Optional(ATTR_LIGHT): cv.boolean, - vol.Optional(ATTR_VOICE_PROMPT_VOLUME): vol.All( - vol.In(VOLUME_MAP), VOLUME_MAP.get - ), - } +SERVICE_SET_PIN_SCHEMA = vol.All( + cv.deprecated(ATTR_SYSTEM_ID), + vol.Schema( + { + vol.Optional(ATTR_DEVICE_ID): cv.string, + vol.Optional(ATTR_SYSTEM_ID): cv.string, + vol.Required(ATTR_PIN_LABEL): cv.string, + vol.Required(ATTR_PIN_VALUE): cv.string, + }, + ), + cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_SYSTEM_ID), +) + +SERVICE_SET_SYSTEM_PROPERTIES_SCHEMA = vol.All( + cv.deprecated(ATTR_SYSTEM_ID), + vol.Schema( + { + vol.Optional(ATTR_DEVICE_ID): cv.string, + vol.Optional(ATTR_SYSTEM_ID): cv.string, + vol.Optional(ATTR_ALARM_DURATION): vol.All( + cv.time_period, + lambda value: value.total_seconds(), + vol.Range(min=MIN_ALARM_DURATION, max=MAX_ALARM_DURATION), + ), + vol.Optional(ATTR_ALARM_VOLUME): vol.All( + vol.In(VOLUME_MAP), VOLUME_MAP.get + ), + vol.Optional(ATTR_CHIME_VOLUME): vol.All( + vol.In(VOLUME_MAP), VOLUME_MAP.get + ), + vol.Optional(ATTR_ENTRY_DELAY_AWAY): vol.All( + cv.time_period, + lambda value: value.total_seconds(), + vol.Range(min=MIN_ENTRY_DELAY_AWAY, max=MAX_ENTRY_DELAY_AWAY), + ), + vol.Optional(ATTR_ENTRY_DELAY_HOME): vol.All( + cv.time_period, + lambda value: value.total_seconds(), + vol.Range(max=MAX_ENTRY_DELAY_HOME), + ), + vol.Optional(ATTR_EXIT_DELAY_AWAY): vol.All( + cv.time_period, + lambda value: value.total_seconds(), + vol.Range(min=MIN_EXIT_DELAY_AWAY, max=MAX_EXIT_DELAY_AWAY), + ), + vol.Optional(ATTR_EXIT_DELAY_HOME): vol.All( + cv.time_period, + lambda value: value.total_seconds(), + vol.Range(max=MAX_EXIT_DELAY_HOME), + ), + vol.Optional(ATTR_LIGHT): cv.boolean, + vol.Optional(ATTR_VOICE_PROMPT_VOLUME): vol.All( + vol.In(VOLUME_MAP), VOLUME_MAP.get + ), + } + ), + cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_SYSTEM_ID), ) WEBSOCKET_EVENTS_REQUIRING_SERIAL = [EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED] @@ -217,6 +246,15 @@ def _async_get_system_for_service_call( hass: HomeAssistant, call: ServiceCall ) -> SystemType: """Get the SimpliSafe system related to a service call (by device ID).""" + if ATTR_SYSTEM_ID in call.data: + for entry in hass.config_entries.async_entries(DOMAIN): + simplisafe = hass.data[DOMAIN][entry.entry_id] + if ( + system := simplisafe.systems.get(int(call.data[ATTR_SYSTEM_ID])) + ) is None: + continue + return cast(SystemType, system) + device_id = call.data[ATTR_DEVICE_ID] device_registry = dr.async_get(hass) @@ -366,7 +404,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) for service, method, schema in ( - (SERVICE_NAME_CLEAR_NOTIFICATIONS, async_clear_notifications, None), + ( + SERVICE_NAME_CLEAR_NOTIFICATIONS, + async_clear_notifications, + SERVICE_CLEAR_NOTIFICATIONS_SCHEMA, + ), (SERVICE_NAME_REMOVE_PIN, async_remove_pin, SERVICE_REMOVE_PIN_SCHEMA), (SERVICE_NAME_SET_PIN, async_set_pin, SERVICE_SET_PIN_SCHEMA), ( From 1bcff0907b347e0c82e61b297e41ed84708cabc2 Mon Sep 17 00:00:00 2001 From: schreyack Date: Sun, 5 Dec 2021 23:56:59 -0800 Subject: [PATCH 0073/2644] Fix previous setting briefly appearing on newer flux_led devices when turning on (#60004) Co-authored-by: J. Nick Koston --- homeassistant/components/flux_led/light.py | 9 +- tests/components/flux_led/__init__.py | 2 + tests/components/flux_led/test_light.py | 127 +++++++++++++++++++-- 3 files changed, 126 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index fafa6a0b22e..d364d8b9581 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -280,10 +280,11 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity): async def _async_turn_on(self, **kwargs: Any) -> None: """Turn the specified or all lights on.""" - if not self.is_on: - await self._device.async_turn_on() - if not kwargs: - return + if self._device.requires_turn_on or not kwargs: + if not self.is_on: + await self._device.async_turn_on() + if not kwargs: + return if MODE_ATTRS.intersection(kwargs): await self._async_set_mode(**kwargs) diff --git a/tests/components/flux_led/__init__.py b/tests/components/flux_led/__init__.py index 5b39c5656f6..764c33686b7 100644 --- a/tests/components/flux_led/__init__.py +++ b/tests/components/flux_led/__init__.py @@ -66,6 +66,7 @@ def _mocked_bulb() -> AIOWifiLedBulb: bulb.data_receive_callback = callback bulb.device_type = DeviceType.Bulb + bulb.requires_turn_on = True bulb.async_setup = AsyncMock(side_effect=_save_setup_callback) bulb.effect_list = ["some_effect"] bulb.async_set_custom_pattern = AsyncMock() @@ -115,6 +116,7 @@ def _mocked_switch() -> AIOWifiLedBulb: switch.data_receive_callback = callback switch.device_type = DeviceType.Switch + switch.requires_turn_on = True switch.async_setup = AsyncMock(side_effect=_save_setup_callback) switch.async_stop = AsyncMock() switch.async_update = AsyncMock() diff --git a/tests/components/flux_led/test_light.py b/tests/components/flux_led/test_light.py index 4f401197173..6f08ae8a307 100644 --- a/tests/components/flux_led/test_light.py +++ b/tests/components/flux_led/test_light.py @@ -226,22 +226,19 @@ async def test_rgb_light(hass: HomeAssistant) -> None: bulb.async_set_levels.reset_mock() bulb.async_turn_on.reset_mock() - await hass.services.async_call( - LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True - ) - bulb.async_turn_on.assert_called_once() - bulb.async_turn_on.reset_mock() - await async_mock_device_turn_on(hass, bulb) - assert hass.states.get(entity_id).state == STATE_ON - await hass.services.async_call( LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, blocking=True, ) + # If its off and the device requires the turn on + # command before setting brightness we need to make sure its called + bulb.async_turn_on.assert_called_once() bulb.async_set_brightness.assert_called_with(100) bulb.async_set_brightness.reset_mock() + await async_mock_device_turn_on(hass, bulb) + assert hass.states.get(entity_id).state == STATE_ON await hass.services.async_call( LIGHT_DOMAIN, @@ -284,6 +281,120 @@ async def test_rgb_light(hass: HomeAssistant) -> None: bulb.async_set_effect.reset_mock() +async def test_rgb_light_auto_on(hass: HomeAssistant) -> None: + """Test an rgb light that does not need the turn on command sent.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + bulb.requires_turn_on = False + bulb.raw_state = bulb.raw_state._replace(model_num=0x33) # RGB only model + bulb.color_modes = {FLUX_COLOR_MODE_RGB} + bulb.color_mode = FLUX_COLOR_MODE_RGB + with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.bulb_rgbcw_ddeeff" + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 128 + assert attributes[ATTR_COLOR_MODE] == "rgb" + assert attributes[ATTR_EFFECT_LIST] == bulb.effect_list + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgb"] + assert attributes[ATTR_HS_COLOR] == (0, 100) + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.async_turn_off.assert_called_once() + + await async_mock_device_turn_off(hass, bulb) + assert hass.states.get(entity_id).state == STATE_OFF + + bulb.brightness = 0 + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: (10, 10, 30)}, + blocking=True, + ) + # If the bulb is off and we are using existing brightness + # it has to be at least 1 or the bulb won't turn on + bulb.async_turn_on.assert_not_called() + bulb.async_set_levels.assert_called_with(10, 10, 30, brightness=1) + bulb.async_set_levels.reset_mock() + bulb.async_turn_on.reset_mock() + + # Should still be called with no kwargs + await hass.services.async_call( + LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.async_turn_on.assert_called_once() + await async_mock_device_turn_on(hass, bulb) + assert hass.states.get(entity_id).state == STATE_ON + bulb.async_turn_on.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, + blocking=True, + ) + bulb.async_turn_on.assert_not_called() + bulb.async_set_brightness.assert_called_with(100) + bulb.async_set_brightness.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: (10, 10, 30)}, + blocking=True, + ) + # If the bulb is on and we are using existing brightness + # and brightness was 0 it means we could not read it because + # an effect is in progress so we use 255 + bulb.async_turn_on.assert_not_called() + bulb.async_set_levels.assert_called_with(10, 10, 30, brightness=255) + bulb.async_set_levels.reset_mock() + + bulb.brightness = 128 + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)}, + blocking=True, + ) + bulb.async_turn_on.assert_not_called() + bulb.async_set_levels.assert_called_with(255, 191, 178, brightness=128) + bulb.async_set_levels.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "random"}, + blocking=True, + ) + bulb.async_turn_on.assert_not_called() + bulb.async_set_effect.assert_called_once() + bulb.async_set_effect.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "purple_fade"}, + blocking=True, + ) + bulb.async_turn_on.assert_not_called() + bulb.async_set_effect.assert_called_with("purple_fade", 50, 50) + bulb.async_set_effect.reset_mock() + + async def test_rgb_cct_light(hass: HomeAssistant) -> None: """Test an rgb cct light.""" config_entry = MockConfigEntry( From bbe4a67a98cb1710f1e439e998fac3672c71bd1d Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 5 Dec 2021 23:59:24 -0800 Subject: [PATCH 0074/2644] Coalesce nest media source preview clips by session and bump google-nest-sdm (#61081) --- homeassistant/components/nest/__init__.py | 2 +- homeassistant/components/nest/manifest.json | 2 +- homeassistant/components/nest/media_source.py | 4 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nest/test_events.py | 12 +-- tests/components/nest/test_media_source.py | 74 +++++++++++-------- 7 files changed, 56 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 382edb80ca5..eebbdcf026a 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -198,7 +198,7 @@ class SignalUpdateCallback: "device_id": device_entry.id, "type": event_type, "timestamp": event_message.timestamp, - "nest_event_id": image_event.event_id, + "nest_event_id": image_event.event_session_id, } self._hass.bus.async_fire(NEST_EVENT, message) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index a82f8395733..b9f20e92670 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["ffmpeg", "http", "media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.3"], + "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.4"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/homeassistant/components/nest/media_source.py b/homeassistant/components/nest/media_source.py index 4f6cd8147d9..8fd7d384e36 100644 --- a/homeassistant/components/nest/media_source.py +++ b/homeassistant/components/nest/media_source.py @@ -181,7 +181,7 @@ class NestMediaSource(MediaSource): browse_device.children = [] events = await _get_events(device) for child_event in events.values(): - event_id = MediaId(media_id.device_id, child_event.event_id) + event_id = MediaId(media_id.device_id, child_event.event_session_id) browse_device.children.append( _browse_event(event_id, device, child_event) ) @@ -203,7 +203,7 @@ class NestMediaSource(MediaSource): async def _get_events(device: Device) -> Mapping[str, ImageEventBase]: """Return relevant events for the specified device.""" events = await device.event_media_manager.async_events() - return {e.event_id: e for e in events} + return {e.event_session_id: e for e in events} def _browse_root() -> BrowseMediaSource: diff --git a/requirements_all.txt b/requirements_all.txt index 501ab2f4819..89896a6dbc8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -741,7 +741,7 @@ google-cloud-pubsub==2.1.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==0.4.3 +google-nest-sdm==0.4.4 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a0ba7da5d24..3da27e4926b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -464,7 +464,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.1.0 # homeassistant.components.nest -google-nest-sdm==0.4.3 +google-nest-sdm==0.4.4 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/tests/components/nest/test_events.py b/tests/components/nest/test_events.py index 6e9dd7dd40d..a2f5c21fdac 100644 --- a/tests/components/nest/test_events.py +++ b/tests/components/nest/test_events.py @@ -117,7 +117,7 @@ async def test_doorbell_chime_event(hass): "device_id": entry.device_id, "type": "doorbell_chime", "timestamp": event_time, - "nest_event_id": EVENT_ID, + "nest_event_id": EVENT_SESSION_ID, } @@ -145,7 +145,7 @@ async def test_camera_motion_event(hass): "device_id": entry.device_id, "type": "camera_motion", "timestamp": event_time, - "nest_event_id": EVENT_ID, + "nest_event_id": EVENT_SESSION_ID, } @@ -173,7 +173,7 @@ async def test_camera_sound_event(hass): "device_id": entry.device_id, "type": "camera_sound", "timestamp": event_time, - "nest_event_id": EVENT_ID, + "nest_event_id": EVENT_SESSION_ID, } @@ -201,7 +201,7 @@ async def test_camera_person_event(hass): "device_id": entry.device_id, "type": "camera_person", "timestamp": event_time, - "nest_event_id": EVENT_ID, + "nest_event_id": EVENT_SESSION_ID, } @@ -238,13 +238,13 @@ async def test_camera_multiple_event(hass): "device_id": entry.device_id, "type": "camera_motion", "timestamp": event_time, - "nest_event_id": EVENT_ID, + "nest_event_id": EVENT_SESSION_ID, } assert events[1].data == { "device_id": entry.device_id, "type": "camera_person", "timestamp": event_time, - "nest_event_id": EVENT_ID, + "nest_event_id": EVENT_SESSION_ID, } diff --git a/tests/components/nest/test_media_source.py b/tests/components/nest/test_media_source.py index 82c87579525..22ed0721eb2 100644 --- a/tests/components/nest/test_media_source.py +++ b/tests/components/nest/test_media_source.py @@ -27,6 +27,7 @@ DEVICE_ID = "example/api/device/id" DEVICE_NAME = "Front" PLATFORM = "camera" NEST_EVENT = "nest_event" +EVENT_ID = "1aXEvi9ajKVTdDsXdJda8fzfCa..." EVENT_SESSION_ID = "CjY5Y3VKaTZwR3o4Y19YbTVfMF..." CAMERA_DEVICE_TYPE = "sdm.devices.types.CAMERA" CAMERA_TRAITS = { @@ -81,26 +82,28 @@ async def async_setup_devices(hass, auth, device_type, traits={}, events=[]): return subscriber -def create_event(event_id, event_type, timestamp=None, device_id=None): +def create_event( + event_session_id, event_id, event_type, timestamp=None, device_id=None +): """Create an EventMessage for a single event type.""" if not timestamp: timestamp = dt_util.now() event_data = { event_type: { - "eventSessionId": EVENT_SESSION_ID, + "eventSessionId": event_session_id, "eventId": event_id, }, } - return create_event_message(event_id, event_data, timestamp, device_id=device_id) + return create_event_message(event_data, timestamp, device_id=device_id) -def create_event_message(event_id, event_data, timestamp, device_id=None): +def create_event_message(event_data, timestamp, device_id=None): """Create an EventMessage for a single event type.""" if device_id is None: device_id = DEVICE_ID return EventMessage( { - "eventId": f"{event_id}-{timestamp}", + "eventId": f"{EVENT_ID}-{timestamp}", "timestamp": timestamp.isoformat(timespec="seconds"), "resourceUpdate": { "name": device_id, @@ -163,7 +166,6 @@ async def test_supported_device(hass, auth): async def test_camera_event(hass, auth, hass_client): """Test a media source and image created for an event.""" - event_id = "FWWVQVUdGNUlTU2V4MGV2aTNXV..." event_timestamp = dt_util.now() await async_setup_devices( hass, @@ -172,7 +174,8 @@ async def test_camera_event(hass, auth, hass_client): CAMERA_TRAITS, events=[ create_event( - event_id, + EVENT_SESSION_ID, + EVENT_ID, PERSON_EVENT, timestamp=event_timestamp, ), @@ -213,7 +216,7 @@ async def test_camera_event(hass, auth, hass_client): # The device expands recent events assert len(browse.children) == 1 assert browse.children[0].domain == DOMAIN - assert browse.children[0].identifier == f"{device.id}/{event_id}" + assert browse.children[0].identifier == f"{device.id}/{EVENT_SESSION_ID}" event_timestamp_string = event_timestamp.strftime(DATE_STR_FORMAT) assert browse.children[0].title == f"Person @ {event_timestamp_string}" assert not browse.children[0].can_expand @@ -221,19 +224,19 @@ async def test_camera_event(hass, auth, hass_client): # Browse to the event browse = await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_id}" + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{EVENT_SESSION_ID}" ) assert browse.domain == DOMAIN - assert browse.identifier == f"{device.id}/{event_id}" + assert browse.identifier == f"{device.id}/{EVENT_SESSION_ID}" assert "Person" in browse.title assert not browse.can_expand assert not browse.children # Resolving the event links to the media media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_id}" + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{EVENT_SESSION_ID}" ) - assert media.url == f"/api/nest/event_media/{device.id}/{event_id}" + assert media.url == f"/api/nest/event_media/{device.id}/{EVENT_SESSION_ID}" assert media.mime_type == "image/jpeg" auth.responses = [ @@ -250,9 +253,9 @@ async def test_camera_event(hass, auth, hass_client): async def test_event_order(hass, auth): """Test multiple events are in descending timestamp order.""" - event_id1 = "FWWVQVUdGNUlTU2V4MGV2aTNXV..." + event_session_id1 = "FWWVQVUdGNUlTU2V4MGV2aTNXV..." event_timestamp1 = dt_util.now() - event_id2 = "GXXWRWVeHNUlUU3V3MGV3bUOYW..." + event_session_id2 = "GXXWRWVeHNUlUU3V3MGV3bUOYW..." event_timestamp2 = event_timestamp1 + datetime.timedelta(seconds=5) await async_setup_devices( hass, @@ -261,12 +264,14 @@ async def test_event_order(hass, auth): CAMERA_TRAITS, events=[ create_event( - event_id1, + event_session_id1, + EVENT_ID + "1", PERSON_EVENT, timestamp=event_timestamp1, ), create_event( - event_id2, + event_session_id2, + EVENT_ID + "2", MOTION_EVENT, timestamp=event_timestamp2, ), @@ -293,7 +298,7 @@ async def test_event_order(hass, auth): # Motion event is most recent assert len(browse.children) == 2 assert browse.children[0].domain == DOMAIN - assert browse.children[0].identifier == f"{device.id}/{event_id2}" + assert browse.children[0].identifier == f"{device.id}/{event_session_id2}" event_timestamp_string = event_timestamp2.strftime(DATE_STR_FORMAT) assert browse.children[0].title == f"Motion @ {event_timestamp_string}" assert not browse.children[0].can_expand @@ -301,7 +306,7 @@ async def test_event_order(hass, auth): # Person event is next assert browse.children[1].domain == DOMAIN - assert browse.children[1].identifier == f"{device.id}/{event_id1}" + assert browse.children[1].identifier == f"{device.id}/{event_session_id1}" event_timestamp_string = event_timestamp1.strftime(DATE_STR_FORMAT) assert browse.children[1].title == f"Person @ {event_timestamp_string}" assert not browse.children[1].can_expand @@ -395,9 +400,12 @@ async def test_resolve_invalid_event_id(hass, auth): async def test_camera_event_clip_preview(hass, auth, hass_client): """Test an event for a battery camera video clip.""" - event_id = "FWWVQVUdGNUlTU2V4MGV2aTNXV..." event_timestamp = dt_util.now() event_data = { + "sdm.devices.events.CameraMotion.Motion": { + "eventSessionId": EVENT_SESSION_ID, + "eventId": "n:2", + }, "sdm.devices.events.CameraClipPreview.ClipPreview": { "eventSessionId": EVENT_SESSION_ID, "previewUrl": "https://127.0.0.1/example", @@ -410,7 +418,6 @@ async def test_camera_event_clip_preview(hass, auth, hass_client): BATTERY_CAMERA_TRAITS, events=[ create_event_message( - event_id, event_data, timestamp=event_timestamp, ), @@ -439,7 +446,7 @@ async def test_camera_event_clip_preview(hass, auth, hass_client): assert browse.children[0].domain == DOMAIN actual_event_id = browse.children[0].identifier event_timestamp_string = event_timestamp.strftime(DATE_STR_FORMAT) - assert browse.children[0].title == f"Event @ {event_timestamp_string}" + assert browse.children[0].title == f"Motion @ {event_timestamp_string}" assert not browse.children[0].can_expand assert len(browse.children[0].children) == 0 @@ -490,7 +497,6 @@ async def test_event_media_render_invalid_event_id(hass, auth, hass_client): async def test_event_media_failure(hass, auth, hass_client): """Test event media fetch sees a failure from the server.""" - event_id = "FWWVQVUdGNUlTU2V4MGV2aTNXV..." event_timestamp = dt_util.now() await async_setup_devices( hass, @@ -499,7 +505,8 @@ async def test_event_media_failure(hass, auth, hass_client): CAMERA_TRAITS, events=[ create_event( - event_id, + EVENT_SESSION_ID, + EVENT_ID, PERSON_EVENT, timestamp=event_timestamp, ), @@ -517,9 +524,9 @@ async def test_event_media_failure(hass, auth, hass_client): # Resolving the event links to the media media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_id}" + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{EVENT_SESSION_ID}" ) - assert media.url == f"/api/nest/event_media/{device.id}/{event_id}" + assert media.url == f"/api/nest/event_media/{device.id}/{EVENT_SESSION_ID}" assert media.mime_type == "image/jpeg" auth.responses = [ @@ -535,7 +542,6 @@ async def test_event_media_failure(hass, auth, hass_client): async def test_media_permission_unauthorized(hass, auth, hass_client, hass_admin_user): """Test case where user does not have permissions to view media.""" - event_id = "FWWVQVUdGNUlTU2V4MGV2aTNXV..." event_timestamp = dt_util.now() await async_setup_devices( hass, @@ -544,7 +550,8 @@ async def test_media_permission_unauthorized(hass, auth, hass_client, hass_admin CAMERA_TRAITS, events=[ create_event( - event_id, + EVENT_SESSION_ID, + EVENT_ID, PERSON_EVENT, timestamp=event_timestamp, ), @@ -560,7 +567,7 @@ async def test_media_permission_unauthorized(hass, auth, hass_client, hass_admin assert device assert device.name == DEVICE_NAME - media_url = f"/api/nest/event_media/{device.id}/{event_id}" + media_url = f"/api/nest/event_media/{device.id}/{EVENT_SESSION_ID}" # Empty policy with no access to the entity hass_admin_user.mock_policy({}) @@ -616,7 +623,12 @@ async def test_multiple_devices(hass, auth, hass_client): # Send events for device #1 for i in range(0, 5): await subscriber.async_receive_event( - create_event(f"event-id-{i}", PERSON_EVENT, device_id=device_id1) + create_event( + f"event-session-id-{i}", + f"event-id-{i}", + PERSON_EVENT, + device_id=device_id1, + ) ) browse = await media_source.async_browse_media( @@ -631,7 +643,9 @@ async def test_multiple_devices(hass, auth, hass_client): # Send events for device #2 for i in range(0, 3): await subscriber.async_receive_event( - create_event(f"other-id-{i}", PERSON_EVENT, device_id=device_id2) + create_event( + f"other-id-{i}", f"event-id{i}", PERSON_EVENT, device_id=device_id2 + ) ) browse = await media_source.async_browse_media( From 1dfadd72cfeb1f1bc136eea50cf9e971471c943b Mon Sep 17 00:00:00 2001 From: Alexander Pitkin Date: Mon, 6 Dec 2021 11:49:31 +0300 Subject: [PATCH 0075/2644] Fix yandex transport for Belarus (#61080) --- homeassistant/components/yandex_transport/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yandex_transport/manifest.json b/homeassistant/components/yandex_transport/manifest.json index 680336fe47b..22872259a6f 100644 --- a/homeassistant/components/yandex_transport/manifest.json +++ b/homeassistant/components/yandex_transport/manifest.json @@ -2,7 +2,7 @@ "domain": "yandex_transport", "name": "Yandex Transport", "documentation": "https://www.home-assistant.io/integrations/yandex_transport", - "requirements": ["aioymaps==1.2.1"], + "requirements": ["aioymaps==1.2.2"], "codeowners": ["@rishatik92", "@devbis"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 89896a6dbc8..10f5afa24a0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -270,7 +270,7 @@ aiovlc==0.1.0 aiowatttime==0.1.1 # homeassistant.components.yandex_transport -aioymaps==1.2.1 +aioymaps==1.2.2 # homeassistant.components.airly airly==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3da27e4926b..af322c62c86 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -200,7 +200,7 @@ aiovlc==0.1.0 aiowatttime==0.1.1 # homeassistant.components.yandex_transport -aioymaps==1.2.1 +aioymaps==1.2.2 # homeassistant.components.airly airly==1.1.0 From 3a56cfed3a1f3b339c9b8f14702881ef3fba6e18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Mon, 6 Dec 2021 13:04:18 +0100 Subject: [PATCH 0076/2644] Update Apple TV integration to support tvOS 15 (#58665) --- homeassistant/components/apple_tv/__init__.py | 82 ++- .../components/apple_tv/config_flow.py | 353 ++++++++----- homeassistant/components/apple_tv/const.py | 5 +- .../components/apple_tv/manifest.json | 8 +- .../components/apple_tv/media_player.py | 75 ++- .../components/apple_tv/strings.json | 32 +- .../components/apple_tv/translations/en.json | 130 ++--- homeassistant/generated/zeroconf.py | 4 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/apple_tv/common.py | 25 +- tests/components/apple_tv/conftest.py | 94 +++- tests/components/apple_tv/test_config_flow.py | 466 ++++++++++++++---- 13 files changed, 919 insertions(+), 359 deletions(-) diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index b710a753da9..29bc5634b38 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -4,11 +4,11 @@ import logging from random import randrange from pyatv import connect, exceptions, scan -from pyatv.const import Protocol +from pyatv.const import DeviceModel, Protocol +from pyatv.convert import model_str from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN -from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import ( ATTR_CONNECTIONS, ATTR_IDENTIFIERS, @@ -19,7 +19,6 @@ from homeassistant.const import ( ATTR_SW_VERSION, CONF_ADDRESS, CONF_NAME, - CONF_PROTOCOL, EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import callback @@ -31,7 +30,7 @@ from homeassistant.helpers.dispatcher import ( ) from homeassistant.helpers.entity import DeviceInfo, Entity -from .const import CONF_CREDENTIALS, CONF_IDENTIFIER, CONF_START_OFF, DOMAIN +from .const import CONF_CREDENTIALS, CONF_IDENTIFIERS, CONF_START_OFF, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -39,9 +38,6 @@ DEFAULT_NAME = "Apple TV" BACKOFF_TIME_UPPER_LIMIT = 300 # Five minutes -NOTIFICATION_TITLE = "Apple TV Notification" -NOTIFICATION_ID = "apple_tv_notification" - SIGNAL_CONNECTED = "apple_tv_connected" SIGNAL_DISCONNECTED = "apple_tv_disconnected" @@ -229,7 +225,12 @@ class AppleTVManager: if conf: await self._connect(conf) except exceptions.AuthenticationError: - self._auth_problem() + self.config_entry.async_start_reauth(self.hass) + asyncio.create_task(self.disconnect()) + _LOGGER.exception( + "Authentication failed for %s, try reconfiguring device", + self.config_entry.data[CONF_NAME], + ) break except asyncio.CancelledError: pass @@ -249,56 +250,37 @@ class AppleTVManager: _LOGGER.debug("Connect loop ended") self._task = None - def _auth_problem(self): - """Problem to authenticate occurred that needs intervention.""" - _LOGGER.debug("Authentication error, reconfigure integration") - - name = self.config_entry.data[CONF_NAME] - identifier = self.config_entry.unique_id - - self.hass.components.persistent_notification.create( - "An irrecoverable connection problem occurred when connecting to " - f"`{name}`. Please go to the Integrations page and reconfigure it", - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID, - ) - - # Add to event queue as this function is called from a task being - # cancelled from disconnect - asyncio.create_task(self.disconnect()) - - self.hass.async_create_task( - self.hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data={CONF_NAME: name, CONF_IDENTIFIER: identifier}, - ) - ) - async def _scan(self): """Try to find device by scanning for it.""" - identifier = self.config_entry.unique_id + identifiers = set( + self.config_entry.data.get(CONF_IDENTIFIERS, [self.config_entry.unique_id]) + ) address = self.config_entry.data[CONF_ADDRESS] - protocol = Protocol(self.config_entry.data[CONF_PROTOCOL]) - _LOGGER.debug("Discovering device %s", identifier) + # Only scan for and set up protocols that was successfully paired + protocols = { + Protocol(int(protocol)) + for protocol in self.config_entry.data[CONF_CREDENTIALS] + } + + _LOGGER.debug("Discovering device %s", self.config_entry.title) atvs = await scan( - self.hass.loop, identifier=identifier, protocol=protocol, hosts=[address] + self.hass.loop, identifier=identifiers, protocol=protocols, hosts=[address] ) if atvs: return atvs[0] _LOGGER.debug( "Failed to find device %s with address %s, trying to scan", - identifier, + self.config_entry.title, address, ) - atvs = await scan(self.hass.loop, identifier=identifier, protocol=protocol) + atvs = await scan(self.hass.loop, identifier=identifiers, protocol=protocols) if atvs: return atvs[0] - _LOGGER.debug("Failed to find device %s, trying later", identifier) + _LOGGER.debug("Failed to find device %s, trying later", self.config_entry.title) return None @@ -307,8 +289,16 @@ class AppleTVManager: credentials = self.config_entry.data[CONF_CREDENTIALS] session = async_get_clientsession(self.hass) - for protocol, creds in credentials.items(): - conf.set_credentials(Protocol(int(protocol)), creds) + for protocol_int, creds in credentials.items(): + protocol = Protocol(int(protocol_int)) + if conf.get_service(protocol) is not None: + conf.set_credentials(protocol, creds) + else: + _LOGGER.warning( + "Protocol %s not found for %s, functionality will be reduced", + protocol.name, + self.config_entry.data[CONF_NAME], + ) _LOGGER.debug("Connecting to device %s", self.config_entry.data[CONF_NAME]) self.atv = await connect(conf, self.hass.loop, session=session) @@ -322,7 +312,7 @@ class AppleTVManager: self._connection_attempts = 0 if self._connection_was_lost: _LOGGER.info( - 'Connection was re-established to Apple TV "%s"', + 'Connection was re-established to device "%s"', self.config_entry.data[CONF_NAME], ) self._connection_was_lost = False @@ -345,7 +335,9 @@ class AppleTVManager: dev_info = self.atv.device_info attrs[ATTR_MODEL] = ( - DEFAULT_NAME + " " + dev_info.model.name.replace("Gen", "") + dev_info.raw_model + if dev_info.model == DeviceModel.Unknown and dev_info.raw_model + else model_str(dev_info.model) ) attrs[ATTR_SW_VERSION] = dev_info.version diff --git a/homeassistant/components/apple_tv/config_flow.py b/homeassistant/components/apple_tv/config_flow.py index 11c41740c69..16a757b2ebb 100644 --- a/homeassistant/components/apple_tv/config_flow.py +++ b/homeassistant/components/apple_tv/config_flow.py @@ -1,22 +1,23 @@ """Config flow for Apple TV integration.""" +from collections import deque from ipaddress import ip_address import logging from random import randrange from pyatv import exceptions, pair, scan -from pyatv.const import Protocol -from pyatv.convert import protocol_str +from pyatv.const import DeviceModel, PairingRequirement, Protocol +from pyatv.convert import model_str, protocol_str +from pyatv.helpers import get_unique_id import voluptuous as vol -from homeassistant import config_entries +from homeassistant import config_entries, data_entry_flow from homeassistant.components import zeroconf -from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PIN, CONF_PROTOCOL +from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PIN from homeassistant.core import callback -from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import CONF_CREDENTIALS, CONF_IDENTIFIER, CONF_START_OFF, DOMAIN +from .const import CONF_CREDENTIALS, CONF_IDENTIFIERS, CONF_START_OFF, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -25,10 +26,9 @@ DEVICE_INPUT = "device_input" INPUT_PIN_SCHEMA = vol.Schema({vol.Required(CONF_PIN, default=None): int}) DEFAULT_START_OFF = False -PROTOCOL_PRIORITY = [Protocol.MRP, Protocol.DMAP, Protocol.AirPlay] -async def device_scan(identifier, loop, cache=None): +async def device_scan(identifier, loop): """Scan for a specific device using identifier as filter.""" def _filter_device(dev): @@ -46,27 +46,14 @@ async def device_scan(identifier, loop, cache=None): except ValueError: return None - if cache: - matches = [atv for atv in cache if _filter_device(atv)] - if matches: - return cache, matches[0] - for hosts in (_host_filter(), None): scan_result = await scan(loop, timeout=3, hosts=hosts) matches = [atv for atv in scan_result if _filter_device(atv)] if matches: - return scan_result, matches[0] + return matches[0], matches[0].all_identifiers - return scan_result, None - - -def is_valid_credentials(credentials): - """Verify that credentials are valid for establishing a connection.""" - return ( - credentials.get(Protocol.MRP.value) is not None - or credentials.get(Protocol.DMAP.value) is not None - ) + return None, None class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -82,19 +69,43 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize a new AppleTVConfigFlow.""" - self.target_device = None - self.scan_result = None + self.scan_filter = None self.atv = None + self.atv_identifiers = None self.protocol = None self.pairing = None self.credentials = {} # Protocol -> credentials + self.protocols_to_pair = deque() - async def async_step_reauth(self, info): + @property + def device_identifier(self): + """Return a identifier for the config entry. + + A device has multiple unique identifiers, but Home Assistant only supports one + per config entry. Normally, a "main identifier" is determined by pyatv by + first collecting all identifiers and then picking one in a pre-determine order. + Under normal circumstances, this works fine but if a service is missing or + removed due to deprecation (which happened with MRP), then another identifier + will be calculated instead. To fix this, all identifiers belonging to a device + is stored with the config entry and one of them (could be random) is used as + unique_id for said entry. When a new (zeroconf) service or device is + discovered, the identifier is first used to look up if it belongs to an + existing config entry. If that's the case, the unique_id from that entry is + re-used, otherwise the newly discovered identifier is used instead. + """ + for entry in self._async_current_entries(): + for identifier in self.atv.all_identifiers: + if identifier in entry.data.get(CONF_IDENTIFIERS, [entry.unique_id]): + return entry.unique_id + return self.atv.identifier + + async def async_step_reauth(self, user_input=None): """Handle initial step when updating invalid credentials.""" - await self.async_set_unique_id(info[CONF_IDENTIFIER]) - self.target_device = info[CONF_IDENTIFIER] - - self.context["title_placeholders"] = {"name": info[CONF_NAME]} + self.context["title_placeholders"] = { + "name": user_input[CONF_NAME], + "type": "Apple TV", + } + self.scan_filter = self.unique_id self.context["identifier"] = self.unique_id return await self.async_step_reconfigure() @@ -102,70 +113,97 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Inform user that reconfiguration is about to start.""" if user_input is not None: return await self.async_find_device_wrapper( - self.async_begin_pairing, allow_exist=True + self.async_pair_next_protocol, allow_exist=True ) return self.async_show_form(step_id="reconfigure") async def async_step_user(self, user_input=None): """Handle the initial step.""" - # Be helpful to the user and look for devices - if self.scan_result is None: - self.scan_result, _ = await device_scan(None, self.hass.loop) - errors = {} - default_suggestion = self._prefill_identifier() if user_input is not None: - self.target_device = user_input[DEVICE_INPUT] + self.scan_filter = user_input[DEVICE_INPUT] try: await self.async_find_device() except DeviceNotFound: errors["base"] = "no_devices_found" except DeviceAlreadyConfigured: errors["base"] = "already_configured" - except exceptions.NoServiceError: - errors["base"] = "no_usable_service" except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: await self.async_set_unique_id( - self.atv.identifier, raise_on_progress=False + self.device_identifier, raise_on_progress=False ) + self.context["all_identifiers"] = self.atv.all_identifiers return await self.async_step_confirm() return self.async_show_form( step_id="user", - data_schema=vol.Schema( - {vol.Required(DEVICE_INPUT, default=default_suggestion): str} - ), + data_schema=vol.Schema({vol.Required(DEVICE_INPUT): str}), errors=errors, - description_placeholders={"devices": self._devices_str()}, ) async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> data_entry_flow.FlowResult: """Handle device found via zeroconf.""" - service_type = discovery_info.type + service_type = discovery_info.type[:-1] # Remove leading . + name = discovery_info.name.replace(f".{service_type}.", "") properties = discovery_info.properties - if service_type == "_mediaremotetv._tcp.local.": - identifier = properties["UniqueIdentifier"] - name = properties["Name"] - elif service_type == "_touch-able._tcp.local.": - identifier = discovery_info.name.split(".")[0] - name = properties["CtlN"] - else: + # Extract unique identifier from service + self.scan_filter = get_unique_id(service_type, name, properties) + if self.scan_filter is None: return self.async_abort(reason="unknown") - await self.async_set_unique_id(identifier) + # Scan for the device in order to extract _all_ unique identifiers assigned to + # it. Not doing it like this will yield multiple config flows for the same + # device, one per protocol, which is undesired. + return await self.async_find_device_wrapper(self.async_found_zeroconf_device) + + async def async_found_zeroconf_device(self, user_input=None): + """Handle device found after Zeroconf discovery.""" + # Suppose we have a device with three services: A, B and C. Let's assume + # service A is discovered by Zeroconf, triggering a device scan that also finds + # service B but *not* C. An identifier is picked from one of the services and + # used as unique_id. The select process is deterministic (let's say in order A, + # B and C) but in practice that doesn't matter. So, a flow is set up for the + # device with unique_id set to "A" for services A and B. + # + # Now, service C is found and the same thing happens again but only service B + # is found. In this case, unique_id will be set to "B" which is problematic + # since both flows really represent the same device. They will however end up + # as two separate flows. + # + # To solve this, all identifiers found during a device scan is stored as + # "all_identifiers" in the flow context. When a new service is discovered, the + # code below will check these identifiers for all active flows and abort if a + # match is found. Before aborting, the original flow is updated with any + # potentially new identifiers. In the example above, when service C is + # discovered, the identifier of service C will be inserted into + # "all_identifiers" of the original flow (making the device complete). + for flow in self._async_in_progress(): + for identifier in self.atv.all_identifiers: + if identifier not in flow["context"].get("all_identifiers", []): + continue + + # Add potentially new identifiers from this device to the existing flow + identifiers = set(flow["context"]["all_identifiers"]) + identifiers.update(self.atv.all_identifiers) + flow["context"]["all_identifiers"] = list(identifiers) + + raise data_entry_flow.AbortFlow("already_in_progress") + + self.context["all_identifiers"] = self.atv.all_identifiers + + # Also abort if an integration with this identifier already exists + await self.async_set_unique_id(self.device_identifier) self._abort_if_unique_id_configured() self.context["identifier"] = self.unique_id - self.context["title_placeholders"] = {"name": name} - self.target_device = identifier - return await self.async_find_device_wrapper(self.async_step_confirm) + return await self.async_step_confirm() async def async_find_device_wrapper(self, next_func, allow_exist=False): """Find a specific device and call another function when done. @@ -187,56 +225,101 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_find_device(self, allow_exist=False): """Scan for the selected device to discover services.""" - self.scan_result, self.atv = await device_scan( - self.target_device, self.hass.loop, cache=self.scan_result + self.atv, self.atv_identifiers = await device_scan( + self.scan_filter, self.hass.loop ) if not self.atv: raise DeviceNotFound() - self.protocol = self.atv.main_service().protocol + # Protocols supported by the device are prospects for pairing + self.protocols_to_pair = deque( + service.protocol for service in self.atv.services if service.enabled + ) + + dev_info = self.atv.device_info + self.context["title_placeholders"] = { + "name": self.atv.name, + "type": ( + dev_info.raw_model + if dev_info.model == DeviceModel.Unknown and dev_info.raw_model + else model_str(dev_info.model) + ), + } if not allow_exist: for identifier in self.atv.all_identifiers: - if identifier in self._async_current_ids(): - raise DeviceAlreadyConfigured() - - # If credentials were found, save them - for service in self.atv.services: - if service.credentials: - self.credentials[service.protocol.value] = service.credentials + for entry in self._async_current_entries(): + if identifier in entry.data.get( + CONF_IDENTIFIERS, [entry.unique_id] + ): + raise DeviceAlreadyConfigured() async def async_step_confirm(self, user_input=None): """Handle user-confirmation of discovered node.""" if user_input is not None: - return await self.async_begin_pairing() + expected_identifier_count = len(self.context["all_identifiers"]) + # If number of services found during device scan mismatch number of + # identifiers collected during Zeroconf discovery, then trigger a new scan + # with hopes of finding all services. + if len(self.atv.all_identifiers) != expected_identifier_count: + try: + await self.async_find_device() + except DeviceNotFound: + return self.async_abort(reason="device_not_found") + + # If all services still were not found, bail out with an error + if len(self.atv.all_identifiers) != expected_identifier_count: + return self.async_abort(reason="inconsistent_device") + + return await self.async_pair_next_protocol() + return self.async_show_form( - step_id="confirm", description_placeholders={"name": self.atv.name} + step_id="confirm", + description_placeholders={ + "name": self.atv.name, + "type": model_str(self.atv.device_info.model), + }, ) - async def async_begin_pairing(self): + async def async_pair_next_protocol(self): """Start pairing process for the next available protocol.""" - self.protocol = self._next_protocol_to_pair() - - # Dispose previous pairing sessions - if self.pairing is not None: - await self.pairing.close() - self.pairing = None + await self._async_cleanup() # Any more protocols to pair? Else bail out here - if not self.protocol: - await self.async_set_unique_id(self.atv.main_service().identifier) - return self._async_get_entry( - self.atv.main_service().protocol, - self.atv.name, - self.credentials, - self.atv.address, - ) + if not self.protocols_to_pair: + return await self._async_get_entry() + + self.protocol = self.protocols_to_pair.popleft() + service = self.atv.get_service(self.protocol) + + # Service requires a password + if service.requires_password: + return await self.async_step_password() + + # Figure out, depending on protocol, what kind of pairing is needed + if service.pairing == PairingRequirement.Unsupported: + _LOGGER.debug("%s does not support pairing", self.protocol) + return await self.async_pair_next_protocol() + if service.pairing == PairingRequirement.Disabled: + return await self.async_step_protocol_disabled() + if service.pairing == PairingRequirement.NotNeeded: + _LOGGER.debug("%s does not require pairing", self.protocol) + self.credentials[self.protocol.value] = None + return await self.async_pair_next_protocol() + + _LOGGER.debug("%s requires pairing", self.protocol) + + # Protocol specific arguments + pair_args = {} + if self.protocol == Protocol.DMAP: + pair_args["name"] = "Home Assistant" + pair_args["zeroconf"] = await zeroconf.async_get_instance(self.hass) # Initiate the pairing process abort_reason = None session = async_get_clientsession(self.hass) self.pairing = await pair( - self.atv, self.protocol, self.hass.loop, session=session + self.atv, self.protocol, self.hass.loop, session=session, **pair_args ) try: await self.pairing.begin() @@ -252,8 +335,7 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): abort_reason = "unknown" if abort_reason: - if self.pairing: - await self.pairing.close() + await self._async_cleanup() return self.async_abort(reason=abort_reason) # Choose step depending on if PIN is required from user or not @@ -262,6 +344,15 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_pair_no_pin() + async def async_step_protocol_disabled(self, user_input=None): + """Inform user that a protocol is disabled and cannot be paired.""" + if user_input is not None: + return await self.async_pair_next_protocol() + return self.async_show_form( + step_id="protocol_disabled", + description_placeholders={"protocol": protocol_str(self.protocol)}, + ) + async def async_step_pair_with_pin(self, user_input=None): """Handle pairing step where a PIN is required from the user.""" errors = {} @@ -270,12 +361,10 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.pairing.pin(user_input[CONF_PIN]) await self.pairing.finish() self.credentials[self.protocol.value] = self.pairing.service.credentials - return await self.async_begin_pairing() + return await self.async_pair_next_protocol() except exceptions.PairingError: _LOGGER.exception("Authentication problem") errors["base"] = "invalid_auth" - except AbortFlow: - raise except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" @@ -293,7 +382,7 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.pairing.finish() if self.pairing.has_paired: self.credentials[self.protocol.value] = self.pairing.service.credentials - return await self.async_begin_pairing() + return await self.async_pair_next_protocol() await self.pairing.close() return self.async_abort(reason="device_did_not_pair") @@ -311,55 +400,57 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_service_problem(self, user_input=None): """Inform user that a service will not be added.""" if user_input is not None: - self.credentials[self.protocol.value] = None - return await self.async_begin_pairing() + return await self.async_pair_next_protocol() return self.async_show_form( step_id="service_problem", description_placeholders={"protocol": protocol_str(self.protocol)}, ) - def _async_get_entry(self, protocol, name, credentials, address): - if not is_valid_credentials(credentials): - return self.async_abort(reason="invalid_config") + async def async_step_password(self, user_input=None): + """Inform user that password is not supported.""" + if user_input is not None: + return await self.async_pair_next_protocol() - data = { - CONF_PROTOCOL: protocol.value, - CONF_NAME: name, - CONF_CREDENTIALS: credentials, - CONF_ADDRESS: str(address), - } - - self._abort_if_unique_id_configured(reload_on_update=False, updates=data) - - return self.async_create_entry(title=name, data=data) - - def _next_protocol_to_pair(self): - def _needs_pairing(protocol): - if self.atv.get_service(protocol) is None: - return False - return protocol.value not in self.credentials - - for protocol in PROTOCOL_PRIORITY: - if _needs_pairing(protocol): - return protocol - return None - - def _devices_str(self): - return ", ".join( - [ - f"`{atv.name} ({atv.address})`" - for atv in self.scan_result - if atv.identifier not in self._async_current_ids() - ] + return self.async_show_form( + step_id="password", + description_placeholders={"protocol": protocol_str(self.protocol)}, ) - def _prefill_identifier(self): - # Return identifier (address) of one device that has not been paired with - for atv in self.scan_result: - if atv.identifier not in self._async_current_ids(): - return str(atv.address) - return "" + async def _async_cleanup(self): + """Clean up allocated resources.""" + if self.pairing is not None: + await self.pairing.close() + self.pairing = None + + async def _async_get_entry(self): + """Return config entry or update existing config entry.""" + # Abort if no protocols were paired + if not self.credentials: + return self.async_abort(reason="setup_failed") + + data = { + CONF_NAME: self.atv.name, + CONF_CREDENTIALS: self.credentials, + CONF_ADDRESS: str(self.atv.address), + CONF_IDENTIFIERS: self.atv_identifiers, + } + + existing_entry = await self.async_set_unique_id( + self.device_identifier, raise_on_progress=False + ) + + # If an existing config entry is updated, then this was a re-auth + if existing_entry: + self.hass.config_entries.async_update_entry( + existing_entry, data=data, unique_id=self.unique_id + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(existing_entry.entry_id) + ) + return self.async_abort(reason="reauth_successful") + + return self.async_create_entry(title=self.atv.name, data=data) class AppleTVOptionsFlow(config_entries.OptionsFlow): diff --git a/homeassistant/components/apple_tv/const.py b/homeassistant/components/apple_tv/const.py index ac04cc1b937..5fb169ec259 100644 --- a/homeassistant/components/apple_tv/const.py +++ b/homeassistant/components/apple_tv/const.py @@ -2,10 +2,7 @@ DOMAIN = "apple_tv" -CONF_IDENTIFIER = "identifier" CONF_CREDENTIALS = "credentials" -CONF_CREDENTIALS_MRP = "mrp" -CONF_CREDENTIALS_DMAP = "dmap" -CONF_CREDENTIALS_AIRPLAY = "airplay" +CONF_IDENTIFIERS = "identifiers" CONF_START_OFF = "start_off" diff --git a/homeassistant/components/apple_tv/manifest.json b/homeassistant/components/apple_tv/manifest.json index 1f3662b11d4..c54e2254fb3 100644 --- a/homeassistant/components/apple_tv/manifest.json +++ b/homeassistant/components/apple_tv/manifest.json @@ -3,8 +3,12 @@ "name": "Apple TV", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/apple_tv", - "requirements": ["pyatv==0.8.2"], - "zeroconf": ["_mediaremotetv._tcp.local.", "_touch-able._tcp.local."], + "requirements": ["pyatv==0.9.7"], + "zeroconf": [ + "_mediaremotetv._tcp.local.", + "_touch-able._tcp.local.", + {"type":"_airplay._tcp.local.","model":"appletv*"} + ], "codeowners": ["@postlund"], "iot_class": "local_push" } diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py index 35b9777394e..7a18fbc3fc7 100644 --- a/homeassistant/components/apple_tv/media_player.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -10,6 +10,7 @@ from pyatv.const import ( RepeatState, ShuffleState, ) +from pyatv.helpers import is_streamable from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( @@ -50,9 +51,14 @@ _LOGGER = logging.getLogger(__name__) PARALLEL_UPDATES = 0 +# We always consider these to be supported +SUPPORT_BASE = SUPPORT_TURN_ON | SUPPORT_TURN_OFF + +# This is the "optimistic" view of supported features and will be returned until the +# actual set of supported feature have been determined (will always be all or a subset +# of these). SUPPORT_APPLE_TV = ( - SUPPORT_TURN_ON - | SUPPORT_TURN_OFF + SUPPORT_BASE | SUPPORT_PLAY_MEDIA | SUPPORT_PAUSE | SUPPORT_PLAY @@ -66,6 +72,23 @@ SUPPORT_APPLE_TV = ( ) +# Map features in pyatv to Home Assistant +SUPPORT_FEATURE_MAPPING = { + FeatureName.PlayUrl: SUPPORT_PLAY_MEDIA, + FeatureName.StreamFile: SUPPORT_PLAY_MEDIA, + FeatureName.Pause: SUPPORT_PAUSE, + FeatureName.Play: SUPPORT_PLAY, + FeatureName.SetPosition: SUPPORT_SEEK, + FeatureName.Stop: SUPPORT_STOP, + FeatureName.Next: SUPPORT_NEXT_TRACK, + FeatureName.Previous: SUPPORT_PREVIOUS_TRACK, + FeatureName.VolumeUp: SUPPORT_VOLUME_STEP, + FeatureName.VolumeDown: SUPPORT_VOLUME_STEP, + FeatureName.SetRepeat: SUPPORT_REPEAT_SET, + FeatureName.SetShuffle: SUPPORT_SHUFFLE_SET, +} + + async def async_setup_entry(hass, config_entry, async_add_entities): """Load Apple TV media player based on a config entry.""" name = config_entry.data[CONF_NAME] @@ -86,8 +109,27 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): @callback def async_device_connected(self, atv): """Handle when connection is made to device.""" - self.atv.push_updater.listener = self - self.atv.push_updater.start() + # NB: Do not use _is_feature_available here as it only works when playing + if self.atv.features.in_state(FeatureState.Available, FeatureName.PushUpdates): + self.atv.push_updater.listener = self + self.atv.push_updater.start() + + self._attr_supported_features = SUPPORT_BASE + + # Determine the actual set of supported features. All features not reported as + # "Unsupported" are considered here as the state of such a feature can never + # change after a connection has been established, i.e. an unsupported feature + # can never change to be supported. + all_features = self.atv.features.all_features() + for feature_name, support_flag in SUPPORT_FEATURE_MAPPING.items(): + feature_info = all_features.get(feature_name) + if feature_info and feature_info.state != FeatureState.Unsupported: + self._attr_supported_features |= support_flag + + # No need to schedule state update here as that will happen when the first + # metadata update arrives (sometime very soon after this callback returns) + + # Listen to power updates self.atv.power.listener = self @callback @@ -96,6 +138,7 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): self.atv.push_updater.stop() self.atv.push_updater.listener = None self.atv.power.listener = None + self._attr_supported_features = SUPPORT_APPLE_TV @property def state(self): @@ -186,13 +229,28 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): async def async_play_media(self, media_type, media_id, **kwargs): """Send the play_media command to the media player.""" - await self.atv.stream.play_url(media_id) + # If input (file) has a file format supported by pyatv, then stream it with + # RAOP. Otherwise try to play it with regular AirPlay. + if self._is_feature_available(FeatureName.StreamFile) and ( + await is_streamable(media_id) or media_type == MEDIA_TYPE_MUSIC + ): + _LOGGER.debug("Streaming %s via RAOP", media_id) + await self.atv.stream.stream_file(media_id) + elif self._is_feature_available(FeatureName.PlayUrl): + _LOGGER.debug("Playing %s via AirPlay", media_id) + await self.atv.stream.play_url(media_id) + else: + _LOGGER.error("Media streaming is not possible with current configuration") @property def media_image_hash(self): """Hash value for media image.""" state = self.state - if self._playing and state not in [None, STATE_OFF, STATE_IDLE]: + if ( + self._playing + and self._is_feature_available(FeatureName.Artwork) + and state not in [None, STATE_OFF, STATE_IDLE] + ): return self.atv.metadata.artwork_id return None @@ -267,7 +325,6 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): """Pause media on media player.""" if self._playing: await self.atv.remote_control.play_pause() - return None async def async_media_play(self): """Play media.""" @@ -302,12 +359,12 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): async def async_volume_up(self): """Turn volume up for media player.""" if self.atv: - await self.atv.remote_control.volume_up() + await self.atv.audio.volume_up() async def async_volume_down(self): """Turn volume down for media player.""" if self.atv: - await self.atv.remote_control.volume_down() + await self.atv.audio.volume_down() async def async_set_repeat(self, repeat): """Set repeat mode.""" diff --git a/homeassistant/components/apple_tv/strings.json b/homeassistant/components/apple_tv/strings.json index d9fe17863dd..f46f531865c 100644 --- a/homeassistant/components/apple_tv/strings.json +++ b/homeassistant/components/apple_tv/strings.json @@ -1,17 +1,17 @@ { "config": { - "flow_title": "{name}", + "flow_title": "{name} ({type})", "step": { "user": { "title": "Setup a new Apple TV", - "description": "Start by entering the device name (e.g. Kitchen or Bedroom) or IP address of the Apple TV you want to add. If any devices were automatically found on your network, they are shown below.\n\nIf you cannot see your device or experience any issues, try specifying the device IP address.\n\n{devices}", + "description": "Start by entering the device name (e.g. Kitchen or Bedroom) or IP address of the Apple TV you want to add.\n\nIf you cannot see your device or experience any issues, try specifying the device IP address.", "data": { "device_input": "Device" } }, "reconfigure": { "title": "Device reconfiguration", - "description": "This Apple TV is experiencing some connection difficulties and must be reconfigured." + "description": "Reconfigure this device to restore its functionality." }, "pair_with_pin": { "title": "Pairing", @@ -22,32 +22,42 @@ }, "pair_no_pin": { "title": "Pairing", - "description": "Pairing is required for the `{protocol}` service. Please enter PIN {pin} on your Apple TV to continue." + "description": "Pairing is required for the `{protocol}` service. Please enter PIN {pin} on your device to continue." + }, + "protocol_disabled": { + "title": "Pairing not possible", + "description": "Pairing is required for `{protocol}` but it is disabled on the device. Please review potential access restrictions (e.g. allow all devices on the local network to connect) on the device.\n\nYou may continue without pairing this protocol, but some functionality will be limited." + }, + "confirm": { + "title": "Confirm adding Apple TV", + "description": "You are about to add `{name}` of type `{type}` to Home Assistant.\n\n**To complete the process, you may have to enter multiple PIN codes.**\n\nPlease note that you will *not* be able to power off your Apple TV with this integration. Only the media player in Home Assistant will turn off!" }, "service_problem": { "title": "Failed to add service", "description": "A problem occurred while pairing protocol `{protocol}`. It will be ignored." }, - "confirm": { - "title": "Confirm adding Apple TV", - "description": "You are about to add the Apple TV named `{name}` to Home Assistant.\n\n**To complete the process, you may have to enter multiple PIN codes.**\n\nPlease note that you will *not* be able to power off your Apple TV with this integration. Only the media player in Home Assistant will turn off!" + "password": { + "title": "Password required", + "description": "A password is required by `{protocol}`. This is not yet supported, please disable password to continue." } }, "error": { "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "no_usable_service": "A device was found but could not identify any way to establish a connection to it. If you keep seeing this message, try specifying its IP address or restarting your Apple TV.", "unknown": "[%key:common::config_flow::error::unknown%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" }, "abort": { "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", - "already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "device_did_not_pair": "No attempt to finish pairing process was made from the device.", "backoff": "Device does not accept pairing reqests at this time (you might have entered an invalid PIN code too many times), try again later.", - "invalid_config": "The configuration for this device is incomplete. Please try adding it again.", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "unknown": "[%key:common::config_flow::error::unknown%]", + "setup_failed": "Failed to set up device.", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "device_not_found": "Device was not found during discovery, please try adding it again.", + "inconsistent_device": "Expected protocols were not found during discovery. This normally indicates a problem with multicast DNS (Zeroconf). Please try adding the device again." } }, "options": { diff --git a/homeassistant/components/apple_tv/translations/en.json b/homeassistant/components/apple_tv/translations/en.json index 304a43363a0..7f755320e34 100644 --- a/homeassistant/components/apple_tv/translations/en.json +++ b/homeassistant/components/apple_tv/translations/en.json @@ -1,64 +1,74 @@ { - "config": { - "abort": { - "already_configured_device": "Device is already configured", - "already_in_progress": "Configuration flow is already in progress", - "backoff": "Device does not accept pairing reqests at this time (you might have entered an invalid PIN code too many times), try again later.", - "device_did_not_pair": "No attempt to finish pairing process was made from the device.", - "invalid_config": "The configuration for this device is incomplete. Please try adding it again.", - "no_devices_found": "No devices found on the network", - "unknown": "Unexpected error" - }, - "error": { - "already_configured": "Device is already configured", - "invalid_auth": "Invalid authentication", - "no_devices_found": "No devices found on the network", - "no_usable_service": "A device was found but could not identify any way to establish a connection to it. If you keep seeing this message, try specifying its IP address or restarting your Apple TV.", - "unknown": "Unexpected error" - }, - "flow_title": "{name}", - "step": { - "confirm": { - "description": "You are about to add the Apple TV named `{name}` to Home Assistant.\n\n**To complete the process, you may have to enter multiple PIN codes.**\n\nPlease note that you will *not* be able to power off your Apple TV with this integration. Only the media player in Home Assistant will turn off!", - "title": "Confirm adding Apple TV" - }, - "pair_no_pin": { - "description": "Pairing is required for the `{protocol}` service. Please enter PIN {pin} on your Apple TV to continue.", - "title": "Pairing" - }, - "pair_with_pin": { - "data": { - "pin": "PIN Code" - }, - "description": "Pairing is required for the `{protocol}` protocol. Please enter the PIN code displayed on screen. Leading zeros shall be omitted, i.e. enter 123 if the displayed code is 0123.", - "title": "Pairing" - }, - "reconfigure": { - "description": "This Apple TV is experiencing some connection difficulties and must be reconfigured.", - "title": "Device reconfiguration" - }, - "service_problem": { - "description": "A problem occurred while pairing protocol `{protocol}`. It will be ignored.", - "title": "Failed to add service" - }, - "user": { - "data": { - "device_input": "Device" - }, - "description": "Start by entering the device name (e.g. Kitchen or Bedroom) or IP address of the Apple TV you want to add. If any devices were automatically found on your network, they are shown below.\n\nIf you cannot see your device or experience any issues, try specifying the device IP address.\n\n{devices}", - "title": "Setup a new Apple TV" - } + "config": { + "flow_title": "{name} ({type})", + "step": { + "user": { + "title": "Setup a new Apple TV", + "description": "Start by entering the device name (e.g. Kitchen or Bedroom) or IP address of the Apple TV you want to add.\n\nIf you cannot see your device or experience any issues, try specifying the device IP address.", + "data": { + "device_input": "Device" } - }, - "options": { - "step": { - "init": { - "data": { - "start_off": "Do not turn device on when starting Home Assistant" - }, - "description": "Configure general device settings" - } + }, + "reconfigure": { + "title": "Device reconfiguration", + "description": "Reconfigure this device to restore its functionality." + }, + "pair_with_pin": { + "title": "Pairing", + "description": "Pairing is required for the `{protocol}` protocol. Please enter the PIN code displayed on screen. Leading zeros shall be omitted, i.e. enter 123 if the displayed code is 0123.", + "data": { + "pin": "PIN Code" } + }, + "pair_no_pin": { + "title": "Pairing", + "description": "Pairing is required for the `{protocol}` service. Please enter PIN {pin} on your device to continue." + }, + "protocol_disabled": { + "title": "Pairing not possible", + "description": "Pairing is required for `{protocol}` but it is disabled on the device. Please review potential access restrictions (e.g. allow all devices on the local network to connect) on the device.\n\nYou may continue without pairing this protocol, but some functionality will be limited." + }, + "confirm": { + "title": "Confirm adding Apple TV", + "description": "You are about to add `{name}` of type `{type}` to Home Assistant.\n\n**To complete the process, you may have to enter multiple PIN codes.**" + }, + "service_problem": { + "title": "Failed to add service", + "description": "A problem occurred while pairing protocol `{protocol}`. It will be ignored." + }, + "password": { + "title": "Password required", + "description": "A password is required by `{protocol}`. This is not yet supported, please disable password to continue." + } }, - "title": "Apple TV" -} \ No newline at end of file + "error": { + "no_devices_found": "No devices found on the network", + "already_configured_device": "Device is already configured", + "unknown": "Unexpected error", + "invalid_auth": "Invalid authentication" + }, + "abort": { + "no_devices_found": "No devices found on the network", + "already_configured_device": "Device is already configured", + "device_did_not_pair": "No attempt to finish pairing process was made from the device.", + "backoff": "Device does not accept pairing reqests at this time (you might have entered an invalid PIN code too many times), try again later.", + "already_in_progress": "Configuration flow is already in progress", + "unknown": "Unexpected error", + "setup_failed": "Failed to set up device.", + "reauth_successful": "Re-authentication was successful", + "device_not_found": "Device was not found during discovery, please try adding it again.", + "inconsistent_device": "Expected protocols were not found during discovery. This normally indicates a problem with multicast DNS (Zeroconf). Please try adding the device again." + } + }, + "options": { + "step": { + "init": { + "description": "Configure general device settings", + "data": { + "start_off": "Do not turn device on when starting Home Assistant", + "reconfigure": "Force reconfiguration of device" + } + } + } + } +} diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index aec93dd36c9..c5c4e0c9a01 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -12,6 +12,10 @@ ZEROCONF = { } ], "_airplay._tcp.local.": [ + { + "domain": "apple_tv", + "model": "appletv*" + }, { "domain": "samsungtv", "manufacturer": "samsung*" diff --git a/requirements_all.txt b/requirements_all.txt index 10f5afa24a0..88a3faf9c6d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1372,7 +1372,7 @@ pyatmo==6.2.0 pyatome==0.1.1 # homeassistant.components.apple_tv -pyatv==0.8.2 +pyatv==0.9.7 # homeassistant.components.balboa pybalboa==0.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index af322c62c86..150fd0b4d28 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -838,7 +838,7 @@ pyatag==0.3.5.3 pyatmo==6.2.0 # homeassistant.components.apple_tv -pyatv==0.8.2 +pyatv==0.9.7 # homeassistant.components.balboa pybalboa==0.13 diff --git a/tests/components/apple_tv/common.py b/tests/components/apple_tv/common.py index 6f13239edcb..687ad256f3a 100644 --- a/tests/components/apple_tv/common.py +++ b/tests/components/apple_tv/common.py @@ -1,6 +1,6 @@ """Test code shared between test files.""" -from pyatv import conf, interface +from pyatv import conf, const, interface from pyatv.const import Protocol @@ -47,3 +47,26 @@ def create_conf(name, address, *services): for service in services: atv.add_service(service) return atv + + +def mrp_service(enabled=True): + """Create example MRP service.""" + return conf.ManualService( + "mrpid", + Protocol.MRP, + 5555, + {}, + pairing_requirement=const.PairingRequirement.Mandatory, + enabled=enabled, + ) + + +def airplay_service(): + """Create example AirPlay service.""" + return conf.ManualService( + "airplayid", + Protocol.AirPlay, + 7777, + {}, + pairing_requirement=const.PairingRequirement.Mandatory, + ) diff --git a/tests/components/apple_tv/conftest.py b/tests/components/apple_tv/conftest.py index f07fa7d70bb..c8a9725610c 100644 --- a/tests/components/apple_tv/conftest.py +++ b/tests/components/apple_tv/conftest.py @@ -3,10 +3,11 @@ from unittest.mock import patch from pyatv import conf -from pyatv.support.http import create_session +from pyatv.const import PairingRequirement, Protocol +from pyatv.support import http import pytest -from .common import MockPairingHandler, create_conf +from .common import MockPairingHandler, airplay_service, create_conf, mrp_service @pytest.fixture(autouse=True, name="mock_scan") @@ -40,7 +41,7 @@ def pairing(): async def _pair(config, protocol, loop, session=None, **kwargs): handler = MockPairingHandler( - await create_session(session), config.get_service(protocol) + await http.create_session(session), config.get_service(protocol) ) handler.always_fail = mock_pair.always_fail return handler @@ -78,9 +79,15 @@ def full_device(mock_scan, dmap_pin): create_conf( "127.0.0.1", "MRP Device", - conf.MrpService("mrpid", 5555), - conf.DmapService("dmapid", None, port=6666), - conf.AirPlayService("airplayid", port=7777), + mrp_service(), + conf.ManualService( + "dmapid", + Protocol.DMAP, + 6666, + {}, + pairing_requirement=PairingRequirement.Mandatory, + ), + airplay_service(), ) ) yield mock_scan @@ -90,7 +97,31 @@ def full_device(mock_scan, dmap_pin): def mrp_device(mock_scan): """Mock pyatv.scan.""" mock_scan.result.append( - create_conf("127.0.0.1", "MRP Device", conf.MrpService("mrpid", 5555)) + create_conf( + "127.0.0.1", + "MRP Device", + mrp_service(), + ) + ) + yield mock_scan + + +@pytest.fixture +def airplay_with_disabled_mrp(mock_scan): + """Mock pyatv.scan.""" + mock_scan.result.append( + create_conf( + "127.0.0.1", + "AirPlay Device", + mrp_service(enabled=False), + conf.ManualService( + "airplayid", + Protocol.AirPlay, + 7777, + {}, + pairing_requirement=PairingRequirement.Mandatory, + ), + ) ) yield mock_scan @@ -102,7 +133,14 @@ def dmap_device(mock_scan): create_conf( "127.0.0.1", "DMAP Device", - conf.DmapService("dmapid", None, port=6666), + conf.ManualService( + "dmapid", + Protocol.DMAP, + 6666, + {}, + credentials=None, + pairing_requirement=PairingRequirement.Mandatory, + ), ) ) yield mock_scan @@ -115,14 +153,48 @@ def dmap_device_with_credentials(mock_scan): create_conf( "127.0.0.1", "DMAP Device", - conf.DmapService("dmapid", "dummy_creds", port=6666), + conf.ManualService( + "dmapid", + Protocol.DMAP, + 6666, + {}, + credentials="dummy_creds", + pairing_requirement=PairingRequirement.NotNeeded, + ), ) ) yield mock_scan @pytest.fixture -def device_with_no_services(mock_scan): +def airplay_device_with_password(mock_scan): """Mock pyatv.scan.""" - mock_scan.result.append(create_conf("127.0.0.1", "Invalid Device")) + mock_scan.result.append( + create_conf( + "127.0.0.1", + "AirPlay Device", + conf.ManualService( + "airplayid", Protocol.AirPlay, 7777, {}, requires_password=True + ), + ) + ) + yield mock_scan + + +@pytest.fixture +def dmap_with_requirement(mock_scan, pairing_requirement): + """Mock pyatv.scan.""" + mock_scan.result.append( + create_conf( + "127.0.0.1", + "DMAP Device", + conf.ManualService( + "dmapid", + Protocol.DMAP, + 6666, + {}, + pairing_requirement=pairing_requirement, + ), + ) + ) yield mock_scan diff --git a/tests/components/apple_tv/test_config_flow.py b/tests/components/apple_tv/test_config_flow.py index a99df9ad856..39403b837ca 100644 --- a/tests/components/apple_tv/test_config_flow.py +++ b/tests/components/apple_tv/test_config_flow.py @@ -3,25 +3,32 @@ from unittest.mock import patch from pyatv import exceptions -from pyatv.const import Protocol +from pyatv.const import PairingRequirement, Protocol import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components import zeroconf from homeassistant.components.apple_tv.const import CONF_START_OFF, DOMAIN +from .common import airplay_service, create_conf, mrp_service + from tests.common import MockConfigEntry DMAP_SERVICE = zeroconf.ZeroconfServiceInfo( host="mock_host", hostname="mock_hostname", - name="dmapid.something", port=None, - properties={"CtlN": "Apple TV"}, type="_touch-able._tcp.local.", + name="dmapid._touch-able._tcp.local.", + properties={"CtlN": "Apple TV"}, ) +@pytest.fixture(autouse=True) +def use_mocked_zeroconf(mock_async_zeroconf): + """Mock zeroconf in all tests.""" + + @pytest.fixture(autouse=True) def mock_setup_entry(): """Mock setting up a config entry.""" @@ -39,9 +46,8 @@ async def test_user_input_device_not_found(hass, mrp_device): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["description_placeholders"] == {"devices": "`MRP Device (127.0.0.1)`"} + assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -81,7 +87,10 @@ async def test_user_adds_full_device(hass, full_device, pairing): {"device_input": "MRP Device"}, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["description_placeholders"] == {"name": "MRP Device"} + assert result2["description_placeholders"] == { + "name": "MRP Device", + "type": "Unknown", + } result3 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -108,8 +117,8 @@ async def test_user_adds_full_device(hass, full_device, pairing): Protocol.MRP.value: "mrp_creds", Protocol.AirPlay.value: "airplay_creds", }, + "identifiers": ["mrpid", "dmapid", "airplayid"], "name": "MRP Device", - "protocol": Protocol.MRP.value, } @@ -124,7 +133,10 @@ async def test_user_adds_dmap_device(hass, dmap_device, dmap_pin, pairing): {"device_input": "DMAP Device"}, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["description_placeholders"] == {"name": "DMAP Device"} + assert result2["description_placeholders"] == { + "name": "DMAP Device", + "type": "Unknown", + } result3 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -137,8 +149,8 @@ async def test_user_adds_dmap_device(hass, dmap_device, dmap_pin, pairing): assert result6["data"] == { "address": "127.0.0.1", "credentials": {Protocol.DMAP.value: "dmap_creds"}, + "identifiers": ["dmapid"], "name": "DMAP Device", - "protocol": Protocol.DMAP.value, } @@ -162,29 +174,6 @@ async def test_user_adds_dmap_device_failed(hass, dmap_device, dmap_pin, pairing assert result2["reason"] == "device_did_not_pair" -async def test_user_adds_device_with_credentials(hass, dmap_device_with_credentials): - """Test adding DMAP device with existing credentials (home sharing).""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"device_input": "DMAP Device"}, - ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["description_placeholders"] == {"name": "DMAP Device"} - - result3 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result3["type"] == "create_entry" - assert result3["data"] == { - "address": "127.0.0.1", - "credentials": {Protocol.DMAP.value: "dummy_creds"}, - "name": "DMAP Device", - "protocol": Protocol.DMAP.value, - } - - async def test_user_adds_device_with_ip_filter( hass, dmap_device_with_credentials, mock_scan ): @@ -198,15 +187,33 @@ async def test_user_adds_device_with_ip_filter( {"device_input": "127.0.0.1"}, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["description_placeholders"] == {"name": "DMAP Device"} - - result3 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result3["type"] == "create_entry" - assert result3["data"] == { - "address": "127.0.0.1", - "credentials": {Protocol.DMAP.value: "dummy_creds"}, + assert result2["description_placeholders"] == { + "name": "DMAP Device", + "type": "Unknown", + } + + +@pytest.mark.parametrize("pairing_requirement", [(PairingRequirement.NotNeeded)]) +async def test_user_pair_no_interaction(hass, dmap_with_requirement, pairing_mock): + """Test pairing service without user interaction.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + await hass.config_entries.flow.async_configure( + result["flow_id"], + {"device_input": "DMAP Device"}, + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + assert result["data"] == { + "address": "127.0.0.1", + "credentials": {Protocol.DMAP.value: None}, + "identifiers": ["dmapid"], "name": "DMAP Device", - "protocol": Protocol.DMAP.value, } @@ -240,20 +247,6 @@ async def test_user_adds_existing_device(hass, mrp_device): assert result2["errors"] == {"base": "already_configured"} -async def test_user_adds_unusable_device(hass, device_with_no_services): - """Test that it is not possible to add device with no services.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"device_input": "Invalid Device"}, - ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["errors"] == {"base": "no_usable_service"} - - async def test_user_connection_failed(hass, mrp_device, pairing_mock): """Test error message when connection to device fails.""" pairing_mock.begin.side_effect = exceptions.ConnectionFailedError @@ -277,7 +270,7 @@ async def test_user_connection_failed(hass, mrp_device, pairing_mock): {}, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result2["reason"] == "invalid_config" + assert result2["reason"] == "setup_failed" async def test_user_start_pair_error_failed(hass, mrp_device, pairing_mock): @@ -301,6 +294,81 @@ async def test_user_start_pair_error_failed(hass, mrp_device, pairing_mock): assert result2["reason"] == "invalid_auth" +async def test_user_pair_service_with_password( + hass, airplay_device_with_password, pairing_mock +): + """Test pairing with service requiring a password (not supported).""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + await hass.config_entries.flow.async_configure( + result["flow_id"], + {"device_input": "AirPlay Device"}, + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "password" + + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result3["reason"] == "setup_failed" + + +@pytest.mark.parametrize("pairing_requirement", [(PairingRequirement.Disabled)]) +async def test_user_pair_disabled_service(hass, dmap_with_requirement, pairing_mock): + """Test pairing with disabled service (is ignored with message).""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + await hass.config_entries.flow.async_configure( + result["flow_id"], + {"device_input": "DMAP Device"}, + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "protocol_disabled" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "setup_failed" + + +@pytest.mark.parametrize("pairing_requirement", [(PairingRequirement.Unsupported)]) +async def test_user_pair_ignore_unsupported(hass, dmap_with_requirement, pairing_mock): + """Test pairing with disabled service (is ignored silently).""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + await hass.config_entries.flow.async_configure( + result["flow_id"], + {"device_input": "DMAP Device"}, + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "setup_failed" + + async def test_user_pair_invalid_pin(hass, mrp_device, pairing_mock): """Test pairing with invalid pin.""" pairing_mock.finish.side_effect = exceptions.PairingError @@ -395,6 +463,41 @@ async def test_user_pair_begin_unexpected_error(hass, mrp_device, pairing_mock): assert result2["reason"] == "unknown" +async def test_ignores_disabled_service(hass, airplay_with_disabled_mrp, pairing): + """Test adding device with only DMAP service.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + # Find based on mrpid (but do not pair that service since it's disabled) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"device_input": "mrpid"}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["description_placeholders"] == { + "name": "AirPlay Device", + "type": "Unknown", + } + + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["description_placeholders"] == {"protocol": "AirPlay"} + + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], {"pin": 1111} + ) + assert result3["type"] == "create_entry" + assert result3["data"] == { + "address": "127.0.0.1", + "credentials": { + Protocol.AirPlay.value: "airplay_creds", + }, + "identifiers": ["mrpid", "airplayid"], + "name": "AirPlay Device", + } + + # Zeroconf @@ -408,8 +511,8 @@ async def test_zeroconf_unsupported_service_aborts(hass): hostname="mock_hostname", name="mock_name", port=None, - properties={}, type="_dummy._tcp.local.", + properties={}, ), ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -424,14 +527,17 @@ async def test_zeroconf_add_mrp_device(hass, mrp_device, pairing): data=zeroconf.ZeroconfServiceInfo( host="mock_host", hostname="mock_hostname", - name="mock_name", port=None, + name="Kitchen", properties={"UniqueIdentifier": "mrpid", "Name": "Kitchen"}, type="_mediaremotetv._tcp.local.", ), ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["description_placeholders"] == {"name": "MRP Device"} + assert result["description_placeholders"] == { + "name": "MRP Device", + "type": "Unknown", + } result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -447,8 +553,8 @@ async def test_zeroconf_add_mrp_device(hass, mrp_device, pairing): assert result3["data"] == { "address": "127.0.0.1", "credentials": {Protocol.MRP.value: "mrp_creds"}, + "identifiers": ["mrpid"], "name": "MRP Device", - "protocol": Protocol.MRP.value, } @@ -458,7 +564,10 @@ async def test_zeroconf_add_dmap_device(hass, dmap_device, dmap_pin, pairing): DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=DMAP_SERVICE ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["description_placeholders"] == {"name": "DMAP Device"} + assert result["description_placeholders"] == { + "name": "DMAP Device", + "type": "Unknown", + } result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -472,8 +581,8 @@ async def test_zeroconf_add_dmap_device(hass, dmap_device, dmap_pin, pairing): assert result3["data"] == { "address": "127.0.0.1", "credentials": {Protocol.DMAP.value: "dmap_creds"}, + "identifiers": ["dmapid"], "name": "DMAP Device", - "protocol": Protocol.DMAP.value, } @@ -521,17 +630,226 @@ async def test_zeroconf_unexpected_error(hass, mock_scan): assert result["reason"] == "unknown" +async def test_zeroconf_abort_if_other_in_progress(hass, mock_scan): + """Test discovering unsupported zeroconf service.""" + mock_scan.result = [create_conf("127.0.0.1", "Device", airplay_service())] + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="mock_host", + hostname="mock_hostname", + port=None, + type="_airplay._tcp.local.", + name="Kitchen", + properties={"deviceid": "airplayid"}, + ), + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "confirm" + + mock_scan.result = [ + create_conf("127.0.0.1", "Device", mrp_service(), airplay_service()) + ] + + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="mock_host", + hostname="mock_hostname", + port=None, + type="_mediaremotetv._tcp.local.", + name="Kitchen", + properties={"UniqueIdentifier": "mrpid", "Name": "Kitchen"}, + ), + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "already_in_progress" + + +async def test_zeroconf_missing_device_during_protocol_resolve( + hass, mock_scan, pairing, mock_zeroconf +): + """Test discovery after service been added to existing flow with missing device.""" + mock_scan.result = [create_conf("127.0.0.1", "Device", airplay_service())] + + # Find device with AirPlay service and set up flow for it + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="mock_host", + hostname="mock_hostname", + port=None, + type="_airplay._tcp.local.", + name="Kitchen", + properties={"deviceid": "airplayid"}, + ), + ) + + mock_scan.result = [ + create_conf("127.0.0.1", "Device", mrp_service(), airplay_service()) + ] + + # Find the same device again, but now also with MRP service. The first flow should + # be updated with the MRP service. + await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="mock_host", + hostname="mock_hostname", + port=None, + type="_mediaremotetv._tcp.local.", + name="Kitchen", + properties={"UniqueIdentifier": "mrpid", "Name": "Kitchen"}, + ), + ) + + mock_scan.result = [] + + # Number of services found during initial scan (1) will not match the updated count + # (2), so it will trigger a re-scan to find all services. This will fail as no + # device is found. + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "device_not_found" + + +async def test_zeroconf_additional_protocol_resolve_failure( + hass, mock_scan, pairing, mock_zeroconf +): + """Test discovery with missing service.""" + mock_scan.result = [create_conf("127.0.0.1", "Device", airplay_service())] + + # Find device with AirPlay service and set up flow for it + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="mock_host", + hostname="mock_hostname", + port=None, + type="_airplay._tcp.local.", + name="Kitchen", + properties={"deviceid": "airplayid"}, + ), + ) + + mock_scan.result = [ + create_conf("127.0.0.1", "Device", mrp_service(), airplay_service()) + ] + + # Find the same device again, but now also with MRP service. The first flow should + # be updated with the MRP service. + await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="mock_host", + hostname="mock_hostname", + port=None, + type="_mediaremotetv._tcp.local.", + name="Kitchen", + properties={"UniqueIdentifier": "mrpid", "Name": "Kitchen"}, + ), + ) + + mock_scan.result = [create_conf("127.0.0.1", "Device", airplay_service())] + + # Number of services found during initial scan (1) will not match the updated count + # (2), so it will trigger a re-scan to find all services. This will however fail + # due to only one of the services found, yielding an error message. + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "inconsistent_device" + + +async def test_zeroconf_pair_additionally_found_protocols( + hass, mock_scan, pairing, mock_zeroconf +): + """Test discovered protocols are merged to original flow.""" + mock_scan.result = [create_conf("127.0.0.1", "Device", airplay_service())] + + # Find device with AirPlay service and set up flow for it + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="mock_host", + hostname="mock_hostname", + port=None, + type="_airplay._tcp.local.", + name="Kitchen", + properties={"deviceid": "airplayid"}, + ), + ) + + mock_scan.result = [ + create_conf("127.0.0.1", "Device", mrp_service(), airplay_service()) + ] + + # Find the same device again, but now also with MRP service. The first flow should + # be updated with the MRP service. + await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="mock_host", + hostname="mock_hostname", + port=None, + type="_mediaremotetv._tcp.local.", + name="Kitchen", + properties={"UniqueIdentifier": "mrpid", "Name": "Kitchen"}, + ), + ) + + # Verify that _both_ protocols are paired + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "pair_with_pin" + assert result2["description_placeholders"] == {"protocol": "MRP"} + + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"pin": 1234}, + ) + assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["step_id"] == "pair_with_pin" + assert result3["description_placeholders"] == {"protocol": "AirPlay"} + + result4 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"pin": 1234}, + ) + assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + # Re-configuration async def test_reconfigure_update_credentials(hass, mrp_device, pairing): """Test that reconfigure flow updates config entry.""" - config_entry = MockConfigEntry(domain="apple_tv", unique_id="mrpid") + config_entry = MockConfigEntry( + domain="apple_tv", unique_id="mrpid", data={"identifiers": ["mrpid"]} + ) config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_REAUTH}, + context={"source": "reauth"}, data={"identifier": "mrpid", "name": "apple tv"}, ) @@ -546,34 +864,16 @@ async def test_reconfigure_update_credentials(hass, mrp_device, pairing): result["flow_id"], {"pin": 1111} ) assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result3["reason"] == "already_configured" + assert result3["reason"] == "reauth_successful" assert config_entry.data == { "address": "127.0.0.1", - "protocol": Protocol.MRP.value, "name": "MRP Device", "credentials": {Protocol.MRP.value: "mrp_creds"}, + "identifiers": ["mrpid"], } -async def test_reconfigure_ongoing_aborts(hass, mrp_device): - """Test start additional reconfigure flow aborts.""" - data = { - "identifier": "mrpid", - "name": "Apple TV", - } - - await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=data - ) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=data - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_in_progress" - - # Options From dddca8aaeb05ffac1a75e8476692a96243c00ace Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 6 Dec 2021 14:46:53 +0100 Subject: [PATCH 0077/2644] Improve zwave_js add-on config flow description (#61099) --- homeassistant/components/zwave_js/strings.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index 1446c1fc7aa..13f65921cdb 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -22,6 +22,7 @@ }, "configure_addon": { "title": "Enter the Z-Wave JS add-on configuration", + "description": "The add-on will generate security keys if those fields are left empty.", "data": { "usb_path": "[%key:common::config_flow::data::usb_path%]", "s0_legacy_key": "S0 Key (Legacy)", @@ -79,6 +80,7 @@ }, "configure_addon": { "title": "Enter the Z-Wave JS add-on configuration", + "description": "The add-on will generate security keys if those fields are left empty.", "data": { "usb_path": "[%key:common::config_flow::data::usb_path%]", "s0_legacy_key": "S0 Key (Legacy)", From bd8bba9e3f12736edc95aade3532af93bfc892ae Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 6 Dec 2021 17:24:59 +0100 Subject: [PATCH 0078/2644] Fix migration of entities of Hue integration (#61095) * fix device name in log * Fix Hue migration for all id versions * fix tests * typo * change to bit more universal approach * fix test again * formatting --- homeassistant/components/hue/migration.py | 138 +++++++++++++--------- tests/components/hue/test_migration.py | 54 +++++++-- 2 files changed, 125 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/hue/migration.py b/homeassistant/components/hue/migration.py index 408ba3fc8e0..9891cc65b0c 100644 --- a/homeassistant/components/hue/migration.py +++ b/homeassistant/components/hue/migration.py @@ -4,6 +4,7 @@ import logging from aiohue import HueBridgeV2 from aiohue.discovery import is_v2_bridge +from aiohue.v2.models.device import DeviceArchetypes from aiohue.v2.models.resource import ResourceTypes from homeassistant import core @@ -18,7 +19,10 @@ from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, ) from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.device_registry import async_get as async_get_device_registry +from homeassistant.helpers.device_registry import ( + async_entries_for_config_entry as devices_for_config_entries, + async_get as async_get_device_registry, +) from homeassistant.helpers.entity_registry import ( async_entries_for_config_entry as entities_for_config_entry, async_entries_for_device, @@ -82,6 +86,18 @@ async def handle_v2_migration(hass: core.HomeAssistant, entry: ConfigEntry) -> N dev_reg = async_get_device_registry(hass) ent_reg = async_get_entity_registry(hass) LOGGER.info("Start of migration of devices and entities to support API schema 2") + + # Create mapping of mac address to HA device id's. + # Identifier in dev reg should be mac-address, + # but in some cases it has a postfix like `-0b` or `-01`. + dev_ids = {} + for hass_dev in devices_for_config_entries(dev_reg, entry.entry_id): + for domain, mac in hass_dev.identifiers: + if domain != DOMAIN: + continue + normalized_mac = mac.split("-")[0] + dev_ids[normalized_mac] = hass_dev.id + # initialize bridge connection just for the migration async with HueBridgeV2(host, api_key, websession) as api: @@ -92,83 +108,93 @@ async def handle_v2_migration(hass: core.HomeAssistant, entry: ConfigEntry) -> N DEVICE_CLASS_TEMPERATURE: ResourceTypes.TEMPERATURE, } - # handle entities attached to device + # migrate entities attached to a device for hue_dev in api.devices: zigbee = api.devices.get_zigbee_connectivity(hue_dev.id) if not zigbee or not zigbee.mac_address: # not a zigbee device or invalid mac continue - # get/update existing device by V1 identifier (mac address) - # the device will now have both the old and the new identifier - identifiers = {(DOMAIN, hue_dev.id), (DOMAIN, zigbee.mac_address)} - hass_dev = dev_reg.async_get_or_create( - config_entry_id=entry.entry_id, identifiers=identifiers - ) - LOGGER.info("Migrated device %s (%s)", hass_dev.name, hass_dev.id) - # loop through al entities for device and find match - for ent in async_entries_for_device(ent_reg, hass_dev.id, True): - # migrate light - if ent.entity_id.startswith("light"): - # should always return one lightid here - new_unique_id = next(iter(hue_dev.lights)) - if ent.unique_id == new_unique_id: - continue # just in case - LOGGER.info( - "Migrating %s from unique id %s to %s", - ent.entity_id, - ent.unique_id, - new_unique_id, - ) - ent_reg.async_update_entity( - ent.entity_id, new_unique_id=new_unique_id - ) - continue - # migrate sensors - matched_dev_class = sensor_class_mapping.get( - ent.original_device_class or "unknown" + + # get existing device by V1 identifier (mac address) + if hue_dev.product_data.product_archetype == DeviceArchetypes.BRIDGE_V2: + hass_dev_id = dev_ids.get(api.config.bridge_id.upper()) + else: + hass_dev_id = dev_ids.get(zigbee.mac_address) + if hass_dev_id is None: + # can be safely ignored, this device does not exist in current config + LOGGER.debug( + "Ignoring device %s (%s) as it does not (yet) exist in the device registry", + hue_dev.metadata.name, + hue_dev.id, ) - if matched_dev_class is None: + continue + dev_reg.async_update_device( + hass_dev_id, new_identifiers={(DOMAIN, hue_dev.id)} + ) + LOGGER.info("Migrated device %s (%s)", hue_dev.metadata.name, hass_dev_id) + + # loop through all entities for device and find match + for ent in async_entries_for_device(ent_reg, hass_dev_id, True): + + if ent.entity_id.startswith("light"): + # migrate light + # should always return one lightid here + new_unique_id = next(iter(hue_dev.lights), None) + else: + # migrate sensors + matched_dev_class = sensor_class_mapping.get( + ent.original_device_class or "unknown" + ) + new_unique_id = next( + ( + sensor.id + for sensor in api.devices.get_sensors(hue_dev.id) + if sensor.type == matched_dev_class + ), + None, + ) + + if new_unique_id is None: # this may happen if we're looking at orphaned or unsupported entity LOGGER.warning( "Skip migration of %s because it no longer exists on the bridge", ent.entity_id, ) continue - for sensor in api.devices.get_sensors(hue_dev.id): - if sensor.type != matched_dev_class: - continue - new_unique_id = sensor.id - if ent.unique_id == new_unique_id: - break # just in case + + try: + ent_reg.async_update_entity( + ent.entity_id, new_unique_id=new_unique_id + ) + except ValueError: + # assume edge case where the entity was already migrated in a previous run + # which got aborted somehow and we do not want + # to crash the entire integration init + LOGGER.warning( + "Skip migration of %s because it already exists", + ent.entity_id, + ) + else: LOGGER.info( - "Migrating %s from unique id %s to %s", + "Migrated entity %s from unique id %s to %s", ent.entity_id, ent.unique_id, new_unique_id, ) - try: - ent_reg.async_update_entity( - ent.entity_id, new_unique_id=sensor.id - ) - except ValueError: - # assume edge case where the entity was already migrated in a previous run - # which got aborted somehow and we do not want - # to crash the entire integration init - LOGGER.warning( - "Skip migration of %s because it already exists", - ent.entity_id, - ) - break # migrate entities that are not connected to a device (groups) for ent in entities_for_config_entry(ent_reg, entry.entry_id): if ent.device_id is not None: continue - v1_id = f"/groups/{ent.unique_id}" - hue_group = api.groups.room.get_by_v1_id(v1_id) - if hue_group is None or hue_group.grouped_light is None: - # try again with zone - hue_group = api.groups.zone.get_by_v1_id(v1_id) + if "-" in ent.unique_id: + # handle case where unique id is v2-id of group/zone + hue_group = api.groups.get(ent.unique_id) + else: + # handle case where the unique id is just the v1 id + v1_id = f"/groups/{ent.unique_id}" + hue_group = api.groups.room.get_by_v1_id( + v1_id + ) or api.groups.zone.get_by_v1_id(v1_id) if hue_group is None or hue_group.grouped_light is None: # this may happen if we're looking at some orphaned entity LOGGER.warning( diff --git a/tests/components/hue/test_migration.py b/tests/components/hue/test_migration.py index 8457ed04170..2dc1636d485 100644 --- a/tests/components/hue/test_migration.py +++ b/tests/components/hue/test_migration.py @@ -54,12 +54,12 @@ async def test_light_entity_migration( # create device/entity with V1 schema in registry device = dev_reg.async_get_or_create( config_entry_id=config_entry.entry_id, - identifiers={(hue.DOMAIN, "00:17:88:01:09:aa:bb:65")}, + identifiers={(hue.DOMAIN, "00:17:88:01:09:aa:bb:65-0b")}, ) ent_reg.async_get_or_create( "light", hue.DOMAIN, - "00:17:88:01:09:aa:bb:65", + "00:17:88:01:09:aa:bb:65-0b", suggested_object_id="migrated_light_1", device_id=device.id, ) @@ -74,14 +74,13 @@ async def test_light_entity_migration( ): await hue.migration.handle_v2_migration(hass, config_entry) - # migrated device should have new identifier (guid) and old style (mac) + # migrated device should now have the new identifier (guid) instead of old style (mac) migrated_device = dev_reg.async_get(device.id) assert migrated_device is not None assert migrated_device.identifiers == { - (hue.DOMAIN, "0b216218-d811-4c95-8c55-bbcda50f9d50"), - (hue.DOMAIN, "00:17:88:01:09:aa:bb:65"), + (hue.DOMAIN, "0b216218-d811-4c95-8c55-bbcda50f9d50") } - # the entity should have the new identifier (guid) + # the entity should have the new unique_id (guid) migrated_entity = ent_reg.async_get("light.migrated_light_1") assert migrated_entity is not None assert migrated_entity.unique_id == "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1" @@ -131,14 +130,13 @@ async def test_sensor_entity_migration( ): await hue.migration.handle_v2_migration(hass, config_entry) - # migrated device should have new identifier (guid) and old style (mac) + # migrated device should now have the new identifier (guid) instead of old style (mac) migrated_device = dev_reg.async_get(device.id) assert migrated_device is not None assert migrated_device.identifiers == { - (hue.DOMAIN, "2330b45d-6079-4c6e-bba6-1b68afb1a0d6"), - (hue.DOMAIN, device_mac), + (hue.DOMAIN, "2330b45d-6079-4c6e-bba6-1b68afb1a0d6") } - # the entities should have the correct V2 identifier (guid) + # the entities should have the correct V2 unique_id (guid) for dev_class, platform, new_id in sensor_mappings: migrated_entity = ent_reg.async_get( f"{platform}.hue_migrated_{dev_class}_sensor" @@ -147,7 +145,7 @@ async def test_sensor_entity_migration( assert migrated_entity.unique_id == new_id -async def test_group_entity_migration( +async def test_group_entity_migration_with_v1_id( hass, mock_bridge_v2, mock_config_entry_v2, v2_resources_test_data ): """Test if entity schema for grouped_lights migrates from v1 to v2.""" @@ -156,6 +154,7 @@ async def test_group_entity_migration( ent_reg = er.async_get(hass) # create (deviceless) entity with V1 schema in registry + # using the legacy style group id as unique id ent_reg.async_get_or_create( "light", hue.DOMAIN, @@ -177,3 +176,36 @@ async def test_group_entity_migration( migrated_entity = ent_reg.async_get("light.hue_migrated_grouped_light") assert migrated_entity is not None assert migrated_entity.unique_id == "e937f8db-2f0e-49a0-936e-027e60e15b34" + + +async def test_group_entity_migration_with_v2_group_id( + hass, mock_bridge_v2, mock_config_entry_v2, v2_resources_test_data +): + """Test if entity schema for grouped_lights migrates from v1 to v2.""" + config_entry = mock_bridge_v2.config_entry = mock_config_entry_v2 + + ent_reg = er.async_get(hass) + + # create (deviceless) entity with V1 schema in registry + # using the V2 group id as unique id + ent_reg.async_get_or_create( + "light", + hue.DOMAIN, + "6ddc9066-7e7d-4a03-a773-c73937968296", + suggested_object_id="hue_migrated_grouped_light", + config_entry=config_entry, + ) + + # now run the migration and check results + await mock_bridge_v2.api.load_test_data(v2_resources_test_data) + await hass.async_block_till_done() + with patch( + "homeassistant.components.hue.migration.HueBridgeV2", + return_value=mock_bridge_v2.api, + ): + await hue.migration.handle_v2_migration(hass, config_entry) + + # the entity should have the new identifier (guid) + migrated_entity = ent_reg.async_get("light.hue_migrated_grouped_light") + assert migrated_entity is not None + assert migrated_entity.unique_id == "e937f8db-2f0e-49a0-936e-027e60e15b34" From 400b7a22bd781f4deeb28e2723eb6b886eb0f1a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20W?= Date: Mon, 6 Dec 2021 17:49:47 +0100 Subject: [PATCH 0079/2644] Add media player volume control in `fr-FR` with Alexa (#60489) * media player volume control in `fr-FR` with Alexa * Apply suggestions from code review Co-authored-by: Erik Montnemery --- homeassistant/components/alexa/capabilities.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index ea8a1ed8681..0182d2aa085 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -695,6 +695,7 @@ class AlexaSpeaker(AlexaCapability): "en-US", "es-ES", "es-MX", + "fr-FR", # Not documented as of 2021-12-04, see PR #60489 "it-IT", "ja-JP", } @@ -752,6 +753,7 @@ class AlexaStepSpeaker(AlexaCapability): "en-IN", "en-US", "es-ES", + "fr-FR", # Not documented as of 2021-12-04, see PR #60489 "it-IT", } From da5374614f49256146fe6f094170ff3b24d97053 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 6 Dec 2021 18:01:46 +0100 Subject: [PATCH 0080/2644] Improve code quality trafikverket_weatherstation (#61044) * Code quality trafikverket_weatherstation * Updates from review * Fix extra attributes settings * Fix for additional review comments --- .../trafikverket_weatherstation/sensor.py | 60 ++++++++++++------- 1 file changed, 39 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/trafikverket_weatherstation/sensor.py b/homeassistant/components/trafikverket_weatherstation/sensor.py index 46fa3d9a5bd..01b70d5d3c7 100644 --- a/homeassistant/components/trafikverket_weatherstation/sensor.py +++ b/homeassistant/components/trafikverket_weatherstation/sensor.py @@ -5,13 +5,15 @@ import asyncio from dataclasses import dataclass from datetime import timedelta import logging +from typing import Any import aiohttp -from pytrafikverket.trafikverket_weather import TrafikverketWeather +from pytrafikverket.trafikverket_weather import TrafikverketWeather, WeatherStationInfo import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + STATE_CLASS_MEASUREMENT, SensorEntity, SensorEntityDescription, ) @@ -70,6 +72,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( native_unit_of_measurement=TEMP_CELSIUS, icon="mdi:thermometer", device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), TrafikverketSensorEntityDescription( key="road_temp", @@ -78,12 +81,14 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( native_unit_of_measurement=TEMP_CELSIUS, icon="mdi:thermometer", device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), TrafikverketSensorEntityDescription( key="precipitation", api_key="precipitationtype", name="Precipitation type", icon="mdi:weather-snowy-rainy", + entity_registry_enabled_default=False, ), TrafikverketSensorEntityDescription( key="wind_direction", @@ -91,6 +96,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( name="Wind direction", native_unit_of_measurement=DEGREE, icon="mdi:flag-triangle", + state_class=STATE_CLASS_MEASUREMENT, ), TrafikverketSensorEntityDescription( key="wind_direction_text", @@ -104,6 +110,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( name="Wind speed", native_unit_of_measurement=SPEED_METERS_PER_SECOND, icon="mdi:weather-windy", + state_class=STATE_CLASS_MEASUREMENT, ), TrafikverketSensorEntityDescription( key="wind_speed_max", @@ -111,6 +118,8 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( name="Wind speed max", native_unit_of_measurement=SPEED_METERS_PER_SECOND, icon="mdi:weather-windy-variant", + entity_registry_enabled_default=False, + state_class=STATE_CLASS_MEASUREMENT, ), TrafikverketSensorEntityDescription( key="humidity", @@ -119,6 +128,8 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, icon="mdi:water-percent", device_class=DEVICE_CLASS_HUMIDITY, + entity_registry_enabled_default=False, + state_class=STATE_CLASS_MEASUREMENT, ), TrafikverketSensorEntityDescription( key="precipitation_amount", @@ -126,18 +137,20 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( name="Precipitation amount", native_unit_of_measurement=LENGTH_MILLIMETERS, icon="mdi:cup-water", + state_class=STATE_CLASS_MEASUREMENT, ), TrafikverketSensorEntityDescription( key="precipitation_amountname", api_key="precipitation_amountname", name="Precipitation name", icon="mdi:weather-pouring", + entity_registry_enabled_default=False, ), ) SENSOR_KEYS = [desc.key for desc in SENSOR_TYPES] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_NAME): cv.string, vol.Required(CONF_API_KEY): cv.string, @@ -172,17 +185,12 @@ async def async_setup_entry( ) -> None: """Set up the Trafikverket sensor entry.""" - sensor_name = entry.data[CONF_STATION] - sensor_api = entry.data[CONF_API_KEY] - sensor_station = entry.data[CONF_STATION] - web_session = async_get_clientsession(hass) - - weather_api = TrafikverketWeather(web_session, sensor_api) + weather_api = TrafikverketWeather(web_session, entry.data[CONF_API_KEY]) entities = [ TrafikverketWeatherStation( - weather_api, sensor_name, sensor_station, description + weather_api, entry.entry_id, entry.data[CONF_STATION], description ) for description in SENSOR_TYPES ] @@ -197,29 +205,36 @@ class TrafikverketWeatherStation(SensorEntity): def __init__( self, - weather_api, - name, - sensor_station, + weather_api: TrafikverketWeather, + entry_id: str, + sensor_station: str, description: TrafikverketSensorEntityDescription, - ): + ) -> None: """Initialize the sensor.""" self.entity_description = description - self._attr_name = f"{name} {description.name}" + self._attr_name = f"{sensor_station} {description.name}" + self._attr_unique_id = f"{entry_id}_{description.key}" self._station = sensor_station self._weather_api = weather_api - self._weather = None + self._weather: WeatherStationInfo | None = None + self._active: bool | None = None + self._measure_time: str | None = None @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of Trafikverket Weatherstation.""" - return { + _additional_attributes: dict[str, Any] = { ATTR_ATTRIBUTION: ATTRIBUTION, - ATTR_ACTIVE: self._weather.active, - ATTR_MEASURE_TIME: self._weather.measure_time, } + if self._active: + _additional_attributes[ATTR_ACTIVE] = self._active + if self._measure_time: + _additional_attributes[ATTR_MEASURE_TIME] = self._measure_time + + return _additional_attributes @Throttle(MIN_TIME_BETWEEN_UPDATES) - async def async_update(self): + async def async_update(self) -> None: """Get the latest data from Trafikverket and updates the states.""" try: self._weather = await self._weather_api.async_get_weather(self._station) @@ -228,3 +243,6 @@ class TrafikverketWeatherStation(SensorEntity): ) except (asyncio.TimeoutError, aiohttp.ClientError, ValueError) as error: _LOGGER.error("Could not fetch weather data: %s", error) + return + self._active = self._weather.active + self._measure_time = self._weather.measure_time From f94085c83eba572c5f4a8b9943ebd7e5d3997e14 Mon Sep 17 00:00:00 2001 From: micha91 Date: Mon, 6 Dec 2021 18:05:49 +0100 Subject: [PATCH 0081/2644] Add Yamaha MusicCast Select Entities (#60645) * Add select entity for Yamaha MusicCast Capabilities * Add musiccast select to .coveragerc * Move status strings to string.select.json and auto generate the english translations from it. Let the device class start with yamaha_musiccast__. * Make all device classes lower case * Use platform enum to add select --- .coveragerc | 1 + .../components/yamaha_musiccast/__init__.py | 2 +- .../components/yamaha_musiccast/select.py | 63 +++++++++++++++++++ .../yamaha_musiccast/strings.select.json | 52 +++++++++++++++ .../translations/select.en.json | 52 +++++++++++++++ 5 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/yamaha_musiccast/select.py create mode 100644 homeassistant/components/yamaha_musiccast/strings.select.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/select.en.json diff --git a/.coveragerc b/.coveragerc index 92a2638dee5..0f4c8d68d22 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1293,6 +1293,7 @@ omit = homeassistant/components/yamaha_musiccast/__init__.py homeassistant/components/yamaha_musiccast/media_player.py homeassistant/components/yamaha_musiccast/number.py + homeassistant/components/yamaha_musiccast/select.py homeassistant/components/yandex_transport/* homeassistant/components/yeelightsunflower/light.py homeassistant/components/yi/camera.py diff --git a/homeassistant/components/yamaha_musiccast/__init__.py b/homeassistant/components/yamaha_musiccast/__init__.py index 142af2d769b..98fed4d0f63 100644 --- a/homeassistant/components/yamaha_musiccast/__init__.py +++ b/homeassistant/components/yamaha_musiccast/__init__.py @@ -30,7 +30,7 @@ from .const import ( ENTITY_CATEGORY_MAPPING, ) -PLATFORMS = [Platform.MEDIA_PLAYER, Platform.NUMBER] +PLATFORMS = [Platform.MEDIA_PLAYER, Platform.NUMBER, Platform.SELECT] _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=60) diff --git a/homeassistant/components/yamaha_musiccast/select.py b/homeassistant/components/yamaha_musiccast/select.py new file mode 100644 index 00000000000..6c808c3cced --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/select.py @@ -0,0 +1,63 @@ +"""The select entities for musiccast.""" + +from aiomusiccast.capabilities import OptionSetter + +from homeassistant.components.select import SelectEntity +from homeassistant.components.yamaha_musiccast import ( + DOMAIN, + MusicCastCapabilityEntity, + MusicCastDataUpdateCoordinator, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up MusicCast select entities based on a config entry.""" + coordinator: MusicCastDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + select_entities = [] + + for capability in coordinator.data.capabilities: + if isinstance(capability, OptionSetter): + select_entities.append(SelectableCapapility(coordinator, capability)) + + for zone, data in coordinator.data.zones.items(): + for capability in data.capabilities: + if isinstance(capability, OptionSetter): + select_entities.append( + SelectableCapapility(coordinator, capability, zone) + ) + + async_add_entities(select_entities) + + +class SelectableCapapility(MusicCastCapabilityEntity, SelectEntity): + """Representation of a MusicCast Select entity.""" + + capability: OptionSetter + + async def async_select_option(self, option: str) -> None: + """Select the given option.""" + value = {val: key for key, val in self.capability.options.items()}[option] + await self.capability.set(value) + + @property + def device_class(self) -> str: + """Return the ID of the capability, to identify the entity for translations.""" + return f"{DOMAIN}__{self.capability.id.lower()}" + + @property + def options(self): + """Return the list possible options.""" + return list(self.capability.options.values()) + + @property + def current_option(self): + """Return the currently selected option.""" + return self.capability.options[self.capability.current] diff --git a/homeassistant/components/yamaha_musiccast/strings.select.json b/homeassistant/components/yamaha_musiccast/strings.select.json new file mode 100644 index 00000000000..59c763017bf --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/strings.select.json @@ -0,0 +1,52 @@ +{ + "state": { + "yamaha_musiccast__dimmer": { + "auto": "Auto" + }, + "yamaha_musiccast__zone_sleep": { + "off": "Off", + "30 min": "30 Minutes", + "60 min": "60 Minutes", + "90 min": "90 Minutes", + "120 min": "120 Minutes" + }, + "yamaha_musiccast__zone_tone_control_mode": { + "manual": "Manual", + "auto": "Auto", + "bypass": "Bypass" + }, + "yamaha_musiccast__zone_surr_decoder_type": { + "toggle": "Toggle", + "auto": "Auto", + "dolby_pl": "Dolby ProLogic", + "dolby_pl2x_movie": "Dolby ProLogic 2x Movie", + "dolby_pl2x_music": "Dolby ProLogic 2x Music", + "dolby_pl2x_game": "Dolby ProLogic 2x Game", + "dolby_surround": "Dolby Surround", + "dts_neural_x": "DTS Neural:X", + "dts_neo6_cinema": "DTS Neo:6 Cinema", + "dts_neo6_music": "DTS Neo:6 Music" + }, + "yamaha_musiccast__zone_equalizer_mode": { + "manual": "Manual", + "auto": "Auto", + "bypass": "Bypass" + }, + "yamaha_musiccast__zone_link_audio_quality": { + "compressed": "Compressed", + "uncompressed": "Uncompressed" + }, + "yamaha_musiccast__zone_link_control": { + "standard": "Standard", + "speed": "Speed", + "stability": "Stability" + }, + "yamaha_musiccast__zone_link_audio_delay": { + "audio_sync_on": "Audio Synchronization On", + "audio_sync_off": "Audio Synchronization Off", + "balanced": "Balanced", + "lip_sync": "Lip Synchronization", + "audio_sync": "Audio Synchronization" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/select.en.json b/homeassistant/components/yamaha_musiccast/translations/select.en.json new file mode 100644 index 00000000000..9f0fc18cd4c --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/select.en.json @@ -0,0 +1,52 @@ +{ + "state": { + "yamaha_musiccast__dimmer": { + "auto": "Auto" + }, + "yamaha_musiccast__zone_equalizer_mode": { + "auto": "Auto", + "bypass": "Bypass", + "manual": "Manual" + }, + "yamaha_musiccast__zone_link_audio_delay": { + "audio_sync": "Audio Synchronization", + "audio_sync_off": "Audio Synchronization Off", + "audio_sync_on": "Audio Synchronization On", + "balanced": "Balanced", + "lip_sync": "Lip Synchronization" + }, + "yamaha_musiccast__zone_link_audio_quality": { + "compressed": "Compressed", + "uncompressed": "Uncompressed" + }, + "yamaha_musiccast__zone_link_control": { + "speed": "Speed", + "stability": "Stability", + "standard": "Standard" + }, + "yamaha_musiccast__zone_sleep": { + "120 min": "120 Minutes", + "30 min": "30 Minutes", + "60 min": "60 Minutes", + "90 min": "90 Minutes", + "off": "Off" + }, + "yamaha_musiccast__zone_surr_decoder_type": { + "auto": "Auto", + "dolby_pl": "Dolby ProLogic", + "dolby_pl2x_game": "Dolby ProLogic 2x Game", + "dolby_pl2x_movie": "Dolby ProLogic 2x Movie", + "dolby_pl2x_music": "Dolby ProLogic 2x Music", + "dolby_surround": "Dolby Surround", + "dts_neo6_cinema": "DTS Neo:6 Cinema", + "dts_neo6_music": "DTS Neo:6 Music", + "dts_neural_x": "DTS Neural:X", + "toggle": "Toggle" + }, + "yamaha_musiccast__zone_tone_control_mode": { + "auto": "Auto", + "bypass": "Bypass", + "manual": "Manual" + } + } +} \ No newline at end of file From 10a423e01ad22d724e1ff970a8d0c04231cded74 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 6 Dec 2021 09:46:17 -0800 Subject: [PATCH 0082/2644] Bump aiohue to 3.0.2 (#61115) --- homeassistant/components/hue/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index 02734df1481..c789755c9a3 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==3.0.1"], + "requirements": ["aiohue==3.0.2"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 88a3faf9c6d..b8b3e13a3c1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -189,7 +189,7 @@ aiohomekit==0.6.4 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.1 +aiohue==3.0.2 # homeassistant.components.imap aioimaplib==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 150fd0b4d28..7fc449bea41 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -134,7 +134,7 @@ aiohomekit==0.6.4 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.1 +aiohue==3.0.2 # homeassistant.components.apache_kafka aiokafka==0.6.0 From e33384d8b95aae530d7dcf1151b3ea691f3feffb Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 6 Dec 2021 18:55:28 +0100 Subject: [PATCH 0083/2644] Fix CO2 calculation when data is missing (#61106) --- .../components/energy/websocket_api.py | 2 + tests/components/energy/test_websocket_api.py | 193 ++++++++++++++++++ 2 files changed, 195 insertions(+) diff --git a/homeassistant/components/energy/websocket_api.py b/homeassistant/components/energy/websocket_api.py index e15713ff8ad..cdc7599b55b 100644 --- a/homeassistant/components/energy/websocket_api.py +++ b/homeassistant/components/energy/websocket_api.py @@ -303,6 +303,8 @@ async def ws_get_fossil_energy_consumption( """Reduce hourly deltas to daily or monthly deltas.""" result: list[dict[str, Any]] = [] deltas: list[float] = [] + if not stat_list: + return result prev_stat: dict[str, Any] = stat_list[0] # Loop over the hourly deltas + a fake entry to end the period diff --git a/tests/components/energy/test_websocket_api.py b/tests/components/energy/test_websocket_api.py index f86e43dd1b2..46c6a5c0fa6 100644 --- a/tests/components/energy/test_websocket_api.py +++ b/tests/components/energy/test_websocket_api.py @@ -472,8 +472,10 @@ async def test_fossil_energy_consumption_hole(hass, hass_ws_client): period1 = dt_util.as_utc(dt_util.parse_datetime("2021-09-01 00:00:00")) period2 = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 23:00:00")) + period2_day_start = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 00:00:00")) period3 = dt_util.as_utc(dt_util.parse_datetime("2021-10-01 00:00:00")) period4 = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 23:00:00")) + period4_day_start = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 00:00:00")) external_energy_statistics_1 = ( { @@ -575,6 +577,197 @@ async def test_fossil_energy_consumption_hole(hass, hass_ws_client): period4.isoformat(): pytest.approx(88.0 - 55.0), } + await client.send_json( + { + "id": 2, + "type": "energy/fossil_energy_consumption", + "start_time": now.isoformat(), + "end_time": later.isoformat(), + "energy_statistic_ids": [ + "test:total_energy_import_tariff_1", + "test:total_energy_import_tariff_2", + ], + "co2_statistic_id": "test:co2_ratio_missing", + "period": "day", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + period2_day_start.isoformat(): pytest.approx(3.0 - 20.0), + period3.isoformat(): pytest.approx(55.0 - 3.0), + period4_day_start.isoformat(): pytest.approx(88.0 - 55.0), + } + + await client.send_json( + { + "id": 3, + "type": "energy/fossil_energy_consumption", + "start_time": now.isoformat(), + "end_time": later.isoformat(), + "energy_statistic_ids": [ + "test:total_energy_import_tariff_1", + "test:total_energy_import_tariff_2", + ], + "co2_statistic_id": "test:co2_ratio_missing", + "period": "month", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + period1.isoformat(): pytest.approx(3.0 - 20.0), + period3.isoformat(): pytest.approx((55.0 - 3.0) + (88.0 - 55.0)), + } + + +@pytest.mark.freeze_time("2021-08-01 00:00:00+00:00") +async def test_fossil_energy_consumption_no_data(hass, hass_ws_client): + """Test fossil_energy_consumption when there is no data.""" + now = dt_util.utcnow() + later = dt_util.as_utc(dt_util.parse_datetime("2022-09-01 00:00:00")) + + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "history", {}) + await async_setup_component(hass, "sensor", {}) + + period1 = dt_util.as_utc(dt_util.parse_datetime("2021-09-01 00:00:00")) + period2 = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 23:00:00")) + period3 = dt_util.as_utc(dt_util.parse_datetime("2021-10-01 00:00:00")) + period4 = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 23:00:00")) + + external_energy_statistics_1 = ( + { + "start": period1, + "last_reset": None, + "state": 0, + "sum": None, + }, + { + "start": period2, + "last_reset": None, + "state": 1, + "sum": 3, + }, + { + "start": period3, + "last_reset": None, + "state": 2, + "sum": 5, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 8, + }, + ) + external_energy_metadata_1 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_1", + "unit_of_measurement": "kWh", + } + external_energy_statistics_2 = ( + { + "start": period1, + "last_reset": None, + "state": 0, + "sum": 20, + }, + { + "start": period2, + "last_reset": None, + "state": 1, + "sum": None, + }, + { + "start": period3, + "last_reset": None, + "state": 2, + "sum": 50, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 80, + }, + ) + external_energy_metadata_2 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_2", + "unit_of_measurement": "kWh", + } + + async_add_external_statistics( + hass, external_energy_metadata_1, external_energy_statistics_1 + ) + async_add_external_statistics( + hass, external_energy_metadata_2, external_energy_statistics_2 + ) + await async_wait_recording_done_without_instance(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "energy/fossil_energy_consumption", + "start_time": now.isoformat(), + "end_time": later.isoformat(), + "energy_statistic_ids": [ + "test:total_energy_import_tariff_1_missing", + "test:total_energy_import_tariff_2_missing", + ], + "co2_statistic_id": "test:co2_ratio_missing", + "period": "hour", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {} + + await client.send_json( + { + "id": 2, + "type": "energy/fossil_energy_consumption", + "start_time": now.isoformat(), + "end_time": later.isoformat(), + "energy_statistic_ids": [ + "test:total_energy_import_tariff_1_missing", + "test:total_energy_import_tariff_2_missing", + ], + "co2_statistic_id": "test:co2_ratio_missing", + "period": "day", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {} + + await client.send_json( + { + "id": 3, + "type": "energy/fossil_energy_consumption", + "start_time": now.isoformat(), + "end_time": later.isoformat(), + "energy_statistic_ids": [ + "test:total_energy_import_tariff_1_missing", + "test:total_energy_import_tariff_2_missing", + ], + "co2_statistic_id": "test:co2_ratio_missing", + "period": "month", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {} + @pytest.mark.freeze_time("2021-08-01 00:00:00+00:00") async def test_fossil_energy_consumption(hass, hass_ws_client): From b6dc89b4b79d0ba7e7b267af54a09f5f01ffca02 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 6 Dec 2021 18:56:46 +0100 Subject: [PATCH 0084/2644] Add max/min/step to NumberEntityDescription (#61100) Co-authored-by: epenet --- homeassistant/components/deconz/number.py | 6 ---- homeassistant/components/number/__init__.py | 31 ++++++++++++++++--- homeassistant/components/template/number.py | 2 ++ homeassistant/components/wallbox/number.py | 3 -- homeassistant/components/wled/number.py | 10 +++--- .../components/xiaomi_miio/number.py | 6 ---- 6 files changed, 35 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/deconz/number.py b/homeassistant/components/deconz/number.py index 6c3ca08710c..300a10f9a27 100644 --- a/homeassistant/components/deconz/number.py +++ b/homeassistant/components/deconz/number.py @@ -29,9 +29,6 @@ class DeconzNumberEntityDescriptionBase: device_property: str suffix: str update_key: str - max_value: int - min_value: int - step: int @dataclass @@ -122,9 +119,6 @@ class DeconzNumber(DeconzDevice, NumberEntity): super().__init__(device, gateway) self._attr_name = f"{device.name} {description.suffix}" - self._attr_max_value = description.max_value - self._attr_min_value = description.min_value - self._attr_step = description.step @callback def async_update_callback(self) -> None: diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index af88b5c86b5..47a80f00561 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -91,13 +91,17 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class NumberEntityDescription(EntityDescription): """A class that describes number entities.""" + max_value: float | None = None + min_value: float | None = None + step: float | None = None + class NumberEntity(Entity): """Representation of a Number entity.""" entity_description: NumberEntityDescription - _attr_max_value: float = DEFAULT_MAX_VALUE - _attr_min_value: float = DEFAULT_MIN_VALUE + _attr_max_value: float + _attr_min_value: float _attr_state: None = None _attr_step: float _attr_mode: NumberMode = NumberMode.AUTO @@ -116,18 +120,37 @@ class NumberEntity(Entity): @property def min_value(self) -> float: """Return the minimum value.""" - return self._attr_min_value + if hasattr(self, "_attr_min_value"): + return self._attr_min_value + if ( + hasattr(self, "entity_description") + and self.entity_description.min_value is not None + ): + return self.entity_description.min_value + return DEFAULT_MIN_VALUE @property def max_value(self) -> float: """Return the maximum value.""" - return self._attr_max_value + if hasattr(self, "_attr_max_value"): + return self._attr_max_value + if ( + hasattr(self, "entity_description") + and self.entity_description.max_value is not None + ): + return self.entity_description.max_value + return DEFAULT_MAX_VALUE @property def step(self) -> float: """Return the increment/decrement step.""" if hasattr(self, "_attr_step"): return self._attr_step + if ( + hasattr(self, "entity_description") + and self.entity_description.step is not None + ): + return self.entity_description.step step = DEFAULT_STEP value_range = abs(self.max_value - self.min_value) if value_range != 0: diff --git a/homeassistant/components/template/number.py b/homeassistant/components/template/number.py index 0f5e8e4bee8..90a944a2d33 100644 --- a/homeassistant/components/template/number.py +++ b/homeassistant/components/template/number.py @@ -149,6 +149,8 @@ class TemplateNumber(TemplateEntity, NumberEntity): self._attr_unique_id = unique_id self._attr_value = None self._attr_step = None + self._attr_min_value = None + self._attr_max_value = None async def async_added_to_hass(self) -> None: """Register callbacks.""" diff --git a/homeassistant/components/wallbox/number.py b/homeassistant/components/wallbox/number.py index 64c1f1e1abb..d8a677c147f 100644 --- a/homeassistant/components/wallbox/number.py +++ b/homeassistant/components/wallbox/number.py @@ -19,8 +19,6 @@ from .const import CONF_MAX_AVAILABLE_POWER_KEY, CONF_MAX_CHARGING_CURRENT_KEY, class WallboxNumberEntityDescription(NumberEntityDescription): """Describes Wallbox sensor entity.""" - min_value: float = 0 - NUMBER_TYPES: dict[str, WallboxNumberEntityDescription] = { CONF_MAX_CHARGING_CURRENT_KEY: WallboxNumberEntityDescription( @@ -71,7 +69,6 @@ class WallboxNumber(CoordinatorEntity, NumberEntity): self.entity_description = description self._coordinator = coordinator self._attr_name = f"{entry.title} {description.name}" - self._attr_min_value = description.min_value @property def max_value(self) -> float: diff --git a/homeassistant/components/wled/number.py b/homeassistant/components/wled/number.py index 5167ee8a37a..48c92cf839c 100644 --- a/homeassistant/components/wled/number.py +++ b/homeassistant/components/wled/number.py @@ -41,11 +41,17 @@ NUMBERS = [ name="Speed", icon="mdi:speedometer", entity_category=ENTITY_CATEGORY_CONFIG, + step=1, + min_value=0, + max_value=255, ), NumberEntityDescription( key=ATTR_INTENSITY, name="Intensity", entity_category=ENTITY_CATEGORY_CONFIG, + step=1, + min_value=0, + max_value=255, ), ] @@ -53,10 +59,6 @@ NUMBERS = [ class WLEDNumber(WLEDEntity, NumberEntity): """Defines a WLED speed number.""" - _attr_step = 1 - _attr_min_value = 0 - _attr_max_value = 255 - def __init__( self, coordinator: WLEDDataUpdateCoordinator, diff --git a/homeassistant/components/xiaomi_miio/number.py b/homeassistant/components/xiaomi_miio/number.py index 2823c6c2582..f8516c66e84 100644 --- a/homeassistant/components/xiaomi_miio/number.py +++ b/homeassistant/components/xiaomi_miio/number.py @@ -82,9 +82,6 @@ ATTR_VOLUME = "volume" class XiaomiMiioNumberDescription(NumberEntityDescription): """A class that describes number entities.""" - min_value: float | None = None - max_value: float | None = None - step: float | None = None available_with_device_off: bool = True method: str | None = None @@ -278,9 +275,6 @@ class XiaomiNumberEntity(XiaomiCoordinatedMiioEntity, NumberEntity): """Initialize the generic Xiaomi attribute selector.""" super().__init__(name, device, entry, unique_id, coordinator) - self._attr_min_value = description.min_value - self._attr_max_value = description.max_value - self._attr_step = description.step self._attr_value = self._extract_value_from_attribute( coordinator.data, description.key ) From d802f3a82f84b040b03b4d6566f2ab9709aaa837 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 6 Dec 2021 19:01:12 +0100 Subject: [PATCH 0085/2644] Add Open-Meteo integration (#60379) Co-authored-by: MatthewFlamm <39341281+MatthewFlamm@users.noreply.github.com> --- .coveragerc | 1 + .pre-commit-config.yaml | 2 +- .strict-typing | 1 + CODEOWNERS | 1 + .../components/open_meteo/__init__.py | 56 + .../components/open_meteo/config_flow.py | 54 + homeassistant/components/open_meteo/const.py | 56 + .../components/open_meteo/manifest.json | 10 + .../components/open_meteo/strings.json | 12 + .../open_meteo/translations/en.json | 12 + .../components/open_meteo/weather.py | 80 + homeassistant/generated/config_flows.py | 1 + mypy.ini | 11 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/open_meteo/__init__.py | 1 + tests/components/open_meteo/conftest.py | 49 + .../open_meteo/fixtures/forecast.json | 6660 +++++++++++++++++ .../components/open_meteo/test_config_flow.py | 33 + tests/components/open_meteo/test_init.py | 68 + 20 files changed, 7113 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/open_meteo/__init__.py create mode 100644 homeassistant/components/open_meteo/config_flow.py create mode 100644 homeassistant/components/open_meteo/const.py create mode 100644 homeassistant/components/open_meteo/manifest.json create mode 100644 homeassistant/components/open_meteo/strings.json create mode 100644 homeassistant/components/open_meteo/translations/en.json create mode 100644 homeassistant/components/open_meteo/weather.py create mode 100644 tests/components/open_meteo/__init__.py create mode 100644 tests/components/open_meteo/conftest.py create mode 100644 tests/components/open_meteo/fixtures/forecast.json create mode 100644 tests/components/open_meteo/test_config_flow.py create mode 100644 tests/components/open_meteo/test_init.py diff --git a/.coveragerc b/.coveragerc index 0f4c8d68d22..04d14121b52 100644 --- a/.coveragerc +++ b/.coveragerc @@ -767,6 +767,7 @@ omit = homeassistant/components/onvif/event.py homeassistant/components/onvif/parsers.py homeassistant/components/onvif/sensor.py + homeassistant/components/open_meteo/weather.py homeassistant/components/opencv/* homeassistant/components/openevse/sensor.py homeassistant/components/openexchangerates/sensor.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 91216c9efa9..17581c68c80 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: hooks: - id: codespell args: - - --ignore-words-list=hass,alot,datas,dof,dur,ether,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing,iam,incomfort,ba,haa + - --ignore-words-list=hass,alot,datas,dof,dur,ether,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,rime,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing,iam,incomfort,ba,haa - --skip="./.*,*.csv,*.json" - --quiet-level=2 exclude_types: [csv, json] diff --git a/.strict-typing b/.strict-typing index 5546086a456..f663ec6a6b9 100644 --- a/.strict-typing +++ b/.strict-typing @@ -95,6 +95,7 @@ homeassistant.components.notify.* homeassistant.components.notion.* homeassistant.components.number.* homeassistant.components.onewire.* +homeassistant.components.open_meteo.* homeassistant.components.openuv.* homeassistant.components.persistent_notification.* homeassistant.components.pi_hole.* diff --git a/CODEOWNERS b/CODEOWNERS index a349954bf65..fa59d00788b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -380,6 +380,7 @@ homeassistant/components/onboarding/* @home-assistant/core homeassistant/components/ondilo_ico/* @JeromeHXP homeassistant/components/onewire/* @garbled1 @epenet homeassistant/components/onvif/* @hunterjm +homeassistant/components/open_meteo/* @frenck homeassistant/components/openerz/* @misialq homeassistant/components/opengarage/* @danielhiversen homeassistant/components/openhome/* @bazwilliams diff --git a/homeassistant/components/open_meteo/__init__.py b/homeassistant/components/open_meteo/__init__.py new file mode 100644 index 00000000000..348f0616556 --- /dev/null +++ b/homeassistant/components/open_meteo/__init__.py @@ -0,0 +1,56 @@ +"""Support for Open-Meteo.""" +from __future__ import annotations + +from open_meteo import Forecast, OpenMeteo, OpenMeteoError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, CONF_ZONE, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN, LOGGER, SCAN_INTERVAL + +PLATFORMS = [Platform.WEATHER] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Open-Meteo from a config entry.""" + session = async_get_clientsession(hass) + open_meteo = OpenMeteo(session=session) + + async def async_update_forecast() -> Forecast: + zone = hass.states.get(entry.data[CONF_ZONE]) + if zone is None: + raise UpdateFailed(f"Zone '{entry.data[CONF_ZONE]}' not found") + + try: + return await open_meteo.forecast( + latitude=zone.attributes[ATTR_LATITUDE], + longitude=zone.attributes[ATTR_LONGITUDE], + current_weather=True, + ) + except OpenMeteoError as err: + raise UpdateFailed("Open-Meteo API communication error") from err + + coordinator: DataUpdateCoordinator[Forecast] = DataUpdateCoordinator( + hass, + LOGGER, + name=f"{DOMAIN}_{entry.data[CONF_ZONE]}", + update_interval=SCAN_INTERVAL, + update_method=async_update_forecast, + ) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload Open-Meteo config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + del hass.data[DOMAIN][entry.entry_id] + return unload_ok diff --git a/homeassistant/components/open_meteo/config_flow.py b/homeassistant/components/open_meteo/config_flow.py new file mode 100644 index 00000000000..76d5436f565 --- /dev/null +++ b/homeassistant/components/open_meteo/config_flow.py @@ -0,0 +1,54 @@ +"""Config flow to configure the Open-Meteo integration.""" +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN, ENTITY_ID_HOME +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_ZONE +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +class OpenMeteoFlowHandler(ConfigFlow, domain=DOMAIN): + """Config flow for OpenMeteo.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + if user_input is not None: + await self.async_set_unique_id(user_input[CONF_ZONE]) + self._abort_if_unique_id_configured() + + zone = self.hass.states.get(user_input[CONF_ZONE]) + return self.async_create_entry( + title=zone.name if zone else "Open-Meteo", + data={CONF_ZONE: user_input[CONF_ZONE]}, + ) + + zones: dict[str, str] = { + entity_id: state.name + for entity_id in self.hass.states.async_entity_ids(ZONE_DOMAIN) + if (state := self.hass.states.get(entity_id)) is not None + } + zones = dict(sorted(zones.items(), key=lambda x: x[1], reverse=True)) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_ZONE): vol.In( + { + ENTITY_ID_HOME: zones.pop(ENTITY_ID_HOME), + **zones, + } + ), + } + ), + ) diff --git a/homeassistant/components/open_meteo/const.py b/homeassistant/components/open_meteo/const.py new file mode 100644 index 00000000000..94e27293bad --- /dev/null +++ b/homeassistant/components/open_meteo/const.py @@ -0,0 +1,56 @@ +"""Constants for the Open-Meteo integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging +from typing import Final + +from homeassistant.components.weather import ( + ATTR_CONDITION_CLOUDY, + ATTR_CONDITION_FOG, + ATTR_CONDITION_LIGHTNING, + ATTR_CONDITION_PARTLYCLOUDY, + ATTR_CONDITION_POURING, + ATTR_CONDITION_RAINY, + ATTR_CONDITION_SNOWY, + ATTR_CONDITION_SUNNY, +) + +DOMAIN: Final = "open_meteo" + +LOGGER = logging.getLogger(__package__) +SCAN_INTERVAL = timedelta(minutes=30) + +# World Meteorological Organization Weather Code +# mapped to Home Assistant weather conditions. +# https://www.weather.gov/tg/wmo +WMO_TO_HA_CONDITION_MAP = { + 0: ATTR_CONDITION_SUNNY, # Clear sky + 1: ATTR_CONDITION_SUNNY, # Mainly clear + 2: ATTR_CONDITION_PARTLYCLOUDY, # Partly cloudy + 3: ATTR_CONDITION_CLOUDY, # Overcast + 45: ATTR_CONDITION_FOG, # Fog + 48: ATTR_CONDITION_FOG, # Depositing rime fog + 51: ATTR_CONDITION_RAINY, # Drizzle: Light intensity + 53: ATTR_CONDITION_RAINY, # Drizzle: Moderate intensity + 55: ATTR_CONDITION_RAINY, # Drizzle: Dense intensity + 56: ATTR_CONDITION_RAINY, # Freezing Drizzle: Light intensity + 57: ATTR_CONDITION_RAINY, # Freezing Drizzle: Dense intensity + 61: ATTR_CONDITION_RAINY, # Rain: Slight intensity + 63: ATTR_CONDITION_RAINY, # Rain: Moderate intensity + 65: ATTR_CONDITION_POURING, # Rain: Heavy intensity + 66: ATTR_CONDITION_RAINY, # Freezing Rain: Light intensity + 67: ATTR_CONDITION_POURING, # Freezing Rain: Heavy intensity + 71: ATTR_CONDITION_SNOWY, # Snow fall: Slight intensity + 73: ATTR_CONDITION_SNOWY, # Snow fall: Moderate intensity + 75: ATTR_CONDITION_SNOWY, # Snow fall: Heavy intensity + 77: ATTR_CONDITION_SNOWY, # Snow grains + 80: ATTR_CONDITION_RAINY, # Rain showers: Slight intensity + 81: ATTR_CONDITION_RAINY, # Rain showers: Moderate intensity + 82: ATTR_CONDITION_POURING, # Rain showers: Violent intensity + 85: ATTR_CONDITION_SNOWY, # Snow showers: Slight intensity + 86: ATTR_CONDITION_SNOWY, # Snow showers: Heavy intensity + 95: ATTR_CONDITION_LIGHTNING, # Thunderstorm: Slight and moderate intensity + 96: ATTR_CONDITION_LIGHTNING, # Thunderstorm with slight hail + 99: ATTR_CONDITION_LIGHTNING, # Thunderstorm with heavy hail +} diff --git a/homeassistant/components/open_meteo/manifest.json b/homeassistant/components/open_meteo/manifest.json new file mode 100644 index 00000000000..34d783387e3 --- /dev/null +++ b/homeassistant/components/open_meteo/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "open_meteo", + "name": "Open-Meteo", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/open_meteo", + "requirements": ["open-meteo==0.2.0"], + "dependencies": ["zone"], + "codeowners": ["@frenck"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/open_meteo/strings.json b/homeassistant/components/open_meteo/strings.json new file mode 100644 index 00000000000..f2f22413403 --- /dev/null +++ b/homeassistant/components/open_meteo/strings.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "description": "Select location to use for weather forecasting", + "data": { + "zone": "Zone" + } + } + } + } +} diff --git a/homeassistant/components/open_meteo/translations/en.json b/homeassistant/components/open_meteo/translations/en.json new file mode 100644 index 00000000000..7736b1da63e --- /dev/null +++ b/homeassistant/components/open_meteo/translations/en.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "zone": "Zone" + }, + "description": "Select location to use for weather forecasting" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/open_meteo/weather.py b/homeassistant/components/open_meteo/weather.py new file mode 100644 index 00000000000..b34de06efe5 --- /dev/null +++ b/homeassistant/components/open_meteo/weather.py @@ -0,0 +1,80 @@ +"""Support for Open-Meteo weather.""" +from __future__ import annotations + +from open_meteo import Forecast as OpenMeteoForecast + +from homeassistant.components.weather import WeatherEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import TEMP_CELSIUS +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import DOMAIN, WMO_TO_HA_CONDITION_MAP + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Open-Meteo weather entity based on a config entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities([OpenMeteoWeatherEntity(entry=entry, coordinator=coordinator)]) + + +class OpenMeteoWeatherEntity(CoordinatorEntity, WeatherEntity): + """Defines an Open-Meteo weather entity.""" + + _attr_temperature_unit = TEMP_CELSIUS + coordinator: DataUpdateCoordinator[OpenMeteoForecast] + + def __init__( + self, *, entry: ConfigEntry, coordinator: DataUpdateCoordinator + ) -> None: + """Initialize Open-Meteo weather entity.""" + super().__init__(coordinator=coordinator) + self._attr_unique_id = entry.entry_id + self._attr_name = entry.title + + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, entry.entry_id)}, + manufacturer="Open-Meteo", + name=entry.title, + ) + + @property + def condition(self) -> str | None: + """Return the current condition.""" + if not self.coordinator.data.current_weather: + return None + return WMO_TO_HA_CONDITION_MAP.get( + self.coordinator.data.current_weather.weather_code + ) + + @property + def temperature(self) -> float | None: + """Return the platform temperature.""" + if not self.coordinator.data.current_weather: + return None + return self.coordinator.data.current_weather.temperature + + @property + def wind_speed(self) -> float | None: + """Return the wind speed.""" + if not self.coordinator.data.current_weather: + return None + return self.coordinator.data.current_weather.wind_speed + + @property + def wind_bearing(self) -> float | str | None: + """Return the wind bearing.""" + if not self.coordinator.data.current_weather: + return None + return self.coordinator.data.current_weather.wind_direction diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 596cdf03fb7..b0f6b6e822a 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -216,6 +216,7 @@ FLOWS = [ "ondilo_ico", "onewire", "onvif", + "open_meteo", "opengarage", "opentherm_gw", "openuv", diff --git a/mypy.ini b/mypy.ini index 47450bab8ed..475840c3b4d 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1056,6 +1056,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.open_meteo.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.openuv.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index b8b3e13a3c1..89cb1dbe40a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1134,6 +1134,9 @@ onvif-zeep-async==1.2.0 # homeassistant.components.opengarage open-garage==0.2.0 +# homeassistant.components.open_meteo +open-meteo==0.2.0 + # homeassistant.components.opencv # opencv-python-headless==4.5.2.54 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7fc449bea41..09038d0303b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -699,6 +699,9 @@ onvif-zeep-async==1.2.0 # homeassistant.components.opengarage open-garage==0.2.0 +# homeassistant.components.open_meteo +open-meteo==0.2.0 + # homeassistant.components.openerz openerz-api==0.1.0 diff --git a/tests/components/open_meteo/__init__.py b/tests/components/open_meteo/__init__.py new file mode 100644 index 00000000000..11ac51700a8 --- /dev/null +++ b/tests/components/open_meteo/__init__.py @@ -0,0 +1 @@ +"""Tests for the Open-Meteo integration.""" diff --git a/tests/components/open_meteo/conftest.py b/tests/components/open_meteo/conftest.py new file mode 100644 index 00000000000..cb950dcc442 --- /dev/null +++ b/tests/components/open_meteo/conftest.py @@ -0,0 +1,49 @@ +"""Fixtures for the Open-Meteo integration tests.""" +from __future__ import annotations + +from collections.abc import Generator +from unittest.mock import MagicMock, patch + +from open_meteo import Forecast +import pytest + +from homeassistant.components.open_meteo.const import DOMAIN +from homeassistant.const import CONF_ZONE + +from tests.common import MockConfigEntry, load_fixture + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="Home", + domain=DOMAIN, + data={CONF_ZONE: "zone.home"}, + unique_id="zone.home", + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[None, None, None]: + """Mock setting up a config entry.""" + with patch( + "homeassistant.components.open_meteo.async_setup_entry", return_value=True + ): + yield + + +@pytest.fixture +def mock_open_meteo(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]: + """Return a mocked Open-Meteo client.""" + fixture: str = "forecast.json" + if hasattr(request, "param") and request.param: + fixture = request.param + + forecast = Forecast.parse_raw(load_fixture(fixture, DOMAIN)) + with patch( + "homeassistant.components.open_meteo.OpenMeteo", autospec=True + ) as open_meteo_mock: + open_meteo = open_meteo_mock.return_value + open_meteo.forecast.return_value = forecast + yield open_meteo diff --git a/tests/components/open_meteo/fixtures/forecast.json b/tests/components/open_meteo/fixtures/forecast.json new file mode 100644 index 00000000000..e9510cb2d2c --- /dev/null +++ b/tests/components/open_meteo/fixtures/forecast.json @@ -0,0 +1,6660 @@ +{ + "generationtime_ms": 2.886056900024414, + "latitude": 52.52, + "daily_units": { + "winddirection_10m_dominant": "°", + "temperature_2m_max": "°C", + "windspeed_10m_max": "km\/h", + "sunrise": "iso8601", + "precipitation_hours": "h", + "temperature_2m_min": "°C", + "apparent_temperature_min": "°C", + "sunset": "iso8601", + "apparent_temperature_max": "°C", + "weathercode": "wmo code", + "windgusts_10m_max": "km\/h", + "shortwave_radiation_sum": "MJ\/m²", + "time": "iso8601", + "precipitation_sum": "mm" + }, + "hourly": { + "soil_moisture_3_9cm": [ + 0.308, + 0.308, + 0.308, + 0.308, + 0.309, + 0.309, + 0.309, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.306, + 0.306, + 0.306, + 0.307, + 0.307, + 0.306, + 0.306, + 0.307, + 0.307, + 0.308, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.305, + 0.305, + 0.306, + 0.308, + 0.31, + 0.306, + 0.306, + 0.306, + 0.307, + 0.307, + 0.308, + 0.308, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.306, + 0.306, + 0.307, + 0.308, + 0.308, + 0.308, + 0.31, + 0.311, + 0.311, + 0.311, + 0.311, + 0.31, + 0.31, + 0.309, + 0.309, + 0.309, + 0.309, + 0.309, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.301, + 0.301, + 0.301, + 0.301, + 0.301, + 0.301, + 0.301, + 0.301, + 0.301, + 0.301, + 0.301, + 0.301, + 0.302, + 0.303, + 0.305, + 0.305, + 0.306, + 0.306, + 0.306, + 0.305, + 0.305, + 0.304, + 0.304, + 0.304, + 0.303, + 0.303, + 0.303, + 0.303, + 0.303, + 0.303, + 0.302, + 0.302, + 0.303, + 0.308 + ], + "soil_temperature_0cm": [ + 6.6, + 6.6, + 6.5, + 6.1, + 6.1, + 6, + 6.1, + 5.7, + 5.4, + 6.3, + 7, + 7.6, + 7.9, + 8.1, + 7.9, + 7.3, + 6.7, + 6.3, + 6.3, + 5.7, + 5.5, + 5.6, + 5.6, + 4.9, + 5.1, + 4.7, + 4.8, + 4.7, + 3.6, + 2.3, + 1.4, + 0.8, + 0.4, + 1.7, + 2.7, + 3.8, + 4.8, + 5.5, + 4.8, + 4.5, + 4.1, + 3.7, + 3.6, + 3.7, + 3.7, + 3.5, + 3.5, + 3.4, + 3.6, + 3.6, + 3.2, + 3.3, + 3.3, + 3.2, + 3, + 3.1, + 3.1, + 3.3, + 4.5, + 4.9, + 5.3, + 5.5, + 5.2, + 4.6, + 3.7, + 3.3, + 3.1, + 2.8, + 2.2, + 1.9, + 1.8, + 1.7, + 1.4, + 1.1, + 0.3, + -0, + -0, + -0, + -0.1, + -0.1, + -0.2, + -0.2, + 1.4, + 2.8, + 4.6, + 5.4, + 5, + 3.7, + 2.5, + 2.6, + 2.4, + 2.6, + 2.4, + 2.1, + 1.6, + 1.7, + 0.8, + -0, + -0.2, + -0.2, + -0.2, + -0.4, + -0.6, + -0.7, + -0.3, + 0.2, + 1, + 1.9, + 2.9, + 3.8, + 3.6, + 3.1, + 2.3, + 2, + 1.7, + 1.3, + 1, + 0.7, + 0.4, + 0.3, + 0.3, + 0.2, + 0.1, + 0.1, + -0.1, + -0.2, + -0.4, + -0.5, + -0.4, + -0.1, + 0.3, + 0.8, + 1.5, + 2.1, + 2.4, + 1.8, + 1.1, + 1.1, + 1.4, + 1.6, + 1.7, + 1.8, + 1.8, + 1.7, + 1.6, + 1.4, + 1.4, + 1.4, + 1.3, + 1.1, + 0.9, + 0.6, + 0.5, + 0.5, + 0.8, + 1.5, + 2.5, + 3.4, + 3.1, + 2.5, + 1.8, + 1.8, + 2.1, + 2.4, + 2.5, + 2.5, + 2.6, + 2.7 + ], + "weathercode": [ + 3, + 3, + 3, + 3, + 51, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 61, + 61, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 2, + 1, + 1, + 1, + 2, + 3, + 3, + 3, + 3, + 3, + 3, + 61, + 3, + 3, + 3, + 61, + 61, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 61, + 61, + 61, + 61, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 2, + 3, + 3, + 3, + 0, + 0, + 1, + 1, + 2, + 3, + 61, + 3, + 3, + 3, + 3, + 3, + 3, + 1, + 0, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 61, + 61, + 61, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 77, + 77, + 77, + 3, + 3, + 3, + 3, + 3, + 3, + 51, + 51, + 51, + 3, + 3, + 3, + 3, + 3, + 3, + 53, + 53, + 53, + 3, + 3, + 3, + 2, + 2, + 2, + 1, + 1, + 1, + 3, + 3, + 3, + 61, + 61, + 61, + 61, + 61, + 61, + 80 + ], + "windspeed_180m": [ + 26.8, + 26.6, + 27.5, + 24.6, + 27.8, + 28.5, + 27, + 24.9, + 26.6, + 22.8, + 21.1, + 18.6, + 14.3, + 14, + 12.3, + 13.3, + 10.8, + 8.6, + 10.7, + 12, + 14, + 16.4, + 17.1, + 17.9, + 20, + 22.4, + 20.9, + 19.5, + 16.9, + 16.5, + 14.5, + 13.8, + 16.2, + 20.7, + 16.3, + 12.2, + 13.8, + 14.6, + 22.7, + 26.5, + 22.9, + 30.1, + 34.3, + 35.4, + 31.3, + 30.7, + 33.5, + 34.4, + 32.8, + 31.4, + 29.8, + 30.4, + 30.5, + 27.5, + 26.1, + 27.1, + 25.9, + 27.8, + 25.4, + 22.1, + 20.1, + 16.7, + 17.1, + 17, + 20.1, + 16.7, + 20.3, + 23.3, + 32.4, + 34.8, + 36.2, + 35.8, + 34.2, + 33, + 36.1, + 38.7, + 38.8, + 37, + 35.5, + 35.7, + 35.9, + 36.9, + 37.6, + 35.1, + 30, + 25, + 21.1, + 24.5, + 29.8, + 30.4, + 29.5, + 29.8, + 31.9, + 32.2, + 24.2, + 25.7, + 27.5, + 25.7, + 26.5, + 27.4, + 28.5, + 28.8, + 28.9, + 28.6, + 28, + 27.1, + 25.8, + 24.6, + 23.2, + 21.7, + 21.5, + 21.6, + 20.9, + 18.8, + 16.6, + 16.5, + 18.7, + 21.9, + 24.6, + 24.1, + 22.4, + 20.4, + 20.1, + 20.3, + 20, + 19.2, + 18.2, + 17.1, + 16.1, + 15.2, + 15.1, + 16, + 17.6, + 19.5, + 21.8, + 25.4, + 30, + 33.1, + 36.4, + 40, + 41.9, + 43.2, + 43.6, + 41.9, + 39, + 35.9, + 35.1, + 34.9, + 34.7, + 34.5, + 34.3, + 33, + 30.8, + 28, + 24.4, + 21.9, + 19.5, + 17.8, + 18.4, + 20, + 23.4, + 28.5, + 36.2, + 44.9, + 47.4, + 47.8, + 47.9, + 48.8 + ], + "soil_moisture_9_27cm": [ + 0.315, + 0.315, + 0.314, + 0.314, + 0.315, + 0.315, + 0.315, + 0.315, + 0.315, + 0.315, + 0.315, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.309, + 0.309, + 0.309, + 0.309, + 0.309, + 0.309, + 0.309, + 0.309, + 0.309, + 0.309, + 0.309, + 0.309, + 0.309, + 0.309, + 0.309 + ], + "shortwave_radiation": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.7, + 23, + 42.1, + 69.7, + 83, + 80.3, + 64.1, + 28, + 7.9, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.7, + 32.4, + 77.2, + 109.4, + 124.7, + 120.1, + 93.1, + 26.9, + 14.6, + 0, + -0, + 0, + -0, + 0, + 0, + -0, + -0, + -0, + 0, + 0, + -0, + -0, + 0, + -0, + 0.3, + 17.4, + 58.5, + 92.6, + 107.1, + 114.2, + 92.2, + 46.9, + 12.3, + 0, + -0, + 0, + -0, + 0.1, + -0, + -0.1, + 0.1, + -0.1, + 0, + 0.1, + -0, + 0.1, + 0, + -0.2, + 0.3, + 27.8, + 71.4, + 115.2, + 117.8, + 83.3, + 87.2, + 54.4, + 11.1, + -0.2, + -0.1, + -0, + -0, + -0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -0, + -0, + 0, + 0, + 51.3, + 121.4, + 182, + 227.4, + 239.7, + 191.8, + 114.5, + 32.3, + 0, + 0, + 0, + -0, + -0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -0, + -0, + 0, + 0, + 44.9, + 103.3, + 139.2, + 150.5, + 141.8, + 116.6, + 74.9, + 23.4, + 0, + 0, + 0, + -0, + -0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -0, + -0, + 0, + 0, + 30.4, + 75.2, + 126.8, + 179.4, + 208.1, + 169.6, + 100.9, + 28.2, + 0, + 0, + 0, + -0, + -0, + 0, + 0 + ], + "cloudcover_mid": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 66, + 88, + 100, + 64, + 63, + 0, + 0, + 0, + 0, + 0, + 0, + 6, + 62, + 62, + 78, + 100, + 92, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 98, + 94, + 95, + 100, + 100, + 100, + 85, + 29, + 21, + 10, + 51, + 0, + 5, + 10, + 16, + 52, + 98, + 100, + 100, + 100, + 100, + 64, + 39, + 35, + 12, + 0, + 33, + 67, + 100, + 95, + 90, + 85, + 90, + 95, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 90, + 81, + 71, + 77, + 82, + 88, + 92, + 96, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 93, + 86, + 79, + 71, + 63, + 55, + 44, + 64, + 84, + 86, + 88, + 90, + 41, + 42, + 42, + 50, + 59, + 67, + 56, + 44, + 33, + 22, + 11, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 20, + 40, + 60, + 73, + 87, + 100, + 100, + 100, + 100, + 94 + ], + "cloudcover": [ + 100, + 100, + 98, + 100, + 100, + 98, + 96, + 88, + 88, + 100, + 94, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 83, + 87, + 100, + 100, + 100, + 79, + 34, + 7, + 0, + 49, + 100, + 99, + 100, + 100, + 95, + 100, + 100, + 100, + 100, + 96, + 95, + 100, + 100, + 100, + 96, + 97, + 100, + 91, + 93, + 100, + 91, + 98, + 100, + 99, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 74, + 100, + 100, + 100, + 0, + 5, + 13, + 18, + 52, + 99, + 100, + 100, + 100, + 100, + 94, + 86, + 88, + 43, + 0, + 33, + 67, + 100, + 99, + 98, + 97, + 98, + 99, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 94, + 88, + 82, + 87, + 92, + 97, + 98, + 99, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 98, + 97, + 95, + 97, + 98, + 100, + 99, + 98, + 98, + 98, + 98, + 98, + 94, + 97, + 99, + 98, + 98, + 97, + 96, + 95, + 95, + 94, + 93, + 92, + 86, + 81, + 75, + 61, + 47, + 33, + 54, + 75, + 96, + 97, + 99, + 100, + 100, + 100, + 100, + 100 + ], + "relativehumidity_2m": [ + 95, + 95, + 94, + 93, + 95, + 95, + 94, + 94, + 95, + 94, + 92, + 88, + 85, + 83, + 82, + 84, + 89, + 89, + 93, + 93, + 95, + 96, + 95, + 96, + 94, + 92, + 89, + 86, + 87, + 90, + 93, + 96, + 97, + 100, + 96, + 88, + 85, + 83, + 79, + 82, + 84, + 83, + 85, + 85, + 84, + 84, + 85, + 86, + 87, + 89, + 90, + 91, + 92, + 91, + 92, + 90, + 88, + 87, + 84, + 80, + 77, + 74, + 73, + 75, + 82, + 86, + 89, + 89, + 83, + 82, + 80, + 78, + 78, + 78, + 79, + 79, + 79, + 81, + 82, + 83, + 83, + 85, + 82, + 77, + 73, + 69, + 68, + 70, + 76, + 82, + 84, + 84, + 83, + 84, + 86, + 87, + 89, + 92, + 92, + 92, + 92, + 93, + 93, + 93, + 92, + 90, + 88, + 85, + 81, + 77, + 77, + 79, + 81, + 83, + 84, + 86, + 87, + 88, + 89, + 88, + 88, + 87, + 87, + 87, + 88, + 88, + 88, + 89, + 90, + 91, + 92, + 89, + 86, + 82, + 81, + 82, + 84, + 87, + 91, + 95, + 94, + 93, + 93, + 93, + 93, + 94, + 94, + 95, + 94, + 93, + 92, + 90, + 89, + 88, + 87, + 83, + 79, + 76, + 77, + 81, + 85, + 88, + 90, + 91, + 91, + 89, + 87, + 88 + ], + "cloudcover_low": [ + 100, + 100, + 98, + 100, + 100, + 98, + 96, + 88, + 88, + 100, + 94, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 83, + 87, + 100, + 100, + 100, + 79, + 34, + 7, + 0, + 49, + 100, + 99, + 100, + 100, + 95, + 100, + 100, + 100, + 91, + 92, + 92, + 91, + 85, + 90, + 96, + 97, + 100, + 91, + 93, + 100, + 86, + 95, + 100, + 97, + 100, + 96, + 93, + 64, + 81, + 62, + 80, + 83, + 89, + 86, + 79, + 20, + 20, + 48, + 71, + 64, + 49, + 19, + 0, + 0, + 0, + 31, + 4, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 0, + 70, + 80, + 89, + 100, + 64, + 88, + 75, + 76, + 34, + 0, + 3, + 6, + 9, + 11, + 12, + 14, + 25, + 36, + 47, + 38, + 29, + 19, + 40, + 61, + 81, + 79, + 77, + 75, + 75, + 75, + 74, + 69, + 64, + 59, + 59, + 58, + 58, + 65, + 72, + 79, + 81, + 84, + 86, + 91, + 95, + 100, + 99, + 98, + 97, + 93, + 90, + 87, + 88, + 93, + 98, + 97, + 96, + 95, + 93, + 91, + 89, + 90, + 91, + 92, + 86, + 81, + 75, + 61, + 47, + 33, + 52, + 71, + 90, + 93, + 96, + 99, + 89, + 80, + 70, + 80 + ], + "soil_moisture_27_81cm": [ + 0.332, + 0.332, + 0.332, + 0.332, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324 + ], + "temperature_2m": [ + 6.9, + 6.8, + 6.8, + 6.6, + 6.5, + 6.4, + 6.4, + 6.2, + 5.9, + 6, + 6.5, + 6.9, + 7.3, + 7.6, + 7.6, + 7.5, + 7.2, + 6.9, + 6.6, + 6.4, + 6, + 5.9, + 5.8, + 5.5, + 5.4, + 5.1, + 5, + 4.9, + 4.2, + 3.2, + 2.2, + 1.5, + 0.8, + 0.2, + 0.6, + 1.8, + 2.7, + 3.8, + 4.5, + 4.5, + 4.4, + 4.2, + 3.9, + 3.9, + 4, + 3.9, + 3.9, + 3.8, + 3.7, + 3.6, + 3.4, + 3.3, + 3.2, + 3.2, + 3.1, + 3.1, + 3.1, + 3.1, + 3.5, + 3.9, + 4.3, + 4.6, + 4.8, + 4.6, + 4, + 3.5, + 3.3, + 3, + 2.7, + 2.3, + 2, + 1.8, + 1.6, + 1.2, + 0.7, + 0.4, + 0.3, + 0.2, + 0.2, + 0.1, + -0, + -0.1, + 0.6, + 1.7, + 2.9, + 4, + 4.5, + 4.2, + 3.5, + 3, + 2.8, + 2.8, + 2.8, + 2.6, + 2.3, + 2, + 1.6, + 0.9, + 0.6, + 0.4, + 0.1, + -0, + -0.2, + -0.2, + -0.1, + 0, + 0.5, + 1.2, + 2.2, + 3.1, + 3.4, + 3.4, + 3.2, + 3, + 2.7, + 2.2, + 1.9, + 1.5, + 1.1, + 0.9, + 0.8, + 0.6, + 0.4, + 0.2, + -0.1, + -0.3, + -0.4, + -0.5, + -0.5, + -0.5, + -0.3, + 0.3, + 1.1, + 2, + 2.2, + 2, + 1.7, + 1.4, + 1, + 0.7, + 2, + 1.9, + 1.8, + 1.8, + 1.7, + 1.6, + 1.5, + 1.5, + 1.3, + 1.1, + 0.8, + 0.5, + 0.1, + -0.2, + -0.3, + 0.2, + 1, + 1.9, + 2.2, + 2.4, + 2.6, + 2.6, + 2.5, + 2.4, + 2.5, + 2.8, + 3, + 3 + ], + "direct_radiation": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.7, + 1.1, + 6.3, + 3.8, + 3.6, + 6.7, + 0.1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.1, + 2.1, + 5.7, + 14.6, + 14.3, + 15.2, + 12.3, + 1.6, + 0.3, + 0, + -0, + 0, + -0, + 0, + 0, + -0, + -0, + 0, + 0, + 0, + -0, + 0, + 0, + -0, + 0, + 0.1, + 1.8, + 2.5, + 1.9, + 1.7, + 0.9, + 0, + -0, + 0, + -0, + 0, + 0, + -0, + -0, + -0.1, + 0.1, + -0.2, + 0, + 0, + -0, + 0.2, + -0.1, + -0.1, + 0.1, + 1.2, + 5.2, + 22, + 27.9, + 15.4, + 26.4, + 3.6, + 0.1, + -0.2, + 0, + -0, + -0, + -0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -0, + -0, + 0, + 0, + 29.8, + 72.1, + 117.5, + 160.8, + 177.8, + 135.5, + 71.8, + 16.7, + 0, + 0, + 0, + -0, + -0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -0, + -0, + 0, + 0, + 18.6, + 42.2, + 53.5, + 52.3, + 43.3, + 38.3, + 23.5, + 7, + 0, + 0, + 0, + -0, + -0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -0, + -0, + 0, + 0, + 5.7, + 17.3, + 44.9, + 84.7, + 113.8, + 92.4, + 52, + 13.2, + 0, + 0, + 0, + -0, + -0, + 0, + 0 + ], + "dewpoint_2m": [ + 6.2, + 6.1, + 5.9, + 5.6, + 5.7, + 5.7, + 5.6, + 5.3, + 5.2, + 5.2, + 5.3, + 5.1, + 4.9, + 4.9, + 4.8, + 5, + 5.6, + 5.2, + 5.5, + 5.4, + 5.3, + 5.2, + 5.1, + 4.8, + 4.5, + 3.9, + 3.4, + 2.7, + 2.3, + 1.7, + 1.2, + 0.9, + 0.4, + 0.1, + -0, + 0, + 0.5, + 1.1, + 1.2, + 1.7, + 1.9, + 1.4, + 1.6, + 1.6, + 1.5, + 1.5, + 1.6, + 1.7, + 1.8, + 2, + 2, + 2, + 2, + 1.9, + 1.9, + 1.6, + 1.3, + 1.2, + 1.1, + 0.8, + 0.6, + 0.4, + 0.4, + 0.6, + 1.2, + 1.3, + 1.6, + 1.4, + 0.7, + 0.5, + 0.1, + -0.4, + -0.6, + -1.1, + -1.7, + -2.3, + -2.3, + -2.5, + -2.5, + -2.5, + -2.4, + -2.1, + -1.8, + -1.4, + -0.9, + -0.5, + -0.2, + 0.1, + 0.6, + 0.9, + 1.1, + 1, + 1, + 1, + 0.8, + 0.4, + -0.3, + -1, + -1.3, + -1.4, + -1.5, + -1.6, + -1.7, + -1.7, + -1.7, + -1.7, + -1.6, + -1.4, + -1.2, + -0.9, + -0.8, + -0.8, + -0.7, + -0.6, + -0.5, + -0.5, + -0.5, + -0.6, + -0.7, + -0.8, + -1, + -1.2, + -1.2, + -1.2, + -1.1, + -1.1, + -1.1, + -1, + -1, + -1, + -1, + -0.8, + -0.6, + -0.4, + -0.4, + -0.5, + -0.5, + -0.1, + 0.5, + 1, + 1.1, + 1, + 0.8, + 0.7, + 0.7, + 0.7, + 0.7, + 0.7, + 0.5, + 0.1, + -0.3, + -1, + -1.4, + -1.9, + -2.3, + -2.4, + -2.3, + -2, + -1.4, + -0.6, + 0.3, + 0.7, + 1, + 1.2, + 1.2, + 1.1, + 1.1, + 1.2 + ], + "freezinglevel_height": [ + 2432, + 2434, + 2512, + 2448, + 2512, + 2524, + 2608, + 2458, + 2446, + 2454, + 2454, + 2438, + 2544, + 2554, + 2544, + 2540, + 2550, + 2578, + 2516, + 2586, + 2540, + 2516, + 2508, + 2440, + 2464, + 2464, + 2412, + 2432, + 2260, + 2003, + 1897, + 1774, + 702, + 702, + 693, + 1511, + 635, + 634, + 522, + 668, + 624, + 697, + 674, + 586, + 910, + 856, + 846, + 697, + 665, + 623, + 611, + 633, + 562, + 566, + 620, + 438, + 399, + 398, + 412, + 466, + 501, + 537, + 548, + 513, + 492, + 488, + 448, + 462, + 564, + 569, + 544, + 555, + 540, + 514, + 565, + 471, + 464, + 530, + 507, + 562, + 603, + 566, + 574, + 489, + 462, + 472, + 569, + 562, + 567, + 579, + 597, + 605, + 604, + 598, + 589, + 585, + 582, + 573, + 562, + 548, + 524, + 498, + 468, + 434, + 416, + 402, + 394, + 399, + 412, + 438, + 469, + 508, + 548, + 559, + 562, + 551, + 529, + 497, + 456, + 431, + 407, + 379, + 366, + 356, + 331, + 291, + 241, + 176, + 125, + 72, + 50, + 105, + 199, + 299, + 326, + 308, + 298, + 329, + 376, + 420, + 424, + 411, + 381, + 344, + 297, + 245, + 227, + 219, + 202, + 172, + 135, + 93, + 67, + 44, + 41, + 68, + 113, + 191, + 258, + 335, + 455, + 565, + 687, + 845, + 950, + 1049, + 1187, + 1320 + ], + "soil_temperature_54cm": [ + 8.5, + 8.5, + 8.5, + 8.5, + 8.4, + 8.4, + 8.4, + 8.4, + 8.4, + 8.4, + 8.4, + 8.4, + 8.4, + 8.4, + 8.4, + 8.4, + 8.4, + 8.4, + 8.4, + 8.4, + 8.4, + 8.4, + 8.4, + 8.4, + 8.4, + 8.4, + 8.3, + 8.3, + 8.3, + 8.3, + 8.3, + 8.3, + 8.3, + 8.3, + 8.3, + 8.3, + 8.3, + 8.2, + 8.2, + 8.2, + 8.2, + 8.2, + 8.2, + 8.2, + 8.1, + 8.1, + 8.1, + 8.1, + 8.1, + 8.1, + 8.1, + 8, + 8, + 8, + 8, + 8, + 8, + 8, + 7.9, + 7.9, + 7.9, + 7.9, + 7.9, + 7.9, + 7.9, + 7.8, + 7.8, + 7.8, + 7.5, + 7.5, + 7.5, + 7.5, + 7.5, + 7.4, + 7.4, + 7.4, + 7.4, + 7.4, + 7.4, + 7.3, + 7.3, + 7.3, + 7.3, + 7.3, + 7.2, + 7.2, + 7.2, + 7.2, + 7.2, + 7.1, + 7.1, + 7.1, + 7.1, + 7.1, + 7.1, + 7, + 7, + 7, + 7, + 7, + 7, + 6.9, + 6.9, + 6.9, + 6.9, + 6.9, + 6.8, + 6.8, + 6.8, + 6.8, + 6.8, + 6.8, + 6.7, + 6.7, + 6.7, + 6.7, + 6.7, + 6.7, + 6.6, + 6.6, + 6.6, + 6.6, + 6.6, + 6.6, + 6.5, + 6.5, + 6.5, + 6.5, + 6.5, + 6.4, + 6.4, + 6.4, + 6.4, + 6.4, + 6.4, + 6.3, + 6.3, + 6.3, + 6.3, + 6.3, + 6.3, + 6.3, + 6.2, + 6.2, + 6.2, + 6.2, + 6.2, + 6.2, + 6.2, + 6.1, + 6.1, + 6.1, + 6.1, + 6.1, + 6.1, + 6.1, + 6.1, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 5.9, + 5.9 + ], + "soil_temperature_6cm": [ + 6.4, + 6.4, + 6.4, + 6.4, + 6.3, + 6.3, + 6.3, + 6.1, + 6, + 6, + 6.3, + 6.7, + 7.1, + 7.5, + 7.6, + 7.5, + 7.3, + 7, + 6.8, + 6.5, + 6.3, + 6.2, + 6.1, + 6, + 5.8, + 5.6, + 5.5, + 5.5, + 5.2, + 4.7, + 4, + 3.4, + 2.9, + 2.7, + 3.1, + 3.6, + 4.2, + 4.7, + 5, + 5, + 4.8, + 4.6, + 4.4, + 4.4, + 4.3, + 4.3, + 4.2, + 4.2, + 4.2, + 4.2, + 4.1, + 4, + 4, + 3.9, + 3.9, + 3.8, + 3.8, + 3.8, + 4, + 4.4, + 4.8, + 5.1, + 5.3, + 5.2, + 4.8, + 4.5, + 4.2, + 4, + 3.5, + 3.2, + 3, + 2.9, + 2.7, + 2.6, + 2.2, + 1.9, + 1.6, + 1.5, + 1.4, + 1.3, + 1.2, + 1.2, + 1.4, + 2, + 2.8, + 3.7, + 4.2, + 4.2, + 3.8, + 3.4, + 3.2, + 3.1, + 3.1, + 2.9, + 2.7, + 2.6, + 2.4, + 1.9, + 1.7, + 1.5, + 1.2, + 1.1, + 1, + 0.9, + 0.8, + 0.8, + 0.9, + 1.4, + 2, + 2.6, + 2.9, + 3.1, + 3.1, + 3, + 2.8, + 2.5, + 2.2, + 2, + 1.7, + 1.6, + 1.5, + 1.3, + 1.2, + 1.1, + 1, + 0.9, + 0.9, + 0.8, + 0.7, + 0.6, + 0.7, + 1, + 1.4, + 1.9, + 2.5, + 2.5, + 2.4, + 2.3, + 2.2, + 2.1, + 2.1, + 2.1, + 2.1, + 2.1, + 2.1, + 2.1, + 2.1, + 2.1, + 2.1, + 2, + 1.9, + 1.7, + 1.5, + 1.3, + 1.2, + 1.6, + 2.1, + 2.7, + 2.8, + 2.8, + 2.8, + 2.7, + 2.7, + 2.6, + 2.6, + 2.7, + 2.8, + 2.8 + ], + "soil_moisture_1_3cm": [ + 0.305, + 0.305, + 0.305, + 0.305, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.305, + 0.305, + 0.305, + 0.305, + 0.305, + 0.305, + 0.305, + 0.305, + 0.305, + 0.305, + 0.305, + 0.305, + 0.305, + 0.305, + 0.305, + 0.305, + 0.305, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.303, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.306, + 0.306, + 0.305, + 0.305, + 0.305, + 0.305, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.303, + 0.303, + 0.303, + 0.303, + 0.302, + 0.303, + 0.308, + 0.31, + 0.312, + 0.303, + 0.303, + 0.303, + 0.303, + 0.303, + 0.303, + 0.303, + 0.303, + 0.303, + 0.303, + 0.303, + 0.303, + 0.303, + 0.303, + 0.303, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.303, + 0.303, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.303, + 0.303, + 0.303, + 0.303, + 0.303, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.303, + 0.303, + 0.303, + 0.303, + 0.303, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.303, + 0.304, + 0.304, + 0.299, + 0.299, + 0.298, + 0.298, + 0.299, + 0.299, + 0.299, + 0.299, + 0.299, + 0.299, + 0.3, + 0.301, + 0.303, + 0.305, + 0.307, + 0.307, + 0.306, + 0.304, + 0.304, + 0.303, + 0.303, + 0.302, + 0.302, + 0.301, + 0.301, + 0.301, + 0.301, + 0.301, + 0.301, + 0.301, + 0.3, + 0.299, + 0.302, + 0.312 + ], + "winddirection_80m": [ + 263, + 268, + 277, + 271, + 278, + 278, + 276, + 273, + 264, + 271, + 263, + 257, + 257, + 266, + 253, + 258, + 240, + 232, + 194, + 198, + 183, + 186, + 179, + 178, + 165, + 167, + 171, + 168, + 154, + 156, + 166, + 173, + 182, + 192, + 202, + 199, + 204, + 208, + 246, + 217, + 230, + 227, + 232, + 235, + 241, + 239, + 245, + 255, + 257, + 260, + 254, + 251, + 257, + 252, + 240, + 243, + 247, + 256, + 253, + 252, + 244, + 239, + 234, + 217, + 204, + 208, + 173, + 172, + 179, + 177, + 173, + 170, + 167, + 159, + 158, + 159, + 158, + 158, + 152, + 147, + 145, + 143, + 141, + 140, + 145, + 138, + 142, + 143, + 148, + 150, + 151, + 144, + 153, + 169, + 167, + 170, + 179, + 181, + 178, + 171, + 162, + 157, + 152, + 148, + 148, + 150, + 152, + 150, + 145, + 140, + 142, + 146, + 153, + 160, + 171, + 188, + 194, + 195, + 195, + 196, + 196, + 198, + 199, + 201, + 203, + 206, + 210, + 216, + 220, + 225, + 235, + 247, + 258, + 266, + 270, + 267, + 265, + 266, + 268, + 270, + 290, + 287, + 285, + 285, + 287, + 289, + 290, + 290, + 290, + 291, + 291, + 291, + 289, + 286, + 281, + 278, + 274, + 268, + 260, + 249, + 231, + 218, + 208, + 202, + 200, + 199, + 199, + 198 + ], + "winddirection_10m": [ + 261, + 265, + 274, + 268, + 275, + 272, + 271, + 269, + 258, + 268, + 261, + 255, + 256, + 263, + 251, + 256, + 233, + 218, + 185, + 188, + 173, + 175, + 168, + 168, + 158, + 160, + 165, + 162, + 149, + 151, + 159, + 158, + 170, + 190, + 202, + 197, + 202, + 205, + 245, + 214, + 223, + 224, + 229, + 231, + 238, + 236, + 243, + 253, + 255, + 258, + 251, + 248, + 254, + 249, + 238, + 240, + 245, + 254, + 252, + 251, + 243, + 237, + 233, + 216, + 201, + 207, + 167, + 167, + 167, + 165, + 162, + 163, + 160, + 152, + 149, + 149, + 148, + 148, + 140, + 136, + 134, + 133, + 135, + 136, + 142, + 135, + 139, + 141, + 144, + 142, + 137, + 130, + 144, + 157, + 146, + 154, + 164, + 160, + 156, + 147, + 136, + 130, + 125, + 123, + 126, + 132, + 137, + 135, + 131, + 127, + 128, + 132, + 137, + 141, + 146, + 156, + 166, + 174, + 181, + 182, + 182, + 182, + 186, + 189, + 194, + 196, + 198, + 203, + 206, + 211, + 222, + 237, + 253, + 262, + 268, + 264, + 260, + 261, + 263, + 265, + 286, + 284, + 282, + 283, + 285, + 287, + 287, + 287, + 288, + 289, + 290, + 290, + 288, + 284, + 280, + 277, + 273, + 267, + 259, + 244, + 212, + 202, + 199, + 198, + 197, + 195, + 194, + 194 + ], + "time": [ + "2021-11-24T00:00", + "2021-11-24T01:00", + "2021-11-24T02:00", + "2021-11-24T03:00", + "2021-11-24T04:00", + "2021-11-24T05:00", + "2021-11-24T06:00", + "2021-11-24T07:00", + "2021-11-24T08:00", + "2021-11-24T09:00", + "2021-11-24T10:00", + "2021-11-24T11:00", + "2021-11-24T12:00", + "2021-11-24T13:00", + "2021-11-24T14:00", + "2021-11-24T15:00", + "2021-11-24T16:00", + "2021-11-24T17:00", + "2021-11-24T18:00", + "2021-11-24T19:00", + "2021-11-24T20:00", + "2021-11-24T21:00", + "2021-11-24T22:00", + "2021-11-24T23:00", + "2021-11-25T00:00", + "2021-11-25T01:00", + "2021-11-25T02:00", + "2021-11-25T03:00", + "2021-11-25T04:00", + "2021-11-25T05:00", + "2021-11-25T06:00", + "2021-11-25T07:00", + "2021-11-25T08:00", + "2021-11-25T09:00", + "2021-11-25T10:00", + "2021-11-25T11:00", + "2021-11-25T12:00", + "2021-11-25T13:00", + "2021-11-25T14:00", + "2021-11-25T15:00", + "2021-11-25T16:00", + "2021-11-25T17:00", + "2021-11-25T18:00", + "2021-11-25T19:00", + "2021-11-25T20:00", + "2021-11-25T21:00", + "2021-11-25T22:00", + "2021-11-25T23:00", + "2021-11-26T00:00", + "2021-11-26T01:00", + "2021-11-26T02:00", + "2021-11-26T03:00", + "2021-11-26T04:00", + "2021-11-26T05:00", + "2021-11-26T06:00", + "2021-11-26T07:00", + "2021-11-26T08:00", + "2021-11-26T09:00", + "2021-11-26T10:00", + "2021-11-26T11:00", + "2021-11-26T12:00", + "2021-11-26T13:00", + "2021-11-26T14:00", + "2021-11-26T15:00", + "2021-11-26T16:00", + "2021-11-26T17:00", + "2021-11-26T18:00", + "2021-11-26T19:00", + "2021-11-26T20:00", + "2021-11-26T21:00", + "2021-11-26T22:00", + "2021-11-26T23:00", + "2021-11-27T00:00", + "2021-11-27T01:00", + "2021-11-27T02:00", + "2021-11-27T03:00", + "2021-11-27T04:00", + "2021-11-27T05:00", + "2021-11-27T06:00", + "2021-11-27T07:00", + "2021-11-27T08:00", + "2021-11-27T09:00", + "2021-11-27T10:00", + "2021-11-27T11:00", + "2021-11-27T12:00", + "2021-11-27T13:00", + "2021-11-27T14:00", + "2021-11-27T15:00", + "2021-11-27T16:00", + "2021-11-27T17:00", + "2021-11-27T18:00", + "2021-11-27T19:00", + "2021-11-27T20:00", + "2021-11-27T21:00", + "2021-11-27T22:00", + "2021-11-27T23:00", + "2021-11-28T00:00", + "2021-11-28T01:00", + "2021-11-28T02:00", + "2021-11-28T03:00", + "2021-11-28T04:00", + "2021-11-28T05:00", + "2021-11-28T06:00", + "2021-11-28T07:00", + "2021-11-28T08:00", + "2021-11-28T09:00", + "2021-11-28T10:00", + "2021-11-28T11:00", + "2021-11-28T12:00", + "2021-11-28T13:00", + "2021-11-28T14:00", + "2021-11-28T15:00", + "2021-11-28T16:00", + "2021-11-28T17:00", + "2021-11-28T18:00", + "2021-11-28T19:00", + "2021-11-28T20:00", + "2021-11-28T21:00", + "2021-11-28T22:00", + "2021-11-28T23:00", + "2021-11-29T00:00", + "2021-11-29T01:00", + "2021-11-29T02:00", + "2021-11-29T03:00", + "2021-11-29T04:00", + "2021-11-29T05:00", + "2021-11-29T06:00", + "2021-11-29T07:00", + "2021-11-29T08:00", + "2021-11-29T09:00", + "2021-11-29T10:00", + "2021-11-29T11:00", + "2021-11-29T12:00", + "2021-11-29T13:00", + "2021-11-29T14:00", + "2021-11-29T15:00", + "2021-11-29T16:00", + "2021-11-29T17:00", + "2021-11-29T18:00", + "2021-11-29T19:00", + "2021-11-29T20:00", + "2021-11-29T21:00", + "2021-11-29T22:00", + "2021-11-29T23:00", + "2021-11-30T00:00", + "2021-11-30T01:00", + "2021-11-30T02:00", + "2021-11-30T03:00", + "2021-11-30T04:00", + "2021-11-30T05:00", + "2021-11-30T06:00", + "2021-11-30T07:00", + "2021-11-30T08:00", + "2021-11-30T09:00", + "2021-11-30T10:00", + "2021-11-30T11:00", + "2021-11-30T12:00", + "2021-11-30T13:00", + "2021-11-30T14:00", + "2021-11-30T15:00", + "2021-11-30T16:00", + "2021-11-30T17:00", + "2021-11-30T18:00", + "2021-11-30T19:00", + "2021-11-30T20:00", + "2021-11-30T21:00", + "2021-11-30T22:00", + "2021-11-30T23:00" + ], + "soil_moisture_0_1cm": [ + 0.304, + 0.304, + 0.304, + 0.304, + 0.305, + 0.305, + 0.305, + 0.305, + 0.305, + 0.305, + 0.305, + 0.304, + 0.304, + 0.304, + 0.303, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.303, + 0.303, + 0.303, + 0.304, + 0.304, + 0.303, + 0.303, + 0.303, + 0.303, + 0.302, + 0.303, + 0.304, + 0.303, + 0.303, + 0.303, + 0.305, + 0.306, + 0.305, + 0.305, + 0.304, + 0.304, + 0.304, + 0.303, + 0.303, + 0.303, + 0.303, + 0.303, + 0.303, + 0.303, + 0.303, + 0.303, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.303, + 0.31, + 0.312, + 0.313, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.301, + 0.301, + 0.301, + 0.301, + 0.301, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.301, + 0.301, + 0.301, + 0.301, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.303, + 0.304, + 0.304, + 0.298, + 0.298, + 0.297, + 0.297, + 0.298, + 0.298, + 0.298, + 0.298, + 0.298, + 0.299, + 0.3, + 0.302, + 0.304, + 0.307, + 0.309, + 0.308, + 0.306, + 0.303, + 0.302, + 0.302, + 0.302, + 0.301, + 0.301, + 0.3, + 0.3, + 0.3, + 0.3, + 0.3, + 0.3, + 0.301, + 0.3, + 0.299, + 0.302, + 0.314 + ], + "direct_normal_irradiance": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.4, + 8.5, + 5.8, + 25.1, + 13.4, + 13, + 28, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1.5, + 24.6, + 32.2, + 58.9, + 50.7, + 54.7, + 52.4, + 10.4, + 3.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1.4, + 10.5, + 10.4, + 6.8, + 6, + 4, + 0.3, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1.4, + 13.3, + 30.5, + 91.3, + 101.2, + 56.8, + 114.8, + 23.7, + 1.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 341.5, + 430.3, + 493.5, + 589.6, + 660.9, + 596.3, + 477.7, + 191.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 213.7, + 256.9, + 227.8, + 193.9, + 162.6, + 170.3, + 158.4, + 80.4, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 65.4, + 107.1, + 193.9, + 317.5, + 431.7, + 415.9, + 356.1, + 151.7, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "soil_temperature_18cm": [ + 6.3, + 6.4, + 6.4, + 6.5, + 6.5, + 6.6, + 6.6, + 6.6, + 6.6, + 6.6, + 6.6, + 6.7, + 6.7, + 6.8, + 6.9, + 7, + 7.1, + 7.1, + 7.1, + 7.1, + 7.1, + 7.1, + 7, + 7, + 6.9, + 6.9, + 6.8, + 6.8, + 6.7, + 6.6, + 6.5, + 6.3, + 6.1, + 5.9, + 5.7, + 5.6, + 5.5, + 5.5, + 5.6, + 5.6, + 5.6, + 5.6, + 5.6, + 5.6, + 5.5, + 5.5, + 5.5, + 5.5, + 5.4, + 5.4, + 5.4, + 5.3, + 5.3, + 5.3, + 5.2, + 5.2, + 5.2, + 5.1, + 5.1, + 5.1, + 5.2, + 5.2, + 5.3, + 5.4, + 5.4, + 5.4, + 5.4, + 5.3, + 4.9, + 4.9, + 4.8, + 4.7, + 4.6, + 4.6, + 4.5, + 4.4, + 4.2, + 4.1, + 4, + 3.9, + 3.8, + 3.7, + 3.6, + 3.5, + 3.5, + 3.6, + 3.7, + 3.9, + 4, + 4, + 4.1, + 4.1, + 4.1, + 4.1, + 4.1, + 4, + 4, + 3.9, + 3.8, + 3.8, + 3.6, + 3.5, + 3.4, + 3.3, + 3.2, + 3.1, + 3.1, + 3, + 3, + 3.1, + 3.1, + 3.2, + 3.4, + 3.4, + 3.4, + 3.5, + 3.5, + 3.4, + 3.4, + 3.4, + 3.3, + 3.2, + 3.2, + 3.1, + 3, + 3, + 2.9, + 2.8, + 2.8, + 2.7, + 2.7, + 2.6, + 2.6, + 2.6, + 2.9, + 2.9, + 3, + 3.1, + 3.1, + 3.1, + 3.1, + 3.1, + 3.1, + 3.1, + 3.1, + 3.1, + 3.1, + 3.1, + 3.1, + 3.1, + 3.1, + 3.1, + 3.1, + 3, + 2.9, + 2.9, + 2.9, + 3, + 3, + 3.1, + 3.2, + 3.2, + 3.2, + 3.3, + 3.3, + 3.3, + 3.3, + 3.4 + ], + "winddirection_180m": [ + 266, + 271, + 284, + 279, + 287, + 290, + 283, + 277, + 277, + 274, + 274, + 267, + 258, + 267, + 254, + 259, + 253, + 246, + 208, + 208, + 196, + 199, + 194, + 191, + 174, + 178, + 181, + 177, + 165, + 168, + 186, + 199, + 201, + 207, + 219, + 222, + 222, + 214, + 247, + 229, + 241, + 236, + 245, + 247, + 247, + 243, + 250, + 257, + 259, + 263, + 257, + 255, + 261, + 256, + 247, + 248, + 250, + 257, + 254, + 253, + 245, + 240, + 234, + 218, + 205, + 209, + 185, + 186, + 200, + 198, + 196, + 193, + 192, + 187, + 183, + 180, + 180, + 181, + 178, + 173, + 170, + 169, + 167, + 168, + 168, + 156, + 144, + 145, + 157, + 166, + 175, + 168, + 177, + 192, + 200, + 195, + 200, + 202, + 199, + 196, + 192, + 190, + 188, + 186, + 185, + 184, + 183, + 183, + 182, + 182, + 181, + 180, + 182, + 189, + 203, + 219, + 221, + 218, + 216, + 217, + 219, + 222, + 223, + 223, + 225, + 230, + 236, + 246, + 253, + 260, + 268, + 271, + 271, + 272, + 302, + 298, + 297, + 300, + 305, + 308, + 306, + 303, + 298, + 296, + 294, + 291, + 291, + 291, + 292, + 293, + 293, + 293, + 291, + 287, + 282, + 278, + 275, + 268, + 264, + 261, + 252, + 239, + 228, + 220, + 215, + 211, + 207, + 206 + ], + "apparent_temperature": [ + 4.4, + 4.2, + 4.3, + 4.4, + 4.1, + 4, + 3.9, + 3.7, + 3.6, + 3.4, + 4.1, + 4.6, + 4.9, + 5.2, + 5.3, + 5.4, + 5.5, + 5.3, + 4.8, + 4.7, + 4.2, + 4, + 3.9, + 3.4, + 3.2, + 2.8, + 2.5, + 2.3, + 1.6, + 0.5, + -0.5, + -1.2, + -2, + -2.9, + -2.6, + -1.3, + -0.3, + 0.7, + 0.9, + 1.1, + 1.4, + 0.8, + 0.4, + 0.5, + 0.5, + 0.3, + 0.2, + 0.1, + 0.1, + -0, + -0.1, + -0.3, + -0.5, + -0.2, + -0.2, + -0.3, + -0.4, + -1, + -0.2, + 0.2, + 0.7, + 1.3, + 1.4, + 1.3, + 0.7, + 0.4, + 0.5, + 0, + -0.6, + -1.1, + -1.5, + -1.9, + -2.2, + -2.6, + -3.2, + -3.5, + -3.6, + -3.6, + -3.7, + -3.8, + -3.9, + -4, + -3.4, + -2, + -0.9, + 0.2, + 0.6, + 0.5, + 0, + -0.3, + -0.3, + -0.4, + -0.4, + -0.6, + -0.8, + -1, + -1.4, + -2.1, + -2.4, + -2.7, + -3, + -3.2, + -3.4, + -3.5, + -3.5, + -3.3, + -2.8, + -2, + -1, + 0, + 0.3, + 0.4, + 0.3, + 0.2, + -0.1, + -0.5, + -0.9, + -1.4, + -1.9, + -2.1, + -2.3, + -2.5, + -2.8, + -3, + -3.3, + -3.5, + -3.6, + -3.6, + -3.6, + -3.6, + -3.3, + -2.7, + -1.9, + -1.2, + -1.4, + -1.4, + -1.6, + -1.9, + -2.4, + -2.9, + -1.6, + -1.8, + -2, + -2.1, + -2.3, + -2.4, + -2.4, + -2.4, + -2.5, + -2.9, + -3.3, + -3.9, + -4.2, + -4.5, + -4.6, + -4, + -3.1, + -2, + -1.3, + -0.7, + -0.3, + -0.5, + -1, + -1.4, + -1.5, + -1.4, + -1.3, + -1.2 + ], + "cloudcover_high": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 3, + 28, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 92, + 1, + 0, + 0, + 0, + 0, + 0, + 83, + 100, + 0, + 7, + 100, + 100, + 100, + 3, + 0, + 0, + 0, + 0, + 0, + 14, + 62, + 1, + 58, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 87, + 99, + 100, + 100, + 100, + 100, + 100, + 100, + 55, + 100, + 100, + 100, + 0, + 0, + 4, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 33, + 67, + 100, + 94, + 87, + 81, + 75, + 70, + 65, + 43, + 22, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 7, + 15, + 22, + 15, + 7, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 11, + 22, + 33, + 22, + 11, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 8, + 16, + 24, + 49, + 75, + 100, + 100, + 100, + 100, + 67 + ], + "pressure_msl": [ + 1025.2, + 1025.7, + 1025.7, + 1025.2, + 1024.7, + 1024.2, + 1024.2, + 1024.2, + 1024.2, + 1023.7, + 1024.2, + 1023.7, + 1023.2, + 1022.2, + 1021.2, + 1021.2, + 1020.7, + 1020.2, + 1020.2, + 1019.7, + 1018.7, + 1018.7, + 1018.2, + 1017.7, + 1016.7, + 1015.7, + 1015.2, + 1014.2, + 1013.7, + 1012.7, + 1012.2, + 1011.1, + 1011.1, + 1010.6, + 1010.1, + 1009.6, + 1008.6, + 1008.1, + 1007.6, + 1007.1, + 1007.1, + 1006.6, + 1006.1, + 1006.1, + 1005.6, + 1005.6, + 1005.1, + 1005.1, + 1005.1, + 1005.1, + 1004.6, + 1004.1, + 1004.1, + 1003.6, + 1003.6, + 1003.6, + 1003.1, + 1002.6, + 1002.6, + 1002.1, + 1001.6, + 1000.6, + 1000.1, + 999.6, + 999.6, + 999.1, + 998.6, + 998.1, + 997.2, + 996.7, + 996.2, + 995.7, + 995.2, + 994.7, + 994.2, + 993.7, + 993.2, + 992.7, + 992.7, + 992.2, + 992.7, + 992.7, + 992.7, + 992.7, + 992.2, + 991.7, + 991.7, + 991.7, + 992.2, + 992.2, + 992.7, + 992.7, + 992.7, + 993.2, + 993.2, + 993.2, + 993.7, + 993.2, + 993.7, + 993.7, + 993.7, + 993.7, + 994.2, + 994.2, + 994.7, + 994.7, + 995.2, + 995.2, + 995.2, + 995.2, + 995.2, + 995.2, + 995.7, + 995.7, + 996.2, + 996.2, + 996.7, + 996.7, + 996.7, + 996.7, + 997.2, + 997.2, + 997.2, + 997.7, + 997.7, + 997.7, + 998.2, + 998.7, + 999.3, + 999.3, + 999.8, + 1000.3, + 1000.3, + 1000.3, + 1000.6, + 1001.1, + 1001.1, + 1001.6, + 1002.1, + 1002.1, + 1006.6, + 1007.1, + 1007.1, + 1007.6, + 1007.6, + 1008.2, + 1008.2, + 1008.7, + 1008.7, + 1008.7, + 1008.7, + 1009.2, + 1009.2, + 1009.7, + 1009.7, + 1009.7, + 1009.2, + 1008.7, + 1008.2, + 1007.6, + 1006.6, + 1005.6, + 1004.6, + 1003.6, + 1002.1, + 1001.1, + 999.1, + 998.1 + ], + "diffuse_radiation": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.7, + 22.3, + 41, + 63.3, + 79.1, + 76.7, + 57.4, + 27.9, + 7.9, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.6, + 30.3, + 71.4, + 94.8, + 110.4, + 104.9, + 80.8, + 25.3, + 14.3, + -0, + 0, + 0, + -0, + 0, + -0, + 0, + 0, + -0, + 0, + 0, + 0, + -0, + 0, + -0, + 0.3, + 17.3, + 56.7, + 90.1, + 105.3, + 112.5, + 91.3, + 46.8, + 12.3, + 0, + -0, + 0, + -0.1, + 0.1, + -0, + -0, + -0, + 0, + -0, + 0.1, + -0, + -0.1, + 0.1, + -0, + 0.2, + 26.7, + 66.2, + 93.2, + 89.9, + 67.9, + 60.8, + 50.8, + 11, + 0, + -0.1, + -0, + -0, + -0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -0, + -0, + 0, + 0, + 21.5, + 49.3, + 64.5, + 66.6, + 62, + 56.3, + 42.7, + 15.6, + 0, + 0, + 0, + -0, + -0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -0, + -0, + 0, + 0, + 26.2, + 61.1, + 85.6, + 98.2, + 98.6, + 78.3, + 51.4, + 16.4, + 0, + 0, + 0, + -0, + -0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -0, + -0, + 0, + 0, + 24.8, + 57.9, + 81.8, + 94.8, + 94.4, + 77.1, + 48.9, + 15, + 0, + 0, + 0, + -0, + -0, + 0, + 0 + ], + "snow_depth": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "windspeed_10m": [ + 10.4, + 10.9, + 10.1, + 7.9, + 8.8, + 8.9, + 9.3, + 9.4, + 8.1, + 9.6, + 8.5, + 7.6, + 7.9, + 7.4, + 7.1, + 6.4, + 4, + 3.1, + 5, + 4.3, + 4.6, + 4.9, + 5.3, + 5.3, + 5.8, + 6, + 6.3, + 5.9, + 5.9, + 6, + 5.2, + 4.7, + 5.1, + 7, + 6.9, + 6.6, + 6.6, + 7.8, + 11.5, + 10.5, + 8.1, + 10.1, + 10.9, + 10.9, + 11.2, + 11.8, + 12.3, + 12.9, + 12.7, + 12.5, + 12.1, + 12.3, + 12.6, + 10.9, + 10, + 10.8, + 10.8, + 14.8, + 12.1, + 11.5, + 10.5, + 8.8, + 9.2, + 8.7, + 9.2, + 7.8, + 6.3, + 7.5, + 7.9, + 7.9, + 8.7, + 9.5, + 9.2, + 9.4, + 9.6, + 9.4, + 9.6, + 9.1, + 9.2, + 9.4, + 9.2, + 9.5, + 10.6, + 9.2, + 9.9, + 10.2, + 10.7, + 10.4, + 8.9, + 8.2, + 7.2, + 7.6, + 7.7, + 7.4, + 6.5, + 6.6, + 6.2, + 5.8, + 5.7, + 5.6, + 5.9, + 6.2, + 6.6, + 7, + 7, + 6.9, + 6.8, + 6.6, + 6.2, + 5.9, + 5.8, + 5.8, + 5.6, + 5.1, + 4.5, + 4.1, + 4.3, + 4.9, + 5.6, + 5.7, + 5.5, + 5.3, + 5.4, + 5.7, + 5.9, + 5.6, + 5.2, + 4.7, + 4.5, + 4.4, + 4.3, + 4.6, + 5.4, + 6.5, + 8.7, + 7.7, + 6.8, + 7.5, + 8.6, + 9.8, + 11.1, + 11.7, + 12.5, + 13, + 13.4, + 13.6, + 13.4, + 12.9, + 12.5, + 12.8, + 13.5, + 14, + 13.8, + 13.3, + 12.4, + 11.9, + 11.3, + 10, + 8.2, + 6, + 5.5, + 7.4, + 10, + 13, + 14.3, + 15.2, + 15.8, + 16.1 + ], + "vapor_pressure_deficit": [ + 0.05, + 0.05, + 0.06, + 0.07, + 0.05, + 0.05, + 0.05, + 0.05, + 0.05, + 0.05, + 0.08, + 0.12, + 0.16, + 0.17, + 0.19, + 0.16, + 0.11, + 0.11, + 0.07, + 0.06, + 0.05, + 0.04, + 0.04, + 0.04, + 0.05, + 0.07, + 0.09, + 0.12, + 0.1, + 0.08, + 0.05, + 0.03, + 0.02, + 0, + 0.03, + 0.08, + 0.11, + 0.14, + 0.18, + 0.15, + 0.14, + 0.14, + 0.12, + 0.12, + 0.13, + 0.13, + 0.12, + 0.11, + 0.1, + 0.08, + 0.08, + 0.07, + 0.06, + 0.07, + 0.06, + 0.08, + 0.09, + 0.1, + 0.13, + 0.16, + 0.19, + 0.22, + 0.23, + 0.21, + 0.14, + 0.11, + 0.09, + 0.08, + 0.13, + 0.13, + 0.14, + 0.15, + 0.15, + 0.15, + 0.14, + 0.14, + 0.13, + 0.12, + 0.11, + 0.11, + 0.1, + 0.09, + 0.12, + 0.16, + 0.21, + 0.25, + 0.27, + 0.25, + 0.18, + 0.14, + 0.12, + 0.12, + 0.13, + 0.12, + 0.1, + 0.09, + 0.08, + 0.06, + 0.05, + 0.05, + 0.05, + 0.05, + 0.04, + 0.04, + 0.05, + 0.06, + 0.08, + 0.1, + 0.14, + 0.17, + 0.18, + 0.17, + 0.15, + 0.13, + 0.12, + 0.1, + 0.09, + 0.08, + 0.08, + 0.08, + 0.08, + 0.08, + 0.08, + 0.08, + 0.08, + 0.07, + 0.07, + 0.07, + 0.06, + 0.05, + 0.05, + 0.07, + 0.09, + 0.13, + 0.14, + 0.13, + 0.11, + 0.09, + 0.06, + 0.03, + 0.04, + 0.05, + 0.05, + 0.05, + 0.05, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.05, + 0.06, + 0.07, + 0.07, + 0.08, + 0.1, + 0.14, + 0.17, + 0.16, + 0.14, + 0.11, + 0.09, + 0.07, + 0.06, + 0.07, + 0.08, + 0.1, + 0.09 + ], + "windgusts_10m": [ + 22.1, + 23.8, + 23.5, + 21.5, + 18.4, + 19.6, + 19.9, + 20.3, + 19.6, + 20.8, + 25, + 19.1, + 17.9, + 17.6, + 16.8, + 15.7, + 14, + 8.5, + 10.4, + 11, + 9.5, + 10.2, + 11.1, + 11.8, + 12.2, + 13, + 14.4, + 13.3, + 13.2, + 12.5, + 12.7, + 10.6, + 10.4, + 15.6, + 16.8, + 18.8, + 15.9, + 17.8, + 25.3, + 24.6, + 22.5, + 21.4, + 23.4, + 23.3, + 24.7, + 25.3, + 26.2, + 29.2, + 28.2, + 27.9, + 26.9, + 26.8, + 28, + 27.2, + 23.5, + 24.9, + 24.6, + 31.8, + 32.6, + 27.5, + 25.1, + 23.1, + 20.4, + 20.2, + 24, + 20.1, + 16.8, + 16, + 20.5, + 20.1, + 21.1, + 22.2, + 23.6, + 24.4, + 25.7, + 26.4, + 26.3, + 27.2, + 27.1, + 27, + 26.4, + 26.4, + 27.1, + 28.7, + 28.1, + 27.2, + 26.5, + 25.5, + 23.6, + 23.6, + 22.2, + 21.4, + 20.4, + 19.4, + 18.4, + 18.1, + 17.7, + 17.4, + 17.6, + 17.9, + 18.1, + 18.4, + 18.6, + 18.9, + 19.2, + 19.5, + 19.9, + 21.5, + 23, + 24.6, + 24.1, + 23.6, + 23.1, + 21.4, + 19.8, + 18.1, + 15.8, + 13.5, + 11.2, + 10.9, + 10.6, + 10.3, + 8.9, + 7.5, + 6, + 5.1, + 4.2, + 3.3, + 5.5, + 7.8, + 10, + 14.4, + 18.8, + 23.2, + 24, + 25.6, + 27.2, + 26.1, + 25, + 24, + 25.6, + 27.1, + 28.7, + 29.6, + 30.5, + 31.4, + 30.5, + 29.7, + 28.8, + 30.3, + 31.8, + 33.3, + 32.3, + 31.3, + 30.4, + 28.3, + 26.2, + 24.2, + 21.1, + 18, + 14.9, + 19.9, + 24.9, + 29.9, + 32, + 34.1, + 36.3, + 36.9 + ], + "winddirection_120m": [ + 265, + 269, + 280, + 274, + 281, + 282, + 279, + 275, + 270, + 273, + 266, + 259, + 257, + 266, + 254, + 258, + 245, + 240, + 202, + 205, + 189, + 192, + 186, + 184, + 169, + 172, + 176, + 172, + 158, + 161, + 174, + 185, + 192, + 201, + 203, + 201, + 205, + 210, + 246, + 221, + 235, + 229, + 235, + 238, + 243, + 241, + 247, + 256, + 258, + 262, + 255, + 252, + 258, + 253, + 243, + 245, + 248, + 256, + 253, + 253, + 245, + 239, + 234, + 218, + 204, + 209, + 179, + 179, + 189, + 188, + 184, + 181, + 179, + 172, + 170, + 169, + 169, + 169, + 164, + 159, + 156, + 155, + 153, + 153, + 147, + 140, + 143, + 144, + 153, + 157, + 163, + 156, + 164, + 179, + 183, + 182, + 190, + 192, + 190, + 185, + 179, + 175, + 172, + 168, + 168, + 168, + 168, + 166, + 164, + 161, + 161, + 162, + 166, + 174, + 187, + 205, + 208, + 207, + 205, + 206, + 207, + 209, + 211, + 212, + 214, + 218, + 223, + 230, + 236, + 243, + 253, + 259, + 265, + 269, + 271, + 272, + 272, + 273, + 274, + 275, + 298, + 293, + 288, + 288, + 289, + 290, + 291, + 291, + 292, + 292, + 293, + 292, + 290, + 286, + 281, + 278, + 275, + 268, + 262, + 255, + 242, + 227, + 215, + 207, + 204, + 203, + 202, + 201 + ], + "windspeed_120m": [ + 24.7, + 24.5, + 23.8, + 20.2, + 21.5, + 22, + 23.5, + 22.3, + 22.2, + 21.2, + 18.6, + 14.6, + 13.8, + 13.2, + 12, + 12.7, + 9.1, + 8.1, + 11.3, + 12.5, + 13.6, + 15, + 15.3, + 16.1, + 17.6, + 18, + 17.9, + 16, + 14.8, + 15, + 14.5, + 12.9, + 14.8, + 18.2, + 10.1, + 9.9, + 9.7, + 13, + 21.6, + 22.1, + 19.3, + 23.4, + 26.6, + 25.5, + 26.5, + 27.7, + 29.1, + 30.2, + 29, + 27.8, + 27, + 26.9, + 27.4, + 23.8, + 22.1, + 23.9, + 23.4, + 28.4, + 23.9, + 21.3, + 19.3, + 15.9, + 16.5, + 16.4, + 19, + 15.9, + 16.9, + 19.5, + 27.9, + 29.4, + 30.7, + 31.2, + 29.3, + 28.6, + 31.6, + 34.1, + 35.2, + 33.5, + 32.8, + 33.5, + 33.4, + 34.1, + 33.3, + 27.9, + 19.2, + 18.6, + 20.4, + 23.3, + 27.3, + 27.1, + 26.5, + 27.4, + 29.3, + 28.8, + 22.7, + 23.9, + 25, + 24.2, + 24, + 23.5, + 23.2, + 23.4, + 23.7, + 24.1, + 24.4, + 24.6, + 24, + 22.1, + 19.5, + 17, + 17.2, + 18.2, + 18.5, + 16.5, + 14, + 13.5, + 15.8, + 19.2, + 22.4, + 22.1, + 20.5, + 18.9, + 19.1, + 19.8, + 20, + 19.1, + 17.7, + 15.9, + 14.8, + 13.8, + 13.4, + 14.1, + 15.5, + 17.2, + 18.2, + 20.8, + 23.9, + 26, + 28, + 30.1, + 34.7, + 33.6, + 32.6, + 32.7, + 32.9, + 33.2, + 33.3, + 33.4, + 33.2, + 33, + 32.6, + 31.4, + 29.6, + 27.3, + 24.3, + 22, + 19.6, + 17.3, + 16.7, + 17, + 18.5, + 21.8, + 27.2, + 34.1, + 37, + 38.7, + 39.8, + 40.2 + ], + "windspeed_80m": [ + 20.9, + 21.3, + 19.9, + 16.7, + 18.1, + 18.4, + 19.3, + 18.9, + 17.9, + 18.3, + 15.5, + 13, + 13, + 12.3, + 11.4, + 11.5, + 7.7, + 7.2, + 10.1, + 10.6, + 11.3, + 12, + 12.3, + 12.7, + 14.1, + 13.8, + 14, + 12.1, + 12.3, + 12.8, + 12.3, + 11.4, + 12.5, + 11.3, + 9.7, + 9.2, + 9.3, + 12, + 20, + 19.4, + 16.4, + 20.5, + 21.8, + 21.8, + 22.3, + 23.6, + 24.6, + 25.8, + 25.1, + 24.1, + 23.4, + 23.6, + 24, + 20.7, + 19.2, + 20.8, + 20.6, + 26.7, + 21.8, + 19.9, + 17.9, + 14.8, + 15.5, + 15.4, + 17.2, + 14.5, + 13.1, + 14.8, + 21.2, + 21.6, + 22.6, + 21.9, + 20.4, + 20.2, + 22.7, + 24.1, + 25.2, + 24.1, + 24.8, + 25, + 24.5, + 25.1, + 21.7, + 16.7, + 17.6, + 17.5, + 19.2, + 20.9, + 20.8, + 20.1, + 19.4, + 20.7, + 21.7, + 21.6, + 18.2, + 18.9, + 19, + 18.7, + 18.5, + 18.2, + 18.3, + 18.7, + 19.3, + 20, + 20.3, + 20.5, + 19.7, + 17.6, + 14.7, + 12.2, + 12.8, + 14.5, + 15.8, + 14.3, + 12.2, + 11.3, + 13, + 15.5, + 17.9, + 17.6, + 16.4, + 15.1, + 15, + 15.2, + 15.1, + 14.5, + 13.6, + 12.4, + 11.5, + 10.7, + 10.2, + 10.6, + 11.9, + 13.5, + 16.1, + 15.4, + 15.3, + 16.5, + 18.4, + 20.4, + 24.6, + 25.3, + 26.3, + 27.2, + 28, + 28.4, + 27.8, + 26.7, + 25.7, + 26.2, + 27.1, + 27.6, + 26.7, + 25.1, + 22.8, + 21, + 19, + 16.5, + 14.9, + 13.8, + 14.3, + 17, + 21.4, + 26.9, + 29.7, + 31.7, + 33.3, + 33.9 + ], + "evapotranspiration": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.01, + 0.01, + 0.02, + 0.02, + 0.02, + 0.02, + 0.01, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.01, + 0.01, + 0.01, + 0.01, + 0, + 0, + 0, + 0, + 0.01, + 0.02, + 0.02, + 0.02, + 0.03, + 0.02, + 0.02, + 0.01, + 0.01, + 0.01, + 0.02, + 0.02, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.02, + 0.02, + 0.03, + 0.02, + 0.02, + 0.02, + 0.02, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.01, + 0.01, + 0.02, + 0.02, + 0.03, + 0.03, + 0.03, + 0.02, + 0.02, + 0.02, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.01, + 0.01, + 0.02, + 0.02, + 0.03, + 0.03, + 0.02, + 0.02, + 0.01, + 0.01, + 0.01, + 0.01, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.03, + 0.03, + 0.03, + 0.02, + 0.02, + 0.02, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.02 + ], + "precipitation": [ + 0, + 0, + 0.01, + 0, + 0.03, + 0.01, + 0, + 0, + 0, + 0, + 0, + 0, + 0.01, + 0.01, + 0, + 0.06, + 0.06, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.03, + 0.07, + 0, + 0, + 0, + 0.09, + 0.09, + 0.01, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.01, + 0.1, + 0.3, + 0.2, + 0.15, + 0, + 0, + 0, + 0, + 0.01, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.1, + 0.01, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.05, + 0.05, + 0.05, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.01, + 0.01, + 0.01, + 0.03, + 0.03, + 0.03, + 0.02, + 0.02, + 0.02, + 0.13, + 0.13, + 0.13, + 0, + 0, + 0, + 0.07, + 0.07, + 0.07, + 0.16, + 0.16, + 0.16, + 0.01, + 0.01, + 0.01, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.03, + 0.03, + 0.03, + 0.04, + 0.04, + 0.04, + 0.88 + ] + }, + "daily": { + "temperature_2m_max": [ + 7.6, + 5.4, + 4.8, + 4.5, + 3.4, + 2.2, + 3 + ], + "precipitation_hours": [ + 7, + 5, + 5, + 3, + 3, + 13, + 15 + ], + "shortwave_radiation_sum": [ + 1.44, + 2.16, + 1.95, + 2.05, + 4.18, + 2.86, + 3.31 + ], + "winddirection_10m_dominant": [ + 251, + 210, + 230, + 143, + 143, + 248, + 256 + ], + "windspeed_10m_max": [ + 10.9, + 12.9, + 14.8, + 10.7, + 7, + 13, + 16.1 + ], + "apparent_temperature_min": [ + 3.4, + -2.9, + -1.9, + -4, + -3.5, + -3.6, + -4.6 + ], + "sunset": [ + "2021-11-24T16:04", + "2021-11-25T16:03", + "2021-11-26T16:02", + "2021-11-27T16:01", + "2021-11-28T16:00", + "2021-11-29T15:59", + "2021-11-30T15:59" + ], + "weathercode": [ + 61, + 61, + 61, + 61, + 61, + 77, + 80 + ], + "sunrise": [ + "2021-11-24T07:41", + "2021-11-25T07:43", + "2021-11-26T07:45", + "2021-11-27T07:46", + "2021-11-28T07:48", + "2021-11-29T07:49", + "2021-11-30T07:51" + ], + "apparent_temperature_max": [ + 5.5, + 3.2, + 1.4, + 0.6, + 0.4, + -1.2, + -0.3 + ], + "temperature_2m_min": [ + 5.5, + 0.2, + 1.8, + -0.1, + -0.2, + -0.5, + -0.3 + ], + "windgusts_10m_max": [ + 8.5, + 10.4, + 16, + 18.1, + 10.9, + 3.3, + 14.9 + ], + "precipitation_sum": [ + 0.19, + 0.29, + 0.76, + 0.12, + 0.15, + 0.64, + 1.74 + ], + "time": [ + "2021-11-24", + "2021-11-25", + "2021-11-26", + "2021-11-27", + "2021-11-28", + "2021-11-29", + "2021-11-30" + ] + }, + "utc_offset_seconds": 3600, + "hourly_units": { + "precipitation": "mm", + "shortwave_radiation": "W\/m²", + "soil_moisture_0_1cm": "m³\/m³", + "pressure_msl": "hPa", + "soil_moisture_3_9cm": "m³\/m³", + "soil_temperature_54cm": "°C", + "soil_temperature_18cm": "°C", + "winddirection_120m": "°", + "vapor_pressure_deficit": "kPa", + "dewpoint_2m": "°C", + "winddirection_180m": "°", + "windspeed_10m": "km\/h", + "cloudcover_low": "%", + "cloudcover_mid": "%", + "cloudcover_high": "%", + "windgusts_10m": "km\/h", + "soil_moisture_9_27cm": "m³\/m³", + "windspeed_120m": "km\/h", + "winddirection_10m": "°", + "time": "iso8601", + "soil_temperature_6cm": "°C", + "apparent_temperature": "°C", + "windspeed_80m": "km\/h", + "soil_moisture_1_3cm": "m³\/m³", + "diffuse_radiation": "W\/m²", + "snow_depth": "m", + "windspeed_180m": "km\/h", + "weathercode": "wmo code", + "direct_normal_irradiance": "W\/m²", + "relativehumidity_2m": "%", + "soil_moisture_27_81cm": "m³\/m³", + "winddirection_80m": "°", + "freezinglevel_height": "m", + "evapotranspiration": "mm", + "cloudcover": "%", + "soil_temperature_0cm": "°C", + "direct_radiation": "W\/m²", + "temperature_2m": "°C" + }, + "longitude": 13.419998, + "elevation": 44.8125, + "current_weather": { + "temperature": 5.5, + "windspeed": 5.3, + "weathercode": 3, + "winddirection": 168, + "time": "2021-11-24T23:00" + } +} \ No newline at end of file diff --git a/tests/components/open_meteo/test_config_flow.py b/tests/components/open_meteo/test_config_flow.py new file mode 100644 index 00000000000..f985e2a6193 --- /dev/null +++ b/tests/components/open_meteo/test_config_flow.py @@ -0,0 +1,33 @@ +"""Tests for the Open-Meteo config flow.""" + +from unittest.mock import MagicMock + +from homeassistant.components.open_meteo.const import DOMAIN +from homeassistant.components.zone import ENTITY_ID_HOME +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_ZONE +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM + + +async def test_full_user_flow( + hass: HomeAssistant, + mock_setup_entry: MagicMock, +) -> None: + """Test the full user configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == SOURCE_USER + assert "flow_id" in result + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_ZONE: ENTITY_ID_HOME}, + ) + + assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("title") == "test home" + assert result2.get("data") == {CONF_ZONE: ENTITY_ID_HOME} diff --git a/tests/components/open_meteo/test_init.py b/tests/components/open_meteo/test_init.py new file mode 100644 index 00000000000..38619bc09db --- /dev/null +++ b/tests/components/open_meteo/test_init.py @@ -0,0 +1,68 @@ +"""Tests for the Open-Meteo integration.""" +from unittest.mock import AsyncMock, MagicMock, patch + +from open_meteo import OpenMeteoConnectionError +from pytest import LogCaptureFixture + +from homeassistant.components.open_meteo.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_ZONE +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_load_unload_config_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_open_meteo: AsyncMock, +) -> None: + """Test the Open-Meteo configuration entry loading/unloading.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert not hass.data.get(DOMAIN) + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + +@patch( + "homeassistant.components.open_meteo.OpenMeteo.forecast", + side_effect=OpenMeteoConnectionError, +) +async def test_config_entry_not_ready( + mock_forecast: MagicMock, + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the Open-Meteo configuration entry not ready.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_forecast.call_count == 1 + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_config_entry_zone_removed( + hass: HomeAssistant, + caplog: LogCaptureFixture, +) -> None: + """Test the Open-Meteo configuration entry not ready.""" + mock_config_entry = MockConfigEntry( + title="My Castle", + domain=DOMAIN, + data={CONF_ZONE: "zone.castle"}, + unique_id="zone.castle", + ) + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + assert "Zone 'zone.castle' not found" in caplog.text From e712b80650dbae02779126013a9b26ee082a5913 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 6 Dec 2021 10:08:52 -0800 Subject: [PATCH 0086/2644] Return native timestamps for home connect (#61116) --- homeassistant/components/home_connect/sensor.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/home_connect/sensor.py b/homeassistant/components/home_connect/sensor.py index 373ad6be295..910bec3e6ab 100644 --- a/homeassistant/components/home_connect/sensor.py +++ b/homeassistant/components/home_connect/sensor.py @@ -63,16 +63,14 @@ class HomeConnectSensor(HomeConnectEntity, SensorEntity): elif ( self._state is not None and self._sign == 1 - and dt_util.parse_datetime(self._state) < dt_util.utcnow() + and self._state < dt_util.utcnow() ): # if the date is supposed to be in the future but we're # already past it, set state to None. self._state = None else: seconds = self._sign * float(status[self._key][ATTR_VALUE]) - self._state = ( - dt_util.utcnow() + timedelta(seconds=seconds) - ).isoformat() + self._state = dt_util.utcnow() + timedelta(seconds=seconds) else: self._state = status[self._key].get(ATTR_VALUE) if self._key == BSH_OPERATION_STATE: From d64bf3cc6c6a1fc43b980a845e60b6d108ae8590 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 6 Dec 2021 20:16:39 +0100 Subject: [PATCH 0087/2644] Use dataclass properties in Zeroconf tests (#61109) Co-authored-by: epenet --- tests/components/zeroconf/test_init.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 1bd96a306eb..e306edba357 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -671,12 +671,12 @@ async def test_info_from_service_non_utf8(hass): info = zeroconf.info_from_service( get_service_info_mock(service_type, f"test.{service_type}") ) - raw_info = info["properties"].pop("_raw", False) + raw_info = info.properties.pop("_raw", False) assert raw_info assert len(raw_info) == len(PROPERTIES) - 1 assert NON_ASCII_KEY not in raw_info - assert len(info["properties"]) <= len(raw_info) - assert "non-utf8-value" not in info["properties"] + assert len(info.properties) <= len(raw_info) + assert "non-utf8-value" not in info.properties assert raw_info["non-utf8-value"] is NON_UTF8_VALUE @@ -695,7 +695,7 @@ async def test_info_from_service_with_link_local_address_first(hass): service_info = get_service_info_mock(service_type, f"test.{service_type}") service_info.addresses = ["169.254.12.3", "192.168.66.12"] info = zeroconf.info_from_service(service_info) - assert info["host"] == "192.168.66.12" + assert info.host == "192.168.66.12" async def test_info_from_service_with_link_local_address_second(hass): @@ -704,7 +704,7 @@ async def test_info_from_service_with_link_local_address_second(hass): service_info = get_service_info_mock(service_type, f"test.{service_type}") service_info.addresses = ["192.168.66.12", "169.254.12.3"] info = zeroconf.info_from_service(service_info) - assert info["host"] == "192.168.66.12" + assert info.host == "192.168.66.12" async def test_info_from_service_with_link_local_address_only(hass): @@ -722,7 +722,7 @@ async def test_info_from_service_prefers_ipv4(hass): service_info = get_service_info_mock(service_type, f"test.{service_type}") service_info.addresses = ["2001:db8:3333:4444:5555:6666:7777:8888", "192.168.66.12"] info = zeroconf.info_from_service(service_info) - assert info["host"] == "192.168.66.12" + assert info.host == "192.168.66.12" async def test_get_instance(hass, mock_async_zeroconf): From 97d292133f2234b7a318eb1b77c0f7bc1657c49c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 6 Dec 2021 23:35:14 +0100 Subject: [PATCH 0088/2644] Revert "Add Open-Meteo integration (#60379)" (#61130) This reverts commit d802f3a82f84b040b03b4d6566f2ab9709aaa837. --- .coveragerc | 1 - .pre-commit-config.yaml | 2 +- .strict-typing | 1 - CODEOWNERS | 1 - .../components/open_meteo/__init__.py | 56 - .../components/open_meteo/config_flow.py | 54 - homeassistant/components/open_meteo/const.py | 56 - .../components/open_meteo/manifest.json | 10 - .../components/open_meteo/strings.json | 12 - .../open_meteo/translations/en.json | 12 - .../components/open_meteo/weather.py | 80 - homeassistant/generated/config_flows.py | 1 - mypy.ini | 11 - requirements_all.txt | 3 - requirements_test_all.txt | 3 - tests/components/open_meteo/__init__.py | 1 - tests/components/open_meteo/conftest.py | 49 - .../open_meteo/fixtures/forecast.json | 6660 ----------------- .../components/open_meteo/test_config_flow.py | 33 - tests/components/open_meteo/test_init.py | 68 - 20 files changed, 1 insertion(+), 7113 deletions(-) delete mode 100644 homeassistant/components/open_meteo/__init__.py delete mode 100644 homeassistant/components/open_meteo/config_flow.py delete mode 100644 homeassistant/components/open_meteo/const.py delete mode 100644 homeassistant/components/open_meteo/manifest.json delete mode 100644 homeassistant/components/open_meteo/strings.json delete mode 100644 homeassistant/components/open_meteo/translations/en.json delete mode 100644 homeassistant/components/open_meteo/weather.py delete mode 100644 tests/components/open_meteo/__init__.py delete mode 100644 tests/components/open_meteo/conftest.py delete mode 100644 tests/components/open_meteo/fixtures/forecast.json delete mode 100644 tests/components/open_meteo/test_config_flow.py delete mode 100644 tests/components/open_meteo/test_init.py diff --git a/.coveragerc b/.coveragerc index 04d14121b52..0f4c8d68d22 100644 --- a/.coveragerc +++ b/.coveragerc @@ -767,7 +767,6 @@ omit = homeassistant/components/onvif/event.py homeassistant/components/onvif/parsers.py homeassistant/components/onvif/sensor.py - homeassistant/components/open_meteo/weather.py homeassistant/components/opencv/* homeassistant/components/openevse/sensor.py homeassistant/components/openexchangerates/sensor.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 17581c68c80..91216c9efa9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: hooks: - id: codespell args: - - --ignore-words-list=hass,alot,datas,dof,dur,ether,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,rime,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing,iam,incomfort,ba,haa + - --ignore-words-list=hass,alot,datas,dof,dur,ether,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing,iam,incomfort,ba,haa - --skip="./.*,*.csv,*.json" - --quiet-level=2 exclude_types: [csv, json] diff --git a/.strict-typing b/.strict-typing index f663ec6a6b9..5546086a456 100644 --- a/.strict-typing +++ b/.strict-typing @@ -95,7 +95,6 @@ homeassistant.components.notify.* homeassistant.components.notion.* homeassistant.components.number.* homeassistant.components.onewire.* -homeassistant.components.open_meteo.* homeassistant.components.openuv.* homeassistant.components.persistent_notification.* homeassistant.components.pi_hole.* diff --git a/CODEOWNERS b/CODEOWNERS index fa59d00788b..a349954bf65 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -380,7 +380,6 @@ homeassistant/components/onboarding/* @home-assistant/core homeassistant/components/ondilo_ico/* @JeromeHXP homeassistant/components/onewire/* @garbled1 @epenet homeassistant/components/onvif/* @hunterjm -homeassistant/components/open_meteo/* @frenck homeassistant/components/openerz/* @misialq homeassistant/components/opengarage/* @danielhiversen homeassistant/components/openhome/* @bazwilliams diff --git a/homeassistant/components/open_meteo/__init__.py b/homeassistant/components/open_meteo/__init__.py deleted file mode 100644 index 348f0616556..00000000000 --- a/homeassistant/components/open_meteo/__init__.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Support for Open-Meteo.""" -from __future__ import annotations - -from open_meteo import Forecast, OpenMeteo, OpenMeteoError - -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, CONF_ZONE, Platform -from homeassistant.core import HomeAssistant -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed - -from .const import DOMAIN, LOGGER, SCAN_INTERVAL - -PLATFORMS = [Platform.WEATHER] - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up Open-Meteo from a config entry.""" - session = async_get_clientsession(hass) - open_meteo = OpenMeteo(session=session) - - async def async_update_forecast() -> Forecast: - zone = hass.states.get(entry.data[CONF_ZONE]) - if zone is None: - raise UpdateFailed(f"Zone '{entry.data[CONF_ZONE]}' not found") - - try: - return await open_meteo.forecast( - latitude=zone.attributes[ATTR_LATITUDE], - longitude=zone.attributes[ATTR_LONGITUDE], - current_weather=True, - ) - except OpenMeteoError as err: - raise UpdateFailed("Open-Meteo API communication error") from err - - coordinator: DataUpdateCoordinator[Forecast] = DataUpdateCoordinator( - hass, - LOGGER, - name=f"{DOMAIN}_{entry.data[CONF_ZONE]}", - update_interval=SCAN_INTERVAL, - update_method=async_update_forecast, - ) - await coordinator.async_config_entry_first_refresh() - - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) - - return True - - -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Unload Open-Meteo config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - del hass.data[DOMAIN][entry.entry_id] - return unload_ok diff --git a/homeassistant/components/open_meteo/config_flow.py b/homeassistant/components/open_meteo/config_flow.py deleted file mode 100644 index 76d5436f565..00000000000 --- a/homeassistant/components/open_meteo/config_flow.py +++ /dev/null @@ -1,54 +0,0 @@ -"""Config flow to configure the Open-Meteo integration.""" -from __future__ import annotations - -from typing import Any - -import voluptuous as vol - -from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN, ENTITY_ID_HOME -from homeassistant.config_entries import ConfigFlow -from homeassistant.const import CONF_ZONE -from homeassistant.data_entry_flow import FlowResult - -from .const import DOMAIN - - -class OpenMeteoFlowHandler(ConfigFlow, domain=DOMAIN): - """Config flow for OpenMeteo.""" - - VERSION = 1 - - async def async_step_user( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle a flow initialized by the user.""" - if user_input is not None: - await self.async_set_unique_id(user_input[CONF_ZONE]) - self._abort_if_unique_id_configured() - - zone = self.hass.states.get(user_input[CONF_ZONE]) - return self.async_create_entry( - title=zone.name if zone else "Open-Meteo", - data={CONF_ZONE: user_input[CONF_ZONE]}, - ) - - zones: dict[str, str] = { - entity_id: state.name - for entity_id in self.hass.states.async_entity_ids(ZONE_DOMAIN) - if (state := self.hass.states.get(entity_id)) is not None - } - zones = dict(sorted(zones.items(), key=lambda x: x[1], reverse=True)) - - return self.async_show_form( - step_id="user", - data_schema=vol.Schema( - { - vol.Required(CONF_ZONE): vol.In( - { - ENTITY_ID_HOME: zones.pop(ENTITY_ID_HOME), - **zones, - } - ), - } - ), - ) diff --git a/homeassistant/components/open_meteo/const.py b/homeassistant/components/open_meteo/const.py deleted file mode 100644 index 94e27293bad..00000000000 --- a/homeassistant/components/open_meteo/const.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Constants for the Open-Meteo integration.""" -from __future__ import annotations - -from datetime import timedelta -import logging -from typing import Final - -from homeassistant.components.weather import ( - ATTR_CONDITION_CLOUDY, - ATTR_CONDITION_FOG, - ATTR_CONDITION_LIGHTNING, - ATTR_CONDITION_PARTLYCLOUDY, - ATTR_CONDITION_POURING, - ATTR_CONDITION_RAINY, - ATTR_CONDITION_SNOWY, - ATTR_CONDITION_SUNNY, -) - -DOMAIN: Final = "open_meteo" - -LOGGER = logging.getLogger(__package__) -SCAN_INTERVAL = timedelta(minutes=30) - -# World Meteorological Organization Weather Code -# mapped to Home Assistant weather conditions. -# https://www.weather.gov/tg/wmo -WMO_TO_HA_CONDITION_MAP = { - 0: ATTR_CONDITION_SUNNY, # Clear sky - 1: ATTR_CONDITION_SUNNY, # Mainly clear - 2: ATTR_CONDITION_PARTLYCLOUDY, # Partly cloudy - 3: ATTR_CONDITION_CLOUDY, # Overcast - 45: ATTR_CONDITION_FOG, # Fog - 48: ATTR_CONDITION_FOG, # Depositing rime fog - 51: ATTR_CONDITION_RAINY, # Drizzle: Light intensity - 53: ATTR_CONDITION_RAINY, # Drizzle: Moderate intensity - 55: ATTR_CONDITION_RAINY, # Drizzle: Dense intensity - 56: ATTR_CONDITION_RAINY, # Freezing Drizzle: Light intensity - 57: ATTR_CONDITION_RAINY, # Freezing Drizzle: Dense intensity - 61: ATTR_CONDITION_RAINY, # Rain: Slight intensity - 63: ATTR_CONDITION_RAINY, # Rain: Moderate intensity - 65: ATTR_CONDITION_POURING, # Rain: Heavy intensity - 66: ATTR_CONDITION_RAINY, # Freezing Rain: Light intensity - 67: ATTR_CONDITION_POURING, # Freezing Rain: Heavy intensity - 71: ATTR_CONDITION_SNOWY, # Snow fall: Slight intensity - 73: ATTR_CONDITION_SNOWY, # Snow fall: Moderate intensity - 75: ATTR_CONDITION_SNOWY, # Snow fall: Heavy intensity - 77: ATTR_CONDITION_SNOWY, # Snow grains - 80: ATTR_CONDITION_RAINY, # Rain showers: Slight intensity - 81: ATTR_CONDITION_RAINY, # Rain showers: Moderate intensity - 82: ATTR_CONDITION_POURING, # Rain showers: Violent intensity - 85: ATTR_CONDITION_SNOWY, # Snow showers: Slight intensity - 86: ATTR_CONDITION_SNOWY, # Snow showers: Heavy intensity - 95: ATTR_CONDITION_LIGHTNING, # Thunderstorm: Slight and moderate intensity - 96: ATTR_CONDITION_LIGHTNING, # Thunderstorm with slight hail - 99: ATTR_CONDITION_LIGHTNING, # Thunderstorm with heavy hail -} diff --git a/homeassistant/components/open_meteo/manifest.json b/homeassistant/components/open_meteo/manifest.json deleted file mode 100644 index 34d783387e3..00000000000 --- a/homeassistant/components/open_meteo/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "open_meteo", - "name": "Open-Meteo", - "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/open_meteo", - "requirements": ["open-meteo==0.2.0"], - "dependencies": ["zone"], - "codeowners": ["@frenck"], - "iot_class": "cloud_polling" -} diff --git a/homeassistant/components/open_meteo/strings.json b/homeassistant/components/open_meteo/strings.json deleted file mode 100644 index f2f22413403..00000000000 --- a/homeassistant/components/open_meteo/strings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "config": { - "step": { - "user": { - "description": "Select location to use for weather forecasting", - "data": { - "zone": "Zone" - } - } - } - } -} diff --git a/homeassistant/components/open_meteo/translations/en.json b/homeassistant/components/open_meteo/translations/en.json deleted file mode 100644 index 7736b1da63e..00000000000 --- a/homeassistant/components/open_meteo/translations/en.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "zone": "Zone" - }, - "description": "Select location to use for weather forecasting" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/open_meteo/weather.py b/homeassistant/components/open_meteo/weather.py deleted file mode 100644 index b34de06efe5..00000000000 --- a/homeassistant/components/open_meteo/weather.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Support for Open-Meteo weather.""" -from __future__ import annotations - -from open_meteo import Forecast as OpenMeteoForecast - -from homeassistant.components.weather import WeatherEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import TEMP_CELSIUS -from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) - -from .const import DOMAIN, WMO_TO_HA_CONDITION_MAP - - -async def async_setup_entry( - hass: HomeAssistant, - entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up Open-Meteo weather entity based on a config entry.""" - coordinator = hass.data[DOMAIN][entry.entry_id] - async_add_entities([OpenMeteoWeatherEntity(entry=entry, coordinator=coordinator)]) - - -class OpenMeteoWeatherEntity(CoordinatorEntity, WeatherEntity): - """Defines an Open-Meteo weather entity.""" - - _attr_temperature_unit = TEMP_CELSIUS - coordinator: DataUpdateCoordinator[OpenMeteoForecast] - - def __init__( - self, *, entry: ConfigEntry, coordinator: DataUpdateCoordinator - ) -> None: - """Initialize Open-Meteo weather entity.""" - super().__init__(coordinator=coordinator) - self._attr_unique_id = entry.entry_id - self._attr_name = entry.title - - self._attr_device_info = DeviceInfo( - entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, entry.entry_id)}, - manufacturer="Open-Meteo", - name=entry.title, - ) - - @property - def condition(self) -> str | None: - """Return the current condition.""" - if not self.coordinator.data.current_weather: - return None - return WMO_TO_HA_CONDITION_MAP.get( - self.coordinator.data.current_weather.weather_code - ) - - @property - def temperature(self) -> float | None: - """Return the platform temperature.""" - if not self.coordinator.data.current_weather: - return None - return self.coordinator.data.current_weather.temperature - - @property - def wind_speed(self) -> float | None: - """Return the wind speed.""" - if not self.coordinator.data.current_weather: - return None - return self.coordinator.data.current_weather.wind_speed - - @property - def wind_bearing(self) -> float | str | None: - """Return the wind bearing.""" - if not self.coordinator.data.current_weather: - return None - return self.coordinator.data.current_weather.wind_direction diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index b0f6b6e822a..596cdf03fb7 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -216,7 +216,6 @@ FLOWS = [ "ondilo_ico", "onewire", "onvif", - "open_meteo", "opengarage", "opentherm_gw", "openuv", diff --git a/mypy.ini b/mypy.ini index 475840c3b4d..47450bab8ed 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1056,17 +1056,6 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.open_meteo.*] -check_untyped_defs = true -disallow_incomplete_defs = true -disallow_subclassing_any = true -disallow_untyped_calls = true -disallow_untyped_decorators = true -disallow_untyped_defs = true -no_implicit_optional = true -warn_return_any = true -warn_unreachable = true - [mypy-homeassistant.components.openuv.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 89cb1dbe40a..b8b3e13a3c1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1134,9 +1134,6 @@ onvif-zeep-async==1.2.0 # homeassistant.components.opengarage open-garage==0.2.0 -# homeassistant.components.open_meteo -open-meteo==0.2.0 - # homeassistant.components.opencv # opencv-python-headless==4.5.2.54 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 09038d0303b..7fc449bea41 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -699,9 +699,6 @@ onvif-zeep-async==1.2.0 # homeassistant.components.opengarage open-garage==0.2.0 -# homeassistant.components.open_meteo -open-meteo==0.2.0 - # homeassistant.components.openerz openerz-api==0.1.0 diff --git a/tests/components/open_meteo/__init__.py b/tests/components/open_meteo/__init__.py deleted file mode 100644 index 11ac51700a8..00000000000 --- a/tests/components/open_meteo/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the Open-Meteo integration.""" diff --git a/tests/components/open_meteo/conftest.py b/tests/components/open_meteo/conftest.py deleted file mode 100644 index cb950dcc442..00000000000 --- a/tests/components/open_meteo/conftest.py +++ /dev/null @@ -1,49 +0,0 @@ -"""Fixtures for the Open-Meteo integration tests.""" -from __future__ import annotations - -from collections.abc import Generator -from unittest.mock import MagicMock, patch - -from open_meteo import Forecast -import pytest - -from homeassistant.components.open_meteo.const import DOMAIN -from homeassistant.const import CONF_ZONE - -from tests.common import MockConfigEntry, load_fixture - - -@pytest.fixture -def mock_config_entry() -> MockConfigEntry: - """Return the default mocked config entry.""" - return MockConfigEntry( - title="Home", - domain=DOMAIN, - data={CONF_ZONE: "zone.home"}, - unique_id="zone.home", - ) - - -@pytest.fixture -def mock_setup_entry() -> Generator[None, None, None]: - """Mock setting up a config entry.""" - with patch( - "homeassistant.components.open_meteo.async_setup_entry", return_value=True - ): - yield - - -@pytest.fixture -def mock_open_meteo(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]: - """Return a mocked Open-Meteo client.""" - fixture: str = "forecast.json" - if hasattr(request, "param") and request.param: - fixture = request.param - - forecast = Forecast.parse_raw(load_fixture(fixture, DOMAIN)) - with patch( - "homeassistant.components.open_meteo.OpenMeteo", autospec=True - ) as open_meteo_mock: - open_meteo = open_meteo_mock.return_value - open_meteo.forecast.return_value = forecast - yield open_meteo diff --git a/tests/components/open_meteo/fixtures/forecast.json b/tests/components/open_meteo/fixtures/forecast.json deleted file mode 100644 index e9510cb2d2c..00000000000 --- a/tests/components/open_meteo/fixtures/forecast.json +++ /dev/null @@ -1,6660 +0,0 @@ -{ - "generationtime_ms": 2.886056900024414, - "latitude": 52.52, - "daily_units": { - "winddirection_10m_dominant": "°", - "temperature_2m_max": "°C", - "windspeed_10m_max": "km\/h", - "sunrise": "iso8601", - "precipitation_hours": "h", - "temperature_2m_min": "°C", - "apparent_temperature_min": "°C", - "sunset": "iso8601", - "apparent_temperature_max": "°C", - "weathercode": "wmo code", - "windgusts_10m_max": "km\/h", - "shortwave_radiation_sum": "MJ\/m²", - "time": "iso8601", - "precipitation_sum": "mm" - }, - "hourly": { - "soil_moisture_3_9cm": [ - 0.308, - 0.308, - 0.308, - 0.308, - 0.309, - 0.309, - 0.309, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.306, - 0.306, - 0.306, - 0.307, - 0.307, - 0.306, - 0.306, - 0.307, - 0.307, - 0.308, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.305, - 0.305, - 0.306, - 0.308, - 0.31, - 0.306, - 0.306, - 0.306, - 0.307, - 0.307, - 0.308, - 0.308, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.306, - 0.306, - 0.307, - 0.308, - 0.308, - 0.308, - 0.31, - 0.311, - 0.311, - 0.311, - 0.311, - 0.31, - 0.31, - 0.309, - 0.309, - 0.309, - 0.309, - 0.309, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.307, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.301, - 0.301, - 0.301, - 0.301, - 0.301, - 0.301, - 0.301, - 0.301, - 0.301, - 0.301, - 0.301, - 0.301, - 0.302, - 0.303, - 0.305, - 0.305, - 0.306, - 0.306, - 0.306, - 0.305, - 0.305, - 0.304, - 0.304, - 0.304, - 0.303, - 0.303, - 0.303, - 0.303, - 0.303, - 0.303, - 0.302, - 0.302, - 0.303, - 0.308 - ], - "soil_temperature_0cm": [ - 6.6, - 6.6, - 6.5, - 6.1, - 6.1, - 6, - 6.1, - 5.7, - 5.4, - 6.3, - 7, - 7.6, - 7.9, - 8.1, - 7.9, - 7.3, - 6.7, - 6.3, - 6.3, - 5.7, - 5.5, - 5.6, - 5.6, - 4.9, - 5.1, - 4.7, - 4.8, - 4.7, - 3.6, - 2.3, - 1.4, - 0.8, - 0.4, - 1.7, - 2.7, - 3.8, - 4.8, - 5.5, - 4.8, - 4.5, - 4.1, - 3.7, - 3.6, - 3.7, - 3.7, - 3.5, - 3.5, - 3.4, - 3.6, - 3.6, - 3.2, - 3.3, - 3.3, - 3.2, - 3, - 3.1, - 3.1, - 3.3, - 4.5, - 4.9, - 5.3, - 5.5, - 5.2, - 4.6, - 3.7, - 3.3, - 3.1, - 2.8, - 2.2, - 1.9, - 1.8, - 1.7, - 1.4, - 1.1, - 0.3, - -0, - -0, - -0, - -0.1, - -0.1, - -0.2, - -0.2, - 1.4, - 2.8, - 4.6, - 5.4, - 5, - 3.7, - 2.5, - 2.6, - 2.4, - 2.6, - 2.4, - 2.1, - 1.6, - 1.7, - 0.8, - -0, - -0.2, - -0.2, - -0.2, - -0.4, - -0.6, - -0.7, - -0.3, - 0.2, - 1, - 1.9, - 2.9, - 3.8, - 3.6, - 3.1, - 2.3, - 2, - 1.7, - 1.3, - 1, - 0.7, - 0.4, - 0.3, - 0.3, - 0.2, - 0.1, - 0.1, - -0.1, - -0.2, - -0.4, - -0.5, - -0.4, - -0.1, - 0.3, - 0.8, - 1.5, - 2.1, - 2.4, - 1.8, - 1.1, - 1.1, - 1.4, - 1.6, - 1.7, - 1.8, - 1.8, - 1.7, - 1.6, - 1.4, - 1.4, - 1.4, - 1.3, - 1.1, - 0.9, - 0.6, - 0.5, - 0.5, - 0.8, - 1.5, - 2.5, - 3.4, - 3.1, - 2.5, - 1.8, - 1.8, - 2.1, - 2.4, - 2.5, - 2.5, - 2.6, - 2.7 - ], - "weathercode": [ - 3, - 3, - 3, - 3, - 51, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 61, - 61, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 2, - 1, - 1, - 1, - 2, - 3, - 3, - 3, - 3, - 3, - 3, - 61, - 3, - 3, - 3, - 61, - 61, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 61, - 61, - 61, - 61, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 2, - 3, - 3, - 3, - 0, - 0, - 1, - 1, - 2, - 3, - 61, - 3, - 3, - 3, - 3, - 3, - 3, - 1, - 0, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 61, - 61, - 61, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 3, - 77, - 77, - 77, - 3, - 3, - 3, - 3, - 3, - 3, - 51, - 51, - 51, - 3, - 3, - 3, - 3, - 3, - 3, - 53, - 53, - 53, - 3, - 3, - 3, - 2, - 2, - 2, - 1, - 1, - 1, - 3, - 3, - 3, - 61, - 61, - 61, - 61, - 61, - 61, - 80 - ], - "windspeed_180m": [ - 26.8, - 26.6, - 27.5, - 24.6, - 27.8, - 28.5, - 27, - 24.9, - 26.6, - 22.8, - 21.1, - 18.6, - 14.3, - 14, - 12.3, - 13.3, - 10.8, - 8.6, - 10.7, - 12, - 14, - 16.4, - 17.1, - 17.9, - 20, - 22.4, - 20.9, - 19.5, - 16.9, - 16.5, - 14.5, - 13.8, - 16.2, - 20.7, - 16.3, - 12.2, - 13.8, - 14.6, - 22.7, - 26.5, - 22.9, - 30.1, - 34.3, - 35.4, - 31.3, - 30.7, - 33.5, - 34.4, - 32.8, - 31.4, - 29.8, - 30.4, - 30.5, - 27.5, - 26.1, - 27.1, - 25.9, - 27.8, - 25.4, - 22.1, - 20.1, - 16.7, - 17.1, - 17, - 20.1, - 16.7, - 20.3, - 23.3, - 32.4, - 34.8, - 36.2, - 35.8, - 34.2, - 33, - 36.1, - 38.7, - 38.8, - 37, - 35.5, - 35.7, - 35.9, - 36.9, - 37.6, - 35.1, - 30, - 25, - 21.1, - 24.5, - 29.8, - 30.4, - 29.5, - 29.8, - 31.9, - 32.2, - 24.2, - 25.7, - 27.5, - 25.7, - 26.5, - 27.4, - 28.5, - 28.8, - 28.9, - 28.6, - 28, - 27.1, - 25.8, - 24.6, - 23.2, - 21.7, - 21.5, - 21.6, - 20.9, - 18.8, - 16.6, - 16.5, - 18.7, - 21.9, - 24.6, - 24.1, - 22.4, - 20.4, - 20.1, - 20.3, - 20, - 19.2, - 18.2, - 17.1, - 16.1, - 15.2, - 15.1, - 16, - 17.6, - 19.5, - 21.8, - 25.4, - 30, - 33.1, - 36.4, - 40, - 41.9, - 43.2, - 43.6, - 41.9, - 39, - 35.9, - 35.1, - 34.9, - 34.7, - 34.5, - 34.3, - 33, - 30.8, - 28, - 24.4, - 21.9, - 19.5, - 17.8, - 18.4, - 20, - 23.4, - 28.5, - 36.2, - 44.9, - 47.4, - 47.8, - 47.9, - 48.8 - ], - "soil_moisture_9_27cm": [ - 0.315, - 0.315, - 0.314, - 0.314, - 0.315, - 0.315, - 0.315, - 0.315, - 0.315, - 0.315, - 0.315, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.314, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.313, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.308, - 0.309, - 0.309, - 0.309, - 0.309, - 0.309, - 0.309, - 0.309, - 0.309, - 0.309, - 0.309, - 0.309, - 0.309, - 0.309, - 0.309, - 0.309 - ], - "shortwave_radiation": [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.7, - 23, - 42.1, - 69.7, - 83, - 80.3, - 64.1, - 28, - 7.9, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.7, - 32.4, - 77.2, - 109.4, - 124.7, - 120.1, - 93.1, - 26.9, - 14.6, - 0, - -0, - 0, - -0, - 0, - 0, - -0, - -0, - -0, - 0, - 0, - -0, - -0, - 0, - -0, - 0.3, - 17.4, - 58.5, - 92.6, - 107.1, - 114.2, - 92.2, - 46.9, - 12.3, - 0, - -0, - 0, - -0, - 0.1, - -0, - -0.1, - 0.1, - -0.1, - 0, - 0.1, - -0, - 0.1, - 0, - -0.2, - 0.3, - 27.8, - 71.4, - 115.2, - 117.8, - 83.3, - 87.2, - 54.4, - 11.1, - -0.2, - -0.1, - -0, - -0, - -0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - -0, - -0, - 0, - 0, - 51.3, - 121.4, - 182, - 227.4, - 239.7, - 191.8, - 114.5, - 32.3, - 0, - 0, - 0, - -0, - -0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - -0, - -0, - 0, - 0, - 44.9, - 103.3, - 139.2, - 150.5, - 141.8, - 116.6, - 74.9, - 23.4, - 0, - 0, - 0, - -0, - -0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - -0, - -0, - 0, - 0, - 30.4, - 75.2, - 126.8, - 179.4, - 208.1, - 169.6, - 100.9, - 28.2, - 0, - 0, - 0, - -0, - -0, - 0, - 0 - ], - "cloudcover_mid": [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 66, - 88, - 100, - 64, - 63, - 0, - 0, - 0, - 0, - 0, - 0, - 6, - 62, - 62, - 78, - 100, - 92, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 98, - 94, - 95, - 100, - 100, - 100, - 85, - 29, - 21, - 10, - 51, - 0, - 5, - 10, - 16, - 52, - 98, - 100, - 100, - 100, - 100, - 64, - 39, - 35, - 12, - 0, - 33, - 67, - 100, - 95, - 90, - 85, - 90, - 95, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 90, - 81, - 71, - 77, - 82, - 88, - 92, - 96, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 93, - 86, - 79, - 71, - 63, - 55, - 44, - 64, - 84, - 86, - 88, - 90, - 41, - 42, - 42, - 50, - 59, - 67, - 56, - 44, - 33, - 22, - 11, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 20, - 40, - 60, - 73, - 87, - 100, - 100, - 100, - 100, - 94 - ], - "cloudcover": [ - 100, - 100, - 98, - 100, - 100, - 98, - 96, - 88, - 88, - 100, - 94, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 83, - 87, - 100, - 100, - 100, - 79, - 34, - 7, - 0, - 49, - 100, - 99, - 100, - 100, - 95, - 100, - 100, - 100, - 100, - 96, - 95, - 100, - 100, - 100, - 96, - 97, - 100, - 91, - 93, - 100, - 91, - 98, - 100, - 99, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 74, - 100, - 100, - 100, - 0, - 5, - 13, - 18, - 52, - 99, - 100, - 100, - 100, - 100, - 94, - 86, - 88, - 43, - 0, - 33, - 67, - 100, - 99, - 98, - 97, - 98, - 99, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 94, - 88, - 82, - 87, - 92, - 97, - 98, - 99, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 98, - 97, - 95, - 97, - 98, - 100, - 99, - 98, - 98, - 98, - 98, - 98, - 94, - 97, - 99, - 98, - 98, - 97, - 96, - 95, - 95, - 94, - 93, - 92, - 86, - 81, - 75, - 61, - 47, - 33, - 54, - 75, - 96, - 97, - 99, - 100, - 100, - 100, - 100, - 100 - ], - "relativehumidity_2m": [ - 95, - 95, - 94, - 93, - 95, - 95, - 94, - 94, - 95, - 94, - 92, - 88, - 85, - 83, - 82, - 84, - 89, - 89, - 93, - 93, - 95, - 96, - 95, - 96, - 94, - 92, - 89, - 86, - 87, - 90, - 93, - 96, - 97, - 100, - 96, - 88, - 85, - 83, - 79, - 82, - 84, - 83, - 85, - 85, - 84, - 84, - 85, - 86, - 87, - 89, - 90, - 91, - 92, - 91, - 92, - 90, - 88, - 87, - 84, - 80, - 77, - 74, - 73, - 75, - 82, - 86, - 89, - 89, - 83, - 82, - 80, - 78, - 78, - 78, - 79, - 79, - 79, - 81, - 82, - 83, - 83, - 85, - 82, - 77, - 73, - 69, - 68, - 70, - 76, - 82, - 84, - 84, - 83, - 84, - 86, - 87, - 89, - 92, - 92, - 92, - 92, - 93, - 93, - 93, - 92, - 90, - 88, - 85, - 81, - 77, - 77, - 79, - 81, - 83, - 84, - 86, - 87, - 88, - 89, - 88, - 88, - 87, - 87, - 87, - 88, - 88, - 88, - 89, - 90, - 91, - 92, - 89, - 86, - 82, - 81, - 82, - 84, - 87, - 91, - 95, - 94, - 93, - 93, - 93, - 93, - 94, - 94, - 95, - 94, - 93, - 92, - 90, - 89, - 88, - 87, - 83, - 79, - 76, - 77, - 81, - 85, - 88, - 90, - 91, - 91, - 89, - 87, - 88 - ], - "cloudcover_low": [ - 100, - 100, - 98, - 100, - 100, - 98, - 96, - 88, - 88, - 100, - 94, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 83, - 87, - 100, - 100, - 100, - 79, - 34, - 7, - 0, - 49, - 100, - 99, - 100, - 100, - 95, - 100, - 100, - 100, - 91, - 92, - 92, - 91, - 85, - 90, - 96, - 97, - 100, - 91, - 93, - 100, - 86, - 95, - 100, - 97, - 100, - 96, - 93, - 64, - 81, - 62, - 80, - 83, - 89, - 86, - 79, - 20, - 20, - 48, - 71, - 64, - 49, - 19, - 0, - 0, - 0, - 31, - 4, - 0, - 0, - 0, - 0, - 0, - 0, - 2, - 0, - 70, - 80, - 89, - 100, - 64, - 88, - 75, - 76, - 34, - 0, - 3, - 6, - 9, - 11, - 12, - 14, - 25, - 36, - 47, - 38, - 29, - 19, - 40, - 61, - 81, - 79, - 77, - 75, - 75, - 75, - 74, - 69, - 64, - 59, - 59, - 58, - 58, - 65, - 72, - 79, - 81, - 84, - 86, - 91, - 95, - 100, - 99, - 98, - 97, - 93, - 90, - 87, - 88, - 93, - 98, - 97, - 96, - 95, - 93, - 91, - 89, - 90, - 91, - 92, - 86, - 81, - 75, - 61, - 47, - 33, - 52, - 71, - 90, - 93, - 96, - 99, - 89, - 80, - 70, - 80 - ], - "soil_moisture_27_81cm": [ - 0.332, - 0.332, - 0.332, - 0.332, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.331, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.33, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.329, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324, - 0.324 - ], - "temperature_2m": [ - 6.9, - 6.8, - 6.8, - 6.6, - 6.5, - 6.4, - 6.4, - 6.2, - 5.9, - 6, - 6.5, - 6.9, - 7.3, - 7.6, - 7.6, - 7.5, - 7.2, - 6.9, - 6.6, - 6.4, - 6, - 5.9, - 5.8, - 5.5, - 5.4, - 5.1, - 5, - 4.9, - 4.2, - 3.2, - 2.2, - 1.5, - 0.8, - 0.2, - 0.6, - 1.8, - 2.7, - 3.8, - 4.5, - 4.5, - 4.4, - 4.2, - 3.9, - 3.9, - 4, - 3.9, - 3.9, - 3.8, - 3.7, - 3.6, - 3.4, - 3.3, - 3.2, - 3.2, - 3.1, - 3.1, - 3.1, - 3.1, - 3.5, - 3.9, - 4.3, - 4.6, - 4.8, - 4.6, - 4, - 3.5, - 3.3, - 3, - 2.7, - 2.3, - 2, - 1.8, - 1.6, - 1.2, - 0.7, - 0.4, - 0.3, - 0.2, - 0.2, - 0.1, - -0, - -0.1, - 0.6, - 1.7, - 2.9, - 4, - 4.5, - 4.2, - 3.5, - 3, - 2.8, - 2.8, - 2.8, - 2.6, - 2.3, - 2, - 1.6, - 0.9, - 0.6, - 0.4, - 0.1, - -0, - -0.2, - -0.2, - -0.1, - 0, - 0.5, - 1.2, - 2.2, - 3.1, - 3.4, - 3.4, - 3.2, - 3, - 2.7, - 2.2, - 1.9, - 1.5, - 1.1, - 0.9, - 0.8, - 0.6, - 0.4, - 0.2, - -0.1, - -0.3, - -0.4, - -0.5, - -0.5, - -0.5, - -0.3, - 0.3, - 1.1, - 2, - 2.2, - 2, - 1.7, - 1.4, - 1, - 0.7, - 2, - 1.9, - 1.8, - 1.8, - 1.7, - 1.6, - 1.5, - 1.5, - 1.3, - 1.1, - 0.8, - 0.5, - 0.1, - -0.2, - -0.3, - 0.2, - 1, - 1.9, - 2.2, - 2.4, - 2.6, - 2.6, - 2.5, - 2.4, - 2.5, - 2.8, - 3, - 3 - ], - "direct_radiation": [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.7, - 1.1, - 6.3, - 3.8, - 3.6, - 6.7, - 0.1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.1, - 2.1, - 5.7, - 14.6, - 14.3, - 15.2, - 12.3, - 1.6, - 0.3, - 0, - -0, - 0, - -0, - 0, - 0, - -0, - -0, - 0, - 0, - 0, - -0, - 0, - 0, - -0, - 0, - 0.1, - 1.8, - 2.5, - 1.9, - 1.7, - 0.9, - 0, - -0, - 0, - -0, - 0, - 0, - -0, - -0, - -0.1, - 0.1, - -0.2, - 0, - 0, - -0, - 0.2, - -0.1, - -0.1, - 0.1, - 1.2, - 5.2, - 22, - 27.9, - 15.4, - 26.4, - 3.6, - 0.1, - -0.2, - 0, - -0, - -0, - -0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - -0, - -0, - 0, - 0, - 29.8, - 72.1, - 117.5, - 160.8, - 177.8, - 135.5, - 71.8, - 16.7, - 0, - 0, - 0, - -0, - -0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - -0, - -0, - 0, - 0, - 18.6, - 42.2, - 53.5, - 52.3, - 43.3, - 38.3, - 23.5, - 7, - 0, - 0, - 0, - -0, - -0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - -0, - -0, - 0, - 0, - 5.7, - 17.3, - 44.9, - 84.7, - 113.8, - 92.4, - 52, - 13.2, - 0, - 0, - 0, - -0, - -0, - 0, - 0 - ], - "dewpoint_2m": [ - 6.2, - 6.1, - 5.9, - 5.6, - 5.7, - 5.7, - 5.6, - 5.3, - 5.2, - 5.2, - 5.3, - 5.1, - 4.9, - 4.9, - 4.8, - 5, - 5.6, - 5.2, - 5.5, - 5.4, - 5.3, - 5.2, - 5.1, - 4.8, - 4.5, - 3.9, - 3.4, - 2.7, - 2.3, - 1.7, - 1.2, - 0.9, - 0.4, - 0.1, - -0, - 0, - 0.5, - 1.1, - 1.2, - 1.7, - 1.9, - 1.4, - 1.6, - 1.6, - 1.5, - 1.5, - 1.6, - 1.7, - 1.8, - 2, - 2, - 2, - 2, - 1.9, - 1.9, - 1.6, - 1.3, - 1.2, - 1.1, - 0.8, - 0.6, - 0.4, - 0.4, - 0.6, - 1.2, - 1.3, - 1.6, - 1.4, - 0.7, - 0.5, - 0.1, - -0.4, - -0.6, - -1.1, - -1.7, - -2.3, - -2.3, - -2.5, - -2.5, - -2.5, - -2.4, - -2.1, - -1.8, - -1.4, - -0.9, - -0.5, - -0.2, - 0.1, - 0.6, - 0.9, - 1.1, - 1, - 1, - 1, - 0.8, - 0.4, - -0.3, - -1, - -1.3, - -1.4, - -1.5, - -1.6, - -1.7, - -1.7, - -1.7, - -1.7, - -1.6, - -1.4, - -1.2, - -0.9, - -0.8, - -0.8, - -0.7, - -0.6, - -0.5, - -0.5, - -0.5, - -0.6, - -0.7, - -0.8, - -1, - -1.2, - -1.2, - -1.2, - -1.1, - -1.1, - -1.1, - -1, - -1, - -1, - -1, - -0.8, - -0.6, - -0.4, - -0.4, - -0.5, - -0.5, - -0.1, - 0.5, - 1, - 1.1, - 1, - 0.8, - 0.7, - 0.7, - 0.7, - 0.7, - 0.7, - 0.5, - 0.1, - -0.3, - -1, - -1.4, - -1.9, - -2.3, - -2.4, - -2.3, - -2, - -1.4, - -0.6, - 0.3, - 0.7, - 1, - 1.2, - 1.2, - 1.1, - 1.1, - 1.2 - ], - "freezinglevel_height": [ - 2432, - 2434, - 2512, - 2448, - 2512, - 2524, - 2608, - 2458, - 2446, - 2454, - 2454, - 2438, - 2544, - 2554, - 2544, - 2540, - 2550, - 2578, - 2516, - 2586, - 2540, - 2516, - 2508, - 2440, - 2464, - 2464, - 2412, - 2432, - 2260, - 2003, - 1897, - 1774, - 702, - 702, - 693, - 1511, - 635, - 634, - 522, - 668, - 624, - 697, - 674, - 586, - 910, - 856, - 846, - 697, - 665, - 623, - 611, - 633, - 562, - 566, - 620, - 438, - 399, - 398, - 412, - 466, - 501, - 537, - 548, - 513, - 492, - 488, - 448, - 462, - 564, - 569, - 544, - 555, - 540, - 514, - 565, - 471, - 464, - 530, - 507, - 562, - 603, - 566, - 574, - 489, - 462, - 472, - 569, - 562, - 567, - 579, - 597, - 605, - 604, - 598, - 589, - 585, - 582, - 573, - 562, - 548, - 524, - 498, - 468, - 434, - 416, - 402, - 394, - 399, - 412, - 438, - 469, - 508, - 548, - 559, - 562, - 551, - 529, - 497, - 456, - 431, - 407, - 379, - 366, - 356, - 331, - 291, - 241, - 176, - 125, - 72, - 50, - 105, - 199, - 299, - 326, - 308, - 298, - 329, - 376, - 420, - 424, - 411, - 381, - 344, - 297, - 245, - 227, - 219, - 202, - 172, - 135, - 93, - 67, - 44, - 41, - 68, - 113, - 191, - 258, - 335, - 455, - 565, - 687, - 845, - 950, - 1049, - 1187, - 1320 - ], - "soil_temperature_54cm": [ - 8.5, - 8.5, - 8.5, - 8.5, - 8.4, - 8.4, - 8.4, - 8.4, - 8.4, - 8.4, - 8.4, - 8.4, - 8.4, - 8.4, - 8.4, - 8.4, - 8.4, - 8.4, - 8.4, - 8.4, - 8.4, - 8.4, - 8.4, - 8.4, - 8.4, - 8.4, - 8.3, - 8.3, - 8.3, - 8.3, - 8.3, - 8.3, - 8.3, - 8.3, - 8.3, - 8.3, - 8.3, - 8.2, - 8.2, - 8.2, - 8.2, - 8.2, - 8.2, - 8.2, - 8.1, - 8.1, - 8.1, - 8.1, - 8.1, - 8.1, - 8.1, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 7.9, - 7.9, - 7.9, - 7.9, - 7.9, - 7.9, - 7.9, - 7.8, - 7.8, - 7.8, - 7.5, - 7.5, - 7.5, - 7.5, - 7.5, - 7.4, - 7.4, - 7.4, - 7.4, - 7.4, - 7.4, - 7.3, - 7.3, - 7.3, - 7.3, - 7.3, - 7.2, - 7.2, - 7.2, - 7.2, - 7.2, - 7.1, - 7.1, - 7.1, - 7.1, - 7.1, - 7.1, - 7, - 7, - 7, - 7, - 7, - 7, - 6.9, - 6.9, - 6.9, - 6.9, - 6.9, - 6.8, - 6.8, - 6.8, - 6.8, - 6.8, - 6.8, - 6.7, - 6.7, - 6.7, - 6.7, - 6.7, - 6.7, - 6.6, - 6.6, - 6.6, - 6.6, - 6.6, - 6.6, - 6.5, - 6.5, - 6.5, - 6.5, - 6.5, - 6.4, - 6.4, - 6.4, - 6.4, - 6.4, - 6.4, - 6.3, - 6.3, - 6.3, - 6.3, - 6.3, - 6.3, - 6.3, - 6.2, - 6.2, - 6.2, - 6.2, - 6.2, - 6.2, - 6.2, - 6.1, - 6.1, - 6.1, - 6.1, - 6.1, - 6.1, - 6.1, - 6.1, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 5.9, - 5.9 - ], - "soil_temperature_6cm": [ - 6.4, - 6.4, - 6.4, - 6.4, - 6.3, - 6.3, - 6.3, - 6.1, - 6, - 6, - 6.3, - 6.7, - 7.1, - 7.5, - 7.6, - 7.5, - 7.3, - 7, - 6.8, - 6.5, - 6.3, - 6.2, - 6.1, - 6, - 5.8, - 5.6, - 5.5, - 5.5, - 5.2, - 4.7, - 4, - 3.4, - 2.9, - 2.7, - 3.1, - 3.6, - 4.2, - 4.7, - 5, - 5, - 4.8, - 4.6, - 4.4, - 4.4, - 4.3, - 4.3, - 4.2, - 4.2, - 4.2, - 4.2, - 4.1, - 4, - 4, - 3.9, - 3.9, - 3.8, - 3.8, - 3.8, - 4, - 4.4, - 4.8, - 5.1, - 5.3, - 5.2, - 4.8, - 4.5, - 4.2, - 4, - 3.5, - 3.2, - 3, - 2.9, - 2.7, - 2.6, - 2.2, - 1.9, - 1.6, - 1.5, - 1.4, - 1.3, - 1.2, - 1.2, - 1.4, - 2, - 2.8, - 3.7, - 4.2, - 4.2, - 3.8, - 3.4, - 3.2, - 3.1, - 3.1, - 2.9, - 2.7, - 2.6, - 2.4, - 1.9, - 1.7, - 1.5, - 1.2, - 1.1, - 1, - 0.9, - 0.8, - 0.8, - 0.9, - 1.4, - 2, - 2.6, - 2.9, - 3.1, - 3.1, - 3, - 2.8, - 2.5, - 2.2, - 2, - 1.7, - 1.6, - 1.5, - 1.3, - 1.2, - 1.1, - 1, - 0.9, - 0.9, - 0.8, - 0.7, - 0.6, - 0.7, - 1, - 1.4, - 1.9, - 2.5, - 2.5, - 2.4, - 2.3, - 2.2, - 2.1, - 2.1, - 2.1, - 2.1, - 2.1, - 2.1, - 2.1, - 2.1, - 2.1, - 2.1, - 2, - 1.9, - 1.7, - 1.5, - 1.3, - 1.2, - 1.6, - 2.1, - 2.7, - 2.8, - 2.8, - 2.8, - 2.7, - 2.7, - 2.6, - 2.6, - 2.7, - 2.8, - 2.8 - ], - "soil_moisture_1_3cm": [ - 0.305, - 0.305, - 0.305, - 0.305, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.306, - 0.305, - 0.305, - 0.305, - 0.305, - 0.305, - 0.305, - 0.305, - 0.305, - 0.305, - 0.305, - 0.305, - 0.305, - 0.305, - 0.305, - 0.305, - 0.305, - 0.305, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.303, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.306, - 0.306, - 0.305, - 0.305, - 0.305, - 0.305, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.303, - 0.303, - 0.303, - 0.303, - 0.302, - 0.303, - 0.308, - 0.31, - 0.312, - 0.303, - 0.303, - 0.303, - 0.303, - 0.303, - 0.303, - 0.303, - 0.303, - 0.303, - 0.303, - 0.303, - 0.303, - 0.303, - 0.303, - 0.303, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.303, - 0.303, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.303, - 0.303, - 0.303, - 0.303, - 0.303, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.303, - 0.303, - 0.303, - 0.303, - 0.303, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.303, - 0.304, - 0.304, - 0.299, - 0.299, - 0.298, - 0.298, - 0.299, - 0.299, - 0.299, - 0.299, - 0.299, - 0.299, - 0.3, - 0.301, - 0.303, - 0.305, - 0.307, - 0.307, - 0.306, - 0.304, - 0.304, - 0.303, - 0.303, - 0.302, - 0.302, - 0.301, - 0.301, - 0.301, - 0.301, - 0.301, - 0.301, - 0.301, - 0.3, - 0.299, - 0.302, - 0.312 - ], - "winddirection_80m": [ - 263, - 268, - 277, - 271, - 278, - 278, - 276, - 273, - 264, - 271, - 263, - 257, - 257, - 266, - 253, - 258, - 240, - 232, - 194, - 198, - 183, - 186, - 179, - 178, - 165, - 167, - 171, - 168, - 154, - 156, - 166, - 173, - 182, - 192, - 202, - 199, - 204, - 208, - 246, - 217, - 230, - 227, - 232, - 235, - 241, - 239, - 245, - 255, - 257, - 260, - 254, - 251, - 257, - 252, - 240, - 243, - 247, - 256, - 253, - 252, - 244, - 239, - 234, - 217, - 204, - 208, - 173, - 172, - 179, - 177, - 173, - 170, - 167, - 159, - 158, - 159, - 158, - 158, - 152, - 147, - 145, - 143, - 141, - 140, - 145, - 138, - 142, - 143, - 148, - 150, - 151, - 144, - 153, - 169, - 167, - 170, - 179, - 181, - 178, - 171, - 162, - 157, - 152, - 148, - 148, - 150, - 152, - 150, - 145, - 140, - 142, - 146, - 153, - 160, - 171, - 188, - 194, - 195, - 195, - 196, - 196, - 198, - 199, - 201, - 203, - 206, - 210, - 216, - 220, - 225, - 235, - 247, - 258, - 266, - 270, - 267, - 265, - 266, - 268, - 270, - 290, - 287, - 285, - 285, - 287, - 289, - 290, - 290, - 290, - 291, - 291, - 291, - 289, - 286, - 281, - 278, - 274, - 268, - 260, - 249, - 231, - 218, - 208, - 202, - 200, - 199, - 199, - 198 - ], - "winddirection_10m": [ - 261, - 265, - 274, - 268, - 275, - 272, - 271, - 269, - 258, - 268, - 261, - 255, - 256, - 263, - 251, - 256, - 233, - 218, - 185, - 188, - 173, - 175, - 168, - 168, - 158, - 160, - 165, - 162, - 149, - 151, - 159, - 158, - 170, - 190, - 202, - 197, - 202, - 205, - 245, - 214, - 223, - 224, - 229, - 231, - 238, - 236, - 243, - 253, - 255, - 258, - 251, - 248, - 254, - 249, - 238, - 240, - 245, - 254, - 252, - 251, - 243, - 237, - 233, - 216, - 201, - 207, - 167, - 167, - 167, - 165, - 162, - 163, - 160, - 152, - 149, - 149, - 148, - 148, - 140, - 136, - 134, - 133, - 135, - 136, - 142, - 135, - 139, - 141, - 144, - 142, - 137, - 130, - 144, - 157, - 146, - 154, - 164, - 160, - 156, - 147, - 136, - 130, - 125, - 123, - 126, - 132, - 137, - 135, - 131, - 127, - 128, - 132, - 137, - 141, - 146, - 156, - 166, - 174, - 181, - 182, - 182, - 182, - 186, - 189, - 194, - 196, - 198, - 203, - 206, - 211, - 222, - 237, - 253, - 262, - 268, - 264, - 260, - 261, - 263, - 265, - 286, - 284, - 282, - 283, - 285, - 287, - 287, - 287, - 288, - 289, - 290, - 290, - 288, - 284, - 280, - 277, - 273, - 267, - 259, - 244, - 212, - 202, - 199, - 198, - 197, - 195, - 194, - 194 - ], - "time": [ - "2021-11-24T00:00", - "2021-11-24T01:00", - "2021-11-24T02:00", - "2021-11-24T03:00", - "2021-11-24T04:00", - "2021-11-24T05:00", - "2021-11-24T06:00", - "2021-11-24T07:00", - "2021-11-24T08:00", - "2021-11-24T09:00", - "2021-11-24T10:00", - "2021-11-24T11:00", - "2021-11-24T12:00", - "2021-11-24T13:00", - "2021-11-24T14:00", - "2021-11-24T15:00", - "2021-11-24T16:00", - "2021-11-24T17:00", - "2021-11-24T18:00", - "2021-11-24T19:00", - "2021-11-24T20:00", - "2021-11-24T21:00", - "2021-11-24T22:00", - "2021-11-24T23:00", - "2021-11-25T00:00", - "2021-11-25T01:00", - "2021-11-25T02:00", - "2021-11-25T03:00", - "2021-11-25T04:00", - "2021-11-25T05:00", - "2021-11-25T06:00", - "2021-11-25T07:00", - "2021-11-25T08:00", - "2021-11-25T09:00", - "2021-11-25T10:00", - "2021-11-25T11:00", - "2021-11-25T12:00", - "2021-11-25T13:00", - "2021-11-25T14:00", - "2021-11-25T15:00", - "2021-11-25T16:00", - "2021-11-25T17:00", - "2021-11-25T18:00", - "2021-11-25T19:00", - "2021-11-25T20:00", - "2021-11-25T21:00", - "2021-11-25T22:00", - "2021-11-25T23:00", - "2021-11-26T00:00", - "2021-11-26T01:00", - "2021-11-26T02:00", - "2021-11-26T03:00", - "2021-11-26T04:00", - "2021-11-26T05:00", - "2021-11-26T06:00", - "2021-11-26T07:00", - "2021-11-26T08:00", - "2021-11-26T09:00", - "2021-11-26T10:00", - "2021-11-26T11:00", - "2021-11-26T12:00", - "2021-11-26T13:00", - "2021-11-26T14:00", - "2021-11-26T15:00", - "2021-11-26T16:00", - "2021-11-26T17:00", - "2021-11-26T18:00", - "2021-11-26T19:00", - "2021-11-26T20:00", - "2021-11-26T21:00", - "2021-11-26T22:00", - "2021-11-26T23:00", - "2021-11-27T00:00", - "2021-11-27T01:00", - "2021-11-27T02:00", - "2021-11-27T03:00", - "2021-11-27T04:00", - "2021-11-27T05:00", - "2021-11-27T06:00", - "2021-11-27T07:00", - "2021-11-27T08:00", - "2021-11-27T09:00", - "2021-11-27T10:00", - "2021-11-27T11:00", - "2021-11-27T12:00", - "2021-11-27T13:00", - "2021-11-27T14:00", - "2021-11-27T15:00", - "2021-11-27T16:00", - "2021-11-27T17:00", - "2021-11-27T18:00", - "2021-11-27T19:00", - "2021-11-27T20:00", - "2021-11-27T21:00", - "2021-11-27T22:00", - "2021-11-27T23:00", - "2021-11-28T00:00", - "2021-11-28T01:00", - "2021-11-28T02:00", - "2021-11-28T03:00", - "2021-11-28T04:00", - "2021-11-28T05:00", - "2021-11-28T06:00", - "2021-11-28T07:00", - "2021-11-28T08:00", - "2021-11-28T09:00", - "2021-11-28T10:00", - "2021-11-28T11:00", - "2021-11-28T12:00", - "2021-11-28T13:00", - "2021-11-28T14:00", - "2021-11-28T15:00", - "2021-11-28T16:00", - "2021-11-28T17:00", - "2021-11-28T18:00", - "2021-11-28T19:00", - "2021-11-28T20:00", - "2021-11-28T21:00", - "2021-11-28T22:00", - "2021-11-28T23:00", - "2021-11-29T00:00", - "2021-11-29T01:00", - "2021-11-29T02:00", - "2021-11-29T03:00", - "2021-11-29T04:00", - "2021-11-29T05:00", - "2021-11-29T06:00", - "2021-11-29T07:00", - "2021-11-29T08:00", - "2021-11-29T09:00", - "2021-11-29T10:00", - "2021-11-29T11:00", - "2021-11-29T12:00", - "2021-11-29T13:00", - "2021-11-29T14:00", - "2021-11-29T15:00", - "2021-11-29T16:00", - "2021-11-29T17:00", - "2021-11-29T18:00", - "2021-11-29T19:00", - "2021-11-29T20:00", - "2021-11-29T21:00", - "2021-11-29T22:00", - "2021-11-29T23:00", - "2021-11-30T00:00", - "2021-11-30T01:00", - "2021-11-30T02:00", - "2021-11-30T03:00", - "2021-11-30T04:00", - "2021-11-30T05:00", - "2021-11-30T06:00", - "2021-11-30T07:00", - "2021-11-30T08:00", - "2021-11-30T09:00", - "2021-11-30T10:00", - "2021-11-30T11:00", - "2021-11-30T12:00", - "2021-11-30T13:00", - "2021-11-30T14:00", - "2021-11-30T15:00", - "2021-11-30T16:00", - "2021-11-30T17:00", - "2021-11-30T18:00", - "2021-11-30T19:00", - "2021-11-30T20:00", - "2021-11-30T21:00", - "2021-11-30T22:00", - "2021-11-30T23:00" - ], - "soil_moisture_0_1cm": [ - 0.304, - 0.304, - 0.304, - 0.304, - 0.305, - 0.305, - 0.305, - 0.305, - 0.305, - 0.305, - 0.305, - 0.304, - 0.304, - 0.304, - 0.303, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.304, - 0.303, - 0.303, - 0.303, - 0.304, - 0.304, - 0.303, - 0.303, - 0.303, - 0.303, - 0.302, - 0.303, - 0.304, - 0.303, - 0.303, - 0.303, - 0.305, - 0.306, - 0.305, - 0.305, - 0.304, - 0.304, - 0.304, - 0.303, - 0.303, - 0.303, - 0.303, - 0.303, - 0.303, - 0.303, - 0.303, - 0.303, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.303, - 0.31, - 0.312, - 0.313, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.301, - 0.301, - 0.301, - 0.301, - 0.301, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.301, - 0.301, - 0.301, - 0.301, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.302, - 0.303, - 0.304, - 0.304, - 0.298, - 0.298, - 0.297, - 0.297, - 0.298, - 0.298, - 0.298, - 0.298, - 0.298, - 0.299, - 0.3, - 0.302, - 0.304, - 0.307, - 0.309, - 0.308, - 0.306, - 0.303, - 0.302, - 0.302, - 0.302, - 0.301, - 0.301, - 0.3, - 0.3, - 0.3, - 0.3, - 0.3, - 0.3, - 0.301, - 0.3, - 0.299, - 0.302, - 0.314 - ], - "direct_normal_irradiance": [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.4, - 8.5, - 5.8, - 25.1, - 13.4, - 13, - 28, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1.5, - 24.6, - 32.2, - 58.9, - 50.7, - 54.7, - 52.4, - 10.4, - 3.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1.4, - 10.5, - 10.4, - 6.8, - 6, - 4, - 0.3, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1.4, - 13.3, - 30.5, - 91.3, - 101.2, - 56.8, - 114.8, - 23.7, - 1.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 341.5, - 430.3, - 493.5, - 589.6, - 660.9, - 596.3, - 477.7, - 191.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 213.7, - 256.9, - 227.8, - 193.9, - 162.6, - 170.3, - 158.4, - 80.4, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 65.4, - 107.1, - 193.9, - 317.5, - 431.7, - 415.9, - 356.1, - 151.7, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - "soil_temperature_18cm": [ - 6.3, - 6.4, - 6.4, - 6.5, - 6.5, - 6.6, - 6.6, - 6.6, - 6.6, - 6.6, - 6.6, - 6.7, - 6.7, - 6.8, - 6.9, - 7, - 7.1, - 7.1, - 7.1, - 7.1, - 7.1, - 7.1, - 7, - 7, - 6.9, - 6.9, - 6.8, - 6.8, - 6.7, - 6.6, - 6.5, - 6.3, - 6.1, - 5.9, - 5.7, - 5.6, - 5.5, - 5.5, - 5.6, - 5.6, - 5.6, - 5.6, - 5.6, - 5.6, - 5.5, - 5.5, - 5.5, - 5.5, - 5.4, - 5.4, - 5.4, - 5.3, - 5.3, - 5.3, - 5.2, - 5.2, - 5.2, - 5.1, - 5.1, - 5.1, - 5.2, - 5.2, - 5.3, - 5.4, - 5.4, - 5.4, - 5.4, - 5.3, - 4.9, - 4.9, - 4.8, - 4.7, - 4.6, - 4.6, - 4.5, - 4.4, - 4.2, - 4.1, - 4, - 3.9, - 3.8, - 3.7, - 3.6, - 3.5, - 3.5, - 3.6, - 3.7, - 3.9, - 4, - 4, - 4.1, - 4.1, - 4.1, - 4.1, - 4.1, - 4, - 4, - 3.9, - 3.8, - 3.8, - 3.6, - 3.5, - 3.4, - 3.3, - 3.2, - 3.1, - 3.1, - 3, - 3, - 3.1, - 3.1, - 3.2, - 3.4, - 3.4, - 3.4, - 3.5, - 3.5, - 3.4, - 3.4, - 3.4, - 3.3, - 3.2, - 3.2, - 3.1, - 3, - 3, - 2.9, - 2.8, - 2.8, - 2.7, - 2.7, - 2.6, - 2.6, - 2.6, - 2.9, - 2.9, - 3, - 3.1, - 3.1, - 3.1, - 3.1, - 3.1, - 3.1, - 3.1, - 3.1, - 3.1, - 3.1, - 3.1, - 3.1, - 3.1, - 3.1, - 3.1, - 3.1, - 3, - 2.9, - 2.9, - 2.9, - 3, - 3, - 3.1, - 3.2, - 3.2, - 3.2, - 3.3, - 3.3, - 3.3, - 3.3, - 3.4 - ], - "winddirection_180m": [ - 266, - 271, - 284, - 279, - 287, - 290, - 283, - 277, - 277, - 274, - 274, - 267, - 258, - 267, - 254, - 259, - 253, - 246, - 208, - 208, - 196, - 199, - 194, - 191, - 174, - 178, - 181, - 177, - 165, - 168, - 186, - 199, - 201, - 207, - 219, - 222, - 222, - 214, - 247, - 229, - 241, - 236, - 245, - 247, - 247, - 243, - 250, - 257, - 259, - 263, - 257, - 255, - 261, - 256, - 247, - 248, - 250, - 257, - 254, - 253, - 245, - 240, - 234, - 218, - 205, - 209, - 185, - 186, - 200, - 198, - 196, - 193, - 192, - 187, - 183, - 180, - 180, - 181, - 178, - 173, - 170, - 169, - 167, - 168, - 168, - 156, - 144, - 145, - 157, - 166, - 175, - 168, - 177, - 192, - 200, - 195, - 200, - 202, - 199, - 196, - 192, - 190, - 188, - 186, - 185, - 184, - 183, - 183, - 182, - 182, - 181, - 180, - 182, - 189, - 203, - 219, - 221, - 218, - 216, - 217, - 219, - 222, - 223, - 223, - 225, - 230, - 236, - 246, - 253, - 260, - 268, - 271, - 271, - 272, - 302, - 298, - 297, - 300, - 305, - 308, - 306, - 303, - 298, - 296, - 294, - 291, - 291, - 291, - 292, - 293, - 293, - 293, - 291, - 287, - 282, - 278, - 275, - 268, - 264, - 261, - 252, - 239, - 228, - 220, - 215, - 211, - 207, - 206 - ], - "apparent_temperature": [ - 4.4, - 4.2, - 4.3, - 4.4, - 4.1, - 4, - 3.9, - 3.7, - 3.6, - 3.4, - 4.1, - 4.6, - 4.9, - 5.2, - 5.3, - 5.4, - 5.5, - 5.3, - 4.8, - 4.7, - 4.2, - 4, - 3.9, - 3.4, - 3.2, - 2.8, - 2.5, - 2.3, - 1.6, - 0.5, - -0.5, - -1.2, - -2, - -2.9, - -2.6, - -1.3, - -0.3, - 0.7, - 0.9, - 1.1, - 1.4, - 0.8, - 0.4, - 0.5, - 0.5, - 0.3, - 0.2, - 0.1, - 0.1, - -0, - -0.1, - -0.3, - -0.5, - -0.2, - -0.2, - -0.3, - -0.4, - -1, - -0.2, - 0.2, - 0.7, - 1.3, - 1.4, - 1.3, - 0.7, - 0.4, - 0.5, - 0, - -0.6, - -1.1, - -1.5, - -1.9, - -2.2, - -2.6, - -3.2, - -3.5, - -3.6, - -3.6, - -3.7, - -3.8, - -3.9, - -4, - -3.4, - -2, - -0.9, - 0.2, - 0.6, - 0.5, - 0, - -0.3, - -0.3, - -0.4, - -0.4, - -0.6, - -0.8, - -1, - -1.4, - -2.1, - -2.4, - -2.7, - -3, - -3.2, - -3.4, - -3.5, - -3.5, - -3.3, - -2.8, - -2, - -1, - 0, - 0.3, - 0.4, - 0.3, - 0.2, - -0.1, - -0.5, - -0.9, - -1.4, - -1.9, - -2.1, - -2.3, - -2.5, - -2.8, - -3, - -3.3, - -3.5, - -3.6, - -3.6, - -3.6, - -3.6, - -3.3, - -2.7, - -1.9, - -1.2, - -1.4, - -1.4, - -1.6, - -1.9, - -2.4, - -2.9, - -1.6, - -1.8, - -2, - -2.1, - -2.3, - -2.4, - -2.4, - -2.4, - -2.5, - -2.9, - -3.3, - -3.9, - -4.2, - -4.5, - -4.6, - -4, - -3.1, - -2, - -1.3, - -0.7, - -0.3, - -0.5, - -1, - -1.4, - -1.5, - -1.4, - -1.3, - -1.2 - ], - "cloudcover_high": [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 3, - 28, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 92, - 1, - 0, - 0, - 0, - 0, - 0, - 83, - 100, - 0, - 7, - 100, - 100, - 100, - 3, - 0, - 0, - 0, - 0, - 0, - 14, - 62, - 1, - 58, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 100, - 87, - 99, - 100, - 100, - 100, - 100, - 100, - 100, - 55, - 100, - 100, - 100, - 0, - 0, - 4, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 33, - 67, - 100, - 94, - 87, - 81, - 75, - 70, - 65, - 43, - 22, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 7, - 15, - 22, - 15, - 7, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 11, - 22, - 33, - 22, - 11, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 8, - 16, - 24, - 49, - 75, - 100, - 100, - 100, - 100, - 67 - ], - "pressure_msl": [ - 1025.2, - 1025.7, - 1025.7, - 1025.2, - 1024.7, - 1024.2, - 1024.2, - 1024.2, - 1024.2, - 1023.7, - 1024.2, - 1023.7, - 1023.2, - 1022.2, - 1021.2, - 1021.2, - 1020.7, - 1020.2, - 1020.2, - 1019.7, - 1018.7, - 1018.7, - 1018.2, - 1017.7, - 1016.7, - 1015.7, - 1015.2, - 1014.2, - 1013.7, - 1012.7, - 1012.2, - 1011.1, - 1011.1, - 1010.6, - 1010.1, - 1009.6, - 1008.6, - 1008.1, - 1007.6, - 1007.1, - 1007.1, - 1006.6, - 1006.1, - 1006.1, - 1005.6, - 1005.6, - 1005.1, - 1005.1, - 1005.1, - 1005.1, - 1004.6, - 1004.1, - 1004.1, - 1003.6, - 1003.6, - 1003.6, - 1003.1, - 1002.6, - 1002.6, - 1002.1, - 1001.6, - 1000.6, - 1000.1, - 999.6, - 999.6, - 999.1, - 998.6, - 998.1, - 997.2, - 996.7, - 996.2, - 995.7, - 995.2, - 994.7, - 994.2, - 993.7, - 993.2, - 992.7, - 992.7, - 992.2, - 992.7, - 992.7, - 992.7, - 992.7, - 992.2, - 991.7, - 991.7, - 991.7, - 992.2, - 992.2, - 992.7, - 992.7, - 992.7, - 993.2, - 993.2, - 993.2, - 993.7, - 993.2, - 993.7, - 993.7, - 993.7, - 993.7, - 994.2, - 994.2, - 994.7, - 994.7, - 995.2, - 995.2, - 995.2, - 995.2, - 995.2, - 995.2, - 995.7, - 995.7, - 996.2, - 996.2, - 996.7, - 996.7, - 996.7, - 996.7, - 997.2, - 997.2, - 997.2, - 997.7, - 997.7, - 997.7, - 998.2, - 998.7, - 999.3, - 999.3, - 999.8, - 1000.3, - 1000.3, - 1000.3, - 1000.6, - 1001.1, - 1001.1, - 1001.6, - 1002.1, - 1002.1, - 1006.6, - 1007.1, - 1007.1, - 1007.6, - 1007.6, - 1008.2, - 1008.2, - 1008.7, - 1008.7, - 1008.7, - 1008.7, - 1009.2, - 1009.2, - 1009.7, - 1009.7, - 1009.7, - 1009.2, - 1008.7, - 1008.2, - 1007.6, - 1006.6, - 1005.6, - 1004.6, - 1003.6, - 1002.1, - 1001.1, - 999.1, - 998.1 - ], - "diffuse_radiation": [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.7, - 22.3, - 41, - 63.3, - 79.1, - 76.7, - 57.4, - 27.9, - 7.9, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.6, - 30.3, - 71.4, - 94.8, - 110.4, - 104.9, - 80.8, - 25.3, - 14.3, - -0, - 0, - 0, - -0, - 0, - -0, - 0, - 0, - -0, - 0, - 0, - 0, - -0, - 0, - -0, - 0.3, - 17.3, - 56.7, - 90.1, - 105.3, - 112.5, - 91.3, - 46.8, - 12.3, - 0, - -0, - 0, - -0.1, - 0.1, - -0, - -0, - -0, - 0, - -0, - 0.1, - -0, - -0.1, - 0.1, - -0, - 0.2, - 26.7, - 66.2, - 93.2, - 89.9, - 67.9, - 60.8, - 50.8, - 11, - 0, - -0.1, - -0, - -0, - -0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - -0, - -0, - 0, - 0, - 21.5, - 49.3, - 64.5, - 66.6, - 62, - 56.3, - 42.7, - 15.6, - 0, - 0, - 0, - -0, - -0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - -0, - -0, - 0, - 0, - 26.2, - 61.1, - 85.6, - 98.2, - 98.6, - 78.3, - 51.4, - 16.4, - 0, - 0, - 0, - -0, - -0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - -0, - -0, - 0, - 0, - 24.8, - 57.9, - 81.8, - 94.8, - 94.4, - 77.1, - 48.9, - 15, - 0, - 0, - 0, - -0, - -0, - 0, - 0 - ], - "snow_depth": [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - "windspeed_10m": [ - 10.4, - 10.9, - 10.1, - 7.9, - 8.8, - 8.9, - 9.3, - 9.4, - 8.1, - 9.6, - 8.5, - 7.6, - 7.9, - 7.4, - 7.1, - 6.4, - 4, - 3.1, - 5, - 4.3, - 4.6, - 4.9, - 5.3, - 5.3, - 5.8, - 6, - 6.3, - 5.9, - 5.9, - 6, - 5.2, - 4.7, - 5.1, - 7, - 6.9, - 6.6, - 6.6, - 7.8, - 11.5, - 10.5, - 8.1, - 10.1, - 10.9, - 10.9, - 11.2, - 11.8, - 12.3, - 12.9, - 12.7, - 12.5, - 12.1, - 12.3, - 12.6, - 10.9, - 10, - 10.8, - 10.8, - 14.8, - 12.1, - 11.5, - 10.5, - 8.8, - 9.2, - 8.7, - 9.2, - 7.8, - 6.3, - 7.5, - 7.9, - 7.9, - 8.7, - 9.5, - 9.2, - 9.4, - 9.6, - 9.4, - 9.6, - 9.1, - 9.2, - 9.4, - 9.2, - 9.5, - 10.6, - 9.2, - 9.9, - 10.2, - 10.7, - 10.4, - 8.9, - 8.2, - 7.2, - 7.6, - 7.7, - 7.4, - 6.5, - 6.6, - 6.2, - 5.8, - 5.7, - 5.6, - 5.9, - 6.2, - 6.6, - 7, - 7, - 6.9, - 6.8, - 6.6, - 6.2, - 5.9, - 5.8, - 5.8, - 5.6, - 5.1, - 4.5, - 4.1, - 4.3, - 4.9, - 5.6, - 5.7, - 5.5, - 5.3, - 5.4, - 5.7, - 5.9, - 5.6, - 5.2, - 4.7, - 4.5, - 4.4, - 4.3, - 4.6, - 5.4, - 6.5, - 8.7, - 7.7, - 6.8, - 7.5, - 8.6, - 9.8, - 11.1, - 11.7, - 12.5, - 13, - 13.4, - 13.6, - 13.4, - 12.9, - 12.5, - 12.8, - 13.5, - 14, - 13.8, - 13.3, - 12.4, - 11.9, - 11.3, - 10, - 8.2, - 6, - 5.5, - 7.4, - 10, - 13, - 14.3, - 15.2, - 15.8, - 16.1 - ], - "vapor_pressure_deficit": [ - 0.05, - 0.05, - 0.06, - 0.07, - 0.05, - 0.05, - 0.05, - 0.05, - 0.05, - 0.05, - 0.08, - 0.12, - 0.16, - 0.17, - 0.19, - 0.16, - 0.11, - 0.11, - 0.07, - 0.06, - 0.05, - 0.04, - 0.04, - 0.04, - 0.05, - 0.07, - 0.09, - 0.12, - 0.1, - 0.08, - 0.05, - 0.03, - 0.02, - 0, - 0.03, - 0.08, - 0.11, - 0.14, - 0.18, - 0.15, - 0.14, - 0.14, - 0.12, - 0.12, - 0.13, - 0.13, - 0.12, - 0.11, - 0.1, - 0.08, - 0.08, - 0.07, - 0.06, - 0.07, - 0.06, - 0.08, - 0.09, - 0.1, - 0.13, - 0.16, - 0.19, - 0.22, - 0.23, - 0.21, - 0.14, - 0.11, - 0.09, - 0.08, - 0.13, - 0.13, - 0.14, - 0.15, - 0.15, - 0.15, - 0.14, - 0.14, - 0.13, - 0.12, - 0.11, - 0.11, - 0.1, - 0.09, - 0.12, - 0.16, - 0.21, - 0.25, - 0.27, - 0.25, - 0.18, - 0.14, - 0.12, - 0.12, - 0.13, - 0.12, - 0.1, - 0.09, - 0.08, - 0.06, - 0.05, - 0.05, - 0.05, - 0.05, - 0.04, - 0.04, - 0.05, - 0.06, - 0.08, - 0.1, - 0.14, - 0.17, - 0.18, - 0.17, - 0.15, - 0.13, - 0.12, - 0.1, - 0.09, - 0.08, - 0.08, - 0.08, - 0.08, - 0.08, - 0.08, - 0.08, - 0.08, - 0.07, - 0.07, - 0.07, - 0.06, - 0.05, - 0.05, - 0.07, - 0.09, - 0.13, - 0.14, - 0.13, - 0.11, - 0.09, - 0.06, - 0.03, - 0.04, - 0.05, - 0.05, - 0.05, - 0.05, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.05, - 0.06, - 0.07, - 0.07, - 0.08, - 0.1, - 0.14, - 0.17, - 0.16, - 0.14, - 0.11, - 0.09, - 0.07, - 0.06, - 0.07, - 0.08, - 0.1, - 0.09 - ], - "windgusts_10m": [ - 22.1, - 23.8, - 23.5, - 21.5, - 18.4, - 19.6, - 19.9, - 20.3, - 19.6, - 20.8, - 25, - 19.1, - 17.9, - 17.6, - 16.8, - 15.7, - 14, - 8.5, - 10.4, - 11, - 9.5, - 10.2, - 11.1, - 11.8, - 12.2, - 13, - 14.4, - 13.3, - 13.2, - 12.5, - 12.7, - 10.6, - 10.4, - 15.6, - 16.8, - 18.8, - 15.9, - 17.8, - 25.3, - 24.6, - 22.5, - 21.4, - 23.4, - 23.3, - 24.7, - 25.3, - 26.2, - 29.2, - 28.2, - 27.9, - 26.9, - 26.8, - 28, - 27.2, - 23.5, - 24.9, - 24.6, - 31.8, - 32.6, - 27.5, - 25.1, - 23.1, - 20.4, - 20.2, - 24, - 20.1, - 16.8, - 16, - 20.5, - 20.1, - 21.1, - 22.2, - 23.6, - 24.4, - 25.7, - 26.4, - 26.3, - 27.2, - 27.1, - 27, - 26.4, - 26.4, - 27.1, - 28.7, - 28.1, - 27.2, - 26.5, - 25.5, - 23.6, - 23.6, - 22.2, - 21.4, - 20.4, - 19.4, - 18.4, - 18.1, - 17.7, - 17.4, - 17.6, - 17.9, - 18.1, - 18.4, - 18.6, - 18.9, - 19.2, - 19.5, - 19.9, - 21.5, - 23, - 24.6, - 24.1, - 23.6, - 23.1, - 21.4, - 19.8, - 18.1, - 15.8, - 13.5, - 11.2, - 10.9, - 10.6, - 10.3, - 8.9, - 7.5, - 6, - 5.1, - 4.2, - 3.3, - 5.5, - 7.8, - 10, - 14.4, - 18.8, - 23.2, - 24, - 25.6, - 27.2, - 26.1, - 25, - 24, - 25.6, - 27.1, - 28.7, - 29.6, - 30.5, - 31.4, - 30.5, - 29.7, - 28.8, - 30.3, - 31.8, - 33.3, - 32.3, - 31.3, - 30.4, - 28.3, - 26.2, - 24.2, - 21.1, - 18, - 14.9, - 19.9, - 24.9, - 29.9, - 32, - 34.1, - 36.3, - 36.9 - ], - "winddirection_120m": [ - 265, - 269, - 280, - 274, - 281, - 282, - 279, - 275, - 270, - 273, - 266, - 259, - 257, - 266, - 254, - 258, - 245, - 240, - 202, - 205, - 189, - 192, - 186, - 184, - 169, - 172, - 176, - 172, - 158, - 161, - 174, - 185, - 192, - 201, - 203, - 201, - 205, - 210, - 246, - 221, - 235, - 229, - 235, - 238, - 243, - 241, - 247, - 256, - 258, - 262, - 255, - 252, - 258, - 253, - 243, - 245, - 248, - 256, - 253, - 253, - 245, - 239, - 234, - 218, - 204, - 209, - 179, - 179, - 189, - 188, - 184, - 181, - 179, - 172, - 170, - 169, - 169, - 169, - 164, - 159, - 156, - 155, - 153, - 153, - 147, - 140, - 143, - 144, - 153, - 157, - 163, - 156, - 164, - 179, - 183, - 182, - 190, - 192, - 190, - 185, - 179, - 175, - 172, - 168, - 168, - 168, - 168, - 166, - 164, - 161, - 161, - 162, - 166, - 174, - 187, - 205, - 208, - 207, - 205, - 206, - 207, - 209, - 211, - 212, - 214, - 218, - 223, - 230, - 236, - 243, - 253, - 259, - 265, - 269, - 271, - 272, - 272, - 273, - 274, - 275, - 298, - 293, - 288, - 288, - 289, - 290, - 291, - 291, - 292, - 292, - 293, - 292, - 290, - 286, - 281, - 278, - 275, - 268, - 262, - 255, - 242, - 227, - 215, - 207, - 204, - 203, - 202, - 201 - ], - "windspeed_120m": [ - 24.7, - 24.5, - 23.8, - 20.2, - 21.5, - 22, - 23.5, - 22.3, - 22.2, - 21.2, - 18.6, - 14.6, - 13.8, - 13.2, - 12, - 12.7, - 9.1, - 8.1, - 11.3, - 12.5, - 13.6, - 15, - 15.3, - 16.1, - 17.6, - 18, - 17.9, - 16, - 14.8, - 15, - 14.5, - 12.9, - 14.8, - 18.2, - 10.1, - 9.9, - 9.7, - 13, - 21.6, - 22.1, - 19.3, - 23.4, - 26.6, - 25.5, - 26.5, - 27.7, - 29.1, - 30.2, - 29, - 27.8, - 27, - 26.9, - 27.4, - 23.8, - 22.1, - 23.9, - 23.4, - 28.4, - 23.9, - 21.3, - 19.3, - 15.9, - 16.5, - 16.4, - 19, - 15.9, - 16.9, - 19.5, - 27.9, - 29.4, - 30.7, - 31.2, - 29.3, - 28.6, - 31.6, - 34.1, - 35.2, - 33.5, - 32.8, - 33.5, - 33.4, - 34.1, - 33.3, - 27.9, - 19.2, - 18.6, - 20.4, - 23.3, - 27.3, - 27.1, - 26.5, - 27.4, - 29.3, - 28.8, - 22.7, - 23.9, - 25, - 24.2, - 24, - 23.5, - 23.2, - 23.4, - 23.7, - 24.1, - 24.4, - 24.6, - 24, - 22.1, - 19.5, - 17, - 17.2, - 18.2, - 18.5, - 16.5, - 14, - 13.5, - 15.8, - 19.2, - 22.4, - 22.1, - 20.5, - 18.9, - 19.1, - 19.8, - 20, - 19.1, - 17.7, - 15.9, - 14.8, - 13.8, - 13.4, - 14.1, - 15.5, - 17.2, - 18.2, - 20.8, - 23.9, - 26, - 28, - 30.1, - 34.7, - 33.6, - 32.6, - 32.7, - 32.9, - 33.2, - 33.3, - 33.4, - 33.2, - 33, - 32.6, - 31.4, - 29.6, - 27.3, - 24.3, - 22, - 19.6, - 17.3, - 16.7, - 17, - 18.5, - 21.8, - 27.2, - 34.1, - 37, - 38.7, - 39.8, - 40.2 - ], - "windspeed_80m": [ - 20.9, - 21.3, - 19.9, - 16.7, - 18.1, - 18.4, - 19.3, - 18.9, - 17.9, - 18.3, - 15.5, - 13, - 13, - 12.3, - 11.4, - 11.5, - 7.7, - 7.2, - 10.1, - 10.6, - 11.3, - 12, - 12.3, - 12.7, - 14.1, - 13.8, - 14, - 12.1, - 12.3, - 12.8, - 12.3, - 11.4, - 12.5, - 11.3, - 9.7, - 9.2, - 9.3, - 12, - 20, - 19.4, - 16.4, - 20.5, - 21.8, - 21.8, - 22.3, - 23.6, - 24.6, - 25.8, - 25.1, - 24.1, - 23.4, - 23.6, - 24, - 20.7, - 19.2, - 20.8, - 20.6, - 26.7, - 21.8, - 19.9, - 17.9, - 14.8, - 15.5, - 15.4, - 17.2, - 14.5, - 13.1, - 14.8, - 21.2, - 21.6, - 22.6, - 21.9, - 20.4, - 20.2, - 22.7, - 24.1, - 25.2, - 24.1, - 24.8, - 25, - 24.5, - 25.1, - 21.7, - 16.7, - 17.6, - 17.5, - 19.2, - 20.9, - 20.8, - 20.1, - 19.4, - 20.7, - 21.7, - 21.6, - 18.2, - 18.9, - 19, - 18.7, - 18.5, - 18.2, - 18.3, - 18.7, - 19.3, - 20, - 20.3, - 20.5, - 19.7, - 17.6, - 14.7, - 12.2, - 12.8, - 14.5, - 15.8, - 14.3, - 12.2, - 11.3, - 13, - 15.5, - 17.9, - 17.6, - 16.4, - 15.1, - 15, - 15.2, - 15.1, - 14.5, - 13.6, - 12.4, - 11.5, - 10.7, - 10.2, - 10.6, - 11.9, - 13.5, - 16.1, - 15.4, - 15.3, - 16.5, - 18.4, - 20.4, - 24.6, - 25.3, - 26.3, - 27.2, - 28, - 28.4, - 27.8, - 26.7, - 25.7, - 26.2, - 27.1, - 27.6, - 26.7, - 25.1, - 22.8, - 21, - 19, - 16.5, - 14.9, - 13.8, - 14.3, - 17, - 21.4, - 26.9, - 29.7, - 31.7, - 33.3, - 33.9 - ], - "evapotranspiration": [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.01, - 0.01, - 0.02, - 0.02, - 0.02, - 0.02, - 0.01, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.01, - 0.01, - 0.01, - 0.01, - 0, - 0, - 0, - 0, - 0.01, - 0.02, - 0.02, - 0.02, - 0.03, - 0.02, - 0.02, - 0.01, - 0.01, - 0.01, - 0.02, - 0.02, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.02, - 0.02, - 0.03, - 0.02, - 0.02, - 0.02, - 0.02, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.02, - 0.02, - 0.02, - 0.02, - 0.02, - 0.02, - 0.02, - 0.01, - 0.01, - 0.02, - 0.02, - 0.03, - 0.03, - 0.03, - 0.02, - 0.02, - 0.02, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.01, - 0.01, - 0.02, - 0.02, - 0.03, - 0.03, - 0.02, - 0.02, - 0.01, - 0.01, - 0.01, - 0.01, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.02, - 0.02, - 0.02, - 0.02, - 0.02, - 0.03, - 0.03, - 0.03, - 0.02, - 0.02, - 0.02, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.02 - ], - "precipitation": [ - 0, - 0, - 0.01, - 0, - 0.03, - 0.01, - 0, - 0, - 0, - 0, - 0, - 0, - 0.01, - 0.01, - 0, - 0.06, - 0.06, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.03, - 0.07, - 0, - 0, - 0, - 0.09, - 0.09, - 0.01, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.01, - 0.1, - 0.3, - 0.2, - 0.15, - 0, - 0, - 0, - 0, - 0.01, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.1, - 0.01, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.05, - 0.05, - 0.05, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.01, - 0.01, - 0.01, - 0.03, - 0.03, - 0.03, - 0.02, - 0.02, - 0.02, - 0.13, - 0.13, - 0.13, - 0, - 0, - 0, - 0.07, - 0.07, - 0.07, - 0.16, - 0.16, - 0.16, - 0.01, - 0.01, - 0.01, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.03, - 0.03, - 0.03, - 0.04, - 0.04, - 0.04, - 0.88 - ] - }, - "daily": { - "temperature_2m_max": [ - 7.6, - 5.4, - 4.8, - 4.5, - 3.4, - 2.2, - 3 - ], - "precipitation_hours": [ - 7, - 5, - 5, - 3, - 3, - 13, - 15 - ], - "shortwave_radiation_sum": [ - 1.44, - 2.16, - 1.95, - 2.05, - 4.18, - 2.86, - 3.31 - ], - "winddirection_10m_dominant": [ - 251, - 210, - 230, - 143, - 143, - 248, - 256 - ], - "windspeed_10m_max": [ - 10.9, - 12.9, - 14.8, - 10.7, - 7, - 13, - 16.1 - ], - "apparent_temperature_min": [ - 3.4, - -2.9, - -1.9, - -4, - -3.5, - -3.6, - -4.6 - ], - "sunset": [ - "2021-11-24T16:04", - "2021-11-25T16:03", - "2021-11-26T16:02", - "2021-11-27T16:01", - "2021-11-28T16:00", - "2021-11-29T15:59", - "2021-11-30T15:59" - ], - "weathercode": [ - 61, - 61, - 61, - 61, - 61, - 77, - 80 - ], - "sunrise": [ - "2021-11-24T07:41", - "2021-11-25T07:43", - "2021-11-26T07:45", - "2021-11-27T07:46", - "2021-11-28T07:48", - "2021-11-29T07:49", - "2021-11-30T07:51" - ], - "apparent_temperature_max": [ - 5.5, - 3.2, - 1.4, - 0.6, - 0.4, - -1.2, - -0.3 - ], - "temperature_2m_min": [ - 5.5, - 0.2, - 1.8, - -0.1, - -0.2, - -0.5, - -0.3 - ], - "windgusts_10m_max": [ - 8.5, - 10.4, - 16, - 18.1, - 10.9, - 3.3, - 14.9 - ], - "precipitation_sum": [ - 0.19, - 0.29, - 0.76, - 0.12, - 0.15, - 0.64, - 1.74 - ], - "time": [ - "2021-11-24", - "2021-11-25", - "2021-11-26", - "2021-11-27", - "2021-11-28", - "2021-11-29", - "2021-11-30" - ] - }, - "utc_offset_seconds": 3600, - "hourly_units": { - "precipitation": "mm", - "shortwave_radiation": "W\/m²", - "soil_moisture_0_1cm": "m³\/m³", - "pressure_msl": "hPa", - "soil_moisture_3_9cm": "m³\/m³", - "soil_temperature_54cm": "°C", - "soil_temperature_18cm": "°C", - "winddirection_120m": "°", - "vapor_pressure_deficit": "kPa", - "dewpoint_2m": "°C", - "winddirection_180m": "°", - "windspeed_10m": "km\/h", - "cloudcover_low": "%", - "cloudcover_mid": "%", - "cloudcover_high": "%", - "windgusts_10m": "km\/h", - "soil_moisture_9_27cm": "m³\/m³", - "windspeed_120m": "km\/h", - "winddirection_10m": "°", - "time": "iso8601", - "soil_temperature_6cm": "°C", - "apparent_temperature": "°C", - "windspeed_80m": "km\/h", - "soil_moisture_1_3cm": "m³\/m³", - "diffuse_radiation": "W\/m²", - "snow_depth": "m", - "windspeed_180m": "km\/h", - "weathercode": "wmo code", - "direct_normal_irradiance": "W\/m²", - "relativehumidity_2m": "%", - "soil_moisture_27_81cm": "m³\/m³", - "winddirection_80m": "°", - "freezinglevel_height": "m", - "evapotranspiration": "mm", - "cloudcover": "%", - "soil_temperature_0cm": "°C", - "direct_radiation": "W\/m²", - "temperature_2m": "°C" - }, - "longitude": 13.419998, - "elevation": 44.8125, - "current_weather": { - "temperature": 5.5, - "windspeed": 5.3, - "weathercode": 3, - "winddirection": 168, - "time": "2021-11-24T23:00" - } -} \ No newline at end of file diff --git a/tests/components/open_meteo/test_config_flow.py b/tests/components/open_meteo/test_config_flow.py deleted file mode 100644 index f985e2a6193..00000000000 --- a/tests/components/open_meteo/test_config_flow.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Tests for the Open-Meteo config flow.""" - -from unittest.mock import MagicMock - -from homeassistant.components.open_meteo.const import DOMAIN -from homeassistant.components.zone import ENTITY_ID_HOME -from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_ZONE -from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM - - -async def test_full_user_flow( - hass: HomeAssistant, - mock_setup_entry: MagicMock, -) -> None: - """Test the full user configuration flow.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - - assert result.get("type") == RESULT_TYPE_FORM - assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_ZONE: ENTITY_ID_HOME}, - ) - - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY - assert result2.get("title") == "test home" - assert result2.get("data") == {CONF_ZONE: ENTITY_ID_HOME} diff --git a/tests/components/open_meteo/test_init.py b/tests/components/open_meteo/test_init.py deleted file mode 100644 index 38619bc09db..00000000000 --- a/tests/components/open_meteo/test_init.py +++ /dev/null @@ -1,68 +0,0 @@ -"""Tests for the Open-Meteo integration.""" -from unittest.mock import AsyncMock, MagicMock, patch - -from open_meteo import OpenMeteoConnectionError -from pytest import LogCaptureFixture - -from homeassistant.components.open_meteo.const import DOMAIN -from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import CONF_ZONE -from homeassistant.core import HomeAssistant - -from tests.common import MockConfigEntry - - -async def test_load_unload_config_entry( - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_open_meteo: AsyncMock, -) -> None: - """Test the Open-Meteo configuration entry loading/unloading.""" - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - - assert mock_config_entry.state is ConfigEntryState.LOADED - - await hass.config_entries.async_unload(mock_config_entry.entry_id) - await hass.async_block_till_done() - - assert not hass.data.get(DOMAIN) - assert mock_config_entry.state is ConfigEntryState.NOT_LOADED - - -@patch( - "homeassistant.components.open_meteo.OpenMeteo.forecast", - side_effect=OpenMeteoConnectionError, -) -async def test_config_entry_not_ready( - mock_forecast: MagicMock, - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, -) -> None: - """Test the Open-Meteo configuration entry not ready.""" - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - - assert mock_forecast.call_count == 1 - assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY - - -async def test_config_entry_zone_removed( - hass: HomeAssistant, - caplog: LogCaptureFixture, -) -> None: - """Test the Open-Meteo configuration entry not ready.""" - mock_config_entry = MockConfigEntry( - title="My Castle", - domain=DOMAIN, - data={CONF_ZONE: "zone.castle"}, - unique_id="zone.castle", - ) - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - - assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY - assert "Zone 'zone.castle' not found" in caplog.text From 9e6e9774d11f685a7c673be1805b36d46670c446 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 7 Dec 2021 00:17:17 +0100 Subject: [PATCH 0089/2644] Remove colon from default entity name in Hue integration (#61118) --- homeassistant/components/hue/v2/entity.py | 2 +- tests/components/hue/test_binary_sensor.py | 2 +- tests/components/hue/test_sensor_v2.py | 6 +++--- tests/components/hue/test_switch.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/hue/v2/entity.py b/homeassistant/components/hue/v2/entity.py index 368a6cfe9d0..6dbc959fd9c 100644 --- a/homeassistant/components/hue/v2/entity.py +++ b/homeassistant/components/hue/v2/entity.py @@ -64,7 +64,7 @@ class HueBaseEntity(Entity): type_title = RESOURCE_TYPE_NAMES.get( self.resource.type, self.resource.type.value.replace("_", " ").title() ) - return f"{dev_name}: {type_title}" + return f"{dev_name} {type_title}" async def async_added_to_hass(self) -> None: """Call when entity is added.""" diff --git a/tests/components/hue/test_binary_sensor.py b/tests/components/hue/test_binary_sensor.py index f1d6a1a8087..ba5a58b4be0 100644 --- a/tests/components/hue/test_binary_sensor.py +++ b/tests/components/hue/test_binary_sensor.py @@ -19,7 +19,7 @@ async def test_binary_sensors(hass, mock_bridge_v2, v2_resources_test_data): sensor = hass.states.get("binary_sensor.hue_motion_sensor_motion") assert sensor is not None assert sensor.state == "off" - assert sensor.name == "Hue motion sensor: Motion" + assert sensor.name == "Hue motion sensor Motion" assert sensor.attributes["device_class"] == "motion" assert sensor.attributes["motion_valid"] is True diff --git a/tests/components/hue/test_sensor_v2.py b/tests/components/hue/test_sensor_v2.py index 2668922590f..256c323ccce 100644 --- a/tests/components/hue/test_sensor_v2.py +++ b/tests/components/hue/test_sensor_v2.py @@ -22,7 +22,7 @@ async def test_sensors(hass, mock_bridge_v2, v2_resources_test_data): sensor = hass.states.get("sensor.hue_motion_sensor_temperature") assert sensor is not None assert sensor.state == "18.1" - assert sensor.attributes["friendly_name"] == "Hue motion sensor: Temperature" + assert sensor.attributes["friendly_name"] == "Hue motion sensor Temperature" assert sensor.attributes["device_class"] == "temperature" assert sensor.attributes["state_class"] == "measurement" assert sensor.attributes["unit_of_measurement"] == "°C" @@ -32,7 +32,7 @@ async def test_sensors(hass, mock_bridge_v2, v2_resources_test_data): sensor = hass.states.get("sensor.hue_motion_sensor_illuminance") assert sensor is not None assert sensor.state == "63" - assert sensor.attributes["friendly_name"] == "Hue motion sensor: Illuminance" + assert sensor.attributes["friendly_name"] == "Hue motion sensor Illuminance" assert sensor.attributes["device_class"] == "illuminance" assert sensor.attributes["state_class"] == "measurement" assert sensor.attributes["unit_of_measurement"] == "lx" @@ -43,7 +43,7 @@ async def test_sensors(hass, mock_bridge_v2, v2_resources_test_data): sensor = hass.states.get("sensor.wall_switch_with_2_controls_battery") assert sensor is not None assert sensor.state == "100" - assert sensor.attributes["friendly_name"] == "Wall switch with 2 controls: Battery" + assert sensor.attributes["friendly_name"] == "Wall switch with 2 controls Battery" assert sensor.attributes["device_class"] == "battery" assert sensor.attributes["state_class"] == "measurement" assert sensor.attributes["unit_of_measurement"] == "%" diff --git a/tests/components/hue/test_switch.py b/tests/components/hue/test_switch.py index 30f4d3634b4..257f1a253c3 100644 --- a/tests/components/hue/test_switch.py +++ b/tests/components/hue/test_switch.py @@ -17,7 +17,7 @@ async def test_switch(hass, mock_bridge_v2, v2_resources_test_data): # test config switch to enable/disable motion sensor test_entity = hass.states.get("switch.hue_motion_sensor_motion") assert test_entity is not None - assert test_entity.name == "Hue motion sensor: Motion" + assert test_entity.name == "Hue motion sensor Motion" assert test_entity.state == "on" assert test_entity.attributes["device_class"] == "switch" From 0adf86d647b64d750f45a64dd249f23aeafbc51b Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 6 Dec 2021 16:20:59 -0700 Subject: [PATCH 0090/2644] Bump simplisafe-python to 2021.12.0 (#61121) --- homeassistant/components/simplisafe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 81cb5b7febc..954c39efce1 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,7 +3,7 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==2021.11.2"], + "requirements": ["simplisafe-python==2021.12.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index b8b3e13a3c1..dff7e51e115 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2155,7 +2155,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==2021.11.2 +simplisafe-python==2021.12.0 # homeassistant.components.sisyphus sisyphus-control==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7fc449bea41..9d98d61ffdc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1279,7 +1279,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==2021.11.2 +simplisafe-python==2021.12.0 # homeassistant.components.slack slackclient==2.5.0 From da4349d13357393951fa20813fb6cf649e374536 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 6 Dec 2021 17:21:28 -0600 Subject: [PATCH 0091/2644] Improve Sonos activity debug logging (#61122) --- homeassistant/components/sonos/helpers.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sonos/helpers.py b/homeassistant/components/sonos/helpers.py index e80d16a491b..74897a618ea 100644 --- a/homeassistant/components/sonos/helpers.py +++ b/homeassistant/components/sonos/helpers.py @@ -40,7 +40,7 @@ def soco_error( return None except (OSError, SoCoException, SoCoUPnPException) as err: error_code = getattr(err, "error_code", None) - function = funct.__name__ + function = funct.__qualname__ if errorcodes and error_code in errorcodes: _LOGGER.debug( "Error code %s ignored in call to %s", error_code, function @@ -59,7 +59,9 @@ def soco_error( return None dispatcher_send( - self.hass, f"{SONOS_SPEAKER_ACTIVITY}-{self.soco.uid}", funct.__name__ + self.hass, + f"{SONOS_SPEAKER_ACTIVITY}-{self.soco.uid}", + funct.__qualname__, ) return result From 4aa7f36a5393c7416daa1291217b641d332f8607 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 6 Dec 2021 16:23:03 -0700 Subject: [PATCH 0092/2644] Deprecate `entity_id` parameter in Guardian service calls (#61129) --- homeassistant/components/guardian/__init__.py | 75 ++++++++++++++----- 1 file changed, 57 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index 7f0834faff5..f91c667bfbd 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio from collections.abc import Awaitable, Callable -from typing import cast +from typing import TYPE_CHECKING, cast from aioguardian import Client from aioguardian.errors import GuardianError @@ -11,6 +11,8 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import ( + ATTR_DEVICE_ID, + ATTR_ENTITY_ID, CONF_DEVICE_ID, CONF_FILENAME, CONF_IP_ADDRESS, @@ -19,7 +21,11 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant, ServiceCall, callback -from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers import ( + config_validation as cv, + device_registry as dr, + entity_registry as er, +) from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import ( @@ -64,20 +70,41 @@ SERVICES = ( SERVICE_NAME_UPGRADE_FIRMWARE, ) -SERVICE_PAIR_UNPAIR_SENSOR_SCHEMA = vol.Schema( - { - vol.Required(CONF_DEVICE_ID): cv.string, - vol.Required(CONF_UID): cv.string, - } +SERVICE_BASE_SCHEMA = vol.All( + cv.deprecated(ATTR_ENTITY_ID), + vol.Schema( + { + vol.Optional(ATTR_DEVICE_ID): cv.string, + vol.Optional(ATTR_ENTITY_ID): cv.entity_id, + } + ), + cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), ) -SERVICE_UPGRADE_FIRMWARE_SCHEMA = vol.Schema( - { - vol.Required(CONF_DEVICE_ID): cv.string, - vol.Optional(CONF_URL): cv.url, - vol.Optional(CONF_PORT): cv.port, - vol.Optional(CONF_FILENAME): cv.string, - }, +SERVICE_PAIR_UNPAIR_SENSOR_SCHEMA = vol.All( + cv.deprecated(ATTR_ENTITY_ID), + vol.Schema( + { + vol.Optional(ATTR_DEVICE_ID): cv.string, + vol.Optional(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(CONF_UID): cv.string, + } + ), + cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), +) + +SERVICE_UPGRADE_FIRMWARE_SCHEMA = vol.All( + cv.deprecated(ATTR_ENTITY_ID), + vol.Schema( + { + vol.Optional(ATTR_DEVICE_ID): cv.string, + vol.Optional(ATTR_ENTITY_ID): cv.entity_id, + vol.Optional(CONF_URL): cv.url, + vol.Optional(CONF_PORT): cv.port, + vol.Optional(CONF_FILENAME): cv.string, + }, + ), + cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), ) @@ -87,6 +114,14 @@ PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH] @callback def async_get_entry_id_for_service_call(hass: HomeAssistant, call: ServiceCall) -> str: """Get the entry ID related to a service call (by device ID).""" + if ATTR_ENTITY_ID in call.data: + entity_registry = er.async_get(hass) + entity_registry_entry = entity_registry.async_get(call.data[ATTR_ENTITY_ID]) + if TYPE_CHECKING: + assert entity_registry_entry + assert entity_registry_entry.config_entry_id + return entity_registry_entry.config_entry_id + device_id = call.data[CONF_DEVICE_ID] device_registry = dr.async_get(hass) @@ -222,15 +257,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) for service_name, schema, method in ( - (SERVICE_NAME_DISABLE_AP, None, async_disable_ap), - (SERVICE_NAME_ENABLE_AP, None, async_enable_ap), + (SERVICE_NAME_DISABLE_AP, SERVICE_BASE_SCHEMA, async_disable_ap), + (SERVICE_NAME_ENABLE_AP, SERVICE_BASE_SCHEMA, async_enable_ap), ( SERVICE_NAME_PAIR_SENSOR, SERVICE_PAIR_UNPAIR_SENSOR_SCHEMA, async_pair_sensor, ), - (SERVICE_NAME_REBOOT, None, async_reboot), - (SERVICE_NAME_RESET_VALVE_DIAGNOSTICS, None, async_reset_valve_diagnostics), + (SERVICE_NAME_REBOOT, SERVICE_BASE_SCHEMA, async_reboot), + ( + SERVICE_NAME_RESET_VALVE_DIAGNOSTICS, + SERVICE_BASE_SCHEMA, + async_reset_valve_diagnostics, + ), ( SERVICE_NAME_UNPAIR_SENSOR, SERVICE_PAIR_UNPAIR_SENSOR_SCHEMA, From b8b4855b8e8f919d31987df272832605c90f0ff6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 7 Dec 2021 00:26:31 +0100 Subject: [PATCH 0093/2644] Prevent log flooding in frame helper (#61085) Co-authored-by: epenet --- homeassistant/helpers/frame.py | 9 +++++++++ tests/helpers/test_frame.py | 22 ++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/homeassistant/helpers/frame.py b/homeassistant/helpers/frame.py index 3995f24102d..13ffea48f81 100644 --- a/homeassistant/helpers/frame.py +++ b/homeassistant/helpers/frame.py @@ -12,6 +12,9 @@ from homeassistant.exceptions import HomeAssistantError _LOGGER = logging.getLogger(__name__) +# Keep track of integrations already reported to prevent flooding +_REPORTED_INTEGRATIONS: set[str] = set() + CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # pylint: disable=invalid-name @@ -85,6 +88,12 @@ def report_integration( """ found_frame, integration, path = integration_frame + # Keep track of integrations already reported to prevent flooding + key = f"{found_frame.filename}:{found_frame.lineno}" + if key in _REPORTED_INTEGRATIONS: + return + _REPORTED_INTEGRATIONS.add(key) + index = found_frame.filename.index(path) if path == "custom_components/": extra = " to the custom component author" diff --git a/tests/helpers/test_frame.py b/tests/helpers/test_frame.py index 5e48b2aec5f..37f3e7ec95f 100644 --- a/tests/helpers/test_frame.py +++ b/tests/helpers/test_frame.py @@ -1,4 +1,5 @@ """Test the frame helper.""" +# pylint: disable=protected-access from unittest.mock import Mock, patch import pytest @@ -70,3 +71,24 @@ async def test_extract_frame_no_integration(caplog): ], ), pytest.raises(frame.MissingIntegrationFrame): frame.get_integration_frame() + + +@pytest.mark.usefixtures("mock_integration_frame") +@patch.object(frame, "_REPORTED_INTEGRATIONS", set()) +async def test_prevent_flooding(caplog): + """Test to ensure a report is only written once to the log.""" + + what = "accessed hi instead of hello" + key = "/home/paulus/homeassistant/components/hue/light.py:23" + + frame.report(what, error_if_core=False) + assert what in caplog.text + assert key in frame._REPORTED_INTEGRATIONS + assert len(frame._REPORTED_INTEGRATIONS) == 1 + + caplog.clear() + + frame.report(what, error_if_core=False) + assert what not in caplog.text + assert key in frame._REPORTED_INTEGRATIONS + assert len(frame._REPORTED_INTEGRATIONS) == 1 From b45f48a35a82bdd0dfda8004c92626039a5e278b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 6 Dec 2021 15:51:03 -0800 Subject: [PATCH 0094/2644] Bump frontend to 20211206.0 (#61133) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 1ca97e4cdd8..6d419276029 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20211203.0" + "home-assistant-frontend==20211206.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 029f889fd85..5082fe5559d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ ciso8601==2.2.0 cryptography==35.0.0 emoji==1.5.0 hass-nabucasa==0.50.0 -home-assistant-frontend==20211203.0 +home-assistant-frontend==20211206.0 httpx==0.21.0 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index dff7e51e115..39b34cbcc7c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -822,7 +822,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211203.0 +home-assistant-frontend==20211206.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9d98d61ffdc..a62a75382a1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -518,7 +518,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211203.0 +home-assistant-frontend==20211206.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 5258c5fc9ccf5236f737f3b3f8f445e53c93106f Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 7 Dec 2021 00:51:52 +0000 Subject: [PATCH 0095/2644] [ci skip] Translation update --- .../components/apple_tv/translations/bg.json | 2 + .../components/apple_tv/translations/de.json | 23 ++- .../components/apple_tv/translations/en.json | 143 +++++++++--------- .../components/apple_tv/translations/et.json | 23 ++- .../components/apple_tv/translations/hu.json | 13 ++ .../components/apple_tv/translations/ru.json | 23 ++- .../aseko_pool_live/translations/bg.json | 20 +++ .../aseko_pool_live/translations/cs.json | 20 +++ .../aseko_pool_live/translations/de.json | 20 +++ .../aseko_pool_live/translations/et.json | 20 +++ .../aseko_pool_live/translations/hu.json | 20 +++ .../aseko_pool_live/translations/ja.json | 20 +++ .../aseko_pool_live/translations/nl.json | 20 +++ .../aseko_pool_live/translations/ru.json | 20 +++ .../aseko_pool_live/translations/tr.json | 20 +++ .../aseko_pool_live/translations/zh-Hant.json | 20 +++ .../binary_sensor/translations/et.json | 2 + .../binary_sensor/translations/tr.json | 2 + .../components/deconz/translations/ja.json | 2 + .../enphase_envoy/translations/de.json | 3 +- .../enphase_envoy/translations/et.json | 3 +- .../enphase_envoy/translations/hu.json | 3 +- .../enphase_envoy/translations/nl.json | 3 +- .../enphase_envoy/translations/ru.json | 3 +- .../enphase_envoy/translations/zh-Hant.json | 3 +- .../components/hue/translations/ja.json | 1 + .../components/insteon/translations/tr.json | 6 +- .../components/knx/translations/bg.json | 2 + .../components/knx/translations/et.json | 2 + .../components/knx/translations/nl.json | 2 + .../components/knx/translations/tr.json | 2 + .../components/mqtt/translations/ja.json | 2 + .../components/nina/translations/bg.json | 11 ++ .../components/nina/translations/et.json | 27 ++++ .../components/nina/translations/nl.json | 27 ++++ .../components/nina/translations/tr.json | 27 ++++ .../components/nina/translations/zh-Hant.json | 27 ++++ .../components/nuheat/translations/tr.json | 2 +- .../components/point/translations/tr.json | 2 +- .../components/ps4/translations/tr.json | 4 +- .../components/ridwell/translations/tr.json | 6 +- .../components/roku/translations/tr.json | 2 +- .../components/roomba/translations/tr.json | 2 +- .../simplisafe/translations/tr.json | 2 +- .../components/solarlog/translations/tr.json | 2 +- .../synology_dsm/translations/tr.json | 4 +- .../tellduslive/translations/tr.json | 2 +- .../transmission/translations/tr.json | 2 +- .../components/unifi/translations/tr.json | 2 +- .../components/watttime/translations/tr.json | 4 +- .../yale_smart_alarm/translations/cs.json | 3 +- .../yale_smart_alarm/translations/tr.json | 6 +- .../translations/select.de.json | 52 +++++++ .../translations/select.et.json | 52 +++++++ .../translations/select.hu.json | 52 +++++++ .../components/zha/translations/ja.json | 4 + .../components/zwave_js/translations/de.json | 2 + .../components/zwave_js/translations/en.json | 2 + .../components/zwave_js/translations/et.json | 2 + .../components/zwave_js/translations/hu.json | 2 + .../components/zwave_js/translations/ru.json | 2 + 61 files changed, 686 insertions(+), 116 deletions(-) create mode 100644 homeassistant/components/aseko_pool_live/translations/bg.json create mode 100644 homeassistant/components/aseko_pool_live/translations/cs.json create mode 100644 homeassistant/components/aseko_pool_live/translations/de.json create mode 100644 homeassistant/components/aseko_pool_live/translations/et.json create mode 100644 homeassistant/components/aseko_pool_live/translations/hu.json create mode 100644 homeassistant/components/aseko_pool_live/translations/ja.json create mode 100644 homeassistant/components/aseko_pool_live/translations/nl.json create mode 100644 homeassistant/components/aseko_pool_live/translations/ru.json create mode 100644 homeassistant/components/aseko_pool_live/translations/tr.json create mode 100644 homeassistant/components/aseko_pool_live/translations/zh-Hant.json create mode 100644 homeassistant/components/nina/translations/bg.json create mode 100644 homeassistant/components/nina/translations/et.json create mode 100644 homeassistant/components/nina/translations/nl.json create mode 100644 homeassistant/components/nina/translations/tr.json create mode 100644 homeassistant/components/nina/translations/zh-Hant.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/select.de.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/select.et.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/select.hu.json diff --git a/homeassistant/components/apple_tv/translations/bg.json b/homeassistant/components/apple_tv/translations/bg.json index 5a7ae474102..9da5f90cf26 100644 --- a/homeassistant/components/apple_tv/translations/bg.json +++ b/homeassistant/components/apple_tv/translations/bg.json @@ -1,8 +1,10 @@ { "config": { "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "already_configured_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "no_devices_found": "\u041d\u0435 \u0441\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "error": { diff --git a/homeassistant/components/apple_tv/translations/de.json b/homeassistant/components/apple_tv/translations/de.json index 6161550d736..d8eff460bd4 100644 --- a/homeassistant/components/apple_tv/translations/de.json +++ b/homeassistant/components/apple_tv/translations/de.json @@ -1,12 +1,17 @@ { "config": { "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "already_configured_device": "Ger\u00e4t ist bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "backoff": "Das Ger\u00e4t akzeptiert derzeit keine Kopplungsanfragen (M\u00f6glicherweise wurde zu oft ein ung\u00fcltiger PIN-Code eingegeben), versuche es sp\u00e4ter erneut.", "device_did_not_pair": "Es wurde kein Versuch unternommen, den Kopplungsvorgang vom Ger\u00e4t aus abzuschlie\u00dfen.", + "device_not_found": "Das Ger\u00e4t wurde bei der Erkennung nicht gefunden. Bitte versuche es erneut hinzuzuf\u00fcgen.", + "inconsistent_device": "Die erwarteten Protokolle wurden bei der Erkennung nicht gefunden. Dies deutet normalerweise auf ein Problem mit Multicast-DNS (Zeroconf) hin. Bitte versuche das Ger\u00e4t erneut hinzuzuf\u00fcgen.", "invalid_config": "Die Konfiguration f\u00fcr dieses Ger\u00e4t ist unvollst\u00e4ndig. Bitte versuche, es erneut hinzuzuf\u00fcgen.", "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", + "setup_failed": "Fehler beim Einrichten des Ger\u00e4ts.", "unknown": "Unerwarteter Fehler" }, "error": { @@ -16,14 +21,14 @@ "no_usable_service": "Es wurde ein Ger\u00e4t gefunden, aber es konnte keine M\u00f6glichkeit gefunden werden, eine Verbindung zu diesem Ger\u00e4t herzustellen. Wenn diese Meldung weiterhin erscheint, versuche, die IP-Adresse anzugeben oder den Apple TV neu zu starten.", "unknown": "Unerwarteter Fehler" }, - "flow_title": "{name}", + "flow_title": "{name} ({type})", "step": { "confirm": { - "description": "Es wird der Apple TV mit dem Namen \" {name} \" zu Home Assistant hinzugef\u00fcgt. \n\n ** Um den Vorgang abzuschlie\u00dfen, m\u00fcssen m\u00f6glicherweise mehrere PIN-Codes eingegeben werden. ** \n\n Bitte beachte, dass der Apple TV mit dieser Integration * nicht * ausgeschalten werden kann. Nur der Media Player in Home Assistant wird ausgeschaltet!", + "description": "Du bist dabei, `{name}` vom Typ `{type}` zu Home Assistant hinzuzuf\u00fcgen. \n\n **Um den Vorgang abzuschlie\u00dfen, musst du m\u00f6glicherweise mehrere PIN-Codes eingeben.** \n\n Bitte beachte, dass du dein Apple TV mit dieser Integration *nicht* ausschalten kannst. Nur der Mediaplayer in Home Assistant wird ausgeschaltet!", "title": "Best\u00e4tige das Hinzuf\u00fcgen vom Apple TV" }, "pair_no_pin": { - "description": "F\u00fcr den Dienst `{protocol}` ist eine Kopplung erforderlich. Bitte gebe die PIN {pin} am Apple TV ein, um fortzufahren.", + "description": "F\u00fcr den Dienst `{protocol}` ist eine Kopplung erforderlich. Bitte gib die PIN {pin} auf deinem Ger\u00e4t ein, um fortzufahren.", "title": "Kopplung" }, "pair_with_pin": { @@ -33,8 +38,16 @@ "description": "F\u00fcr das Protokoll `{protocol}` ist eine Kopplung erforderlich. Bitte gebe den auf dem Bildschirm angezeigten PIN-Code ein. F\u00fchrende Nullen m\u00fcssen weggelassen werden, d.h. gebe 123 ein, wenn der angezeigte Code 0123 lautet.", "title": "Kopplung" }, + "password": { + "description": "Ein Passwort ist f\u00fcr `{protocol}` erforderlich. Dies wird noch nicht unterst\u00fctzt, bitte deaktiviere das Passwort, um fortzufahren.", + "title": "Passwort erforderlich" + }, + "protocol_disabled": { + "description": "Die Kopplung ist f\u00fcr `{protocol}` erforderlich, aber auf dem Ger\u00e4t deaktiviert. Bitte \u00fcberpr\u00fcfe m\u00f6gliche Zugriffsbeschr\u00e4nkungen (z. B. Verbindung aller Ger\u00e4te im lokalen Netzwerk zulassen) auf dem Ger\u00e4t. \n\nDu kannst fortfahren, ohne dieses Protokoll zu koppeln, aber einige Funktionen sind eingeschr\u00e4nkt.", + "title": "Kopplung nicht m\u00f6glich" + }, "reconfigure": { - "description": "Dieser Apple TV hat Verbindungsprobleme und muss neu konfiguriert werden.", + "description": "Konfiguriere dieses Ger\u00e4t neu, um seine Funktionalit\u00e4t wiederherzustellen.", "title": "Ger\u00e4teneukonfiguration" }, "service_problem": { @@ -45,7 +58,7 @@ "data": { "device_input": "Ger\u00e4t" }, - "description": "Gebe zun\u00e4chst den Ger\u00e4tenamen (z. B. K\u00fcche oder Schlafzimmer) oder die IP-Adresse des Apple TV ein, der hinzugef\u00fcgt werden soll. Wenn Ger\u00e4te automatisch im Netzwerk gefunden wurden, werden sie unten angezeigt. \n\nWenn das Ger\u00e4t nicht sichtbar ist oder Probleme auftreten, gebe die IP-Adresse des Ger\u00e4ts an. \n\n{devices}", + "description": "Gib zun\u00e4chst den Ger\u00e4tenamen (z. B. K\u00fcche oder Schlafzimmer) oder die IP-Adresse des Apple TV ein, den du hinzuf\u00fcgen m\u00f6chtest.\n\nWenn du dein Ger\u00e4t nicht sehen kannst oder Probleme auftreten, versuche die IP-Adresse des Ger\u00e4ts einzugeben.", "title": "Neuen Apple TV einrichten" } } diff --git a/homeassistant/components/apple_tv/translations/en.json b/homeassistant/components/apple_tv/translations/en.json index 7f755320e34..4d4d1bb1679 100644 --- a/homeassistant/components/apple_tv/translations/en.json +++ b/homeassistant/components/apple_tv/translations/en.json @@ -1,74 +1,77 @@ { - "config": { - "flow_title": "{name} ({type})", - "step": { - "user": { - "title": "Setup a new Apple TV", - "description": "Start by entering the device name (e.g. Kitchen or Bedroom) or IP address of the Apple TV you want to add.\n\nIf you cannot see your device or experience any issues, try specifying the device IP address.", - "data": { - "device_input": "Device" + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_configured_device": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "backoff": "Device does not accept pairing reqests at this time (you might have entered an invalid PIN code too many times), try again later.", + "device_did_not_pair": "No attempt to finish pairing process was made from the device.", + "device_not_found": "Device was not found during discovery, please try adding it again.", + "inconsistent_device": "Expected protocols were not found during discovery. This normally indicates a problem with multicast DNS (Zeroconf). Please try adding the device again.", + "invalid_config": "The configuration for this device is incomplete. Please try adding it again.", + "no_devices_found": "No devices found on the network", + "reauth_successful": "Re-authentication was successful", + "setup_failed": "Failed to set up device.", + "unknown": "Unexpected error" + }, + "error": { + "already_configured": "Device is already configured", + "invalid_auth": "Invalid authentication", + "no_devices_found": "No devices found on the network", + "no_usable_service": "A device was found but could not identify any way to establish a connection to it. If you keep seeing this message, try specifying its IP address or restarting your Apple TV.", + "unknown": "Unexpected error" + }, + "flow_title": "{name} ({type})", + "step": { + "confirm": { + "description": "You are about to add `{name}` of type `{type}` to Home Assistant.\n\n**To complete the process, you may have to enter multiple PIN codes.**\n\nPlease note that you will *not* be able to power off your Apple TV with this integration. Only the media player in Home Assistant will turn off!", + "title": "Confirm adding Apple TV" + }, + "pair_no_pin": { + "description": "Pairing is required for the `{protocol}` service. Please enter PIN {pin} on your device to continue.", + "title": "Pairing" + }, + "pair_with_pin": { + "data": { + "pin": "PIN Code" + }, + "description": "Pairing is required for the `{protocol}` protocol. Please enter the PIN code displayed on screen. Leading zeros shall be omitted, i.e. enter 123 if the displayed code is 0123.", + "title": "Pairing" + }, + "password": { + "description": "A password is required by `{protocol}`. This is not yet supported, please disable password to continue.", + "title": "Password required" + }, + "protocol_disabled": { + "description": "Pairing is required for `{protocol}` but it is disabled on the device. Please review potential access restrictions (e.g. allow all devices on the local network to connect) on the device.\n\nYou may continue without pairing this protocol, but some functionality will be limited.", + "title": "Pairing not possible" + }, + "reconfigure": { + "description": "Reconfigure this device to restore its functionality.", + "title": "Device reconfiguration" + }, + "service_problem": { + "description": "A problem occurred while pairing protocol `{protocol}`. It will be ignored.", + "title": "Failed to add service" + }, + "user": { + "data": { + "device_input": "Device" + }, + "description": "Start by entering the device name (e.g. Kitchen or Bedroom) or IP address of the Apple TV you want to add.\n\nIf you cannot see your device or experience any issues, try specifying the device IP address.", + "title": "Setup a new Apple TV" + } } - }, - "reconfigure": { - "title": "Device reconfiguration", - "description": "Reconfigure this device to restore its functionality." - }, - "pair_with_pin": { - "title": "Pairing", - "description": "Pairing is required for the `{protocol}` protocol. Please enter the PIN code displayed on screen. Leading zeros shall be omitted, i.e. enter 123 if the displayed code is 0123.", - "data": { - "pin": "PIN Code" - } - }, - "pair_no_pin": { - "title": "Pairing", - "description": "Pairing is required for the `{protocol}` service. Please enter PIN {pin} on your device to continue." - }, - "protocol_disabled": { - "title": "Pairing not possible", - "description": "Pairing is required for `{protocol}` but it is disabled on the device. Please review potential access restrictions (e.g. allow all devices on the local network to connect) on the device.\n\nYou may continue without pairing this protocol, but some functionality will be limited." - }, - "confirm": { - "title": "Confirm adding Apple TV", - "description": "You are about to add `{name}` of type `{type}` to Home Assistant.\n\n**To complete the process, you may have to enter multiple PIN codes.**" - }, - "service_problem": { - "title": "Failed to add service", - "description": "A problem occurred while pairing protocol `{protocol}`. It will be ignored." - }, - "password": { - "title": "Password required", - "description": "A password is required by `{protocol}`. This is not yet supported, please disable password to continue." - } }, - "error": { - "no_devices_found": "No devices found on the network", - "already_configured_device": "Device is already configured", - "unknown": "Unexpected error", - "invalid_auth": "Invalid authentication" - }, - "abort": { - "no_devices_found": "No devices found on the network", - "already_configured_device": "Device is already configured", - "device_did_not_pair": "No attempt to finish pairing process was made from the device.", - "backoff": "Device does not accept pairing reqests at this time (you might have entered an invalid PIN code too many times), try again later.", - "already_in_progress": "Configuration flow is already in progress", - "unknown": "Unexpected error", - "setup_failed": "Failed to set up device.", - "reauth_successful": "Re-authentication was successful", - "device_not_found": "Device was not found during discovery, please try adding it again.", - "inconsistent_device": "Expected protocols were not found during discovery. This normally indicates a problem with multicast DNS (Zeroconf). Please try adding the device again." - } - }, - "options": { - "step": { - "init": { - "description": "Configure general device settings", - "data": { - "start_off": "Do not turn device on when starting Home Assistant", - "reconfigure": "Force reconfiguration of device" + "options": { + "step": { + "init": { + "data": { + "start_off": "Do not turn device on when starting Home Assistant" + }, + "description": "Configure general device settings" + } } - } - } - } -} + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/et.json b/homeassistant/components/apple_tv/translations/et.json index a4a06d8e1b1..fa660662d8b 100644 --- a/homeassistant/components/apple_tv/translations/et.json +++ b/homeassistant/components/apple_tv/translations/et.json @@ -1,12 +1,17 @@ { "config": { "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "already_configured_device": "Seade on juba h\u00e4\u00e4lestatud", "already_in_progress": "Seadistamine on juba k\u00e4imas", "backoff": "Seade ei aktsepteeri praegu sidumisn\u00f5udeid (v\u00f5ib-olla oled liiga palju kordi vale PIN-koodi sisestanud), proovi hiljem uuesti.", "device_did_not_pair": "Seade ei \u00fcritatud sidumisprotsessi l\u00f5pule viia.", + "device_not_found": "Seadet avastamise ajal ei leitud, proovi seda uuesti lisada.", + "inconsistent_device": "Eeldatavaid protokolle avastamise ajal ei leitud. See n\u00e4itab tavaliselt probleemi multcast DNS-iga (Zeroconf). Proovi seade uuesti lisada.", "invalid_config": "Selle seadme s\u00e4tted on puudulikud. Proovi see uuesti lisada.", "no_devices_found": "V\u00f5rgust ei leitud \u00fchtegi seadet", + "reauth_successful": "Taastuvastamine \u00f5nnestus", + "setup_failed": "Seadme h\u00e4\u00e4lestamine nurjus.", "unknown": "Ootamatu t\u00f5rge" }, "error": { @@ -16,14 +21,14 @@ "no_usable_service": "Leiti seade kuid ei suudetud tuvastada moodust \u00fchenduse loomiseks. Kui n\u00e4ed seda teadet pidevalt, proovi m\u00e4\u00e4rata seadme IP-aadress v\u00f5i taask\u00e4ivita Apple TV.", "unknown": "Ootamatu t\u00f5rge" }, - "flow_title": "{name}", + "flow_title": "{name} ({type})", "step": { "confirm": { - "description": "Oled Home Assistantile lisamas Apple TV-d nimega {name}.\n\n**Protsessi l\u00f5puleviimiseks pead v\u00f5ib-olla sisestama mitu PIN-koodi.**\n\nPane t\u00e4hele, et selle sidumisega * ei saa * v\u00e4lja l\u00fclitada oma Apple TV-d. Ainult Home Assistant-i meediam\u00e4ngija l\u00fclitub v\u00e4lja!", + "description": "Oled Home Assistantile lisamas `{type}` seadet nimega {name}.\n\n**Protsessi l\u00f5puleviimiseks pead v\u00f5ib-olla sisestama mitu PIN-koodi.**\n\nPane t\u00e4hele, et selle sidumisega * ei saa * v\u00e4lja l\u00fclitada oma Apple TV-d. Ainult Home Assistant-i meediam\u00e4ngija l\u00fclitub v\u00e4lja!", "title": "Kinnita Apple TV lisamine" }, "pair_no_pin": { - "description": "Teenuse {protocol} sidumine on vajalik. J\u00e4tkamiseks sisesta oma Apple TV-s PIN-kood {pin} .", + "description": "Teenuse {protocol} sidumine on vajalik. J\u00e4tkamiseks sisesta oma seadmes PIN-kood {pin} .", "title": "Sidumine" }, "pair_with_pin": { @@ -33,8 +38,16 @@ "description": "Vajalik on protokolli {protocol} sidumine. Sisesta ekraanil kuvatav PIN-kood. Alguse nullid j\u00e4etakse v\u00e4lja, st. sisesta 123, kui kuvatav kood on 0123.", "title": "Sidumine" }, + "password": { + "description": "`{protocol}` vajab salas\u00f5na. See ei ole veel toetatud, j\u00e4tkamiseks l\u00fclita salas\u00f5na v\u00e4lja.", + "title": "Salas\u00f5na on n\u00f5utav" + }, + "protocol_disabled": { + "description": "`{protocol}` jaoks on vajalik sidumine kuid see on seadmes keelatud. Vaata \u00fcle seadme v\u00f5imalikud juurdep\u00e4\u00e4supiirangud (nt luba k\u00f5igil kohtv\u00f5rgu seadmetel \u00fchenduda). \n\n V\u00f5id j\u00e4tkata ilma seda protokolli sidumata, kuid m\u00f5ned funktsioonid on piiratud.", + "title": "Sidumine pole v\u00f5imalik" + }, "reconfigure": { - "description": "Sellel Apple TV-l on \u00fchendusprobleemid ja see tuleb uuesti seadistada.", + "description": "Seadme funktsionaalsuse taastamiseks konfigureeri see seade \u00fcmber.", "title": "Seadme \u00fcmberseadistamine" }, "service_problem": { @@ -45,7 +58,7 @@ "data": { "device_input": "Seade" }, - "description": "Alustuseks sisesta lisatava Apple TV seadme nimi (nt K\u00f6\u00f6k v\u00f5i Magamistuba) v\u00f5i IP-aadress. Kui m\u00f5ni seade leiti teie v\u00f5rgust automaatselt kuvatakse see allpool. \n\n Kui ei n\u00e4e oma seadet v\u00f5i on probleeme, proovi m\u00e4\u00e4rata seadme IP-aadress. \n\n {devices}", + "description": "Alustuseks sisesta lisatava Apple TV seadme nimi (nt K\u00f6\u00f6k v\u00f5i Magamistuba) v\u00f5i IP-aadress. Kui m\u00f5ni seade leiti teie v\u00f5rgust automaatselt kuvatakse see allpool. \n\n Kui ei n\u00e4e oma seadet v\u00f5i on probleeme, proovi m\u00e4\u00e4rata seadme IP-aadress.", "title": "Seadista uus Apple TV sidumine" } } diff --git a/homeassistant/components/apple_tv/translations/hu.json b/homeassistant/components/apple_tv/translations/hu.json index 3ee74bbf419..f76063c5eeb 100644 --- a/homeassistant/components/apple_tv/translations/hu.json +++ b/homeassistant/components/apple_tv/translations/hu.json @@ -1,12 +1,17 @@ { "config": { "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "already_configured_device": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", "backoff": "Az eszk\u00f6z jelenleg nem fogadja el a p\u00e1ros\u00edt\u00e1si k\u00e9relmeket (lehet, hogy t\u00fal sokszor adott meg \u00e9rv\u00e9nytelen PIN-k\u00f3dot), pr\u00f3b\u00e1lkozzon \u00fajra k\u00e9s\u0151bb.", "device_did_not_pair": "A p\u00e1ros\u00edt\u00e1s folyamat\u00e1t az eszk\u00f6zr\u0151l nem pr\u00f3b\u00e1lt\u00e1k befejezni.", + "device_not_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a felder\u00edt\u00e9s sor\u00e1n, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg \u00fajra hozz\u00e1adni.", + "inconsistent_device": "Az elv\u00e1rt protokollok nem tal\u00e1lhat\u00f3k a felder\u00edt\u00e9s sor\u00e1n. Ez \u00e1ltal\u00e1ban a multicast DNS (Zeroconf) probl\u00e9m\u00e1j\u00e1t jelzi. K\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg \u00fajra hozz\u00e1adni az eszk\u00f6zt.", "invalid_config": "Az eszk\u00f6z konfigur\u00e1l\u00e1sa nem teljes. K\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg \u00fajra hozz\u00e1adni.", "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", + "setup_failed": "Az eszk\u00f6z be\u00e1ll\u00edt\u00e1sa sikertelen.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "error": { @@ -33,6 +38,14 @@ "description": "P\u00e1ros\u00edt\u00e1sra van sz\u00fcks\u00e9g a {protocol} protokollhoz. K\u00e9rj\u00fck, adja meg a k\u00e9perny\u0151n megjelen\u0151 PIN-k\u00f3dot. A vezet\u0151 null\u00e1kat el kell hagyni, pl. \u00edrja be a 123 \u00e9rt\u00e9ket, ha a megjelen\u00edtett k\u00f3d 0123.", "title": "P\u00e1ros\u00edt\u00e1s" }, + "password": { + "description": "`{protocol}` jelsz\u00f3t ig\u00e9nyel. Ez m\u00e9g nem t\u00e1mogatott, k\u00e9rj\u00fck, a folytat\u00e1shoz tiltsa le a jelsz\u00f3t.", + "title": "Jelsz\u00f3 sz\u00fcks\u00e9ges" + }, + "protocol_disabled": { + "description": "P\u00e1ros\u00edt\u00e1s sz\u00fcks\u00e9ges a `{protokoll}` miatt, de az eszk\u00f6z\u00f6n le van tiltva. K\u00e9rj\u00fck, vizsg\u00e1lja meg az eszk\u00f6z\u00f6n az esetleges hozz\u00e1f\u00e9r\u00e9si korl\u00e1toz\u00e1sokat (pl. enged\u00e9lyezze a helyi h\u00e1l\u00f3zaton l\u00e9v\u0151 \u00f6sszes eszk\u00f6z csatlakoztat\u00e1s\u00e1t).\n\nFolytathatja a protokoll p\u00e1ros\u00edt\u00e1sa n\u00e9lk\u00fcl is, de bizonyos funkci\u00f3k korl\u00e1tozottak lesznek.", + "title": "A p\u00e1ros\u00edt\u00e1s nem lehets\u00e9ges" + }, "reconfigure": { "description": "Ez az Apple TV csatlakoz\u00e1si neh\u00e9zs\u00e9gekkel k\u00fczd, ez\u00e9rt \u00fajra kell konfigur\u00e1lni.", "title": "Eszk\u00f6z \u00fajrakonfigur\u00e1l\u00e1sa" diff --git a/homeassistant/components/apple_tv/translations/ru.json b/homeassistant/components/apple_tv/translations/ru.json index b37452d6bcb..4863a541603 100644 --- a/homeassistant/components/apple_tv/translations/ru.json +++ b/homeassistant/components/apple_tv/translations/ru.json @@ -1,12 +1,17 @@ { "config": { "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "already_configured_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", "backoff": "\u0412 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0435\u0442 \u0437\u0430\u043f\u0440\u043e\u0441\u044b \u043d\u0430 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 (\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e, \u0412\u044b \u0441\u043b\u0438\u0448\u043a\u043e\u043c \u043c\u043d\u043e\u0433\u043e \u0440\u0430\u0437 \u0432\u0432\u043e\u0434\u0438\u043b\u0438 \u043d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 PIN-\u043a\u043e\u0434), \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435.", "device_did_not_pair": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043f\u044b\u0442\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043f\u0440\u043e\u0446\u0435\u0441\u0441 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f.", + "device_not_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0435\u0433\u043e \u0435\u0449\u0451 \u0440\u0430\u0437.", + "inconsistent_device": "\u041d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u044b\u0435 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u044b. \u041e\u0431\u044b\u0447\u043d\u043e \u044d\u0442\u043e \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u0442 \u043d\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443 \u0441 \u043c\u043d\u043e\u0433\u043e\u0430\u0434\u0440\u0435\u0441\u043d\u044b\u043c DNS (Zeroconf). \u041f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0435\u0449\u0451 \u0440\u0430\u0437.", "invalid_config": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0430. \u041f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0435\u0433\u043e \u0435\u0449\u0451 \u0440\u0430\u0437.", "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e.", + "setup_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { @@ -16,14 +21,14 @@ "no_usable_service": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0441\u043f\u043e\u0441\u043e\u0431 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u043e\u043c\u0443 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443. \u0415\u0441\u043b\u0438 \u0412\u044b \u0443\u0436\u0435 \u0432\u0438\u0434\u0435\u043b\u0438 \u044d\u0442\u043e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0443\u043a\u0430\u0437\u0430\u0442\u044c IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0438\u043b\u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 \u0435\u0433\u043e.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, - "flow_title": "{name}", + "flow_title": "{name} ({type})", "step": { "confirm": { - "description": "\u0412\u044b \u0441\u043e\u0431\u0438\u0440\u0430\u0435\u0442\u0435\u0441\u044c \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c Apple TV `{name}` \u0432 Home Assistant. \n\n**\u0414\u043b\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u044f \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0430 \u0412\u0430\u043c \u043c\u043e\u0436\u0435\u0442 \u043f\u043e\u0442\u0440\u0435\u0431\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0432\u0432\u0435\u0441\u0442\u0438 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e PIN-\u043a\u043e\u0434\u043e\u0432.** \n\n\u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0447\u0442\u043e \u0412\u044b *\u043d\u0435* \u0441\u043c\u043e\u0436\u0435\u0442\u0435 \u0432\u044b\u043a\u043b\u044e\u0447\u0438\u0442\u044c Apple TV \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u044d\u0442\u043e\u0439 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438. \u0412 Home Assistant \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0432\u044b\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043c\u0435\u0434\u0438\u0430\u043f\u043b\u0435\u0435\u0440!", + "description": "\u0412\u044b \u0441\u043e\u0431\u0438\u0440\u0430\u0435\u0442\u0435\u0441\u044c \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c Apple TV `{name}` `{type}` \u0432 Home Assistant. \n\n**\u0414\u043b\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u044f \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0430 \u0412\u0430\u043c \u043c\u043e\u0436\u0435\u0442 \u043f\u043e\u0442\u0440\u0435\u0431\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0432\u0432\u0435\u0441\u0442\u0438 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e PIN-\u043a\u043e\u0434\u043e\u0432.** \n\n\u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0447\u0442\u043e \u0412\u044b *\u043d\u0435* \u0441\u043c\u043e\u0436\u0435\u0442\u0435 \u0432\u044b\u043a\u043b\u044e\u0447\u0438\u0442\u044c Apple TV \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u044d\u0442\u043e\u0439 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438. \u0412 Home Assistant \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0432\u044b\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043c\u0435\u0434\u0438\u0430\u043f\u043b\u0435\u0435\u0440!", "title": "\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 Apple TV" }, "pair_no_pin": { - "description": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0434\u043b\u044f \u0441\u043b\u0443\u0436\u0431\u044b`{protocol}`. \u0414\u043b\u044f \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0435\u043d\u0438\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 PIN-\u043a\u043e\u0434 {pin} \u043d\u0430 \u0412\u0430\u0448\u0435\u043c Apple TV.", + "description": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0434\u043b\u044f \u0441\u043b\u0443\u0436\u0431\u044b`{protocol}`. \u0414\u043b\u044f \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0435\u043d\u0438\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 PIN-\u043a\u043e\u0434 {pin} \u043d\u0430 \u0412\u0430\u0448\u0435\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435.", "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435" }, "pair_with_pin": { @@ -33,8 +38,16 @@ "description": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0430 `{protocol}`. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 PIN-\u043a\u043e\u0434, \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u043c\u044b\u0439 \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435. \u041f\u0435\u0440\u0432\u044b\u0435 \u043d\u0443\u043b\u0438 \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u043e\u043f\u0443\u0449\u0435\u043d\u044b, \u0442.\u0435. \u0432\u0432\u0435\u0434\u0438\u0442\u0435 123, \u0435\u0441\u043b\u0438 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u043c\u044b\u0439 \u043a\u043e\u0434 0123.", "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435" }, + "password": { + "description": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b `{protocol}` \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u043f\u0430\u0440\u043e\u043b\u044c. \u042d\u0442\u043e \u043f\u043e\u043a\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f. \u041e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c, \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c.", + "title": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u0430\u0440\u043e\u043b\u044c" + }, + "protocol_disabled": { + "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0434\u043b\u044f `{protocol}`, \u043d\u043e \u043e\u043d\u043e \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435. \u041f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u044b\u0435 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u044f \u0434\u043e\u0441\u0442\u0443\u043f\u0430 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u0440\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u0435 \u0432\u0441\u0435\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c \u0432 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0439 \u0441\u0435\u0442\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0442\u044c\u0441\u044f) \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443. \n\n\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c \u0431\u0435\u0437 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f, \u043d\u043e \u043d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u0438 \u0431\u0443\u0434\u0443\u0442 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u044b.", + "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e" + }, "reconfigure": { - "description": "\u0423 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Apple TV \u0432\u043e\u0437\u043d\u0438\u043a\u0430\u044e\u0442 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u043f\u0440\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438, \u0435\u0433\u043e \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043f\u0435\u0440\u0435\u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c.", + "description": "\u041f\u0435\u0440\u0435\u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u044d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u0447\u0442\u043e\u0431\u044b \u0432\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0435\u0433\u043e \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u044c.", "title": "\u041f\u0435\u0440\u0435\u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" }, "service_problem": { @@ -45,7 +58,7 @@ "data": { "device_input": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" }, - "description": "\u041d\u0430\u0447\u043d\u0438\u0442\u0435 \u0441 \u0432\u0432\u043e\u0434\u0430 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u041a\u0443\u0445\u043d\u044f \u0438\u043b\u0438 \u0421\u043f\u0430\u043b\u044c\u043d\u044f) \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441\u0430 Apple TV, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c. \u0415\u0441\u043b\u0438 \u043a\u0430\u043a\u0438\u0435-\u043b\u0438\u0431\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0431\u044b\u043b\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u044b \u0432 \u0412\u0430\u0448\u0435\u0439 \u0441\u0435\u0442\u0438, \u043e\u043d\u0438 \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u044b \u043d\u0438\u0436\u0435. \n\n\u0415\u0441\u043b\u0438 \u0412\u044b \u043d\u0435 \u0432\u0438\u0434\u0438\u0442\u0435 \u0441\u0432\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0438\u043b\u0438 \u0432\u043e\u0437\u043d\u0438\u043a\u0430\u044e\u0442 \u043a\u0430\u043a\u0438\u0435-\u043b\u0438\u0431\u043e \u0434\u0440\u0443\u0433\u0438\u0435 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u043f\u0440\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0443\u043a\u0430\u0437\u0430\u0442\u044c IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430. \n\n {devices}", + "description": "\u041d\u0430\u0447\u043d\u0438\u0442\u0435 \u0441 \u0432\u0432\u043e\u0434\u0430 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u041a\u0443\u0445\u043d\u044f \u0438\u043b\u0438 \u0421\u043f\u0430\u043b\u044c\u043d\u044f) \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441\u0430 Apple TV, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c. \n\n\u0415\u0441\u043b\u0438 \u0412\u044b \u043d\u0435 \u0432\u0438\u0434\u0438\u0442\u0435 \u0441\u0432\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0438\u043b\u0438 \u0432\u043e\u0437\u043d\u0438\u043a\u0430\u044e\u0442 \u043a\u0430\u043a\u0438\u0435-\u043b\u0438\u0431\u043e \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0443\u043a\u0430\u0437\u0430\u0442\u044c IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.", "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043d\u043e\u0432\u043e\u0433\u043e Apple TV" } } diff --git a/homeassistant/components/aseko_pool_live/translations/bg.json b/homeassistant/components/aseko_pool_live/translations/bg.json new file mode 100644 index 00000000000..982674c337e --- /dev/null +++ b/homeassistant/components/aseko_pool_live/translations/bg.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aseko_pool_live/translations/cs.json b/homeassistant/components/aseko_pool_live/translations/cs.json new file mode 100644 index 00000000000..580d10b354d --- /dev/null +++ b/homeassistant/components/aseko_pool_live/translations/cs.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "email": "E-mail", + "password": "Heslo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aseko_pool_live/translations/de.json b/homeassistant/components/aseko_pool_live/translations/de.json new file mode 100644 index 00000000000..6714068ab30 --- /dev/null +++ b/homeassistant/components/aseko_pool_live/translations/de.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "email": "E-Mail", + "password": "Passwort" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aseko_pool_live/translations/et.json b/homeassistant/components/aseko_pool_live/translations/et.json new file mode 100644 index 00000000000..b2550d5634b --- /dev/null +++ b/homeassistant/components/aseko_pool_live/translations/et.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Konto on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "email": "E-posti aadress", + "password": "Salas\u00f5na" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aseko_pool_live/translations/hu.json b/homeassistant/components/aseko_pool_live/translations/hu.json new file mode 100644 index 00000000000..46a9e952fcf --- /dev/null +++ b/homeassistant/components/aseko_pool_live/translations/hu.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "email": "E-mail", + "password": "Jelsz\u00f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aseko_pool_live/translations/ja.json b/homeassistant/components/aseko_pool_live/translations/ja.json new file mode 100644 index 00000000000..aed2b7c0932 --- /dev/null +++ b/homeassistant/components/aseko_pool_live/translations/ja.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "user": { + "data": { + "email": "E\u30e1\u30fc\u30eb", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aseko_pool_live/translations/nl.json b/homeassistant/components/aseko_pool_live/translations/nl.json new file mode 100644 index 00000000000..a347122d078 --- /dev/null +++ b/homeassistant/components/aseko_pool_live/translations/nl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Account is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "email": "E-mail", + "password": "Wachtwoord" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aseko_pool_live/translations/ru.json b/homeassistant/components/aseko_pool_live/translations/ru.json new file mode 100644 index 00000000000..23896af8091 --- /dev/null +++ b/homeassistant/components/aseko_pool_live/translations/ru.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aseko_pool_live/translations/tr.json b/homeassistant/components/aseko_pool_live/translations/tr.json new file mode 100644 index 00000000000..3d71918869e --- /dev/null +++ b/homeassistant/components/aseko_pool_live/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "email": "E-posta", + "password": "Parola" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aseko_pool_live/translations/zh-Hant.json b/homeassistant/components/aseko_pool_live/translations/zh-Hant.json new file mode 100644 index 00000000000..afeaa113fbb --- /dev/null +++ b/homeassistant/components/aseko_pool_live/translations/zh-Hant.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "email": "\u96fb\u5b50\u90f5\u4ef6", + "password": "\u5bc6\u78bc" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/et.json b/homeassistant/components/binary_sensor/translations/et.json index 316d1738e62..f2c38f1bc7c 100644 --- a/homeassistant/components/binary_sensor/translations/et.json +++ b/homeassistant/components/binary_sensor/translations/et.json @@ -84,6 +84,7 @@ "not_powered": "{entity_name} pole toidet", "not_present": "{entity_name} puudub", "not_running": "{entity_name} ei t\u00f6\u00f6ta enam", + "not_tampered": "{entity_name} l\u00f5petas omavolilise muutmise tuvastamise", "not_unsafe": "{entity_name} muutus turvaliseks", "occupied": "{entity_name} h\u00f5ivati", "opened": "{entity_name} avanes", @@ -94,6 +95,7 @@ "running": "{entity_name} alustas t\u00f6\u00f6d", "smoke": "{entity_name} tuvastas suitsu", "sound": "{entity_name} tuvastas heli", + "tampered": "{entity_name} tuvastas omavolilist muutmist", "turned_off": "{entity_name} l\u00fclitus v\u00e4lja", "turned_on": "{entity_name} l\u00fclitus sisse", "unsafe": "{entity_name} on ebaturvaline", diff --git a/homeassistant/components/binary_sensor/translations/tr.json b/homeassistant/components/binary_sensor/translations/tr.json index 72022dd9d39..086e139c533 100644 --- a/homeassistant/components/binary_sensor/translations/tr.json +++ b/homeassistant/components/binary_sensor/translations/tr.json @@ -84,6 +84,7 @@ "not_powered": "{entity_name} desteklenmiyor", "not_present": "{entity_name} mevcut de\u011fil", "not_running": "{entity_name} art\u0131k \u00e7al\u0131\u015fm\u0131yor", + "not_tampered": "{entity_name} kurcalamay\u0131 alg\u0131lamay\u0131 durdurdu", "not_unsafe": "{entity_name} g\u00fcvenli hale geldi", "occupied": "{entity_name} i\u015fgal edildi", "opened": "{entity_name} a\u00e7\u0131ld\u0131", @@ -94,6 +95,7 @@ "running": "{entity_name} \u00e7al\u0131\u015fmaya ba\u015flad\u0131", "smoke": "{entity_name} duman alg\u0131lamaya ba\u015flad\u0131", "sound": "{entity_name} sesi alg\u0131lamaya ba\u015flad\u0131", + "tampered": "{entity_name} , kurcalamay\u0131 alg\u0131lamaya ba\u015flad\u0131", "turned_off": "{entity_name} kapat\u0131ld\u0131", "turned_on": "{entity_name} a\u00e7\u0131ld\u0131", "unsafe": "{entity_name} g\u00fcvensiz hale geldi", diff --git a/homeassistant/components/deconz/translations/ja.json b/homeassistant/components/deconz/translations/ja.json index a0c7e7b9cd4..8b6730ab337 100644 --- a/homeassistant/components/deconz/translations/ja.json +++ b/homeassistant/components/deconz/translations/ja.json @@ -71,6 +71,7 @@ "remote_button_quintuple_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u30925\u56de(quintuple)\u30af\u30ea\u30c3\u30af", "remote_button_rotated": "\u30dc\u30bf\u30f3\u304c\u56de\u8ee2\u3057\u305f \"{subtype}\"", "remote_button_rotated_fast": "\u30dc\u30bf\u30f3\u304c\u9ad8\u901f\u56de\u8ee2\u3057\u305f \"{subtype}\"", + "remote_button_rotation_stopped": "\u30dc\u30bf\u30f3\u306e\u56de\u8ee2 \"{subtype}\" \u304c\u505c\u6b62\u3057\u307e\u3057\u305f", "remote_button_short_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u304c\u62bc\u3055\u308c\u307e\u3057\u305f\u3002", "remote_button_short_release": "\"{subtype}\" \u30dc\u30bf\u30f3\u304c\u30ea\u30ea\u30fc\u30b9\u3055\u308c\u307e\u3057\u305f", "remote_button_triple_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u30923\u56de\u30af\u30ea\u30c3\u30af", @@ -80,6 +81,7 @@ "remote_flip_180_degrees": "\u30c7\u30d0\u30a4\u30b9\u304c180\u5ea6\u53cd\u8ee2", "remote_flip_90_degrees": "\u30c7\u30d0\u30a4\u30b9\u304c90\u5ea6\u53cd\u8ee2", "remote_gyro_activated": "\u30c7\u30d0\u30a4\u30b9\u304c\u63fa\u308c\u308b", + "remote_moved": "\u30c7\u30d0\u30a4\u30b9\u306f \"{subtype}\" \u3092\u4e0a\u306b\u3057\u3066\u79fb\u52d5\u3057\u307e\u3057\u305f", "remote_moved_any_side": "\u30c7\u30d0\u30a4\u30b9\u304c\u4efb\u610f\u306e\u9762\u3092\u4e0a\u306b\u3057\u3066\u79fb\u52d5\u3057\u305f", "remote_rotate_from_side_1": "\u30c7\u30d0\u30a4\u30b9\u304c\u3001\"side 1\" \u304b\u3089 \"{subtype} \"\u306b\u56de\u8ee2\u3057\u305f", "remote_rotate_from_side_2": "\u30c7\u30d0\u30a4\u30b9\u304c\u3001\"side 2\" \u304b\u3089 \"{subtype} \"\u306b\u56de\u8ee2\u3057\u305f", diff --git a/homeassistant/components/enphase_envoy/translations/de.json b/homeassistant/components/enphase_envoy/translations/de.json index 6b3ed588042..84bb9a04f34 100644 --- a/homeassistant/components/enphase_envoy/translations/de.json +++ b/homeassistant/components/enphase_envoy/translations/de.json @@ -16,7 +16,8 @@ "host": "Host", "password": "Passwort", "username": "Benutzername" - } + }, + "description": "Bei neueren Modellen gib den Benutzernamen `envoy` ohne Passwort ein. F\u00fcr \u00e4ltere Modelle gib den Benutzernamen `installer` ohne Passwort ein. F\u00fcr alle anderen Modelle gib einen g\u00fcltigen Benutzernamen und ein Passwort ein." } } } diff --git a/homeassistant/components/enphase_envoy/translations/et.json b/homeassistant/components/enphase_envoy/translations/et.json index a692ed3f79e..8c814f8002c 100644 --- a/homeassistant/components/enphase_envoy/translations/et.json +++ b/homeassistant/components/enphase_envoy/translations/et.json @@ -16,7 +16,8 @@ "host": "Host", "password": "Salas\u00f5na", "username": "Kasutajanimi" - } + }, + "description": "Uuemate mudelite puhul sisesta kasutajanimi \"envoy\" ilma salas\u00f5nata. Vanemate mudelite puhul sisesta kasutajanimi \"installer\" ilma salas\u00f5nata. K\u00f5igi teiste mudelite puhul sisesta kehtiv kasutajanimi ja salas\u00f5nal." } } } diff --git a/homeassistant/components/enphase_envoy/translations/hu.json b/homeassistant/components/enphase_envoy/translations/hu.json index 38177f8930c..9da8d49341d 100644 --- a/homeassistant/components/enphase_envoy/translations/hu.json +++ b/homeassistant/components/enphase_envoy/translations/hu.json @@ -16,7 +16,8 @@ "host": "C\u00edm", "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "description": "\u00dajabb t\u00edpusok eset\u00e9n adja meg az `envoy` felhaszn\u00e1l\u00f3nevet jelsz\u00f3 n\u00e9lk\u00fcl. R\u00e9gebbi t\u00edpusok eset\u00e9n adja meg a `installer` felhaszn\u00e1l\u00f3nevet jelsz\u00f3 n\u00e9lk\u00fcl. Minden m\u00e1s t\u00edpus eset\u00e9ben adjon meg egy \u00e9rv\u00e9nyes felhaszn\u00e1l\u00f3nevet \u00e9s jelsz\u00f3t." } } } diff --git a/homeassistant/components/enphase_envoy/translations/nl.json b/homeassistant/components/enphase_envoy/translations/nl.json index 45f595dd86e..26a96d5bde2 100644 --- a/homeassistant/components/enphase_envoy/translations/nl.json +++ b/homeassistant/components/enphase_envoy/translations/nl.json @@ -16,7 +16,8 @@ "host": "Host", "password": "Wachtwoord", "username": "Gebruikersnaam" - } + }, + "description": "Voer voor nieuwere modellen gebruikersnaam 'envoy' in zonder wachtwoord. Voer voor oudere modellen gebruikersnaam `installer` in zonder wachtwoord. Voer voor alle andere modellen een geldige gebruikersnaam en wachtwoord in." } } } diff --git a/homeassistant/components/enphase_envoy/translations/ru.json b/homeassistant/components/enphase_envoy/translations/ru.json index b6e025f63e0..fd15561e4cf 100644 --- a/homeassistant/components/enphase_envoy/translations/ru.json +++ b/homeassistant/components/enphase_envoy/translations/ru.json @@ -16,7 +16,8 @@ "host": "\u0425\u043e\u0441\u0442", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" - } + }, + "description": "\u0414\u043b\u044f \u043d\u043e\u0432\u044b\u0445 \u043c\u043e\u0434\u0435\u043b\u0435\u0439 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f `envoy` \u0431\u0435\u0437 \u043f\u0430\u0440\u043e\u043b\u044f. \u0414\u043b\u044f \u0441\u0442\u0430\u0440\u044b\u0445 \u043c\u043e\u0434\u0435\u043b\u0435\u0439 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f `installer` \u0431\u0435\u0437 \u043f\u0430\u0440\u043e\u043b\u044f. \u0414\u043b\u044f \u0432\u0441\u0435\u0445 \u043e\u0441\u0442\u0430\u043b\u044c\u043d\u044b\u0445 \u043c\u043e\u0434\u0435\u043b\u0435\u0439 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0438 \u043f\u0430\u0440\u043e\u043b\u044c." } } } diff --git a/homeassistant/components/enphase_envoy/translations/zh-Hant.json b/homeassistant/components/enphase_envoy/translations/zh-Hant.json index c568b749e50..4fda54f9fa8 100644 --- a/homeassistant/components/enphase_envoy/translations/zh-Hant.json +++ b/homeassistant/components/enphase_envoy/translations/zh-Hant.json @@ -16,7 +16,8 @@ "host": "\u4e3b\u6a5f\u7aef", "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" - } + }, + "description": "\u5c0d\u65bc\u8f03\u65b0\u7684\u578b\u865f\u3001\u8f38\u5165\u4f7f\u7528\u8005\u540d\u7a31 `envoy` \u4f46\u4e0d\u9700\u8f38\u5165\u5bc6\u78bc\u3002\u8f03\u820a\u7684\u578b\u865f\uff0c\u5247\u8f38\u5165\u4f7f\u7528\u8005\u540d\u7a31 `envoy` \u4e0d\u542b\u5bc6\u78bc\u3002\u5176\u4ed6\u6240\u6709\u578b\u865f\uff0c\u8acb\u8f38\u5165\u6709\u6548\u7684\u4f7f\u7528\u8005\u540d\u7a31\u8207\u5bc6\u78bc\u3002" } } } diff --git a/homeassistant/components/hue/translations/ja.json b/homeassistant/components/hue/translations/ja.json index d1bcad85ca9..31fea90149d 100644 --- a/homeassistant/components/hue/translations/ja.json +++ b/homeassistant/components/hue/translations/ja.json @@ -54,6 +54,7 @@ "double_short_release": "\u4e21\u65b9\u306e \"{subtype}\" \u3092\u96e2\u3059", "initial_press": "\u30dc\u30bf\u30f3 \"{subtype}\" \u6700\u521d\u306b\u62bc\u3055\u308c\u305f", "long_release": "\u30dc\u30bf\u30f3 \"{subtype}\" \u96e2\u3057\u305f\u5f8c\u306b\u9577\u62bc\u3057", + "remote_button_long_release": "\u9577\u62bc\u3057\u3059\u308b\u3068 \"{subtype}\" \u30dc\u30bf\u30f3\u304c\u30ea\u30ea\u30fc\u30b9\u3055\u308c\u308b", "remote_button_short_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u304c\u62bc\u3055\u308c\u307e\u3057\u305f\u3002", "remote_button_short_release": "\"{subtype}\" \u30dc\u30bf\u30f3\u304c\u30ea\u30ea\u30fc\u30b9\u3055\u308c\u307e\u3057\u305f", "remote_double_button_long_press": "\u4e21\u65b9\u306e \"{subtype}\" \u306f\u9577\u62bc\u3057\u5f8c\u306b\u30ea\u30ea\u30fc\u30b9\u3055\u308c\u307e\u3057\u305f", diff --git a/homeassistant/components/insteon/translations/tr.json b/homeassistant/components/insteon/translations/tr.json index cd1940cbec3..3cb23de55f7 100644 --- a/homeassistant/components/insteon/translations/tr.json +++ b/homeassistant/components/insteon/translations/tr.json @@ -14,7 +14,7 @@ "host": "IP Adresi", "port": "Port" }, - "description": "Insteon Hub S\u00fcr\u00fcm 1'i (2014 \u00f6ncesi) yap\u0131land\u0131r\u0131n.", + "description": "Insteon Hub S\u00fcr\u00fcm 1 'i (2014 \u00f6ncesi) yap\u0131land\u0131r\u0131n.", "title": "Insteon Hub S\u00fcr\u00fcm 1" }, "hubv2": { @@ -53,7 +53,7 @@ "add_override": { "data": { "address": "Cihaz adresi (\u00f6rnek: 1a2b3c)", - "cat": "Cihaz alt kategorisi (yani 0x0a)", + "cat": "Cihaz alt kategorisi (\u00f6rnek: 0x10)", "subcat": "Cihaz alt kategorisi (yani 0x0a)" }, "description": "Bir cihaz\u0131 ge\u00e7ersiz k\u0131lma ekleyin.", @@ -64,7 +64,7 @@ "housecode": "Ev kodu (a - p)", "platform": "Platform", "steps": "Dimmer ad\u0131mlar\u0131 (yaln\u0131zca hafif cihazlar i\u00e7in varsay\u0131lan 22)", - "unitcode": "Birim kodu (1-16)" + "unitcode": "Birim kodu (1 - 16)" }, "description": "Insteon Hub parolas\u0131n\u0131 de\u011fi\u015ftirin.", "title": "Insteon" diff --git a/homeassistant/components/knx/translations/bg.json b/homeassistant/components/knx/translations/bg.json index 52e22a0a80e..f1c253ed5f1 100644 --- a/homeassistant/components/knx/translations/bg.json +++ b/homeassistant/components/knx/translations/bg.json @@ -11,6 +11,7 @@ "manual_tunnel": { "data": { "host": "\u0425\u043e\u0441\u0442", + "local_ip": "\u041b\u043e\u043a\u0430\u043b\u0435\u043d IP (\u043e\u0441\u0442\u0430\u0432\u0435\u0442\u0435 \u043f\u0440\u0430\u0437\u043d\u043e, \u0430\u043a\u043e \u043d\u0435 \u0441\u0442\u0435 \u0441\u0438\u0433\u0443\u0440\u043d\u0438)", "port": "\u041f\u043e\u0440\u0442" } } @@ -21,6 +22,7 @@ "tunnel": { "data": { "host": "\u0425\u043e\u0441\u0442", + "local_ip": "\u041b\u043e\u043a\u0430\u043b\u0435\u043d IP (\u043e\u0441\u0442\u0430\u0432\u0435\u0442\u0435 \u043f\u0440\u0430\u0437\u043d\u043e, \u0430\u043a\u043e \u043d\u0435 \u0441\u0442\u0435 \u0441\u0438\u0433\u0443\u0440\u043d\u0438)", "port": "\u041f\u043e\u0440\u0442" } } diff --git a/homeassistant/components/knx/translations/et.json b/homeassistant/components/knx/translations/et.json index d64e025f029..a8c65b53209 100644 --- a/homeassistant/components/knx/translations/et.json +++ b/homeassistant/components/knx/translations/et.json @@ -12,6 +12,7 @@ "data": { "host": "Host", "individual_address": "\u00dchenduse individuaalne aadress", + "local_ip": "Kohalik IP (j\u00e4ta t\u00fchjaks, kui ei ole kindel)", "port": "Port", "route_back": "Marsruudi tagasitee / NAT-re\u017eiim" }, @@ -54,6 +55,7 @@ "tunnel": { "data": { "host": "Host", + "local_ip": "Kohalik IP (j\u00e4ta t\u00fchjaks, kui ei ole kindel)", "port": "Port", "route_back": "Marsruudi tagasitee / NAT-re\u017eiim" } diff --git a/homeassistant/components/knx/translations/nl.json b/homeassistant/components/knx/translations/nl.json index 3a5e3dc5d70..004d32ba900 100644 --- a/homeassistant/components/knx/translations/nl.json +++ b/homeassistant/components/knx/translations/nl.json @@ -12,6 +12,7 @@ "data": { "host": "Host", "individual_address": "Individueel adres voor de verbinding", + "local_ip": "Lokaal IP (laat leeg indien niet zeker)", "port": "Poort", "route_back": "Route Back / NAT Mode" }, @@ -54,6 +55,7 @@ "tunnel": { "data": { "host": "Host", + "local_ip": "Lokaal IP (laat leeg indien niet zeker)", "port": "Poort", "route_back": "Route Back / NAT Mode" } diff --git a/homeassistant/components/knx/translations/tr.json b/homeassistant/components/knx/translations/tr.json index 6fe72d8259c..89165632163 100644 --- a/homeassistant/components/knx/translations/tr.json +++ b/homeassistant/components/knx/translations/tr.json @@ -12,6 +12,7 @@ "data": { "host": "Sunucu", "individual_address": "Ba\u011flant\u0131 i\u00e7in bireysel adres", + "local_ip": "Yerel IP (emin de\u011filseniz bo\u015f b\u0131rak\u0131n)", "port": "Port", "route_back": "Geri Y\u00f6nlendirme / NAT Modu" }, @@ -54,6 +55,7 @@ "tunnel": { "data": { "host": "Sunucu", + "local_ip": "Yerel IP (emin de\u011filseniz bo\u015f b\u0131rak\u0131n)", "port": "Port", "route_back": "Geri Y\u00f6nlendirme / NAT Modu" } diff --git a/homeassistant/components/mqtt/translations/ja.json b/homeassistant/components/mqtt/translations/ja.json index a7de3f583b2..0c40af6bf6d 100644 --- a/homeassistant/components/mqtt/translations/ja.json +++ b/homeassistant/components/mqtt/translations/ja.json @@ -41,9 +41,11 @@ "trigger_type": { "button_double_press": "\"{subtype}\" \u30c0\u30d6\u30eb\u30af\u30ea\u30c3\u30af", "button_long_press": "\"{subtype}\" \u304c\u3001\u7d99\u7d9a\u7684\u306b\u62bc\u3055\u308c\u305f", + "button_long_release": "\"{subtype}\" \u306f\u9577\u62bc\u3057\u5f8c\u306b\u30ea\u30ea\u30fc\u30b9\u3055\u308c\u307e\u3057\u305f", "button_quadruple_press": "\"{subtype}\" 4\u56de(quadruple)\u30af\u30ea\u30c3\u30af", "button_quintuple_press": "\"{subtype}\" 5\u56de(quintuple)\u30af\u30ea\u30c3\u30af", "button_short_press": "\"{subtype}\" \u304c\u3001\u62bc\u3055\u308c\u307e\u3057\u305f", + "button_short_release": "\"{subtype}\" \u30ea\u30ea\u30fc\u30b9", "button_triple_press": "\"{subtype}\" 3\u56de\u30af\u30ea\u30c3\u30af" } }, diff --git a/homeassistant/components/nina/translations/bg.json b/homeassistant/components/nina/translations/bg.json new file mode 100644 index 00000000000..7520f3a8624 --- /dev/null +++ b/homeassistant/components/nina/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nina/translations/et.json b/homeassistant/components/nina/translations/et.json new file mode 100644 index 00000000000..db454b7996c --- /dev/null +++ b/homeassistant/components/nina/translations/et.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Lubatud on ainult \u00fcks sidumine" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "no_selection": "Vali v\u00e4hemalt \u00fcks linn/maakond", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "_a_to_d": "Linn/maakond (A-D)", + "_e_to_h": "Linn/maakond (E-H)", + "_i_to_l": "Linn/maakond (I-L)", + "_m_to_q": "Linn/maakond (M-Q)", + "_r_to_u": "Linn/maakond (R-U)", + "_v_to_z": "Linn/maakond (V-Z)", + "corona_filter": "Koroonahoiatuste eemaldamine", + "slots": "Maksimaalne hoiatuste arv linna/maakonna kohta" + }, + "title": "Vali linn/maakond" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nina/translations/nl.json b/homeassistant/components/nina/translations/nl.json new file mode 100644 index 00000000000..3531cd8bb72 --- /dev/null +++ b/homeassistant/components/nina/translations/nl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "no_selection": "Selecteer ten minste \u00e9\u00e9n stad/deelstaat", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "_a_to_d": "Stad/deelstaat (A-D)", + "_e_to_h": "Stad/deelstaat (E-H)", + "_i_to_l": "Stad/deelstaat (I-L)", + "_m_to_q": "Stad/deelstaat (M-Q)", + "_r_to_u": "Stad/Deelstaat (R-U)", + "_v_to_z": "Stad/Deelstaat (V-Z)", + "corona_filter": "Corona-waarschuwingen verwijderen", + "slots": "Maximale waarschuwingen per stad/provincie" + }, + "title": "Selecteer stad/provincie" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nina/translations/tr.json b/homeassistant/components/nina/translations/tr.json new file mode 100644 index 00000000000..5fffe4c4f69 --- /dev/null +++ b/homeassistant/components/nina/translations/tr.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "no_selection": "L\u00fctfen en az bir \u015fehir/il\u00e7e se\u00e7in", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "_a_to_d": "\u015eehir/il\u00e7e (A-D)", + "_e_to_h": "\u015eehir/il\u00e7e (E-H)", + "_i_to_l": "\u015eehir/il\u00e7e (I-L)", + "_m_to_q": "\u015eehir/il\u00e7e (M-Q)", + "_r_to_u": "\u015eehir/il\u00e7e (R-U)", + "_v_to_z": "\u015eehir/il\u00e7e (V-Z)", + "corona_filter": "Korona Uyar\u0131lar\u0131n\u0131 Kald\u0131r", + "slots": "\u015eehir/il\u00e7e ba\u015f\u0131na maksimum uyar\u0131 say\u0131s\u0131" + }, + "title": "\u015eehir/il\u00e7e se\u00e7in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nina/translations/zh-Hant.json b/homeassistant/components/nina/translations/zh-Hant.json new file mode 100644 index 00000000000..6ab597dbef1 --- /dev/null +++ b/homeassistant/components/nina/translations/zh-Hant.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "no_selection": "\u8acb\u81f3\u5c11\u9078\u64c7\u4e00\u500b\u57ce\u5e02/\u570b\u5bb6", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "_a_to_d": "\u57ce\u5e02/\u570b\u5bb6\uff08A-D\uff09", + "_e_to_h": "\u57ce\u5e02/\u570b\u5bb6\uff08E-H\uff09", + "_i_to_l": "\u57ce\u5e02/\u570b\u5bb6\uff08I-L\uff09", + "_m_to_q": "\u57ce\u5e02/\u570b\u5bb6\uff08M-Q\uff09", + "_r_to_u": "\u57ce\u5e02/\u570b\u5bb6\uff08R-U\uff09", + "_v_to_z": "\u57ce\u5e02/\u570b\u5bb6\uff08V-Z\uff09", + "corona_filter": "\u79fb\u9664 Corona \u8b66\u544a", + "slots": "\u6bcf\u500b\u57ce\u5e02/\u570b\u5bb6\u6700\u5927\u8b66\u544a\u503c" + }, + "title": "\u9078\u64c7\u57ce\u5e02/\u570b\u5bb6" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuheat/translations/tr.json b/homeassistant/components/nuheat/translations/tr.json index ee240b2c242..381117d108e 100644 --- a/homeassistant/components/nuheat/translations/tr.json +++ b/homeassistant/components/nuheat/translations/tr.json @@ -16,7 +16,7 @@ "serial_number": "Termostat\u0131n seri numaras\u0131.", "username": "Kullan\u0131c\u0131 Ad\u0131" }, - "description": "https://MyNuHeat.com'da oturum a\u00e7\u0131p termostat(lar)\u0131n\u0131z\u0131 se\u00e7erek termostat\u0131n\u0131z\u0131n say\u0131sal seri numaras\u0131n\u0131 veya kimli\u011fini alman\u0131z gerekecektir.", + "description": "https://MyNuHeat.com 'da oturum a\u00e7\u0131p termostat(lar)\u0131n\u0131z\u0131 se\u00e7erek termostat\u0131n\u0131z\u0131n say\u0131sal seri numaras\u0131n\u0131 veya kimli\u011fini alman\u0131z gerekecektir.", "title": "NuHeat'e ba\u011flan\u0131n" } } diff --git a/homeassistant/components/point/translations/tr.json b/homeassistant/components/point/translations/tr.json index d3258722848..9bdf954f0c8 100644 --- a/homeassistant/components/point/translations/tr.json +++ b/homeassistant/components/point/translations/tr.json @@ -12,7 +12,7 @@ }, "error": { "follow_link": "L\u00fctfen ba\u011flant\u0131y\u0131 takip edin ve G\u00f6nder'e basmadan \u00f6nce kimlik do\u011frulamas\u0131 yap\u0131n", - "no_token": "Eri\u015fim Belirteci" + "no_token": "Ge\u00e7ersiz eri\u015fim anahtar\u0131" }, "step": { "auth": { diff --git a/homeassistant/components/ps4/translations/tr.json b/homeassistant/components/ps4/translations/tr.json index a526daff2a6..8243e179a98 100644 --- a/homeassistant/components/ps4/translations/tr.json +++ b/homeassistant/components/ps4/translations/tr.json @@ -21,7 +21,7 @@ "link": { "data": { "code": "PIN Kodu", - "ip_address": "Ip Adresi", + "ip_address": "IP Adresi", "name": "Ad", "region": "B\u00f6lge" }, @@ -30,7 +30,7 @@ }, "mode": { "data": { - "ip_address": "\u0130p Adresi (Otomatik Bulma kullan\u0131l\u0131yorsa bo\u015f b\u0131rak\u0131n).", + "ip_address": "IP Adresi (Otomatik Ke\u015fif kullan\u0131l\u0131yorsa bo\u015f b\u0131rak\u0131n).", "mode": "Yap\u0131land\u0131rma Modu" }, "description": "Yap\u0131land\u0131rma i\u00e7in modu se\u00e7in. IP Adresi alan\u0131, Otomatik Ke\u015fif se\u00e7ildi\u011finde cihazlar otomatik olarak ke\u015ffedilece\u011finden bo\u015f b\u0131rak\u0131labilir.", diff --git a/homeassistant/components/ridwell/translations/tr.json b/homeassistant/components/ridwell/translations/tr.json index d9ab1290edb..fe1206d334f 100644 --- a/homeassistant/components/ridwell/translations/tr.json +++ b/homeassistant/components/ridwell/translations/tr.json @@ -11,15 +11,15 @@ "step": { "reauth_confirm": { "data": { - "password": "\u015eifre" + "password": "Parola" }, "description": "L\u00fctfen {username} parolas\u0131n\u0131 yeniden girin:", "title": "Entegrasyonu Yeniden Do\u011frula" }, "user": { "data": { - "password": "\u015eifre", - "username": "Kullan\u0131c\u0131 ad\u0131" + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" }, "description": "Kullan\u0131c\u0131 ad\u0131n\u0131z\u0131 ve \u015fifrenizi girin:" } diff --git a/homeassistant/components/roku/translations/tr.json b/homeassistant/components/roku/translations/tr.json index 23b33b69efe..3e930a6c1b2 100644 --- a/homeassistant/components/roku/translations/tr.json +++ b/homeassistant/components/roku/translations/tr.json @@ -28,7 +28,7 @@ }, "user": { "data": { - "host": "Ana Bilgisayar" + "host": "Sunucu" }, "description": "Roku bilgilerinizi girin." } diff --git a/homeassistant/components/roomba/translations/tr.json b/homeassistant/components/roomba/translations/tr.json index 0904205993d..97b4b61ada2 100644 --- a/homeassistant/components/roomba/translations/tr.json +++ b/homeassistant/components/roomba/translations/tr.json @@ -19,7 +19,7 @@ "title": "Cihaza otomatik olarak ba\u011flan" }, "link": { - "description": "Cihaz bir ses olu\u015fturana kadar (yakla\u015f\u0131k iki saniye) {name} \u00fczerindeki Ana Sayfa d\u00fc\u011fmesini bas\u0131l\u0131 tutun.", + "description": "Cihaz bir ses \u00e7\u0131karana kadar (yakla\u015f\u0131k iki saniye) {name} \u00fczerindeki Ana Sayfa d\u00fc\u011fmesini bas\u0131l\u0131 tutun, ard\u0131ndan 30 saniye i\u00e7inde g\u00f6nderin.", "title": "\u015eifre Al" }, "link_manual": { diff --git a/homeassistant/components/simplisafe/translations/tr.json b/homeassistant/components/simplisafe/translations/tr.json index 6f8f07ccb9d..09b2b61d267 100644 --- a/homeassistant/components/simplisafe/translations/tr.json +++ b/homeassistant/components/simplisafe/translations/tr.json @@ -34,7 +34,7 @@ "data": { "code": "Kod (Home Assistant kullan\u0131c\u0131 aray\u00fcz\u00fcnde kullan\u0131l\u0131r)", "password": "Parola", - "username": "E-posta adresi" + "username": "E-posta" }, "description": "2021'den itibaren SimpliSafe, web uygulamas\u0131 arac\u0131l\u0131\u011f\u0131yla yeni bir kimlik do\u011frulama mekanizmas\u0131na ge\u00e7ti. Teknik s\u0131n\u0131rlamalar nedeniyle bu i\u015flemin sonunda manuel bir ad\u0131m vard\u0131r; l\u00fctfen ba\u015flamadan \u00f6nce [belgeleri](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) okudu\u011funuzdan emin olun. \n\n Haz\u0131r oldu\u011funuzda SimpliSafe web uygulamas\u0131n\u0131 a\u00e7mak ve kimlik bilgilerinizi girmek i\u00e7in [buray\u0131]( {url} \u0130\u015flem tamamland\u0131\u011f\u0131nda buraya d\u00f6n\u00fcn ve G\u00f6nder'e t\u0131klay\u0131n.", "title": "Bilgilerinizi doldurun." diff --git a/homeassistant/components/solarlog/translations/tr.json b/homeassistant/components/solarlog/translations/tr.json index 7f69014887b..9077d146d5f 100644 --- a/homeassistant/components/solarlog/translations/tr.json +++ b/homeassistant/components/solarlog/translations/tr.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "host": "Ana Bilgisayar", + "host": "Sunucu", "name": "Solar-Log sens\u00f6rleriniz i\u00e7in kullan\u0131lacak \u00f6nek" }, "title": "Solar-Log ba\u011flant\u0131n\u0131z\u0131 tan\u0131mlay\u0131n" diff --git a/homeassistant/components/synology_dsm/translations/tr.json b/homeassistant/components/synology_dsm/translations/tr.json index 2941e260c93..0b7bbfda1b7 100644 --- a/homeassistant/components/synology_dsm/translations/tr.json +++ b/homeassistant/components/synology_dsm/translations/tr.json @@ -41,8 +41,8 @@ }, "reauth_confirm": { "data": { - "password": "\u015eifre", - "username": "Kullan\u0131c\u0131 ad\u0131" + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" }, "title": "Synology DSM Entegrasyonu Yeniden Do\u011frula" }, diff --git a/homeassistant/components/tellduslive/translations/tr.json b/homeassistant/components/tellduslive/translations/tr.json index 7b462a4b61d..fb260840f5c 100644 --- a/homeassistant/components/tellduslive/translations/tr.json +++ b/homeassistant/components/tellduslive/translations/tr.json @@ -16,7 +16,7 @@ }, "user": { "data": { - "host": "Ana Bilgisayar" + "host": "Sunucu" }, "description": "Bo\u015f", "title": "Biti\u015f noktas\u0131n\u0131 se\u00e7in." diff --git a/homeassistant/components/transmission/translations/tr.json b/homeassistant/components/transmission/translations/tr.json index 94ba7572e03..72b3410062e 100644 --- a/homeassistant/components/transmission/translations/tr.json +++ b/homeassistant/components/transmission/translations/tr.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "Ana Bilgisayar", + "host": "Sunucu", "name": "Ad", "password": "Parola", "port": "Port", diff --git a/homeassistant/components/unifi/translations/tr.json b/homeassistant/components/unifi/translations/tr.json index 6baa8108098..9b53e712fa5 100644 --- a/homeassistant/components/unifi/translations/tr.json +++ b/homeassistant/components/unifi/translations/tr.json @@ -14,7 +14,7 @@ "step": { "user": { "data": { - "host": "Ana Bilgisayar", + "host": "Sunucu", "password": "Parola", "port": "Port", "site": "Site Kimli\u011fi", diff --git a/homeassistant/components/watttime/translations/tr.json b/homeassistant/components/watttime/translations/tr.json index a9531c6deaf..c0b5b1dfdb9 100644 --- a/homeassistant/components/watttime/translations/tr.json +++ b/homeassistant/components/watttime/translations/tr.json @@ -32,8 +32,8 @@ }, "user": { "data": { - "password": "\u015eifre", - "username": "Kullan\u0131c\u0131 ad\u0131" + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" }, "description": "Kullan\u0131c\u0131 ad\u0131n\u0131z\u0131 ve \u015fifrenizi girin:" } diff --git a/homeassistant/components/yale_smart_alarm/translations/cs.json b/homeassistant/components/yale_smart_alarm/translations/cs.json index 70947657e4d..5b310205d10 100644 --- a/homeassistant/components/yale_smart_alarm/translations/cs.json +++ b/homeassistant/components/yale_smart_alarm/translations/cs.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u00da\u010det je ji\u017e nastaven" + "already_configured": "\u00da\u010det je ji\u017e nastaven", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" diff --git a/homeassistant/components/yale_smart_alarm/translations/tr.json b/homeassistant/components/yale_smart_alarm/translations/tr.json index 24b37440160..376de5dc2ff 100644 --- a/homeassistant/components/yale_smart_alarm/translations/tr.json +++ b/homeassistant/components/yale_smart_alarm/translations/tr.json @@ -12,15 +12,15 @@ "data": { "area_id": "Alan Kodu", "name": "Ad", - "password": "\u015eifre", + "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" } }, "user": { "data": { "area_id": "Alan Kodu", - "name": "\u0130sim", - "password": "\u015eifre", + "name": "Ad", + "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" } } diff --git a/homeassistant/components/yamaha_musiccast/translations/select.de.json b/homeassistant/components/yamaha_musiccast/translations/select.de.json new file mode 100644 index 00000000000..e21b228c2a3 --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/select.de.json @@ -0,0 +1,52 @@ +{ + "state": { + "yamaha_musiccast__dimmer": { + "auto": "Automatisch" + }, + "yamaha_musiccast__zone_equalizer_mode": { + "auto": "Automatisch", + "bypass": "Bypass", + "manual": "Manuell" + }, + "yamaha_musiccast__zone_link_audio_delay": { + "audio_sync": "Audio-Synchronisation", + "audio_sync_off": "Audio-Synchronisation Aus", + "audio_sync_on": "Audio-Synchronisation Ein", + "balanced": "Ausgeglichen", + "lip_sync": "Lippensynchronisation" + }, + "yamaha_musiccast__zone_link_audio_quality": { + "compressed": "Komprimiert", + "uncompressed": "Unkomprimiert" + }, + "yamaha_musiccast__zone_link_control": { + "speed": "Geschwindigkeit", + "stability": "Stabilit\u00e4t", + "standard": "Standard" + }, + "yamaha_musiccast__zone_sleep": { + "120 min": "120 Minuten", + "30 min": "30 Minuten", + "60 min": "60 Minuten", + "90 min": "90 Minuten", + "off": "Aus" + }, + "yamaha_musiccast__zone_surr_decoder_type": { + "auto": "Automatisch", + "dolby_pl": "Dolby ProLogic", + "dolby_pl2x_game": "Dolby ProLogic 2x Game", + "dolby_pl2x_movie": "Dolby ProLogic 2x Movie", + "dolby_pl2x_music": "Dolby ProLogic 2x Music", + "dolby_surround": "Dolby Surround", + "dts_neo6_cinema": "DTS Neo:6 Cinema", + "dts_neo6_music": "DTS Neo:6 Music", + "dts_neural_x": "DTS Neural:X", + "toggle": "Umschalten" + }, + "yamaha_musiccast__zone_tone_control_mode": { + "auto": "Automatisch", + "bypass": "Bypass", + "manual": "Manuell" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/select.et.json b/homeassistant/components/yamaha_musiccast/translations/select.et.json new file mode 100644 index 00000000000..79430ce0c3e --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/select.et.json @@ -0,0 +1,52 @@ +{ + "state": { + "yamaha_musiccast__dimmer": { + "auto": "Automaatne" + }, + "yamaha_musiccast__zone_equalizer_mode": { + "auto": "Automaatne", + "bypass": "M\u00f6\u00f6daviik", + "manual": "K\u00e4sitsi" + }, + "yamaha_musiccast__zone_link_audio_delay": { + "audio_sync": "Heli s\u00fcnkroonimine", + "audio_sync_off": "Heli s\u00fcnkroonimine v\u00e4ljas", + "audio_sync_on": "Heli s\u00fcnkroonimine sees", + "balanced": "Tasakaalustatud", + "lip_sync": "Huulte s\u00fcnkroonimine" + }, + "yamaha_musiccast__zone_link_audio_quality": { + "compressed": "Tihendatud", + "uncompressed": "Tihendamata" + }, + "yamaha_musiccast__zone_link_control": { + "speed": "Kiirus", + "stability": "Stabiilne", + "standard": "Standartne" + }, + "yamaha_musiccast__zone_sleep": { + "120 min": "120 minutit", + "30 min": "30 minutit", + "60 min": "60 minutit", + "90 min": "90 minutit", + "off": "V\u00e4ljas" + }, + "yamaha_musiccast__zone_surr_decoder_type": { + "auto": "Automaatne", + "dolby_pl": "Dolby ProLogic", + "dolby_pl2x_game": "Dolby ProLogic 2x Game", + "dolby_pl2x_movie": "Dolby ProLogic 2x Movie", + "dolby_pl2x_music": "Dolby ProLogic 2x Music", + "dolby_surround": "Dolby Surround", + "dts_neo6_cinema": "DTS Neo:6 Cinema", + "dts_neo6_music": "DTS Neo:6 Music", + "dts_neural_x": "DTS Neural:X", + "toggle": "Muuda olekut" + }, + "yamaha_musiccast__zone_tone_control_mode": { + "auto": "Automaatne", + "bypass": "M\u00f6\u00f6daviik", + "manual": "K\u00e4sitsi" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/select.hu.json b/homeassistant/components/yamaha_musiccast/translations/select.hu.json new file mode 100644 index 00000000000..82186ebb4e5 --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/select.hu.json @@ -0,0 +1,52 @@ +{ + "state": { + "yamaha_musiccast__dimmer": { + "auto": "Automatikus" + }, + "yamaha_musiccast__zone_equalizer_mode": { + "auto": "Automatikus", + "bypass": "Kihagy\u00e1s", + "manual": "Manu\u00e1lis" + }, + "yamaha_musiccast__zone_link_audio_delay": { + "audio_sync": "Audi\u00f3 szinkroniz\u00e1l\u00e1s", + "audio_sync_off": "Audi\u00f3 szinkroniz\u00e1l\u00e1s kikapcsolva", + "audio_sync_on": "Audi\u00f3 szinkroniz\u00e1l\u00e1s bekapcsolva", + "balanced": "Kiegyens\u00falyozott", + "lip_sync": "K\u00e9p-hang k\u00e9sleltet\u00e9s" + }, + "yamaha_musiccast__zone_link_audio_quality": { + "compressed": "T\u00f6m\u00f6r\u00edtve", + "uncompressed": "T\u00f6m\u00f6r\u00edtetlen" + }, + "yamaha_musiccast__zone_link_control": { + "speed": "Sebess\u00e9g", + "stability": "Stabilit\u00e1s", + "standard": "Standard" + }, + "yamaha_musiccast__zone_sleep": { + "120 min": "120 perc", + "30 min": "30 perc", + "60 min": "60 perc", + "90 min": "90 perc", + "off": "Ki" + }, + "yamaha_musiccast__zone_surr_decoder_type": { + "auto": "Automatikus", + "dolby_pl": "Dolby ProLogic", + "dolby_pl2x_game": "Dolby ProLogic 2x J\u00e1t\u00e9k", + "dolby_pl2x_movie": "Dolby ProLogic 2x Film", + "dolby_pl2x_music": "Dolby ProLogic 2x Zene", + "dolby_surround": "Dolby Surround", + "dts_neo6_cinema": "DTS Neo:6 Mozi", + "dts_neo6_music": "DTS Neo:6 Zene", + "dts_neural_x": "DTS Neural:X", + "toggle": "V\u00e1lt\u00e1s" + }, + "yamaha_musiccast__zone_tone_control_mode": { + "auto": "Automatikus", + "bypass": "Kihagy\u00e1s", + "manual": "Manu\u00e1lis" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/ja.json b/homeassistant/components/zha/translations/ja.json index 7095ef8813d..94257d5cb3d 100644 --- a/homeassistant/components/zha/translations/ja.json +++ b/homeassistant/components/zha/translations/ja.json @@ -89,15 +89,19 @@ "device_tilted": "\u30c7\u30d0\u30a4\u30b9\u304c\u50be\u3044\u3066\u3044\u308b", "remote_button_alt_double_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u3092\u30c0\u30d6\u30eb\u30af\u30ea\u30c3\u30af(\u4ee3\u66ff\u30e2\u30fc\u30c9)", "remote_button_alt_long_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u3092\u62bc\u3057\u7d9a\u3051\u308b(\u4ee3\u66ff\u30e2\u30fc\u30c9)", + "remote_button_alt_long_release": "\u9577\u62bc\u3057\u3059\u308b\u3068 \"{subtype}\" \u30dc\u30bf\u30f3\u304c\u30ea\u30ea\u30fc\u30b9\u3055\u308c\u308b(\u4ee3\u66ff\u30e2\u30fc\u30c9)", "remote_button_alt_quadruple_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u30924\u56de(quadruple)\u30af\u30ea\u30c3\u30af(\u4ee3\u66ff\u30e2\u30fc\u30c9)", "remote_button_alt_quintuple_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u30925\u56de(quintuple)\u30af\u30ea\u30c3\u30af(\u4ee3\u66ff\u30e2\u30fc\u30c9)", "remote_button_alt_short_press": "\"{subtype}\" \u62bc\u3057\u7d9a\u3051\u308b(\u4ee3\u66ff\u30e2\u30fc\u30c9)", + "remote_button_alt_short_release": "\"{subtype}\" \u30dc\u30bf\u30f3\u304c\u30ea\u30ea\u30fc\u30b9\u3055\u308c\u307e\u3057\u305f(\u4ee3\u66ff\u30e2\u30fc\u30c9)", "remote_button_alt_triple_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u30923\u56de\u30af\u30ea\u30c3\u30af(\u4ee3\u66ff\u30e2\u30fc\u30c9)", "remote_button_double_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u3092\u30c0\u30d6\u30eb\u30af\u30ea\u30c3\u30af", "remote_button_long_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u3092\u62bc\u3057\u7d9a\u3051\u308b", + "remote_button_long_release": "\u9577\u62bc\u3057\u3059\u308b\u3068 \"{subtype}\" \u30dc\u30bf\u30f3\u304c\u30ea\u30ea\u30fc\u30b9\u3055\u308c\u308b", "remote_button_quadruple_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u30924\u56de(quadruple)\u30af\u30ea\u30c3\u30af", "remote_button_quintuple_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u30925\u56de(quintuple)\u30af\u30ea\u30c3\u30af", "remote_button_short_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u304c\u62bc\u3055\u308c\u307e\u3057\u305f\u3002", + "remote_button_short_release": "\"{subtype}\" \u30dc\u30bf\u30f3\u304c\u30ea\u30ea\u30fc\u30b9\u3055\u308c\u307e\u3057\u305f", "remote_button_triple_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u30923\u56de\u30af\u30ea\u30c3\u30af" } } diff --git a/homeassistant/components/zwave_js/translations/de.json b/homeassistant/components/zwave_js/translations/de.json index 8bdf7a78237..4900c9055d9 100644 --- a/homeassistant/components/zwave_js/translations/de.json +++ b/homeassistant/components/zwave_js/translations/de.json @@ -33,6 +33,7 @@ "s2_unauthenticated_key": "S2 Nicht authentifizierter Schl\u00fcssel", "usb_path": "USB-Ger\u00e4te-Pfad" }, + "description": "Das Add-on generiert Sicherheitsschl\u00fcssel, wenn diese Felder leer gelassen werden.", "title": "Gib die Konfiguration des Z-Wave JS Add-ons ein" }, "hassio_confirm": { @@ -119,6 +120,7 @@ "s2_unauthenticated_key": "S2 Nicht authentifizierter Schl\u00fcssel", "usb_path": "USB-Ger\u00e4te-Pfad" }, + "description": "Das Add-on generiert Sicherheitsschl\u00fcssel, wenn diese Felder leer gelassen werden.", "title": "Gib die Konfiguration des Z-Wave JS-Add-ons ein" }, "install_addon": { diff --git a/homeassistant/components/zwave_js/translations/en.json b/homeassistant/components/zwave_js/translations/en.json index 46650ca5439..3962674ff9c 100644 --- a/homeassistant/components/zwave_js/translations/en.json +++ b/homeassistant/components/zwave_js/translations/en.json @@ -33,6 +33,7 @@ "s2_unauthenticated_key": "S2 Unauthenticated Key", "usb_path": "USB Device Path" }, + "description": "The add-on will generate security keys if those fields are left empty.", "title": "Enter the Z-Wave JS add-on configuration" }, "hassio_confirm": { @@ -119,6 +120,7 @@ "s2_unauthenticated_key": "S2 Unauthenticated Key", "usb_path": "USB Device Path" }, + "description": "The add-on will generate security keys if those fields are left empty.", "title": "Enter the Z-Wave JS add-on configuration" }, "install_addon": { diff --git a/homeassistant/components/zwave_js/translations/et.json b/homeassistant/components/zwave_js/translations/et.json index 10a813aad85..c6c5db1ebd7 100644 --- a/homeassistant/components/zwave_js/translations/et.json +++ b/homeassistant/components/zwave_js/translations/et.json @@ -33,6 +33,7 @@ "s2_unauthenticated_key": "Autentimata S2 v\u00f5ti", "usb_path": "USB-seadme asukoha rada" }, + "description": "Lisandmoodul loob ise turvav\u00f5tmed kui need v\u00e4ljad t\u00fchjaks j\u00e4etakse.", "title": "Sisesta Z-Wave JS lisandmooduli seaded" }, "hassio_confirm": { @@ -119,6 +120,7 @@ "s2_unauthenticated_key": "Autentimata S2 v\u00f5ti", "usb_path": "USB-seadme asukoha rada" }, + "description": "Lisandmoodul loob ise turvav\u00f5tmed kui need v\u00e4ljad t\u00fchjaks j\u00e4etakse.", "title": "Sisesta Z-Wave JS lisandmooduli seaded" }, "install_addon": { diff --git a/homeassistant/components/zwave_js/translations/hu.json b/homeassistant/components/zwave_js/translations/hu.json index 62afae94725..7e4ba47bff7 100644 --- a/homeassistant/components/zwave_js/translations/hu.json +++ b/homeassistant/components/zwave_js/translations/hu.json @@ -33,6 +33,7 @@ "s2_unauthenticated_key": "S2 nem hiteles\u00edtett kulcs", "usb_path": "USB eszk\u00f6z el\u00e9r\u00e9si \u00fat" }, + "description": "A b\u0151v\u00edtm\u00e9ny gener\u00e1lni fogja a biztons\u00e1gi kulcsokat, ha ezek a mez\u0151k \u00fcresen maradnak.", "title": "Adja meg a Z-Wave JS kieg\u00e9sz\u00edt\u0151 konfigur\u00e1ci\u00f3j\u00e1t" }, "hassio_confirm": { @@ -119,6 +120,7 @@ "s2_unauthenticated_key": "S2 nem hiteles\u00edtett kulcs", "usb_path": "USB eszk\u00f6z \u00fatvonala" }, + "description": "A b\u0151v\u00edtm\u00e9ny gener\u00e1lni fogja a biztons\u00e1gi kulcsokat, ha ezek a mez\u0151k \u00fcresen maradnak.", "title": "Adja meg a Z-Wave JS kieg\u00e9sz\u00edt\u0151 konfigur\u00e1ci\u00f3j\u00e1t" }, "install_addon": { diff --git a/homeassistant/components/zwave_js/translations/ru.json b/homeassistant/components/zwave_js/translations/ru.json index 9ae79edb32d..bc1d3e2cbd4 100644 --- a/homeassistant/components/zwave_js/translations/ru.json +++ b/homeassistant/components/zwave_js/translations/ru.json @@ -33,6 +33,7 @@ "s2_unauthenticated_key": "\u041a\u043b\u044e\u0447 \u0431\u0435\u0437 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 S2", "usb_path": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" }, + "description": "\u0415\u0441\u043b\u0438 \u044d\u0442\u0438 \u043f\u043e\u043b\u044f \u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u043f\u0443\u0441\u0442\u044b\u043c\u0438, \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0441\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0443\u0435\u0442 \u043a\u043b\u044e\u0447\u0438 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u0438.", "title": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f Z-Wave JS" }, "hassio_confirm": { @@ -119,6 +120,7 @@ "s2_unauthenticated_key": "\u041a\u043b\u044e\u0447 \u0431\u0435\u0437 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 S2", "usb_path": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" }, + "description": "\u0415\u0441\u043b\u0438 \u044d\u0442\u0438 \u043f\u043e\u043b\u044f \u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u043f\u0443\u0441\u0442\u044b\u043c\u0438, \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0441\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0443\u0435\u0442 \u043a\u043b\u044e\u0447\u0438 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u0438.", "title": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f Z-Wave JS" }, "install_addon": { From bbaec2c48123564576c7a2b011cfcb0ad1aa61e5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 6 Dec 2021 18:02:40 -0800 Subject: [PATCH 0096/2644] Bump cache version CI (#61137) --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ebbd2b2fb9e..f36324a7b9d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -10,7 +10,7 @@ on: pull_request: ~ env: - CACHE_VERSION: 3 + CACHE_VERSION: 4 DEFAULT_PYTHON: 3.8 PRE_COMMIT_CACHE: ~/.cache/pre-commit SQLALCHEMY_WARN_20: 1 From e3ba533464000fbf65b7e0ac124d59b16079c5a3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 6 Dec 2021 21:24:18 -0800 Subject: [PATCH 0097/2644] Disable lupusec (#61142) --- homeassistant/components/lupusec/__init__.py | 1 + homeassistant/components/lupusec/binary_sensor.py | 1 + homeassistant/components/lupusec/manifest.json | 1 + homeassistant/components/lupusec/switch.py | 1 + requirements_all.txt | 3 --- 5 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lupusec/__init__.py b/homeassistant/components/lupusec/__init__.py index 3ae07bd8105..734c7affe90 100644 --- a/homeassistant/components/lupusec/__init__.py +++ b/homeassistant/components/lupusec/__init__.py @@ -1,4 +1,5 @@ """Support for Lupusec Home Security system.""" +# pylint: disable=import-error import logging import lupupy diff --git a/homeassistant/components/lupusec/binary_sensor.py b/homeassistant/components/lupusec/binary_sensor.py index 963c82da5fa..9668b06b0ef 100644 --- a/homeassistant/components/lupusec/binary_sensor.py +++ b/homeassistant/components/lupusec/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Lupusec Security System binary sensors.""" +# pylint: disable=import-error from datetime import timedelta import lupupy.constants as CONST diff --git a/homeassistant/components/lupusec/manifest.json b/homeassistant/components/lupusec/manifest.json index 6541925a5e4..ce200fe196a 100644 --- a/homeassistant/components/lupusec/manifest.json +++ b/homeassistant/components/lupusec/manifest.json @@ -1,4 +1,5 @@ { + "disabled": "Library has incompatible requirements.", "domain": "lupusec", "name": "Lupus Electronics LUPUSEC", "documentation": "https://www.home-assistant.io/integrations/lupusec", diff --git a/homeassistant/components/lupusec/switch.py b/homeassistant/components/lupusec/switch.py index f35322eb773..5321d1b4f25 100644 --- a/homeassistant/components/lupusec/switch.py +++ b/homeassistant/components/lupusec/switch.py @@ -1,4 +1,5 @@ """Support for Lupusec Security System switches.""" +# pylint: disable=import-error from datetime import timedelta import lupupy.constants as CONST diff --git a/requirements_all.txt b/requirements_all.txt index 39b34cbcc7c..27be10465e7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -971,9 +971,6 @@ london-tube-status==0.2 # homeassistant.components.luftdaten luftdaten==0.7.1 -# homeassistant.components.lupusec -lupupy==0.0.21 - # homeassistant.components.lw12wifi lw12==0.9.2 From 009a28ba7a357c2c7e7afea18b11d7ca495ddc0c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 6 Dec 2021 22:07:56 -0800 Subject: [PATCH 0098/2644] Block pytest_asyncio (#61141) --- homeassistant/package_constraints.txt | 3 +++ script/gen_requirements_all.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5082fe5559d..cf1a20e1925 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -84,3 +84,6 @@ anyio>=3.3.1 # websockets 10.0 is broken with AWS # https://github.com/aaugustin/websockets/issues/1065 websockets==9.1 + +# pytest_asyncio breaks our test suite. We rely on pytest-aiohttp instead +pytest_asyncio==1000000000.0.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 3deec512b4f..dd8bc989874 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -110,6 +110,9 @@ anyio>=3.3.1 # websockets 10.0 is broken with AWS # https://github.com/aaugustin/websockets/issues/1065 websockets==9.1 + +# pytest_asyncio breaks our test suite. We rely on pytest-aiohttp instead +pytest_asyncio==1000000000.0.0 """ IGNORE_PRE_COMMIT_HOOK_ID = ( From 5161126b5844ed988551afda4c37df58cf23d1f3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 7 Dec 2021 08:07:31 +0100 Subject: [PATCH 0099/2644] Bump hatasmota to 0.3.1 (#61120) --- .../components/tasmota/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tasmota/test_cover.py | 30 +++++++++++++++++++ 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index 9ea06bea545..bd30231396f 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -3,7 +3,7 @@ "name": "Tasmota", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tasmota", - "requirements": ["hatasmota==0.3.0"], + "requirements": ["hatasmota==0.3.1"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"], diff --git a/requirements_all.txt b/requirements_all.txt index 27be10465e7..dcf44749cdb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -795,7 +795,7 @@ hass-nabucasa==0.50.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.3.0 +hatasmota==0.3.1 # homeassistant.components.jewish_calendar hdate==0.10.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a62a75382a1..6677487dd3b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -500,7 +500,7 @@ hangups==0.4.14 hass-nabucasa==0.50.0 # homeassistant.components.tasmota -hatasmota==0.3.0 +hatasmota==0.3.1 # homeassistant.components.jewish_calendar hdate==0.10.4 diff --git a/tests/components/tasmota/test_cover.py b/tests/components/tasmota/test_cover.py index 54ac192f7c1..c036f490f6d 100644 --- a/tests/components/tasmota/test_cover.py +++ b/tests/components/tasmota/test_cover.py @@ -9,6 +9,7 @@ from hatasmota.utils import ( get_topic_tele_sensor, get_topic_tele_will, ) +import pytest from homeassistant.components import cover from homeassistant.components.tasmota.const import DEFAULT_PREFIX @@ -34,6 +35,35 @@ async def test_missing_relay(hass, mqtt_mock, setup_tasmota): """Test no cover is discovered if relays are missing.""" +@pytest.mark.parametrize( + "relay_config, num_covers", + [ + ([3, 3, 3, 3, 3, 3, 1, 1, 3, 3], 4), + ([3, 3, 3, 3, 0, 0, 0, 0], 2), + ([3, 3, 1, 1, 0, 0, 0, 0], 1), + ([3, 3, 3, 1, 0, 0, 0, 0], 0), + ], +) +async def test_multiple_covers( + hass, mqtt_mock, setup_tasmota, relay_config, num_covers +): + """Test discovery of multiple covers.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["rl"] = relay_config + mac = config["mac"] + + assert len(hass.states.async_all("cover")) == 0 + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all("cover")) == num_covers + + async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota): """Test state update via MQTT.""" config = copy.deepcopy(DEFAULT_CONFIG) From a87a6d307248b3a81bf0639a56c72a43b218b51f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Tue, 7 Dec 2021 08:36:13 +0100 Subject: [PATCH 0100/2644] Add volume and tv metadata to Apple TV (#61107) --- .../components/apple_tv/media_player.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py index 7a18fbc3fc7..bc4697e0d5e 100644 --- a/homeassistant/components/apple_tv/media_player.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -31,6 +31,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, + SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, ) from homeassistant.const import ( @@ -66,6 +67,7 @@ SUPPORT_APPLE_TV = ( | SUPPORT_STOP | SUPPORT_NEXT_TRACK | SUPPORT_PREVIOUS_TRACK + | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_STEP | SUPPORT_REPEAT_SET | SUPPORT_SHUFFLE_SET @@ -86,6 +88,7 @@ SUPPORT_FEATURE_MAPPING = { FeatureName.VolumeDown: SUPPORT_VOLUME_STEP, FeatureName.SetRepeat: SUPPORT_REPEAT_SET, FeatureName.SetShuffle: SUPPORT_SHUFFLE_SET, + FeatureName.SetVolume: SUPPORT_VOLUME_SET, } @@ -206,6 +209,20 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): }.get(self._playing.media_type) return None + @property + def media_content_id(self): + """Content ID of current playing media.""" + if self._playing: + return self._playing.content_identifier + return None + + @property + def volume_level(self): + """Volume level of the media player (0..1).""" + if self._is_feature_available(FeatureName.Volume): + return self.atv.audio.volume / 100.0 # from percent + return None + @property def media_duration(self): """Duration of current playing media in seconds.""" @@ -285,6 +302,27 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): return self._playing.album return None + @property + def media_series_title(self): + """Title of series of current playing media, TV show only.""" + if self._is_feature_available(FeatureName.SeriesName): + return self._playing.series_name + return None + + @property + def media_season(self): + """Season of current playing media, TV show only.""" + if self._is_feature_available(FeatureName.SeasonNumber): + return str(self._playing.season_number) + return None + + @property + def media_episode(self): + """Episode of current playing media, TV show only.""" + if self._is_feature_available(FeatureName.EpisodeNumber): + return str(self._playing.episode_number) + return None + @property def repeat(self): """Return current repeat mode.""" @@ -366,6 +404,12 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): if self.atv: await self.atv.audio.volume_down() + async def async_set_volume_level(self, volume): + """Set volume level, range 0..1.""" + if self.atv: + # pyatv expects volume in percent + await self.atv.audio.set_volume(volume * 100.0) + async def async_set_repeat(self, repeat): """Set repeat mode.""" if self.atv: From 8ee2ca8a56cbab0381946300b928db02a9c2ced2 Mon Sep 17 00:00:00 2001 From: Zac West <74188+zacwest@users.noreply.github.com> Date: Mon, 6 Dec 2021 23:56:08 -0800 Subject: [PATCH 0101/2644] Deprecate ios push config (in favor of inline actions) (#61078) --- homeassistant/components/ios/__init__.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ios/__init__.py b/homeassistant/components/ios/__init__.py index 0f1cadb55ba..442cfe5b673 100644 --- a/homeassistant/components/ios/__init__.py +++ b/homeassistant/components/ios/__init__.py @@ -150,10 +150,13 @@ ACTION_LIST_SCHEMA = vol.All(cv.ensure_list, [ACTION_SCHEMA]) CONFIG_SCHEMA = vol.Schema( { - DOMAIN: { - CONF_PUSH: {CONF_PUSH_CATEGORIES: PUSH_CATEGORY_LIST_SCHEMA}, - CONF_ACTIONS: ACTION_LIST_SCHEMA, - } + DOMAIN: vol.All( + cv.deprecated(CONF_PUSH), + { + CONF_PUSH: {CONF_PUSH_CATEGORIES: PUSH_CATEGORY_LIST_SCHEMA}, + CONF_ACTIONS: ACTION_LIST_SCHEMA, + }, + ) }, extra=vol.ALLOW_EXTRA, ) From c469bf2db98a46868ce9336a08ec2a6b6097bfb5 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Tue, 7 Dec 2021 09:00:30 +0100 Subject: [PATCH 0102/2644] Fix point availability (#61144) --- homeassistant/components/point/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/point/__init__.py b/homeassistant/components/point/__init__.py index dee0ae1a492..5cbab59eabf 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -185,7 +185,7 @@ class MinutPointClient: async def _sync(self): """Update local list of devices.""" - if not await self._client.update() and self._is_available: + if not await self._client.update(): self._is_available = False _LOGGER.warning("Device is unavailable") async_dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY) From 4563d95d40af431f7a0d491fa1175a85af37f309 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 7 Dec 2021 11:41:43 +0100 Subject: [PATCH 0103/2644] Replace deprecated DEVICE_CLASS constants with new enums in Hue integration (#61149) --- homeassistant/components/hue/migration.py | 20 +++++++------------ homeassistant/components/hue/switch.py | 4 ++-- .../components/hue/v1/binary_sensor.py | 4 ++-- homeassistant/components/hue/v1/sensor.py | 15 +++++++------- .../components/hue/v2/binary_sensor.py | 7 +++---- homeassistant/components/hue/v2/sensor.py | 19 +++++++++--------- 6 files changed, 32 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/hue/migration.py b/homeassistant/components/hue/migration.py index 9891cc65b0c..3dbfef42d16 100644 --- a/homeassistant/components/hue/migration.py +++ b/homeassistant/components/hue/migration.py @@ -8,16 +8,10 @@ from aiohue.v2.models.device import DeviceArchetypes from aiohue.v2.models.resource import ResourceTypes from homeassistant import core -from homeassistant.components.binary_sensor import DEVICE_CLASS_MOTION +from homeassistant.components.binary_sensor import BinarySensorDeviceClass +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_API_KEY, - CONF_HOST, - CONF_USERNAME, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_TEMPERATURE, -) +from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_USERNAME from homeassistant.helpers import aiohttp_client from homeassistant.helpers.device_registry import ( async_entries_for_config_entry as devices_for_config_entries, @@ -102,10 +96,10 @@ async def handle_v2_migration(hass: core.HomeAssistant, entry: ConfigEntry) -> N async with HueBridgeV2(host, api_key, websession) as api: sensor_class_mapping = { - DEVICE_CLASS_BATTERY: ResourceTypes.DEVICE_POWER, - DEVICE_CLASS_MOTION: ResourceTypes.MOTION, - DEVICE_CLASS_ILLUMINANCE: ResourceTypes.LIGHT_LEVEL, - DEVICE_CLASS_TEMPERATURE: ResourceTypes.TEMPERATURE, + SensorDeviceClass.BATTERY.value: ResourceTypes.DEVICE_POWER, + BinarySensorDeviceClass.MOTION.value: ResourceTypes.MOTION, + SensorDeviceClass.ILLUMINANCE.value: ResourceTypes.LIGHT_LEVEL, + SensorDeviceClass.TEMPERATURE.value: ResourceTypes.TEMPERATURE, } # migrate entities attached to a device diff --git a/homeassistant/components/hue/switch.py b/homeassistant/components/hue/switch.py index 3de96b45842..d8d0d0fd05c 100644 --- a/homeassistant/components/hue/switch.py +++ b/homeassistant/components/hue/switch.py @@ -8,7 +8,7 @@ from aiohue.v2.controllers.events import EventType from aiohue.v2.controllers.sensors import LightLevelController, MotionController from aiohue.v2.models.resource import SensingService -from homeassistant.components.switch import DEVICE_CLASS_SWITCH, SwitchEntity +from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant, callback @@ -63,7 +63,7 @@ class HueSensingServiceEnabledEntity(HueBaseEntity, SwitchEntity): """Representation of a Switch entity from Hue SensingService.""" _attr_entity_category = ENTITY_CATEGORY_CONFIG - _attr_device_class = DEVICE_CLASS_SWITCH + _attr_device_class = SwitchDeviceClass.SWITCH def __init__( self, diff --git a/homeassistant/components/hue/v1/binary_sensor.py b/homeassistant/components/hue/v1/binary_sensor.py index 21650e52b9c..78cedc4437f 100644 --- a/homeassistant/components/hue/v1/binary_sensor.py +++ b/homeassistant/components/hue/v1/binary_sensor.py @@ -2,7 +2,7 @@ from aiohue.v1.sensors import TYPE_ZLL_PRESENCE from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_MOTION, + BinarySensorDeviceClass, BinarySensorEntity, ) @@ -27,7 +27,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class HuePresence(GenericZLLSensor, BinarySensorEntity): """The presence sensor entity for a Hue motion sensor device.""" - _attr_device_class = DEVICE_CLASS_MOTION + _attr_device_class = BinarySensorDeviceClass.MOTION @property def is_on(self): diff --git a/homeassistant/components/hue/v1/sensor.py b/homeassistant/components/hue/v1/sensor.py index df12fe84274..fc490ab1e4b 100644 --- a/homeassistant/components/hue/v1/sensor.py +++ b/homeassistant/components/hue/v1/sensor.py @@ -6,11 +6,12 @@ from aiohue.v1.sensors import ( TYPE_ZLL_TEMPERATURE, ) -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + SensorDeviceClass, + SensorEntity, +) from homeassistant.const import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_TEMPERATURE, ENTITY_CATEGORY_DIAGNOSTIC, LIGHT_LUX, PERCENTAGE, @@ -42,7 +43,7 @@ class GenericHueGaugeSensorEntity(GenericZLLSensor, SensorEntity): class HueLightLevel(GenericHueGaugeSensorEntity): """The light level sensor entity for a Hue motion sensor device.""" - _attr_device_class = DEVICE_CLASS_ILLUMINANCE + _attr_device_class = SensorDeviceClass.ILLUMINANCE _attr_native_unit_of_measurement = LIGHT_LUX @property @@ -77,7 +78,7 @@ class HueLightLevel(GenericHueGaugeSensorEntity): class HueTemperature(GenericHueGaugeSensorEntity): """The temperature sensor entity for a Hue motion sensor device.""" - _attr_device_class = DEVICE_CLASS_TEMPERATURE + _attr_device_class = SensorDeviceClass.TEMPERATURE _attr_state_class = STATE_CLASS_MEASUREMENT _attr_native_unit_of_measurement = TEMP_CELSIUS @@ -93,7 +94,7 @@ class HueTemperature(GenericHueGaugeSensorEntity): class HueBattery(GenericHueSensor, SensorEntity): """Battery class for when a batt-powered device is only represented as an event.""" - _attr_device_class = DEVICE_CLASS_BATTERY + _attr_device_class = SensorDeviceClass.BATTERY _attr_state_class = STATE_CLASS_MEASUREMENT _attr_native_unit_of_measurement = PERCENTAGE _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC diff --git a/homeassistant/components/hue/v2/binary_sensor.py b/homeassistant/components/hue/v2/binary_sensor.py index f655e203755..55ec6a41549 100644 --- a/homeassistant/components/hue/v2/binary_sensor.py +++ b/homeassistant/components/hue/v2/binary_sensor.py @@ -14,8 +14,7 @@ from aiohue.v2.models.entertainment import ( from aiohue.v2.models.motion import Motion from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_MOTION, - DEVICE_CLASS_RUNNING, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -80,7 +79,7 @@ class HueBinarySensorBase(HueBaseEntity, BinarySensorEntity): class HueMotionSensor(HueBinarySensorBase): """Representation of a Hue Motion sensor.""" - _attr_device_class = DEVICE_CLASS_MOTION + _attr_device_class = BinarySensorDeviceClass.MOTION @property def is_on(self) -> bool | None: @@ -96,7 +95,7 @@ class HueMotionSensor(HueBinarySensorBase): class HueEntertainmentActiveSensor(HueBinarySensorBase): """Representation of a Hue Entertainment Configuration as binary sensor.""" - _attr_device_class = DEVICE_CLASS_RUNNING + _attr_device_class = BinarySensorDeviceClass.RUNNING @property def is_on(self) -> bool | None: diff --git a/homeassistant/components/hue/v2/sensor.py b/homeassistant/components/hue/v2/sensor.py index bb801b7817d..daff7c8d6de 100644 --- a/homeassistant/components/hue/v2/sensor.py +++ b/homeassistant/components/hue/v2/sensor.py @@ -17,13 +17,14 @@ from aiohue.v2.models.device_power import DevicePower from aiohue.v2.models.light_level import LightLevel from aiohue.v2.models.temperature import Temperature -from homeassistant.components.binary_sensor import DEVICE_CLASS_CONNECTIVITY -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity +from homeassistant.components.binary_sensor import BinarySensorDeviceClass +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + SensorDeviceClass, + SensorEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_TEMPERATURE, ENTITY_CATEGORY_DIAGNOSTIC, LIGHT_LUX, PERCENTAGE, @@ -101,7 +102,7 @@ class HueTemperatureSensor(HueSensorBase): """Representation of a Hue Temperature sensor.""" _attr_native_unit_of_measurement = TEMP_CELSIUS - _attr_device_class = DEVICE_CLASS_TEMPERATURE + _attr_device_class = SensorDeviceClass.TEMPERATURE @property def native_value(self) -> float: @@ -118,7 +119,7 @@ class HueLightLevelSensor(HueSensorBase): """Representation of a Hue LightLevel (illuminance) sensor.""" _attr_native_unit_of_measurement = LIGHT_LUX - _attr_device_class = DEVICE_CLASS_ILLUMINANCE + _attr_device_class = SensorDeviceClass.ILLUMINANCE @property def native_value(self) -> int: @@ -142,7 +143,7 @@ class HueBatterySensor(HueSensorBase): """Representation of a Hue Battery sensor.""" _attr_native_unit_of_measurement = PERCENTAGE - _attr_device_class = DEVICE_CLASS_BATTERY + _attr_device_class = SensorDeviceClass.BATTERY _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC @property @@ -159,7 +160,7 @@ class HueBatterySensor(HueSensorBase): class HueZigbeeConnectivitySensor(HueSensorBase): """Representation of a Hue ZigbeeConnectivity sensor.""" - _attr_device_class = DEVICE_CLASS_CONNECTIVITY + _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC _attr_entity_registry_enabled_default = False From af2e95d8919f3ee3979a007353a8b38677f83c81 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 7 Dec 2021 12:44:50 +0100 Subject: [PATCH 0104/2644] Guard against missing states in Alexa state updates (#61152) --- homeassistant/components/alexa/state_report.py | 9 +++++---- tests/components/alexa/test_state_report.py | 12 ++++++++++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index 1ab12041e32..767bfa18224 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -182,12 +182,13 @@ async def async_send_add_or_update_message(hass, config, entity_ids): endpoints = [] for entity_id in entity_ids: - domain = entity_id.split(".", 1)[0] - - if domain not in ENTITY_ADAPTERS: + if (domain := entity_id.split(".", 1)[0]) not in ENTITY_ADAPTERS: continue - alexa_entity = ENTITY_ADAPTERS[domain](hass, config, hass.states.get(entity_id)) + if (state := hass.states.get(entity_id)) is None: + continue + + alexa_entity = ENTITY_ADAPTERS[domain](hass, config, state) endpoints.append(alexa_entity.serialize_discovery()) payload = {"endpoints": endpoints, "scope": {"type": "BearerToken", "token": token}} diff --git a/tests/components/alexa/test_state_report.py b/tests/components/alexa/test_state_report.py index bd91dc8f846..29624e7d1ff 100644 --- a/tests/components/alexa/test_state_report.py +++ b/tests/components/alexa/test_state_report.py @@ -117,10 +117,18 @@ async def test_send_add_or_update_message(hass, aioclient_mock): {"friendly_name": "Test Contact Sensor", "device_class": "door"}, ) - await state_report.async_send_add_or_update_message( - hass, DEFAULT_CONFIG, ["binary_sensor.test_contact", "zwave.bla"] + hass.states.async_set( + "zwave.bla", + "wow_such_unsupported", ) + entities = [ + "binary_sensor.test_contact", + "binary_sensor.non_existing", # Supported, but does not exist + "zwave.bla", # Unsupported + ] + await state_report.async_send_add_or_update_message(hass, DEFAULT_CONFIG, entities) + assert len(aioclient_mock.mock_calls) == 1 call = aioclient_mock.mock_calls From 4eeee795170411d2ae4baeaf105b683b7f12b405 Mon Sep 17 00:00:00 2001 From: Alexander Leisentritt Date: Tue, 7 Dec 2021 13:01:34 +0100 Subject: [PATCH 0105/2644] Change ondilo_ico update interval to 5 minutes (#61153) API has a rate limit of 30 queries per hours, 5 minutes uses 12 queries and leave a reasonable reserve but ensures timestamps of measurements in Home Assistant are about equal to real timestamps --- homeassistant/components/ondilo_ico/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ondilo_ico/sensor.py b/homeassistant/components/ondilo_ico/sensor.py index e0a07c6fe26..e45f3139529 100644 --- a/homeassistant/components/ondilo_ico/sensor.py +++ b/homeassistant/components/ondilo_ico/sensor.py @@ -89,7 +89,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ) -SCAN_INTERVAL = timedelta(hours=1) +SCAN_INTERVAL = timedelta(minutes=5) _LOGGER = logging.getLogger(__name__) From f0006b92be392aa80c489adf11819096034f3cf4 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Tue, 7 Dec 2021 13:16:24 +0100 Subject: [PATCH 0106/2644] Allow to lock SQLite database during backup (#60874) * Allow to set CONF_DB_URL This is useful for test which need a custom DB path. * Introduce write_lock_db helper to lock SQLite database * Introduce Websocket API which allows to lock database during backup * Fix isort * Avoid mutable default arguments * Address pylint issues * Avoid holding executor thread * Set unlock event in case timeout occures This makes sure the database is left unlocked even in case of a race condition. * Add more unit tests * Address new pylint errors * Lower timeout to speedup tests * Introduce queue overflow test * Unlock database if necessary This makes sure that the test runs through in case locking actually succeeds (and the test fails). * Make DB_LOCK_TIMEOUT a global There is no good reason for this to be an argument. The recorder needs to pick a sensible value. * Add Websocket Timeout test * Test lock_database() return * Update homeassistant/components/recorder/__init__.py Co-authored-by: Erik Montnemery * Fix format Co-authored-by: J. Nick Koston Co-authored-by: Erik Montnemery --- homeassistant/components/recorder/__init__.py | 100 ++++++++++++++++-- homeassistant/components/recorder/util.py | 19 ++++ .../components/recorder/websocket_api.py | 40 +++++++ tests/common.py | 5 +- tests/components/recorder/test_init.py | 79 ++++++++++++++ tests/components/recorder/test_util.py | 19 ++++ .../components/recorder/test_websocket_api.py | 59 +++++++++++ 7 files changed, 310 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index da3955cb9b8..8a907a8d9fa 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from collections.abc import Callable, Iterable import concurrent.futures +from dataclasses import dataclass from datetime import datetime, timedelta import logging import queue @@ -76,6 +77,7 @@ from .util import ( session_scope, setup_connection_for_dialect, validate_or_move_away_sqlite_database, + write_lock_db, ) _LOGGER = logging.getLogger(__name__) @@ -123,6 +125,9 @@ KEEPALIVE_TIME = 30 # States and Events objects EXPIRE_AFTER_COMMITS = 120 +DB_LOCK_TIMEOUT = 30 +DB_LOCK_QUEUE_CHECK_TIMEOUT = 1 + CONF_AUTO_PURGE = "auto_purge" CONF_DB_URL = "db_url" CONF_DB_MAX_RETRIES = "db_max_retries" @@ -370,6 +375,15 @@ class WaitTask: """An object to insert into the recorder queue to tell it set the _queue_watch event.""" +@dataclass +class DatabaseLockTask: + """An object to insert into the recorder queue to prevent writes to the database.""" + + database_locked: asyncio.Event + database_unlock: threading.Event + queue_overflow: bool + + class Recorder(threading.Thread): """A threaded recorder class.""" @@ -419,6 +433,7 @@ class Recorder(threading.Thread): self.migration_in_progress = False self._queue_watcher = None self._db_supports_row_number = True + self._database_lock_task: DatabaseLockTask | None = None self.enabled = True @@ -687,6 +702,8 @@ class Recorder(threading.Thread): def _process_one_event_or_recover(self, event): """Process an event, reconnect, or recover a malformed database.""" try: + if self._process_one_task(event): + return self._process_one_event(event) return except exc.DatabaseError as err: @@ -788,34 +805,63 @@ class Recorder(threading.Thread): # Schedule a new statistics task if this one didn't finish self.queue.put(ExternalStatisticsTask(metadata, stats)) - def _process_one_event(self, event): + def _lock_database(self, task: DatabaseLockTask): + @callback + def _async_set_database_locked(task: DatabaseLockTask): + task.database_locked.set() + + with write_lock_db(self): + # Notify that lock is being held, wait until database can be used again. + self.hass.add_job(_async_set_database_locked, task) + while not task.database_unlock.wait(timeout=DB_LOCK_QUEUE_CHECK_TIMEOUT): + if self.queue.qsize() > MAX_QUEUE_BACKLOG * 0.9: + _LOGGER.warning( + "Database queue backlog reached more than 90% of maximum queue " + "length while waiting for backup to finish; recorder will now " + "resume writing to database. The backup can not be trusted and " + "must be restarted" + ) + task.queue_overflow = True + break + _LOGGER.info( + "Database queue backlog reached %d entries during backup", + self.queue.qsize(), + ) + + def _process_one_task(self, event) -> bool: """Process one event.""" if isinstance(event, PurgeTask): self._run_purge(event.purge_before, event.repack, event.apply_filter) - return + return True if isinstance(event, PurgeEntitiesTask): self._run_purge_entities(event.entity_filter) - return + return True if isinstance(event, PerodicCleanupTask): perodic_db_cleanups(self) - return + return True if isinstance(event, StatisticsTask): self._run_statistics(event.start) - return + return True if isinstance(event, ClearStatisticsTask): statistics.clear_statistics(self, event.statistic_ids) - return + return True if isinstance(event, UpdateStatisticsMetadataTask): statistics.update_statistics_metadata( self, event.statistic_id, event.unit_of_measurement ) - return + return True if isinstance(event, ExternalStatisticsTask): self._run_external_statistics(event.metadata, event.statistics) - return + return True if isinstance(event, WaitTask): self._queue_watch.set() - return + return True + if isinstance(event, DatabaseLockTask): + self._lock_database(event) + return True + return False + + def _process_one_event(self, event): if event.event_type == EVENT_TIME_CHANGED: self._keepalive_count += 1 if self._keepalive_count >= KEEPALIVE_TIME: @@ -982,6 +1028,42 @@ class Recorder(threading.Thread): self.queue.put(WaitTask()) self._queue_watch.wait() + async def lock_database(self) -> bool: + """Lock database so it can be backed up safely.""" + if self._database_lock_task: + _LOGGER.warning("Database already locked") + return False + + database_locked = asyncio.Event() + task = DatabaseLockTask(database_locked, threading.Event(), False) + self.queue.put(task) + try: + await asyncio.wait_for(database_locked.wait(), timeout=DB_LOCK_TIMEOUT) + except asyncio.TimeoutError as err: + task.database_unlock.set() + raise TimeoutError( + f"Could not lock database within {DB_LOCK_TIMEOUT} seconds." + ) from err + self._database_lock_task = task + return True + + @callback + def unlock_database(self) -> bool: + """Unlock database. + + Returns true if database lock has been held throughout the process. + """ + if not self._database_lock_task: + _LOGGER.warning("Database currently not locked") + return False + + self._database_lock_task.database_unlock.set() + success = not self._database_lock_task.queue_overflow + + self._database_lock_task = None + + return success + def _setup_connection(self): """Ensure database is ready to fly.""" kwargs = {} diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index c63f6abee3a..3900641db63 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -457,6 +457,25 @@ def perodic_db_cleanups(instance: Recorder): connection.execute(text("PRAGMA wal_checkpoint(TRUNCATE);")) +@contextmanager +def write_lock_db(instance: Recorder): + """Lock database for writes.""" + + if instance.engine.dialect.name == "sqlite": + with instance.engine.connect() as connection: + # Execute sqlite to create a wal checkpoint + # This is optional but makes sure the backup is going to be minimal + connection.execute(text("PRAGMA wal_checkpoint(TRUNCATE)")) + # Create write lock + _LOGGER.debug("Lock database") + connection.execute(text("BEGIN IMMEDIATE;")) + try: + yield + finally: + _LOGGER.debug("Unlock database") + connection.execute(text("END;")) + + def async_migration_in_progress(hass: HomeAssistant) -> bool: """Determine is a migration is in progress. diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index 5a4f0425919..f6d4d57a7e5 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -1,6 +1,7 @@ """The Energy websocket API.""" from __future__ import annotations +import logging from typing import TYPE_CHECKING import voluptuous as vol @@ -15,6 +16,8 @@ from .util import async_migration_in_progress if TYPE_CHECKING: from . import Recorder +_LOGGER: logging.Logger = logging.getLogger(__package__) + @callback def async_setup(hass: HomeAssistant) -> None: @@ -23,6 +26,8 @@ def async_setup(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, ws_clear_statistics) websocket_api.async_register_command(hass, ws_update_statistics_metadata) websocket_api.async_register_command(hass, ws_info) + websocket_api.async_register_command(hass, ws_backup_start) + websocket_api.async_register_command(hass, ws_backup_end) @websocket_api.websocket_command( @@ -106,3 +111,38 @@ def ws_info( "thread_running": thread_alive, } connection.send_result(msg["id"], recorder_info) + + +@websocket_api.require_admin +@websocket_api.websocket_command({vol.Required("type"): "backup/start"}) +@websocket_api.async_response +async def ws_backup_start( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Backup start notification.""" + + _LOGGER.info("Backup start notification, locking database for writes") + instance: Recorder = hass.data[DATA_INSTANCE] + try: + await instance.lock_database() + except TimeoutError as err: + connection.send_error(msg["id"], "timeout_error", str(err)) + return + connection.send_result(msg["id"]) + + +@websocket_api.require_admin +@websocket_api.websocket_command({vol.Required("type"): "backup/end"}) +@websocket_api.async_response +async def ws_backup_end( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Backup end notification.""" + + instance: Recorder = hass.data[DATA_INSTANCE] + _LOGGER.info("Backup end notification, releasing write lock") + if not instance.unlock_database(): + connection.send_error( + msg["id"], "database_unlock_failed", "Failed to unlock database." + ) + connection.send_result(msg["id"]) diff --git a/tests/common.py b/tests/common.py index 55c76e953cd..9d4a9cfe366 100644 --- a/tests/common.py +++ b/tests/common.py @@ -902,8 +902,9 @@ def init_recorder_component(hass, add_config=None): async def async_init_recorder_component(hass, add_config=None): """Initialize the recorder asynchronously.""" - config = dict(add_config) if add_config else {} - config[recorder.CONF_DB_URL] = "sqlite://" + config = add_config or {} + if recorder.CONF_DB_URL not in config: + config[recorder.CONF_DB_URL] = "sqlite://" with patch("homeassistant.components.recorder.migration.migrate_schema"): assert await async_setup_component( diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index e41a0da34ba..7d7c3f27fb6 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -1,5 +1,6 @@ """The tests for the Recorder component.""" # pylint: disable=protected-access +import asyncio from datetime import datetime, timedelta import sqlite3 from unittest.mock import patch @@ -1134,3 +1135,81 @@ def test_entity_id_filter(hass_recorder): db_events = list(session.query(Events).filter_by(event_type="hello")) # Keep referring idx + 1, as no new events are being added assert len(db_events) == idx + 1, data + + +async def test_database_lock_and_unlock(hass: HomeAssistant, tmp_path): + """Test writing events during lock getting written after unlocking.""" + # Use file DB, in memory DB cannot do write locks. + config = {recorder.CONF_DB_URL: "sqlite:///" + str(tmp_path / "pytest.db")} + await async_init_recorder_component(hass, config) + await hass.async_block_till_done() + + instance: Recorder = hass.data[DATA_INSTANCE] + + assert await instance.lock_database() + + assert not await instance.lock_database() + + event_type = "EVENT_TEST" + event_data = {"test_attr": 5, "test_attr_10": "nice"} + hass.bus.fire(event_type, event_data) + task = asyncio.create_task(async_wait_recording_done(hass, instance)) + + # Recording can't be finished while lock is held + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(asyncio.shield(task), timeout=1) + + with session_scope(hass=hass) as session: + db_events = list(session.query(Events).filter_by(event_type=event_type)) + assert len(db_events) == 0 + + assert instance.unlock_database() + + await task + with session_scope(hass=hass) as session: + db_events = list(session.query(Events).filter_by(event_type=event_type)) + assert len(db_events) == 1 + + +async def test_database_lock_and_overflow(hass: HomeAssistant, tmp_path): + """Test writing events during lock leading to overflow the queue causes the database to unlock.""" + # Use file DB, in memory DB cannot do write locks. + config = {recorder.CONF_DB_URL: "sqlite:///" + str(tmp_path / "pytest.db")} + await async_init_recorder_component(hass, config) + await hass.async_block_till_done() + + instance: Recorder = hass.data[DATA_INSTANCE] + + with patch.object(recorder, "MAX_QUEUE_BACKLOG", 1), patch.object( + recorder, "DB_LOCK_QUEUE_CHECK_TIMEOUT", 0.1 + ): + await instance.lock_database() + + event_type = "EVENT_TEST" + event_data = {"test_attr": 5, "test_attr_10": "nice"} + hass.bus.fire(event_type, event_data) + + # Check that this causes the queue to overflow and write succeeds + # even before unlocking. + await async_wait_recording_done(hass, instance) + + with session_scope(hass=hass) as session: + db_events = list(session.query(Events).filter_by(event_type=event_type)) + assert len(db_events) == 1 + + assert not instance.unlock_database() + + +async def test_database_lock_timeout(hass): + """Test locking database timeout when recorder stopped.""" + await async_init_recorder_component(hass) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + instance: Recorder = hass.data[DATA_INSTANCE] + with patch.object(recorder, "DB_LOCK_TIMEOUT", 0.1): + try: + with pytest.raises(TimeoutError): + await instance.lock_database() + finally: + instance.unlock_database() diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 940925c48ca..fa449aefefc 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -8,6 +8,7 @@ import pytest from sqlalchemy import text from sqlalchemy.sql.elements import TextClause +from homeassistant.components import recorder from homeassistant.components.recorder import run_information_with_session, util from homeassistant.components.recorder.const import DATA_INSTANCE, SQLITE_URL_PREFIX from homeassistant.components.recorder.models import RecorderRuns @@ -556,3 +557,21 @@ def test_perodic_db_cleanups(hass_recorder): ][0] assert isinstance(text_obj, TextClause) assert str(text_obj) == "PRAGMA wal_checkpoint(TRUNCATE);" + + +async def test_write_lock_db(hass, tmp_path): + """Test database write lock.""" + from sqlalchemy.exc import OperationalError + + # Use file DB, in memory DB cannot do write locks. + config = {recorder.CONF_DB_URL: "sqlite:///" + str(tmp_path / "pytest.db")} + await async_init_recorder_component(hass, config) + await hass.async_block_till_done() + + instance = hass.data[DATA_INSTANCE] + + with util.write_lock_db(instance): + # Database should be locked now, try writing SQL command + with instance.engine.connect() as connection: + with pytest.raises(OperationalError): + connection.execute(text("DROP TABLE events;")) diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 7a45dea0379..994d1c677af 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -358,3 +358,62 @@ async def test_recorder_info_migration_queue_exhausted(hass, hass_ws_client): assert response["result"]["migration_in_progress"] is False assert response["result"]["recording"] is True assert response["result"]["thread_running"] is True + + +async def test_backup_start_no_recorder(hass, hass_ws_client): + """Test getting backup start when recorder is not present.""" + client = await hass_ws_client() + + await client.send_json({"id": 1, "type": "backup/start"}) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "unknown_command" + + +async def test_backup_start_timeout(hass, hass_ws_client): + """Test getting backup start when recorder is not present.""" + client = await hass_ws_client() + await async_init_recorder_component(hass) + + # Ensure there are no queued events + await async_wait_recording_done_without_instance(hass) + + with patch.object(recorder, "DB_LOCK_TIMEOUT", 0): + try: + await client.send_json({"id": 1, "type": "backup/start"}) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "timeout_error" + finally: + await client.send_json({"id": 2, "type": "backup/end"}) + + +async def test_backup_end(hass, hass_ws_client): + """Test backup start.""" + client = await hass_ws_client() + await async_init_recorder_component(hass) + + # Ensure there are no queued events + await async_wait_recording_done_without_instance(hass) + + await client.send_json({"id": 1, "type": "backup/start"}) + response = await client.receive_json() + assert response["success"] + + await client.send_json({"id": 2, "type": "backup/end"}) + response = await client.receive_json() + assert response["success"] + + +async def test_backup_end_without_start(hass, hass_ws_client): + """Test backup start.""" + client = await hass_ws_client() + await async_init_recorder_component(hass) + + # Ensure there are no queued events + await async_wait_recording_done_without_instance(hass) + + await client.send_json({"id": 1, "type": "backup/end"}) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "database_unlock_failed" From 45c463b61c2c00e595ed5a8bee01eace59c3e56c Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 7 Dec 2021 13:56:31 +0100 Subject: [PATCH 0107/2644] Fix Netatmo climate issue (#61154) Signed-off-by: cgtobi --- homeassistant/components/netatmo/climate.py | 11 +- .../components/netatmo/data_handler.py | 6 +- homeassistant/components/netatmo/select.py | 8 ++ .../netatmo/fixtures/homesdata.json | 102 ++++++++++++++++-- .../homestatus_111111111111111111111401.json | 4 + 5 files changed, 121 insertions(+), 10 deletions(-) create mode 100644 tests/components/netatmo/fixtures/homestatus_111111111111111111111401.json diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 49e02f566a0..1ead9d7cbdb 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -135,9 +135,14 @@ async def async_setup_entry( entities = [] for home_id in climate_topology.home_ids: signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}" - await data_handler.register_data_class( - CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id - ) + + try: + await data_handler.register_data_class( + CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id + ) + except KeyError: + continue + climate_state = data_handler.data[signal_name] climate_topology.register_handler(home_id, climate_state.process_topology) diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index c62522a931a..7a97ec3748f 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -194,7 +194,11 @@ class NetatmoDataHandler: self._auth, **kwargs ) - await self.async_fetch_data(data_class_entry) + try: + await self.async_fetch_data(data_class_entry) + except KeyError: + self.data_classes.pop(data_class_entry) + raise self._queue.append(self.data_classes[data_class_entry]) _LOGGER.debug("Data class %s added", data_class_entry) diff --git a/homeassistant/components/netatmo/select.py b/homeassistant/components/netatmo/select.py index 9902155be73..98576497f3e 100644 --- a/homeassistant/components/netatmo/select.py +++ b/homeassistant/components/netatmo/select.py @@ -48,6 +48,14 @@ async def async_setup_entry( entities = [] for home_id in climate_topology.home_ids: signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}" + + try: + await data_handler.register_data_class( + CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id + ) + except KeyError: + continue + await data_handler.register_data_class( CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id ) diff --git a/tests/components/netatmo/fixtures/homesdata.json b/tests/components/netatmo/fixtures/homesdata.json index fd63a0c200f..8c6587ca973 100644 --- a/tests/components/netatmo/fixtures/homesdata.json +++ b/tests/components/netatmo/fixtures/homesdata.json @@ -5,7 +5,10 @@ "id": "91763b24c43d3e344f424e8b", "name": "MYHOME", "altitude": 112, - "coordinates": [52.516263, 13.377726], + "coordinates": [ + 52.516263, + 13.377726 + ], "country": "DE", "timezone": "Europe/Berlin", "rooms": [ @@ -13,25 +16,33 @@ "id": "2746182631", "name": "Livingroom", "type": "livingroom", - "module_ids": ["12:34:56:00:01:ae"] + "module_ids": [ + "12:34:56:00:01:ae" + ] }, { "id": "3688132631", "name": "Hall", "type": "custom", - "module_ids": ["12:34:56:00:f1:62"] + "module_ids": [ + "12:34:56:00:f1:62" + ] }, { "id": "2833524037", "name": "Entrada", "type": "lobby", - "module_ids": ["12:34:56:03:a5:54"] + "module_ids": [ + "12:34:56:03:a5:54" + ] }, { "id": "2940411577", "name": "Cocina", "type": "kitchen", - "module_ids": ["12:34:56:03:a0:ac"] + "module_ids": [ + "12:34:56:03:a0:ac" + ] } ], "modules": [ @@ -388,6 +399,85 @@ } ], "therm_mode": "schedule" + }, + { + "id": "111111111111111111111401", + "name": "Home with no modules", + "altitude": 9, + "coordinates": [ + 1.23456789, + 50.0987654 + ], + "country": "BE", + "timezone": "Europe/Brussels", + "rooms": [ + { + "id": "1111111401", + "name": "Livingroom", + "type": "livingroom" + } + ], + "temperature_control_mode": "heating", + "therm_mode": "away", + "therm_setpoint_default_duration": 120, + "cooling_mode": "schedule", + "schedules": [ + { + "away_temp": 14, + "hg_temp": 7, + "name": "Week", + "timetable": [ + { + "zone_id": 1, + "m_offset": 0 + }, + { + "zone_id": 6, + "m_offset": 420 + } + ], + "zones": [ + { + "type": 0, + "name": "Comfort", + "rooms_temp": [], + "id": 0, + "rooms": [] + }, + { + "type": 1, + "name": "Nacht", + "rooms_temp": [], + "id": 1, + "rooms": [] + }, + { + "type": 5, + "name": "Eco", + "rooms_temp": [], + "id": 4, + "rooms": [] + }, + { + "type": 4, + "name": "Tussenin", + "rooms_temp": [], + "id": 5, + "rooms": [] + }, + { + "type": 4, + "name": "Ochtend", + "rooms_temp": [], + "id": 6, + "rooms": [] + } + ], + "id": "700000000000000000000401", + "selected": true, + "type": "therm" + } + ] } ], "user": { @@ -404,4 +494,4 @@ "status": "ok", "time_exec": 0.056135892868042, "time_server": 1559171003 -} +} \ No newline at end of file diff --git a/tests/components/netatmo/fixtures/homestatus_111111111111111111111401.json b/tests/components/netatmo/fixtures/homestatus_111111111111111111111401.json new file mode 100644 index 00000000000..2ae65dc0d21 --- /dev/null +++ b/tests/components/netatmo/fixtures/homestatus_111111111111111111111401.json @@ -0,0 +1,4 @@ +{ + "status": "ok", + "time_server": 1638873670 +} \ No newline at end of file From 489d85d862789523788bcc539a2701400052ae26 Mon Sep 17 00:00:00 2001 From: Tim Rightnour <6556271+garbled1@users.noreply.github.com> Date: Tue, 7 Dec 2021 05:59:43 -0700 Subject: [PATCH 0108/2644] Add Onewire diagnostic and config switches and binary_sensors (#59309) * Onewire: Add diagnostic and config switches and binary_sensors This commit adds diagnostic and config switches and binary_sensors to the HobbyBoards devices. With these, the user will be able to configure those devices, without having to run owwrite/owread commands outside of HA. * Address review from @epenet * Add HB_HUB to DEVICE_SUPPORT_OWSERVER * Device class and entity category enums * Fixup merge breakage * Remove duplicate lines --- .../components/onewire/binary_sensor.py | 37 ++++- homeassistant/components/onewire/const.py | 3 +- homeassistant/components/onewire/switch.py | 58 ++++++- tests/components/onewire/const.py | 150 ++++++++++++++++++ 4 files changed, 241 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/onewire/binary_sensor.py b/homeassistant/components/onewire/binary_sensor.py index 7f569b150c2..945ec2344d4 100644 --- a/homeassistant/components/onewire/binary_sensor.py +++ b/homeassistant/components/onewire/binary_sensor.py @@ -6,6 +6,7 @@ import os from typing import TYPE_CHECKING from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -13,10 +14,12 @@ from homeassistant.components.onewire.model import OWServerDeviceDescription from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_TYPE from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( CONF_TYPE_OWSERVER, + DEVICE_KEYS_0_3, DEVICE_KEYS_0_7, DEVICE_KEYS_A_B, DOMAIN, @@ -61,8 +64,33 @@ DEVICE_BINARY_SENSORS: dict[str, tuple[OneWireBinarySensorEntityDescription, ... ) for id in DEVICE_KEYS_A_B ), + "EF": (), # "HobbyBoard": special } +# EF sensors are usually hobbyboards specialized sensors. +HOBBYBOARD_EF: dict[str, tuple[OneWireBinarySensorEntityDescription, ...]] = { + "HB_HUB": tuple( + OneWireBinarySensorEntityDescription( + key=f"hub/short.{id}", + entity_registry_enabled_default=False, + name=f"Hub Short on Branch {id}", + read_mode=READ_MODE_BOOL, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=BinarySensorDeviceClass.PROBLEM, + ) + for id in DEVICE_KEYS_0_3 + ), +} + + +def get_sensor_types( + device_sub_type: str, +) -> dict[str, tuple[OneWireBinarySensorEntityDescription, ...]]: + """Return the proper info array for the device type.""" + if "HobbyBoard" in device_sub_type: + return HOBBYBOARD_EF + return DEVICE_BINARY_SENSORS + async def async_setup_entry( hass: HomeAssistant, @@ -89,11 +117,16 @@ def get_entities(onewirehub: OneWireHub) -> list[BinarySensorEntity]: assert isinstance(device, OWServerDeviceDescription) family = device.family device_id = device.id + device_type = device.type device_info = device.device_info + device_sub_type = "std" + if "EF" in family: + device_sub_type = "HobbyBoard" + family = device_type - if family not in DEVICE_BINARY_SENSORS: + if family not in get_sensor_types(device_sub_type): continue - for description in DEVICE_BINARY_SENSORS[family]: + for description in get_sensor_types(device_sub_type)[family]: device_file = os.path.join(os.path.split(device.path)[0], description.key) name = f"{device_id} {description.name}" entities.append( diff --git a/homeassistant/components/onewire/const.py b/homeassistant/components/onewire/const.py index 91f931e2517..fc13ad9c3b0 100644 --- a/homeassistant/components/onewire/const.py +++ b/homeassistant/components/onewire/const.py @@ -15,6 +15,7 @@ DEFAULT_SYSBUS_MOUNT_DIR = "/sys/bus/w1/devices/" DOMAIN = "onewire" +DEVICE_KEYS_0_3 = range(4) DEVICE_KEYS_0_7 = range(8) DEVICE_KEYS_A_B = ("A", "B") @@ -32,7 +33,7 @@ DEVICE_SUPPORT_OWSERVER = { "3B": (), "42": (), "7E": ("EDS0066", "EDS0068"), - "EF": ("HB_MOISTURE_METER", "HobbyBoards_EF"), + "EF": ("HB_HUB", "HB_MOISTURE_METER", "HobbyBoards_EF"), } DEVICE_SUPPORT_SYSBUS = ["10", "22", "28", "3B", "42"] diff --git a/homeassistant/components/onewire/switch.py b/homeassistant/components/onewire/switch.py index 49f1ece51ca..a7dc1335a46 100644 --- a/homeassistant/components/onewire/switch.py +++ b/homeassistant/components/onewire/switch.py @@ -2,7 +2,6 @@ from __future__ import annotations from dataclasses import dataclass -import logging import os from typing import TYPE_CHECKING, Any @@ -16,6 +15,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( CONF_TYPE_OWSERVER, + DEVICE_KEYS_0_3, DEVICE_KEYS_0_7, DEVICE_KEYS_A_B, DOMAIN, @@ -97,9 +97,54 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = { ) for id in DEVICE_KEYS_A_B ), + "EF": (), # "HobbyBoard": special } -LOGGER = logging.getLogger(__name__) +# EF sensors are usually hobbyboards specialized sensors. + +HOBBYBOARD_EF: dict[str, tuple[OneWireEntityDescription, ...]] = { + "HB_HUB": tuple( + OneWireSwitchEntityDescription( + key=f"hub/branch.{id}", + entity_registry_enabled_default=False, + name=f"Hub Branch {id} Enable", + read_mode=READ_MODE_BOOL, + entity_category=EntityCategory.CONFIG, + ) + for id in DEVICE_KEYS_0_3 + ), + "HB_MOISTURE_METER": tuple( + [ + OneWireSwitchEntityDescription( + key=f"moisture/is_leaf.{id}", + entity_registry_enabled_default=False, + name=f"Leaf Sensor {id} Enable", + read_mode=READ_MODE_BOOL, + entity_category=EntityCategory.CONFIG, + ) + for id in DEVICE_KEYS_0_3 + ] + + [ + OneWireSwitchEntityDescription( + key=f"moisture/is_moisture.{id}", + entity_registry_enabled_default=False, + name=f"Moisture Sensor {id} Enable", + read_mode=READ_MODE_BOOL, + entity_category=EntityCategory.CONFIG, + ) + for id in DEVICE_KEYS_0_3 + ] + ), +} + + +def get_sensor_types( + device_sub_type: str, +) -> dict[str, tuple[OneWireEntityDescription, ...]]: + """Return the proper info array for the device type.""" + if "HobbyBoard" in device_sub_type: + return HOBBYBOARD_EF + return DEVICE_SWITCHES async def async_setup_entry( @@ -127,12 +172,17 @@ def get_entities(onewirehub: OneWireHub) -> list[SwitchEntity]: if TYPE_CHECKING: assert isinstance(device, OWServerDeviceDescription) family = device.family + device_type = device.type device_id = device.id device_info = device.device_info + device_sub_type = "std" + if "EF" in family: + device_sub_type = "HobbyBoard" + family = device_type - if family not in DEVICE_SWITCHES: + if family not in get_sensor_types(device_sub_type): continue - for description in DEVICE_SWITCHES[family]: + for description in get_sensor_types(device_sub_type)[family]: device_file = os.path.join(os.path.split(device.path)[0], description.key) name = f"{device_id} {description.name}" entities.append( diff --git a/tests/components/onewire/const.py b/tests/components/onewire/const.py index 2153e153961..3eb0ef51742 100644 --- a/tests/components/onewire/const.py +++ b/tests/components/onewire/const.py @@ -2,6 +2,7 @@ from pi1wire import InvalidCRCException, UnsupportResponseException from pyownet.protocol import Error as ProtocolError +from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.onewire.const import ( DOMAIN, MANUFACTURER_EDS, @@ -796,6 +797,155 @@ MOCK_OWPROXY_DEVICES = { ATTR_UNIT_OF_MEASUREMENT: PRESSURE_CBAR, }, ], + Platform.SWITCH: [ + { + ATTR_DEFAULT_DISABLED: True, + ATTR_ENTITY_CATEGORY: EntityCategory.CONFIG, + ATTR_ENTITY_ID: "switch.ef_111111111112_leaf_sensor_0_enable", + ATTR_INJECT_READS: b"1", + ATTR_STATE: STATE_ON, + ATTR_UNIQUE_ID: "/EF.111111111112/moisture/is_leaf.0", + }, + { + ATTR_DEFAULT_DISABLED: True, + ATTR_ENTITY_CATEGORY: EntityCategory.CONFIG, + ATTR_ENTITY_ID: "switch.ef_111111111112_leaf_sensor_1_enable", + ATTR_INJECT_READS: b"1", + ATTR_STATE: STATE_ON, + ATTR_UNIQUE_ID: "/EF.111111111112/moisture/is_leaf.1", + }, + { + ATTR_DEFAULT_DISABLED: True, + ATTR_ENTITY_CATEGORY: EntityCategory.CONFIG, + ATTR_ENTITY_ID: "switch.ef_111111111112_leaf_sensor_2_enable", + ATTR_INJECT_READS: b"0", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "/EF.111111111112/moisture/is_leaf.2", + }, + { + ATTR_DEFAULT_DISABLED: True, + ATTR_ENTITY_CATEGORY: EntityCategory.CONFIG, + ATTR_ENTITY_ID: "switch.ef_111111111112_leaf_sensor_3_enable", + ATTR_INJECT_READS: b"0", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "/EF.111111111112/moisture/is_leaf.3", + }, + { + ATTR_DEFAULT_DISABLED: True, + ATTR_ENTITY_CATEGORY: EntityCategory.CONFIG, + ATTR_ENTITY_ID: "switch.ef_111111111112_moisture_sensor_0_enable", + ATTR_INJECT_READS: b"1", + ATTR_STATE: STATE_ON, + ATTR_UNIQUE_ID: "/EF.111111111112/moisture/is_moisture.0", + }, + { + ATTR_DEFAULT_DISABLED: True, + ATTR_ENTITY_CATEGORY: EntityCategory.CONFIG, + ATTR_ENTITY_ID: "switch.ef_111111111112_moisture_sensor_1_enable", + ATTR_INJECT_READS: b"1", + ATTR_STATE: STATE_ON, + ATTR_UNIQUE_ID: "/EF.111111111112/moisture/is_moisture.1", + }, + { + ATTR_DEFAULT_DISABLED: True, + ATTR_ENTITY_CATEGORY: EntityCategory.CONFIG, + ATTR_ENTITY_ID: "switch.ef_111111111112_moisture_sensor_2_enable", + ATTR_INJECT_READS: b"0", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "/EF.111111111112/moisture/is_moisture.2", + }, + { + ATTR_DEFAULT_DISABLED: True, + ATTR_ENTITY_CATEGORY: EntityCategory.CONFIG, + ATTR_ENTITY_ID: "switch.ef_111111111112_moisture_sensor_3_enable", + ATTR_INJECT_READS: b"0", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "/EF.111111111112/moisture/is_moisture.3", + }, + ], + }, + "EF.111111111113": { + ATTR_INJECT_READS: [ + b"HB_HUB", # read type + ], + ATTR_DEVICE_INFO: { + ATTR_IDENTIFIERS: {(DOMAIN, "EF.111111111113")}, + ATTR_MANUFACTURER: MANUFACTURER_HOBBYBOARDS, + ATTR_MODEL: "HB_HUB", + ATTR_NAME: "EF.111111111113", + }, + Platform.BINARY_SENSOR: [ + { + ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: BinarySensorDeviceClass.PROBLEM, + ATTR_ENTITY_CATEGORY: EntityCategory.DIAGNOSTIC, + ATTR_ENTITY_ID: "binary_sensor.ef_111111111113_hub_short_on_branch_0", + ATTR_INJECT_READS: b"1", + ATTR_STATE: STATE_ON, + ATTR_UNIQUE_ID: "/EF.111111111113/hub/short.0", + }, + { + ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: BinarySensorDeviceClass.PROBLEM, + ATTR_ENTITY_CATEGORY: EntityCategory.DIAGNOSTIC, + ATTR_ENTITY_ID: "binary_sensor.ef_111111111113_hub_short_on_branch_1", + ATTR_INJECT_READS: b"0", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "/EF.111111111113/hub/short.1", + }, + { + ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: BinarySensorDeviceClass.PROBLEM, + ATTR_ENTITY_CATEGORY: EntityCategory.DIAGNOSTIC, + ATTR_ENTITY_ID: "binary_sensor.ef_111111111113_hub_short_on_branch_2", + ATTR_INJECT_READS: b"1", + ATTR_STATE: STATE_ON, + ATTR_UNIQUE_ID: "/EF.111111111113/hub/short.2", + }, + { + ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: BinarySensorDeviceClass.PROBLEM, + ATTR_ENTITY_CATEGORY: EntityCategory.DIAGNOSTIC, + ATTR_ENTITY_ID: "binary_sensor.ef_111111111113_hub_short_on_branch_3", + ATTR_INJECT_READS: b"0", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "/EF.111111111113/hub/short.3", + }, + ], + Platform.SWITCH: [ + { + ATTR_DEFAULT_DISABLED: True, + ATTR_ENTITY_CATEGORY: EntityCategory.CONFIG, + ATTR_ENTITY_ID: "switch.ef_111111111113_hub_branch_0_enable", + ATTR_INJECT_READS: b"1", + ATTR_STATE: STATE_ON, + ATTR_UNIQUE_ID: "/EF.111111111113/hub/branch.0", + }, + { + ATTR_DEFAULT_DISABLED: True, + ATTR_ENTITY_CATEGORY: EntityCategory.CONFIG, + ATTR_ENTITY_ID: "switch.ef_111111111113_hub_branch_1_enable", + ATTR_INJECT_READS: b"0", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "/EF.111111111113/hub/branch.1", + }, + { + ATTR_DEFAULT_DISABLED: True, + ATTR_ENTITY_CATEGORY: EntityCategory.CONFIG, + ATTR_ENTITY_ID: "switch.ef_111111111113_hub_branch_2_enable", + ATTR_INJECT_READS: b"1", + ATTR_STATE: STATE_ON, + ATTR_UNIQUE_ID: "/EF.111111111113/hub/branch.2", + }, + { + ATTR_DEFAULT_DISABLED: True, + ATTR_ENTITY_CATEGORY: EntityCategory.CONFIG, + ATTR_ENTITY_ID: "switch.ef_111111111113_hub_branch_3_enable", + ATTR_INJECT_READS: b"0", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "/EF.111111111113/hub/branch.3", + }, + ], }, "7E.111111111111": { ATTR_INJECT_READS: [ From 200a5c7e05e83a265e26d8aff09dc6a043826884 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 7 Dec 2021 07:40:36 -0800 Subject: [PATCH 0109/2644] Fix flaky tests with unsynchronized stream `available` assertions (#61167) Remove assertions that are not guaratuneed to be safe depending on the state of the background worker. This leaves in the state check for the keepalive case which does have some synchronization already. --- tests/components/stream/test_hls.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index 9c529d7abe5..5e0ee15f097 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -135,7 +135,6 @@ async def test_hls_stream(hass, hls_stream, stream_worker_sync): # Request stream stream.add_provider(HLS_PROVIDER) - assert stream.available stream.start() hls_client = await hls_stream(stream) @@ -162,9 +161,6 @@ async def test_hls_stream(hass, hls_stream, stream_worker_sync): stream_worker_sync.resume() - # The stream worker reported end of stream and exited - assert not stream.available - # Stop stream, if it hasn't quit already stream.stop() @@ -185,7 +181,6 @@ async def test_stream_timeout(hass, hass_client, stream_worker_sync): # Request stream stream.add_provider(HLS_PROVIDER) - assert stream.available stream.start() url = stream.endpoint_url(HLS_PROVIDER) @@ -247,6 +242,7 @@ async def test_stream_keepalive(hass): # Setup demo HLS track source = "test_stream_keepalive_source" stream = create_stream(hass, source, {}) + assert stream.available track = stream.add_provider(HLS_PROVIDER) track.num_segments = 2 @@ -276,6 +272,7 @@ async def test_stream_keepalive(hass): # Stop stream, if it hasn't quit already stream.stop() + assert not stream.available async def test_hls_playlist_view_no_output(hass, hls_stream): From 5256e26b6aa57a796f75ee4ec77a93491eb8cf7d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 7 Dec 2021 18:05:41 +0100 Subject: [PATCH 0110/2644] Fix incorrect docstring in automation trace code (#61168) --- homeassistant/helpers/trace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/trace.py b/homeassistant/helpers/trace.py index 8848be00e79..4332f34107c 100644 --- a/homeassistant/helpers/trace.py +++ b/homeassistant/helpers/trace.py @@ -216,7 +216,7 @@ def script_execution_set(reason: str) -> None: def script_execution_get() -> str | None: - """Return the current trace.""" + """Return the stop reason.""" if (data := script_execution_cv.get()) is None: return None return data.script_execution From 84dbc8279d23d53f8cf89b321247c5dce3d17c4d Mon Sep 17 00:00:00 2001 From: bsmappee <58250533+bsmappee@users.noreply.github.com> Date: Tue, 7 Dec 2021 19:15:51 +0100 Subject: [PATCH 0111/2644] Bump pysmappee to 0.2.29 (#61160) --- homeassistant/components/smappee/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smappee/manifest.json b/homeassistant/components/smappee/manifest.json index 91192a13484..6a1edaf41ae 100644 --- a/homeassistant/components/smappee/manifest.json +++ b/homeassistant/components/smappee/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/smappee", "dependencies": ["http"], "requirements": [ - "pysmappee==0.2.27" + "pysmappee==0.2.29" ], "codeowners": [ "@bsmappee" diff --git a/requirements_all.txt b/requirements_all.txt index dcf44749cdb..4668f970b75 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1811,7 +1811,7 @@ pyskyqhub==0.1.3 pysma==0.6.9 # homeassistant.components.smappee -pysmappee==0.2.27 +pysmappee==0.2.29 # homeassistant.components.smartthings pysmartapp==0.3.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6677487dd3b..e96fca06f38 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1115,7 +1115,7 @@ pysignalclirestapi==0.3.4 pysma==0.6.9 # homeassistant.components.smappee -pysmappee==0.2.27 +pysmappee==0.2.29 # homeassistant.components.smartthings pysmartapp==0.3.3 From 00a82bf945e143ef30806028f8a7dbaa87afd144 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Tue, 7 Dec 2021 13:47:44 -0500 Subject: [PATCH 0112/2644] Remove loopenergy integration (#61175) * Remove loopenergy integration * Fix requirements_all.txt * Fix requirements_test_all.txt --- .coveragerc | 1 - CODEOWNERS | 1 - .../components/loopenergy/__init__.py | 1 - .../components/loopenergy/manifest.json | 8 - homeassistant/components/loopenergy/sensor.py | 149 ------------------ requirements_all.txt | 3 - 6 files changed, 163 deletions(-) delete mode 100644 homeassistant/components/loopenergy/__init__.py delete mode 100644 homeassistant/components/loopenergy/manifest.json delete mode 100644 homeassistant/components/loopenergy/sensor.py diff --git a/.coveragerc b/.coveragerc index 0f4c8d68d22..8d26873b7af 100644 --- a/.coveragerc +++ b/.coveragerc @@ -600,7 +600,6 @@ omit = homeassistant/components/lookin/models.py homeassistant/components/lookin/sensor.py homeassistant/components/lookin/climate.py - homeassistant/components/loopenergy/sensor.py homeassistant/components/luci/device_tracker.py homeassistant/components/luftdaten/__init__.py homeassistant/components/luftdaten/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index a349954bf65..109cb234734 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -294,7 +294,6 @@ homeassistant/components/local_ip/* @issacg homeassistant/components/logger/* @home-assistant/core homeassistant/components/logi_circle/* @evanjd homeassistant/components/lookin/* @ANMalko -homeassistant/components/loopenergy/* @pavoni homeassistant/components/lovelace/* @home-assistant/frontend homeassistant/components/luci/* @mzdrale homeassistant/components/luftdaten/* @fabaff diff --git a/homeassistant/components/loopenergy/__init__.py b/homeassistant/components/loopenergy/__init__.py deleted file mode 100644 index 4e963f2828a..00000000000 --- a/homeassistant/components/loopenergy/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The loopenergy component.""" diff --git a/homeassistant/components/loopenergy/manifest.json b/homeassistant/components/loopenergy/manifest.json deleted file mode 100644 index 01a18dc01db..00000000000 --- a/homeassistant/components/loopenergy/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "loopenergy", - "name": "Loop Energy", - "documentation": "https://www.home-assistant.io/integrations/loopenergy", - "requirements": ["pyloopenergy==0.2.1"], - "codeowners": ["@pavoni"], - "iot_class": "cloud_push" -} diff --git a/homeassistant/components/loopenergy/sensor.py b/homeassistant/components/loopenergy/sensor.py deleted file mode 100644 index 05d7f79ebfd..00000000000 --- a/homeassistant/components/loopenergy/sensor.py +++ /dev/null @@ -1,149 +0,0 @@ -"""Support for Loop Energy sensors.""" -import logging - -import pyloopenergy -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import ( - CONF_UNIT_SYSTEM_IMPERIAL, - CONF_UNIT_SYSTEM_METRIC, - EVENT_HOMEASSISTANT_STOP, -) -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -CONF_ELEC = "electricity" -CONF_GAS = "gas" - -CONF_ELEC_SERIAL = "electricity_serial" -CONF_ELEC_SECRET = "electricity_secret" - -CONF_GAS_SERIAL = "gas_serial" -CONF_GAS_SECRET = "gas_secret" -CONF_GAS_CALORIFIC = "gas_calorific" - -CONF_GAS_TYPE = "gas_type" - -DEFAULT_CALORIFIC = 39.11 -DEFAULT_UNIT = "kW" - -ELEC_SCHEMA = vol.Schema( - { - vol.Required(CONF_ELEC_SERIAL): cv.string, - vol.Required(CONF_ELEC_SECRET): cv.string, - } -) - -GAS_TYPE_SCHEMA = vol.In([CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL]) - -GAS_SCHEMA = vol.Schema( - { - vol.Required(CONF_GAS_SERIAL): cv.string, - vol.Required(CONF_GAS_SECRET): cv.string, - vol.Optional(CONF_GAS_TYPE, default=CONF_UNIT_SYSTEM_METRIC): GAS_TYPE_SCHEMA, - vol.Optional(CONF_GAS_CALORIFIC, default=DEFAULT_CALORIFIC): vol.Coerce(float), - } -) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Required(CONF_ELEC): ELEC_SCHEMA, vol.Optional(CONF_GAS): GAS_SCHEMA} -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Loop Energy sensors.""" - elec_config = config.get(CONF_ELEC) - gas_config = config.get(CONF_GAS, {}) - - controller = pyloopenergy.LoopEnergy( - elec_config.get(CONF_ELEC_SERIAL), - elec_config.get(CONF_ELEC_SECRET), - gas_config.get(CONF_GAS_SERIAL), - gas_config.get(CONF_GAS_SECRET), - gas_config.get(CONF_GAS_TYPE), - gas_config.get(CONF_GAS_CALORIFIC), - ) - - def stop_loopenergy(event): - """Shutdown loopenergy thread on exit.""" - _LOGGER.info("Shutting down loopenergy") - controller.terminate() - - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_loopenergy) - - sensors = [LoopEnergyElec(controller)] - - if gas_config.get(CONF_GAS_SERIAL): - sensors.append(LoopEnergyGas(controller)) - - add_entities(sensors) - - -class LoopEnergySensor(SensorEntity): - """Implementation of an Loop Energy base sensor.""" - - def __init__(self, controller): - """Initialize the sensor.""" - self._state = None - self._unit_of_measurement = DEFAULT_UNIT - self._controller = controller - self._name = None - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def native_unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement - - def _callback(self): - self.schedule_update_ha_state(True) - - -class LoopEnergyElec(LoopEnergySensor): - """Implementation of an Loop Energy Electricity sensor.""" - - def __init__(self, controller): - """Initialize the sensor.""" - super().__init__(controller) - self._name = "Power Usage" - - async def async_added_to_hass(self): - """Subscribe to updates.""" - self._controller.subscribe_elecricity(self._callback) - - def update(self): - """Get the cached Loop energy reading.""" - self._state = round(self._controller.electricity_useage, 2) - - -class LoopEnergyGas(LoopEnergySensor): - """Implementation of an Loop Energy Gas sensor.""" - - def __init__(self, controller): - """Initialize the sensor.""" - super().__init__(controller) - self._name = "Gas Usage" - - async def async_added_to_hass(self): - """Subscribe to updates.""" - self._controller.subscribe_gas(self._callback) - - def update(self): - """Get the cached Loop gas reading.""" - self._state = round(self._controller.gas_useage, 2) diff --git a/requirements_all.txt b/requirements_all.txt index 4668f970b75..d7d0e76af01 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1612,9 +1612,6 @@ pylitejet==0.3.0 # homeassistant.components.litterrobot pylitterbot==2021.11.0 -# homeassistant.components.loopenergy -pyloopenergy==0.2.1 - # homeassistant.components.lutron_caseta pylutron-caseta==0.11.0 From 9af8d59a7afc78f1f759660e893d8f54ca85eac6 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 7 Dec 2021 12:29:54 -0700 Subject: [PATCH 0113/2644] Bump py17track to 2021.12.2 (#61166) --- homeassistant/components/seventeentrack/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/seventeentrack/manifest.json b/homeassistant/components/seventeentrack/manifest.json index 05f240043a9..01fdb22395c 100644 --- a/homeassistant/components/seventeentrack/manifest.json +++ b/homeassistant/components/seventeentrack/manifest.json @@ -2,7 +2,7 @@ "domain": "seventeentrack", "name": "17TRACK", "documentation": "https://www.home-assistant.io/integrations/seventeentrack", - "requirements": ["py17track==2021.12.1"], + "requirements": ["py17track==2021.12.2"], "codeowners": [], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index d7d0e76af01..e345918b076 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1305,7 +1305,7 @@ py-synologydsm-api==1.0.4 py-zabbix==1.1.7 # homeassistant.components.seventeentrack -py17track==2021.12.1 +py17track==2021.12.2 # homeassistant.components.hdmi_cec pyCEC==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e96fca06f38..5b15427bcba 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -795,7 +795,7 @@ py-nightscout==1.2.2 py-synologydsm-api==1.0.4 # homeassistant.components.seventeentrack -py17track==2021.12.1 +py17track==2021.12.2 # homeassistant.components.control4 pyControl4==0.0.6 From 6d867e04157b32314d3b26beb9b5b0e9be1f90d0 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 7 Dec 2021 11:30:23 -0800 Subject: [PATCH 0114/2644] Bump nest to 0.4.5 to fix media player event expiration (#61174) --- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index b9f20e92670..11a464dbaf1 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["ffmpeg", "http", "media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.4"], + "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.5"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index e345918b076..91db3c545eb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -741,7 +741,7 @@ google-cloud-pubsub==2.1.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==0.4.4 +google-nest-sdm==0.4.5 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5b15427bcba..76048ed7565 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -464,7 +464,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.1.0 # homeassistant.components.nest -google-nest-sdm==0.4.4 +google-nest-sdm==0.4.5 # homeassistant.components.google_travel_time googlemaps==2.5.1 From dced4d4542a72141fd5bcde3753466642fbda846 Mon Sep 17 00:00:00 2001 From: einarhauks Date: Tue, 7 Dec 2021 19:33:24 +0000 Subject: [PATCH 0115/2644] Display energy in wh instead of kWh (#61169) --- .../components/tesla_wall_connector/sensor.py | 10 +++++----- tests/components/tesla_wall_connector/test_sensor.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/tesla_wall_connector/sensor.py b/homeassistant/components/tesla_wall_connector/sensor.py index b2353681291..8219d121ae3 100644 --- a/homeassistant/components/tesla_wall_connector/sensor.py +++ b/homeassistant/components/tesla_wall_connector/sensor.py @@ -14,7 +14,7 @@ from homeassistant.const import ( DEVICE_CLASS_VOLTAGE, ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, - ENERGY_KILO_WATT_HOUR, + ENERGY_WATT_HOUR, ENTITY_CATEGORY_DIAGNOSTIC, FREQUENCY_HERTZ, TEMP_CELSIUS, @@ -120,10 +120,10 @@ WALL_CONNECTOR_SENSORS = [ entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), WallConnectorSensorDescription( - key="total_energy_kWh", - name=prefix_entity_name("Total Energy"), - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - value_fn=lambda data: data[WALLCONNECTOR_DATA_LIFETIME].energy_wh / 1000.0, + key="energy_kWh", + name=prefix_entity_name("Energy"), + native_unit_of_measurement=ENERGY_WATT_HOUR, + value_fn=lambda data: data[WALLCONNECTOR_DATA_LIFETIME].energy_wh, state_class=STATE_CLASS_TOTAL_INCREASING, device_class=DEVICE_CLASS_ENERGY, ), diff --git a/tests/components/tesla_wall_connector/test_sensor.py b/tests/components/tesla_wall_connector/test_sensor.py index 6763f685441..0cafc15c6f1 100644 --- a/tests/components/tesla_wall_connector/test_sensor.py +++ b/tests/components/tesla_wall_connector/test_sensor.py @@ -24,7 +24,7 @@ async def test_sensors(hass: HomeAssistant) -> None: "sensor.tesla_wall_connector_grid_frequency", "50.021", "49.981" ), EntityAndExpectedValues( - "sensor.tesla_wall_connector_total_energy", "988.022", "989.0" + "sensor.tesla_wall_connector_energy", "988022", "989000" ), EntityAndExpectedValues( "sensor.tesla_wall_connector_phase_a_current", "10", "7" From 4a814405c2193e5933ed7896e300d87c401ef52d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 7 Dec 2021 20:55:16 +0100 Subject: [PATCH 0116/2644] Minor deduplication of condition validation code (#61170) --- homeassistant/components/automation/config.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py index e852b6cc4c0..228e78ac446 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -19,7 +19,7 @@ from homeassistant.const import ( ) from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, config_validation as cv, script -from homeassistant.helpers.condition import async_validate_condition_config +from homeassistant.helpers.condition import async_validate_conditions_config from homeassistant.helpers.trigger import async_validate_trigger_config from homeassistant.loader import IntegrationNotFound @@ -76,11 +76,8 @@ async def async_validate_config_item(hass, config, full_config=None): ) if CONF_CONDITION in config: - config[CONF_CONDITION] = await asyncio.gather( - *( - async_validate_condition_config(hass, cond) - for cond in config[CONF_CONDITION] - ) + config[CONF_CONDITION] = await async_validate_conditions_config( + hass, config[CONF_CONDITION] ) config[CONF_ACTION] = await script.async_validate_actions_config( From 7c7df5bb5185c315e329570a0ad80f1d9eb8173a Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 7 Dec 2021 21:50:34 +0100 Subject: [PATCH 0117/2644] Change check for existence of options flow (#61147) Co-authored-by: Martin Hjelmare Co-authored-by: Paulus Schoutsen --- homeassistant/components/config/config_entries.py | 10 ++++------ homeassistant/components/hue/config_flow.py | 15 ++++++++++----- homeassistant/config_entries.py | 6 ++++++ tests/components/config/test_config_entries.py | 8 +++++++- tests/components/hue/test_config_flow.py | 5 ++--- 5 files changed, 29 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index b1f686e23a4..61df9dc190d 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -366,13 +366,11 @@ async def ignore_config_flow(hass, connection, msg): def entry_json(entry: config_entries.ConfigEntry) -> dict: """Return JSON value of a config entry.""" handler = config_entries.HANDLERS.get(entry.domain) - supports_options = ( - # Guard in case handler is no longer registered (custom component etc) - handler is not None - # pylint: disable=comparison-with-callable - and handler.async_get_options_flow - != config_entries.ConfigFlow.async_get_options_flow + # work out if handler has support for options flow + supports_options = handler is not None and handler.async_supports_options_flow( + entry ) + return { "entry_id": entry.entry_id, "domain": entry.domain, diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index ceb5a9a1a8e..49fca2158d5 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -12,7 +12,7 @@ import async_timeout import slugify as unicode_slug import voluptuous as vol -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import ssdp, zeroconf from homeassistant.const import CONF_API_KEY, CONF_HOST from homeassistant.core import callback @@ -48,10 +48,15 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): config_entry: config_entries.ConfigEntry, ) -> HueOptionsFlowHandler: """Get the options flow for this handler.""" - if config_entry.data.get(CONF_API_VERSION, 1) == 1: - # Options for Hue are only applicable to V1 bridges. - return HueOptionsFlowHandler(config_entry) - raise data_entry_flow.UnknownHandler + return HueOptionsFlowHandler(config_entry) + + @classmethod + @callback + def async_supports_options_flow( + cls, config_entry: config_entries.ConfigEntry + ) -> bool: + """Return options flow support for this handler.""" + return config_entry.data.get(CONF_API_VERSION, 1) == 1 def __init__(self) -> None: """Initialize the Hue flow.""" diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 45bc10f5774..cdea9da2540 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1163,6 +1163,12 @@ class ConfigFlow(data_entry_flow.FlowHandler): """Get the options flow for this handler.""" raise data_entry_flow.UnknownHandler + @classmethod + @callback + def async_supports_options_flow(cls, config_entry: ConfigEntry) -> bool: + """Return options flow support for this handler.""" + return cls.async_get_options_flow is not ConfigFlow.async_get_options_flow + @callback def _async_abort_entries_match( self, match_dict: dict[str, Any] | None = None diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 8b890148d51..20a19495597 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -47,10 +47,16 @@ async def test_get_entries(hass, client): @staticmethod @callback - def async_get_options_flow(config, options): + def async_get_options_flow(config_entry): """Get options flow.""" pass + @classmethod + @callback + def async_supports_options_flow(cls, config_entry): + """Return options flow support for this handler.""" + return True + hass.helpers.config_entry_flow.register_discovery_flow( "comp2", "Comp 2", lambda: None ) diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index 65d3dd696d6..6ce8ff3e1c4 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -7,7 +7,7 @@ from aiohue.errors import LinkButtonNotPressed import pytest import voluptuous as vol -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import ssdp, zeroconf from homeassistant.components.hue import config_flow, const from homeassistant.components.hue.errors import CannotConnect @@ -706,8 +706,7 @@ async def test_options_flow_v2(hass): ) entry.add_to_hass(hass) - with pytest.raises(data_entry_flow.UnknownHandler): - await hass.config_entries.options.async_init(entry.entry_id) + assert config_flow.HueFlowHandler.async_supports_options_flow(entry) is False async def test_bridge_zeroconf(hass, aioclient_mock): From b0affe7bfb434e4753156f81125c2da691352d3b Mon Sep 17 00:00:00 2001 From: Alberto Geniola Date: Tue, 7 Dec 2021 22:42:55 +0100 Subject: [PATCH 0118/2644] Elmax integration (#59321) * Add elmax integration. * Run hassfest and generate requirements_all * Remove secondary platforms from elmax integration as per first component integration. * Move ElmaxCoordinator and ElmaxEntity into external file Linting review * Remove useless variables * Fix wrong indentation. * Remove unecessary platforms. * Remove unnecessary attributes from manifest. * Rely on property getters/setters rathern than private attribute from parent. Update internal entity state just after transitory state update. * Update homeassistant/components/elmax/const.py Reference Platform constant Co-authored-by: Marvin Wichmann * Update username/password values Rely on already-present templating constants Co-authored-by: Marvin Wichmann * Add missing constant import. * Remove unnecessary test_unhandled_error() callback implementation. * Add common.py to coverage ignore list. * Improve coverage of config_flow. * Rename the integration. Co-authored-by: Franck Nijhof * Fix reauth bug and improve testing. * Refactor lambdas into generators. Co-authored-by: Marvin Wichmann Co-authored-by: Franck Nijhof --- .coveragerc | 4 + CODEOWNERS | 1 + homeassistant/components/elmax/__init__.py | 56 ++++ homeassistant/components/elmax/common.py | 229 +++++++++++++++ homeassistant/components/elmax/config_flow.py | 249 ++++++++++++++++ homeassistant/components/elmax/const.py | 17 ++ homeassistant/components/elmax/manifest.json | 11 + homeassistant/components/elmax/strings.json | 34 +++ homeassistant/components/elmax/switch.py | 83 ++++++ .../components/elmax/translations/en.json | 45 +++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/elmax/__init__.py | 15 + tests/components/elmax/conftest.py | 42 +++ .../components/elmax/fixtures/get_panel.json | 126 ++++++++ .../elmax/fixtures/list_devices.json | 11 + tests/components/elmax/fixtures/login.json | 8 + tests/components/elmax/test_config_flow.py | 268 ++++++++++++++++++ 19 files changed, 1206 insertions(+) create mode 100644 homeassistant/components/elmax/__init__.py create mode 100644 homeassistant/components/elmax/common.py create mode 100644 homeassistant/components/elmax/config_flow.py create mode 100644 homeassistant/components/elmax/const.py create mode 100644 homeassistant/components/elmax/manifest.json create mode 100644 homeassistant/components/elmax/strings.json create mode 100644 homeassistant/components/elmax/switch.py create mode 100644 homeassistant/components/elmax/translations/en.json create mode 100644 tests/components/elmax/__init__.py create mode 100644 tests/components/elmax/conftest.py create mode 100644 tests/components/elmax/fixtures/get_panel.json create mode 100644 tests/components/elmax/fixtures/list_devices.json create mode 100644 tests/components/elmax/fixtures/login.json create mode 100644 tests/components/elmax/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 8d26873b7af..4bb0a13751a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -257,6 +257,10 @@ omit = homeassistant/components/eight_sleep/* homeassistant/components/eliqonline/sensor.py homeassistant/components/elkm1/* + homeassistant/components/elmax/__init__.py + homeassistant/components/elmax/common.py + homeassistant/components/elmax/const.py + homeassistant/components/elmax/switch.py homeassistant/components/elv/* homeassistant/components/emby/media_player.py homeassistant/components/emoncms/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 109cb234734..e0c7b99865e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -146,6 +146,7 @@ homeassistant/components/egardia/* @jeroenterheerdt homeassistant/components/eight_sleep/* @mezz64 @raman325 homeassistant/components/elgato/* @frenck homeassistant/components/elkm1/* @gwww @bdraco +homeassistant/components/elmax/* @albertogeniola homeassistant/components/elv/* @majuss homeassistant/components/emby/* @mezz64 homeassistant/components/emoncms/* @borpin diff --git a/homeassistant/components/elmax/__init__.py b/homeassistant/components/elmax/__init__.py new file mode 100644 index 00000000000..8b136cd2acd --- /dev/null +++ b/homeassistant/components/elmax/__init__.py @@ -0,0 +1,56 @@ +"""The elmax-cloud integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import HomeAssistantType + +from .common import ElmaxCoordinator +from .const import ( + CONF_ELMAX_PANEL_ID, + CONF_ELMAX_PANEL_PIN, + CONF_ELMAX_PASSWORD, + CONF_ELMAX_USERNAME, + DOMAIN, + ELMAX_PLATFORMS, + POLLING_SECONDS, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: + """Set up elmax-cloud from a config entry.""" + # Create the API client object and attempt a login, so that we immediately know + # if there is something wrong with user credentials + coordinator = ElmaxCoordinator( + hass=hass, + logger=_LOGGER, + username=entry.data[CONF_ELMAX_USERNAME], + password=entry.data[CONF_ELMAX_PASSWORD], + panel_id=entry.data[CONF_ELMAX_PANEL_ID], + panel_pin=entry.data[CONF_ELMAX_PANEL_PIN], + name=f"Elmax Cloud {entry.entry_id}", + update_interval=timedelta(seconds=POLLING_SECONDS), + ) + + # Issue a first refresh, so that we trigger a re-auth flow if necessary + await coordinator.async_config_entry_first_refresh() + + # Store a global reference to the coordinator for later use + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + + # Perform platform initialization. + hass.config_entries.async_setup_platforms(entry, ELMAX_PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, ELMAX_PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/elmax/common.py b/homeassistant/components/elmax/common.py new file mode 100644 index 00000000000..b44ffa2152b --- /dev/null +++ b/homeassistant/components/elmax/common.py @@ -0,0 +1,229 @@ +"""Elmax integration common classes and utilities.""" +from __future__ import annotations + +from collections.abc import Mapping +from datetime import timedelta +import logging +from logging import Logger +from typing import Any + +import async_timeout +from elmax_api.exceptions import ( + ElmaxApiError, + ElmaxBadLoginError, + ElmaxBadPinError, + ElmaxNetworkError, +) +from elmax_api.http import Elmax +from elmax_api.model.endpoint import DeviceEndpoint +from elmax_api.model.panel import PanelEntry, PanelStatus + +from homeassistant.components.elmax.const import DEFAULT_TIMEOUT, DOMAIN +from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + + +class ElmaxCoordinator(DataUpdateCoordinator): + """Coordinator helper to handle Elmax API polling.""" + + def __init__( + self, + hass: HomeAssistantType, + logger: Logger, + username: str, + password: str, + panel_id: str, + panel_pin: str, + name: str, + update_interval: timedelta, + ) -> None: + """Instantiate the object.""" + self._client = Elmax(username=username, password=password) + self._panel_id = panel_id + self._panel_pin = panel_pin + self._panel_entry = None + self._state_by_endpoint = None + super().__init__( + hass=hass, logger=logger, name=name, update_interval=update_interval + ) + + @property + def panel_entry(self) -> PanelEntry | None: + """Return the panel entry.""" + return self._panel_entry + + @property + def panel_status(self) -> PanelStatus | None: + """Return the last fetched panel status.""" + return self.data + + def get_endpoint_state(self, endpoint_id: str) -> DeviceEndpoint | None: + """Return the last fetched status for the given endpoint-id.""" + if self._state_by_endpoint is not None: + return self._state_by_endpoint.get(endpoint_id) + return None + + @property + def http_client(self): + """Return the current http client being used by this instance.""" + return self._client + + async def _async_update_data(self): + try: + async with async_timeout.timeout(DEFAULT_TIMEOUT): + # Retrieve the panel online status first + panels = await self._client.list_control_panels() + panels = list(filter(lambda x: x.hash == self._panel_id, panels)) + + # If the panel is no more available within the given. Raise config error as the user must + # reconfigure it in order to make it work again + if len(panels) < 1: + _LOGGER.error( + "Panel ID %s is no more linked to this user account", + self._panel_id, + ) + raise ConfigEntryAuthFailed() + + panel = panels[0] + self._panel_entry = panel + + # If the panel is online, proceed with fetching its state + # and return it right away + if panel.online: + status = await self._client.get_panel_status( + control_panel_id=panel.hash, pin=self._panel_pin + ) # type: PanelStatus + + # Store a dictionary for fast endpoint state access + self._state_by_endpoint = { + k.endpoint_id: k for k in status.all_endpoints + } + return status + + # Otherwise, return None. Listeners will know that this means the device is offline + return None + + except ElmaxBadPinError as err: + _LOGGER.error("Control panel pin was refused") + raise ConfigEntryAuthFailed from err + except ElmaxBadLoginError as err: + _LOGGER.error("Refused username/password") + raise ConfigEntryAuthFailed from err + except ElmaxApiError as err: + raise HomeAssistantError( + f"Error communicating with ELMAX API: {err}" + ) from err + except ElmaxNetworkError as err: + raise HomeAssistantError( + "Network error occurred while contacting ELMAX cloud" + ) from err + except Exception as err: + _LOGGER.exception("Unexpected exception") + raise HomeAssistantError("An unexpected error occurred") from err + + +class ElmaxEntity(Entity): + """Wrapper for Elmax entities.""" + + def __init__( + self, + panel: PanelEntry, + elmax_device: DeviceEndpoint, + panel_version: str, + coordinator: ElmaxCoordinator, + ) -> None: + """Construct the object.""" + self._panel = panel + self._device = elmax_device + self._panel_version = panel_version + self._coordinator = coordinator + self._transitory_state = None + + @property + def transitory_state(self) -> Any | None: + """Return the transitory state for this entity.""" + return self._transitory_state + + @transitory_state.setter + def transitory_state(self, value: Any) -> None: + """Set the transitory state value.""" + self._transitory_state = value + + @property + def panel_id(self) -> str: + """Retrieve the panel id.""" + return self._panel.hash + + @property + def unique_id(self) -> str | None: + """Provide a unique id for this entity.""" + return self._device.endpoint_id + + @property + def name(self) -> str | None: + """Return the entity name.""" + return self._device.name + + @property + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """Return extra attributes.""" + return { + "index": self._device.index, + "visible": self._device.visible, + } + + @property + def device_info(self): + """Return device specific attributes.""" + return { + "identifiers": {(DOMAIN, self._panel.hash)}, + "name": self._panel.get_name_by_user( + self._coordinator.http_client.get_authenticated_username() + ), + "manufacturer": "Elmax", + "model": self._panel_version, + "sw_version": self._panel_version, + } + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._panel.online + + def _http_data_changed(self) -> None: + # Whenever new HTTP data is received from the coordinator we extract the stat of this + # device and store it locally for later use + device_state = self._coordinator.get_endpoint_state(self._device.endpoint_id) + if self._device is None or device_state.__dict__ != self._device.__dict__: + # If HTTP data has changed, we need to schedule a forced refresh + self._device = device_state + self.async_schedule_update_ha_state(force_refresh=True) + + # Reset the transitory state as we did receive a fresh state + self._transitory_state = None + + @property + def should_poll(self) -> bool: + """Return True if entity has to be polled for state. + + False if entity pushes its state to HA. + """ + return False + + async def async_added_to_hass(self) -> None: + """Run when entity about to be added to hass. + + To be extended by integrations. + """ + self._coordinator.async_add_listener(self._http_data_changed) + + async def async_will_remove_from_hass(self) -> None: + """Run when entity will be removed from hass. + + To be extended by integrations. + """ + self._coordinator.async_remove_listener(self._http_data_changed) diff --git a/homeassistant/components/elmax/config_flow.py b/homeassistant/components/elmax/config_flow.py new file mode 100644 index 00000000000..c19c49f4b0b --- /dev/null +++ b/homeassistant/components/elmax/config_flow.py @@ -0,0 +1,249 @@ +"""Config flow for elmax-cloud integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from elmax_api.exceptions import ElmaxBadLoginError, ElmaxBadPinError, ElmaxNetworkError +from elmax_api.http import Elmax +from elmax_api.model.panel import PanelEntry +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components.elmax.const import ( + CONF_ELMAX_PANEL_ID, + CONF_ELMAX_PANEL_NAME, + CONF_ELMAX_PANEL_PIN, + CONF_ELMAX_PASSWORD, + CONF_ELMAX_USERNAME, + DOMAIN, +) +from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError + +_LOGGER = logging.getLogger(__name__) + +LOGIN_FORM_SCHEMA = vol.Schema( + { + vol.Required(CONF_ELMAX_USERNAME): str, + vol.Required(CONF_ELMAX_PASSWORD): str, + } +) + + +def _store_panel_by_name( + panel: PanelEntry, username: str, panel_names: dict[str, str] +) -> None: + original_panel_name = panel.get_name_by_user(username=username) + panel_id = panel.hash + collisions_count = 0 + panel_name = original_panel_name + while panel_name in panel_names: + # Handle same-name collision. + collisions_count += 1 + panel_name = f"{original_panel_name} ({collisions_count})" + panel_names[panel_name] = panel_id + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for elmax-cloud.""" + + VERSION = 1 + + def __init__(self): + """Initialize.""" + self._client: Elmax = None + self._username: str = None + self._password: str = None + self._panels_schema = None + self._panel_names = None + self._reauth_username = None + self._reauth_panelid = None + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + # When invokes without parameters, show the login form. + if user_input is None: + return self.async_show_form(step_id="user", data_schema=LOGIN_FORM_SCHEMA) + + errors: dict[str, str] = {} + username = user_input[CONF_ELMAX_USERNAME] + password = user_input[CONF_ELMAX_PASSWORD] + + # Otherwise, it means we are handling now the "submission" of the user form. + # In this case, let's try to log in to the Elmax cloud and retrieve the available panels. + try: + client = Elmax(username=username, password=password) + await client.login() + + # If the login succeeded, retrieve the list of available panels and filter the online ones + online_panels = [x for x in await client.list_control_panels() if x.online] + + # If no online panel was found, we display an error in the next UI. + panels = list(online_panels) + if len(panels) < 1: + raise NoOnlinePanelsError() + + # Show the panel selection. + # We want the user to choose the panel using the associated name, we set up a mapping + # dictionary to handle that case. + panel_names: dict[str, str] = {} + username = client.get_authenticated_username() + for panel in panels: + _store_panel_by_name( + panel=panel, username=username, panel_names=panel_names + ) + + self._client = client + self._panel_names = panel_names + schema = vol.Schema( + { + vol.Required(CONF_ELMAX_PANEL_NAME): vol.In( + self._panel_names.keys() + ), + vol.Required(CONF_ELMAX_PANEL_PIN, default="000000"): str, + } + ) + self._panels_schema = schema + self._username = username + self._password = password + return self.async_show_form( + step_id="panels", data_schema=schema, errors=errors + ) + + except ElmaxBadLoginError: + _LOGGER.error("Wrong credentials or failed login") + errors["base"] = "bad_auth" + except NoOnlinePanelsError: + _LOGGER.warning("No online device panel was found") + errors["base"] = "no_panel_online" + except ElmaxNetworkError: + _LOGGER.exception("A network error occurred") + errors["base"] = "network_error" + + # If an error occurred, show back the login form. + return self.async_show_form( + step_id="user", data_schema=LOGIN_FORM_SCHEMA, errors=errors + ) + + async def async_step_panels(self, user_input: dict[str, Any]) -> FlowResult: + """Handle Panel selection step.""" + errors = {} + panel_name = user_input[CONF_ELMAX_PANEL_NAME] + panel_pin = user_input[CONF_ELMAX_PANEL_PIN] + + # Lookup the panel id from the panel name. + panel_id = self._panel_names[panel_name] + + # Make sure this is the only elmax integration for this specific panel id. + await self.async_set_unique_id(panel_id) + self._abort_if_unique_id_configured() + + # Try to list all the devices using the given PIN. + try: + await self._client.get_panel_status( + control_panel_id=panel_id, pin=panel_pin + ) + return self.async_create_entry( + title=f"Elmax {panel_name}", + data={ + CONF_ELMAX_PANEL_ID: panel_id, + CONF_ELMAX_PANEL_PIN: panel_pin, + CONF_ELMAX_USERNAME: self._username, + CONF_ELMAX_PASSWORD: self._password, + }, + ) + except ElmaxBadPinError: + errors["base"] = "invalid_pin" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error occurred") + errors["base"] = "unknown_error" + + return self.async_show_form( + step_id="panels", data_schema=self._panels_schema, errors=errors + ) + + async def async_step_reauth(self, user_input=None): + """Perform reauth upon an API authentication error.""" + self._reauth_username = user_input.get(CONF_ELMAX_USERNAME) + self._reauth_panelid = user_input.get(CONF_ELMAX_PANEL_ID) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm(self, user_input=None): + """Handle reauthorization flow.""" + errors = {} + if user_input is not None: + panel_pin = user_input.get(CONF_ELMAX_PANEL_PIN) + password = user_input.get(CONF_ELMAX_PASSWORD) + entry = await self.async_set_unique_id(self._reauth_panelid) + + # Handle authentication, make sure the panel we are re-authenticating against is listed among results + # and verify its pin is correct. + try: + # Test login. + client = Elmax(username=self._reauth_username, password=password) + await client.login() + + # Make sure the panel we are authenticating to is still available. + panels = [ + p + for p in await client.list_control_panels() + if p.hash == self._reauth_panelid + ] + if len(panels) < 1: + raise NoOnlinePanelsError() + + # Verify the pin is still valid.from + await client.get_panel_status( + control_panel_id=self._reauth_panelid, pin=panel_pin + ) + + # If it is, proceed with configuration update. + self.hass.config_entries.async_update_entry( + entry, + data={ + CONF_ELMAX_PANEL_ID: self._reauth_panelid, + CONF_ELMAX_PANEL_PIN: panel_pin, + CONF_ELMAX_USERNAME: self._reauth_username, + CONF_ELMAX_PASSWORD: password, + }, + ) + await self.hass.config_entries.async_reload(entry.entry_id) + self._reauth_username = None + self._reauth_panelid = None + return self.async_abort(reason="reauth_successful") + + except ElmaxBadLoginError: + _LOGGER.error( + "Wrong credentials or failed login while re-authenticating" + ) + errors["base"] = "bad_auth" + except NoOnlinePanelsError: + _LOGGER.warning( + "Panel ID %s is no longer associated to this user", + self._reauth_panelid, + ) + errors["base"] = "reauth_panel_disappeared" + except ElmaxBadPinError: + errors["base"] = "invalid_pin" + + # We want the user to re-authenticate only for the given panel id using the same login. + # We pin them to the UI, so the user realizes she must log in with the appropriate credentials + # for the that specific panel. + schema = vol.Schema( + { + vol.Required(CONF_ELMAX_USERNAME): self._reauth_username, + vol.Required(CONF_ELMAX_PASSWORD): str, + vol.Required(CONF_ELMAX_PANEL_ID): self._reauth_panelid, + vol.Required(CONF_ELMAX_PANEL_PIN): str, + } + ) + return self.async_show_form( + step_id="reauth_confirm", data_schema=schema, errors=errors + ) + + +class NoOnlinePanelsError(HomeAssistantError): + """Error occurring when no online panel was found.""" diff --git a/homeassistant/components/elmax/const.py b/homeassistant/components/elmax/const.py new file mode 100644 index 00000000000..21864e98f1a --- /dev/null +++ b/homeassistant/components/elmax/const.py @@ -0,0 +1,17 @@ +"""Constants for the elmax-cloud integration.""" +from homeassistant.const import Platform + +DOMAIN = "elmax" +CONF_ELMAX_USERNAME = "username" +CONF_ELMAX_PASSWORD = "password" +CONF_ELMAX_PANEL_ID = "panel_id" +CONF_ELMAX_PANEL_PIN = "panel_pin" +CONF_ELMAX_PANEL_NAME = "panel_name" + +CONF_CONFIG_ENTRY_ID = "config_entry_id" +CONF_ENDPOINT_ID = "endpoint_id" + +ELMAX_PLATFORMS = [Platform.SWITCH] + +POLLING_SECONDS = 30 +DEFAULT_TIMEOUT = 10.0 diff --git a/homeassistant/components/elmax/manifest.json b/homeassistant/components/elmax/manifest.json new file mode 100644 index 00000000000..b89ca55ce3d --- /dev/null +++ b/homeassistant/components/elmax/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "elmax", + "name": "Elmax", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/elmax", + "requirements": ["elmax_api==0.0.2"], + "codeowners": [ + "@albertogeniola" + ], + "iot_class": "cloud_polling" +} \ No newline at end of file diff --git a/homeassistant/components/elmax/strings.json b/homeassistant/components/elmax/strings.json new file mode 100644 index 00000000000..505622aa6ae --- /dev/null +++ b/homeassistant/components/elmax/strings.json @@ -0,0 +1,34 @@ +{ + "title": "Elmax Cloud Setup", + "config": { + "step": { + "user": { + "title": "Account Login", + "description": "Please login to the Elmax cloud using your credentials", + "data": { + "password": "[%key:common::config_flow::data::password%]", + "username": "[%key:common::config_flow::data::username%]" + } + }, + "panels": { + "title": "Panel selection", + "description": "Select which panel you would like to control with this integration. Please note that the panel must be ON in order to be configured.", + "data": { + "panel_name": "Panel Name", + "panel_id": "Panel ID", + "panel_pin": "PIN Code" + } + } + }, + "error": { + "no_panel_online": "No online Elmax control panel was found.", + "bad_auth": "Invalid authentication", + "network_error": "A network error occurred", + "invalid_pin": "The provided pin is invalid", + "unknown_error": "An unexpected error occurred" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elmax/switch.py b/homeassistant/components/elmax/switch.py new file mode 100644 index 00000000000..4a8cd5f4214 --- /dev/null +++ b/homeassistant/components/elmax/switch.py @@ -0,0 +1,83 @@ +"""Elmax switch platform.""" +from typing import Any + +from elmax_api.model.command import SwitchCommand +from elmax_api.model.panel import PanelStatus + +from homeassistant.components.elmax import ElmaxCoordinator +from homeassistant.components.elmax.common import ElmaxEntity +from homeassistant.components.elmax.const import DOMAIN +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import HomeAssistantType + + +class ElmaxSwitch(ElmaxEntity, SwitchEntity): + """Implement the Elmax switch entity.""" + + @property + def is_on(self) -> bool: + """Return True if entity is on.""" + if self.transitory_state is not None: + return self.transitory_state + return self._device.opened + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + client = self._coordinator.http_client + await client.execute_command( + endpoint_id=self._device.endpoint_id, command=SwitchCommand.TURN_ON + ) + self.transitory_state = True + await self.async_update_ha_state() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + client = self._coordinator.http_client + await client.execute_command( + endpoint_id=self._device.endpoint_id, command=SwitchCommand.TURN_OFF + ) + self.transitory_state = False + await self.async_update_ha_state() + + @property + def assumed_state(self) -> bool: + """Return True if unable to access real state of the entity.""" + return False + + +async def async_setup_entry( + hass: HomeAssistantType, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Elmax switch platform.""" + coordinator: ElmaxCoordinator = hass.data[DOMAIN][config_entry.entry_id] + known_devices = set() + + def _discover_new_devices(): + panel_status = coordinator.panel_status # type: PanelStatus + # In case the panel is offline, its status will be None. In that case, simply do nothing + if panel_status is None: + return + + # Otherwise, add all the entities we found + entities = [] + for actuator in panel_status.actuators: + entity = ElmaxSwitch( + panel=coordinator.panel_entry, + elmax_device=actuator, + panel_version=panel_status.release, + coordinator=coordinator, + ) + if entity.unique_id not in known_devices: + entities.append(entity) + async_add_entities(entities, True) + known_devices.update([entity.unique_id for entity in entities]) + + # Register a listener for the discovery of new devices + coordinator.async_add_listener(_discover_new_devices) + + # Immediately run a discovery, so we don't need to wait for the next update + _discover_new_devices() diff --git a/homeassistant/components/elmax/translations/en.json b/homeassistant/components/elmax/translations/en.json new file mode 100644 index 00000000000..e49e57a4ce6 --- /dev/null +++ b/homeassistant/components/elmax/translations/en.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "There already is an integration for that Elmaxc panel." + }, + "error": { + "bad_auth": "Invalid authentication", + "invalid_pin": "The provided pin is invalid", + "network_error": "A network error occurred", + "no_panel_online": "No online Elmax control panel was found.", + "unknown_error": "An unexpected error occurred", + "reauth_panel_disappeared": "The panel is no longer associated to your account." + }, + "step": { + "panels": { + "data": { + "panel_id": "Panel ID", + "panel_name": "Panel Name", + "panel_pin": "PIN Code" + }, + "description": "Select which panel you would like to control with this integration. Please note that the panel must be ON in order to be configured.", + "title": "Panel selection" + }, + "reauth_confirm": { + "data": { + "username": "Username", + "password": "Password", + "panel_id": "Panel ID", + "panel_pin": "PIN Code" + }, + "description": "Please authenticate again to the Elmax cloud.", + "title": "Re-Authenticate" + }, + "user": { + "data": { + "password": "Password", + "username": "Username" + }, + "description": "Please login to the Elmax cloud using your credentials", + "title": "Account Login" + } + } + }, + "title": "Elmax Cloud Setup" +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 596cdf03fb7..1663c3f131e 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -78,6 +78,7 @@ FLOWS = [ "efergy", "elgato", "elkm1", + "elmax", "emonitor", "emulated_roku", "enocean", diff --git a/requirements_all.txt b/requirements_all.txt index 91db3c545eb..6e8d30ddb65 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -590,6 +590,9 @@ eliqonline==1.2.2 # homeassistant.components.elkm1 elkm1-lib==1.0.0 +# homeassistant.components.elmax +elmax_api==0.0.2 + # homeassistant.components.mobile_app emoji==1.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 76048ed7565..978239ef499 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -368,6 +368,9 @@ elgato==2.2.0 # homeassistant.components.elkm1 elkm1-lib==1.0.0 +# homeassistant.components.elmax +elmax_api==0.0.2 + # homeassistant.components.mobile_app emoji==1.5.0 diff --git a/tests/components/elmax/__init__.py b/tests/components/elmax/__init__.py new file mode 100644 index 00000000000..cf1bce356c7 --- /dev/null +++ b/tests/components/elmax/__init__.py @@ -0,0 +1,15 @@ +"""Tests for the Elmax component.""" + +MOCK_USER_JWT = ( + "JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" + ".eyJfaWQiOiIxYjExYmIxMWJiYjExMTExYjFiMTFiMWIiLCJlbWFpbCI6InRoaXMuaXNAdGVzdC5jb20iLCJyb2xlIjoid" + "XNlciIsImlhdCI6MTYzNjE5OTk5OCwiZXhwIjoxNjM2MjM1OTk4fQ.1C7lXuKyX1HEGOfMxNwxJ2n-CjoW4rwvNRITQxLI" + "Cv0" +) +MOCK_USERNAME = "this.is@test.com" +MOCK_USER_ROLE = "user" +MOCK_USER_ID = "1b11bb11bbb11111b1b11b1b" +MOCK_PANEL_ID = "2db3dae30b9102de4d078706f94d0708" +MOCK_PANEL_NAME = "Test Panel Name" +MOCK_PANEL_PIN = "000000" +MOCK_PASSWORD = "password" diff --git a/tests/components/elmax/conftest.py b/tests/components/elmax/conftest.py new file mode 100644 index 00000000000..17ad58b6292 --- /dev/null +++ b/tests/components/elmax/conftest.py @@ -0,0 +1,42 @@ +"""Configuration for Elmax tests.""" +import json + +from elmax_api.constants import ( + BASE_URL, + ENDPOINT_DEVICES, + ENDPOINT_DISCOVERY, + ENDPOINT_LOGIN, +) +from httpx import Response +import pytest +import respx + +from tests.common import load_fixture +from tests.components.elmax import MOCK_PANEL_ID, MOCK_PANEL_PIN + + +@pytest.fixture(autouse=True) +def httpx_mock_fixture(requests_mock): + """Configure httpx fixture.""" + with respx.mock(base_url=BASE_URL, assert_all_called=False) as respx_mock: + # Mock Login POST. + login_route = respx_mock.post(f"/{ENDPOINT_LOGIN}", name="login") + login_route.return_value = Response( + 200, json=json.loads(load_fixture("login.json", "elmax")) + ) + + # Mock Device list GET. + list_devices_route = respx_mock.get(f"/{ENDPOINT_DEVICES}", name="list_devices") + list_devices_route.return_value = Response( + 200, json=json.loads(load_fixture("list_devices.json", "elmax")) + ) + + # Mock Panel GET. + get_panel_route = respx_mock.get( + f"/{ENDPOINT_DISCOVERY}/{MOCK_PANEL_ID}/{MOCK_PANEL_PIN}", name="get_panel" + ) + get_panel_route.return_value = Response( + 200, json=json.loads(load_fixture("get_panel.json", "elmax")) + ) + + yield respx_mock diff --git a/tests/components/elmax/fixtures/get_panel.json b/tests/components/elmax/fixtures/get_panel.json new file mode 100644 index 00000000000..04fcfd48605 --- /dev/null +++ b/tests/components/elmax/fixtures/get_panel.json @@ -0,0 +1,126 @@ +{ + "release": 11.7, + "tappFeature": true, + "sceneFeature": true, + "zone": [ + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-zona-0", + "visibile": true, + "indice": 0, + "nome": "Feed zone 0", + "aperta": false, + "esclusa": false + }, + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-zona-1", + "visibile": true, + "indice": 1, + "nome": "Feed Zone 1", + "aperta": false, + "esclusa": false + }, + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-zona-2", + "visibile": true, + "indice": 2, + "nome": "Feed Zone 2", + "aperta": false, + "esclusa": false + } + ], + "uscite": [ + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-uscita-0", + "visibile": true, + "indice": 0, + "nome": "Actuator 0", + "aperta": false + }, + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-uscita-1", + "visibile": true, + "indice": 1, + "nome": "Actuator 1", + "aperta": false + }, + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-uscita-2", + "visibile": true, + "indice": 2, + "nome": "Actuator 2", + "aperta": true + } + ], + "aree": [ + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-area-0", + "visibile": true, + "indice": 0, + "nome": "AREA 0", + "statiDisponibili": [0, 1, 2, 3, 4], + "statiSessioneDisponibili": [0, 1, 2, 3], + "stato": 0, + "statoSessione": 0 + }, + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-area-1", + "visibile": true, + "indice": 1, + "nome": "AREA 1", + "statiDisponibili": [0, 1, 2, 3, 4], + "statiSessioneDisponibili": [0, 1, 2, 3], + "stato": 0, + "statoSessione": 0 + }, + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-area-2", + "visibile": false, + "indice": 2, + "nome": "AREA 2", + "statiDisponibili": [0, 1, 2, 3, 4], + "statiSessioneDisponibili": [0, 1, 2, 3], + "stato": 0, + "statoSessione": 0 + } + ], + "tapparelle": [ + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-tapparella-0", + "visibile": true, + "indice": 0, + "stato": "stop", + "posizione": 100, + "nome": "Cover 0" + } + ], + "gruppi": [ + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-gruppo-0", + "visibile": true, + "indice": 0, + "nome": "Group 0" + }, + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-gruppo-1", + "visibile": false, + "indice": 1, + "nome": "Group 1" + } + ], + "scenari": [ + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-scenario-0", + "visibile": true, + "indice": 0, + "nome": "Automation 0" + }, + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-scenario-2", + "visibile": true, + "indice": 2, + "nome": "Automation 2" + } + ], + "utente": "this.is@test.com", + "centrale": "2db3dae30b9102de4d078706f94d0708" +} \ No newline at end of file diff --git a/tests/components/elmax/fixtures/list_devices.json b/tests/components/elmax/fixtures/list_devices.json new file mode 100644 index 00000000000..19cb1c44ed9 --- /dev/null +++ b/tests/components/elmax/fixtures/list_devices.json @@ -0,0 +1,11 @@ +[ + { + "centrale_online": true, + "hash": "2db3dae30b9102de4d078706f94d0708", + "username": [{"name": "this.is@test.com", "label": "Test Panel Name"}] + },{ + "centrale_online": true, + "hash": "d8e8fca2dc0f896fd7cb4cb0031ba249", + "username": [{"name": "this.is@test.com", "label": "Test Panel Name"}] + } +] \ No newline at end of file diff --git a/tests/components/elmax/fixtures/login.json b/tests/components/elmax/fixtures/login.json new file mode 100644 index 00000000000..59f4aba559d --- /dev/null +++ b/tests/components/elmax/fixtures/login.json @@ -0,0 +1,8 @@ +{ + "token": "JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiIxYjExYmIxMWJiYjExMTExYjFiMTFiMWIiLCJlbWFpbCI6InRoaXMuaXNAdGVzdC5jb20iLCJyb2xlIjoidXNlciIsImlhdCI6MTYzNjE5OTk5OCwiZXhwIjoxNjM2MjM1OTk4fQ.1C7lXuKyX1HEGOfMxNwxJ2n-CjoW4rwvNRITQxLICv0", + "user": { + "_id": "1b11bb11bbb11111b1b11b1b", + "email": "this.is@test.com", + "role": "user" + } +} \ No newline at end of file diff --git a/tests/components/elmax/test_config_flow.py b/tests/components/elmax/test_config_flow.py new file mode 100644 index 00000000000..4584ab679f4 --- /dev/null +++ b/tests/components/elmax/test_config_flow.py @@ -0,0 +1,268 @@ +"""Tests for the Abode config flow.""" +from unittest.mock import patch + +from elmax_api.exceptions import ElmaxBadLoginError, ElmaxBadPinError, ElmaxNetworkError + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.elmax.const import ( + CONF_ELMAX_PANEL_ID, + CONF_ELMAX_PANEL_NAME, + CONF_ELMAX_PANEL_PIN, + CONF_ELMAX_PASSWORD, + CONF_ELMAX_USERNAME, + DOMAIN, +) +from homeassistant.config_entries import SOURCE_REAUTH +from homeassistant.data_entry_flow import FlowResult + +from tests.components.elmax import ( + MOCK_PANEL_ID, + MOCK_PANEL_NAME, + MOCK_PANEL_PIN, + MOCK_PASSWORD, + MOCK_USERNAME, +) + +CONF_POLLING = "polling" + + +def _has_error(errors): + return errors is not None and len(errors.keys()) > 0 + + +async def _bootstrap( + hass, + source=config_entries.SOURCE_USER, + username=MOCK_USERNAME, + password=MOCK_PASSWORD, + panel_name=MOCK_PANEL_NAME, + panel_pin=MOCK_PANEL_PIN, +) -> FlowResult: + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": source} + ) + if result["type"] != data_entry_flow.RESULT_TYPE_FORM or _has_error( + result["errors"] + ): + return result + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ELMAX_USERNAME: username, + CONF_ELMAX_PASSWORD: password, + }, + ) + if result2["type"] != data_entry_flow.RESULT_TYPE_FORM or _has_error( + result2["errors"] + ): + return result2 + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_ELMAX_PANEL_NAME: panel_name, + CONF_ELMAX_PANEL_PIN: panel_pin, + }, + ) + return result3 + + +async def _reauth(hass): + + # Trigger reauth + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH}, + data={ + CONF_ELMAX_PANEL_ID: MOCK_PANEL_ID, + CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, + CONF_ELMAX_USERNAME: MOCK_USERNAME, + CONF_ELMAX_PASSWORD: MOCK_PASSWORD, + }, + ) + if result2["type"] != data_entry_flow.RESULT_TYPE_FORM or _has_error( + result2["errors"] + ): + return result2 + + # Perform reauth confirm step + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_ELMAX_PANEL_ID: MOCK_PANEL_ID, + CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, + CONF_ELMAX_USERNAME: MOCK_USERNAME, + CONF_ELMAX_PASSWORD: MOCK_PASSWORD, + }, + ) + return result3 + + +async def test_show_form(hass): + """Test that the form is served with no input.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + +async def test_one_config_allowed(hass): + """Test that only one Elmax configuration is allowed for each panel.""" + # Setup once. + attempt1 = await _bootstrap(hass) + assert attempt1["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + # Attempt to add another instance of the integration for the very same panel, it must fail. + attempt2 = await _bootstrap(hass) + assert attempt2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert attempt2["reason"] == "already_configured" + + +async def test_invalid_credentials(hass): + """Test that invalid credentials throws an error.""" + with patch( + "elmax_api.http.Elmax.login", + side_effect=ElmaxBadLoginError(), + ): + result = await _bootstrap( + hass, username="wrong_user_name@email.com", password="incorrect_password" + ) + assert result["step_id"] == "user" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "bad_auth"} + + +async def test_connection_error(hass): + """Test other than invalid credentials throws an error.""" + with patch( + "elmax_api.http.Elmax.login", + side_effect=ElmaxNetworkError(), + ): + result = await _bootstrap( + hass, username="wrong_user_name@email.com", password="incorrect_password" + ) + assert result["step_id"] == "user" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "network_error"} + + +async def test_unhandled_error(hass): + """Test unhandled exceptions.""" + with patch( + "elmax_api.http.Elmax.get_panel_status", + side_effect=Exception(), + ): + result = await _bootstrap(hass) + assert result["step_id"] == "panels" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "unknown_error"} + + +async def test_invalid_pin(hass): + """Test error is thrown when a wrong pin is used to pair a panel.""" + # Simulate bad pin response. + with patch( + "elmax_api.http.Elmax.get_panel_status", + side_effect=ElmaxBadPinError(), + ): + result = await _bootstrap(hass) + assert result["step_id"] == "panels" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "invalid_pin"} + + +async def test_no_online_panel(hass): + """Test no-online panel is available.""" + # Simulate low-level api returns no panels. + with patch( + "elmax_api.http.Elmax.list_control_panels", + return_value=[], + ): + result = await _bootstrap(hass) + assert result["step_id"] == "user" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "no_panel_online"} + + +async def test_step_user(hass): + """Test that the user step works.""" + result = await _bootstrap(hass) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + CONF_ELMAX_PANEL_ID: MOCK_PANEL_ID, + CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, + CONF_ELMAX_USERNAME: MOCK_USERNAME, + CONF_ELMAX_PASSWORD: MOCK_PASSWORD, + } + + +async def test_show_reauth(hass): + """Test that the reauth form shows.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH}, + data={ + CONF_ELMAX_PANEL_ID: MOCK_PANEL_ID, + CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, + CONF_ELMAX_USERNAME: MOCK_USERNAME, + CONF_ELMAX_PASSWORD: MOCK_PASSWORD, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" + + +async def test_reauth_flow(hass): + """Test that the reauth flow works.""" + # Simulate a first setup + await _bootstrap(hass) + # Trigger reauth + result = await _reauth(hass) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "reauth_successful" + + +async def test_reauth_panel_disappeared(hass): + """Test that the case where panel is no longer associated with the user.""" + # Simulate a first setup + await _bootstrap(hass) + # Trigger reauth + with patch( + "elmax_api.http.Elmax.list_control_panels", + return_value=[], + ): + result = await _reauth(hass) + assert result["step_id"] == "reauth_confirm" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "reauth_panel_disappeared"} + + +async def test_reauth_invalid_pin(hass): + """Test that the case where panel is no longer associated with the user.""" + # Simulate a first setup + await _bootstrap(hass) + # Trigger reauth + with patch( + "elmax_api.http.Elmax.get_panel_status", + side_effect=ElmaxBadPinError(), + ): + result = await _reauth(hass) + assert result["step_id"] == "reauth_confirm" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "invalid_pin"} + + +async def test_reauth_bad_login(hass): + """Test bad login attempt at reauth time.""" + # Simulate a first setup + await _bootstrap(hass) + # Trigger reauth + with patch( + "elmax_api.http.Elmax.login", + side_effect=ElmaxBadLoginError(), + ): + result = await _reauth(hass) + assert result["step_id"] == "reauth_confirm" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "bad_auth"} From 1768b19f71f097a68f4b2794fe4b87dfb538ac21 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 7 Dec 2021 18:38:34 -0500 Subject: [PATCH 0119/2644] Bump ZHA dependency zigpy-znp from 0.6.3 to 0.6.4 (#61194) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index aa167fc2df4..daeb90be801 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -12,7 +12,7 @@ "zigpy==0.42.0", "zigpy-xbee==0.14.0", "zigpy-zigate==0.7.3", - "zigpy-znp==0.6.3" + "zigpy-znp==0.6.4" ], "usb": [ {"vid":"10C4","pid":"EA60","description":"*2652*","known_devices":["slae.sh cc2652rb stick"]}, diff --git a/requirements_all.txt b/requirements_all.txt index 6e8d30ddb65..59daf1e74c7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2510,7 +2510,7 @@ zigpy-xbee==0.14.0 zigpy-zigate==0.7.3 # homeassistant.components.zha -zigpy-znp==0.6.3 +zigpy-znp==0.6.4 # homeassistant.components.zha zigpy==0.42.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 978239ef499..1021a529fca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1497,7 +1497,7 @@ zigpy-xbee==0.14.0 zigpy-zigate==0.7.3 # homeassistant.components.zha -zigpy-znp==0.6.3 +zigpy-znp==0.6.4 # homeassistant.components.zha zigpy==0.42.0 From d69c6e3ab3c199ac1eb76ccf1cb55d6c8d83030b Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 8 Dec 2021 00:13:50 +0000 Subject: [PATCH 0120/2644] [ci skip] Translation update --- .../alarm_control_panel/translations/fr.json | 1 + .../components/apple_tv/translations/he.json | 2 + .../components/apple_tv/translations/id.json | 23 ++++++-- .../components/apple_tv/translations/ja.json | 13 +++++ .../components/apple_tv/translations/no.json | 23 ++++++-- .../components/apple_tv/translations/pl.json | 7 +++ .../aseko_pool_live/translations/he.json | 20 +++++++ .../aseko_pool_live/translations/id.json | 20 +++++++ .../aseko_pool_live/translations/no.json | 20 +++++++ .../binary_sensor/translations/id.json | 2 + .../binary_sensor/translations/no.json | 2 + .../components/elmax/translations/en.json | 15 +----- .../enphase_envoy/translations/id.json | 3 +- .../enphase_envoy/translations/ja.json | 3 +- .../enphase_envoy/translations/no.json | 3 +- .../components/fronius/translations/he.json | 4 +- .../components/fronius/translations/id.json | 7 ++- .../components/fronius/translations/no.json | 7 ++- .../components/knx/translations/id.json | 2 + .../components/knx/translations/no.json | 2 + .../components/knx/translations/pl.json | 1 + .../components/nest/translations/pl.json | 6 +++ .../components/nina/translations/he.json | 11 ++++ .../components/nina/translations/id.json | 27 ++++++++++ .../components/nina/translations/no.json | 27 ++++++++++ .../components/nina/translations/pl.json | 19 +++++++ .../components/powerwall/translations/ja.json | 1 + .../components/sentry/translations/ja.json | 1 + .../components/toon/translations/ja.json | 1 + .../tuya/translations/select.pl.json | 3 ++ .../wolflink/translations/sensor.ja.json | 5 ++ .../translations/select.bg.json | 33 ++++++++++++ .../translations/select.id.json | 52 +++++++++++++++++++ .../translations/select.ja.json | 52 +++++++++++++++++++ .../translations/select.no.json | 52 +++++++++++++++++++ .../translations/select.pl.json | 36 +++++++++++++ .../translations/select.ru.json | 52 +++++++++++++++++++ .../components/zha/translations/ja.json | 6 ++- .../components/zwave_js/translations/id.json | 2 + .../components/zwave_js/translations/ja.json | 2 + .../components/zwave_js/translations/no.json | 2 + 41 files changed, 540 insertions(+), 30 deletions(-) create mode 100644 homeassistant/components/aseko_pool_live/translations/he.json create mode 100644 homeassistant/components/aseko_pool_live/translations/id.json create mode 100644 homeassistant/components/aseko_pool_live/translations/no.json create mode 100644 homeassistant/components/nina/translations/he.json create mode 100644 homeassistant/components/nina/translations/id.json create mode 100644 homeassistant/components/nina/translations/no.json create mode 100644 homeassistant/components/nina/translations/pl.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/select.bg.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/select.id.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/select.ja.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/select.no.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/select.pl.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/select.ru.json diff --git a/homeassistant/components/alarm_control_panel/translations/fr.json b/homeassistant/components/alarm_control_panel/translations/fr.json index bbcb26f7184..277233bdfb4 100644 --- a/homeassistant/components/alarm_control_panel/translations/fr.json +++ b/homeassistant/components/alarm_control_panel/translations/fr.json @@ -20,6 +20,7 @@ "armed_away": "Armer {entity_name} en mode \"sortie\"", "armed_home": "Armer {entity_name} en mode \"maison\"", "armed_night": "Armer {entity_name} en mode \"nuit\"", + "armed_vacation": "{entity_name} vacances arm\u00e9es", "disarmed": "{entity_name} d\u00e9sarm\u00e9", "triggered": "{entity_name} d\u00e9clench\u00e9" } diff --git a/homeassistant/components/apple_tv/translations/he.json b/homeassistant/components/apple_tv/translations/he.json index 81e13ec1878..209cd7069f0 100644 --- a/homeassistant/components/apple_tv/translations/he.json +++ b/homeassistant/components/apple_tv/translations/he.json @@ -1,9 +1,11 @@ { "config": { "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_configured_device": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "error": { diff --git a/homeassistant/components/apple_tv/translations/id.json b/homeassistant/components/apple_tv/translations/id.json index 443a0dd49f1..8a978eca737 100644 --- a/homeassistant/components/apple_tv/translations/id.json +++ b/homeassistant/components/apple_tv/translations/id.json @@ -1,12 +1,17 @@ { "config": { "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", "already_configured_device": "Perangkat sudah dikonfigurasi", "already_in_progress": "Alur konfigurasi sedang berlangsung", "backoff": "Perangkat tidak bisa menerima permintaan pemasangan saat ini (Anda mungkin telah berulang kali memasukkan kode PIN yang salah). Coba lagi nanti.", "device_did_not_pair": "Tidak ada upaya untuk menyelesaikan proses pemasangan dari sisi perangkat.", + "device_not_found": "Perangkat tidak ditemukan selama penemuan, coba tambahkan lagi.", + "inconsistent_device": "Protokol yang diharapkan tidak ditemukan selama penemuan. Ini biasanya terjadi karena masalah dengan DNS multicast (Zeroconf). Coba tambahkan perangkat lagi.", "invalid_config": "Konfigurasi untuk perangkat ini tidak lengkap. Coba tambahkan lagi.", "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "reauth_successful": "Autentikasi ulang berhasil", + "setup_failed": "Gagal menyiapkan perangkat.", "unknown": "Kesalahan yang tidak diharapkan" }, "error": { @@ -16,14 +21,14 @@ "no_usable_service": "Perangkat ditemukan tetapi kami tidak dapat mengidentifikasi berbagai cara untuk membuat koneksi ke perangkat tersebut. Jika Anda terus melihat pesan ini, coba tentukan alamat IP-nya atau mulai ulang Apple TV Anda.", "unknown": "Kesalahan yang tidak diharapkan" }, - "flow_title": "{name}", + "flow_title": "{name} ({type})", "step": { "confirm": { - "description": "Anda akan menambahkan Apple TV bernama `{name}` ke Home Assistant.\n\n** Untuk menyelesaikan proses, Anda mungkin harus memasukkan kode PIN beberapa kali.**\n\nPerhatikan bahwa Anda *tidak* akan dapat mematikan Apple TV dengan integrasi ini. Hanya pemutar media di Home Assistant yang akan dimatikan!", + "description": "Anda akan menambahkan Apple TV bernama `{name}` dengan jenis `{type}` ke Home Assistant.\n\n** Untuk menyelesaikan proses, Anda mungkin harus memasukkan kode PIN beberapa kali.**\n\nPerhatikan bahwa Anda *tidak* akan dapat mematikan Apple TV dengan integrasi ini. Hanya pemutar media di Home Assistant yang akan dimatikan!", "title": "Konfirmasikan menambahkan Apple TV" }, "pair_no_pin": { - "description": "Pemasangan diperlukan untuk layanan `{protocol}`. Masukkan PIN {pin} di Apple TV untuk melanjutkan.", + "description": "Pemasangan diperlukan untuk layanan `{protocol}`. Masukkan PIN {pin} di perangkat Anda untuk melanjutkan.", "title": "Pemasangan" }, "pair_with_pin": { @@ -33,8 +38,16 @@ "description": "Pemasangan diperlukan untuk protokol `{protocol}`. Masukkan kode PIN yang ditampilkan pada layar. Angka nol di awal harus dihilangkan. Misalnya, masukkan 123 jika kode yang ditampilkan adalah 0123.", "title": "Pemasangan" }, + "password": { + "description": "Kata sandi diperlukan oleh `{protocol}`. Ini belum didukung, nonaktifkan kata sandi untuk melanjutkan.", + "title": "Kata sandi diperlukan" + }, + "protocol_disabled": { + "description": "Pemasangan diperlukan untuk `{protocol}` tetapi dinonaktifkan pada perangkat. Tinjau kemungkinan pembatasan akses (misalnya izinkan semua perangkat di jaringan lokal untuk terhubung) pada perangkat.\n\nAnda dapat melanjutkan tanpa memasangkan protokol ini, tetapi beberapa fungsi akan terbatas.", + "title": "Pemasangan tidak dimungkinkan" + }, "reconfigure": { - "description": "Apple TV ini mengalami masalah koneksi dan harus dikonfigurasi ulang.", + "description": "Konfigurasi ulang perangkat ini untuk memulihkan fungsinya.", "title": "Konfigurasi ulang perangkat" }, "service_problem": { @@ -45,7 +58,7 @@ "data": { "device_input": "Perangkat" }, - "description": "Mulai dengan memasukkan nama perangkat (misalnya Dapur atau Kamar Tidur) atau alamat IP Apple TV yang ingin ditambahkan. Jika ada perangkat yang ditemukan secara otomatis di jaringan Anda, perangkat tersebut akan ditampilkan di bawah ini.\n\nJika Anda tidak dapat melihat perangkat atau mengalami masalah, coba tentukan alamat IP perangkat.\n\n{devices}", + "description": "Mulai dengan memasukkan nama perangkat (misalnya Dapur atau Kamar Tidur) atau alamat IP Apple TV yang ingin ditambahkan. \n\nJika Anda tidak dapat melihat perangkat atau mengalami masalah, coba tentukan alamat IP perangkat.", "title": "Siapkan Apple TV baru" } } diff --git a/homeassistant/components/apple_tv/translations/ja.json b/homeassistant/components/apple_tv/translations/ja.json index a73cb4cbbd6..c70dda18d01 100644 --- a/homeassistant/components/apple_tv/translations/ja.json +++ b/homeassistant/components/apple_tv/translations/ja.json @@ -1,12 +1,17 @@ { "config": { "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "already_configured_device": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", "backoff": "\u73fe\u5728\u3001\u30c7\u30d0\u30a4\u30b9\u306f\u30da\u30a2\u30ea\u30f3\u30b0\u8981\u6c42\u3092\u53d7\u3051\u4ed8\u3051\u3066\u3044\u307e\u305b\u3093(\u7121\u52b9\u306aPIN\u30b3\u30fc\u30c9\u3092\u4f55\u5ea6\u3082\u5165\u529b\u3057\u305f\u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059)\u3001\u5f8c\u3067\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "device_did_not_pair": "\u30c7\u30d0\u30a4\u30b9\u304b\u3089\u30da\u30a2\u30ea\u30f3\u30b0\u30d7\u30ed\u30bb\u30b9\u3092\u7d42\u4e86\u3059\u308b\u8a66\u307f\u306f\u884c\u308f\u308c\u307e\u305b\u3093\u3067\u3057\u305f\u3002", + "device_not_found": "\u691c\u51fa\u4e2d\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u3082\u3046\u4e00\u5ea6\u8ffd\u52a0\u3057\u3066\u307f\u3066\u304f\u3060\u3055\u3044\u3002", + "inconsistent_device": "\u691c\u51fa\u4e2d\u306b\u671f\u5f85\u3057\u305f\u30d7\u30ed\u30c8\u30b3\u30eb\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u3053\u308c\u306f\u901a\u5e38\u3001\u30de\u30eb\u30c1\u30ad\u30e3\u30b9\u30c8DNS(Zeroconf)\u306b\u554f\u984c\u304c\u3042\u308b\u3053\u3068\u3092\u793a\u3057\u3066\u3044\u307e\u3059\u3002\u30c7\u30d0\u30a4\u30b9\u3092\u3082\u3046\u4e00\u5ea6\u8ffd\u52a0\u3057\u3066\u307f\u3066\u304f\u3060\u3055\u3044\u3002", "invalid_config": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306e\u8a2d\u5b9a\u306f\u4e0d\u5b8c\u5168\u3067\u3059\u3002\u3082\u3046\u4e00\u5ea6\u8ffd\u52a0\u3057\u3066\u307f\u3066\u304f\u3060\u3055\u3044\u3002", "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", + "setup_failed": "\u30c7\u30d0\u30a4\u30b9\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { @@ -33,6 +38,14 @@ "description": "`{protocol}` \u30d7\u30ed\u30c8\u30b3\u30eb\u306b\u306f\u30da\u30a2\u30ea\u30f3\u30b0\u304c\u5fc5\u8981\u3067\u3059\u3002\u753b\u9762\u306b\u8868\u793a\u3055\u308c\u3066\u3044\u308bPIN\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u306a\u304a\u5148\u982d\u306e\u30bc\u30ed\u306f\u7701\u7565\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u3064\u307e\u308a\u3001\u8868\u793a\u3055\u308c\u308b\u30b3\u30fc\u30c9\u304c0123\u306e\u5834\u5408\u306f123\u3068\u5165\u529b\u3057\u307e\u3059\u3002", "title": "\u30da\u30a2\u30ea\u30f3\u30b0" }, + "password": { + "description": "`{protocol}` \u306b\u306f\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u5fc5\u8981\u3067\u3059\u3002\u3053\u308c\u306f\u307e\u3060\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u7d9a\u884c\u3059\u308b\u306b\u306f\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u7121\u52b9\u306b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u5fc5\u8981" + }, + "protocol_disabled": { + "description": "`{protocol}` \u306b\u306f\u30da\u30a2\u30ea\u30f3\u30b0\u3092\u5fc5\u8981\u3068\u3057\u307e\u3059\u304c\u3001\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u7121\u52b9\u306b\u306a\u3063\u3066\u3044\u307e\u3059\u3002\u6a5f\u5668\u5074\u306e\u30a2\u30af\u30bb\u30b9\u5236\u9650(\u4f8b: \u30ed\u30fc\u30ab\u30eb\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306e\u3059\u3079\u3066\u306e\u6a5f\u5668\u306e\u63a5\u7d9a\u3092\u8a31\u53ef\u3059\u308b)\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\n\u3053\u306e\u30d7\u30ed\u30c8\u30b3\u30eb\u306f\u30da\u30a2\u30ea\u30f3\u30b0\u305b\u305a\u306b\u7d9a\u884c\u3067\u304d\u307e\u3059\u304c\u3001\u4e00\u90e8\u306e\u6a5f\u80fd\u304c\u5236\u9650\u3055\u308c\u307e\u3059\u3002", + "title": "\u30da\u30a2\u30ea\u30f3\u30b0\u3067\u304d\u307e\u305b\u3093" + }, "reconfigure": { "description": "\u3053\u306eApple TV\u306b\u306f\u63a5\u7d9a\u969c\u5bb3\u304c\u767a\u751f\u3057\u3066\u3044\u308b\u305f\u3081\u3001\u518d\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", "title": "\u30c7\u30d0\u30a4\u30b9\u306e\u518d\u69cb\u6210" diff --git a/homeassistant/components/apple_tv/translations/no.json b/homeassistant/components/apple_tv/translations/no.json index 993f7708367..4821fb2128d 100644 --- a/homeassistant/components/apple_tv/translations/no.json +++ b/homeassistant/components/apple_tv/translations/no.json @@ -1,12 +1,17 @@ { "config": { "abort": { + "already_configured": "Enheten er allerede konfigurert", "already_configured_device": "Enheten er allerede konfigurert", "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", "backoff": "Enheten godtar ikke parringsanmodninger for \u00f8yeblikket (du har kanskje angitt en ugyldig PIN-kode for mange ganger), pr\u00f8v igjen senere.", "device_did_not_pair": "Ingen fors\u00f8k p\u00e5 \u00e5 fullf\u00f8re paringsprosessen ble gjort fra enheten", + "device_not_found": "Enheten ble ikke funnet under oppdagelsen. Pr\u00f8v \u00e5 legge den til p\u00e5 nytt.", + "inconsistent_device": "Forventede protokoller ble ikke funnet under oppdagelsen. Dette indikerer vanligvis et problem med multicast DNS (Zeroconf). Pr\u00f8v \u00e5 legge til enheten p\u00e5 nytt.", "invalid_config": "Konfigurasjonen for denne enheten er ufullstendig. Pr\u00f8v \u00e5 legge den til p\u00e5 nytt.", "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "setup_failed": "Kunne ikke konfigurere enheten.", "unknown": "Uventet feil" }, "error": { @@ -16,14 +21,14 @@ "no_usable_service": "En enhet ble funnet, men kunne ikke identifisere noen m\u00e5te \u00e5 etablere en tilkobling til den. Hvis du fortsetter \u00e5 se denne meldingen, kan du pr\u00f8ve \u00e5 angi IP-adressen eller starte Apple TV p\u00e5 nytt.", "unknown": "Uventet feil" }, - "flow_title": "{name}", + "flow_title": "{name} ( {type} )", "step": { "confirm": { - "description": "Du er i ferd med \u00e5 legge til Apple TV med navnet {name} i Home Assistant.\n\n**For \u00e5 fullf\u00f8re prosessen m\u00e5 du kanskje angi flere PIN-koder.**\n\nV\u00e6r oppmerksom p\u00e5 at du *ikke* kan sl\u00e5 av Apple TV med denne integreringen. Bare mediespilleren i Home Assistant sl\u00e5r seg av!", + "description": "Du er i ferd med \u00e5 legge til ` {name} ` av typen ` {type} ` til Home Assistant. \n\n **For \u00e5 fullf\u00f8re prosessen m\u00e5 du kanskje angi flere PIN-koder.** \n\n V\u00e6r oppmerksom p\u00e5 at du *ikke* vil kunne sl\u00e5 av Apple TV med denne integrasjonen. Bare mediespilleren i Home Assistant vil sl\u00e5 seg av!", "title": "Bekreft at du legger til Apple TV" }, "pair_no_pin": { - "description": "Paring kreves for tjenesten {protocol}. Skriv inn PIN-koden {pin} p\u00e5 Apple TV for \u00e5 fortsette.", + "description": "Paring er n\u00f8dvendig for tjenesten ` {protocol} `. Skriv inn PIN-koden {pin} p\u00e5 enheten din for \u00e5 fortsette.", "title": "Sammenkobling" }, "pair_with_pin": { @@ -33,8 +38,16 @@ "description": "Paring kreves for protokollen {protocol}. Skriv inn PIN-koden som vises p\u00e5 skjermen. Ledende nuller utelates, det vil si angi 123 hvis den viste koden er 0123.", "title": "Sammenkobling" }, + "password": { + "description": "Et passord kreves av ` {protocol} `. Dette st\u00f8ttes ikke enn\u00e5. Deaktiver passordet for \u00e5 fortsette.", + "title": "Passord kreves" + }, + "protocol_disabled": { + "description": "Paring er n\u00f8dvendig for ` {protocol} `, men den er deaktivert p\u00e5 enheten. Se gjennom potensielle tilgangsbegrensninger (f.eks. la alle enheter p\u00e5 det lokale nettverket koble seg til) p\u00e5 enheten. \n\n Du kan fortsette uten \u00e5 pare denne protokollen, men noe funksjonalitet vil v\u00e6re begrenset.", + "title": "Sammenkobling ikke mulig" + }, "reconfigure": { - "description": "Denne Apple TVen har noen tilkoblingsvansker og m\u00e5 konfigureres p\u00e5 nytt", + "description": "Konfigurer denne enheten p\u00e5 nytt for \u00e5 gjenopprette funksjonaliteten.", "title": "Omkonfigurering av enheter" }, "service_problem": { @@ -45,7 +58,7 @@ "data": { "device_input": "Enhet" }, - "description": "Start med \u00e5 skrive inn enhetsnavnet (f.eks. kj\u00f8kken eller soverom) eller IP-adressen til Apple TV-en du vil legge til. Hvis noen enheter ble funnet automatisk p\u00e5 nettverket ditt, vises de nedenfor.\n\nHvis du ikke kan se enheten eller oppleve problemer, kan du pr\u00f8ve \u00e5 angi enhetens IP-adresse.\n\n{devices}", + "description": "Start med \u00e5 skrive inn enhetsnavnet (f.eks. Kj\u00f8kken eller soverom) eller IP-adressen til Apple TV-en du vil legge til. \n\n Hvis du ikke kan se enheten eller opplever problemer, pr\u00f8v \u00e5 spesifisere enhetens IP-adresse.", "title": "Konfigurere en ny Apple TV" } } diff --git a/homeassistant/components/apple_tv/translations/pl.json b/homeassistant/components/apple_tv/translations/pl.json index 48de231527e..7936f1ccea6 100644 --- a/homeassistant/components/apple_tv/translations/pl.json +++ b/homeassistant/components/apple_tv/translations/pl.json @@ -7,6 +7,7 @@ "device_did_not_pair": "Nie podj\u0119to pr\u00f3by zako\u0144czenia procesu parowania z urz\u0105dzenia.", "invalid_config": "Konfiguracja tego urz\u0105dzenia jest niekompletna. Spr\u00f3buj doda\u0107 go ponownie.", "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", + "setup_failed": "Nie uda\u0142o si\u0119 skonfigurowa\u0107 urz\u0105dzenia.", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "error": { @@ -33,6 +34,12 @@ "description": "Parowanie jest wymagane dla protoko\u0142u \"{protocol}\". Wprowad\u017a kod PIN wy\u015bwietlony na ekranie. Zera poprzedzaj\u0105ce nale\u017cy pomin\u0105\u0107, tj. wpisa\u0107 123, zamiast 0123.", "title": "Parowanie" }, + "password": { + "title": "Wymagane has\u0142o" + }, + "protocol_disabled": { + "title": "Brak mo\u017cliwo\u015bci sparowania" + }, "reconfigure": { "description": "Ten Apple TV ma pewne problemy z po\u0142\u0105czeniem i musi zosta\u0107 ponownie skonfigurowany.", "title": "Ponowna konfiguracja urz\u0105dzenia" diff --git a/homeassistant/components/aseko_pool_live/translations/he.json b/homeassistant/components/aseko_pool_live/translations/he.json new file mode 100644 index 00000000000..2b083313602 --- /dev/null +++ b/homeassistant/components/aseko_pool_live/translations/he.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "email": "\u05d3\u05d5\u05d0\"\u05dc", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aseko_pool_live/translations/id.json b/homeassistant/components/aseko_pool_live/translations/id.json new file mode 100644 index 00000000000..b3d46b4e412 --- /dev/null +++ b/homeassistant/components/aseko_pool_live/translations/id.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "Kata Sandi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aseko_pool_live/translations/no.json b/homeassistant/components/aseko_pool_live/translations/no.json new file mode 100644 index 00000000000..8c08ab0c561 --- /dev/null +++ b/homeassistant/components/aseko_pool_live/translations/no.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "email": "E-post", + "password": "Passord" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/id.json b/homeassistant/components/binary_sensor/translations/id.json index b9688d494db..3f678149694 100644 --- a/homeassistant/components/binary_sensor/translations/id.json +++ b/homeassistant/components/binary_sensor/translations/id.json @@ -84,6 +84,7 @@ "not_powered": "{entity_name} tidak ditenagai", "not_present": "{entity_name} tidak ada", "not_running": "{entity_name} tidak lagi berjalan", + "not_tampered": "{entity_name} berhenti mendeteksi gangguan", "not_unsafe": "{entity_name} menjadi aman", "occupied": "{entity_name} menjadi ditempati", "opened": "{entity_name} terbuka", @@ -94,6 +95,7 @@ "running": "{entity_name} mulai berjalan", "smoke": "{entity_name} mulai mendeteksi asap", "sound": "{entity_name} mulai mendeteksi suara", + "tampered": "{entity_name} mulai mendeteksi gangguan", "turned_off": "{entity_name} dimatikan", "turned_on": "{entity_name} dinyalakan", "unsafe": "{entity_name} menjadi tidak aman", diff --git a/homeassistant/components/binary_sensor/translations/no.json b/homeassistant/components/binary_sensor/translations/no.json index da2fea944c4..a9bdf88d930 100644 --- a/homeassistant/components/binary_sensor/translations/no.json +++ b/homeassistant/components/binary_sensor/translations/no.json @@ -84,6 +84,7 @@ "not_powered": "{entity_name} spenningsl\u00f8s", "not_present": "{entity_name} ikke til stede", "not_running": "{entity_name} kj\u00f8rer ikke lenger", + "not_tampered": "{entity_name} sluttet \u00e5 oppdage manipulering", "not_unsafe": "{entity_name} ble trygg", "occupied": "{entity_name} ble opptatt", "opened": "{entity_name} \u00e5pnet", @@ -94,6 +95,7 @@ "running": "{entity_name} begynte \u00e5 kj\u00f8re", "smoke": "{entity_name} begynte \u00e5 registrere r\u00f8yk", "sound": "{entity_name} begynte \u00e5 registrere lyd", + "tampered": "{entity_name} begynte \u00e5 oppdage manipulering", "turned_off": "{entity_name} sl\u00e5tt av", "turned_on": "{entity_name} sl\u00e5tt p\u00e5", "unsafe": "{entity_name} ble usikker", diff --git a/homeassistant/components/elmax/translations/en.json b/homeassistant/components/elmax/translations/en.json index e49e57a4ce6..b3de51d64fc 100644 --- a/homeassistant/components/elmax/translations/en.json +++ b/homeassistant/components/elmax/translations/en.json @@ -1,15 +1,14 @@ { "config": { "abort": { - "single_instance_allowed": "There already is an integration for that Elmaxc panel." + "already_configured": "Device is already configured" }, "error": { "bad_auth": "Invalid authentication", "invalid_pin": "The provided pin is invalid", "network_error": "A network error occurred", "no_panel_online": "No online Elmax control panel was found.", - "unknown_error": "An unexpected error occurred", - "reauth_panel_disappeared": "The panel is no longer associated to your account." + "unknown_error": "An unexpected error occurred" }, "step": { "panels": { @@ -21,16 +20,6 @@ "description": "Select which panel you would like to control with this integration. Please note that the panel must be ON in order to be configured.", "title": "Panel selection" }, - "reauth_confirm": { - "data": { - "username": "Username", - "password": "Password", - "panel_id": "Panel ID", - "panel_pin": "PIN Code" - }, - "description": "Please authenticate again to the Elmax cloud.", - "title": "Re-Authenticate" - }, "user": { "data": { "password": "Password", diff --git a/homeassistant/components/enphase_envoy/translations/id.json b/homeassistant/components/enphase_envoy/translations/id.json index 31c8251820d..71b453b2552 100644 --- a/homeassistant/components/enphase_envoy/translations/id.json +++ b/homeassistant/components/enphase_envoy/translations/id.json @@ -16,7 +16,8 @@ "host": "Host", "password": "Kata Sandi", "username": "Nama Pengguna" - } + }, + "description": "Untuk model yang lebih baru, masukkan nama pengguna `envoy` tanpa kata sandi. Untuk model yang lebih lama, masukkan nama pengguna `installer` tanpa kata sandi. Untuk semua model lainnya, masukkan nama pengguna dan kata sandi yang valid." } } } diff --git a/homeassistant/components/enphase_envoy/translations/ja.json b/homeassistant/components/enphase_envoy/translations/ja.json index e14d0fe713b..2cfb00ff1fa 100644 --- a/homeassistant/components/enphase_envoy/translations/ja.json +++ b/homeassistant/components/enphase_envoy/translations/ja.json @@ -16,7 +16,8 @@ "host": "\u30db\u30b9\u30c8", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" - } + }, + "description": "\u65b0\u3057\u3044\u30e2\u30c7\u30eb\u306e\u5834\u5408\u306f\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u306a\u3057\u3067\u30e6\u30fc\u30b6\u30fc\u540d `envoy` \u3092\u5165\u529b\u3057\u307e\u3059\u3002\u53e4\u3044\u30e2\u30c7\u30eb\u306e\u5834\u5408\u306f\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u306a\u3057\u3067\u30e6\u30fc\u30b6\u30fc\u540d `installer` \u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u4ed6\u306e\u3059\u3079\u3066\u306e\u30e2\u30c7\u30eb\u3067\u306f\u3001\u6709\u52b9\u306a\u30e6\u30fc\u30b6\u30fc\u540d\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u307e\u3059\u3002" } } } diff --git a/homeassistant/components/enphase_envoy/translations/no.json b/homeassistant/components/enphase_envoy/translations/no.json index 9422d97056a..091d76d55ec 100644 --- a/homeassistant/components/enphase_envoy/translations/no.json +++ b/homeassistant/components/enphase_envoy/translations/no.json @@ -16,7 +16,8 @@ "host": "Vert", "password": "Passord", "username": "Brukernavn" - } + }, + "description": "For nyere modeller, skriv inn brukernavnet \"envoy\" uten passord. For eldre modeller, skriv inn brukernavnet `installer` uten passord. For alle andre modeller, skriv inn et gyldig brukernavn og passord." } } } diff --git a/homeassistant/components/fronius/translations/he.json b/homeassistant/components/fronius/translations/he.json index 1699e0f8e19..76241415389 100644 --- a/homeassistant/components/fronius/translations/he.json +++ b/homeassistant/components/fronius/translations/he.json @@ -1,12 +1,14 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "invalid_host": "\u05e9\u05dd \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea IP \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, + "flow_title": "{device}", "step": { "user": { "data": { diff --git a/homeassistant/components/fronius/translations/id.json b/homeassistant/components/fronius/translations/id.json index 538dcc33ee8..49fc8cac104 100644 --- a/homeassistant/components/fronius/translations/id.json +++ b/homeassistant/components/fronius/translations/id.json @@ -1,13 +1,18 @@ { "config": { "abort": { - "already_configured": "Perangkat sudah dikonfigurasi" + "already_configured": "Perangkat sudah dikonfigurasi", + "invalid_host": "Nama host atau alamat IP tidak valid" }, "error": { "cannot_connect": "Gagal terhubung", "unknown": "Kesalahan yang tidak diharapkan" }, + "flow_title": "{device}", "step": { + "confirm_discovery": { + "description": "Ingin menambahkan {device} to Home Assistant?" + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/fronius/translations/no.json b/homeassistant/components/fronius/translations/no.json index 8a44a9ff8af..d934909a2fa 100644 --- a/homeassistant/components/fronius/translations/no.json +++ b/homeassistant/components/fronius/translations/no.json @@ -1,13 +1,18 @@ { "config": { "abort": { - "already_configured": "Enheten er allerede konfigurert" + "already_configured": "Enheten er allerede konfigurert", + "invalid_host": "Ugyldig vertsnavn eller IP-adresse" }, "error": { "cannot_connect": "Tilkobling mislyktes", "unknown": "Uventet feil" }, + "flow_title": "{device}", "step": { + "confirm_discovery": { + "description": "Vil du legge til {device} i Home Assistant?" + }, "user": { "data": { "host": "Vert" diff --git a/homeassistant/components/knx/translations/id.json b/homeassistant/components/knx/translations/id.json index cfab50507ae..8c73be7fdb0 100644 --- a/homeassistant/components/knx/translations/id.json +++ b/homeassistant/components/knx/translations/id.json @@ -12,6 +12,7 @@ "data": { "host": "Host", "individual_address": "Alamat individu untuk koneksi", + "local_ip": "IP lokal (kosongkan jika tidak yakin)", "port": "Port", "route_back": "Dirutekan Kembali/Mode NAT" }, @@ -54,6 +55,7 @@ "tunnel": { "data": { "host": "Host", + "local_ip": "IP lokal (kosongkan jika tidak yakin)", "port": "Port", "route_back": "Dirutekan Kembali/Mode NAT" } diff --git a/homeassistant/components/knx/translations/no.json b/homeassistant/components/knx/translations/no.json index 223dd66402a..75a13da35c0 100644 --- a/homeassistant/components/knx/translations/no.json +++ b/homeassistant/components/knx/translations/no.json @@ -12,6 +12,7 @@ "data": { "host": "Vert", "individual_address": "Individuell adresse for tilkoblingen", + "local_ip": "Lokal IP (la st\u00e5 tomt hvis du er usikker)", "port": "Port", "route_back": "Rute tilbake / NAT-modus" }, @@ -54,6 +55,7 @@ "tunnel": { "data": { "host": "Vert", + "local_ip": "Lokal IP (la st\u00e5 tomt hvis du er usikker)", "port": "Port", "route_back": "Rute tilbake / NAT-modus" } diff --git a/homeassistant/components/knx/translations/pl.json b/homeassistant/components/knx/translations/pl.json index c987e2cc937..9f8c851b5d1 100644 --- a/homeassistant/components/knx/translations/pl.json +++ b/homeassistant/components/knx/translations/pl.json @@ -54,6 +54,7 @@ "tunnel": { "data": { "host": "Nazwa hosta lub adres IP", + "local_ip": "Lokalny adres IP (pozostaw pusty, je\u015bli nie masz pewno\u015bci)", "port": "Port", "route_back": "Tryb Route Back / NAT" } diff --git a/homeassistant/components/nest/translations/pl.json b/homeassistant/components/nest/translations/pl.json index 23ac1d4f28f..7658b9dfc64 100644 --- a/homeassistant/components/nest/translations/pl.json +++ b/homeassistant/components/nest/translations/pl.json @@ -42,6 +42,12 @@ "pick_implementation": { "title": "Wybierz metod\u0119 uwierzytelniania" }, + "pubsub": { + "data": { + "cloud_project_id": "Identyfikator projektu Google Cloud" + }, + "title": "Skonfiguruj Google Cloud" + }, "reauth_confirm": { "description": "Integracja Nest wymaga ponownego uwierzytelnienia Twojego konta", "title": "Ponownie uwierzytelnij integracj\u0119" diff --git a/homeassistant/components/nina/translations/he.json b/homeassistant/components/nina/translations/he.json new file mode 100644 index 00000000000..22681442909 --- /dev/null +++ b/homeassistant/components/nina/translations/he.json @@ -0,0 +1,11 @@ +{ + "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": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nina/translations/id.json b/homeassistant/components/nina/translations/id.json new file mode 100644 index 00000000000..03f6feecc22 --- /dev/null +++ b/homeassistant/components/nina/translations/id.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "error": { + "cannot_connect": "Gagal terhubung", + "no_selection": "Pilih setidaknya satu kota/kabupaten", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "_a_to_d": "Kota/kabupaten (A-D)", + "_e_to_h": "Kota/kabupaten (E-H)", + "_i_to_l": "Kota/kabupaten (I-L)", + "_m_to_q": "Kota/kabupaten (M-Q)", + "_r_to_u": "Kota/kabupaten (R-U)", + "_v_to_z": "Kota/kabupaten (V-Z)", + "corona_filter": "Hapus Peringatan Corona", + "slots": "Peringatan maksimum per kota/kabupaten" + }, + "title": "Pilih kota/kabupaten" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nina/translations/no.json b/homeassistant/components/nina/translations/no.json new file mode 100644 index 00000000000..a4e062a7812 --- /dev/null +++ b/homeassistant/components/nina/translations/no.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "no_selection": "Velg minst \u00e9n by/fylke", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "_a_to_d": "By/fylke (A-D)", + "_e_to_h": "By/fylke (E-H)", + "_i_to_l": "By/fylke (I-L)", + "_m_to_q": "By/fylke (M-Q)", + "_r_to_u": "By/fylke (R-U)", + "_v_to_z": "By/fylke (V-Z)", + "corona_filter": "Fjern koronaadvarsler", + "slots": "Maksimalt antall advarsler per by/fylke" + }, + "title": "Velg by/fylke" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nina/translations/pl.json b/homeassistant/components/nina/translations/pl.json new file mode 100644 index 00000000000..2a6b459e102 --- /dev/null +++ b/homeassistant/components/nina/translations/pl.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "no_selection": "Prosz\u0119 przynajmniej wybra\u0107 miasto lub powiat" + }, + "step": { + "user": { + "data": { + "_a_to_d": "Miasto/powiat (A-D)", + "_e_to_h": "Miasto/powiat (E-H)", + "_i_to_l": "Miasto/powiat (I-L)", + "_m_to_q": "Miasto/powiat (M-Q)", + "_r_to_u": "Miasto/powiat (R-U)", + "_v_to_z": "Miasto/powiat (V-Z)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/powerwall/translations/ja.json b/homeassistant/components/powerwall/translations/ja.json index 27c9cfded6a..5dc6cb6fa4a 100644 --- a/homeassistant/components/powerwall/translations/ja.json +++ b/homeassistant/components/powerwall/translations/ja.json @@ -17,6 +17,7 @@ "ip_address": "IP\u30a2\u30c9\u30ec\u30b9", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, + "description": "\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u901a\u5e38\u3001Backup Gateway\u306e\u30b7\u30ea\u30a2\u30eb\u756a\u53f7\u306e\u6700\u5f8c\u306e5\u6587\u5b57\u3067\u3042\u308a\u3001Tesla\u30a2\u30d7\u30ea\u3067\u898b\u3064\u3051\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002\u307e\u305f\u306f\u3001Backup Gateway2\u306e\u30c9\u30a2\u306e\u5185\u5074\u306b\u3042\u308b\u30d1\u30b9\u30ef\u30fc\u30c9\u306e\u6700\u5f8c\u306e5\u6587\u5b57\u3067\u3059\u3002", "title": "Powerwall\u306b\u63a5\u7d9a" } } diff --git a/homeassistant/components/sentry/translations/ja.json b/homeassistant/components/sentry/translations/ja.json index 49cb304dad2..13ce3b4a5ff 100644 --- a/homeassistant/components/sentry/translations/ja.json +++ b/homeassistant/components/sentry/translations/ja.json @@ -26,6 +26,7 @@ "event_handled": "\u51e6\u7406\u3055\u308c\u305f\u30a4\u30d9\u30f3\u30c8\u3092\u9001\u4fe1", "event_third_party_packages": "\u30b5\u30fc\u30c9\u30d1\u30fc\u30c6\u30a3\u88fd\u30d1\u30c3\u30b1\u30fc\u30b8\u304b\u3089\u306e\u30a4\u30d9\u30f3\u30c8\u3092\u9001\u4fe1\u3059\u308b", "logging_event_level": "Sentry\u304c\u30a4\u30d9\u30f3\u30c8\u3092\u767b\u9332\u3059\u308b\u969b\u306e\u30ed\u30b0\u30ec\u30d9\u30eb", + "logging_level": "Sentry\u306f\u30ed\u30b0\u30ec\u30d9\u30eb\u3092\u3001\u30ed\u30b0\u3092\u30d1\u30f3\u304f\u305a\u30ea\u30b9\u30c8(breadcrums)\u3068\u3057\u3066\u8a18\u9332", "tracing": "\u30d1\u30d5\u30a9\u30fc\u30de\u30f3\u30b9\u30c8\u30ec\u30fc\u30b9\u3092\u6709\u52b9\u306b\u3059\u308b", "tracing_sample_rate": "\u30c8\u30ec\u30fc\u30b9\u306e\u30b5\u30f3\u30d7\u30eb\u30ec\u30fc\u30c8; 0.0 \u304b\u3089 1.0(1.0 = 100%)" } diff --git a/homeassistant/components/toon/translations/ja.json b/homeassistant/components/toon/translations/ja.json index 26889353f92..746b717ae08 100644 --- a/homeassistant/components/toon/translations/ja.json +++ b/homeassistant/components/toon/translations/ja.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u9078\u629e\u3057\u305f\u5951\u7d04(Agreement)\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002", "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", "no_agreements": "\u3053\u306e\u30a2\u30ab\u30a6\u30f3\u30c8\u306b\u306f\u3001Toon displays\u304c\u3042\u308a\u307e\u305b\u3093\u3002", diff --git a/homeassistant/components/tuya/translations/select.pl.json b/homeassistant/components/tuya/translations/select.pl.json index c5d71d2eb55..c44eae538d6 100644 --- a/homeassistant/components/tuya/translations/select.pl.json +++ b/homeassistant/components/tuya/translations/select.pl.json @@ -14,6 +14,9 @@ "0": "Niska czu\u0142o\u015b\u0107", "1": "Wysoka czu\u0142o\u015b\u0107" }, + "tuya__fingerbot_mode": { + "switch": "Prze\u0142\u0105cznik" + }, "tuya__ipc_work_mode": { "0": "Tryb niskiego poboru mocy", "1": "Tryb pracy ci\u0105g\u0142ej" diff --git a/homeassistant/components/wolflink/translations/sensor.ja.json b/homeassistant/components/wolflink/translations/sensor.ja.json index 519e2062e5b..2a8b091ca36 100644 --- a/homeassistant/components/wolflink/translations/sensor.ja.json +++ b/homeassistant/components/wolflink/translations/sensor.ja.json @@ -22,6 +22,7 @@ "dhw_prior": "DHWPrior", "eco": "\u30a8\u30b3", "ein": "\u6709\u52b9", + "estrichtrocknung": "Screed drying", "externe_deaktivierung": "\u5916\u90e8\u306e\u975e\u30a2\u30af\u30c6\u30a3\u30d6\u5316", "fernschalter_ein": "\u30ea\u30e2\u30fc\u30c8\u5236\u5fa1\u304c\u6709\u52b9", "frost_heizkreis": "\u6696\u623f(\u52a0\u71b1)\u56de\u8def\u306e\u971c", @@ -38,8 +39,11 @@ "kalibration_heizbetrieb": "\u6696\u623f(\u52a0\u71b1)\u306e\u30ad\u30e3\u30ea\u30d6\u30ec\u30fc\u30b7\u30e7\u30f3", "kalibration_kombibetrieb": "\u30b3\u30f3\u30d3\u30e2\u30fc\u30c9 \u30ad\u30e3\u30ea\u30d6\u30ec\u30fc\u30b7\u30e7\u30f3", "kalibration_warmwasserbetrieb": "DHW\u30ad\u30e3\u30ea\u30d6\u30ec\u30fc\u30b7\u30e7\u30f3", + "kaskadenbetrieb": "Cascade\u64cd\u4f5c", "kombibetrieb": "\u30b3\u30f3\u30d3\u30e2\u30fc\u30c9", "kombigerat": "\u30b3\u30f3\u30d3 \u30dc\u30a4\u30e9\u30fc", + "kombigerat_mit_solareinbindung": "\u592a\u967d\u71b1\u5229\u7528\u306e\u30b3\u30f3\u30d3\u30dc\u30a4\u30e9\u30fc", + "mindest_kombizeit": "\u6700\u5c0fcombi time", "nachlauf_heizkreispumpe": "\u6696\u623f(\u52a0\u71b1)\u56de\u8def\u30dd\u30f3\u30d7\u306e\u4f5c\u52d5", "nachspulen": "\u30d5\u30e9\u30c3\u30b7\u30e5\u5f8c(Post-flush)", "nur_heizgerat": "\u30dc\u30a4\u30e9\u30fc\u306e\u307f", @@ -71,6 +75,7 @@ "tpw": "TPW", "urlaubsmodus": "\u30db\u30ea\u30c7\u30fc(\u4f11\u65e5)\u30e2\u30fc\u30c9", "ventilprufung": "\u30d0\u30eb\u30d6\u30c6\u30b9\u30c8", + "vorspulen": "\u30a8\u30f3\u30c8\u30ea\u30fc\u3092\u6d17\u3044\u6d41\u3059(rinsing)", "warmwasser": "DHW", "warmwasser_schnellstart": "DHW\u30af\u30a4\u30c3\u30af\u30b9\u30bf\u30fc\u30c8", "warmwasserbetrieb": "DHW\u30e2\u30fc\u30c9", diff --git a/homeassistant/components/yamaha_musiccast/translations/select.bg.json b/homeassistant/components/yamaha_musiccast/translations/select.bg.json new file mode 100644 index 00000000000..3d6f452fc29 --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/select.bg.json @@ -0,0 +1,33 @@ +{ + "state": { + "yamaha_musiccast__dimmer": { + "auto": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e" + }, + "yamaha_musiccast__zone_equalizer_mode": { + "auto": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e", + "manual": "\u0420\u044a\u0447\u043d\u043e" + }, + "yamaha_musiccast__zone_sleep": { + "120 min": "120 \u043c\u0438\u043d\u0443\u0442\u0438", + "30 min": "30 \u043c\u0438\u043d\u0443\u0442\u0438", + "60 min": "60 \u043c\u0438\u043d\u0443\u0442\u0438", + "90 min": "90 \u043c\u0438\u043d\u0443\u0442\u0438", + "off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + }, + "yamaha_musiccast__zone_surr_decoder_type": { + "auto": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e", + "dolby_pl": "Dolby ProLogic", + "dolby_pl2x_game": "Dolby ProLogic 2x Game", + "dolby_pl2x_movie": "Dolby ProLogic 2x Movie", + "dolby_pl2x_music": "Dolby ProLogic 2x Music", + "dolby_surround": "Dolby Surround", + "dts_neo6_cinema": "DTS Neo:6 Cinema", + "dts_neo6_music": "DTS Neo:6 Music", + "dts_neural_x": "DTS Neural:X" + }, + "yamaha_musiccast__zone_tone_control_mode": { + "auto": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e", + "manual": "\u0420\u044a\u0447\u043d\u043e" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/select.id.json b/homeassistant/components/yamaha_musiccast/translations/select.id.json new file mode 100644 index 00000000000..3bcdb6f7650 --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/select.id.json @@ -0,0 +1,52 @@ +{ + "state": { + "yamaha_musiccast__dimmer": { + "auto": "Otomatis" + }, + "yamaha_musiccast__zone_equalizer_mode": { + "auto": "Otomatis", + "bypass": "Pintas", + "manual": "Manual" + }, + "yamaha_musiccast__zone_link_audio_delay": { + "audio_sync": "Sinkronisasi Audio", + "audio_sync_off": "Sinkronisasi Audio Mati", + "audio_sync_on": "Sinkronisasi Audio Nyala", + "balanced": "Seimbang", + "lip_sync": "Sinkronisasi Bibir" + }, + "yamaha_musiccast__zone_link_audio_quality": { + "compressed": "Terkompresi", + "uncompressed": "Tidak terkompresi" + }, + "yamaha_musiccast__zone_link_control": { + "speed": "Kecepatan", + "stability": "Stabilitas", + "standard": "Standar" + }, + "yamaha_musiccast__zone_sleep": { + "120 min": "120 Menit", + "30 min": "30 Menit", + "60 min": "60 Menit", + "90 min": "90 Menit", + "off": "Mati" + }, + "yamaha_musiccast__zone_surr_decoder_type": { + "auto": "Otomatis", + "dolby_pl": "Dolby ProLogic", + "dolby_pl2x_game": "Dolby ProLogic 2x Game", + "dolby_pl2x_movie": "Dolby ProLogic 2x Movie", + "dolby_pl2x_music": "Dolby ProLogic 2x Music", + "dolby_surround": "Dolby Surround", + "dts_neo6_cinema": "DTS Neo:6 Cinema", + "dts_neo6_music": "DTS Neo:6 Music", + "dts_neural_x": "DTS Neural:X", + "toggle": "Alihkan" + }, + "yamaha_musiccast__zone_tone_control_mode": { + "auto": "Otomatis", + "bypass": "Pintas", + "manual": "Manual" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/select.ja.json b/homeassistant/components/yamaha_musiccast/translations/select.ja.json new file mode 100644 index 00000000000..c698e6fe13e --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/select.ja.json @@ -0,0 +1,52 @@ +{ + "state": { + "yamaha_musiccast__dimmer": { + "auto": "\u30aa\u30fc\u30c8" + }, + "yamaha_musiccast__zone_equalizer_mode": { + "auto": "\u30aa\u30fc\u30c8", + "bypass": "\u30d0\u30a4\u30d1\u30b9", + "manual": "\u30de\u30cb\u30e5\u30a2\u30eb" + }, + "yamaha_musiccast__zone_link_audio_delay": { + "audio_sync": "\u30aa\u30fc\u30c7\u30a3\u30aa\u540c\u671f", + "audio_sync_off": "\u30aa\u30fc\u30c7\u30a3\u30aa\u540c\u671f \u30aa\u30d5", + "audio_sync_on": "\u30aa\u30fc\u30c7\u30a3\u30aa\u540c\u671f \u30aa\u30f3", + "balanced": "\u30d0\u30e9\u30f3\u30b9", + "lip_sync": "\u30ea\u30c3\u30d7\u30b7\u30f3\u30af" + }, + "yamaha_musiccast__zone_link_audio_quality": { + "compressed": "\u5727\u7e2e", + "uncompressed": "\u975e\u5727\u7e2e" + }, + "yamaha_musiccast__zone_link_control": { + "speed": "\u901f\u5ea6", + "stability": "\u5b89\u5b9a\u6027", + "standard": "\u30b9\u30bf\u30f3\u30c0\u30fc\u30c9" + }, + "yamaha_musiccast__zone_sleep": { + "120 min": "120\u5206", + "30 min": "30\u5206", + "60 min": "60\u5206", + "90 min": "90\u5206", + "off": "\u30aa\u30d5" + }, + "yamaha_musiccast__zone_surr_decoder_type": { + "auto": "\u30aa\u30fc\u30c8", + "dolby_pl": "Dolby ProLogic", + "dolby_pl2x_game": "Dolby ProLogic 2x Game", + "dolby_pl2x_movie": "Dolby ProLogic 2x Movie", + "dolby_pl2x_music": "Dolby ProLogic 2x Music", + "dolby_surround": "Dolby Surround", + "dts_neo6_cinema": "DTS Neo:6 Cinema", + "dts_neo6_music": "DTS Neo:6 Music", + "dts_neural_x": "DTS Neural:X", + "toggle": "\u30c8\u30b0\u30eb" + }, + "yamaha_musiccast__zone_tone_control_mode": { + "auto": "\u30aa\u30fc\u30c8", + "bypass": "\u30d0\u30a4\u30d1\u30b9", + "manual": "\u30de\u30cb\u30e5\u30a2\u30eb" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/select.no.json b/homeassistant/components/yamaha_musiccast/translations/select.no.json new file mode 100644 index 00000000000..3007e1b881b --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/select.no.json @@ -0,0 +1,52 @@ +{ + "state": { + "yamaha_musiccast__dimmer": { + "auto": "Auto" + }, + "yamaha_musiccast__zone_equalizer_mode": { + "auto": "Auto", + "bypass": "Bypass", + "manual": "Manuell" + }, + "yamaha_musiccast__zone_link_audio_delay": { + "audio_sync": "Lydsynkronisering", + "audio_sync_off": "Lydsynkronisering av", + "audio_sync_on": "Lydsynkronisering p\u00e5", + "balanced": "Balansert", + "lip_sync": "Leppesynkronisering" + }, + "yamaha_musiccast__zone_link_audio_quality": { + "compressed": "Komprimert", + "uncompressed": "Ukomprimert" + }, + "yamaha_musiccast__zone_link_control": { + "speed": "Hastighet", + "stability": "Stabilitet", + "standard": "Standard" + }, + "yamaha_musiccast__zone_sleep": { + "120 min": "120 minutter", + "30 min": "30 minutter", + "60 min": "60 minutter", + "90 min": "90 minutter", + "off": "Av" + }, + "yamaha_musiccast__zone_surr_decoder_type": { + "auto": "Auto", + "dolby_pl": "Dolby ProLogic", + "dolby_pl2x_game": "Dolby ProLogic 2x-spill", + "dolby_pl2x_movie": "Dolby ProLogic 2x film", + "dolby_pl2x_music": "Dolby ProLogic 2x musikk", + "dolby_surround": "Dolby Surround", + "dts_neo6_cinema": "DTS Neo:6 kino", + "dts_neo6_music": "DTS Neo:6 musikk", + "dts_neural_x": "DTS Neural:X", + "toggle": "Veksle" + }, + "yamaha_musiccast__zone_tone_control_mode": { + "auto": "Auto", + "bypass": "Bypass", + "manual": "Manuell" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/select.pl.json b/homeassistant/components/yamaha_musiccast/translations/select.pl.json new file mode 100644 index 00000000000..e5c4bd8f3d0 --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/select.pl.json @@ -0,0 +1,36 @@ +{ + "state": { + "yamaha_musiccast__zone_equalizer_mode": { + "auto": "Automatycznie", + "manual": "R\u0119cznie" + }, + "yamaha_musiccast__zone_link_audio_delay": { + "balanced": "Zr\u00f3wnowa\u017cone" + }, + "yamaha_musiccast__zone_link_audio_quality": { + "compressed": "Skompresowane", + "uncompressed": "Nieskompresowane" + }, + "yamaha_musiccast__zone_link_control": { + "speed": "Pr\u0119dko\u015b\u0107", + "stability": "Stabilno\u015b\u0107", + "standard": "Normalnie" + }, + "yamaha_musiccast__zone_sleep": { + "120 min": "120 minut", + "30 min": "30 minut", + "60 min": "60 minut", + "90 min": "90 minut", + "off": "wy\u0142\u0105czone" + }, + "yamaha_musiccast__zone_surr_decoder_type": { + "auto": "Automatycznie", + "toggle": "Prze\u0142\u0105cz" + }, + "yamaha_musiccast__zone_tone_control_mode": { + "auto": "Automatycznie", + "bypass": "Polski", + "manual": "R\u0119cznie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/select.ru.json b/homeassistant/components/yamaha_musiccast/translations/select.ru.json new file mode 100644 index 00000000000..aa65bfa4146 --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/select.ru.json @@ -0,0 +1,52 @@ +{ + "state": { + "yamaha_musiccast__dimmer": { + "auto": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438" + }, + "yamaha_musiccast__zone_equalizer_mode": { + "auto": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438", + "bypass": "\u0411\u0430\u0439\u043f\u0430\u0441", + "manual": "\u0420\u0443\u0447\u043d\u043e\u0439" + }, + "yamaha_musiccast__zone_link_audio_delay": { + "audio_sync": "\u0421\u0438\u043d\u0445\u0440\u043e\u043d\u0438\u0437\u0430\u0446\u0438\u044f \u0437\u0432\u0443\u043a\u0430", + "audio_sync_off": "\u0421\u0438\u043d\u0445\u0440\u043e\u043d\u0438\u0437\u0430\u0446\u0438\u044f \u0437\u0432\u0443\u043a\u0430 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0430", + "audio_sync_on": "\u0421\u0438\u043d\u0445\u0440\u043e\u043d\u0438\u0437\u0430\u0446\u0438\u044f \u0437\u0432\u0443\u043a\u0430 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0430", + "balanced": "\u0421\u0431\u0430\u043b\u0430\u043d\u0441\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439", + "lip_sync": "\u0421\u0438\u043d\u0445\u0440\u043e\u043d\u0438\u0437\u0430\u0446\u0438\u044f \u0433\u0443\u0431" + }, + "yamaha_musiccast__zone_link_audio_quality": { + "compressed": "\u0421\u0436\u0430\u0442\u044b\u0439", + "uncompressed": "\u041d\u0435\u0441\u0436\u0430\u0442\u044b\u0439" + }, + "yamaha_musiccast__zone_link_control": { + "speed": "\u0421\u043a\u043e\u0440\u043e\u0441\u0442\u044c", + "stability": "\u0421\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u043e\u0441\u0442\u044c", + "standard": "\u0421\u0442\u0430\u043d\u0434\u0430\u0440\u0442" + }, + "yamaha_musiccast__zone_sleep": { + "120 min": "120 \u043c\u0438\u043d\u0443\u0442", + "30 min": "30 \u043c\u0438\u043d\u0443\u0442", + "60 min": "60 \u043c\u0438\u043d\u0443\u0442", + "90 min": "90 \u043c\u0438\u043d\u0443\u0442", + "off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + }, + "yamaha_musiccast__zone_surr_decoder_type": { + "auto": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438", + "dolby_pl": "Dolby ProLogic", + "dolby_pl2x_game": "Dolby ProLogic 2x \u0418\u0433\u0440\u0430", + "dolby_pl2x_movie": "Dolby ProLogic 2x \u0424\u0438\u043b\u044c\u043c", + "dolby_pl2x_music": "Dolby ProLogic 2x \u041c\u0443\u0437\u044b\u043a\u0430", + "dolby_surround": "Dolby Surround", + "dts_neo6_cinema": "DTS Neo:6 \u041a\u0438\u043d\u043e\u0442\u0435\u0430\u0442\u0440", + "dts_neo6_music": "DTS Neo:6 \u041c\u0443\u0437\u044b\u043a\u0430", + "dts_neural_x": "DTS Neural:X", + "toggle": "\u041f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442\u044c" + }, + "yamaha_musiccast__zone_tone_control_mode": { + "auto": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438", + "bypass": "\u0411\u0430\u0439\u043f\u0430\u0441", + "manual": "\u0420\u0443\u0447\u043d\u043e\u0439" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/ja.json b/homeassistant/components/zha/translations/ja.json index 94257d5cb3d..636df6f047e 100644 --- a/homeassistant/components/zha/translations/ja.json +++ b/homeassistant/components/zha/translations/ja.json @@ -46,6 +46,8 @@ "title": "\u30a2\u30e9\u30fc\u30e0 \u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u30d1\u30cd\u30eb\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" }, "zha_options": { + "consider_unavailable_battery": "(\u79d2)\u5f8c\u306b\u30d0\u30c3\u30c6\u30ea\u30fc\u99c6\u52d5\u306e\u30c7\u30d0\u30a4\u30b9\u304c\u4f7f\u7528\u3067\u304d\u306a\u304f\u306a\u308b\u3068\u898b\u306a\u3059", + "consider_unavailable_mains": "(\u79d2)\u5f8c\u306b\u4e3b\u96fb\u6e90\u304c\u30c7\u30d0\u30a4\u30b9\u304b\u3089\u4f7f\u7528\u3067\u304d\u306a\u304f\u306a\u308b\u3068\u898b\u306a\u3059", "default_light_transition": "\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u30e9\u30a4\u30c8\u9077\u79fb\u6642\u9593(\u79d2)", "enable_identify_on_join": "\u30c7\u30d0\u30a4\u30b9\u304c\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u306b\u53c2\u52a0\u3059\u308b\u969b\u306b\u3001\u8b58\u5225\u52b9\u679c\u3092\u6709\u52b9\u306b\u3059\u308b", "title": "\u30b0\u30ed\u30fc\u30d0\u30eb\u30aa\u30d7\u30b7\u30e7\u30f3" @@ -53,7 +55,8 @@ }, "device_automation": { "action_type": { - "squawk": "\u30b9\u30b3\u30fc\u30af(Squawk)" + "squawk": "\u30b9\u30b3\u30fc\u30af(Squawk)", + "warn": "Warn" }, "trigger_subtype": { "both_buttons": "\u4e21\u65b9\u306e\u30dc\u30bf\u30f3", @@ -80,6 +83,7 @@ "turn_on": "\u30aa\u30f3\u306b\u3059\u308b" }, "trigger_type": { + "device_dropped": "\u30c7\u30d0\u30a4\u30b9dropped", "device_flipped": "\u30c7\u30d0\u30a4\u30b9\u304c\u53cd\u8ee2\u3057\u307e\u3057\u305f \"{subtype}\"", "device_knocked": "\u30c7\u30d0\u30a4\u30b9\u304c\u30ce\u30c3\u30af\u3055\u308c\u307e\u3057\u305f \"{subtype}\"", "device_offline": "\u30c7\u30d0\u30a4\u30b9\u304c\u30aa\u30d5\u30e9\u30a4\u30f3", diff --git a/homeassistant/components/zwave_js/translations/id.json b/homeassistant/components/zwave_js/translations/id.json index 19f94f0ab1f..7ffb9d7b713 100644 --- a/homeassistant/components/zwave_js/translations/id.json +++ b/homeassistant/components/zwave_js/translations/id.json @@ -33,6 +33,7 @@ "s2_unauthenticated_key": "Kunci S2 Tidak Diautentikasi", "usb_path": "Jalur Perangkat USB" }, + "description": "Add-on akan menghasilkan kunci keamanan jika bidang tersebut dibiarkan kosong.", "title": "Masukkan konfigurasi add-on Z-Wave JS" }, "hassio_confirm": { @@ -119,6 +120,7 @@ "s2_unauthenticated_key": "Kunci S2 Tidak Diautentikasi", "usb_path": "Jalur Perangkat USB" }, + "description": "Add-on akan menghasilkan kunci keamanan jika bidang tersebut dibiarkan kosong.", "title": "Masukkan konfigurasi add-on Z-Wave JS" }, "install_addon": { diff --git a/homeassistant/components/zwave_js/translations/ja.json b/homeassistant/components/zwave_js/translations/ja.json index 248c8d001e6..c85025c1394 100644 --- a/homeassistant/components/zwave_js/translations/ja.json +++ b/homeassistant/components/zwave_js/translations/ja.json @@ -33,6 +33,7 @@ "s2_unauthenticated_key": "S2\u8a8d\u8a3c\u3055\u308c\u3066\u3044\u306a\u3044\u30ad\u30fc", "usb_path": "USB\u30c7\u30d0\u30a4\u30b9\u306e\u30d1\u30b9" }, + "description": "\u3053\u308c\u3089\u306e\u30d5\u30a3\u30fc\u30eb\u30c9\u304c\u7a7a\u306e\u307e\u307e\u306e\u5834\u5408\u3001\u30a2\u30c9\u30aa\u30f3\u306f\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u30ad\u30fc\u3092\u751f\u6210\u3057\u307e\u3059\u3002", "title": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u306e\u8a2d\u5b9a\u3092\u5165\u529b" }, "hassio_confirm": { @@ -119,6 +120,7 @@ "s2_unauthenticated_key": "S2\u8a8d\u8a3c\u3055\u308c\u3066\u3044\u306a\u3044\u30ad\u30fc", "usb_path": "USB\u30c7\u30d0\u30a4\u30b9\u306e\u30d1\u30b9" }, + "description": "\u3053\u308c\u3089\u306e\u30d5\u30a3\u30fc\u30eb\u30c9\u304c\u7a7a\u306e\u307e\u307e\u306e\u5834\u5408\u3001\u30a2\u30c9\u30aa\u30f3\u306f\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u30ad\u30fc\u3092\u751f\u6210\u3057\u307e\u3059\u3002", "title": "Z-Wave JS\u306e\u30a2\u30c9\u30aa\u30f3\u304c\u59cb\u307e\u308a\u307e\u3059\u3002" }, "install_addon": { diff --git a/homeassistant/components/zwave_js/translations/no.json b/homeassistant/components/zwave_js/translations/no.json index f08f5bd07cb..24efdf1c573 100644 --- a/homeassistant/components/zwave_js/translations/no.json +++ b/homeassistant/components/zwave_js/translations/no.json @@ -33,6 +33,7 @@ "s2_unauthenticated_key": "S2 Uautentisert n\u00f8kkel", "usb_path": "USB enhetsbane" }, + "description": "Tillegget vil generere sikkerhetsn\u00f8kler hvis disse feltene er tomme.", "title": "Angi konfigurasjon for Z-Wave JS-tillegg" }, "hassio_confirm": { @@ -119,6 +120,7 @@ "s2_unauthenticated_key": "S2 Uautentisert n\u00f8kkel", "usb_path": "USB enhetsbane" }, + "description": "Tillegget vil generere sikkerhetsn\u00f8kler hvis disse feltene er tomme.", "title": "Angi konfigurasjon for Z-Wave JS-tillegg" }, "install_addon": { From ea58778a5cc26e0cde16a7667659a0bf0f6a8e7b Mon Sep 17 00:00:00 2001 From: Robert Blomqvist Date: Wed, 8 Dec 2021 02:19:23 +0100 Subject: [PATCH 0121/2644] Rephrase upgrade notification message to avoid installing Python 3.10 (#61181) --- homeassistant/bootstrap.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index f111ff6a079..64a6e98aa87 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -252,8 +252,7 @@ async def async_from_config_dict( f"{'.'.join(str(x) for x in sys.version_info[:3])} is deprecated and will " f"be removed in Home Assistant {REQUIRED_NEXT_PYTHON_HA_RELEASE}. " "Please upgrade Python to " - f"{'.'.join(str(x) for x in REQUIRED_NEXT_PYTHON_VER)} or " - "higher." + f"{'.'.join(str(x) for x in REQUIRED_NEXT_PYTHON_VER[:2])}." ) _LOGGER.warning(msg) hass.components.persistent_notification.async_create( From 6b70bd74957b6e78e9874a37da38206e26cae4ef Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 Dec 2021 15:20:38 -1000 Subject: [PATCH 0122/2644] Fix uncaught exception in bond config flow (#61184) --- homeassistant/components/bond/config_flow.py | 5 ++- tests/components/bond/test_config_flow.py | 41 ++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bond/config_flow.py b/homeassistant/components/bond/config_flow.py index 5fce8477a28..d3a7b4adf72 100644 --- a/homeassistant/components/bond/config_flow.py +++ b/homeassistant/components/bond/config_flow.py @@ -87,7 +87,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self._discovered[CONF_ACCESS_TOKEN] = token - _, hub_name = await _validate_input(self.hass, self._discovered) + try: + _, hub_name = await _validate_input(self.hass, self._discovered) + except InputValidationError: + return self._discovered[CONF_NAME] = hub_name async def async_step_zeroconf( diff --git a/tests/components/bond/test_config_flow.py b/tests/components/bond/test_config_flow.py index 4a6efa8f89b..b36637897d8 100644 --- a/tests/components/bond/test_config_flow.py +++ b/tests/components/bond/test_config_flow.py @@ -1,6 +1,7 @@ """Test the Bond config flow.""" from __future__ import annotations +from http import HTTPStatus from typing import Any from unittest.mock import MagicMock, Mock, patch @@ -304,6 +305,46 @@ async def test_zeroconf_form_with_token_available(hass: core.HomeAssistant): assert len(mock_setup_entry.mock_calls) == 1 +async def test_zeroconf_form_with_token_available_name_unavailable( + hass: core.HomeAssistant, +): + """Test we get the discovery form when we can get the token but the name is unavailable.""" + + with patch_bond_version( + side_effect=ClientResponseError(Mock(), (), status=HTTPStatus.BAD_REQUEST) + ), patch_bond_token(return_value={"token": "discovered-token"}): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="test-host", + hostname="mock_hostname", + name="test-bond-id.some-other-tail-info", + port=None, + properties={}, + type="mock_type", + ), + ) + await hass.async_block_till_done() + assert result["type"] == "form" + assert result["errors"] == {} + + with _patch_async_setup_entry() as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "test-bond-id" + assert result2["data"] == { + CONF_HOST: "test-host", + CONF_ACCESS_TOKEN: "discovered-token", + } + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_zeroconf_already_configured(hass: core.HomeAssistant): """Test starting a flow from discovery when already configured.""" From 81dc84aef537d4dda8546cc4f1f299759215f41b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 Dec 2021 15:20:55 -1000 Subject: [PATCH 0123/2644] Fix log spam from flux_led 0x08 devices when in music mode (#61196) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 71d8fd350b7..defaa348262 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.25.17"], + "requirements": ["flux_led==0.26.2"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 59daf1e74c7..3f2e428944f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -664,7 +664,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.25.17 +flux_led==0.26.2 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1021a529fca..0d98a4302c0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -405,7 +405,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.25.17 +flux_led==0.26.2 # homeassistant.components.homekit fnvhash==0.1.0 From df9154268ebdea0d8d89dde9ac5ac0f335495020 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 Dec 2021 16:15:56 -1000 Subject: [PATCH 0124/2644] Update MagicHome/flux_led OUIs for DHCP discovery (#61192) --- homeassistant/components/flux_led/manifest.json | 4 ++++ homeassistant/generated/dhcp.py | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index defaa348262..92a3024869c 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -20,6 +20,10 @@ "macaddress": "7CB94C*", "hostname": "[ba][lk]*" }, + { + "macaddress": "ACCF23*", + "hostname": "[ba][lk]*" + }, { "macaddress": "B4E842*", "hostname": "[ba][lk]*" diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 4313aa3f486..fae3df053f1 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -86,6 +86,11 @@ DHCP = [ "macaddress": "7CB94C*", "hostname": "[ba][lk]*" }, + { + "domain": "flux_led", + "macaddress": "ACCF23*", + "hostname": "[ba][lk]*" + }, { "domain": "flux_led", "macaddress": "B4E842*", From 8cee47072dbc15b86f4c35024efb52bb0be12c85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 8 Dec 2021 03:48:16 +0100 Subject: [PATCH 0125/2644] Add local access for Adax (#60019) --- homeassistant/components/adax/climate.py | 52 ++- homeassistant/components/adax/config_flow.py | 143 ++++++--- homeassistant/components/adax/const.py | 5 + homeassistant/components/adax/manifest.json | 4 +- homeassistant/components/adax/strings.json | 18 +- requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/adax/test_config_flow.py | 314 ++++++++++++++++++- 8 files changed, 486 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/adax/climate.py b/homeassistant/components/adax/climate.py index 783c2a9f2f8..48cbc9b270c 100644 --- a/homeassistant/components/adax/climate.py +++ b/homeassistant/components/adax/climate.py @@ -4,6 +4,7 @@ from __future__ import annotations from typing import Any from adax import Adax +from adax_local import Adax as AdaxLocal from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -14,7 +15,10 @@ from homeassistant.components.climate.const import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_TEMPERATURE, + CONF_IP_ADDRESS, CONF_PASSWORD, + CONF_TOKEN, + CONF_UNIQUE_ID, PRECISION_WHOLE, TEMP_CELSIUS, ) @@ -23,7 +27,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ACCOUNT_ID, DOMAIN +from .const import ACCOUNT_ID, CONNECTION_TYPE, DOMAIN, LOCAL async def async_setup_entry( @@ -32,6 +36,17 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Adax thermostat with config flow.""" + if entry.data.get(CONNECTION_TYPE) == LOCAL: + adax_data_handler = AdaxLocal( + entry.data[CONF_IP_ADDRESS], + entry.data[CONF_TOKEN], + websession=async_get_clientsession(hass, verify_ssl=False), + ) + async_add_entities( + [LocalAdaxDevice(adax_data_handler, entry.data[CONF_UNIQUE_ID])], True + ) + return + adax_data_handler = Adax( entry.data[ACCOUNT_ID], entry.data[CONF_PASSWORD], @@ -107,3 +122,38 @@ class AdaxDevice(ClimateEntity): self._attr_hvac_mode = HVAC_MODE_OFF self._attr_icon = "mdi:radiator-off" return + + +class LocalAdaxDevice(ClimateEntity): + """Representation of a heater.""" + + _attr_hvac_modes = [HVAC_MODE_HEAT] + _attr_hvac_mode = HVAC_MODE_HEAT + _attr_max_temp = 35 + _attr_min_temp = 5 + _attr_supported_features = SUPPORT_TARGET_TEMPERATURE + _attr_target_temperature_step = PRECISION_WHOLE + _attr_temperature_unit = TEMP_CELSIUS + + def __init__(self, adax_data_handler, unique_id): + """Initialize the heater.""" + self._adax_data_handler = adax_data_handler + self._attr_unique_id = unique_id + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + manufacturer="Adax", + ) + + async def async_set_temperature(self, **kwargs): + """Set new target temperature.""" + temperature = kwargs.get(ATTR_TEMPERATURE) + if temperature is None: + return + await self._adax_data_handler.set_target_temperature(temperature) + + async def async_update(self) -> None: + """Get the latest data.""" + data = await self._adax_data_handler.get_status() + self._attr_target_temperature = data["target_temperature"] + self._attr_current_temperature = data["current_temperature"] + self._attr_available = self._attr_current_temperature is not None diff --git a/homeassistant/components/adax/config_flow.py b/homeassistant/components/adax/config_flow.py index cf845df5e06..2788b563678 100644 --- a/homeassistant/components/adax/config_flow.py +++ b/homeassistant/components/adax/config_flow.py @@ -5,35 +5,30 @@ import logging from typing import Any import adax +import adax_local import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_PASSWORD -from homeassistant.core import HomeAssistant +from homeassistant.const import ( + CONF_IP_ADDRESS, + CONF_PASSWORD, + CONF_TOKEN, + CONF_UNIQUE_ID, +) from homeassistant.data_entry_flow import FlowResult -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import ACCOUNT_ID, DOMAIN - -_LOGGER = logging.getLogger(__name__) - -STEP_USER_DATA_SCHEMA = vol.Schema( - {vol.Required(ACCOUNT_ID): int, vol.Required(CONF_PASSWORD): str} +from .const import ( + ACCOUNT_ID, + CLOUD, + CONNECTION_TYPE, + DOMAIN, + LOCAL, + WIFI_PSWD, + WIFI_SSID, ) - -async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None: - """Validate the user input allows us to connect.""" - account_id = data[ACCOUNT_ID] - password = data[CONF_PASSWORD].replace(" ", "") - - token = await adax.get_adax_token( - async_get_clientsession(hass), account_id, password - ) - if token is None: - _LOGGER.info("Adax: Failed to login to retrieve token") - raise CannotConnect +_LOGGER = logging.getLogger(__name__) class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -41,33 +36,107 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_user( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + async def async_step_user(self, user_input=None): """Handle the initial step.""" + data_schema = vol.Schema( + { + vol.Required(CONNECTION_TYPE, default=CLOUD): vol.In( + ( + CLOUD, + LOCAL, + ) + ) + } + ) + if user_input is None: return self.async_show_form( - step_id="user", data_schema=STEP_USER_DATA_SCHEMA + step_id="user", + data_schema=data_schema, ) + if user_input[CONNECTION_TYPE] == LOCAL: + return await self.async_step_local() + return await self.async_step_cloud() + + async def async_step_local(self, user_input=None): + """Handle the local step.""" + data_schema = vol.Schema( + {vol.Required(WIFI_SSID): str, vol.Required(WIFI_PSWD): str} + ) + if user_input is None: + return self.async_show_form( + step_id="local", + data_schema=data_schema, + ) + + wifi_ssid = user_input[WIFI_SSID].replace(" ", "") + wifi_pswd = user_input[WIFI_PSWD].replace(" ", "") + configurator = adax_local.AdaxConfig(wifi_ssid, wifi_pswd) + + try: + if not await configurator.configure_device(): + return self.async_show_form( + step_id="local", + data_schema=data_schema, + errors={"base": "cannot_connect"}, + ) + except adax_local.HeaterNotAvailable: + return self.async_abort(reason="heater_not_available") + except adax_local.HeaterNotFound: + return self.async_abort(reason="heater_not_found") + except adax_local.InvalidWifiCred: + return self.async_abort(reason="invalid_auth") + + unique_id = configurator.mac_id + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=unique_id, + data={ + CONF_IP_ADDRESS: configurator.device_ip, + CONF_TOKEN: configurator.access_token, + CONF_UNIQUE_ID: unique_id, + CONNECTION_TYPE: LOCAL, + }, + ) + + async def async_step_cloud( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the cloud step.""" + data_schema = vol.Schema( + {vol.Required(ACCOUNT_ID): int, vol.Required(CONF_PASSWORD): str} + ) + if user_input is None: + return self.async_show_form(step_id="cloud", data_schema=data_schema) + errors = {} await self.async_set_unique_id(user_input[ACCOUNT_ID]) self._abort_if_unique_id_configured() - try: - await validate_input(self.hass, user_input) - except CannotConnect: + account_id = user_input[ACCOUNT_ID] + password = user_input[CONF_PASSWORD].replace(" ", "") + + token = await adax.get_adax_token( + async_get_clientsession(self.hass), account_id, password + ) + if token is None: + _LOGGER.info("Adax: Failed to login to retrieve token") errors["base"] = "cannot_connect" - else: - return self.async_create_entry( - title=user_input[ACCOUNT_ID], data=user_input + return self.async_show_form( + step_id="cloud", + data_schema=data_schema, + errors=errors, ) - return self.async_show_form( - step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + return self.async_create_entry( + title=user_input[ACCOUNT_ID], + data={ + ACCOUNT_ID: account_id, + CONF_PASSWORD: password, + CONNECTION_TYPE: CLOUD, + }, ) - - -class CannotConnect(HomeAssistantError): - """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/adax/const.py b/homeassistant/components/adax/const.py index ecb83f9b0f7..86c627aa130 100644 --- a/homeassistant/components/adax/const.py +++ b/homeassistant/components/adax/const.py @@ -2,4 +2,9 @@ from typing import Final ACCOUNT_ID: Final = "account_id" +CLOUD = "Cloud" +CONNECTION_TYPE = "connection_type" DOMAIN: Final = "adax" +LOCAL = "Local" +WIFI_SSID = "wifi_ssid" +WIFI_PSWD = "wifi_pswd" diff --git a/homeassistant/components/adax/manifest.json b/homeassistant/components/adax/manifest.json index cf5cbbd02a5..75d389e912a 100644 --- a/homeassistant/components/adax/manifest.json +++ b/homeassistant/components/adax/manifest.json @@ -4,10 +4,10 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/adax", "requirements": [ - "adax==0.2.0" + "adax==0.2.0", "Adax-local==0.1.1" ], "codeowners": [ "@danielhiversen" ], - "iot_class": "cloud_polling" + "iot_class": "local_polling" } diff --git a/homeassistant/components/adax/strings.json b/homeassistant/components/adax/strings.json index 213e1f95cf9..0cb60dceac9 100644 --- a/homeassistant/components/adax/strings.json +++ b/homeassistant/components/adax/strings.json @@ -2,6 +2,19 @@ "config": { "step": { "user": { + "data": { + "connection_type": "Select connection type" + }, + "description": "Select connection type. Local requires heaters with bluetooth" + }, + "local": { + "data": { + "wifi_ssid": "Wifi ssid", + "wifi_pswd": "Wifi password" + }, + "description": "Reset the heater by pressing + and OK until display shows 'Reset'. Then press and hold OK button on the heater until the blue led starts blinking before pressing Submit. Configuring heater might take some minutes." + }, + "cloud": { "data": { "account_id": "Account ID", "password": "[%key:common::config_flow::data::password%]" @@ -12,7 +25,10 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "heater_not_available": "Heater not available. Try to reset the heater by pressing + and OK for some seconds.", + "heater_not_found": "Heater not found. Try to move the heater closer to Home Assistant computer.", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" } } } diff --git a/requirements_all.txt b/requirements_all.txt index 3f2e428944f..2b6f4c3e117 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -13,6 +13,9 @@ Adafruit-SHT31==1.0.2 # homeassistant.components.bbb_gpio # Adafruit_BBIO==1.1.1 +# homeassistant.components.adax +Adax-local==0.1.1 + # homeassistant.components.homekit HAP-python==4.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0d98a4302c0..20e2e297c2a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -6,6 +6,9 @@ # homeassistant.components.aemet AEMET-OpenData==0.2.1 +# homeassistant.components.adax +Adax-local==0.1.1 + # homeassistant.components.homekit HAP-python==4.3.0 diff --git a/tests/components/adax/test_config_flow.py b/tests/components/adax/test_config_flow.py index f9638e52cbf..d35a18cdacc 100644 --- a/tests/components/adax/test_config_flow.py +++ b/tests/components/adax/test_config_flow.py @@ -1,10 +1,21 @@ """Test the Adax config flow.""" from unittest.mock import patch +import adax_local + from homeassistant import config_entries -from homeassistant.components.adax.const import ACCOUNT_ID, DOMAIN +from homeassistant.components.adax.const import ( + ACCOUNT_ID, + CLOUD, + CONNECTION_TYPE, + DOMAIN, + LOCAL, + WIFI_PSWD, + WIFI_SSID, +) from homeassistant.const import CONF_PASSWORD from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM from tests.common import MockConfigEntry @@ -19,24 +30,33 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] == RESULT_TYPE_FORM assert result["errors"] is None + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONNECTION_TYPE: CLOUD, + }, + ) + assert result2["type"] == RESULT_TYPE_FORM + with patch("adax.get_adax_token", return_value="test_token",), patch( "homeassistant.components.adax.async_setup_entry", return_value=True, ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], TEST_DATA, ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" - assert result2["title"] == TEST_DATA["account_id"] - assert result2["data"] == { - "account_id": TEST_DATA["account_id"], - "password": TEST_DATA["password"], + assert result3["type"] == "create_entry" + assert result3["title"] == TEST_DATA["account_id"] + assert result3["data"] == { + ACCOUNT_ID: TEST_DATA["account_id"], + CONF_PASSWORD: TEST_DATA["password"], + CONNECTION_TYPE: CLOUD, } assert len(mock_setup_entry.mock_calls) == 1 @@ -47,16 +67,24 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONNECTION_TYPE: CLOUD, + }, + ) + assert result2["type"] == RESULT_TYPE_FORM + with patch( "adax.get_adax_token", return_value=None, ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], TEST_DATA, ) - assert result2["type"] == "form" - assert result2["errors"] == {"base": "cannot_connect"} + assert result3["type"] == RESULT_TYPE_FORM + assert result3["errors"] == {"base": "cannot_connect"} async def test_flow_entry_already_exists(hass: HomeAssistant) -> None: @@ -69,10 +97,266 @@ async def test_flow_entry_already_exists(hass: HomeAssistant) -> None: ) first_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONNECTION_TYPE: CLOUD, + }, + ) + assert result2["type"] == RESULT_TYPE_FORM + with patch("adax.get_adax_token", return_value="token"): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=TEST_DATA + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + TEST_DATA, + ) + await hass.async_block_till_done() + + assert result3["type"] == "abort" + assert result3["reason"] == "already_configured" + + +# local API: + + +async def test_local_create_entry(hass): + """Test create entry from user input.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONNECTION_TYPE: LOCAL, + }, + ) + assert result2["type"] == RESULT_TYPE_FORM + + test_data = { + WIFI_SSID: "ssid", + WIFI_PSWD: "pswd", + } + + with patch( + "homeassistant.components.adax.async_setup_entry", + return_value=True, + ), patch( + "homeassistant.components.adax.config_flow.adax_local.AdaxConfig", autospec=True + ) as mock_client_class: + client = mock_client_class.return_value + client.configure_device.return_value = True + client.device_ip = "192.168.1.4" + client.access_token = "token" + client.mac_id = "8383838" + result = await hass.config_entries.flow.async_configure( + result2["flow_id"], + test_data, + ) + + test_data[CONNECTION_TYPE] = LOCAL + assert result["type"] == "create_entry" + assert result["title"] == "8383838" + assert result["data"] == { + "connection_type": "Local", + "ip_address": "192.168.1.4", + "token": "token", + "unique_id": "8383838", + } + + +async def test_local_flow_entry_already_exists(hass): + """Test user input for config_entry that already exists.""" + + test_data = { + WIFI_SSID: "ssid", + WIFI_PSWD: "pswd", + } + + first_entry = MockConfigEntry( + domain="adax", + data=test_data, + unique_id="8383838", + ) + first_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONNECTION_TYPE: LOCAL, + }, + ) + assert result2["type"] == RESULT_TYPE_FORM + + test_data = { + WIFI_SSID: "ssid", + WIFI_PSWD: "pswd", + } + + with patch("adax_local.AdaxConfig", autospec=True) as mock_client_class: + client = mock_client_class.return_value + client.configure_device.return_value = True + client.device_ip = "192.168.1.4" + client.access_token = "token" + client.mac_id = "8383838" + + result = await hass.config_entries.flow.async_configure( + result2["flow_id"], + test_data, ) assert result["type"] == "abort" assert result["reason"] == "already_configured" + + +async def test_local_connection_error(hass): + """Test connection error.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONNECTION_TYPE: LOCAL, + }, + ) + assert result2["type"] == RESULT_TYPE_FORM + + test_data = { + WIFI_SSID: "ssid", + WIFI_PSWD: "pswd", + } + + with patch( + "homeassistant.components.adax.config_flow.adax_local.AdaxConfig.configure_device", + return_value=False, + ): + result = await hass.config_entries.flow.async_configure( + result2["flow_id"], + test_data, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_local_heater_not_available(hass): + """Test connection error.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONNECTION_TYPE: LOCAL, + }, + ) + assert result2["type"] == RESULT_TYPE_FORM + + test_data = { + WIFI_SSID: "ssid", + WIFI_PSWD: "pswd", + } + + with patch( + "homeassistant.components.adax.config_flow.adax_local.AdaxConfig.configure_device", + side_effect=adax_local.HeaterNotAvailable, + ): + result = await hass.config_entries.flow.async_configure( + result2["flow_id"], + test_data, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "heater_not_available" + + +async def test_local_heater_not_found(hass): + """Test connection error.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONNECTION_TYPE: LOCAL, + }, + ) + assert result2["type"] == RESULT_TYPE_FORM + + test_data = { + WIFI_SSID: "ssid", + WIFI_PSWD: "pswd", + } + + with patch( + "homeassistant.components.adax.config_flow.adax_local.AdaxConfig.configure_device", + side_effect=adax_local.HeaterNotFound, + ): + result = await hass.config_entries.flow.async_configure( + result2["flow_id"], + test_data, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "heater_not_found" + + +async def test_local_invalid_wifi_cred(hass): + """Test connection error.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONNECTION_TYPE: LOCAL, + }, + ) + assert result2["type"] == RESULT_TYPE_FORM + + test_data = { + WIFI_SSID: "ssid", + WIFI_PSWD: "pswd", + } + + with patch( + "homeassistant.components.adax.config_flow.adax_local.AdaxConfig.configure_device", + side_effect=adax_local.InvalidWifiCred, + ): + result = await hass.config_entries.flow.async_configure( + result2["flow_id"], + test_data, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "invalid_auth" From 54d55fdf8804a22cf20276ab1d0c36978b51dcd3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 8 Dec 2021 06:06:27 +0100 Subject: [PATCH 0126/2644] Use list comprehension in onewire sensor descriptions (#61157) * Add 0-3 range to constants * Use list comprehension in sensor definitions Co-authored-by: epenet --- homeassistant/components/onewire/sensor.py | 51 +++++----------------- 1 file changed, 12 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index cbc616872ba..ad6ad74989c 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -40,6 +40,8 @@ from .const import ( CONF_NAMES, CONF_TYPE_OWSERVER, CONF_TYPE_SYSBUS, + DEVICE_KEYS_0_3, + DEVICE_KEYS_A_B, DOMAIN, READ_MODE_FLOAT, READ_MODE_INT, @@ -188,21 +190,15 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { "28": (SIMPLE_TEMPERATURE_SENSOR_DESCRIPTION,), "3B": (SIMPLE_TEMPERATURE_SENSOR_DESCRIPTION,), "42": (SIMPLE_TEMPERATURE_SENSOR_DESCRIPTION,), - "1D": ( + "1D": tuple( OneWireSensorEntityDescription( - key="counter.A", - name="Counter A", + key=f"counter.{id}", + name=f"Counter {id}", native_unit_of_measurement="count", read_mode=READ_MODE_INT, state_class=SensorStateClass.TOTAL_INCREASING, - ), - OneWireSensorEntityDescription( - key="counter.B", - name="Counter B", - native_unit_of_measurement="count", - read_mode=READ_MODE_INT, - state_class=SensorStateClass.TOTAL_INCREASING, - ), + ) + for id in DEVICE_KEYS_A_B ), } @@ -237,39 +233,16 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { state_class=SensorStateClass.MEASUREMENT, ), ), - "HB_MOISTURE_METER": ( + "HB_MOISTURE_METER": tuple( OneWireSensorEntityDescription( - key="moisture/sensor.0", + key=f"moisture/sensor.{id}", device_class=SensorDeviceClass.PRESSURE, - name="Moisture 0", + name=f"Moisture {id}", native_unit_of_measurement=PRESSURE_CBAR, read_mode=READ_MODE_FLOAT, state_class=SensorStateClass.MEASUREMENT, - ), - OneWireSensorEntityDescription( - key="moisture/sensor.1", - device_class=SensorDeviceClass.PRESSURE, - name="Moisture 1", - native_unit_of_measurement=PRESSURE_CBAR, - read_mode=READ_MODE_FLOAT, - state_class=SensorStateClass.MEASUREMENT, - ), - OneWireSensorEntityDescription( - key="moisture/sensor.2", - device_class=SensorDeviceClass.PRESSURE, - name="Moisture 2", - native_unit_of_measurement=PRESSURE_CBAR, - read_mode=READ_MODE_FLOAT, - state_class=SensorStateClass.MEASUREMENT, - ), - OneWireSensorEntityDescription( - key="moisture/sensor.3", - device_class=SensorDeviceClass.PRESSURE, - name="Moisture 3", - native_unit_of_measurement=PRESSURE_CBAR, - read_mode=READ_MODE_FLOAT, - state_class=SensorStateClass.MEASUREMENT, - ), + ) + for id in DEVICE_KEYS_0_3 ), } From 21f897bb5b6a2b9ff4521f52729421bb4d0c3f0d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:12:26 -0800 Subject: [PATCH 0127/2644] Fix repetier timestamp sensors (#61214) --- homeassistant/components/repetier/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/repetier/sensor.py b/homeassistant/components/repetier/sensor.py index 393d8a16ae3..25c70cc2960 100644 --- a/homeassistant/components/repetier/sensor.py +++ b/homeassistant/components/repetier/sensor.py @@ -160,7 +160,7 @@ class RepetierJobEndSensor(RepetierSensor): print_time = data["print_time"] from_start = data["from_start"] time_end = start + round(print_time, 0) - self._state = datetime.utcfromtimestamp(time_end).isoformat() + self._state = datetime.utcfromtimestamp(time_end) remaining = print_time - from_start remaining_secs = int(round(remaining, 0)) _LOGGER.debug( @@ -182,7 +182,7 @@ class RepetierJobStartSensor(RepetierSensor): job_name = data["job_name"] start = data["start"] from_start = data["from_start"] - self._state = datetime.utcfromtimestamp(start).isoformat() + self._state = datetime.utcfromtimestamp(start) elapsed_secs = int(round(from_start, 0)) _LOGGER.debug( "Job %s elapsed %s", From 4819484cbb1811d53fd7480b9a64549353a077d8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:14:28 -0800 Subject: [PATCH 0128/2644] Fix yandex_transport timestamp sensor (#61217) --- homeassistant/components/yandex_transport/sensor.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/yandex_transport/sensor.py b/homeassistant/components/yandex_transport/sensor.py index bd5d85d3ffe..724fca14725 100644 --- a/homeassistant/components/yandex_transport/sensor.py +++ b/homeassistant/components/yandex_transport/sensor.py @@ -129,9 +129,7 @@ class DiscoverYandexTransport(SensorEntity): if closer_time is None: self._state = None else: - self._state = dt_util.utc_from_timestamp(closer_time).isoformat( - timespec="seconds" - ) + self._state = dt_util.utc_from_timestamp(closer_time).replace(microsecond=0) self._attrs = attrs @property From 6257b3e0709ed96c38a5fc52453bbff76aa1c2ac Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:14:52 -0800 Subject: [PATCH 0129/2644] Fix oasa_telematics timestamp sensor (#61213) --- homeassistant/components/oasa_telematics/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/oasa_telematics/sensor.py b/homeassistant/components/oasa_telematics/sensor.py index 1a51738cb77..a5a4a98c3d4 100644 --- a/homeassistant/components/oasa_telematics/sensor.py +++ b/homeassistant/components/oasa_telematics/sensor.py @@ -120,7 +120,7 @@ class OASATelematicsSensor(SensorEntity): self._name_data = self.data.name_data next_arrival_data = self._times[0] if ATTR_NEXT_ARRIVAL in next_arrival_data: - self._state = next_arrival_data[ATTR_NEXT_ARRIVAL].isoformat() + self._state = next_arrival_data[ATTR_NEXT_ARRIVAL] class OASATelematicsData: From ea42384afd3dfd678a332017e23b865c78ca9ee0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:23:14 -0800 Subject: [PATCH 0130/2644] Fix nextbus timestamp sensor (#61212) --- homeassistant/components/nextbus/sensor.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/nextbus/sensor.py b/homeassistant/components/nextbus/sensor.py index f9df0d60412..3756c1853b7 100644 --- a/homeassistant/components/nextbus/sensor.py +++ b/homeassistant/components/nextbus/sensor.py @@ -218,6 +218,4 @@ class NextBusDepartureSensor(SensorEntity): ) latest_prediction = maybe_first(predictions) - self._state = utc_from_timestamp( - int(latest_prediction["epochTime"]) / 1000 - ).isoformat() + self._state = utc_from_timestamp(int(latest_prediction["epochTime"]) / 1000) From 0780bf142fc9a1d3872bb5da551b942fcf2cc2c7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:25:42 -0800 Subject: [PATCH 0131/2644] Fix meteo_france timestamp sensor (#61210) --- homeassistant/components/meteo_france/sensor.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index ac1ccc13009..7f4e3e0a77b 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -142,11 +142,7 @@ class MeteoFranceRainSensor(MeteoFranceSensor): (cadran for cadran in self.coordinator.data.forecast if cadran["rain"] > 1), None, ) - return ( - dt_util.utc_from_timestamp(next_rain["dt"]).isoformat() - if next_rain - else None - ) + return dt_util.utc_from_timestamp(next_rain["dt"]) if next_rain else None @property def extra_state_attributes(self): From 3519ad430976e2b41e8164b617125d13432b215c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:35:13 -0800 Subject: [PATCH 0132/2644] Fix vallox timestamp sensor (#61216) * Fix vallox timestamp sensor * Change old state type --- homeassistant/components/vallox/sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/vallox/sensor.py b/homeassistant/components/vallox/sensor.py index 17bcf0e4499..6eee46be737 100644 --- a/homeassistant/components/vallox/sensor.py +++ b/homeassistant/components/vallox/sensor.py @@ -56,7 +56,7 @@ class ValloxSensor(CoordinatorEntity, SensorEntity): self._attr_unique_id = f"{uuid}-{description.key}" @property - def native_value(self) -> StateType: + def native_value(self) -> StateType | datetime: """Return the value reported by the sensor.""" if (metric_key := self.entity_description.metric_key) is None: return None @@ -84,7 +84,7 @@ class ValloxFanSpeedSensor(ValloxSensor): """Child class for fan speed reporting.""" @property - def native_value(self) -> StateType: + def native_value(self) -> StateType | datetime: """Return the value reported by the sensor.""" fan_is_on = self.coordinator.data.get_metric(METRIC_KEY_MODE) == MODE_ON return super().native_value if fan_is_on else 0 @@ -94,7 +94,7 @@ class ValloxFilterRemainingSensor(ValloxSensor): """Child class for filter remaining time reporting.""" @property - def native_value(self) -> StateType: + def native_value(self) -> StateType | datetime: """Return the value reported by the sensor.""" super_native_value = super().native_value @@ -107,7 +107,7 @@ class ValloxFilterRemainingSensor(ValloxSensor): days_remaining_delta = timedelta(days=days_remaining) now = datetime.utcnow().replace(hour=13, minute=0, second=0, microsecond=0) - return (now + days_remaining_delta).isoformat() + return now + days_remaining_delta class ValloxCellStateSensor(ValloxSensor): From 566cf9785edc6c9fa81ac1c63920a9ef897683c7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:35:38 -0800 Subject: [PATCH 0133/2644] Fix modern_forms timestmap sensors (#61211) --- homeassistant/components/modern_forms/sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/modern_forms/sensor.py b/homeassistant/components/modern_forms/sensor.py index 1e51ec9a1ae..5c9e0a18575 100644 --- a/homeassistant/components/modern_forms/sensor.py +++ b/homeassistant/components/modern_forms/sensor.py @@ -73,7 +73,7 @@ class ModernFormsLightTimerRemainingTimeSensor(ModernFormsSensor): self._attr_device_class = DEVICE_CLASS_TIMESTAMP @property - def native_value(self) -> StateType: + def native_value(self) -> StateType | datetime: """Return the state of the sensor.""" sleep_time: datetime = dt_util.utc_from_timestamp( self.coordinator.data.state.light_sleep_timer @@ -83,7 +83,7 @@ class ModernFormsLightTimerRemainingTimeSensor(ModernFormsSensor): or (sleep_time - dt_util.utcnow()).total_seconds() < 0 ): return None - return sleep_time.isoformat() + return sleep_time class ModernFormsFanTimerRemainingTimeSensor(ModernFormsSensor): @@ -103,7 +103,7 @@ class ModernFormsFanTimerRemainingTimeSensor(ModernFormsSensor): self._attr_device_class = DEVICE_CLASS_TIMESTAMP @property - def native_value(self) -> StateType: + def native_value(self) -> StateType | datetime: """Return the state of the sensor.""" sleep_time: datetime = dt_util.utc_from_timestamp( self.coordinator.data.state.fan_sleep_timer @@ -115,4 +115,4 @@ class ModernFormsFanTimerRemainingTimeSensor(ModernFormsSensor): ): return None - return sleep_time.isoformat() + return sleep_time From 2c451e6a76198bfd95ea2d05b25270bd6a973980 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:38:35 -0800 Subject: [PATCH 0134/2644] Fix litterrobot timestamp sensor (#61208) * Fix litterrobot timestamp sensor * Update type --- homeassistant/components/litterrobot/sensor.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/litterrobot/sensor.py b/homeassistant/components/litterrobot/sensor.py index 1fab6983249..6b4dc1b3300 100644 --- a/homeassistant/components/litterrobot/sensor.py +++ b/homeassistant/components/litterrobot/sensor.py @@ -1,9 +1,11 @@ """Support for Litter-Robot sensors.""" from __future__ import annotations +from datetime import datetime + from pylitterbot.robot import Robot -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorEntity, StateType from homeassistant.config_entries import ConfigEntry from homeassistant.const import DEVICE_CLASS_TIMESTAMP, PERCENTAGE from homeassistant.core import HomeAssistant @@ -36,7 +38,7 @@ class LitterRobotPropertySensor(LitterRobotEntity, SensorEntity): self.sensor_attribute = sensor_attribute @property - def native_value(self) -> str: + def native_value(self) -> StateType | datetime: """Return the state.""" return getattr(self.robot, self.sensor_attribute) @@ -59,10 +61,10 @@ class LitterRobotSleepTimeSensor(LitterRobotPropertySensor): """Litter-Robot sleep time sensor.""" @property - def native_value(self) -> str | None: + def native_value(self) -> StateType | datetime: """Return the state.""" if self.robot.sleep_mode_enabled: - return super().native_value.isoformat() + return super().native_value return None @property From 271b798dc9de5e2a6d853d8f0019c049e2f4491f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:46:45 -0800 Subject: [PATCH 0135/2644] Fix lyric timestamp sensor (#61209) * Fix lyric timestamp sensor * Update type --- homeassistant/components/lyric/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lyric/sensor.py b/homeassistant/components/lyric/sensor.py index 6f550813ad8..be156594524 100644 --- a/homeassistant/components/lyric/sensor.py +++ b/homeassistant/components/lyric/sensor.py @@ -47,7 +47,7 @@ LYRIC_SETPOINT_STATUS_NAMES = { class LyricSensorEntityDescription(SensorEntityDescription): """Class describing Honeywell Lyric sensor entities.""" - value: Callable[[LyricDevice], StateType] = round + value: Callable[[LyricDevice], StateType | datetime] = round def get_datetime_from_future_time(time: str) -> datetime: @@ -133,7 +133,7 @@ async def async_setup_entry( device_class=DEVICE_CLASS_TIMESTAMP, value=lambda device: get_datetime_from_future_time( device.changeableValues.nextPeriodTime - ).isoformat(), + ), ), location, device, From fa75c1f92f62b3a41caca209fc9b15100866db96 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:47:38 -0800 Subject: [PATCH 0136/2644] Fix bbox timestamp (#61202) --- homeassistant/components/bbox/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/bbox/sensor.py b/homeassistant/components/bbox/sensor.py index 53a7e8720b1..129803ad1e1 100644 --- a/homeassistant/components/bbox/sensor.py +++ b/homeassistant/components/bbox/sensor.py @@ -134,7 +134,7 @@ class BboxUptimeSensor(SensorEntity): uptime = utcnow() - timedelta( seconds=self.bbox_data.router_infos["device"]["uptime"] ) - self._attr_native_value = uptime.replace(microsecond=0).isoformat() + self._attr_native_value = uptime.replace(microsecond=0) class BboxSensor(SensorEntity): From 113a850c6920048a2ebd33d1b61572ea935e6ba2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:48:04 -0800 Subject: [PATCH 0137/2644] Fix flipr timestamp sensor (#61203) --- homeassistant/components/flipr/sensor.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/homeassistant/components/flipr/sensor.py b/homeassistant/components/flipr/sensor.py index e79ba131618..527742539c5 100644 --- a/homeassistant/components/flipr/sensor.py +++ b/homeassistant/components/flipr/sensor.py @@ -1,8 +1,6 @@ """Sensor platform for the Flipr's pool_sensor.""" from __future__ import annotations -from datetime import datetime - from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, @@ -60,7 +58,4 @@ class FliprSensor(FliprEntity, SensorEntity): @property def native_value(self): """State of the sensor.""" - state = self.coordinator.data[self.entity_description.key] - if isinstance(state, datetime): - return state.isoformat() - return state + return self.coordinator.data[self.entity_description.key] From e0110737897154e2fa9b7086d5d787681ff86e16 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:49:43 -0800 Subject: [PATCH 0138/2644] Fix gtfs timestamp sensor (#61204) --- homeassistant/components/gtfs/sensor.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/gtfs/sensor.py b/homeassistant/components/gtfs/sensor.py index 9450c717148..9a622e417ad 100644 --- a/homeassistant/components/gtfs/sensor.py +++ b/homeassistant/components/gtfs/sensor.py @@ -544,7 +544,7 @@ class GTFSDepartureSensor(SensorEntity): self._available = False self._icon = ICON self._name = "" - self._state: str | None = None + self._state: datetime.datetime | None = None self._attributes: dict[str, Any] = {} self._agency = None @@ -563,7 +563,7 @@ class GTFSDepartureSensor(SensorEntity): return self._name @property - def native_value(self) -> str | None: + def native_value(self) -> datetime.datetime | None: """Return the state of the sensor.""" return self._state @@ -619,9 +619,7 @@ class GTFSDepartureSensor(SensorEntity): if not self._departure: self._state = None else: - self._state = dt_util.as_utc( - self._departure["departure_time"] - ).isoformat() + self._state = dt_util.as_utc(self._departure["departure_time"]) # Fetch trip and route details once, unless updated if not self._departure: From ef70dec7a4c11b54d6c5311e661c78751cd3bef4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:55:43 -0800 Subject: [PATCH 0139/2644] Fix Rova using strings as timestamp (#61201) --- homeassistant/components/rova/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/rova/sensor.py b/homeassistant/components/rova/sensor.py index 54e2c315a4e..ca9f201b302 100644 --- a/homeassistant/components/rova/sensor.py +++ b/homeassistant/components/rova/sensor.py @@ -116,7 +116,7 @@ class RovaSensor(SensorEntity): self.data_service.update() pickup_date = self.data_service.data.get(self.entity_description.key) if pickup_date is not None: - self._attr_native_value = pickup_date.isoformat() + self._attr_native_value = pickup_date class RovaData: From 6b6b60b589df7fa262e803504a205a5eab2bd6d3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:56:07 -0800 Subject: [PATCH 0140/2644] Fix hvv_departures timestamp sensor (#61205) --- homeassistant/components/hvv_departures/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/hvv_departures/sensor.py b/homeassistant/components/hvv_departures/sensor.py index da52fd878d8..d82a15cebe9 100644 --- a/homeassistant/components/hvv_departures/sensor.py +++ b/homeassistant/components/hvv_departures/sensor.py @@ -116,7 +116,7 @@ class HVVDepartureSensor(SensorEntity): departure_time + timedelta(minutes=departure["timeOffset"]) + timedelta(seconds=delay) - ).isoformat() + ) self.attr.update( { From 1ca8df95846c43e84f65b1db520feb8a5d7b7823 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 22:56:22 -0800 Subject: [PATCH 0141/2644] Fix hydrawise timestamp sensor (#61206) --- homeassistant/components/hydrawise/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/hydrawise/sensor.py b/homeassistant/components/hydrawise/sensor.py index f8c02309569..ee9e931a351 100644 --- a/homeassistant/components/hydrawise/sensor.py +++ b/homeassistant/components/hydrawise/sensor.py @@ -83,4 +83,4 @@ class HydrawiseSensor(HydrawiseEntity, SensorEntity): _LOGGER.debug("New cycle time: %s", next_cycle) self._attr_native_value = dt.utc_from_timestamp( dt.as_timestamp(dt.now()) + next_cycle - ).isoformat() + ) From a81026ea90eacde55f035cc909d67cdf9b7875a3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 23:14:21 -0800 Subject: [PATCH 0142/2644] Fix follow-up review comment for bbox (#61219) --- homeassistant/components/bbox/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/bbox/sensor.py b/homeassistant/components/bbox/sensor.py index 129803ad1e1..fc9c8982733 100644 --- a/homeassistant/components/bbox/sensor.py +++ b/homeassistant/components/bbox/sensor.py @@ -131,10 +131,9 @@ class BboxUptimeSensor(SensorEntity): def update(self): """Get the latest data from Bbox and update the state.""" self.bbox_data.update() - uptime = utcnow() - timedelta( + self._attr_native_value = utcnow() - timedelta( seconds=self.bbox_data.router_infos["device"]["uptime"] ) - self._attr_native_value = uptime.replace(microsecond=0) class BboxSensor(SensorEntity): From c8fbf4c3392f3f78c3e39656f03110d9b2c35d71 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 23:26:45 -0800 Subject: [PATCH 0143/2644] don't convert GTFS timestamp to UTC in timestamp sensor (#61221) --- homeassistant/components/gtfs/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/gtfs/sensor.py b/homeassistant/components/gtfs/sensor.py index 9a622e417ad..367a45aa073 100644 --- a/homeassistant/components/gtfs/sensor.py +++ b/homeassistant/components/gtfs/sensor.py @@ -619,7 +619,7 @@ class GTFSDepartureSensor(SensorEntity): if not self._departure: self._state = None else: - self._state = dt_util.as_utc(self._departure["departure_time"]) + self._state = self._departure["departure_time"] # Fetch trip and route details once, unless updated if not self._departure: From 7b3a7ee2d1f55fac2e1d0b2cf9d85db0d794601e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 23:38:52 -0800 Subject: [PATCH 0144/2644] Jewish Calendar: Do not convert datetimes to UTC (#61222) --- .../components/jewish_calendar/sensor.py | 25 ++++++------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index 6db80036614..cb3a85b78cd 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -1,7 +1,7 @@ """Platform to retrieve Jewish calendar information for Home Assistant.""" from __future__ import annotations -from datetime import date as Date, datetime +from datetime import date as Date import logging from typing import Any @@ -13,7 +13,7 @@ from homeassistant.const import DEVICE_CLASS_TIMESTAMP, SUN_EVENT_SUNSET from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.sun import get_astral_event_date -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util from . import DOMAIN @@ -166,14 +166,8 @@ class JewishCalendarSensor(SensorEntity): self._candle_lighting_offset = data["candle_lighting_offset"] self._havdalah_offset = data["havdalah_offset"] self._diaspora = data["diaspora"] - self._state: datetime | None = None self._holiday_attrs: dict[str, str] = {} - @property - def native_value(self) -> datetime | StateType: - """Return the state of the sensor.""" - return self._state - async def async_update(self) -> None: """Update the state of the sensor.""" now = dt_util.now() @@ -208,8 +202,12 @@ class JewishCalendarSensor(SensorEntity): if today_times.havdalah and now > today_times.havdalah: after_tzais_date = daytime_date.next_day - self._state = self.get_state(daytime_date, after_shkia_date, after_tzais_date) - _LOGGER.debug("New value for %s: %s", self.entity_description.key, self._state) + self._attr_native_value = self.get_state( + daytime_date, after_shkia_date, after_tzais_date + ) + _LOGGER.debug( + "New value for %s: %s", self.entity_description.key, self._attr_native_value + ) def make_zmanim(self, date: Date) -> Zmanim: """Create a Zmanim object.""" @@ -259,13 +257,6 @@ class JewishCalendarTimeSensor(JewishCalendarSensor): _attr_device_class = DEVICE_CLASS_TIMESTAMP - @property - def native_value(self) -> datetime | None: - """Return the state of the sensor.""" - if self._state is None: - return None - return dt_util.as_utc(self._state) - def get_state( self, daytime_date: HDate, after_shkia_date: HDate, after_tzais_date: HDate ) -> Any | None: From b7539fc0de0cede0fce7918205566dc0123d13fc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Dec 2021 23:39:27 -0800 Subject: [PATCH 0145/2644] Fix islamic prayer times timestamp sensor (#61207) --- homeassistant/components/islamic_prayer_times/sensor.py | 6 ++---- tests/components/islamic_prayer_times/test_config_flow.py | 1 + 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/islamic_prayer_times/sensor.py b/homeassistant/components/islamic_prayer_times/sensor.py index 99cc65bb548..38a95fb803b 100644 --- a/homeassistant/components/islamic_prayer_times/sensor.py +++ b/homeassistant/components/islamic_prayer_times/sensor.py @@ -45,10 +45,8 @@ class IslamicPrayerTimeSensor(SensorEntity): @property def native_value(self): """Return the state of the sensor.""" - return ( - self.client.prayer_times_info.get(self.sensor_type) - .astimezone(dt_util.UTC) - .isoformat() + return self.client.prayer_times_info.get(self.sensor_type).astimezone( + dt_util.UTC ) async def async_added_to_hass(self): diff --git a/tests/components/islamic_prayer_times/test_config_flow.py b/tests/components/islamic_prayer_times/test_config_flow.py index 842a877e292..18d64842c65 100644 --- a/tests/components/islamic_prayer_times/test_config_flow.py +++ b/tests/components/islamic_prayer_times/test_config_flow.py @@ -5,6 +5,7 @@ import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components import islamic_prayer_times +from homeassistant.components.islamic_prayer_times import config_flow # noqa: F401 from homeassistant.components.islamic_prayer_times.const import CONF_CALC_METHOD, DOMAIN from tests.common import MockConfigEntry From fad531415443d2283b0a3199a562207a56932111 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Dec 2021 09:06:06 +0100 Subject: [PATCH 0146/2644] Bump actions/upload-artifact from 2.2.4 to 2.3.0 (#61215) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 2 +- .github/workflows/wheels.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f36324a7b9d..3a283a856f6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -712,7 +712,7 @@ jobs: -p no:sugar \ tests/components/${{ matrix.group }} - name: Upload coverage artifact - uses: actions/upload-artifact@v2.2.4 + uses: actions/upload-artifact@v2.3.0 with: name: coverage-${{ matrix.python-version }}-${{ matrix.group }} path: coverage.xml diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 56b09886736..d118d00d9de 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -45,13 +45,13 @@ jobs: ) > .env_file - name: Upload env_file - uses: actions/upload-artifact@v2.2.4 + uses: actions/upload-artifact@v2.3.0 with: name: env_file path: ./.env_file - name: Upload requirements_diff - uses: actions/upload-artifact@v2.2.4 + uses: actions/upload-artifact@v2.3.0 with: name: requirements_diff path: ./requirements_diff.txt From d5aa4a9ce1300bea01ff1ac665cafe8dff6d3558 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 Dec 2021 22:30:22 -1000 Subject: [PATCH 0147/2644] Updating naming for flux_led (#61187) --- homeassistant/components/flux_led/config_flow.py | 2 +- homeassistant/components/flux_led/entity.py | 4 ++-- homeassistant/components/flux_led/light.py | 2 +- homeassistant/components/flux_led/manifest.json | 2 +- homeassistant/components/flux_led/switch.py | 2 +- homeassistant/components/flux_led/util.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/flux_led/config_flow.py b/homeassistant/components/flux_led/config_flow.py index 87d07bba2b1..b6efc763b6d 100644 --- a/homeassistant/components/flux_led/config_flow.py +++ b/homeassistant/components/flux_led/config_flow.py @@ -43,7 +43,7 @@ _LOGGER = logging.getLogger(__name__) class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a config flow for FluxLED/MagicHome Integration.""" + """Handle a config flow for Magic Home Integration.""" VERSION = 1 diff --git a/homeassistant/components/flux_led/entity.py b/homeassistant/components/flux_led/entity.py index f4425f3b2a2..0e70e1f05f0 100644 --- a/homeassistant/components/flux_led/entity.py +++ b/homeassistant/components/flux_led/entity.py @@ -1,4 +1,4 @@ -"""Support for FluxLED/MagicHome lights.""" +"""Support for Magic Home lights.""" from __future__ import annotations from abc import abstractmethod @@ -36,7 +36,7 @@ class FluxEntity(CoordinatorEntity): if self.unique_id: self._attr_device_info = DeviceInfo( connections={(dr.CONNECTION_NETWORK_MAC, self.unique_id)}, - manufacturer="FluxLED/Magic Home", + manufacturer="Magic Home (Zengge)", model=self._device.model, name=self.name, sw_version=str(self._device.version_num), diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index d364d8b9581..b138d41419d 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -1,4 +1,4 @@ -"""Support for FluxLED/MagicHome lights.""" +"""Support for Magic Home lights.""" from __future__ import annotations import ast diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 92a3024869c..538ababcb98 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -1,6 +1,6 @@ { "domain": "flux_led", - "name": "Flux LED/MagicHome", + "name": "Magic Home", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", "requirements": ["flux_led==0.26.2"], diff --git a/homeassistant/components/flux_led/switch.py b/homeassistant/components/flux_led/switch.py index d022acc1c74..01473bfc67c 100644 --- a/homeassistant/components/flux_led/switch.py +++ b/homeassistant/components/flux_led/switch.py @@ -1,4 +1,4 @@ -"""Support for FluxLED/MagicHome switches.""" +"""Support for Magic Home switches.""" from __future__ import annotations from typing import Any diff --git a/homeassistant/components/flux_led/util.py b/homeassistant/components/flux_led/util.py index 774ae1aaa53..818b5c45506 100644 --- a/homeassistant/components/flux_led/util.py +++ b/homeassistant/components/flux_led/util.py @@ -1,4 +1,4 @@ -"""Utils for FluxLED/MagicHome.""" +"""Utils for Magic Home.""" from __future__ import annotations from flux_led.aio import AIOWifiLedBulb From 9a46e802b7ab354778fe1cd45f86b623af7507e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 8 Dec 2021 10:04:13 +0100 Subject: [PATCH 0148/2644] Address late review of Adax (#61200) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/adax/config_flow.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/adax/config_flow.py b/homeassistant/components/adax/config_flow.py index 2788b563678..e3921258426 100644 --- a/homeassistant/components/adax/config_flow.py +++ b/homeassistant/components/adax/config_flow.py @@ -75,12 +75,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): configurator = adax_local.AdaxConfig(wifi_ssid, wifi_pswd) try: - if not await configurator.configure_device(): - return self.async_show_form( - step_id="local", - data_schema=data_schema, - errors={"base": "cannot_connect"}, - ) + device_configured = await configurator.configure_device() except adax_local.HeaterNotAvailable: return self.async_abort(reason="heater_not_available") except adax_local.HeaterNotFound: @@ -88,6 +83,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except adax_local.InvalidWifiCred: return self.async_abort(reason="invalid_auth") + if not device_configured: + return self.async_show_form( + step_id="local", + data_schema=data_schema, + errors={"base": "cannot_connect"}, + ) + unique_id = configurator.mac_id await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured() From 9f15e7dcf4c0009954874a7fe35abb5a11c71094 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 8 Dec 2021 10:50:56 +0100 Subject: [PATCH 0149/2644] Fix flaky tests around frame helper (#61179) Co-authored-by: epenet --- tests/helpers/test_aiohttp_client.py | 2 ++ tests/helpers/test_httpx_client.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/tests/helpers/test_aiohttp_client.py b/tests/helpers/test_aiohttp_client.py index f68c7ba2181..bfd933a4afd 100644 --- a/tests/helpers/test_aiohttp_client.py +++ b/tests/helpers/test_aiohttp_client.py @@ -107,6 +107,7 @@ async def test_get_clientsession_patched_close(hass): assert mock_close.call_count == 0 +@patch("homeassistant.helpers.frame._REPORTED_INTEGRATIONS", set()) async def test_warning_close_session_integration(hass, caplog): """Test log warning message when closing the session from integration context.""" with patch( @@ -138,6 +139,7 @@ async def test_warning_close_session_integration(hass, caplog): ) in caplog.text +@patch("homeassistant.helpers.frame._REPORTED_INTEGRATIONS", set()) async def test_warning_close_session_custom(hass, caplog): """Test log warning message when closing the session from custom context.""" with patch( diff --git a/tests/helpers/test_httpx_client.py b/tests/helpers/test_httpx_client.py index a47463b6b98..cdb650f7686 100644 --- a/tests/helpers/test_httpx_client.py +++ b/tests/helpers/test_httpx_client.py @@ -93,6 +93,7 @@ async def test_get_async_client_context_manager(hass): assert mock_aclose.call_count == 0 +@patch("homeassistant.helpers.frame._REPORTED_INTEGRATIONS", set()) async def test_warning_close_session_integration(hass, caplog): """Test log warning message when closing the session from integration context.""" with patch( @@ -125,6 +126,7 @@ async def test_warning_close_session_integration(hass, caplog): ) in caplog.text +@patch("homeassistant.helpers.frame._REPORTED_INTEGRATIONS", set()) async def test_warning_close_session_custom(hass, caplog): """Test log warning message when closing the session from custom context.""" with patch( From 5169ee69c7fdb346c6272457642054b493e73543 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 8 Dec 2021 16:08:02 +0100 Subject: [PATCH 0150/2644] Attempt to fix flaky prometheus test (#61242) --- tests/components/prometheus/test_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index f9f6ebbf0f5..df8c07bfc73 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -45,11 +45,11 @@ async def prometheus_client(hass, hass_client, namespace): await async_setup_component( hass, climate.DOMAIN, {"climate": [{"platform": "demo"}]} ) - await hass.async_block_till_done() await async_setup_component( hass, humidifier.DOMAIN, {"humidifier": [{"platform": "demo"}]} ) + await hass.async_block_till_done() sensor1 = DemoSensor( None, "Television Energy", 74, None, None, ENERGY_KILO_WATT_HOUR, None From bbe6d3c9aec10156b0bb570f380db4dbfbe91c17 Mon Sep 17 00:00:00 2001 From: Thomas Dietrich Date: Wed, 8 Dec 2021 16:53:18 +0100 Subject: [PATCH 0151/2644] Statistics refactor testcases in async pytest style (#60935) * Implement optional manually defined uniqueid * Fix test case via mocked environment * Refactor testcases * Fix missing awaits * Revert order changes, reduce use of block command * Tidy up mocked time testcases --- tests/components/statistics/test_sensor.py | 1921 +++++++++----------- 1 file changed, 903 insertions(+), 1018 deletions(-) diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index 50f6993e44c..e3ace35f0ae 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -1,15 +1,12 @@ """The test for the statistics sensor platform.""" from datetime import datetime, timedelta import statistics -import unittest from unittest.mock import patch -import pytest - from homeassistant import config as hass_config -from homeassistant.components import recorder from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT -from homeassistant.components.statistics.sensor import DOMAIN, StatisticsSensor +from homeassistant.components.statistics import DOMAIN as STATISTICS_DOMAIN +from homeassistant.components.statistics.sensor import StatisticsSensor from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, SERVICE_RELOAD, @@ -18,22 +15,18 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.helpers import entity_registry as er -from homeassistant.setup import async_setup_component, setup_component +from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from tests.common import ( - fire_time_changed, + async_fire_time_changed, + async_init_recorder_component, get_fixture_path, - get_test_home_assistant, - init_recorder_component, ) -from tests.components.recorder.common import wait_recording_done +from tests.components.recorder.common import async_wait_recording_done_without_instance - -@pytest.fixture(autouse=True) -def mock_legacy_time(legacy_patchable_time): - """Make time patchable for all the tests.""" - yield +VALUES_BINARY = ["on", "off", "on", "off", "on", "off", "on", "off", "on"] +VALUES_NUMERIC = [17, 20, 15.2, 5, 3.8, 9.2, 6.7, 14, 6] async def test_unique_id(hass): @@ -55,936 +48,259 @@ async def test_unique_id(hass): await hass.async_block_till_done() entity_reg = er.async_get(hass) - entity_id = entity_reg.async_get_entity_id("sensor", DOMAIN, "uniqueid_sensor_test") + entity_id = entity_reg.async_get_entity_id( + "sensor", STATISTICS_DOMAIN, "uniqueid_sensor_test" + ) assert entity_id == "sensor.test" -class TestStatisticsSensor(unittest.TestCase): - """Test the Statistics sensor.""" - - def setup_method(self, method): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.values_binary = ["on", "off", "on", "off", "on", "off", "on", "off", "on"] - self.mean_binary = round( - 100 / len(self.values_binary) * self.values_binary.count("on"), 2 - ) - self.values = [17, 20, 15.2, 5, 3.8, 9.2, 6.7, 14, 6] - self.mean = round(sum(self.values) / len(self.values), 2) - self.addCleanup(self.hass.stop) - - def test_sensor_defaults_numeric(self): - """Test the general behavior of the sensor, with numeric source sensor.""" - assert setup_component( - self.hass, - "sensor", - { - "sensor": [ - { - "platform": "statistics", - "name": "test", - "entity_id": "sensor.test_monitored", - }, - ] - }, - ) - - self.hass.block_till_done() - self.hass.start() - self.hass.block_till_done() - - for value in self.values: - self.hass.states.set( - "sensor.test_monitored", - value, - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, - ) - self.hass.block_till_done() - - state = self.hass.states.get("sensor.test") - assert state.state == str(self.mean) - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT - assert state.attributes.get("buffer_usage_ratio") == round(9 / 20, 2) - assert state.attributes.get("source_value_valid") is True - assert "age_coverage_ratio" not in state.attributes - - # Source sensor turns unavailable, then available with valid value, - # statistics sensor should follow - state = self.hass.states.get("sensor.test") - self.hass.states.set( - "sensor.test_monitored", - STATE_UNAVAILABLE, - ) - self.hass.block_till_done() - new_state = self.hass.states.get("sensor.test") - assert new_state.state == STATE_UNAVAILABLE - assert new_state.attributes.get("source_value_valid") is None - self.hass.states.set( - "sensor.test_monitored", - 0, - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, - ) - self.hass.block_till_done() - new_state = self.hass.states.get("sensor.test") - new_mean = round(sum(self.values) / (len(self.values) + 1), 2) - assert new_state.state == str(new_mean) - assert new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS - assert new_state.attributes.get("buffer_usage_ratio") == round(10 / 20, 2) - assert new_state.attributes.get("source_value_valid") is True - - # Source sensor has a nonnumerical state, unit and state should not change - state = self.hass.states.get("sensor.test") - self.hass.states.set("sensor.test_monitored", "beer", {}) - self.hass.block_till_done() - new_state = self.hass.states.get("sensor.test") - assert new_state.state == str(new_mean) - assert new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS - assert new_state.attributes.get("source_value_valid") is False - - # Source sensor has the STATE_UNKNOWN state, unit and state should not change - state = self.hass.states.get("sensor.test") - self.hass.states.set("sensor.test_monitored", STATE_UNKNOWN, {}) - self.hass.block_till_done() - new_state = self.hass.states.get("sensor.test") - assert new_state.state == str(new_mean) - assert new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS - assert new_state.attributes.get("source_value_valid") is False - - # Source sensor is removed, unit and state should not change - # This is equal to a None value being published - self.hass.states.remove("sensor.test_monitored") - self.hass.block_till_done() - new_state = self.hass.states.get("sensor.test") - assert new_state.state == str(new_mean) - assert new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS - assert new_state.attributes.get("source_value_valid") is False - - def test_sensor_defaults_binary(self): - """Test the general behavior of the sensor, with binary source sensor.""" - assert setup_component( - self.hass, - "sensor", - { - "sensor": [ - { - "platform": "statistics", - "name": "test", - "entity_id": "binary_sensor.test_monitored", - }, - ] - }, - ) - - self.hass.block_till_done() - self.hass.start() - self.hass.block_till_done() - for value in self.values_binary: - self.hass.states.set( - "binary_sensor.test_monitored", - value, - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, - ) - self.hass.block_till_done() - - state = self.hass.states.get("sensor.test") - assert state.state == str(len(self.values_binary)) - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT - assert state.attributes.get("buffer_usage_ratio") == round(9 / 20, 2) - assert state.attributes.get("source_value_valid") is True - assert "age_coverage_ratio" not in state.attributes - - def test_sensor_source_with_force_update(self): - """Test the behavior of the sensor when the source sensor force-updates with same value.""" - repeating_values = [18, 0, 0, 0, 0, 0, 0, 0, 9] - assert setup_component( - self.hass, - "sensor", - { - "sensor": [ - { - "platform": "statistics", - "name": "test_normal", - "entity_id": "sensor.test_monitored_normal", - "state_characteristic": "mean", - }, - { - "platform": "statistics", - "name": "test_force", - "entity_id": "sensor.test_monitored_force", - "state_characteristic": "mean", - }, - ] - }, - ) - - self.hass.block_till_done() - self.hass.start() - self.hass.block_till_done() - - for value in repeating_values: - self.hass.states.set( - "sensor.test_monitored_normal", - value, - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, - ) - self.hass.states.set( - "sensor.test_monitored_force", - value, - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, - force_update=True, - ) - self.hass.block_till_done() - - state_normal = self.hass.states.get("sensor.test_normal") - state_force = self.hass.states.get("sensor.test_force") - assert state_normal.state == str(round(sum(repeating_values) / 3, 2)) - assert state_force.state == str(round(sum(repeating_values) / 9, 2)) - assert state_normal.attributes.get("buffer_usage_ratio") == round(3 / 20, 2) - assert state_force.attributes.get("buffer_usage_ratio") == round(9 / 20, 2) - - def test_sampling_size_non_default(self): - """Test rotation.""" - assert setup_component( - self.hass, - "sensor", - { - "sensor": [ - { - "platform": "statistics", - "name": "test", - "entity_id": "sensor.test_monitored", - "state_characteristic": "mean", - "sampling_size": 5, - }, - ] - }, - ) - - self.hass.block_till_done() - self.hass.start() - self.hass.block_till_done() - - for value in self.values: - self.hass.states.set( - "sensor.test_monitored", - value, - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, - ) - self.hass.block_till_done() - - state = self.hass.states.get("sensor.test") - new_mean = round(sum(self.values[-5:]) / len(self.values[-5:]), 2) - assert state.state == str(new_mean) - assert state.attributes.get("buffer_usage_ratio") == round(5 / 5, 2) - - def test_sampling_size_1(self): - """Test validity of stats requiring only one sample.""" - assert setup_component( - self.hass, - "sensor", - { - "sensor": [ - { - "platform": "statistics", - "name": "test", - "entity_id": "sensor.test_monitored", - "state_characteristic": "mean", - "sampling_size": 1, - }, - ] - }, - ) - - self.hass.block_till_done() - self.hass.start() - self.hass.block_till_done() - - for value in self.values[-3:]: # just the last 3 will do - self.hass.states.set( - "sensor.test_monitored", - value, - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, - ) - self.hass.block_till_done() - - state = self.hass.states.get("sensor.test") - new_mean = float(self.values[-1]) - assert state.state == str(new_mean) - assert state.attributes.get("buffer_usage_ratio") == round(1 / 1, 2) - - def test_age_limit_expiry(self): - """Test that values are removed after certain age.""" - now = dt_util.utcnow() - mock_data = { - "return_time": datetime(now.year + 1, 8, 2, 12, 23, tzinfo=dt_util.UTC) - } - - def mock_now(): - return mock_data["return_time"] - - with patch( - "homeassistant.components.statistics.sensor.dt_util.utcnow", new=mock_now - ): - assert setup_component( - self.hass, - "sensor", - { - "sensor": [ - { - "platform": "statistics", - "name": "test", - "entity_id": "sensor.test_monitored", - "state_characteristic": "mean", - "max_age": {"minutes": 4}, - }, - ] - }, - ) - - self.hass.block_till_done() - self.hass.start() - self.hass.block_till_done() - - for value in self.values: - self.hass.states.set( - "sensor.test_monitored", - value, - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, - ) - self.hass.block_till_done() - mock_data["return_time"] += timedelta(minutes=1) - - # After adding all values, we should only see 5 values in memory - - state = self.hass.states.get("sensor.test") - new_mean = round(sum(self.values[-5:]) / len(self.values[-5:]), 2) - assert state.state == str(new_mean) - assert state.attributes.get("buffer_usage_ratio") == round(5 / 20, 2) - assert state.attributes.get("age_coverage_ratio") == 1.0 - - # Values expire over time. Only two are left - - mock_data["return_time"] += timedelta(minutes=2) - fire_time_changed(self.hass, mock_data["return_time"]) - self.hass.block_till_done() - - state = self.hass.states.get("sensor.test") - new_mean = round(sum(self.values[-2:]) / len(self.values[-2:]), 2) - assert state.state == str(new_mean) - assert state.attributes.get("buffer_usage_ratio") == round(2 / 20, 2) - assert state.attributes.get("age_coverage_ratio") == 1 / 4 - - # Values expire over time. Only one is left - - mock_data["return_time"] += timedelta(minutes=1) - fire_time_changed(self.hass, mock_data["return_time"]) - self.hass.block_till_done() - - state = self.hass.states.get("sensor.test") - new_mean = float(self.values[-1]) - assert state.state == str(new_mean) - assert state.attributes.get("buffer_usage_ratio") == round(1 / 20, 2) - assert state.attributes.get("age_coverage_ratio") == 0 - - # Values expire over time. Memory is empty - - mock_data["return_time"] += timedelta(minutes=1) - fire_time_changed(self.hass, mock_data["return_time"]) - self.hass.block_till_done() - - state = self.hass.states.get("sensor.test") - assert state.state == STATE_UNKNOWN - assert state.attributes.get("buffer_usage_ratio") == round(0 / 20, 2) - assert state.attributes.get("age_coverage_ratio") is None - - def test_precision_0(self): - """Test correct result with precision=0 as integer.""" - assert setup_component( - self.hass, - "sensor", - { - "sensor": [ - { - "platform": "statistics", - "name": "test", - "entity_id": "sensor.test_monitored", - "state_characteristic": "mean", - "precision": 0, - }, - ] - }, - ) - - self.hass.block_till_done() - self.hass.start() - self.hass.block_till_done() - - for value in self.values: - self.hass.states.set( - "sensor.test_monitored", - value, - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, - ) - self.hass.block_till_done() - - state = self.hass.states.get("sensor.test") - assert state.state == str(int(round(self.mean))) - - def test_precision_1(self): - """Test correct result with precision=1 rounded to one decimal.""" - assert setup_component( - self.hass, - "sensor", - { - "sensor": [ - { - "platform": "statistics", - "name": "test", - "entity_id": "sensor.test_monitored", - "state_characteristic": "mean", - "precision": 1, - }, - ] - }, - ) - - self.hass.block_till_done() - self.hass.start() - self.hass.block_till_done() - - for value in self.values: - self.hass.states.set( - "sensor.test_monitored", - value, - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, - ) - self.hass.block_till_done() - - state = self.hass.states.get("sensor.test") - assert state.state == str(round(sum(self.values) / len(self.values), 1)) - - def test_state_class(self): - """Test state class, which depends on the characteristic configured.""" - assert setup_component( - self.hass, - "sensor", - { - "sensor": [ - { - "platform": "statistics", - "name": "test_normal", - "entity_id": "sensor.test_monitored", - "state_characteristic": "count", - }, - { - "platform": "statistics", - "name": "test_nan", - "entity_id": "sensor.test_monitored", - "state_characteristic": "datetime_oldest", - }, - ] - }, - ) - - self.hass.block_till_done() - self.hass.start() - self.hass.block_till_done() - - for value in self.values: - self.hass.states.set( - "sensor.test_monitored", - value, - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, - ) - self.hass.block_till_done() - - state = self.hass.states.get("sensor.test_normal") - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT - state = self.hass.states.get("sensor.test_nan") - assert state.attributes.get(ATTR_STATE_CLASS) is None - - def test_unitless_source_sensor(self): - """Statistics for a unitless source sensor should never have a unit.""" - assert setup_component( - self.hass, - "sensor", - { - "sensor": [ - { - "platform": "statistics", - "name": "test_unitless_1", - "entity_id": "sensor.test_monitored_unitless", - "state_characteristic": "count", - }, - { - "platform": "statistics", - "name": "test_unitless_2", - "entity_id": "sensor.test_monitored_unitless", - "state_characteristic": "mean", - }, - { - "platform": "statistics", - "name": "test_unitless_3", - "entity_id": "sensor.test_monitored_unitless", - "state_characteristic": "change_second", - }, - { - "platform": "statistics", - "name": "test_unitless_4", - "entity_id": "binary_sensor.test_monitored_unitless", - }, - { - "platform": "statistics", - "name": "test_unitless_5", - "entity_id": "binary_sensor.test_monitored_unitless", - "state_characteristic": "mean", - }, - ] - }, - ) - - self.hass.block_till_done() - self.hass.start() - self.hass.block_till_done() - - for value in self.values: - self.hass.states.set( - "sensor.test_monitored_unitless", - value, - ) - self.hass.block_till_done() - for value in self.values_binary: - self.hass.states.set( - "binary_sensor.test_monitored_unitless", - value, - ) - self.hass.block_till_done() - - state = self.hass.states.get("sensor.test_unitless_1") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - state = self.hass.states.get("sensor.test_unitless_2") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - state = self.hass.states.get("sensor.test_unitless_3") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - state = self.hass.states.get("sensor.test_unitless_4") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - state = self.hass.states.get("sensor.test_unitless_5") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "%" - - def test_state_characteristics(self): - """Test configured state characteristic for value and unit.""" - now = dt_util.utcnow() - mock_data = { - "return_time": datetime(now.year + 1, 8, 2, 12, 23, 42, tzinfo=dt_util.UTC) - } - - def mock_now(): - return mock_data["return_time"] - - value_spacing_minutes = 1 - - characteristics = ( - { - "source_sensor_domain": "sensor", - "name": "average_linear", - "value_0": STATE_UNKNOWN, - "value_1": STATE_UNKNOWN, - "value_9": 10.68, - "unit": "°C", - }, - { - "source_sensor_domain": "sensor", - "name": "average_step", - "value_0": STATE_UNKNOWN, - "value_1": STATE_UNKNOWN, - "value_9": 11.36, - "unit": "°C", - }, - { - "source_sensor_domain": "sensor", - "name": "average_timeless", - "value_0": STATE_UNKNOWN, - "value_1": float(self.values[0]), - "value_9": float(self.mean), - "unit": "°C", - }, - { - "source_sensor_domain": "sensor", - "name": "change", - "value_0": STATE_UNKNOWN, - "value_1": float(0), - "value_9": float(round(self.values[-1] - self.values[0], 2)), - "unit": "°C", - }, - { - "source_sensor_domain": "sensor", - "name": "change_sample", - "value_0": STATE_UNKNOWN, - "value_1": STATE_UNKNOWN, - "value_9": float( - round( - (self.values[-1] - self.values[0]) / (len(self.values) - 1), 2 - ) - ), - "unit": "°C/sample", - }, - { - "source_sensor_domain": "sensor", - "name": "change_second", - "value_0": STATE_UNKNOWN, - "value_1": STATE_UNKNOWN, - "value_9": float( - round( - (self.values[-1] - self.values[0]) - / (60 * (len(self.values) - 1)), - 2, - ) - ), - "unit": "°C/s", - }, - { - "source_sensor_domain": "sensor", - "name": "count", - "value_0": 0, - "value_1": 1, - "value_9": len(self.values), - "unit": None, - }, - { - "source_sensor_domain": "sensor", - "name": "datetime_newest", - "value_0": STATE_UNKNOWN, - "value_1": datetime( - now.year + 1, - 8, - 2, - 12, - 23 + len(self.values) + 10, - 42, - tzinfo=dt_util.UTC, - ), - "value_9": datetime( - now.year + 1, - 8, - 2, - 12, - 23 + len(self.values) - 1, - 42, - tzinfo=dt_util.UTC, - ), - "unit": None, - }, - { - "source_sensor_domain": "sensor", - "name": "datetime_oldest", - "value_0": STATE_UNKNOWN, - "value_1": datetime( - now.year + 1, - 8, - 2, - 12, - 23 + len(self.values) + 10, - 42, - tzinfo=dt_util.UTC, - ), - "value_9": datetime(now.year + 1, 8, 2, 12, 23, 42, tzinfo=dt_util.UTC), - "unit": None, - }, - { - "source_sensor_domain": "sensor", - "name": "distance_95_percent_of_values", - "value_0": STATE_UNKNOWN, - "value_1": STATE_UNKNOWN, - "value_9": float(round(2 * 1.96 * statistics.stdev(self.values), 2)), - "unit": "°C", - }, - { - "source_sensor_domain": "sensor", - "name": "distance_99_percent_of_values", - "value_0": STATE_UNKNOWN, - "value_1": STATE_UNKNOWN, - "value_9": float(round(2 * 2.58 * statistics.stdev(self.values), 2)), - "unit": "°C", - }, - { - "source_sensor_domain": "sensor", - "name": "distance_absolute", - "value_0": STATE_UNKNOWN, - "value_1": float(0), - "value_9": float(max(self.values) - min(self.values)), - "unit": "°C", - }, - { - "source_sensor_domain": "sensor", - "name": "mean", - "value_0": STATE_UNKNOWN, - "value_1": float(self.values[0]), - "value_9": float(self.mean), - "unit": "°C", - }, - { - "source_sensor_domain": "sensor", - "name": "median", - "value_0": STATE_UNKNOWN, - "value_1": float(self.values[0]), - "value_9": float(round(statistics.median(self.values), 2)), - "unit": "°C", - }, - { - "source_sensor_domain": "sensor", - "name": "noisiness", - "value_0": STATE_UNKNOWN, - "value_1": STATE_UNKNOWN, - "value_9": float( - round(sum([3, 4.8, 10.2, 1.2, 5.4, 2.5, 7.3, 8]) / 8, 2) - ), - "unit": "°C", - }, - { - "source_sensor_domain": "sensor", - "name": "quantiles", - "value_0": STATE_UNKNOWN, - "value_1": STATE_UNKNOWN, - "value_9": [ - round(quantile, 2) for quantile in statistics.quantiles(self.values) - ], - "unit": None, - }, - { - "source_sensor_domain": "sensor", - "name": "standard_deviation", - "value_0": STATE_UNKNOWN, - "value_1": STATE_UNKNOWN, - "value_9": float(round(statistics.stdev(self.values), 2)), - "unit": "°C", - }, - { - "source_sensor_domain": "sensor", - "name": "total", - "value_0": STATE_UNKNOWN, - "value_1": float(self.values[0]), - "value_9": float(sum(self.values)), - "unit": "°C", - }, - { - "source_sensor_domain": "sensor", - "name": "value_max", - "value_0": STATE_UNKNOWN, - "value_1": float(self.values[0]), - "value_9": float(max(self.values)), - "unit": "°C", - }, - { - "source_sensor_domain": "sensor", - "name": "value_min", - "value_0": STATE_UNKNOWN, - "value_1": float(self.values[0]), - "value_9": float(min(self.values)), - "unit": "°C", - }, - { - "source_sensor_domain": "sensor", - "name": "variance", - "value_0": STATE_UNKNOWN, - "value_1": STATE_UNKNOWN, - "value_9": float(round(statistics.variance(self.values), 2)), - "unit": "°C²", - }, - { - "source_sensor_domain": "binary_sensor", - "name": "average_step", - "value_0": STATE_UNKNOWN, - "value_1": STATE_UNKNOWN, - "value_9": 50.0, - "unit": "%", - }, - { - "source_sensor_domain": "binary_sensor", - "name": "average_timeless", - "value_0": STATE_UNKNOWN, - "value_1": 100.0, - "value_9": float(self.mean_binary), - "unit": "%", - }, - { - "source_sensor_domain": "binary_sensor", - "name": "count", - "value_0": 0, - "value_1": 1, - "value_9": len(self.values_binary), - "unit": None, - }, - { - "source_sensor_domain": "binary_sensor", - "name": "mean", - "value_0": STATE_UNKNOWN, - "value_1": 100.0, - "value_9": float(self.mean_binary), - "unit": "%", - }, - ) - sensors_config = [] - for characteristic in characteristics: - sensors_config.append( +async def test_sensor_defaults_numeric(hass): + """Test the general behavior of the sensor, with numeric source sensor.""" + assert await async_setup_component( + hass, + "sensor", + { + "sensor": [ { "platform": "statistics", - "name": f"test_{characteristic['source_sensor_domain']}_{characteristic['name']}", - "entity_id": f"{characteristic['source_sensor_domain']}.test_monitored", - "state_characteristic": characteristic["name"], - "max_age": {"minutes": 10}, - } - ) + "name": "test", + "entity_id": "sensor.test_monitored", + }, + ] + }, + ) + await hass.async_block_till_done() - with patch( - "homeassistant.components.statistics.sensor.dt_util.utcnow", new=mock_now - ): - assert setup_component( - self.hass, - "sensor", - {"sensor": sensors_config}, - ) - - self.hass.block_till_done() - self.hass.start() - self.hass.block_till_done() - - # With all values in buffer - - for i in range(len(self.values)): - self.hass.states.set( - "sensor.test_monitored", - self.values[i], - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, - ) - self.hass.states.set( - "binary_sensor.test_monitored", - self.values_binary[i], - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, - ) - self.hass.block_till_done() - mock_data["return_time"] += timedelta(minutes=value_spacing_minutes) - - for characteristic in characteristics: - state = self.hass.states.get( - f"sensor.test_{characteristic['source_sensor_domain']}_{characteristic['name']}" - ) - assert state.state == str(characteristic["value_9"]), ( - f"value mismatch for characteristic " - f"'{characteristic['source_sensor_domain']}/{characteristic['name']}' " - f"(buffer filled) - " - f"assert {state.state} == {str(characteristic['value_9'])}" - ) - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == characteristic["unit"] - ), f"unit mismatch for characteristic '{characteristic['name']}'" - - # With empty buffer - - mock_data["return_time"] += timedelta(minutes=10) - fire_time_changed(self.hass, mock_data["return_time"]) - self.hass.block_till_done() - - for characteristic in characteristics: - state = self.hass.states.get( - f"sensor.test_{characteristic['source_sensor_domain']}_{characteristic['name']}" - ) - assert state.state == str(characteristic["value_0"]), ( - f"value mismatch for characteristic " - f"'{characteristic['source_sensor_domain']}/{characteristic['name']}' " - f"(buffer empty) - " - f"assert {state.state} == {str(characteristic['value_0'])}" - ) - - # With single value in buffer - - self.hass.states.set( - "sensor.test_monitored", - self.values[0], - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, - ) - self.hass.states.set( - "binary_sensor.test_monitored", - self.values_binary[0], - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, - force_update=True, - ) - mock_data["return_time"] += timedelta(minutes=1) - fire_time_changed(self.hass, mock_data["return_time"]) - self.hass.block_till_done() - - for characteristic in characteristics: - state = self.hass.states.get( - f"sensor.test_{characteristic['source_sensor_domain']}_{characteristic['name']}" - ) - assert state.state == str(characteristic["value_1"]), ( - f"value mismatch for characteristic " - f"'{characteristic['source_sensor_domain']}/{characteristic['name']}' " - f"(one stored value) - " - f"assert {state.state} == {str(characteristic['value_1'])}" - ) - - def test_invalid_state_characteristic(self): - """Test the detection of wrong state_characteristics selected.""" - assert setup_component( - self.hass, - "sensor", - { - "sensor": [ - { - "platform": "statistics", - "name": "test_numeric", - "entity_id": "sensor.test_monitored", - "state_characteristic": "invalid", - }, - { - "platform": "statistics", - "name": "test_binary", - "entity_id": "binary_sensor.test_monitored", - "state_characteristic": "variance", - }, - ] - }, - ) - - self.hass.block_till_done() - self.hass.start() - self.hass.block_till_done() - - self.hass.states.set( + for value in VALUES_NUMERIC: + hass.states.async_set( "sensor.test_monitored", - self.values[0], + value, {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, ) - self.hass.block_till_done() + await hass.async_block_till_done() - state = self.hass.states.get("sensor.test_numeric") - assert state is None - state = self.hass.states.get("sensor.test_binary") - assert state is None + state = hass.states.get("sensor.test") + assert state.state == str(round(sum(VALUES_NUMERIC) / len(VALUES_NUMERIC), 2)) + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get("buffer_usage_ratio") == round(9 / 20, 2) + assert state.attributes.get("source_value_valid") is True + assert "age_coverage_ratio" not in state.attributes - def test_initialize_from_database(self): - """Test initializing the statistics from the database.""" - # enable the recorder - init_recorder_component(self.hass) - self.hass.block_till_done() - self.hass.data[recorder.DATA_INSTANCE].block_till_done() - # store some values - for value in self.values: - self.hass.states.set( - "sensor.test_monitored", - value, - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, - ) - self.hass.block_till_done() - # wait for the recorder to really store the data - wait_recording_done(self.hass) - # only now create the statistics component, so that it must read the - # data from the database - assert setup_component( - self.hass, + # Source sensor turns unavailable, then available with valid value, + # statistics sensor should follow + state = hass.states.get("sensor.test") + hass.states.async_set( + "sensor.test_monitored", + STATE_UNAVAILABLE, + ) + await hass.async_block_till_done() + new_state = hass.states.get("sensor.test") + assert new_state.state == STATE_UNAVAILABLE + assert new_state.attributes.get("source_value_valid") is None + hass.states.async_set( + "sensor.test_monitored", + 0, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + ) + await hass.async_block_till_done() + new_state = hass.states.get("sensor.test") + new_mean = round(sum(VALUES_NUMERIC) / (len(VALUES_NUMERIC) + 1), 2) + assert new_state.state == str(new_mean) + assert new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert new_state.attributes.get("buffer_usage_ratio") == round(10 / 20, 2) + assert new_state.attributes.get("source_value_valid") is True + + # Source sensor has a nonnumerical state, unit and state should not change + state = hass.states.get("sensor.test") + hass.states.async_set("sensor.test_monitored", "beer", {}) + await hass.async_block_till_done() + new_state = hass.states.get("sensor.test") + assert new_state.state == str(new_mean) + assert new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert new_state.attributes.get("source_value_valid") is False + + # Source sensor has the STATE_UNKNOWN state, unit and state should not change + state = hass.states.get("sensor.test") + hass.states.async_set("sensor.test_monitored", STATE_UNKNOWN, {}) + await hass.async_block_till_done() + new_state = hass.states.get("sensor.test") + assert new_state.state == str(new_mean) + assert new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert new_state.attributes.get("source_value_valid") is False + + # Source sensor is removed, unit and state should not change + # This is equal to a None value being published + hass.states.async_remove("sensor.test_monitored") + await hass.async_block_till_done() + new_state = hass.states.get("sensor.test") + assert new_state.state == str(new_mean) + assert new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert new_state.attributes.get("source_value_valid") is False + + +async def test_sensor_defaults_binary(hass): + """Test the general behavior of the sensor, with binary source sensor.""" + assert await async_setup_component( + hass, + "sensor", + { + "sensor": [ + { + "platform": "statistics", + "name": "test", + "entity_id": "binary_sensor.test_monitored", + }, + ] + }, + ) + await hass.async_block_till_done() + + for value in VALUES_BINARY: + hass.states.async_set( + "binary_sensor.test_monitored", + value, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test") + assert state.state == str(len(VALUES_BINARY)) + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get("buffer_usage_ratio") == round(9 / 20, 2) + assert state.attributes.get("source_value_valid") is True + assert "age_coverage_ratio" not in state.attributes + + +async def test_sensor_source_with_force_update(hass): + """Test the behavior of the sensor when the source sensor force-updates with same value.""" + repeating_values = [18, 0, 0, 0, 0, 0, 0, 0, 9] + assert await async_setup_component( + hass, + "sensor", + { + "sensor": [ + { + "platform": "statistics", + "name": "test_normal", + "entity_id": "sensor.test_monitored_normal", + "state_characteristic": "mean", + }, + { + "platform": "statistics", + "name": "test_force", + "entity_id": "sensor.test_monitored_force", + "state_characteristic": "mean", + }, + ] + }, + ) + await hass.async_block_till_done() + + for value in repeating_values: + hass.states.async_set( + "sensor.test_monitored_normal", + value, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + ) + hass.states.async_set( + "sensor.test_monitored_force", + value, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + force_update=True, + ) + await hass.async_block_till_done() + + state_normal = hass.states.get("sensor.test_normal") + state_force = hass.states.get("sensor.test_force") + assert state_normal.state == str(round(sum(repeating_values) / 3, 2)) + assert state_force.state == str(round(sum(repeating_values) / 9, 2)) + assert state_normal.attributes.get("buffer_usage_ratio") == round(3 / 20, 2) + assert state_force.attributes.get("buffer_usage_ratio") == round(9 / 20, 2) + + +async def test_sampling_size_non_default(hass): + """Test rotation.""" + assert await async_setup_component( + hass, + "sensor", + { + "sensor": [ + { + "platform": "statistics", + "name": "test", + "entity_id": "sensor.test_monitored", + "state_characteristic": "mean", + "sampling_size": 5, + }, + ] + }, + ) + await hass.async_block_till_done() + + for value in VALUES_NUMERIC: + hass.states.async_set( + "sensor.test_monitored", + value, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test") + new_mean = round(sum(VALUES_NUMERIC[-5:]) / len(VALUES_NUMERIC[-5:]), 2) + assert state.state == str(new_mean) + assert state.attributes.get("buffer_usage_ratio") == round(5 / 5, 2) + + +async def test_sampling_size_1(hass): + """Test validity of stats requiring only one sample.""" + assert await async_setup_component( + hass, + "sensor", + { + "sensor": [ + { + "platform": "statistics", + "name": "test", + "entity_id": "sensor.test_monitored", + "state_characteristic": "mean", + "sampling_size": 1, + }, + ] + }, + ) + await hass.async_block_till_done() + + for value in VALUES_NUMERIC[-3:]: # just the last 3 will do + hass.states.async_set( + "sensor.test_monitored", + value, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test") + new_mean = float(VALUES_NUMERIC[-1]) + assert state.state == str(new_mean) + assert state.attributes.get("buffer_usage_ratio") == round(1 / 1, 2) + + +async def test_age_limit_expiry(hass): + """Test that values are removed after certain age.""" + now = dt_util.utcnow() + mock_data = { + "return_time": datetime(now.year + 1, 8, 2, 12, 23, tzinfo=dt_util.UTC) + } + + def mock_now(): + return mock_data["return_time"] + + with patch( + "homeassistant.components.statistics.sensor.dt_util.utcnow", new=mock_now + ): + assert await async_setup_component( + hass, "sensor", { "sensor": [ @@ -993,99 +309,668 @@ class TestStatisticsSensor(unittest.TestCase): "name": "test", "entity_id": "sensor.test_monitored", "state_characteristic": "mean", - "sampling_size": 100, + "max_age": {"minutes": 4}, }, ] }, ) + await hass.async_block_till_done() - self.hass.block_till_done() - self.hass.start() - self.hass.block_till_done() - - # check if the result is as in test_sensor_source() - state = self.hass.states.get("sensor.test") - assert str(self.mean) == state.state - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS - - def test_initialize_from_database_with_maxage(self): - """Test initializing the statistics from the database.""" - now = dt_util.utcnow() - mock_data = { - "return_time": datetime(now.year + 1, 8, 2, 12, 23, 42, tzinfo=dt_util.UTC) - } - - def mock_now(): - return mock_data["return_time"] - - # Testing correct retrieval from recorder, thus we do not - # want purging to occur within the class itself. - def mock_purge(self): - return - - # enable the recorder - init_recorder_component(self.hass) - self.hass.block_till_done() - self.hass.data[recorder.DATA_INSTANCE].block_till_done() - - with patch( - "homeassistant.components.statistics.sensor.dt_util.utcnow", new=mock_now - ), patch.object(StatisticsSensor, "_purge_old", mock_purge): - # store some values - for value in self.values: - self.hass.states.set( - "sensor.test_monitored", - value, - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, - ) - self.hass.block_till_done() - # insert the next value 1 hour later - mock_data["return_time"] += timedelta(hours=1) - - # wait for the recorder to really store the data - wait_recording_done(self.hass) - # only now create the statistics component, so that it must read - # the data from the database - assert setup_component( - self.hass, - "sensor", - { - "sensor": [ - { - "platform": "statistics", - "name": "test", - "entity_id": "sensor.test_monitored", - "sampling_size": 100, - "state_characteristic": "datetime_newest", - "max_age": {"hours": 3}, - }, - ] - }, + for value in VALUES_NUMERIC: + mock_data["return_time"] += timedelta(minutes=1) + async_fire_time_changed(hass, mock_data["return_time"]) + hass.states.async_set( + "sensor.test_monitored", + value, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, ) - self.hass.block_till_done() + await hass.async_block_till_done() - self.hass.block_till_done() - self.hass.start() - self.hass.block_till_done() + # After adding all values, we should only see 5 values in memory - # check if the result is as in test_sensor_source() - state = self.hass.states.get("sensor.test") + state = hass.states.get("sensor.test") + new_mean = round(sum(VALUES_NUMERIC[-5:]) / len(VALUES_NUMERIC[-5:]), 2) + assert state.state == str(new_mean) + assert state.attributes.get("buffer_usage_ratio") == round(5 / 20, 2) + assert state.attributes.get("age_coverage_ratio") == 1.0 - assert state.attributes.get("age_coverage_ratio") == round(2 / 3, 2) - # The max_age timestamp should be 1 hour before what we have right - # now in mock_data['return_time']. - assert mock_data["return_time"] == datetime.strptime( - state.state, "%Y-%m-%d %H:%M:%S%z" - ) + timedelta(hours=1) + # Values expire over time. Only two are left + + mock_data["return_time"] += timedelta(minutes=3) + async_fire_time_changed(hass, mock_data["return_time"]) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test") + new_mean = round(sum(VALUES_NUMERIC[-2:]) / len(VALUES_NUMERIC[-2:]), 2) + assert state.state == str(new_mean) + assert state.attributes.get("buffer_usage_ratio") == round(2 / 20, 2) + assert state.attributes.get("age_coverage_ratio") == 1 / 4 + + # Values expire over time. Only one is left + + mock_data["return_time"] += timedelta(minutes=1) + async_fire_time_changed(hass, mock_data["return_time"]) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test") + new_mean = float(VALUES_NUMERIC[-1]) + assert state.state == str(new_mean) + assert state.attributes.get("buffer_usage_ratio") == round(1 / 20, 2) + assert state.attributes.get("age_coverage_ratio") == 0 + + # Values expire over time. Buffer is empty + + mock_data["return_time"] += timedelta(minutes=1) + async_fire_time_changed(hass, mock_data["return_time"]) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test") + assert state.state == STATE_UNKNOWN + assert state.attributes.get("buffer_usage_ratio") == round(0 / 20, 2) + assert state.attributes.get("age_coverage_ratio") is None + + +async def test_precision(hass): + """Test correct result with precision set.""" + assert await async_setup_component( + hass, + "sensor", + { + "sensor": [ + { + "platform": "statistics", + "name": "test_precision_0", + "entity_id": "sensor.test_monitored", + "state_characteristic": "mean", + "precision": 0, + }, + { + "platform": "statistics", + "name": "test_precision_3", + "entity_id": "sensor.test_monitored", + "state_characteristic": "mean", + "precision": 3, + }, + ] + }, + ) + await hass.async_block_till_done() + + for value in VALUES_NUMERIC: + hass.states.async_set( + "sensor.test_monitored", + value, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + ) + await hass.async_block_till_done() + + mean = sum(VALUES_NUMERIC) / len(VALUES_NUMERIC) + state = hass.states.get("sensor.test_precision_0") + assert state.state == str(int(round(mean, 0))) + state = hass.states.get("sensor.test_precision_3") + assert state.state == str(round(mean, 3)) + + +async def test_state_class(hass): + """Test state class, which depends on the characteristic configured.""" + assert await async_setup_component( + hass, + "sensor", + { + "sensor": [ + { + "platform": "statistics", + "name": "test_normal", + "entity_id": "sensor.test_monitored", + "state_characteristic": "count", + }, + { + "platform": "statistics", + "name": "test_nan", + "entity_id": "sensor.test_monitored", + "state_characteristic": "datetime_oldest", + }, + ] + }, + ) + await hass.async_block_till_done() + + for value in VALUES_NUMERIC: + hass.states.async_set( + "sensor.test_monitored", + value, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_normal") + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + state = hass.states.get("sensor.test_nan") + assert state.attributes.get(ATTR_STATE_CLASS) is None + + +async def test_unitless_source_sensor(hass): + """Statistics for a unitless source sensor should never have a unit.""" + assert await async_setup_component( + hass, + "sensor", + { + "sensor": [ + { + "platform": "statistics", + "name": "test_unitless_1", + "entity_id": "sensor.test_monitored_unitless", + "state_characteristic": "count", + }, + { + "platform": "statistics", + "name": "test_unitless_2", + "entity_id": "sensor.test_monitored_unitless", + "state_characteristic": "mean", + }, + { + "platform": "statistics", + "name": "test_unitless_3", + "entity_id": "sensor.test_monitored_unitless", + "state_characteristic": "change_second", + }, + { + "platform": "statistics", + "name": "test_unitless_4", + "entity_id": "binary_sensor.test_monitored_unitless", + }, + { + "platform": "statistics", + "name": "test_unitless_5", + "entity_id": "binary_sensor.test_monitored_unitless", + "state_characteristic": "mean", + }, + ] + }, + ) + await hass.async_block_till_done() + + for value in VALUES_NUMERIC: + hass.states.async_set( + "sensor.test_monitored_unitless", + value, + ) + for value in VALUES_BINARY: + hass.states.async_set( + "binary_sensor.test_monitored_unitless", + value, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_unitless_1") + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + state = hass.states.get("sensor.test_unitless_2") + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + state = hass.states.get("sensor.test_unitless_3") + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + state = hass.states.get("sensor.test_unitless_4") + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + state = hass.states.get("sensor.test_unitless_5") + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "%" + + +async def test_state_characteristics(hass): + """Test configured state characteristic for value and unit.""" + now = dt_util.utcnow() + start_datetime = datetime(now.year + 1, 8, 2, 12, 23, 42, tzinfo=dt_util.UTC) + mock_data = {"return_time": start_datetime} + + def mock_now(): + return mock_data["return_time"] + + characteristics = ( + { + "source_sensor_domain": "sensor", + "name": "average_linear", + "value_0": STATE_UNKNOWN, + "value_1": STATE_UNKNOWN, + "value_9": 10.68, + "unit": "°C", + }, + { + "source_sensor_domain": "sensor", + "name": "average_step", + "value_0": STATE_UNKNOWN, + "value_1": STATE_UNKNOWN, + "value_9": 11.36, + "unit": "°C", + }, + { + "source_sensor_domain": "sensor", + "name": "average_timeless", + "value_0": STATE_UNKNOWN, + "value_1": float(VALUES_NUMERIC[-1]), + "value_9": float(round(sum(VALUES_NUMERIC) / len(VALUES_NUMERIC), 2)), + "unit": "°C", + }, + { + "source_sensor_domain": "sensor", + "name": "change", + "value_0": STATE_UNKNOWN, + "value_1": float(0), + "value_9": float(round(VALUES_NUMERIC[-1] - VALUES_NUMERIC[0], 2)), + "unit": "°C", + }, + { + "source_sensor_domain": "sensor", + "name": "change_sample", + "value_0": STATE_UNKNOWN, + "value_1": STATE_UNKNOWN, + "value_9": float( + round( + (VALUES_NUMERIC[-1] - VALUES_NUMERIC[0]) + / (len(VALUES_NUMERIC) - 1), + 2, + ) + ), + "unit": "°C/sample", + }, + { + "source_sensor_domain": "sensor", + "name": "change_second", + "value_0": STATE_UNKNOWN, + "value_1": STATE_UNKNOWN, + "value_9": float( + round( + (VALUES_NUMERIC[-1] - VALUES_NUMERIC[0]) + / (60 * (len(VALUES_NUMERIC) - 1)), + 2, + ) + ), + "unit": "°C/s", + }, + { + "source_sensor_domain": "sensor", + "name": "count", + "value_0": 0, + "value_1": 1, + "value_9": 9, + "unit": None, + }, + { + "source_sensor_domain": "sensor", + "name": "datetime_newest", + "value_0": STATE_UNKNOWN, + "value_1": start_datetime + timedelta(minutes=9), + "value_9": start_datetime + timedelta(minutes=9), + "unit": None, + }, + { + "source_sensor_domain": "sensor", + "name": "datetime_oldest", + "value_0": STATE_UNKNOWN, + "value_1": start_datetime + timedelta(minutes=9), + "value_9": start_datetime + timedelta(minutes=1), + "unit": None, + }, + { + "source_sensor_domain": "sensor", + "name": "distance_95_percent_of_values", + "value_0": STATE_UNKNOWN, + "value_1": STATE_UNKNOWN, + "value_9": float(round(2 * 1.96 * statistics.stdev(VALUES_NUMERIC), 2)), + "unit": "°C", + }, + { + "source_sensor_domain": "sensor", + "name": "distance_99_percent_of_values", + "value_0": STATE_UNKNOWN, + "value_1": STATE_UNKNOWN, + "value_9": float(round(2 * 2.58 * statistics.stdev(VALUES_NUMERIC), 2)), + "unit": "°C", + }, + { + "source_sensor_domain": "sensor", + "name": "distance_absolute", + "value_0": STATE_UNKNOWN, + "value_1": float(0), + "value_9": float(max(VALUES_NUMERIC) - min(VALUES_NUMERIC)), + "unit": "°C", + }, + { + "source_sensor_domain": "sensor", + "name": "mean", + "value_0": STATE_UNKNOWN, + "value_1": float(VALUES_NUMERIC[-1]), + "value_9": float(round(sum(VALUES_NUMERIC) / len(VALUES_NUMERIC), 2)), + "unit": "°C", + }, + { + "source_sensor_domain": "sensor", + "name": "median", + "value_0": STATE_UNKNOWN, + "value_1": float(VALUES_NUMERIC[-1]), + "value_9": float(round(statistics.median(VALUES_NUMERIC), 2)), + "unit": "°C", + }, + { + "source_sensor_domain": "sensor", + "name": "noisiness", + "value_0": STATE_UNKNOWN, + "value_1": STATE_UNKNOWN, + "value_9": float(round(sum([3, 4.8, 10.2, 1.2, 5.4, 2.5, 7.3, 8]) / 8, 2)), + "unit": "°C", + }, + { + "source_sensor_domain": "sensor", + "name": "quantiles", + "value_0": STATE_UNKNOWN, + "value_1": STATE_UNKNOWN, + "value_9": [ + round(quantile, 2) for quantile in statistics.quantiles(VALUES_NUMERIC) + ], + "unit": None, + }, + { + "source_sensor_domain": "sensor", + "name": "standard_deviation", + "value_0": STATE_UNKNOWN, + "value_1": STATE_UNKNOWN, + "value_9": float(round(statistics.stdev(VALUES_NUMERIC), 2)), + "unit": "°C", + }, + { + "source_sensor_domain": "sensor", + "name": "total", + "value_0": STATE_UNKNOWN, + "value_1": float(VALUES_NUMERIC[-1]), + "value_9": float(sum(VALUES_NUMERIC)), + "unit": "°C", + }, + { + "source_sensor_domain": "sensor", + "name": "value_max", + "value_0": STATE_UNKNOWN, + "value_1": float(VALUES_NUMERIC[-1]), + "value_9": float(max(VALUES_NUMERIC)), + "unit": "°C", + }, + { + "source_sensor_domain": "sensor", + "name": "value_min", + "value_0": STATE_UNKNOWN, + "value_1": float(VALUES_NUMERIC[-1]), + "value_9": float(min(VALUES_NUMERIC)), + "unit": "°C", + }, + { + "source_sensor_domain": "sensor", + "name": "variance", + "value_0": STATE_UNKNOWN, + "value_1": STATE_UNKNOWN, + "value_9": float(round(statistics.variance(VALUES_NUMERIC), 2)), + "unit": "°C²", + }, + { + "source_sensor_domain": "binary_sensor", + "name": "average_step", + "value_0": STATE_UNKNOWN, + "value_1": STATE_UNKNOWN, + "value_9": 50.0, + "unit": "%", + }, + { + "source_sensor_domain": "binary_sensor", + "name": "average_timeless", + "value_0": STATE_UNKNOWN, + "value_1": 100.0, + "value_9": float( + round(100 / len(VALUES_BINARY) * VALUES_BINARY.count("on"), 2) + ), + "unit": "%", + }, + { + "source_sensor_domain": "binary_sensor", + "name": "count", + "value_0": 0, + "value_1": 1, + "value_9": len(VALUES_BINARY), + "unit": None, + }, + { + "source_sensor_domain": "binary_sensor", + "name": "mean", + "value_0": STATE_UNKNOWN, + "value_1": 100.0, + "value_9": float( + round(100 / len(VALUES_BINARY) * VALUES_BINARY.count("on"), 2) + ), + "unit": "%", + }, + ) + sensors_config = [] + for characteristic in characteristics: + sensors_config.append( + { + "platform": "statistics", + "name": f"test_{characteristic['source_sensor_domain']}_{characteristic['name']}", + "entity_id": f"{characteristic['source_sensor_domain']}.test_monitored", + "state_characteristic": characteristic["name"], + "max_age": {"minutes": 8}, # 9 values spaces by one minute + } + ) + + with patch( + "homeassistant.components.statistics.sensor.dt_util.utcnow", new=mock_now + ): + assert await async_setup_component( + hass, + "sensor", + {"sensor": sensors_config}, + ) + await hass.async_block_till_done() + + # With all values in buffer + + for i in range(len(VALUES_NUMERIC)): + mock_data["return_time"] += timedelta(minutes=1) + async_fire_time_changed(hass, mock_data["return_time"]) + hass.states.async_set( + "sensor.test_monitored", + VALUES_NUMERIC[i], + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + ) + hass.states.async_set( + "binary_sensor.test_monitored", + VALUES_BINARY[i], + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + ) + await hass.async_block_till_done() + + for characteristic in characteristics: + state = hass.states.get( + f"sensor.test_{characteristic['source_sensor_domain']}_{characteristic['name']}" + ) + assert state.state == str(characteristic["value_9"]), ( + f"value mismatch for characteristic " + f"'{characteristic['source_sensor_domain']}/{characteristic['name']}' " + f"(buffer filled) - " + f"assert {state.state} == {str(characteristic['value_9'])}" + ) + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == characteristic["unit"] + ), f"unit mismatch for characteristic '{characteristic['name']}'" + + # With single value in buffer + + mock_data["return_time"] += timedelta(minutes=8) + async_fire_time_changed(hass, mock_data["return_time"]) + await hass.async_block_till_done() + + for characteristic in characteristics: + state = hass.states.get( + f"sensor.test_{characteristic['source_sensor_domain']}_{characteristic['name']}" + ) + assert state.state == str(characteristic["value_1"]), ( + f"value mismatch for characteristic " + f"'{characteristic['source_sensor_domain']}/{characteristic['name']}' " + f"(one stored value) - " + f"assert {state.state} == {str(characteristic['value_1'])}" + ) + + # With empty buffer + + mock_data["return_time"] += timedelta(minutes=1) + async_fire_time_changed(hass, mock_data["return_time"]) + await hass.async_block_till_done() + + for characteristic in characteristics: + state = hass.states.get( + f"sensor.test_{characteristic['source_sensor_domain']}_{characteristic['name']}" + ) + assert state.state == str(characteristic["value_0"]), ( + f"value mismatch for characteristic " + f"'{characteristic['source_sensor_domain']}/{characteristic['name']}' " + f"(buffer empty) - " + f"assert {state.state} == {str(characteristic['value_0'])}" + ) + + +async def test_invalid_state_characteristic(hass): + """Test the detection of wrong state_characteristics selected.""" + assert await async_setup_component( + hass, + "sensor", + { + "sensor": [ + { + "platform": "statistics", + "name": "test_numeric", + "entity_id": "sensor.test_monitored", + "state_characteristic": "invalid", + }, + { + "platform": "statistics", + "name": "test_binary", + "entity_id": "binary_sensor.test_monitored", + "state_characteristic": "variance", + }, + ] + }, + ) + await hass.async_block_till_done() + + hass.states.async_set( + "sensor.test_monitored", + VALUES_NUMERIC[0], + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_numeric") + assert state is None + state = hass.states.get("sensor.test_binary") + assert state is None + + +async def test_initialize_from_database(hass): + """Test initializing the statistics from the recorder database.""" + # enable and pre-fill the recorder + await async_init_recorder_component(hass) + await hass.async_block_till_done() + await async_wait_recording_done_without_instance(hass) + + for value in VALUES_NUMERIC: + hass.states.async_set( + "sensor.test_monitored", + value, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + ) + await hass.async_block_till_done() + await async_wait_recording_done_without_instance(hass) + + # create the statistics component, get filled from database + assert await async_setup_component( + hass, + "sensor", + { + "sensor": [ + { + "platform": "statistics", + "name": "test", + "entity_id": "sensor.test_monitored", + "state_characteristic": "mean", + "sampling_size": 100, + }, + ] + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test") + assert state.state == str(round(sum(VALUES_NUMERIC) / len(VALUES_NUMERIC), 2)) + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + + +async def test_initialize_from_database_with_maxage(hass): + """Test initializing the statistics from the database.""" + now = dt_util.utcnow() + mock_data = { + "return_time": datetime(now.year + 1, 8, 2, 12, 23, 42, tzinfo=dt_util.UTC) + } + + def mock_now(): + return mock_data["return_time"] + + # Testing correct retrieval from recorder, thus we do not + # want purging to occur within the class itself. + def mock_purge(self): + return + + # enable and pre-fill the recorder + await async_init_recorder_component(hass) + await hass.async_block_till_done() + await async_wait_recording_done_without_instance(hass) + + with patch( + "homeassistant.components.statistics.sensor.dt_util.utcnow", new=mock_now + ), patch.object(StatisticsSensor, "_purge_old", mock_purge): + for value in VALUES_NUMERIC: + hass.states.async_set( + "sensor.test_monitored", + value, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + ) + await hass.async_block_till_done() + mock_data["return_time"] += timedelta(hours=1) + await async_wait_recording_done_without_instance(hass) + # create the statistics component, get filled from database + assert await async_setup_component( + hass, + "sensor", + { + "sensor": [ + { + "platform": "statistics", + "name": "test", + "entity_id": "sensor.test_monitored", + "sampling_size": 100, + "state_characteristic": "datetime_newest", + "max_age": {"hours": 3}, + }, + ] + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test") + assert state.attributes.get("age_coverage_ratio") == round(2 / 3, 2) + # The max_age timestamp should be 1 hour before what we have right + # now in mock_data['return_time']. + assert mock_data["return_time"] == datetime.strptime( + state.state, "%Y-%m-%d %H:%M:%S%z" + ) + timedelta(hours=1) async def test_reload(hass): - """Verify we can reload filter sensors.""" - await hass.async_add_executor_job( - init_recorder_component, hass - ) # force in memory db + """Verify we can reload statistics sensors.""" + await async_init_recorder_component(hass) - hass.states.async_set("sensor.test_monitored", 12345) await async_setup_component( hass, "sensor", @@ -1102,22 +987,22 @@ async def test_reload(hass): }, ) await hass.async_block_till_done() - await hass.async_start() + + hass.states.async_set("sensor.test_monitored", 12345) await hass.async_block_till_done() assert len(hass.states.async_all()) == 2 - assert hass.states.get("sensor.test") yaml_path = get_fixture_path("configuration.yaml", "statistics") with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( - DOMAIN, + STATISTICS_DOMAIN, SERVICE_RELOAD, {}, blocking=True, ) - await hass.async_block_till_done() + await hass.async_block_till_done() assert len(hass.states.async_all()) == 2 From f30eb05870f76a6f720bcbd31d0aca3bc0067978 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 8 Dec 2021 16:54:26 +0100 Subject: [PATCH 0152/2644] Refactor recorder queue handling (#61161) * Refactor recorder queue handling * Address pylint's concerns * Implement workaround for mypy bug * Address review comments --- homeassistant/components/recorder/__init__.py | 214 ++++++++++-------- tests/components/recorder/test_init.py | 4 +- 2 files changed, 123 insertions(+), 95 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 8a907a8d9fa..333d955ab63 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -1,6 +1,7 @@ """Support for recording details.""" from __future__ import annotations +import abc import asyncio from collections.abc import Callable, Iterable import concurrent.futures @@ -11,7 +12,7 @@ import queue import sqlite3 import threading import time -from typing import Any, NamedTuple +from typing import Any from sqlalchemy import create_engine, event as sqlalchemy_event, exc, func, select from sqlalchemy.exc import SQLAlchemyError @@ -327,66 +328,161 @@ def _async_register_services(hass, instance): ) -class ClearStatisticsTask(NamedTuple): +class RecorderTask(abc.ABC): + """ABC for recorder tasks.""" + + @abc.abstractmethod + def run(self, instance: Recorder) -> None: + """Handle the task.""" + + +@dataclass +class ClearStatisticsTask(RecorderTask): """Object to store statistics_ids which for which to remove statistics.""" statistic_ids: list[str] + def run(self, instance: Recorder) -> None: + """Handle the task.""" + statistics.clear_statistics(instance, self.statistic_ids) -class UpdateStatisticsMetadataTask(NamedTuple): + +@dataclass +class UpdateStatisticsMetadataTask(RecorderTask): """Object to store statistics_id and unit for update of statistics metadata.""" statistic_id: str unit_of_measurement: str | None + def run(self, instance: Recorder) -> None: + """Handle the task.""" + statistics.update_statistics_metadata( + instance, self.statistic_id, self.unit_of_measurement + ) -class PurgeTask(NamedTuple): + +@dataclass +class PurgeTask(RecorderTask): """Object to store information about purge task.""" purge_before: datetime repack: bool apply_filter: bool + def run(self, instance: Recorder) -> None: + """Purge the database.""" + if purge.purge_old_data( + instance, self.purge_before, self.repack, self.apply_filter + ): + # We always need to do the db cleanups after a purge + # is finished to ensure the WAL checkpoint and other + # tasks happen after a vacuum. + perodic_db_cleanups(instance) + return + # Schedule a new purge task if this one didn't finish + instance.queue.put(PurgeTask(self.purge_before, self.repack, self.apply_filter)) -class PurgeEntitiesTask(NamedTuple): + +@dataclass +class PurgeEntitiesTask(RecorderTask): """Object to store entity information about purge task.""" entity_filter: Callable[[str], bool] + def run(self, instance: Recorder) -> None: + """Purge entities from the database.""" + if purge.purge_entity_data(instance, self.entity_filter): + return + # Schedule a new purge task if this one didn't finish + instance.queue.put(PurgeEntitiesTask(self.entity_filter)) -class PerodicCleanupTask: + +@dataclass +class PerodicCleanupTask(RecorderTask): """An object to insert into the recorder to trigger cleanup tasks when auto purge is disabled.""" + def run(self, instance: Recorder) -> None: + """Handle the task.""" + perodic_db_cleanups(instance) -class StatisticsTask(NamedTuple): + +@dataclass +class StatisticsTask(RecorderTask): """An object to insert into the recorder queue to run a statistics task.""" start: datetime + def run(self, instance: Recorder) -> None: + """Run statistics task.""" + if statistics.compile_statistics(instance, self.start): + return + # Schedule a new statistics task if this one didn't finish + instance.queue.put(StatisticsTask(self.start)) -class ExternalStatisticsTask(NamedTuple): + +@dataclass +class ExternalStatisticsTask(RecorderTask): """An object to insert into the recorder queue to run an external statistics task.""" metadata: dict statistics: Iterable[dict] - -class WaitTask: - """An object to insert into the recorder queue to tell it set the _queue_watch event.""" + def run(self, instance: Recorder) -> None: + """Run statistics task.""" + if statistics.add_external_statistics(instance, self.metadata, self.statistics): + return + # Schedule a new statistics task if this one didn't finish + instance.queue.put(ExternalStatisticsTask(self.metadata, self.statistics)) @dataclass -class DatabaseLockTask: +class WaitTask(RecorderTask): + """An object to insert into the recorder queue to tell it set the _queue_watch event.""" + + def run(self, instance: Recorder) -> None: + """Handle the task.""" + instance._queue_watch.set() # pylint: disable=[protected-access] + + +@dataclass +class DatabaseLockTask(RecorderTask): """An object to insert into the recorder queue to prevent writes to the database.""" database_locked: asyncio.Event database_unlock: threading.Event queue_overflow: bool + def run(self, instance: Recorder) -> None: + """Handle the task.""" + instance._lock_database(self) # pylint: disable=[protected-access] + + +@dataclass +class StopTask(RecorderTask): + """An object to insert into the recorder queue to stop the event handler.""" + + def run(self, instance: Recorder) -> None: + """Handle the task.""" + instance.stop_requested = True + + +@dataclass +class EventTask(RecorderTask): + """An object to insert into the recorder queue to stop the event handler.""" + + event: bool + + def run(self, instance: Recorder) -> None: + """Handle the task.""" + # pylint: disable-next=[protected-access] + instance._process_one_event(self.event) + class Recorder(threading.Thread): """A threaded recorder class.""" + stop_requested: bool + def __init__( self, hass: HomeAssistant, @@ -406,7 +502,7 @@ class Recorder(threading.Thread): self.auto_purge = auto_purge self.keep_days = keep_days self.commit_interval = commit_interval - self.queue: Any = queue.SimpleQueue() + self.queue: queue.SimpleQueue[RecorderTask] = queue.SimpleQueue() self.recording_start = dt_util.utcnow() self.db_url = uri self.db_max_retries = db_max_retries @@ -537,7 +633,7 @@ class Recorder(threading.Thread): self.queue.get_nowait() except queue.Empty: break - self.queue.put(None) + self.queue.put(StopTask()) self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_FINAL_WRITE, _empty_queue) @@ -545,7 +641,7 @@ class Recorder(threading.Thread): """Shut down the Recorder.""" if not hass_started.done(): hass_started.set_result(shutdown_task) - self.queue.put(None) + self.queue.put(StopTask()) self.hass.add_job(self._async_stop_queue_watcher_and_event_listener) self.join() @@ -691,31 +787,28 @@ class Recorder(threading.Thread): # Use a session for the event read loop # with a commit every time the event time # has changed. This reduces the disk io. - while event := self.queue.get(): + self.stop_requested = False + while not self.stop_requested: + task = self.queue.get() try: - self._process_one_event_or_recover(event) + self._process_one_task_or_recover(task) except Exception as err: # pylint: disable=broad-except - _LOGGER.exception("Error while processing event %s: %s", event, err) + _LOGGER.exception("Error while processing event %s: %s", task, err) self._shutdown() - def _process_one_event_or_recover(self, event): + def _process_one_task_or_recover(self, task: RecorderTask): """Process an event, reconnect, or recover a malformed database.""" try: - if self._process_one_task(event): - return - self._process_one_event(event) - return + return task.run(self) except exc.DatabaseError as err: if self._handle_database_error(err): return _LOGGER.exception( - "Unhandled database error while processing event %s: %s", event, err + "Unhandled database error while processing task %s: %s", task, err ) except SQLAlchemyError as err: - _LOGGER.exception( - "SQLAlchemyError error processing event %s: %s", event, err - ) + _LOGGER.exception("SQLAlchemyError error processing task %s: %s", task, err) # Reset the session if an SQLAlchemyError (including DatabaseError) # happens to rollback and recover @@ -773,38 +866,6 @@ class Recorder(threading.Thread): self.migration_in_progress = False persistent_notification.dismiss(self.hass, "recorder_database_migration") - def _run_purge(self, purge_before, repack, apply_filter): - """Purge the database.""" - if purge.purge_old_data(self, purge_before, repack, apply_filter): - # We always need to do the db cleanups after a purge - # is finished to ensure the WAL checkpoint and other - # tasks happen after a vacuum. - perodic_db_cleanups(self) - return - # Schedule a new purge task if this one didn't finish - self.queue.put(PurgeTask(purge_before, repack, apply_filter)) - - def _run_purge_entities(self, entity_filter): - """Purge entities from the database.""" - if purge.purge_entity_data(self, entity_filter): - return - # Schedule a new purge task if this one didn't finish - self.queue.put(PurgeEntitiesTask(entity_filter)) - - def _run_statistics(self, start): - """Run statistics task.""" - if statistics.compile_statistics(self, start): - return - # Schedule a new statistics task if this one didn't finish - self.queue.put(StatisticsTask(start)) - - def _run_external_statistics(self, metadata, stats): - """Run statistics task.""" - if statistics.add_external_statistics(self, metadata, stats): - return - # Schedule a new statistics task if this one didn't finish - self.queue.put(ExternalStatisticsTask(metadata, stats)) - def _lock_database(self, task: DatabaseLockTask): @callback def _async_set_database_locked(task: DatabaseLockTask): @@ -828,39 +889,6 @@ class Recorder(threading.Thread): self.queue.qsize(), ) - def _process_one_task(self, event) -> bool: - """Process one event.""" - if isinstance(event, PurgeTask): - self._run_purge(event.purge_before, event.repack, event.apply_filter) - return True - if isinstance(event, PurgeEntitiesTask): - self._run_purge_entities(event.entity_filter) - return True - if isinstance(event, PerodicCleanupTask): - perodic_db_cleanups(self) - return True - if isinstance(event, StatisticsTask): - self._run_statistics(event.start) - return True - if isinstance(event, ClearStatisticsTask): - statistics.clear_statistics(self, event.statistic_ids) - return True - if isinstance(event, UpdateStatisticsMetadataTask): - statistics.update_statistics_metadata( - self, event.statistic_id, event.unit_of_measurement - ) - return True - if isinstance(event, ExternalStatisticsTask): - self._run_external_statistics(event.metadata, event.statistics) - return True - if isinstance(event, WaitTask): - self._queue_watch.set() - return True - if isinstance(event, DatabaseLockTask): - self._lock_database(event) - return True - return False - def _process_one_event(self, event): if event.event_type == EVENT_TIME_CHANGED: self._keepalive_count += 1 @@ -1010,7 +1038,7 @@ class Recorder(threading.Thread): @callback def event_listener(self, event): """Listen for new events and put them in the process queue.""" - self.queue.put(event) + self.queue.put(EventTask(event)) def block_till_done(self): """Block till all events processed. diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 7d7c3f27fb6..64560e4d33b 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -261,7 +261,7 @@ def test_saving_state_with_sqlalchemy_exception(hass, hass_recorder, caplog): hass.states.set(entity_id, "fail", attributes) wait_recording_done(hass) - assert "SQLAlchemyError error processing event" in caplog.text + assert "SQLAlchemyError error processing task" in caplog.text caplog.clear() hass.states.set(entity_id, state, attributes) @@ -273,7 +273,7 @@ def test_saving_state_with_sqlalchemy_exception(hass, hass_recorder, caplog): assert "Error executing query" not in caplog.text assert "Error saving events" not in caplog.text - assert "SQLAlchemyError error processing event" not in caplog.text + assert "SQLAlchemyError error processing task" not in caplog.text async def test_force_shutdown_with_queue_of_writes_that_generate_exceptions( From af91addc6cf979fb08acc7cf1b43c25faf663764 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Wed, 8 Dec 2021 17:01:52 +0000 Subject: [PATCH 0153/2644] Use SensorDeviceClass and SensorStateClass enums in Aurora ABB (#61245) --- .../components/aurora_abb_powerone/sensor.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/aurora_abb_powerone/sensor.py b/homeassistant/components/aurora_abb_powerone/sensor.py index 35be4e2b7d7..59c74e221bd 100644 --- a/homeassistant/components/aurora_abb_powerone/sensor.py +++ b/homeassistant/components/aurora_abb_powerone/sensor.py @@ -10,19 +10,16 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( CONF_ADDRESS, CONF_DEVICE, CONF_NAME, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_TEMPERATURE, ENERGY_KILO_WATT_HOUR, POWER_WATT, TEMP_CELSIUS, @@ -37,23 +34,23 @@ _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = [ SensorEntityDescription( key="instantaneouspower", - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, native_unit_of_measurement=POWER_WATT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, name="Power Output", ), SensorEntityDescription( key="temp", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, name="Temperature", ), SensorEntityDescription( key="totalenergy", - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, name="Total Energy", ), ] From 9f3a4c361773f3c821503da4ffab276ac6a0d892 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 8 Dec 2021 12:28:27 -0600 Subject: [PATCH 0154/2644] Improve Sonos tests, begin adding coverage (#61198) * Update entity registry handling * Add and use fixtures to test setup via config entry * Remove legacy redundant tests * Remove unnecessary mock_coro * Remove unnecessary namespace change * Move zeroconf payload to fixture * Begin adding Sonos to codecov * Mock proper return value * Revert return value for platform --- .coveragerc | 11 ++++- tests/components/sonos/conftest.py | 35 +++++++++++++- tests/components/sonos/test_config_flow.py | 26 +++------- tests/components/sonos/test_init.py | 9 ++-- tests/components/sonos/test_media_player.py | 51 +++----------------- tests/components/sonos/test_plex_playback.py | 9 +--- tests/components/sonos/test_sensor.py | 48 +++++++----------- tests/components/sonos/test_switch.py | 29 +++-------- 8 files changed, 88 insertions(+), 130 deletions(-) diff --git a/.coveragerc b/.coveragerc index 4bb0a13751a..2afc0300d8e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1003,7 +1003,16 @@ omit = homeassistant/components/somfy/switch.py homeassistant/components/somfy_mylink/__init__.py homeassistant/components/somfy_mylink/cover.py - homeassistant/components/sonos/* + homeassistant/components/sonos/__init__.py + homeassistant/components/sonos/alarms.py + homeassistant/components/sonos/entity.py + homeassistant/components/sonos/favorites.py + homeassistant/components/sonos/helpers.py + homeassistant/components/sonos/household_coordinator.py + homeassistant/components/sonos/media_browser.py + homeassistant/components/sonos/media_player.py + homeassistant/components/sonos/speaker.py + homeassistant/components/sonos/switch.py homeassistant/components/sony_projector/switch.py homeassistant/components/spc/* homeassistant/components/spider/* diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index f7f8d67589f..f9adf1be8f9 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -1,9 +1,9 @@ """Configuration for Sonos tests.""" -from unittest.mock import AsyncMock, MagicMock, Mock, patch as patch +from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest -from homeassistant.components import ssdp +from homeassistant.components import ssdp, zeroconf from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.sonos import DOMAIN from homeassistant.const import CONF_HOSTS @@ -42,6 +42,37 @@ class SonosMockEvent: return self.variables[var_name] +@pytest.fixture +def zeroconf_payload(): + """Return a default zeroconf payload.""" + return zeroconf.ZeroconfServiceInfo( + host="192.168.4.2", + hostname="Sonos-aaa", + name="Sonos-aaa@Living Room._sonos._tcp.local.", + port=None, + properties={"bootseq": "1234"}, + type="mock_type", + ) + + +@pytest.fixture +async def async_autosetup_sonos(async_setup_sonos): + """Set up a Sonos integration instance on test run.""" + await async_setup_sonos() + + +@pytest.fixture +def async_setup_sonos(hass, config_entry): + """Return a coroutine to set up a Sonos integration instance on demand.""" + + async def _wrapper(): + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return _wrapper + + @pytest.fixture(name="config_entry") def config_entry_fixture(): """Create a mock Sonos config entry.""" diff --git a/tests/components/sonos/test_config_flow.py b/tests/components/sonos/test_config_flow.py index 9677ee73759..eee644abeba 100644 --- a/tests/components/sonos/test_config_flow.py +++ b/tests/components/sonos/test_config_flow.py @@ -37,21 +37,14 @@ async def test_user_form(discover_mock: MagicMock, hass: core.HomeAssistant): assert len(mock_setup_entry.mock_calls) == 1 -async def test_zeroconf_form(hass: core.HomeAssistant): - """Test we pass sonos devices to the discovery manager.""" +async def test_zeroconf_form(hass: core.HomeAssistant, zeroconf_payload): + """Test we pass Zeroconf discoveries to the manager.""" mock_manager = hass.data[DATA_SONOS_DISCOVERY_MANAGER] = MagicMock() result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( - host="192.168.4.2", - hostname="Sonos-aaa", - name="Sonos-aaa@Living Room._sonos._tcp.local.", - port=None, - properties={"bootseq": "1234"}, - type="mock_type", - ), + data=zeroconf_payload, ) assert result["type"] == "form" assert result["errors"] is None @@ -128,21 +121,16 @@ async def test_zeroconf_sonos_v1(hass: core.HomeAssistant): assert len(mock_manager.mock_calls) == 2 -async def test_zeroconf_form_not_sonos(hass: core.HomeAssistant): +async def test_zeroconf_form_not_sonos(hass: core.HomeAssistant, zeroconf_payload): """Test we abort on non-sonos devices.""" mock_manager = hass.data[DATA_SONOS_DISCOVERY_MANAGER] = MagicMock() + zeroconf_payload.hostname = "not-aaa" + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( - host="192.168.4.2", - hostname="not-aaa", - name="mock_name", - port=None, - properties={"bootseq": "1234"}, - type="mock_type", - ), + data=zeroconf_payload, ) assert result["type"] == "abort" assert result["reason"] == "not_sonos_device" diff --git a/tests/components/sonos/test_init.py b/tests/components/sonos/test_init.py index bf4b5d5e7cc..71f89f6d880 100644 --- a/tests/components/sonos/test_init.py +++ b/tests/components/sonos/test_init.py @@ -5,14 +5,11 @@ from homeassistant import config_entries, data_entry_flow from homeassistant.components import sonos from homeassistant.setup import async_setup_component -from tests.common import mock_coro - async def test_creating_entry_sets_up_media_player(hass): """Test setting up Sonos loads the media player.""" with patch( "homeassistant.components.sonos.media_player.async_setup_entry", - return_value=mock_coro(True), ) as mock_setup, patch("soco.discover", return_value=True): result = await hass.config_entries.flow.async_init( sonos.DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -32,7 +29,8 @@ async def test_creating_entry_sets_up_media_player(hass): async def test_configuring_sonos_creates_entry(hass): """Test that specifying config will create an entry.""" with patch( - "homeassistant.components.sonos.async_setup_entry", return_value=mock_coro(True) + "homeassistant.components.sonos.async_setup_entry", + return_value=True, ) as mock_setup, patch("soco.discover", return_value=True): await async_setup_component( hass, @@ -47,7 +45,8 @@ async def test_configuring_sonos_creates_entry(hass): async def test_not_configuring_sonos_not_creates_entry(hass): """Test that no config will not create an entry.""" with patch( - "homeassistant.components.sonos.async_setup_entry", return_value=mock_coro(True) + "homeassistant.components.sonos.async_setup_entry", + return_value=True, ) as mock_setup, patch("soco.discover", return_value=True): await async_setup_component(hass, sonos.DOMAIN, {}) await hass.async_block_till_done() diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index 9fb1d7639eb..a73ff22aba6 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -9,54 +9,23 @@ from homeassistant.const import STATE_IDLE from homeassistant.core import Context from homeassistant.exceptions import Unauthorized from homeassistant.helpers import device_registry as dr -from homeassistant.setup import async_setup_component -async def setup_platform(hass, config_entry, config): - """Set up the media player platform for testing.""" - config_entry.add_to_hass(hass) - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - - -async def test_async_setup_entry_hosts(hass, config_entry, config, soco): - """Test static setup.""" - await setup_platform(hass, config_entry, config) - - speakers = list(hass.data[DATA_SONOS].discovered.values()) - speaker = speakers[0] - assert speaker.soco == soco - - media_player = hass.states.get("media_player.zone_a") - assert media_player.state == STATE_IDLE - - -async def test_async_setup_entry_discover(hass, config_entry, discover): - """Test discovery setup.""" - await setup_platform(hass, config_entry, {}) - - speakers = list(hass.data[DATA_SONOS].discovered.values()) - speaker = speakers[0] - assert speaker.soco.uid == "RINCON_test" - - media_player = hass.states.get("media_player.zone_a") - assert media_player.state == STATE_IDLE - - -async def test_discovery_ignore_unsupported_device(hass, config_entry, soco, caplog): +async def test_discovery_ignore_unsupported_device( + hass, async_setup_sonos, soco, caplog +): """Test discovery setup.""" message = f"GetVolume not supported on {soco.ip_address}" type(soco).volume = PropertyMock(side_effect=NotSupportedException(message)) - await setup_platform(hass, config_entry, {}) + + await async_setup_sonos() assert message in caplog.text assert not hass.data[DATA_SONOS].discovered -async def test_services(hass, config_entry, config, hass_read_only_user): +async def test_services(hass, async_autosetup_sonos, hass_read_only_user): """Test join/unjoin requires control access.""" - await setup_platform(hass, config_entry, config) - with pytest.raises(Unauthorized): await hass.services.async_call( DOMAIN, @@ -67,10 +36,8 @@ async def test_services(hass, config_entry, config, hass_read_only_user): ) -async def test_device_registry(hass, config_entry, config, soco): +async def test_device_registry(hass, async_autosetup_sonos, soco): """Test sonos device registered in the device registry.""" - await setup_platform(hass, config_entry, config) - device_registry = dr.async_get(hass) reg_device = device_registry.async_get_device( identifiers={("sonos", "RINCON_test")} @@ -86,10 +53,8 @@ async def test_device_registry(hass, config_entry, config, soco): assert reg_device.name == "Zone A" -async def test_entity_basic(hass, config_entry, discover): +async def test_entity_basic(hass, async_autosetup_sonos, discover): """Test basic state and attributes.""" - await setup_platform(hass, config_entry, {}) - state = hass.states.get("media_player.zone_a") assert state.state == STATE_IDLE attributes = state.attributes diff --git a/tests/components/sonos/test_plex_playback.py b/tests/components/sonos/test_plex_playback.py index f9bedbfe1f7..d9f809e8395 100644 --- a/tests/components/sonos/test_plex_playback.py +++ b/tests/components/sonos/test_plex_playback.py @@ -14,16 +14,9 @@ from homeassistant.components.plex.const import PLEX_URI_SCHEME from homeassistant.const import ATTR_ENTITY_ID from homeassistant.exceptions import HomeAssistantError -from .test_media_player import setup_platform - -async def test_plex_play_media( - hass, - config_entry, - config, -): +async def test_plex_play_media(hass, async_autosetup_sonos): """Test playing media via the Plex integration.""" - await setup_platform(hass, config_entry, config) media_player = "media_player.zone_a" media_content_id = ( '{"library_name": "Music", "artist_name": "Artist", "album_name": "Album"}' diff --git a/tests/components/sonos/test_sensor.py b/tests/components/sonos/test_sensor.py index a45d587cc08..633bf750962 100644 --- a/tests/components/sonos/test_sensor.py +++ b/tests/components/sonos/test_sensor.py @@ -1,48 +1,36 @@ """Tests for the Sonos battery sensor platform.""" from soco.exceptions import NotSupportedException -from homeassistant.components.sonos import DOMAIN from homeassistant.components.sonos.binary_sensor import ATTR_BATTERY_POWER_SOURCE from homeassistant.const import STATE_OFF, STATE_ON -from homeassistant.setup import async_setup_component +from homeassistant.helpers import entity_registry as ent_reg -async def setup_platform(hass, config_entry, config): - """Set up the media player platform for testing.""" - config_entry.add_to_hass(hass) - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - - -async def test_entity_registry_unsupported(hass, config_entry, config, soco): +async def test_entity_registry_unsupported(hass, async_setup_sonos, soco): """Test sonos device without battery registered in the device registry.""" soco.get_battery_info.side_effect = NotSupportedException - await setup_platform(hass, config_entry, config) + await async_setup_sonos() - entity_registry = await hass.helpers.entity_registry.async_get_registry() + entity_registry = ent_reg.async_get(hass) assert "media_player.zone_a" in entity_registry.entities assert "sensor.zone_a_battery" not in entity_registry.entities assert "binary_sensor.zone_a_power" not in entity_registry.entities -async def test_entity_registry_supported(hass, config_entry, config, soco): +async def test_entity_registry_supported(hass, async_autosetup_sonos, soco): """Test sonos device with battery registered in the device registry.""" - await setup_platform(hass, config_entry, config) - - entity_registry = await hass.helpers.entity_registry.async_get_registry() + entity_registry = ent_reg.async_get(hass) assert "media_player.zone_a" in entity_registry.entities assert "sensor.zone_a_battery" in entity_registry.entities assert "binary_sensor.zone_a_power" in entity_registry.entities -async def test_battery_attributes(hass, config_entry, config, soco): +async def test_battery_attributes(hass, async_autosetup_sonos, soco): """Test sonos device with battery state.""" - await setup_platform(hass, config_entry, config) - - entity_registry = await hass.helpers.entity_registry.async_get_registry() + entity_registry = ent_reg.async_get(hass) battery = entity_registry.entities["sensor.zone_a_battery"] battery_state = hass.states.get(battery.entity_id) @@ -57,16 +45,16 @@ async def test_battery_attributes(hass, config_entry, config, soco): ) -async def test_battery_on_S1(hass, config_entry, config, soco, battery_event): +async def test_battery_on_S1(hass, async_setup_sonos, soco, battery_event): """Test battery state updates on a Sonos S1 device.""" soco.get_battery_info.return_value = {} - await setup_platform(hass, config_entry, config) + await async_setup_sonos() subscription = soco.deviceProperties.subscribe.return_value sub_callback = subscription.callback - entity_registry = await hass.helpers.entity_registry.async_get_registry() + entity_registry = ent_reg.async_get(hass) assert "sensor.zone_a_battery" not in entity_registry.entities assert "binary_sensor.zone_a_power" not in entity_registry.entities @@ -86,12 +74,12 @@ async def test_battery_on_S1(hass, config_entry, config, soco, battery_event): async def test_device_payload_without_battery( - hass, config_entry, config, soco, battery_event, caplog + hass, async_setup_sonos, soco, battery_event, caplog ): """Test device properties event update without battery info.""" soco.get_battery_info.return_value = None - await setup_platform(hass, config_entry, config) + await async_setup_sonos() subscription = soco.deviceProperties.subscribe.return_value sub_callback = subscription.callback @@ -106,12 +94,12 @@ async def test_device_payload_without_battery( async def test_device_payload_without_battery_and_ignored_keys( - hass, config_entry, config, soco, battery_event, caplog + hass, async_setup_sonos, soco, battery_event, caplog ): """Test device properties event update without battery info and ignored keys.""" soco.get_battery_info.return_value = None - await setup_platform(hass, config_entry, config) + await async_setup_sonos() subscription = soco.deviceProperties.subscribe.return_value sub_callback = subscription.callback @@ -125,11 +113,9 @@ async def test_device_payload_without_battery_and_ignored_keys( assert ignored_payload not in caplog.text -async def test_audio_input_sensor(hass, config_entry, config, soco): +async def test_audio_input_sensor(hass, async_autosetup_sonos, soco): """Test sonos device with battery state.""" - await setup_platform(hass, config_entry, config) - - entity_registry = await hass.helpers.entity_registry.async_get_registry() + entity_registry = ent_reg.async_get(hass) audio_input_sensor = entity_registry.entities["sensor.zone_a_audio_input_format"] audio_input_state = hass.states.get(audio_input_sensor.entity_id) diff --git a/tests/components/sonos/test_switch.py b/tests/components/sonos/test_switch.py index 9c25e2b8cc7..63ac9de6065 100644 --- a/tests/components/sonos/test_switch.py +++ b/tests/components/sonos/test_switch.py @@ -3,7 +3,6 @@ from copy import copy from datetime import timedelta from unittest.mock import patch -from homeassistant.components.sonos import DOMAIN from homeassistant.components.sonos.const import DATA_SONOS_DISCOVERY_MANAGER from homeassistant.components.sonos.switch import ( ATTR_DURATION, @@ -15,8 +14,7 @@ from homeassistant.components.sonos.switch import ( ) from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.const import ATTR_TIME, STATE_OFF, STATE_ON -from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry -from homeassistant.setup import async_setup_component +from homeassistant.helpers import entity_registry as ent_reg from homeassistant.util import dt from .conftest import SonosMockEvent @@ -24,18 +22,9 @@ from .conftest import SonosMockEvent from tests.common import async_fire_time_changed -async def setup_platform(hass, config_entry, config): - """Set up the switch platform for testing.""" - config_entry.add_to_hass(hass) - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - - -async def test_entity_registry(hass, config_entry, config): +async def test_entity_registry(hass, async_autosetup_sonos): """Test sonos device with alarm registered in the device registry.""" - await setup_platform(hass, config_entry, config) - - entity_registry = await hass.helpers.entity_registry.async_get_registry() + entity_registry = ent_reg.async_get(hass) assert "media_player.zone_a" in entity_registry.entities assert "switch.sonos_alarm_14" in entity_registry.entities @@ -47,11 +36,9 @@ async def test_entity_registry(hass, config_entry, config): assert "switch.sonos_zone_a_touch_controls" in entity_registry.entities -async def test_switch_attributes(hass, config_entry, config, soco): +async def test_switch_attributes(hass, async_autosetup_sonos, soco): """Test for correct Sonos switch states.""" - await setup_platform(hass, config_entry, config) - - entity_registry = await hass.helpers.entity_registry.async_get_registry() + entity_registry = ent_reg.async_get(hass) alarm = entity_registry.entities["switch.sonos_alarm_14"] alarm_state = hass.states.get(alarm.entity_id) @@ -125,15 +112,15 @@ async def test_switch_attributes(hass, config_entry, config, soco): async def test_alarm_create_delete( - hass, config_entry, config, soco, alarm_clock, alarm_clock_extended, alarm_event + hass, async_setup_sonos, soco, alarm_clock, alarm_clock_extended, alarm_event ): """Test for correct creation and deletion of alarms during runtime.""" - entity_registry = async_get_entity_registry(hass) + entity_registry = ent_reg.async_get(hass) one_alarm = copy(alarm_clock.ListAlarms.return_value) two_alarms = copy(alarm_clock_extended.ListAlarms.return_value) - await setup_platform(hass, config_entry, config) + await async_setup_sonos() assert "switch.sonos_alarm_14" in entity_registry.entities assert "switch.sonos_alarm_15" not in entity_registry.entities From 17a542689f8f84b3e0741911d6ffaa2d1c66cd19 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 8 Dec 2021 19:32:25 +0100 Subject: [PATCH 0155/2644] Fix pvoutput template use and REST integer parsing (#61171) * Fix pvoutput template use and REST integer parsing * revert accepting templates as input --- homeassistant/components/pvoutput/sensor.py | 8 ++++++-- homeassistant/components/rest/utils.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/pvoutput/sensor.py b/homeassistant/components/pvoutput/sensor.py index 8126e00d8e5..512fb75067b 100644 --- a/homeassistant/components/pvoutput/sensor.py +++ b/homeassistant/components/pvoutput/sensor.py @@ -25,9 +25,10 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.template import Template _LOGGER = logging.getLogger(__name__) -_ENDPOINT = "http://pvoutput.org/service/r2/getstatus.jsp" +_ENDPOINT = "https://pvoutput.org/service/r2/getstatus.jsp" ATTR_ENERGY_GENERATION = "energy_generation" ATTR_POWER_GENERATION = "power_generation" @@ -59,7 +60,10 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= method = "GET" payload = auth = None verify_ssl = DEFAULT_VERIFY_SSL - headers = {"X-Pvoutput-Apikey": api_key, "X-Pvoutput-SystemId": system_id} + headers = { + "X-Pvoutput-Apikey": Template(api_key, hass), + "X-Pvoutput-SystemId": Template(system_id, hass), + } rest = RestData(hass, method, _ENDPOINT, auth, headers, None, payload, verify_ssl) await rest.async_update() diff --git a/homeassistant/components/rest/utils.py b/homeassistant/components/rest/utils.py index 24c58d294e1..35b3c22db31 100644 --- a/homeassistant/components/rest/utils.py +++ b/homeassistant/components/rest/utils.py @@ -23,5 +23,5 @@ def render_templates(tpl_dict: dict[str, Template] | None): rendered_items = {} for item_name, template_header in tpl_dict.items(): if (value := template_header.async_render()) is not None: - rendered_items[item_name] = value + rendered_items[item_name] = str(value) return rendered_items From b5f7e149854f85e074d55238e31edc053d98b2c2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 8 Dec 2021 19:56:59 +0100 Subject: [PATCH 0156/2644] Use new DeviceClass enums in ads (#61249) Co-authored-by: epenet --- homeassistant/components/ads/binary_sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ads/binary_sensor.py b/homeassistant/components/ads/binary_sensor.py index fda2aae3d5b..0cdec25313f 100644 --- a/homeassistant/components/ads/binary_sensor.py +++ b/homeassistant/components/ads/binary_sensor.py @@ -2,9 +2,9 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_MOVING, DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME @@ -40,7 +40,7 @@ class AdsBinarySensor(AdsEntity, BinarySensorEntity): def __init__(self, ads_hub, name, ads_var, device_class): """Initialize ADS binary sensor.""" super().__init__(ads_hub, name, ads_var) - self._attr_device_class = device_class or DEVICE_CLASS_MOVING + self._attr_device_class = device_class or BinarySensorDeviceClass.MOVING async def async_added_to_hass(self): """Register device notification.""" From d79169ca2e538c10eb1086101eafea1d6623d3ab Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 8 Dec 2021 19:57:18 +0100 Subject: [PATCH 0157/2644] Use new DeviceClass enums in acmeda (#61248) Co-authored-by: epenet --- homeassistant/components/acmeda/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/acmeda/sensor.py b/homeassistant/components/acmeda/sensor.py index 43f5e32c74f..57e5b50bd1f 100644 --- a/homeassistant/components/acmeda/sensor.py +++ b/homeassistant/components/acmeda/sensor.py @@ -1,6 +1,6 @@ """Support for Acmeda Roller Blind Batteries.""" -from homeassistant.components.sensor import SensorEntity -from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.const import PERCENTAGE from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -33,7 +33,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class AcmedaBattery(AcmedaBase, SensorEntity): """Representation of a Acmeda cover device.""" - device_class = DEVICE_CLASS_BATTERY + device_class = SensorDeviceClass.BATTERY _attr_native_unit_of_measurement = PERCENTAGE @property From d03b73eb234ab64d13c8dfd8ab15264cec0f5e3c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 8 Dec 2021 19:57:36 +0100 Subject: [PATCH 0158/2644] Use new DeviceClass enums in accuweather (#61246) Co-authored-by: epenet --- homeassistant/components/accuweather/const.py | 23 +++++++++---------- .../components/accuweather/sensor.py | 8 +++---- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/accuweather/const.py b/homeassistant/components/accuweather/const.py index deab90de706..408d4700422 100644 --- a/homeassistant/components/accuweather/const.py +++ b/homeassistant/components/accuweather/const.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Final -from homeassistant.components.sensor import SensorStateClass +from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLOUDY, @@ -22,7 +22,6 @@ from homeassistant.components.weather import ( ) from homeassistant.const import ( CONCENTRATION_PARTS_PER_CUBIC_METER, - DEVICE_CLASS_TEMPERATURE, LENGTH_FEET, LENGTH_INCHES, LENGTH_METERS, @@ -123,21 +122,21 @@ FORECAST_SENSOR_TYPES: Final[tuple[AccuWeatherSensorDescription, ...]] = ( ), AccuWeatherSensorDescription( key="RealFeelTemperatureMax", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, name="RealFeel Temperature Max", unit_metric=TEMP_CELSIUS, unit_imperial=TEMP_FAHRENHEIT, ), AccuWeatherSensorDescription( key="RealFeelTemperatureMin", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, name="RealFeel Temperature Min", unit_metric=TEMP_CELSIUS, unit_imperial=TEMP_FAHRENHEIT, ), AccuWeatherSensorDescription( key="RealFeelTemperatureShadeMax", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, name="RealFeel Temperature Shade Max", unit_metric=TEMP_CELSIUS, unit_imperial=TEMP_FAHRENHEIT, @@ -145,7 +144,7 @@ FORECAST_SENSOR_TYPES: Final[tuple[AccuWeatherSensorDescription, ...]] = ( ), AccuWeatherSensorDescription( key="RealFeelTemperatureShadeMin", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, name="RealFeel Temperature Shade Min", unit_metric=TEMP_CELSIUS, unit_imperial=TEMP_FAHRENHEIT, @@ -215,7 +214,7 @@ FORECAST_SENSOR_TYPES: Final[tuple[AccuWeatherSensorDescription, ...]] = ( SENSOR_TYPES: Final[tuple[AccuWeatherSensorDescription, ...]] = ( AccuWeatherSensorDescription( key="ApparentTemperature", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, name="Apparent Temperature", unit_metric=TEMP_CELSIUS, unit_imperial=TEMP_FAHRENHEIT, @@ -241,7 +240,7 @@ SENSOR_TYPES: Final[tuple[AccuWeatherSensorDescription, ...]] = ( ), AccuWeatherSensorDescription( key="DewPoint", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, name="Dew Point", unit_metric=TEMP_CELSIUS, unit_imperial=TEMP_FAHRENHEIT, @@ -250,7 +249,7 @@ SENSOR_TYPES: Final[tuple[AccuWeatherSensorDescription, ...]] = ( ), AccuWeatherSensorDescription( key="RealFeelTemperature", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, name="RealFeel Temperature", unit_metric=TEMP_CELSIUS, unit_imperial=TEMP_FAHRENHEIT, @@ -258,7 +257,7 @@ SENSOR_TYPES: Final[tuple[AccuWeatherSensorDescription, ...]] = ( ), AccuWeatherSensorDescription( key="RealFeelTemperatureShade", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, name="RealFeel Temperature Shade", unit_metric=TEMP_CELSIUS, unit_imperial=TEMP_FAHRENHEIT, @@ -291,7 +290,7 @@ SENSOR_TYPES: Final[tuple[AccuWeatherSensorDescription, ...]] = ( ), AccuWeatherSensorDescription( key="WetBulbTemperature", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, name="Wet Bulb Temperature", unit_metric=TEMP_CELSIUS, unit_imperial=TEMP_FAHRENHEIT, @@ -300,7 +299,7 @@ SENSOR_TYPES: Final[tuple[AccuWeatherSensorDescription, ...]] = ( ), AccuWeatherSensorDescription( key="WindChillTemperature", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, name="Wind Chill Temperature", unit_metric=TEMP_CELSIUS, unit_imperial=TEMP_FAHRENHEIT, diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index 62047f801fb..448c00eb53f 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -3,9 +3,9 @@ from __future__ import annotations from typing import Any, cast -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME, DEVICE_CLASS_TEMPERATURE +from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo @@ -107,7 +107,7 @@ class AccuWeatherSensor(CoordinatorEntity, SensorEntity): def native_value(self) -> StateType: """Return the state.""" if self.forecast_day is not None: - if self.entity_description.device_class == DEVICE_CLASS_TEMPERATURE: + if self.entity_description.device_class == SensorDeviceClass.TEMPERATURE: return cast(float, self._sensor_data["Value"]) if self.entity_description.key == "UVIndex": return cast(int, self._sensor_data["Value"]) @@ -117,7 +117,7 @@ class AccuWeatherSensor(CoordinatorEntity, SensorEntity): return round(self._sensor_data[self._unit_system]["Value"]) if self.entity_description.key == "PressureTendency": return cast(str, self._sensor_data["LocalizedText"].lower()) - if self.entity_description.device_class == DEVICE_CLASS_TEMPERATURE: + if self.entity_description.device_class == SensorDeviceClass.TEMPERATURE: return cast(float, self._sensor_data[self._unit_system]["Value"]) if self.entity_description.key == "Precipitation": return cast(float, self._sensor_data[self._unit_system]["Value"]) From 40828e221efee7497ec46c8fb4ff4bf1e6db1a8a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 8 Dec 2021 19:58:01 +0100 Subject: [PATCH 0159/2644] Use new DeviceClass enums in abode (#61244) Co-authored-by: epenet --- homeassistant/components/abode/binary_sensor.py | 4 ++-- homeassistant/components/abode/sensor.py | 15 +++++++-------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/abode/binary_sensor.py b/homeassistant/components/abode/binary_sensor.py index 7175fbc550a..ea921470e8f 100644 --- a/homeassistant/components/abode/binary_sensor.py +++ b/homeassistant/components/abode/binary_sensor.py @@ -2,7 +2,7 @@ import abodepy.helpers.constants as CONST from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_WINDOW, + BinarySensorDeviceClass, BinarySensorEntity, ) @@ -42,5 +42,5 @@ class AbodeBinarySensor(AbodeDevice, BinarySensorEntity): def device_class(self): """Return the class of the binary sensor.""" if self._device.get_value("is_window") == "1": - return DEVICE_CLASS_WINDOW + return BinarySensorDeviceClass.WINDOW return self._device.generic_type diff --git a/homeassistant/components/abode/sensor.py b/homeassistant/components/abode/sensor.py index 03687fc3907..ebc45370062 100644 --- a/homeassistant/components/abode/sensor.py +++ b/homeassistant/components/abode/sensor.py @@ -3,11 +3,10 @@ from __future__ import annotations import abodepy.helpers.constants as CONST -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription -from homeassistant.const import ( - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_TEMPERATURE, +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, ) from . import AbodeDevice @@ -17,17 +16,17 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key=CONST.TEMP_STATUS_KEY, name="Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( key=CONST.HUMI_STATUS_KEY, name="Humidity", - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=CONST.LUX_STATUS_KEY, name="Lux", - device_class=DEVICE_CLASS_ILLUMINANCE, + device_class=SensorDeviceClass.ILLUMINANCE, ), ) From dbe0a801c6bda8087501b106d8f60156902937bd Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Wed, 8 Dec 2021 19:58:46 +0100 Subject: [PATCH 0160/2644] Use _attr_* in whois integration (#61250) --- homeassistant/components/whois/sensor.py | 50 ++++++------------------ 1 file changed, 12 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/whois/sensor.py b/homeassistant/components/whois/sensor.py index 5d5e595fa50..3742ed90131 100644 --- a/homeassistant/components/whois/sensor.py +++ b/homeassistant/components/whois/sensor.py @@ -49,45 +49,19 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class WhoisSensor(SensorEntity): """Implementation of a WHOIS sensor.""" + _attr_icon = "mdi:calendar-clock" + _attr_native_unit_of_measurement = TIME_DAYS + def __init__(self, name, domain): """Initialize the sensor.""" self.whois = whois.whois - - self._name = name self._domain = domain + self._attr_name = name - self._state = None - self._attributes = None - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def icon(self): - """Return the icon to represent this sensor.""" - return "mdi:calendar-clock" - - @property - def native_unit_of_measurement(self): - """Return the unit of measurement to present the value in.""" - return TIME_DAYS - - @property - def native_value(self): - """Return the expiration days for hostname.""" - return self._state - - @property - def extra_state_attributes(self): - """Get the more info attributes.""" - return self._attributes - - def _empty_state_and_attributes(self): + def _empty_value_and_attributes(self): """Empty the state and attributes on an error.""" - self._state = None - self._attributes = None + self._attr_native_value = None + self._attr_extra_state_attributes = None def update(self): """Get the current WHOIS data for the domain.""" @@ -95,7 +69,7 @@ class WhoisSensor(SensorEntity): response = self.whois(self._domain) except whois.BaseException as ex: # pylint: disable=broad-except _LOGGER.error("Exception %s occurred during WHOIS lookup", ex) - self._empty_state_and_attributes() + self._empty_value_and_attributes() return if response: @@ -105,12 +79,12 @@ class WhoisSensor(SensorEntity): "Did find: %s", ", ".join(response.keys()), ) - self._empty_state_and_attributes() + self._empty_value_and_attributes() return if not response["expiration_date"]: _LOGGER.error("Whois response contains empty expiration_date") - self._empty_state_and_attributes() + self._empty_value_and_attributes() return attrs = {} @@ -137,5 +111,5 @@ class WhoisSensor(SensorEntity): time_delta = expiration_date - expiration_date.now() - self._attributes = attrs - self._state = time_delta.days + self._attr_extra_state_attributes = attrs + self._attr_native_value = time_delta.days From 159506262a25f362bafec3d0b48e4b0045f096d7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 8 Dec 2021 19:59:26 +0100 Subject: [PATCH 0161/2644] Skip duplicated data when calculating fossil energy consumption (#60599) --- .../components/energy/websocket_api.py | 4 +- tests/components/energy/test_websocket_api.py | 217 +++++++++++++++++- 2 files changed, 219 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/energy/websocket_api.py b/homeassistant/components/energy/websocket_api.py index cdc7599b55b..d243faae89f 100644 --- a/homeassistant/components/energy/websocket_api.py +++ b/homeassistant/components/energy/websocket_api.py @@ -274,14 +274,16 @@ async def ws_get_fossil_energy_consumption( ) -> dict[datetime, float]: """Combine multiple statistics, returns a dict indexed by start time.""" result: defaultdict[datetime, float] = defaultdict(float) + seen: defaultdict[datetime, set[str]] = defaultdict(set) for statistics_id, stat in stats.items(): if statistics_id not in statistic_ids: continue for period in stat: - if period["sum"] is None: + if period["sum"] is None or statistics_id in seen[period["start"]]: continue result[period["start"]] += period["sum"] + seen[period["start"]].add(statistics_id) return {key: result[key] for key in sorted(result)} diff --git a/tests/components/energy/test_websocket_api.py b/tests/components/energy/test_websocket_api.py index 46c6a5c0fa6..c1dc195a63e 100644 --- a/tests/components/energy/test_websocket_api.py +++ b/tests/components/energy/test_websocket_api.py @@ -1,9 +1,10 @@ """Test the Energy websocket API.""" -from unittest.mock import AsyncMock, Mock +from unittest.mock import AsyncMock, Mock, patch import pytest from homeassistant.components.energy import data, is_configured +from homeassistant.components.recorder import statistics from homeassistant.components.recorder.statistics import async_add_external_statistics from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -963,6 +964,220 @@ async def test_fossil_energy_consumption(hass, hass_ws_client): } +@pytest.mark.freeze_time("2021-08-01 00:00:00+00:00") +async def test_fossil_energy_consumption_duplicate(hass, hass_ws_client): + """Test fossil_energy_consumption with co2 sensor data.""" + now = dt_util.utcnow() + later = dt_util.as_utc(dt_util.parse_datetime("2022-09-01 00:00:00")) + + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "history", {}) + await async_setup_component(hass, "sensor", {}) + + period1 = dt_util.as_utc(dt_util.parse_datetime("2021-09-01 00:00:00")) + period2 = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 23:00:00")) + period2_day_start = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 00:00:00")) + period3 = dt_util.as_utc(dt_util.parse_datetime("2021-10-01 00:00:00")) + period4 = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 23:00:00")) + period4_day_start = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 00:00:00")) + + external_energy_statistics_1 = ( + { + "start": period1, + "last_reset": None, + "state": 0, + "sum": 2, + }, + { + "start": period2, + "last_reset": None, + "state": 1, + "sum": 3, + }, + { + "start": period3, + "last_reset": None, + "state": 2, + "sum": 4, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 5, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 5, + }, + ) + external_energy_metadata_1 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_1", + "unit_of_measurement": "kWh", + } + external_energy_statistics_2 = ( + { + "start": period1, + "last_reset": None, + "state": 0, + "sum": 20, + }, + { + "start": period2, + "last_reset": None, + "state": 1, + "sum": 30, + }, + { + "start": period3, + "last_reset": None, + "state": 2, + "sum": 40, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 50, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 50, + }, + ) + external_energy_metadata_2 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_2", + "unit_of_measurement": "kWh", + } + external_co2_statistics = ( + { + "start": period1, + "last_reset": None, + "mean": 10, + }, + { + "start": period2, + "last_reset": None, + "mean": 30, + }, + { + "start": period3, + "last_reset": None, + "mean": 60, + }, + { + "start": period4, + "last_reset": None, + "mean": 90, + }, + ) + external_co2_metadata = { + "has_mean": True, + "has_sum": False, + "name": "Fossil percentage", + "source": "test", + "statistic_id": "test:fossil_percentage", + "unit_of_measurement": "%", + } + + with patch.object( + statistics, "_statistics_exists", return_value=False + ), patch.object( + statistics, "_insert_statistics", wraps=statistics._insert_statistics + ) as insert_statistics_mock: + async_add_external_statistics( + hass, external_energy_metadata_1, external_energy_statistics_1 + ) + async_add_external_statistics( + hass, external_energy_metadata_2, external_energy_statistics_2 + ) + async_add_external_statistics( + hass, external_co2_metadata, external_co2_statistics + ) + await async_wait_recording_done_without_instance(hass) + assert insert_statistics_mock.call_count == 14 + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "energy/fossil_energy_consumption", + "start_time": now.isoformat(), + "end_time": later.isoformat(), + "energy_statistic_ids": [ + "test:total_energy_import_tariff_1", + "test:total_energy_import_tariff_2", + ], + "co2_statistic_id": "test:fossil_percentage", + "period": "hour", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + period2.isoformat(): pytest.approx((33.0 - 22.0) * 0.3), + period3.isoformat(): pytest.approx((44.0 - 33.0) * 0.6), + period4.isoformat(): pytest.approx((55.0 - 44.0) * 0.9), + } + + await client.send_json( + { + "id": 2, + "type": "energy/fossil_energy_consumption", + "start_time": now.isoformat(), + "end_time": later.isoformat(), + "energy_statistic_ids": [ + "test:total_energy_import_tariff_1", + "test:total_energy_import_tariff_2", + ], + "co2_statistic_id": "test:fossil_percentage", + "period": "day", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + period2_day_start.isoformat(): pytest.approx((33.0 - 22.0) * 0.3), + period3.isoformat(): pytest.approx((44.0 - 33.0) * 0.6), + period4_day_start.isoformat(): pytest.approx((55.0 - 44.0) * 0.9), + } + + await client.send_json( + { + "id": 3, + "type": "energy/fossil_energy_consumption", + "start_time": now.isoformat(), + "end_time": later.isoformat(), + "energy_statistic_ids": [ + "test:total_energy_import_tariff_1", + "test:total_energy_import_tariff_2", + ], + "co2_statistic_id": "test:fossil_percentage", + "period": "month", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + period1.isoformat(): pytest.approx((33.0 - 22.0) * 0.3), + period3.isoformat(): pytest.approx( + ((44.0 - 33.0) * 0.6) + ((55.0 - 44.0) * 0.9) + ), + } + + async def test_fossil_energy_consumption_checks(hass, hass_ws_client): """Test fossil_energy_consumption parameter validation.""" client = await hass_ws_client(hass) From 7d256f56c5cfaaba43a36c4e69e2b2379f3558ec Mon Sep 17 00:00:00 2001 From: alim4r <7687869+alim4r@users.noreply.github.com> Date: Wed, 8 Dec 2021 20:18:21 +0100 Subject: [PATCH 0162/2644] Refactor Prometheus tests (#60451) * Removed prometheus from .coveragerc * Update prometheus tests with handler categories * Updated prometheus metrics to use the current registry - don't use the registry created on import (needed for tests) * Reset the prometheus CollectorRegistry before every test * Update prometheus metrics generation - Use latest registry when generating a response * Add default collectors when resetting the registry * Move entities to the specific prometheus test case * Refactor body generation for prometheus tests * Add test case for sensors without unit after rebase * Fix prometheus tests - Wait for events in prometheus tests - Add workaround for demo platform dependecy conversation (aiohttp frozen router) * Added prometheus tests for attribute metrics * Added prometheus tests for binary_sensor * Add prometheus test for input_boolean * Add prometheus test for lights * Add prometheus test for lock * Add prometheus test for sensor fahrenheit conversion * Fix prometheus test for input_number --- .coveragerc | 1 - .../components/prometheus/__init__.py | 9 +- tests/components/prometheus/test_init.py | 553 ++++++++++++++---- 3 files changed, 447 insertions(+), 116 deletions(-) diff --git a/.coveragerc b/.coveragerc index 2afc0300d8e..7ea4fa34a13 100644 --- a/.coveragerc +++ b/.coveragerc @@ -844,7 +844,6 @@ omit = homeassistant/components/progettihwsw/__init__.py homeassistant/components/progettihwsw/binary_sensor.py homeassistant/components/progettihwsw/switch.py - homeassistant/components/prometheus/* homeassistant/components/prowl/notify.py homeassistant/components/proxmoxve/* homeassistant/components/proxy/camera.py diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index 2eb4193377d..5b72a109d44 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -210,7 +210,12 @@ class PrometheusMetrics: full_metric_name = self._sanitize_metric_name( f"{self.metrics_prefix}{metric}" ) - self._metrics[metric] = factory(full_metric_name, documentation, labels) + self._metrics[metric] = factory( + full_metric_name, + documentation, + labels, + registry=self.prometheus_cli.REGISTRY, + ) return self._metrics[metric] @staticmethod @@ -524,6 +529,6 @@ class PrometheusView(HomeAssistantView): _LOGGER.debug("Received Prometheus metrics request") return web.Response( - body=self.prometheus_cli.generate_latest(), + body=self.prometheus_cli.generate_latest(self.prometheus_cli.REGISTRY), content_type=CONTENT_TYPE_TEXT_PLAIN, ) diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index df8c07bfc73..b625642d12e 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -4,9 +4,12 @@ import datetime from http import HTTPStatus import unittest.mock as mock +import prometheus_client import pytest -from homeassistant.components import climate, humidifier, sensor +from homeassistant.components import climate, humidifier, lock, sensor +from homeassistant.components.demo.binary_sensor import DemoBinarySensor +from homeassistant.components.demo.light import DemoLight from homeassistant.components.demo.number import DemoNumber from homeassistant.components.demo.sensor import DemoSensor import homeassistant.components.prometheus as prometheus @@ -15,8 +18,10 @@ from homeassistant.const import ( CONTENT_TYPE_TEXT_PLAIN, DEGREE, DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, ENERGY_KILO_WATT_HOUR, EVENT_STATE_CHANGED, + TEMP_FAHRENHEIT, ) from homeassistant.core import split_entity_id from homeassistant.setup import async_setup_component @@ -33,24 +38,109 @@ class FilterTest: should_pass: bool -async def prometheus_client(hass, hass_client, namespace): +async def setup_prometheus_client(hass, hass_client, namespace): """Initialize an hass_client with Prometheus component.""" + # Reset registry + prometheus_client.REGISTRY = prometheus_client.CollectorRegistry(auto_describe=True) + prometheus_client.ProcessCollector(registry=prometheus_client.REGISTRY) + prometheus_client.PlatformCollector(registry=prometheus_client.REGISTRY) + prometheus_client.GCCollector(registry=prometheus_client.REGISTRY) + config = {} if namespace is not None: config[prometheus.CONF_PROM_NAMESPACE] = namespace - await async_setup_component(hass, prometheus.DOMAIN, {prometheus.DOMAIN: config}) - - await async_setup_component(hass, sensor.DOMAIN, {"sensor": [{"platform": "demo"}]}) - - await async_setup_component( - hass, climate.DOMAIN, {"climate": [{"platform": "demo"}]} - ) - - await async_setup_component( - hass, humidifier.DOMAIN, {"humidifier": [{"platform": "demo"}]} + assert await async_setup_component( + hass, prometheus.DOMAIN, {prometheus.DOMAIN: config} ) await hass.async_block_till_done() + return await hass_client() + + +async def generate_latest_metrics(client): + """Generate the latest metrics and transform the body.""" + resp = await client.get(prometheus.API_ENDPOINT) + assert resp.status == HTTPStatus.OK + assert resp.headers["content-type"] == CONTENT_TYPE_TEXT_PLAIN + body = await resp.text() + body = body.split("\n") + + assert len(body) > 3 + + return body + + +async def test_view_empty_namespace(hass, hass_client): + """Test prometheus metrics view.""" + client = await setup_prometheus_client(hass, hass_client, "") + + sensor2 = DemoSensor( + None, "Radio Energy", 14, DEVICE_CLASS_POWER, None, ENERGY_KILO_WATT_HOUR, None + ) + sensor2.hass = hass + sensor2.entity_id = "sensor.radio_energy" + with mock.patch( + "homeassistant.util.dt.utcnow", + return_value=datetime.datetime(1970, 1, 2, tzinfo=dt_util.UTC), + ): + await sensor2.async_update_ha_state() + + await hass.async_block_till_done() + body = await generate_latest_metrics(client) + + assert "# HELP python_info Python platform information" in body + assert ( + "# HELP python_gc_objects_collected_total " + "Objects collected during gc" in body + ) + + assert ( + 'entity_available{domain="sensor",' + 'entity="sensor.radio_energy",' + 'friendly_name="Radio Energy"} 1.0' in body + ) + + assert ( + 'last_updated_time_seconds{domain="sensor",' + 'entity="sensor.radio_energy",' + 'friendly_name="Radio Energy"} 86400.0' in body + ) + + +async def test_view_default_namespace(hass, hass_client): + """Test prometheus metrics view.""" + assert await async_setup_component( + hass, + "conversation", + {}, + ) + + client = await setup_prometheus_client(hass, hass_client, None) + + assert await async_setup_component( + hass, sensor.DOMAIN, {"sensor": [{"platform": "demo"}]} + ) + await hass.async_block_till_done() + + body = await generate_latest_metrics(client) + + assert "# HELP python_info Python platform information" in body + assert ( + "# HELP python_gc_objects_collected_total " + "Objects collected during gc" in body + ) + + assert ( + 'homeassistant_sensor_temperature_celsius{domain="sensor",' + 'entity="sensor.outside_temperature",' + 'friendly_name="Outside Temperature"} 15.6' in body + ) + + +async def test_sensor_unit(hass, hass_client): + """Test prometheus metrics for sensors with a unit.""" + client = await setup_prometheus_client(hass, hass_client, "") + sensor1 = DemoSensor( None, "Television Energy", 74, None, None, ENERGY_KILO_WATT_HOUR, None ) @@ -100,6 +190,38 @@ async def prometheus_client(hass, hass_client, namespace): sensor5.entity_id = "sensor.sps30_pm_1um_weight_concentration" await sensor5.async_update_ha_state() + await hass.async_block_till_done() + body = await generate_latest_metrics(client) + + assert ( + 'sensor_unit_kwh{domain="sensor",' + 'entity="sensor.television_energy",' + 'friendly_name="Television Energy"} 74.0' in body + ) + + assert ( + 'sensor_unit_sek_per_kwh{domain="sensor",' + 'entity="sensor.electricity_price",' + 'friendly_name="Electricity price"} 0.123' in body + ) + + assert ( + 'sensor_unit_u0xb0{domain="sensor",' + 'entity="sensor.wind_direction",' + 'friendly_name="Wind Direction"} 25.0' in body + ) + + assert ( + 'sensor_unit_u0xb5g_per_mu0xb3{domain="sensor",' + 'entity="sensor.sps30_pm_1um_weight_concentration",' + 'friendly_name="SPS30 PM <1µm Weight concentration"} 3.7069' in body + ) + + +async def test_sensor_without_unit(hass, hass_client): + """Test prometheus metrics for sensors without a unit.""" + client = await setup_prometheus_client(hass, hass_client, "") + sensor6 = DemoSensor(None, "Trend Gradient", 0.002, None, None, None, None) sensor6.hass = hass sensor6.entity_id = "sensor.trend_gradient" @@ -115,6 +237,90 @@ async def prometheus_client(hass, hass_client, namespace): sensor8.entity_id = "sensor.text_unit" await sensor8.async_update_ha_state() + body = await generate_latest_metrics(client) + + assert ( + 'sensor_state{domain="sensor",' + 'entity="sensor.trend_gradient",' + 'friendly_name="Trend Gradient"} 0.002' in body + ) + + assert ( + 'sensor_state{domain="sensor",' + 'entity="sensor.text",' + 'friendly_name="Text"} 0' not in body + ) + + assert ( + 'sensor_unit_text{domain="sensor",' + 'entity="sensor.text_unit",' + 'friendly_name="Text Unit"} 0' not in body + ) + + +async def test_sensor_device_class(hass, hass_client): + """Test prometheus metrics for sensor with a device_class.""" + assert await async_setup_component( + hass, + "conversation", + {}, + ) + + client = await setup_prometheus_client(hass, hass_client, "") + + await async_setup_component(hass, sensor.DOMAIN, {"sensor": [{"platform": "demo"}]}) + await hass.async_block_till_done() + + sensor1 = DemoSensor( + None, "Fahrenheit", 50, DEVICE_CLASS_TEMPERATURE, None, TEMP_FAHRENHEIT, None + ) + sensor1.hass = hass + sensor1.entity_id = "sensor.fahrenheit" + await sensor1.async_update_ha_state() + + sensor2 = DemoSensor( + None, "Radio Energy", 14, DEVICE_CLASS_POWER, None, ENERGY_KILO_WATT_HOUR, None + ) + sensor2.hass = hass + sensor2.entity_id = "sensor.radio_energy" + with mock.patch( + "homeassistant.util.dt.utcnow", + return_value=datetime.datetime(1970, 1, 2, tzinfo=dt_util.UTC), + ): + await sensor2.async_update_ha_state() + + await hass.async_block_till_done() + body = await generate_latest_metrics(client) + + assert ( + 'sensor_temperature_celsius{domain="sensor",' + 'entity="sensor.fahrenheit",' + 'friendly_name="Fahrenheit"} 10.0' in body + ) + + assert ( + 'sensor_temperature_celsius{domain="sensor",' + 'entity="sensor.outside_temperature",' + 'friendly_name="Outside Temperature"} 15.6' in body + ) + + assert ( + 'sensor_humidity_percent{domain="sensor",' + 'entity="sensor.outside_humidity",' + 'friendly_name="Outside Humidity"} 54.0' in body + ) + + assert ( + 'sensor_power_kwh{domain="sensor",' + 'entity="sensor.radio_energy",' + 'friendly_name="Radio Energy"} 14.0' in body + ) + + +async def test_input_number(hass, hass_client): + """Test prometheus metrics for input_number.""" + client = await setup_prometheus_client(hass, hass_client, "") + number1 = DemoNumber(None, "Threshold", 5.2, None, False, 0, 10, 0.1) number1.hass = hass number1.entity_id = "input_number.threshold" @@ -126,39 +332,61 @@ async def prometheus_client(hass, hass_client, namespace): number2._attr_name = None await number2.async_update_ha_state() - return await hass_client() + await hass.async_block_till_done() + body = await generate_latest_metrics(client) - -async def test_view_empty_namespace(hass, hass_client): - """Test prometheus metrics view.""" - client = await prometheus_client(hass, hass_client, "") - resp = await client.get(prometheus.API_ENDPOINT) - - assert resp.status == HTTPStatus.OK - assert resp.headers["content-type"] == CONTENT_TYPE_TEXT_PLAIN - body = await resp.text() - body = body.split("\n") - - assert len(body) > 3 - - assert "# HELP python_info Python platform information" in body assert ( - "# HELP python_gc_objects_collected_total " - "Objects collected during gc" in body + 'input_number_state{domain="input_number",' + 'entity="input_number.threshold",' + 'friendly_name="Threshold"} 5.2' in body ) assert ( - 'sensor_temperature_celsius{domain="sensor",' - 'entity="sensor.outside_temperature",' - 'friendly_name="Outside Temperature"} 15.6' in body + 'input_number_state{domain="input_number",' + 'entity="input_number.brightness",' + 'friendly_name="None"} 60.0' in body ) + +async def test_battery(hass, hass_client): + """Test prometheus metrics for battery.""" + assert await async_setup_component( + hass, + "conversation", + {}, + ) + + client = await setup_prometheus_client(hass, hass_client, "") + + await async_setup_component(hass, sensor.DOMAIN, {"sensor": [{"platform": "demo"}]}) + await hass.async_block_till_done() + + body = await generate_latest_metrics(client) + assert ( 'battery_level_percent{domain="sensor",' 'entity="sensor.outside_temperature",' 'friendly_name="Outside Temperature"} 12.0' in body ) + +async def test_climate(hass, hass_client): + """Test prometheus metrics for battery.""" + assert await async_setup_component( + hass, + "conversation", + {}, + ) + + client = await setup_prometheus_client(hass, hass_client, "") + + await async_setup_component( + hass, climate.DOMAIN, {"climate": [{"platform": "demo"}]} + ) + + await hass.async_block_till_done() + body = await generate_latest_metrics(client) + assert ( 'climate_current_temperature_celsius{domain="climate",' 'entity="climate.heatpump",' @@ -183,6 +411,24 @@ async def test_view_empty_namespace(hass, hass_client): 'friendly_name="Ecobee"} 24.0' in body ) + +async def test_humidifier(hass, hass_client): + """Test prometheus metrics for battery.""" + assert await async_setup_component( + hass, + "conversation", + {}, + ) + + client = await setup_prometheus_client(hass, hass_client, "") + + await async_setup_component( + hass, humidifier.DOMAIN, {"humidifier": [{"platform": "demo"}]} + ) + + await hass.async_block_till_done() + body = await generate_latest_metrics(client) + assert ( 'humidifier_target_humidity_percent{domain="humidifier",' 'entity="humidifier.humidifier",' @@ -208,107 +454,188 @@ async def test_view_empty_namespace(hass, hass_client): 'mode="eco"} 0.0' in body ) + +async def test_attributes(hass, hass_client): + """Test prometheus metrics for entity attributes.""" + client = await setup_prometheus_client(hass, hass_client, "") + + switch1 = DemoSensor(None, "Boolean", 74, None, None, None, None) + switch1.hass = hass + switch1.entity_id = "switch.boolean" + switch1._attr_extra_state_attributes = {"boolean": True} + await switch1.async_update_ha_state() + + switch2 = DemoSensor(None, "Number", 42, None, None, None, None) + switch2.hass = hass + switch2.entity_id = "switch.number" + switch2._attr_extra_state_attributes = {"Number": 10.2} + await switch2.async_update_ha_state() + + await hass.async_block_till_done() + body = await generate_latest_metrics(client) + assert ( - 'sensor_humidity_percent{domain="sensor",' - 'entity="sensor.outside_humidity",' - 'friendly_name="Outside Humidity"} 54.0' in body + 'switch_state{domain="switch",' + 'entity="switch.boolean",' + 'friendly_name="Boolean"} 74.0' in body ) assert ( - 'sensor_unit_kwh{domain="sensor",' - 'entity="sensor.television_energy",' - 'friendly_name="Television Energy"} 74.0' in body + 'switch_attr_boolean{domain="switch",' + 'entity="switch.boolean",' + 'friendly_name="Boolean"} 1.0' in body ) assert ( - 'sensor_power_kwh{domain="sensor",' - 'entity="sensor.radio_energy",' - 'friendly_name="Radio Energy"} 14.0' in body + 'switch_state{domain="switch",' + 'entity="switch.number",' + 'friendly_name="Number"} 42.0' in body ) assert ( - 'entity_available{domain="sensor",' - 'entity="sensor.radio_energy",' - 'friendly_name="Radio Energy"} 1.0' in body - ) - - assert ( - 'last_updated_time_seconds{domain="sensor",' - 'entity="sensor.radio_energy",' - 'friendly_name="Radio Energy"} 86400.0' in body - ) - - assert ( - 'sensor_unit_sek_per_kwh{domain="sensor",' - 'entity="sensor.electricity_price",' - 'friendly_name="Electricity price"} 0.123' in body - ) - - assert ( - 'sensor_unit_u0xb0{domain="sensor",' - 'entity="sensor.wind_direction",' - 'friendly_name="Wind Direction"} 25.0' in body - ) - - assert ( - 'sensor_unit_u0xb5g_per_mu0xb3{domain="sensor",' - 'entity="sensor.sps30_pm_1um_weight_concentration",' - 'friendly_name="SPS30 PM <1µm Weight concentration"} 3.7069' in body - ) - - assert ( - 'sensor_state{domain="sensor",' - 'entity="sensor.trend_gradient",' - 'friendly_name="Trend Gradient"} 0.002' in body - ) - - assert ( - 'sensor_state{domain="sensor",' - 'entity="sensor.text",' - 'friendly_name="Text"} 0' not in body - ) - - assert ( - 'sensor_unit_text{domain="sensor",' - 'entity="sensor.text_unit",' - 'friendly_name="Text Unit"} 0' not in body - ) - - assert ( - 'input_number_state{domain="input_number",' - 'entity="input_number.threshold",' - 'friendly_name="Threshold"} 5.2' in body - ) - - assert ( - 'input_number_state{domain="input_number",' - 'entity="input_number.brightness",' - 'friendly_name="None"} 60.0' in body + 'switch_attr_number{domain="switch",' + 'entity="switch.number",' + 'friendly_name="Number"} 10.2' in body ) -async def test_view_default_namespace(hass, hass_client): - """Test prometheus metrics view.""" - client = await prometheus_client(hass, hass_client, None) - resp = await client.get(prometheus.API_ENDPOINT) +async def test_binary_sensor(hass, hass_client): + """Test prometheus metrics for binary_sensor.""" + client = await setup_prometheus_client(hass, hass_client, "") - assert resp.status == HTTPStatus.OK - assert resp.headers["content-type"] == CONTENT_TYPE_TEXT_PLAIN - body = await resp.text() - body = body.split("\n") + binary_sensor1 = DemoBinarySensor(None, "Door", True, None) + binary_sensor1.hass = hass + binary_sensor1.entity_id = "binary_sensor.door" + await binary_sensor1.async_update_ha_state() - assert len(body) > 3 + binary_sensor1 = DemoBinarySensor(None, "Window", False, None) + binary_sensor1.hass = hass + binary_sensor1.entity_id = "binary_sensor.window" + await binary_sensor1.async_update_ha_state() + + await hass.async_block_till_done() + body = await generate_latest_metrics(client) - assert "# HELP python_info Python platform information" in body assert ( - "# HELP python_gc_objects_collected_total " - "Objects collected during gc" in body + 'binary_sensor_state{domain="binary_sensor",' + 'entity="binary_sensor.door",' + 'friendly_name="Door"} 1.0' in body ) assert ( - 'homeassistant_sensor_temperature_celsius{domain="sensor",' - 'entity="sensor.outside_temperature",' - 'friendly_name="Outside Temperature"} 15.6' in body + 'binary_sensor_state{domain="binary_sensor",' + 'entity="binary_sensor.window",' + 'friendly_name="Window"} 0.0' in body + ) + + +async def test_input_boolean(hass, hass_client): + """Test prometheus metrics for input_boolean.""" + client = await setup_prometheus_client(hass, hass_client, "") + + input_boolean1 = DemoSensor(None, "Test", 1, None, None, None, None) + input_boolean1.hass = hass + input_boolean1.entity_id = "input_boolean.test" + await input_boolean1.async_update_ha_state() + + input_boolean2 = DemoSensor(None, "Helper", 0, None, None, None, None) + input_boolean2.hass = hass + input_boolean2.entity_id = "input_boolean.helper" + await input_boolean2.async_update_ha_state() + + await hass.async_block_till_done() + body = await generate_latest_metrics(client) + + assert ( + 'input_boolean_state{domain="input_boolean",' + 'entity="input_boolean.test",' + 'friendly_name="Test"} 1.0' in body + ) + + assert ( + 'input_boolean_state{domain="input_boolean",' + 'entity="input_boolean.helper",' + 'friendly_name="Helper"} 0.0' in body + ) + + +async def test_light(hass, hass_client): + """Test prometheus metrics for lights.""" + client = await setup_prometheus_client(hass, hass_client, "") + + light1 = DemoSensor(None, "Desk", 1, None, None, None, None) + light1.hass = hass + light1.entity_id = "light.desk" + await light1.async_update_ha_state() + + light2 = DemoSensor(None, "Wall", 0, None, None, None, None) + light2.hass = hass + light2.entity_id = "light.wall" + await light2.async_update_ha_state() + + light3 = DemoLight(None, "TV", True, True, 255, None, None) + light3.hass = hass + light3.entity_id = "light.tv" + await light3.async_update_ha_state() + + light4 = DemoLight(None, "PC", True, True, 180, None, None) + light4.hass = hass + light4.entity_id = "light.pc" + await light4.async_update_ha_state() + + await hass.async_block_till_done() + body = await generate_latest_metrics(client) + + assert ( + 'light_brightness_percent{domain="light",' + 'entity="light.desk",' + 'friendly_name="Desk"} 100.0' in body + ) + + assert ( + 'light_brightness_percent{domain="light",' + 'entity="light.wall",' + 'friendly_name="Wall"} 0.0' in body + ) + + assert ( + 'light_brightness_percent{domain="light",' + 'entity="light.tv",' + 'friendly_name="TV"} 100.0' in body + ) + + assert ( + 'light_brightness_percent{domain="light",' + 'entity="light.pc",' + 'friendly_name="PC"} 70.58823529411765' in body + ) + + +async def test_lock(hass, hass_client): + """Test prometheus metrics for lock.""" + assert await async_setup_component( + hass, + "conversation", + {}, + ) + + client = await setup_prometheus_client(hass, hass_client, "") + + await async_setup_component(hass, lock.DOMAIN, {"lock": [{"platform": "demo"}]}) + + await hass.async_block_till_done() + body = await generate_latest_metrics(client) + + assert ( + 'lock_state{domain="lock",' + 'entity="lock.front_door",' + 'friendly_name="Front Door"} 1.0' in body + ) + + assert ( + 'lock_state{domain="lock",' + 'entity="lock.kitchen_door",' + 'friendly_name="Kitchen Door"} 0.0' in body ) From 77a74a9bf4a64315759f7987e0962e2fb5c36f5b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 8 Dec 2021 11:44:53 -0800 Subject: [PATCH 0163/2644] Fix smartthings timestamp sensor (#61254) --- homeassistant/components/smartthings/sensor.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index eab840bd629..fa749e07dfb 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -38,6 +38,7 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, VOLUME_CUBIC_METERS, ) +from homeassistant.util import dt as dt_util from . import SmartThingsEntity from .const import DATA_BROKERS, DOMAIN @@ -656,7 +657,12 @@ class SmartThingsSensor(SmartThingsEntity, SensorEntity): @property def native_value(self): """Return the state of the sensor.""" - return self._device.status.attributes[self._attribute].value + value = self._device.status.attributes[self._attribute].value + + if self._device_class != DEVICE_CLASS_TIMESTAMP: + return value + + return dt_util.parse_datetime(value) @property def device_class(self): From c71fe7165448a257477bb4611566277e0abfdb90 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 8 Dec 2021 20:49:02 +0100 Subject: [PATCH 0164/2644] Use new SensorDeviceClass enum in airvisual (#61261) Co-authored-by: epenet --- homeassistant/components/airvisual/sensor.py | 35 ++++++++------------ 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index f5dfc4f1b11..13b87112716 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations from homeassistant.components.sensor import ( + SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, @@ -18,21 +19,11 @@ from homeassistant.const import ( CONF_LONGITUDE, CONF_SHOW_ON_MAP, CONF_STATE, - DEVICE_CLASS_AQI, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_CO2, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_PM1, - DEVICE_CLASS_PM10, - DEVICE_CLASS_PM25, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, - ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -79,7 +70,7 @@ GEOGRAPHY_SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=SENSOR_KIND_AQI, name="Air Quality Index", - device_class=DEVICE_CLASS_AQI, + device_class=SensorDeviceClass.AQI, native_unit_of_measurement="AQI", state_class=SensorStateClass.MEASUREMENT, ), @@ -96,62 +87,62 @@ NODE_PRO_SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=SENSOR_KIND_AQI, name="Air Quality Index", - device_class=DEVICE_CLASS_AQI, + device_class=SensorDeviceClass.AQI, native_unit_of_measurement="AQI", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_KIND_BATTERY_LEVEL, name="Battery", - device_class=DEVICE_CLASS_BATTERY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=SensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( key=SENSOR_KIND_CO2, name="C02", - device_class=DEVICE_CLASS_CO2, + device_class=SensorDeviceClass.CO2, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_KIND_HUMIDITY, name="Humidity", - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( key=SENSOR_KIND_PM_0_1, name="PM 0.1", - device_class=DEVICE_CLASS_PM1, + device_class=SensorDeviceClass.PM1, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_KIND_PM_1_0, name="PM 1.0", - device_class=DEVICE_CLASS_PM10, + device_class=SensorDeviceClass.PM10, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_KIND_PM_2_5, name="PM 2.5", - device_class=DEVICE_CLASS_PM25, + device_class=SensorDeviceClass.PM25, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_KIND_TEMPERATURE, name="Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_KIND_VOC, name="VOC", - device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, state_class=SensorStateClass.MEASUREMENT, ), From 8a11cf3d1f18d04a1d284d59a396bab0e29c2bd7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 8 Dec 2021 21:06:31 +0100 Subject: [PATCH 0165/2644] Use new DeviceClass, StateClass and EntityCategory enums in ambient_station (#61266) Co-authored-by: epenet --- .../ambient_station/binary_sensor.py | 102 ++++----- .../components/ambient_station/sensor.py | 211 +++++++++--------- 2 files changed, 153 insertions(+), 160 deletions(-) diff --git a/homeassistant/components/ambient_station/binary_sensor.py b/homeassistant/components/ambient_station/binary_sensor.py index 0c819a9a9b7..153fbf066db 100644 --- a/homeassistant/components/ambient_station/binary_sensor.py +++ b/homeassistant/components/ambient_station/binary_sensor.py @@ -5,14 +5,14 @@ from dataclasses import dataclass from typing import Literal from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_CONNECTIVITY, + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_NAME, ENTITY_CATEGORY_DIAGNOSTIC +from homeassistant.const import ATTR_NAME from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AmbientWeatherEntity @@ -62,169 +62,169 @@ BINARY_SENSOR_DESCRIPTIONS = ( AmbientBinarySensorDescription( key=TYPE_BATTOUT, name="Battery", - device_class=DEVICE_CLASS_BATTERY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT1, name="Battery 1", - device_class=DEVICE_CLASS_BATTERY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT2, name="Battery 2", - device_class=DEVICE_CLASS_BATTERY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT3, name="Battery 3", - device_class=DEVICE_CLASS_BATTERY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT4, name="Battery 4", - device_class=DEVICE_CLASS_BATTERY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT5, name="Battery 5", - device_class=DEVICE_CLASS_BATTERY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT6, name="Battery 6", - device_class=DEVICE_CLASS_BATTERY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT7, name="Battery 7", - device_class=DEVICE_CLASS_BATTERY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT8, name="Battery 8", - device_class=DEVICE_CLASS_BATTERY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT9, name="Battery 9", - device_class=DEVICE_CLASS_BATTERY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT10, name="Battery 10", - device_class=DEVICE_CLASS_BATTERY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_CO2, name="CO2 Battery", - device_class=DEVICE_CLASS_BATTERY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_PM25IN_BATT, name="PM25 Indoor Battery", - device_class=DEVICE_CLASS_BATTERY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_PM25_BATT, name="PM25 Battery", - device_class=DEVICE_CLASS_BATTERY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_RELAY1, name="Relay 1", - device_class=DEVICE_CLASS_CONNECTIVITY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=EntityCategory.DIAGNOSTIC, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_RELAY2, name="Relay 2", - device_class=DEVICE_CLASS_CONNECTIVITY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=EntityCategory.DIAGNOSTIC, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_RELAY3, name="Relay 3", - device_class=DEVICE_CLASS_CONNECTIVITY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=EntityCategory.DIAGNOSTIC, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_RELAY4, name="Relay 4", - device_class=DEVICE_CLASS_CONNECTIVITY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=EntityCategory.DIAGNOSTIC, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_RELAY5, name="Relay 5", - device_class=DEVICE_CLASS_CONNECTIVITY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=EntityCategory.DIAGNOSTIC, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_RELAY6, name="Relay 6", - device_class=DEVICE_CLASS_CONNECTIVITY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=EntityCategory.DIAGNOSTIC, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_RELAY7, name="Relay 7", - device_class=DEVICE_CLASS_CONNECTIVITY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=EntityCategory.DIAGNOSTIC, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_RELAY8, name="Relay 8", - device_class=DEVICE_CLASS_CONNECTIVITY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=EntityCategory.DIAGNOSTIC, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_RELAY9, name="Relay 9", - device_class=DEVICE_CLASS_CONNECTIVITY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=EntityCategory.DIAGNOSTIC, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_RELAY10, name="Relay 10", - device_class=DEVICE_CLASS_CONNECTIVITY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=EntityCategory.DIAGNOSTIC, on_state=1, ), ) diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index 58ac081efbf..c5b8b57297f 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -4,10 +4,10 @@ from __future__ import annotations from datetime import datetime from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -15,13 +15,6 @@ from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_MILLION, DEGREE, - DEVICE_CLASS_CO2, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_PM25, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_TIMESTAMP, IRRADIATION_WATTS_PER_SQUARE_METER, LIGHT_LUX, PERCENTAGE, @@ -122,430 +115,430 @@ SENSOR_DESCRIPTIONS = ( name="24 Hr Rain", icon="mdi:water", native_unit_of_measurement=PRECIPITATION_INCHES, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key=TYPE_BAROMABSIN, name="Abs Pressure", native_unit_of_measurement=PRESSURE_INHG, - device_class=DEVICE_CLASS_PRESSURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_BAROMRELIN, name="Rel Pressure", native_unit_of_measurement=PRESSURE_INHG, - device_class=DEVICE_CLASS_PRESSURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_CO2, name="co2", native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, - device_class=DEVICE_CLASS_CO2, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CO2, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_DAILYRAININ, name="Daily Rain", icon="mdi:water", native_unit_of_measurement=PRECIPITATION_INCHES, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key=TYPE_DEWPOINT, name="Dew Point", native_unit_of_measurement=TEMP_FAHRENHEIT, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_EVENTRAININ, name="Event Rain", icon="mdi:water", native_unit_of_measurement=PRECIPITATION_INCHES, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_FEELSLIKE, name="Feels Like", native_unit_of_measurement=TEMP_FAHRENHEIT, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_HOURLYRAININ, name="Hourly Rain Rate", icon="mdi:water", native_unit_of_measurement=PRECIPITATION_INCHES_PER_HOUR, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key=TYPE_HUMIDITY10, name="Humidity 10", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_HUMIDITY1, name="Humidity 1", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_HUMIDITY2, name="Humidity 2", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_HUMIDITY3, name="Humidity 3", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_HUMIDITY4, name="Humidity 4", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_HUMIDITY5, name="Humidity 5", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_HUMIDITY6, name="Humidity 6", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_HUMIDITY7, name="Humidity 7", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_HUMIDITY8, name="Humidity 8", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_HUMIDITY9, name="Humidity 9", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_HUMIDITY, name="Humidity", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_HUMIDITYIN, name="Humidity In", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_LASTRAIN, name="Last Rain", icon="mdi:water", - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, ), SensorEntityDescription( key=TYPE_MAXDAILYGUST, name="Max Gust", icon="mdi:weather-windy", native_unit_of_measurement=SPEED_MILES_PER_HOUR, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_MONTHLYRAININ, name="Monthly Rain", icon="mdi:water", native_unit_of_measurement=PRECIPITATION_INCHES, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_PM25_24H, name="PM25 24h Avg", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - device_class=DEVICE_CLASS_PM25, + device_class=SensorDeviceClass.PM25, ), SensorEntityDescription( key=TYPE_PM25_IN, name="PM25 Indoor", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - device_class=DEVICE_CLASS_PM25, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.PM25, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_PM25_IN_24H, name="PM25 Indoor 24h Avg", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - device_class=DEVICE_CLASS_PM25, + device_class=SensorDeviceClass.PM25, ), SensorEntityDescription( key=TYPE_PM25, name="PM25", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - device_class=DEVICE_CLASS_PM25, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.PM25, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILHUM10, name="Soil Humidity 10", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_SOILHUM1, name="Soil Humidity 1", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_SOILHUM2, name="Soil Humidity 2", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_SOILHUM3, name="Soil Humidity 3", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_SOILHUM4, name="Soil Humidity 4", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_SOILHUM5, name="Soil Humidity 5", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_SOILHUM6, name="Soil Humidity 6", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_SOILHUM7, name="Soil Humidity 7", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_SOILHUM8, name="Soil Humidity 8", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_SOILHUM9, name="Soil Humidity 9", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_SOILTEMP10F, name="Soil Temp 10", native_unit_of_measurement=TEMP_FAHRENHEIT, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP1F, name="Soil Temp 1", native_unit_of_measurement=TEMP_FAHRENHEIT, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP2F, name="Soil Temp 2", native_unit_of_measurement=TEMP_FAHRENHEIT, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP3F, name="Soil Temp 3", native_unit_of_measurement=TEMP_FAHRENHEIT, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP4F, name="Soil Temp 4", native_unit_of_measurement=TEMP_FAHRENHEIT, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP5F, name="Soil Temp 5", native_unit_of_measurement=TEMP_FAHRENHEIT, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP6F, name="Soil Temp 6", native_unit_of_measurement=TEMP_FAHRENHEIT, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP7F, name="Soil Temp 7", native_unit_of_measurement=TEMP_FAHRENHEIT, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP8F, name="Soil Temp 8", native_unit_of_measurement=TEMP_FAHRENHEIT, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP9F, name="Soil Temp 9", native_unit_of_measurement=TEMP_FAHRENHEIT, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOLARRADIATION, name="Solar Rad", native_unit_of_measurement=IRRADIATION_WATTS_PER_SQUARE_METER, - device_class=DEVICE_CLASS_ILLUMINANCE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOLARRADIATION_LX, name="Solar Rad", native_unit_of_measurement=LIGHT_LUX, - device_class=DEVICE_CLASS_ILLUMINANCE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_TEMP10F, name="Temp 10", native_unit_of_measurement=TEMP_FAHRENHEIT, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_TEMP1F, name="Temp 1", native_unit_of_measurement=TEMP_FAHRENHEIT, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_TEMP2F, name="Temp 2", native_unit_of_measurement=TEMP_FAHRENHEIT, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_TEMP3F, name="Temp 3", native_unit_of_measurement=TEMP_FAHRENHEIT, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_TEMP4F, name="Temp 4", native_unit_of_measurement=TEMP_FAHRENHEIT, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_TEMP5F, name="Temp 5", native_unit_of_measurement=TEMP_FAHRENHEIT, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_TEMP6F, name="Temp 6", native_unit_of_measurement=TEMP_FAHRENHEIT, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_TEMP7F, name="Temp 7", native_unit_of_measurement=TEMP_FAHRENHEIT, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_TEMP8F, name="Temp 8", native_unit_of_measurement=TEMP_FAHRENHEIT, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_TEMP9F, name="Temp 9", native_unit_of_measurement=TEMP_FAHRENHEIT, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_TEMPF, name="Temp", native_unit_of_measurement=TEMP_FAHRENHEIT, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_TEMPINF, name="Inside Temp", native_unit_of_measurement=TEMP_FAHRENHEIT, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_TOTALRAININ, name="Lifetime Rain", icon="mdi:water", native_unit_of_measurement=PRECIPITATION_INCHES, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_UV, name="UV Index", native_unit_of_measurement="Index", - device_class=DEVICE_CLASS_ILLUMINANCE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_WEEKLYRAININ, name="Weekly Rain", icon="mdi:water", native_unit_of_measurement=PRECIPITATION_INCHES, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_WINDDIR, @@ -576,7 +569,7 @@ SENSOR_DESCRIPTIONS = ( name="Wind Gust", icon="mdi:weather-windy", native_unit_of_measurement=SPEED_MILES_PER_HOUR, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_WINDSPDMPH_AVG10M, @@ -595,14 +588,14 @@ SENSOR_DESCRIPTIONS = ( name="Wind Speed", icon="mdi:weather-windy", native_unit_of_measurement=SPEED_MILES_PER_HOUR, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_YEARLYRAININ, name="Yearly Rain", icon="mdi:water", native_unit_of_measurement=PRECIPITATION_INCHES, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, ), ) From e4e0dcbae003d67705dcd6596be25d9487470fcd Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 8 Dec 2021 21:13:18 +0100 Subject: [PATCH 0166/2644] Use new BinarySensorDeviceClass enum in amcrest (#61268) Co-authored-by: epenet --- .../components/amcrest/binary_sensor.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/amcrest/binary_sensor.py b/homeassistant/components/amcrest/binary_sensor.py index 4fc810fd773..8cbc00e1f1f 100644 --- a/homeassistant/components/amcrest/binary_sensor.py +++ b/homeassistant/components/amcrest/binary_sensor.py @@ -12,9 +12,7 @@ from amcrest import AmcrestError import voluptuous as vol from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_CONNECTIVITY, - DEVICE_CLASS_MOTION, - DEVICE_CLASS_SOUND, + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -72,46 +70,46 @@ BINARY_SENSORS: tuple[AmcrestSensorEntityDescription, ...] = ( AmcrestSensorEntityDescription( key=_AUDIO_DETECTED_KEY, name=_AUDIO_DETECTED_NAME, - device_class=DEVICE_CLASS_SOUND, + device_class=BinarySensorDeviceClass.SOUND, event_code=_AUDIO_DETECTED_EVENT_CODE, ), AmcrestSensorEntityDescription( key=_AUDIO_DETECTED_POLLED_KEY, name=_AUDIO_DETECTED_NAME, - device_class=DEVICE_CLASS_SOUND, + device_class=BinarySensorDeviceClass.SOUND, event_code=_AUDIO_DETECTED_EVENT_CODE, should_poll=True, ), AmcrestSensorEntityDescription( key=_CROSSLINE_DETECTED_KEY, name=_CROSSLINE_DETECTED_NAME, - device_class=DEVICE_CLASS_MOTION, + device_class=BinarySensorDeviceClass.MOTION, event_code=_CROSSLINE_DETECTED_EVENT_CODE, ), AmcrestSensorEntityDescription( key=_CROSSLINE_DETECTED_POLLED_KEY, name=_CROSSLINE_DETECTED_NAME, - device_class=DEVICE_CLASS_MOTION, + device_class=BinarySensorDeviceClass.MOTION, event_code=_CROSSLINE_DETECTED_EVENT_CODE, should_poll=True, ), AmcrestSensorEntityDescription( key=_MOTION_DETECTED_KEY, name=_MOTION_DETECTED_NAME, - device_class=DEVICE_CLASS_MOTION, + device_class=BinarySensorDeviceClass.MOTION, event_code=_MOTION_DETECTED_EVENT_CODE, ), AmcrestSensorEntityDescription( key=_MOTION_DETECTED_POLLED_KEY, name=_MOTION_DETECTED_NAME, - device_class=DEVICE_CLASS_MOTION, + device_class=BinarySensorDeviceClass.MOTION, event_code=_MOTION_DETECTED_EVENT_CODE, should_poll=True, ), AmcrestSensorEntityDescription( key=_ONLINE_KEY, name="Online", - device_class=DEVICE_CLASS_CONNECTIVITY, + device_class=BinarySensorDeviceClass.CONNECTIVITY, should_poll=True, ), ) From 549b72e48e9cd904faf7b8265ae868e49a151b5e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 8 Dec 2021 21:13:33 +0100 Subject: [PATCH 0167/2644] Use new SensorStateClass enum in amberelectric (#61265) Co-authored-by: epenet --- homeassistant/components/amberelectric/sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/amberelectric/sensor.py b/homeassistant/components/amberelectric/sensor.py index 7cee95dcfcf..64ff09470e5 100644 --- a/homeassistant/components/amberelectric/sensor.py +++ b/homeassistant/components/amberelectric/sensor.py @@ -15,9 +15,9 @@ from amberelectric.model.current_interval import CurrentInterval from amberelectric.model.forecast_interval import ForecastInterval from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CURRENCY_DOLLAR, ENERGY_KILO_WATT_HOUR @@ -209,7 +209,7 @@ async def async_setup_entry( key="current", name=f"{entry.title} - {friendly_channel_type(channel_type)} Price", native_unit_of_measurement=UNIT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, icon=ICONS[channel_type], ) entities.append(AmberPriceSensor(coordinator, description, channel_type)) @@ -219,7 +219,7 @@ async def async_setup_entry( key="forecasts", name=f"{entry.title} - {friendly_channel_type(channel_type)} Forecast", native_unit_of_measurement=UNIT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, icon=ICONS[channel_type], ) entities.append(AmberForecastSensor(coordinator, description, channel_type)) @@ -228,7 +228,7 @@ async def async_setup_entry( key="renewables", name=f"{entry.title} - Renewables", native_unit_of_measurement="%", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, icon="mdi:solar-power", ) entities.append(AmberGridSensor(coordinator, renewables_description)) From c1a09d2bac01f678b0210963bde2d2f41d39b3de Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 8 Dec 2021 21:13:59 +0100 Subject: [PATCH 0168/2644] Use new DeviceClass and StateClass enums in ambee (#61264) Co-authored-by: epenet --- homeassistant/components/ambee/const.py | 54 ++++++++++++------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/ambee/const.py b/homeassistant/components/ambee/const.py index a63aa4b804d..8f8f2237654 100644 --- a/homeassistant/components/ambee/const.py +++ b/homeassistant/components/ambee/const.py @@ -6,15 +6,15 @@ import logging from typing import Final from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, + SensorDeviceClass, SensorEntityDescription, + SensorStateClass, ) from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_MILLION, - DEVICE_CLASS_CO, ) DOMAIN: Final = "ambee" @@ -37,43 +37,43 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { key="particulate_matter_2_5", name="Particulate Matter < 2.5 μm", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="particulate_matter_10", name="Particulate Matter < 10 μm", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="sulphur_dioxide", name="Sulphur Dioxide (SO2)", native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="nitrogen_dioxide", name="Nitrogen Dioxide (NO2)", native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="ozone", name="Ozone", native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="carbon_monoxide", name="Carbon Monoxide (CO)", - device_class=DEVICE_CLASS_CO, + device_class=SensorDeviceClass.CO, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="air_quality_index", name="Air Quality Index (AQI)", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), ], SERVICE_POLLEN: [ @@ -81,21 +81,21 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { key="grass", name="Grass Pollen", icon="mdi:grass", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, ), SensorEntityDescription( key="tree", name="Tree Pollen", icon="mdi:tree", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, ), SensorEntityDescription( key="weed", name="Weed Pollen", icon="mdi:sprout", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, ), SensorEntityDescription( @@ -120,7 +120,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { key="grass_poaceae", name="Poaceae Grass Pollen", icon="mdi:grass", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), @@ -128,7 +128,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { key="tree_alder", name="Alder Tree Pollen", icon="mdi:tree", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), @@ -136,7 +136,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { key="tree_birch", name="Birch Tree Pollen", icon="mdi:tree", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), @@ -144,7 +144,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { key="tree_cypress", name="Cypress Tree Pollen", icon="mdi:tree", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), @@ -152,7 +152,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { key="tree_elm", name="Elm Tree Pollen", icon="mdi:tree", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), @@ -160,7 +160,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { key="tree_hazel", name="Hazel Tree Pollen", icon="mdi:tree", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), @@ -168,7 +168,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { key="tree_oak", name="Oak Tree Pollen", icon="mdi:tree", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), @@ -176,7 +176,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { key="tree_pine", name="Pine Tree Pollen", icon="mdi:tree", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), @@ -184,7 +184,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { key="tree_plane", name="Plane Tree Pollen", icon="mdi:tree", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), @@ -192,7 +192,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { key="tree_poplar", name="Poplar Tree Pollen", icon="mdi:tree", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), @@ -200,7 +200,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { key="weed_chenopod", name="Chenopod Weed Pollen", icon="mdi:sprout", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), @@ -208,7 +208,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { key="weed_mugwort", name="Mugwort Weed Pollen", icon="mdi:sprout", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), @@ -216,7 +216,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { key="weed_nettle", name="Nettle Weed Pollen", icon="mdi:sprout", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), @@ -224,7 +224,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { key="weed_ragweed", name="Ragweed Weed Pollen", icon="mdi:sprout", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False, ), From e460eec134bc858fc48321a2c8eee489f8f35e41 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 8 Dec 2021 21:16:43 +0100 Subject: [PATCH 0169/2644] Use new CoverDeviceClass enum in aladdin_connect (#61262) Co-authored-by: epenet --- homeassistant/components/aladdin_connect/cover.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index 5cebe3622dc..f05b27eea8d 100644 --- a/homeassistant/components/aladdin_connect/cover.py +++ b/homeassistant/components/aladdin_connect/cover.py @@ -8,8 +8,8 @@ from aladdin_connect import AladdinConnectClient import voluptuous as vol from homeassistant.components.cover import ( - DEVICE_CLASS_GARAGE, PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, + CoverDeviceClass, CoverEntity, ) from homeassistant.const import ( @@ -65,7 +65,7 @@ def setup_platform( class AladdinDevice(CoverEntity): """Representation of Aladdin Connect cover.""" - _attr_device_class = DEVICE_CLASS_GARAGE + _attr_device_class = CoverDeviceClass.GARAGE _attr_supported_features = SUPPORTED_FEATURES def __init__(self, acc: AladdinConnectClient, device: DoorDevice) -> None: From 646da7a9b77d089b85f25da06ab5938c9f7609cf Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 8 Dec 2021 21:17:03 +0100 Subject: [PATCH 0170/2644] Use new EntityCategory enum in airthings (#61260) Co-authored-by: epenet --- homeassistant/components/airthings/sensor.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/airthings/sensor.py b/homeassistant/components/airthings/sensor.py index 7f3d3693a4b..a753a0b213f 100644 --- a/homeassistant/components/airthings/sensor.py +++ b/homeassistant/components/airthings/sensor.py @@ -15,14 +15,13 @@ from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, - ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, PRESSURE_MBAR, SIGNAL_STRENGTH_DECIBELS, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -59,7 +58,7 @@ SENSORS: dict[str, SensorEntityDescription] = { key="battery", device_class=SensorDeviceClass.BATTERY, native_unit_of_measurement=PERCENTAGE, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, name="Battery", ), "co2": SensorEntityDescription( @@ -92,7 +91,7 @@ SENSORS: dict[str, SensorEntityDescription] = { device_class=SensorDeviceClass.SIGNAL_STRENGTH, name="RSSI", entity_registry_enabled_default=False, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), "pm1": SensorEntityDescription( key="pm1", From 25224f6945899e4724d15d5152e7bfb73aec8555 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 8 Dec 2021 21:17:20 +0100 Subject: [PATCH 0171/2644] Use new SensorStateClass enum in airnow (#61259) Co-authored-by: epenet --- homeassistant/components/airnow/sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/airnow/sensor.py b/homeassistant/components/airnow/sensor.py index c56530613a2..974530c9504 100644 --- a/homeassistant/components/airnow/sensor.py +++ b/homeassistant/components/airnow/sensor.py @@ -2,9 +2,9 @@ from __future__ import annotations from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.const import ( ATTR_ATTRIBUTION, @@ -35,21 +35,21 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( icon="mdi:blur", name=ATTR_API_AQI, native_unit_of_measurement="aqi", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_PM25, icon="mdi:blur", name=ATTR_API_PM25, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_O3, icon="mdi:blur", name=ATTR_API_O3, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), ) From 048bdd321e3ff9e5fa2e0fe99117738da3524c8b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 8 Dec 2021 21:17:41 +0100 Subject: [PATCH 0172/2644] Use new DeviceClass and StateClass enums in airly (#61258) Co-authored-by: epenet --- homeassistant/components/airly/sensor.py | 36 ++++++++++-------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index 8da42b86a7c..3dab90f5620 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -6,22 +6,16 @@ from dataclasses import dataclass from typing import Any, cast from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONF_NAME, - DEVICE_CLASS_AQI, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_PM1, - DEVICE_CLASS_PM10, - DEVICE_CLASS_PM25, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, PERCENTAGE, PRESSURE_HPA, TEMP_CELSIUS, @@ -72,52 +66,52 @@ class AirlySensorEntityDescription(SensorEntityDescription): SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = ( AirlySensorEntityDescription( key=ATTR_API_CAQI, - device_class=DEVICE_CLASS_AQI, + device_class=SensorDeviceClass.AQI, name=ATTR_API_CAQI, native_unit_of_measurement="CAQI", ), AirlySensorEntityDescription( key=ATTR_API_PM1, - device_class=DEVICE_CLASS_PM1, + device_class=SensorDeviceClass.PM1, name=ATTR_API_PM1, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), AirlySensorEntityDescription( key=ATTR_API_PM25, - device_class=DEVICE_CLASS_PM25, + device_class=SensorDeviceClass.PM25, name="PM2.5", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), AirlySensorEntityDescription( key=ATTR_API_PM10, - device_class=DEVICE_CLASS_PM10, + device_class=SensorDeviceClass.PM10, name=ATTR_API_PM10, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), AirlySensorEntityDescription( key=ATTR_API_HUMIDITY, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, name=ATTR_API_HUMIDITY.capitalize(), native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, value=lambda value: round(value, 1), ), AirlySensorEntityDescription( key=ATTR_API_PRESSURE, - device_class=DEVICE_CLASS_PRESSURE, + device_class=SensorDeviceClass.PRESSURE, name=ATTR_API_PRESSURE.capitalize(), native_unit_of_measurement=PRESSURE_HPA, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), AirlySensorEntityDescription( key=ATTR_API_TEMPERATURE, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, name=ATTR_API_TEMPERATURE.capitalize(), native_unit_of_measurement=TEMP_CELSIUS, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, value=lambda value: round(value, 1), ), ) From fd58c1eff5dd3c14aadcc33c22613ea247ca56f9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 8 Dec 2021 21:18:14 +0100 Subject: [PATCH 0173/2644] Use new SensorDeviceClass enums in aemet (#61256) Co-authored-by: epenet --- homeassistant/components/aemet/const.py | 28 ++++++++++++------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/aemet/const.py b/homeassistant/components/aemet/const.py index 40542d88506..4be90011f5a 100644 --- a/homeassistant/components/aemet/const.py +++ b/homeassistant/components/aemet/const.py @@ -1,7 +1,11 @@ """Constant values for the AEMET OpenData component.""" from __future__ import annotations -from homeassistant.components.sensor import SensorEntityDescription, SensorStateClass +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntityDescription, + SensorStateClass, +) from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLOUDY, @@ -24,10 +28,6 @@ from homeassistant.components.weather import ( ) from homeassistant.const import ( DEGREE, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_TIMESTAMP, PERCENTAGE, PRECIPITATION_MILLIMETERS_PER_HOUR, PRESSURE_HPA, @@ -219,18 +219,18 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key=ATTR_FORECAST_TEMP, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( key=ATTR_FORECAST_TEMP_LOW, name="Temperature Low", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( key=ATTR_FORECAST_TIME, name="Time", - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, ), SensorEntityDescription( key=ATTR_FORECAST_WIND_BEARING, @@ -252,14 +252,14 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key=ATTR_API_HUMIDITY, name="Humidity", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_PRESSURE, name="Pressure", native_unit_of_measurement=PRESSURE_HPA, - device_class=DEVICE_CLASS_PRESSURE, + device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( @@ -295,7 +295,7 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key=ATTR_API_STATION_TIMESTAMP, name="Station timestamp", - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, ), SensorEntityDescription( key=ATTR_API_STORM_PROB, @@ -307,14 +307,14 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key=ATTR_API_TEMPERATURE, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_TEMPERATURE_FEELING, name="Temperature feeling", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( @@ -328,7 +328,7 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key=ATTR_API_TOWN_TIMESTAMP, name="Town timestamp", - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, ), SensorEntityDescription( key=ATTR_API_WIND_BEARING, From 7215244c176660a5920623d09bd1cb3a6c2fa775 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 8 Dec 2021 21:19:10 +0100 Subject: [PATCH 0174/2644] Use new DeviceClass and EntityCategory enums in advantage_air (#61255) Co-authored-by: epenet --- .../components/advantage_air/binary_sensor.py | 13 ++++++------- homeassistant/components/advantage_air/cover.py | 4 ++-- homeassistant/components/advantage_air/sensor.py | 15 ++++++++------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/advantage_air/binary_sensor.py b/homeassistant/components/advantage_air/binary_sensor.py index eec0ce7dfa5..3a0990c55ef 100644 --- a/homeassistant/components/advantage_air/binary_sensor.py +++ b/homeassistant/components/advantage_air/binary_sensor.py @@ -1,11 +1,10 @@ """Binary Sensor platform for Advantage Air integration.""" from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_MOTION, - DEVICE_CLASS_PROBLEM, + BinarySensorDeviceClass, BinarySensorEntity, ) -from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC +from homeassistant.helpers.entity import EntityCategory from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN from .entity import AdvantageAirEntity @@ -34,8 +33,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class AdvantageAirZoneFilter(AdvantageAirEntity, BinarySensorEntity): """Advantage Air Filter.""" - _attr_device_class = DEVICE_CLASS_PROBLEM - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_device_class = BinarySensorDeviceClass.PROBLEM + _attr_entity_category = EntityCategory.DIAGNOSTIC def __init__(self, instance, ac_key): """Initialize an Advantage Air Filter.""" @@ -54,7 +53,7 @@ class AdvantageAirZoneFilter(AdvantageAirEntity, BinarySensorEntity): class AdvantageAirZoneMotion(AdvantageAirEntity, BinarySensorEntity): """Advantage Air Zone Motion.""" - _attr_device_class = DEVICE_CLASS_MOTION + _attr_device_class = BinarySensorDeviceClass.MOTION def __init__(self, instance, ac_key, zone_key): """Initialize an Advantage Air Zone Motion.""" @@ -74,7 +73,7 @@ class AdvantageAirZoneMyZone(AdvantageAirEntity, BinarySensorEntity): """Advantage Air Zone MyZone.""" _attr_entity_registry_enabled_default = False - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_entity_category = EntityCategory.DIAGNOSTIC def __init__(self, instance, ac_key, zone_key): """Initialize an Advantage Air Zone MyZone.""" diff --git a/homeassistant/components/advantage_air/cover.py b/homeassistant/components/advantage_air/cover.py index 04960dab002..d308a024f14 100644 --- a/homeassistant/components/advantage_air/cover.py +++ b/homeassistant/components/advantage_air/cover.py @@ -2,10 +2,10 @@ from homeassistant.components.cover import ( ATTR_POSITION, - DEVICE_CLASS_DAMPER, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION, + CoverDeviceClass, CoverEntity, ) @@ -36,7 +36,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class AdvantageAirZoneVent(AdvantageAirEntity, CoverEntity): """Advantage Air Cover Class.""" - _attr_device_class = DEVICE_CLASS_DAMPER + _attr_device_class = CoverDeviceClass.DAMPER _attr_supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION def __init__(self, instance, ac_key, zone_key): diff --git a/homeassistant/components/advantage_air/sensor.py b/homeassistant/components/advantage_air/sensor.py index 8b83de2b923..bb3082a84bb 100644 --- a/homeassistant/components/advantage_air/sensor.py +++ b/homeassistant/components/advantage_air/sensor.py @@ -2,12 +2,13 @@ import voluptuous as vol from homeassistant.components.sensor import ( - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass, SensorEntity, SensorStateClass, ) -from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, TEMP_CELSIUS +from homeassistant.const import PERCENTAGE, TEMP_CELSIUS from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers.entity import EntityCategory from .const import ADVANTAGE_AIR_STATE_OPEN, DOMAIN as ADVANTAGE_AIR_DOMAIN from .entity import AdvantageAirEntity @@ -50,7 +51,7 @@ class AdvantageAirTimeTo(AdvantageAirEntity, SensorEntity): """Representation of Advantage Air timer control.""" _attr_native_unit_of_measurement = ADVANTAGE_AIR_SET_COUNTDOWN_UNIT - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_entity_category = EntityCategory.DIAGNOSTIC def __init__(self, instance, ac_key, action): """Initialize the Advantage Air timer control.""" @@ -85,7 +86,7 @@ class AdvantageAirZoneVent(AdvantageAirEntity, SensorEntity): _attr_native_unit_of_measurement = PERCENTAGE _attr_state_class = SensorStateClass.MEASUREMENT - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_entity_category = EntityCategory.DIAGNOSTIC def __init__(self, instance, ac_key, zone_key): """Initialize an Advantage Air Zone Vent Sensor.""" @@ -115,7 +116,7 @@ class AdvantageAirZoneSignal(AdvantageAirEntity, SensorEntity): _attr_native_unit_of_measurement = PERCENTAGE _attr_state_class = SensorStateClass.MEASUREMENT - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_entity_category = EntityCategory.DIAGNOSTIC def __init__(self, instance, ac_key, zone_key): """Initialize an Advantage Air Zone wireless signal sensor.""" @@ -148,10 +149,10 @@ class AdvantageAirZoneTemp(AdvantageAirEntity, SensorEntity): """Representation of Advantage Air Zone temperature sensor.""" _attr_native_unit_of_measurement = TEMP_CELSIUS - _attr_device_class = DEVICE_CLASS_TEMPERATURE + _attr_device_class = SensorDeviceClass.TEMPERATURE _attr_state_class = SensorStateClass.MEASUREMENT _attr_entity_registry_enabled_default = False - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_entity_category = EntityCategory.DIAGNOSTIC def __init__(self, instance, ac_key, zone_key): """Initialize an Advantage Air Zone Temp Sensor.""" From 3223332c1e53f437451b1d1bcb1de63524baa640 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 8 Dec 2021 12:19:22 -0800 Subject: [PATCH 0175/2644] Use correct template parameter in Rest template rendering (#61269) --- homeassistant/components/rest/data.py | 4 ++-- homeassistant/components/rest/switch.py | 8 ++++---- homeassistant/components/rest/utils.py | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/rest/data.py b/homeassistant/components/rest/data.py index 513f2393127..e9fbb5718a5 100644 --- a/homeassistant/components/rest/data.py +++ b/homeassistant/components/rest/data.py @@ -52,8 +52,8 @@ class RestData: self._hass, verify_ssl=self._verify_ssl ) - rendered_headers = render_templates(self._headers) - rendered_params = render_templates(self._params) + rendered_headers = render_templates(self._headers, False) + rendered_params = render_templates(self._params, True) _LOGGER.debug("Updating from %s", self._resource) try: diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index 3448b79979c..1fd04b66559 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -207,8 +207,8 @@ class RestSwitch(SwitchEntity): """Send a state update to the device.""" websession = async_get_clientsession(self.hass, self._verify_ssl) - rendered_headers = render_templates(self._headers) - rendered_params = render_templates(self._params) + rendered_headers = render_templates(self._headers, False) + rendered_params = render_templates(self._params, True) async with async_timeout.timeout(self._timeout): req = await getattr(websession, self._method)( @@ -233,8 +233,8 @@ class RestSwitch(SwitchEntity): """Get the latest data from REST API and update the state.""" websession = async_get_clientsession(hass, self._verify_ssl) - rendered_headers = render_templates(self._headers) - rendered_params = render_templates(self._params) + rendered_headers = render_templates(self._headers, False) + rendered_params = render_templates(self._params, True) async with async_timeout.timeout(self._timeout): req = await websession.get( diff --git a/homeassistant/components/rest/utils.py b/homeassistant/components/rest/utils.py index 35b3c22db31..f3fdba651ac 100644 --- a/homeassistant/components/rest/utils.py +++ b/homeassistant/components/rest/utils.py @@ -15,13 +15,13 @@ def inject_hass_in_templates_list( tpl.hass = hass -def render_templates(tpl_dict: dict[str, Template] | None): +def render_templates(tpl_dict: dict[str, Template] | None, parse_result: bool): """Render a dict of templates.""" if tpl_dict is None: return None rendered_items = {} - for item_name, template_header in tpl_dict.items(): - if (value := template_header.async_render()) is not None: - rendered_items[item_name] = str(value) + for item_name, template in tpl_dict.items(): + if (value := template.async_render(parse_result=parse_result)) is not None: + rendered_items[item_name] = value return rendered_items From ce59ed2a5ec8b81b7f573149b6c9baac2250f29a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 8 Dec 2021 12:21:33 -0800 Subject: [PATCH 0176/2644] Guard cannot connect during Tuya init (#61267) --- homeassistant/components/tuya/__init__.py | 44 ++++++++++------------- 1 file changed, 18 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index 4f34d3c31bf..fb5b4d759f3 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -4,6 +4,7 @@ from __future__ import annotations import logging from typing import NamedTuple +import requests from tuya_iot import ( AuthType, TuyaDevice, @@ -18,6 +19,7 @@ from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import dispatcher_send @@ -60,18 +62,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: data.pop(CONF_PROJECT_TYPE) hass.config_entries.async_update_entry(entry, data=data) - success = await _init_tuya_sdk(hass, entry) - - if not success: - hass.data[DOMAIN].pop(entry.entry_id) - - if not hass.data[DOMAIN]: - hass.data.pop(DOMAIN) - - return bool(success) - - -async def _init_tuya_sdk(hass: HomeAssistant, entry: ConfigEntry) -> bool: auth_type = AuthType(entry.data[CONF_AUTH_TYPE]) api = TuyaOpenAPI( endpoint=entry.data[CONF_ENDPOINT], @@ -82,22 +72,24 @@ async def _init_tuya_sdk(hass: HomeAssistant, entry: ConfigEntry) -> bool: api.set_dev_channel("hass") - if auth_type == AuthType.CUSTOM: - response = await hass.async_add_executor_job( - api.connect, entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD] - ) - else: - response = await hass.async_add_executor_job( - api.connect, - entry.data[CONF_USERNAME], - entry.data[CONF_PASSWORD], - entry.data[CONF_COUNTRY_CODE], - entry.data[CONF_APP_TYPE], - ) + try: + if auth_type == AuthType.CUSTOM: + response = await hass.async_add_executor_job( + api.connect, entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD] + ) + else: + response = await hass.async_add_executor_job( + api.connect, + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], + entry.data[CONF_COUNTRY_CODE], + entry.data[CONF_APP_TYPE], + ) + except requests.exceptions.RequestException as err: + raise ConfigEntryNotReady(err) from err if response.get("success", False) is False: - _LOGGER.error("Tuya login error response: %s", response) - return False + raise ConfigEntryNotReady(response) tuya_mq = TuyaOpenMQ(api) tuya_mq.start() From 9c11bb8ba15ae9675079ccc00710546fa1e1c17c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 8 Dec 2021 21:21:49 +0100 Subject: [PATCH 0177/2644] Use new SensorDeviceClass in aqualogic (#61272) Co-authored-by: epenet --- homeassistant/components/aqualogic/sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/aqualogic/sensor.py b/homeassistant/components/aqualogic/sensor.py index c1823ca2bb5..7d66e558e6f 100644 --- a/homeassistant/components/aqualogic/sensor.py +++ b/homeassistant/components/aqualogic/sensor.py @@ -7,12 +7,12 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, + SensorDeviceClass, SensorEntity, SensorEntityDescription, ) from homeassistant.const import ( CONF_MONITORED_CONDITIONS, - DEVICE_CLASS_TEMPERATURE, PERCENTAGE, POWER_WATT, TEMP_CELSIUS, @@ -39,7 +39,7 @@ SENSOR_TYPES: tuple[AquaLogicSensorEntityDescription, ...] = ( name="Air Temperature", unit_metric=TEMP_CELSIUS, unit_imperial=TEMP_FAHRENHEIT, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), AquaLogicSensorEntityDescription( key="pool_temp", @@ -47,7 +47,7 @@ SENSOR_TYPES: tuple[AquaLogicSensorEntityDescription, ...] = ( unit_metric=TEMP_CELSIUS, unit_imperial=TEMP_FAHRENHEIT, icon="mdi:oil-temperature", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), AquaLogicSensorEntityDescription( key="spa_temp", @@ -55,7 +55,7 @@ SENSOR_TYPES: tuple[AquaLogicSensorEntityDescription, ...] = ( unit_metric=TEMP_CELSIUS, unit_imperial=TEMP_FAHRENHEIT, icon="mdi:oil-temperature", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), AquaLogicSensorEntityDescription( key="pool_chlorinator", From c05eca1c82a054a8d286a5fbc0200ac5d90e256f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 8 Dec 2021 21:28:26 +0100 Subject: [PATCH 0178/2644] Remove log flooding prevention logic from discovery info (#61243) Co-authored-by: epenet --- homeassistant/components/dhcp/__init__.py | 27 ++++----- homeassistant/components/mqtt/__init__.py | 15 ++--- homeassistant/components/ssdp/__init__.py | 41 +++++-------- homeassistant/components/usb/__init__.py | 15 ++--- homeassistant/components/zeroconf/__init__.py | 27 ++++----- tests/components/dhcp/test_init.py | 28 ++++----- tests/components/mqtt/test_init.py | 12 ++-- tests/components/ssdp/test_init.py | 58 +++++++++++++++++++ tests/components/usb/test_init.py | 11 ++-- tests/components/zeroconf/test_init.py | 28 ++++----- tests/conftest.py | 29 +++++++++- tests/helpers/conftest.py | 31 ---------- 12 files changed, 163 insertions(+), 159 deletions(-) delete mode 100644 tests/helpers/conftest.py diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index 7da74fb66b0..aa3fb2118cb 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -68,22 +68,17 @@ class DhcpServiceInfo(BaseServiceInfo): hostname: str macaddress: str - # Used to prevent log flooding. To be removed in 2022.6 - _warning_logged: bool = False - def __getitem__(self, name: str) -> Any: """ Enable method for compatibility reason. Deprecated, and will be removed in version 2022.6. """ - if not self._warning_logged: - report( - f"accessed discovery_info['{name}'] instead of discovery_info.{name}; this will fail in version 2022.6", - exclude_integrations={DOMAIN}, - error_if_core=False, - ) - self._warning_logged = True + report( + f"accessed discovery_info['{name}'] instead of discovery_info.{name}; this will fail in version 2022.6", + exclude_integrations={DOMAIN}, + error_if_core=False, + ) return getattr(self, name) def get(self, name: str, default: Any = None) -> Any: @@ -92,13 +87,11 @@ class DhcpServiceInfo(BaseServiceInfo): Deprecated, and will be removed in version 2022.6. """ - if not self._warning_logged: - report( - f"accessed discovery_info.get('{name}') instead of discovery_info.{name}; this will fail in version 2022.6", - exclude_integrations={DOMAIN}, - error_if_core=False, - ) - self._warning_logged = True + report( + f"accessed discovery_info.get('{name}') instead of discovery_info.{name}; this will fail in version 2022.6", + exclude_integrations={DOMAIN}, + error_if_core=False, + ) if hasattr(self, name): return getattr(self, name) return default diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index cdf37cd6381..ae7b60c4454 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -261,22 +261,17 @@ class MqttServiceInfo(BaseServiceInfo): subscribed_topic: str timestamp: dt.datetime - # Used to prevent log flooding. To be removed in 2022.6 - _warning_logged: bool = False - def __getitem__(self, name: str) -> Any: """ Allow property access by name for compatibility reason. Deprecated, and will be removed in version 2022.6. """ - if not self._warning_logged: - report( - f"accessed discovery_info['{name}'] instead of discovery_info.{name}; this will fail in version 2022.6", - exclude_integrations={"mqtt"}, - error_if_core=False, - ) - self._warning_logged = True + report( + f"accessed discovery_info['{name}'] instead of discovery_info.{name}; this will fail in version 2022.6", + exclude_integrations={DOMAIN}, + error_if_core=False, + ) return getattr(self, name) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index 57948000701..f7ccff153ac 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -103,22 +103,17 @@ class SsdpServiceInfo( ): """Prepared info from ssdp/upnp entries.""" - # Used to prevent log flooding. To be removed in 2022.6 - _warning_logged: bool = False - def __getitem__(self, name: str) -> Any: """ Allow property access by name for compatibility reason. Deprecated, and will be removed in version 2022.6. """ - if not self._warning_logged: - report( - f"accessed discovery_info['{name}'] instead of discovery_info.{name}; this will fail in version 2022.6", - exclude_integrations={DOMAIN}, - error_if_core=False, - ) - self._warning_logged = True + report( + f"accessed discovery_info['{name}'] instead of discovery_info.{name}; this will fail in version 2022.6", + exclude_integrations={DOMAIN}, + error_if_core=False, + ) # Use a property if it is available, fallback to upnp data if hasattr(self, name): return getattr(self, name) @@ -132,13 +127,11 @@ class SsdpServiceInfo( Deprecated, and will be removed in version 2022.6. """ - if not self._warning_logged: - report( - f"accessed discovery_info.get('{name}') instead of discovery_info.{name}; this will fail in version 2022.6", - exclude_integrations={DOMAIN}, - error_if_core=False, - ) - self._warning_logged = True + report( + f"accessed discovery_info.get('{name}') instead of discovery_info.{name}; this will fail in version 2022.6", + exclude_integrations={DOMAIN}, + error_if_core=False, + ) if hasattr(self, name): return getattr(self, name) return self.upnp.get(name, self.ssdp_headers.get(name, default)) @@ -149,14 +142,12 @@ class SsdpServiceInfo( Deprecated, and will be removed in version 2022.6. """ - if not self._warning_logged: - report( - "accessed discovery_info.__contains__() instead of discovery_info.upnp.__contains__() " - "or discovery_info.ssdp_headers.__contains__(); this will fail in version 2022.6", - exclude_integrations={DOMAIN}, - error_if_core=False, - ) - self._warning_logged = True + report( + f"accessed discovery_info.__contains__('{name}') instead of discovery_info.upnp.__contains__('{name}') " + f"or discovery_info.ssdp_headers.__contains__('{name}'); this will fail in version 2022.6", + exclude_integrations={DOMAIN}, + error_if_core=False, + ) if hasattr(self, name): return getattr(self, name) is not None return name in self.upnp or name in self.ssdp_headers diff --git a/homeassistant/components/usb/__init__.py b/homeassistant/components/usb/__init__.py index 87216c65e9e..ec3ffa0405a 100644 --- a/homeassistant/components/usb/__init__.py +++ b/homeassistant/components/usb/__init__.py @@ -44,22 +44,17 @@ class UsbServiceInfo(BaseServiceInfo): manufacturer: str | None description: str | None - # Used to prevent log flooding. To be removed in 2022.6 - _warning_logged: bool = False - def __getitem__(self, name: str) -> Any: """ Allow property access by name for compatibility reason. Deprecated, and will be removed in version 2022.6. """ - if not self._warning_logged: - report( - f"accessed discovery_info['{name}'] instead of discovery_info.{name}; this will fail in version 2022.6", - exclude_integrations={"usb"}, - error_if_core=False, - ) - self._warning_logged = True + report( + f"accessed discovery_info['{name}'] instead of discovery_info.{name}; this will fail in version 2022.6", + exclude_integrations={DOMAIN}, + error_if_core=False, + ) return getattr(self, name) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 50db346451f..221a4d6b834 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -107,22 +107,17 @@ class ZeroconfServiceInfo(BaseServiceInfo): name: str properties: dict[str, Any] - # Used to prevent log flooding. To be removed in 2022.6 - _warning_logged: bool = False - def __getitem__(self, name: str) -> Any: """ Enable method for compatibility reason. Deprecated, and will be removed in version 2022.6. """ - if not self._warning_logged: - report( - f"accessed discovery_info['{name}'] instead of discovery_info.{name}; this will fail in version 2022.6", - exclude_integrations={DOMAIN}, - error_if_core=False, - ) - self._warning_logged = True + report( + f"accessed discovery_info['{name}'] instead of discovery_info.{name}; this will fail in version 2022.6", + exclude_integrations={DOMAIN}, + error_if_core=False, + ) return getattr(self, name) def get(self, name: str, default: Any = None) -> Any: @@ -131,13 +126,11 @@ class ZeroconfServiceInfo(BaseServiceInfo): Deprecated, and will be removed in version 2022.6. """ - if not self._warning_logged: - report( - f"accessed discovery_info.get('{name}') instead of discovery_info.{name}; this will fail in version 2022.6", - exclude_integrations={DOMAIN}, - error_if_core=False, - ) - self._warning_logged = True + report( + f"accessed discovery_info.get('{name}') instead of discovery_info.{name}; this will fail in version 2022.6", + exclude_integrations={DOMAIN}, + error_if_core=False, + ) if hasattr(self, name): return getattr(self, name) return default diff --git a/tests/components/dhcp/test_init.py b/tests/components/dhcp/test_init.py index 41059722113..fb3387aeab6 100644 --- a/tests/components/dhcp/test_init.py +++ b/tests/components/dhcp/test_init.py @@ -3,7 +3,8 @@ import datetime import threading from unittest.mock import MagicMock, patch -from scapy import arch # pylint: unused-import # noqa: F401 +import pytest +from scapy import arch # pylint: disable=unused-import # noqa: F401 from scapy.error import Scapy_Exception from scapy.layers.dhcp import DHCP from scapy.layers.l2 import Ether @@ -845,6 +846,7 @@ async def test_aiodiscover_finds_new_hosts_after_interval(hass): ) +@pytest.mark.usefixtures("mock_integration_frame") async def test_service_info_compatibility(hass, caplog): """Test compatibility with old-style dict. @@ -856,21 +858,13 @@ async def test_service_info_compatibility(hass, caplog): macaddress="b8b7f16db533", ) - # Ensure first call get logged - assert discovery_info["ip"] == "192.168.210.56" - assert discovery_info.get("ip") == "192.168.210.56" + with patch("homeassistant.helpers.frame._REPORTED_INTEGRATIONS", set()): + assert discovery_info["ip"] == "192.168.210.56" + assert "Detected integration that accessed discovery_info['ip']" in caplog.text + + with patch("homeassistant.helpers.frame._REPORTED_INTEGRATIONS", set()): + assert discovery_info.get("ip") == "192.168.210.56" + assert "Detected integration that accessed discovery_info.get('ip')" in caplog.text + assert discovery_info.get("ip", "fallback_host") == "192.168.210.56" assert discovery_info.get("invalid_key", "fallback_host") == "fallback_host" - assert "Detected code that accessed discovery_info['ip']" in caplog.text - assert "Detected code that accessed discovery_info.get('ip')" not in caplog.text - - # Ensure second call doesn't get logged - caplog.clear() - assert discovery_info["ip"] == "192.168.210.56" - assert discovery_info.get("ip") == "192.168.210.56" - assert "Detected code that accessed discovery_info['ip']" not in caplog.text - assert "Detected code that accessed discovery_info.get('ip')" not in caplog.text - - discovery_info._warning_logged = False # pylint: disable=[protected-access] - assert discovery_info.get("ip") == "192.168.210.56" - assert "Detected code that accessed discovery_info.get('ip')" in caplog.text diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index acc25b59442..a4a3c1d6909 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1814,6 +1814,7 @@ async def test_publish_json_from_template(hass, mqtt_mock): assert mqtt_mock.async_publish.call_args[0][1] == test_str +@pytest.mark.usefixtures("mock_integration_frame") async def test_service_info_compatibility(hass, caplog): """Test compatibility with old-style dict. @@ -1828,11 +1829,6 @@ async def test_service_info_compatibility(hass, caplog): timestamp=None, ) - # Ensure first call get logged - assert discovery_info["topic"] == "tasmota/discovery/DC4F220848A2/config" - assert "Detected code that accessed discovery_info['topic']" in caplog.text - - # Ensure second call doesn't get logged - caplog.clear() - assert discovery_info["topic"] == "tasmota/discovery/DC4F220848A2/config" - assert "Detected code that accessed discovery_info['topic']" not in caplog.text + with patch("homeassistant.helpers.frame._REPORTED_INTEGRATIONS", set()): + assert discovery_info["topic"] == "tasmota/discovery/DC4F220848A2/config" + assert "Detected integration that accessed discovery_info['topic']" in caplog.text diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index 9e4d7424eb9..758f8fb79ba 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -793,3 +793,61 @@ async def test_ipv4_does_additional_search_for_sonos( ), ) assert ssdp_listener.async_search.call_args[1] == {} + + +@pytest.mark.usefixtures("mock_integration_frame") +async def test_service_info_compatibility(hass, caplog): + """Test compatibility with old-style dict. + + To be removed in 2022.6 + """ + discovery_info = ssdp.SsdpServiceInfo( + ssdp_st="mock-st", + ssdp_location="http://1.1.1.1", + ssdp_usn="uuid:mock-udn::mock-st", + ssdp_server="mock-server", + ssdp_ext="", + ssdp_headers=_ssdp_headers( + { + "st": "mock-st", + "location": "http://1.1.1.1", + "usn": "uuid:mock-udn::mock-st", + "server": "mock-server", + "ext": "", + } + ), + upnp={ssdp.ATTR_UPNP_DEVICE_TYPE: "ABC"}, + ) + + with patch("homeassistant.helpers.frame._REPORTED_INTEGRATIONS", set()): + assert discovery_info["ssdp_st"] == "mock-st" + assert "Detected integration that accessed discovery_info['ssdp_st']" in caplog.text + + with patch("homeassistant.helpers.frame._REPORTED_INTEGRATIONS", set()): + assert discovery_info.get("ssdp_location") == "http://1.1.1.1" + assert ( + "Detected integration that accessed discovery_info.get('ssdp_location')" + in caplog.text + ) + + with patch("homeassistant.helpers.frame._REPORTED_INTEGRATIONS", set()): + assert "ssdp_usn" in discovery_info + assert ( + "Detected integration that accessed discovery_info.__contains__('ssdp_usn')" + in caplog.text + ) + + # Root item + assert discovery_info["ssdp_usn"] == "uuid:mock-udn::mock-st" + assert discovery_info.get("ssdp_usn") == "uuid:mock-udn::mock-st" + assert "ssdp_usn" in discovery_info + + # SSDP header + assert discovery_info["st"] == "mock-st" + assert discovery_info.get("st") == "mock-st" + assert "st" in discovery_info + + # UPnP item + assert discovery_info[ssdp.ATTR_UPNP_DEVICE_TYPE] == "ABC" + assert discovery_info.get(ssdp.ATTR_UPNP_DEVICE_TYPE) == "ABC" + assert ssdp.ATTR_UPNP_DEVICE_TYPE in discovery_info diff --git a/tests/components/usb/test_init.py b/tests/components/usb/test_init.py index cc34add6726..ce86965f093 100644 --- a/tests/components/usb/test_init.py +++ b/tests/components/usb/test_init.py @@ -779,6 +779,7 @@ def test_human_readable_device_name(): assert "8A2A" in name +@pytest.mark.usefixtures("mock_integration_frame") async def test_service_info_compatibility(hass, caplog): """Test compatibility with old-style dict. @@ -794,10 +795,6 @@ async def test_service_info_compatibility(hass, caplog): ) # Ensure first call get logged - assert discovery_info["vid"] == 12345 - assert "Detected code that accessed discovery_info['vid']" in caplog.text - - # Ensure second call doesn't get logged - caplog.clear() - assert discovery_info["vid"] == 12345 - assert "Detected code that accessed discovery_info['vid']" not in caplog.text + with patch("homeassistant.helpers.frame._REPORTED_INTEGRATIONS", set()): + assert discovery_info["vid"] == 12345 + assert "Detected integration that accessed discovery_info['vid']" in caplog.text diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index e306edba357..16aca70d7fc 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -3,6 +3,7 @@ from ipaddress import ip_address from typing import Any from unittest.mock import call, patch +import pytest from zeroconf import InterfaceChoice, IPVersion, ServiceStateChange from zeroconf.asyncio import AsyncServiceInfo @@ -1032,6 +1033,7 @@ async def test_no_name(hass, mock_async_zeroconf): assert info.name == "Home._home-assistant._tcp.local." +@pytest.mark.usefixtures("mock_integration_frame") async def test_service_info_compatibility(hass, caplog): """Test compatibility with old-style dict. @@ -1046,21 +1048,15 @@ async def test_service_info_compatibility(hass, caplog): properties={}, ) - # Ensure first call get logged - assert discovery_info["host"] == "mock_host" - assert discovery_info.get("host") == "mock_host" + with patch("homeassistant.helpers.frame._REPORTED_INTEGRATIONS", set()): + assert discovery_info["host"] == "mock_host" + assert "Detected integration that accessed discovery_info['host']" in caplog.text + + with patch("homeassistant.helpers.frame._REPORTED_INTEGRATIONS", set()): + assert discovery_info.get("host") == "mock_host" + assert ( + "Detected integration that accessed discovery_info.get('host')" in caplog.text + ) + assert discovery_info.get("host", "fallback_host") == "mock_host" assert discovery_info.get("invalid_key", "fallback_host") == "fallback_host" - assert "Detected code that accessed discovery_info['host']" in caplog.text - assert "Detected code that accessed discovery_info.get('host')" not in caplog.text - - # Ensure second call doesn't get logged - caplog.clear() - assert discovery_info["host"] == "mock_host" - assert discovery_info.get("host") == "mock_host" - assert "Detected code that accessed discovery_info['host']" not in caplog.text - assert "Detected code that accessed discovery_info.get('host')" not in caplog.text - - discovery_info._warning_logged = False - assert discovery_info.get("host") == "mock_host" - assert "Detected code that accessed discovery_info.get('host')" in caplog.text diff --git a/tests/conftest.py b/tests/conftest.py index 10a9dd1627b..88651a0ec3f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,7 +6,7 @@ import logging import socket import ssl import threading -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, Mock, patch from aiohttp.test_utils import make_mocked_request import freezegun @@ -778,3 +778,30 @@ def hass_recorder(enable_statistics, hass_storage): yield setup_recorder hass.stop() + + +@pytest.fixture +def mock_integration_frame(): + """Mock as if we're calling code from inside an integration.""" + correct_frame = Mock( + filename="/home/paulus/homeassistant/components/hue/light.py", + lineno="23", + line="self.light.is_on", + ) + with patch( + "homeassistant.helpers.frame.extract_stack", + return_value=[ + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="23", + line="do_something()", + ), + correct_frame, + Mock( + filename="/home/paulus/aiohue/lights.py", + lineno="2", + line="something()", + ), + ], + ): + yield correct_frame diff --git a/tests/helpers/conftest.py b/tests/helpers/conftest.py deleted file mode 100644 index 4b3b9bf465d..00000000000 --- a/tests/helpers/conftest.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Fixtures for helpers.""" -from unittest.mock import Mock, patch - -import pytest - - -@pytest.fixture -def mock_integration_frame(): - """Mock as if we're calling code from inside an integration.""" - correct_frame = Mock( - filename="/home/paulus/homeassistant/components/hue/light.py", - lineno="23", - line="self.light.is_on", - ) - with patch( - "homeassistant.helpers.frame.extract_stack", - return_value=[ - Mock( - filename="/home/paulus/homeassistant/core.py", - lineno="23", - line="do_something()", - ), - correct_frame, - Mock( - filename="/home/paulus/aiohue/lights.py", - lineno="2", - line="something()", - ), - ], - ): - yield correct_frame From 1990b5b6087353a3f5409381bd02fbd9440dde7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 8 Dec 2021 21:46:30 +0100 Subject: [PATCH 0179/2644] Use new EntityCategory enum in Surepetcare (#61282) --- homeassistant/components/surepetcare/binary_sensor.py | 6 +++--- homeassistant/components/surepetcare/sensor.py | 10 +++------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/surepetcare/binary_sensor.py b/homeassistant/components/surepetcare/binary_sensor.py index d53252fae9b..f4565ea3757 100644 --- a/homeassistant/components/surepetcare/binary_sensor.py +++ b/homeassistant/components/surepetcare/binary_sensor.py @@ -12,8 +12,8 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import SurePetcareDataCoordinator @@ -67,7 +67,7 @@ class Hub(SurePetcareBinarySensor): """Sure Petcare Hub.""" _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_entity_category = EntityCategory.DIAGNOSTIC @property def available(self) -> bool: @@ -117,7 +117,7 @@ class DeviceConnectivity(SurePetcareBinarySensor): """Sure Petcare Device.""" _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_entity_category = EntityCategory.DIAGNOSTIC def __init__( self, diff --git a/homeassistant/components/surepetcare/sensor.py b/homeassistant/components/surepetcare/sensor.py index e53f319bdc5..e9967054900 100644 --- a/homeassistant/components/surepetcare/sensor.py +++ b/homeassistant/components/surepetcare/sensor.py @@ -9,13 +9,9 @@ from surepy.enums import EntityType from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_VOLTAGE, - ENTITY_CATEGORY_DIAGNOSTIC, - PERCENTAGE, - VOLUME_MILLILITERS, -) +from homeassistant.const import ATTR_VOLTAGE, PERCENTAGE, VOLUME_MILLILITERS from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import SurePetcareDataCoordinator @@ -52,7 +48,7 @@ class SureBattery(SurePetcareEntity, SensorEntity): """A sensor implementation for Sure Petcare batteries.""" _attr_device_class = SensorDeviceClass.BATTERY - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_native_unit_of_measurement = PERCENTAGE def __init__( From e1306881416fe061c61350b86c2b632d02518001 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 8 Dec 2021 10:54:41 -1000 Subject: [PATCH 0180/2644] Bump flux_led to 0.26.3 (#61287) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 538ababcb98..ca6dc5a5a1d 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Magic Home", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.26.2"], + "requirements": ["flux_led==0.26.3"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 2b6f4c3e117..eb70fef8bfa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -667,7 +667,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.26.2 +flux_led==0.26.3 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 20e2e297c2a..183e515e702 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -408,7 +408,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.26.2 +flux_led==0.26.3 # homeassistant.components.homekit fnvhash==0.1.0 From c0529ac1ce567e76f96d3267bb4caee7b5b18f8d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 8 Dec 2021 21:54:50 +0100 Subject: [PATCH 0181/2644] Use new SensorDeviceClass enum in awair (#61290) Co-authored-by: epenet --- homeassistant/components/awair/const.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/awair/const.py b/homeassistant/components/awair/const.py index 68ca3335d97..352237da386 100644 --- a/homeassistant/components/awair/const.py +++ b/homeassistant/components/awair/const.py @@ -8,15 +8,11 @@ import logging from python_awair.air_data import AirData from python_awair.devices import AwairDevice -from homeassistant.components.sensor import SensorEntityDescription +from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, - DEVICE_CLASS_CO2, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_TEMPERATURE, LIGHT_LUX, PERCENTAGE, SOUND_PRESSURE_WEIGHTED_DBA, @@ -69,14 +65,14 @@ SENSOR_TYPE_SCORE = AwairSensorEntityDescription( SENSOR_TYPES: tuple[AwairSensorEntityDescription, ...] = ( AwairSensorEntityDescription( key=API_HUMID, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=PERCENTAGE, name="Humidity", unique_id_tag="HUMID", # matches legacy format ), AwairSensorEntityDescription( key=API_LUX, - device_class=DEVICE_CLASS_ILLUMINANCE, + device_class=SensorDeviceClass.ILLUMINANCE, native_unit_of_measurement=LIGHT_LUX, name="Illuminance", unique_id_tag="illuminance", @@ -97,14 +93,14 @@ SENSOR_TYPES: tuple[AwairSensorEntityDescription, ...] = ( ), AwairSensorEntityDescription( key=API_TEMP, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, name="Temperature", unique_id_tag="TEMP", # matches legacy format ), AwairSensorEntityDescription( key=API_CO2, - device_class=DEVICE_CLASS_CO2, + device_class=SensorDeviceClass.CO2, icon="mdi:cloud", native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, name="Carbon dioxide", From 880a2b69b6b9ec96e531d1d0d2ac5b29040c14be Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 8 Dec 2021 21:55:44 +0100 Subject: [PATCH 0182/2644] Use new SensorDeviceClass in apcupsd (#61271) Co-authored-by: epenet --- homeassistant/components/apcupsd/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py index 6f3b83a4867..faa1f5a09e2 100644 --- a/homeassistant/components/apcupsd/sensor.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -8,12 +8,12 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, + SensorDeviceClass, SensorEntity, SensorEntityDescription, ) from homeassistant.const import ( CONF_RESOURCES, - DEVICE_CLASS_TEMPERATURE, ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, FREQUENCY_HERTZ, @@ -155,7 +155,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="itemp", name="Internal Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( key="lastxfer", From 84141ff3ddcfdeec0da10a94837a66edfbe9f899 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 8 Dec 2021 21:56:05 +0100 Subject: [PATCH 0183/2644] Use new SensorDeviceClass in arlo (#61274) Co-authored-by: epenet --- homeassistant/components/arlo/sensor.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/arlo/sensor.py b/homeassistant/components/arlo/sensor.py index 7fbc57f9c6b..868b2c81e87 100644 --- a/homeassistant/components/arlo/sensor.py +++ b/homeassistant/components/arlo/sensor.py @@ -8,15 +8,13 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, + SensorDeviceClass, SensorEntity, SensorEntityDescription, ) from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, CONF_MONITORED_CONDITIONS, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_TEMPERATURE, PERCENTAGE, TEMP_CELSIUS, ) @@ -49,7 +47,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="battery_level", name="Battery Level", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_BATTERY, + device_class=SensorDeviceClass.BATTERY, ), SensorEntityDescription( key="signal_strength", @@ -60,13 +58,13 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="temperature", name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( key="humidity", name="Humidity", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key="air_quality", From 25db4a4f1f599d7a10cdd767bff5d0b6e61fd25e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 8 Dec 2021 21:56:41 +0100 Subject: [PATCH 0184/2644] Use new SensorDeviceClass in arwn (#61275) Co-authored-by: epenet --- homeassistant/components/arwn/sensor.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/arwn/sensor.py b/homeassistant/components/arwn/sensor.py index 2571d35f98e..4f06ac169ad 100644 --- a/homeassistant/components/arwn/sensor.py +++ b/homeassistant/components/arwn/sensor.py @@ -3,10 +3,9 @@ import json import logging from homeassistant.components import mqtt -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.const import ( DEGREE, - DEVICE_CLASS_TEMPERATURE, PRECIPITATION_INCHES, TEMP_CELSIUS, TEMP_FAHRENHEIT, @@ -37,7 +36,7 @@ def discover_sensors(topic, payload): else: unit = TEMP_CELSIUS return ArwnSensor( - topic, name, "temp", unit, device_class=DEVICE_CLASS_TEMPERATURE + topic, name, "temp", unit, device_class=SensorDeviceClass.TEMPERATURE ) if domain == "moisture": name = f"{parts[2]} Moisture" From d2216363c5c69f59135f58065de6aee25758ea8c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 8 Dec 2021 21:57:25 +0100 Subject: [PATCH 0185/2644] Use new DeviceClass and StateClass enums in aseko_pool_live (#61276) Co-authored-by: epenet --- homeassistant/components/aseko_pool_live/sensor.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/aseko_pool_live/sensor.py b/homeassistant/components/aseko_pool_live/sensor.py index 41036b582a7..74051ef454f 100644 --- a/homeassistant/components/aseko_pool_live/sensor.py +++ b/homeassistant/components/aseko_pool_live/sensor.py @@ -3,9 +3,12 @@ from __future__ import annotations from aioaseko import Unit, Variable -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import DEVICE_CLASS_TEMPERATURE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -33,7 +36,7 @@ async def async_setup_entry( class VariableSensorEntity(AsekoEntity, SensorEntity): """Representation of a unit variable sensor entity.""" - attr_state_class = STATE_CLASS_MEASUREMENT + attr_state_class = SensorStateClass.MEASUREMENT def __init__( self, unit: Unit, variable: Variable, coordinator: AsekoDataUpdateCoordinator @@ -61,8 +64,8 @@ class VariableSensorEntity(AsekoEntity, SensorEntity): }.get(self._variable.type) self._attr_device_class = { - "airTemp": DEVICE_CLASS_TEMPERATURE, - "waterTemp": DEVICE_CLASS_TEMPERATURE, + "airTemp": SensorDeviceClass.TEMPERATURE, + "waterTemp": SensorDeviceClass.TEMPERATURE, }.get(self._variable.type) @property From aca2c3a27a2f987cfc50526c2d0f73ca132b1fa7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 8 Dec 2021 11:03:07 -1000 Subject: [PATCH 0186/2644] Restore rest integration ability to follow http redirects (#61293) --- homeassistant/components/rest/data.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/rest/data.py b/homeassistant/components/rest/data.py index e9fbb5718a5..bc98b0caf68 100644 --- a/homeassistant/components/rest/data.py +++ b/homeassistant/components/rest/data.py @@ -65,6 +65,7 @@ class RestData: auth=self._auth, data=self._request_data, timeout=self._timeout, + follow_redirects=True, ) self.data = response.text self.headers = response.headers From fd7328ce234ec02ea531c739537362190e951060 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 8 Dec 2021 22:03:24 +0100 Subject: [PATCH 0187/2644] Use new DeviceClass and StateClass enums in atome (#61284) Co-authored-by: epenet --- homeassistant/components/atome/sensor.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index 0402e80949e..e9a55d0e089 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -7,16 +7,14 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, + SensorStateClass, ) from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, CONF_USERNAME, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR, POWER_WATT, ) @@ -253,13 +251,13 @@ class AtomeSensor(SensorEntity): self._sensor_type = sensor_type if sensor_type == LIVE_TYPE: - self._attr_device_class = DEVICE_CLASS_POWER + self._attr_device_class = SensorDeviceClass.POWER self._attr_native_unit_of_measurement = POWER_WATT - self._attr_state_class = STATE_CLASS_MEASUREMENT + self._attr_state_class = SensorStateClass.MEASUREMENT else: - self._attr_device_class = DEVICE_CLASS_ENERGY + self._attr_device_class = SensorDeviceClass.ENERGY self._attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR - self._attr_state_class = STATE_CLASS_TOTAL_INCREASING + self._attr_state_class = SensorStateClass.TOTAL_INCREASING def update(self): """Update device state.""" From c893ad80e4a7b056c05d5417128e67a9209cf4d7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 8 Dec 2021 22:03:51 +0100 Subject: [PATCH 0188/2644] Use new SwitchDeviceClass in aten_pe (#61281) Co-authored-by: epenet --- homeassistant/components/aten_pe/switch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/aten_pe/switch.py b/homeassistant/components/aten_pe/switch.py index 044c890fc65..afe77669458 100644 --- a/homeassistant/components/aten_pe/switch.py +++ b/homeassistant/components/aten_pe/switch.py @@ -6,8 +6,8 @@ from atenpdu import AtenPE, AtenPEError import voluptuous as vol from homeassistant.components.switch import ( - DEVICE_CLASS_OUTLET, PLATFORM_SCHEMA, + SwitchDeviceClass, SwitchEntity, ) from homeassistant.const import CONF_HOST, CONF_PORT, CONF_USERNAME @@ -67,7 +67,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class AtenSwitch(SwitchEntity): """Represents an ATEN PE switch.""" - _attr_device_class = DEVICE_CLASS_OUTLET + _attr_device_class = SwitchDeviceClass.OUTLET def __init__(self, device, mac, outlet, name): """Initialize an ATEN PE switch.""" From eae1e669d02c70529900d0def258e74cedd50018 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 8 Dec 2021 22:04:11 +0100 Subject: [PATCH 0189/2644] Use new EntityCategory enum in Tibber (#61279) --- homeassistant/components/tibber/sensor.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index cd4d4994f97..0958996bfee 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -18,7 +18,6 @@ from homeassistant.const import ( ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, - ENTITY_CATEGORY_DIAGNOSTIC, EVENT_HOMEASSISTANT_STOP, PERCENTAGE, POWER_WATT, @@ -28,7 +27,7 @@ from homeassistant.core import callback from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import update_coordinator from homeassistant.helpers.device_registry import async_get as async_get_dev_reg -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_registry import async_get as async_get_entity_reg from homeassistant.util import Throttle, dt as dt_util @@ -165,7 +164,7 @@ RT_SENSORS: tuple[SensorEntityDescription, ...] = ( device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, state_class=SensorStateClass.MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="accumulatedReward", From 07d0b6f726597bda14a4fec1183d82e72819dfbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 8 Dec 2021 22:08:05 +0100 Subject: [PATCH 0190/2644] Use new EntityCategory in rfxtrx (#61295) --- homeassistant/components/rfxtrx/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index d4d37bcd07b..c13e499bbf0 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -19,7 +19,6 @@ from homeassistant.const import ( ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, - ENTITY_CATEGORY_DIAGNOSTIC, LENGTH_MILLIMETERS, PERCENTAGE, POWER_WATT, @@ -31,6 +30,7 @@ from homeassistant.const import ( UV_INDEX, ) from homeassistant.core import callback +from homeassistant.helpers.entity import EntityCategory from . import ( CONF_DATA_BITS, @@ -78,7 +78,7 @@ SENSOR_TYPES = ( state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, convert=_battery_convert, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), RfxtrxSensorEntityDescription( key="Current", @@ -122,7 +122,7 @@ SENSOR_TYPES = ( state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, convert=_rssi_convert, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), RfxtrxSensorEntityDescription( key="Temperature", From 503593b3011c6df0d42645278f221f43ec1f29c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 8 Dec 2021 22:08:44 +0100 Subject: [PATCH 0191/2644] Use new EntityCategory in Mill (#61294) --- homeassistant/components/mill/sensor.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mill/sensor.py b/homeassistant/components/mill/sensor.py index f82d3dbcc34..44d08e828f3 100644 --- a/homeassistant/components/mill/sensor.py +++ b/homeassistant/components/mill/sensor.py @@ -14,12 +14,11 @@ from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, CONF_USERNAME, ENERGY_KILO_WATT_HOUR, - ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, TEMP_CELSIUS, ) from homeassistant.core import callback -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -75,7 +74,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, name="Battery", state_class=SensorStateClass.MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key=ECO2, From dff77e39cedf4e17567ef786309c9a1a8418a610 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 8 Dec 2021 22:09:19 +0100 Subject: [PATCH 0192/2644] Use new BinarySensorDeviceClass enum in axis (#61291) Co-authored-by: epenet --- homeassistant/components/axis/binary_sensor.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/axis/binary_sensor.py b/homeassistant/components/axis/binary_sensor.py index 7ef3838b1f7..01cfb834f26 100644 --- a/homeassistant/components/axis/binary_sensor.py +++ b/homeassistant/components/axis/binary_sensor.py @@ -17,10 +17,7 @@ from axis.event_stream import ( ) from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_CONNECTIVITY, - DEVICE_CLASS_LIGHT, - DEVICE_CLASS_MOTION, - DEVICE_CLASS_SOUND, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.core import callback @@ -32,10 +29,10 @@ from .axis_base import AxisEventBase from .const import DOMAIN as AXIS_DOMAIN DEVICE_CLASS = { - CLASS_INPUT: DEVICE_CLASS_CONNECTIVITY, - CLASS_LIGHT: DEVICE_CLASS_LIGHT, - CLASS_MOTION: DEVICE_CLASS_MOTION, - CLASS_SOUND: DEVICE_CLASS_SOUND, + CLASS_INPUT: BinarySensorDeviceClass.CONNECTIVITY, + CLASS_LIGHT: BinarySensorDeviceClass.LIGHT, + CLASS_MOTION: BinarySensorDeviceClass.MOTION, + CLASS_SOUND: BinarySensorDeviceClass.SOUND, } From d817b4c7ea1ea4b71d66304b6e20cf13f663f5ca Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 8 Dec 2021 22:11:49 +0100 Subject: [PATCH 0193/2644] Use new BinarySensorDeviceClass enum in balboa (#61292) Co-authored-by: epenet --- homeassistant/components/balboa/binary_sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/balboa/binary_sensor.py b/homeassistant/components/balboa/binary_sensor.py index e00537439a1..b73872b6647 100644 --- a/homeassistant/components/balboa/binary_sensor.py +++ b/homeassistant/components/balboa/binary_sensor.py @@ -1,6 +1,6 @@ """Support for Balboa Spa binary sensors.""" from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_MOVING, + BinarySensorDeviceClass, BinarySensorEntity, ) @@ -28,7 +28,7 @@ async def async_setup_entry(hass, entry, async_add_entities): class BalboaSpaBinarySensor(BalboaEntity, BinarySensorEntity): """Representation of a Balboa Spa binary sensor entity.""" - _attr_device_class = DEVICE_CLASS_MOVING + _attr_device_class = BinarySensorDeviceClass.MOVING class BalboaSpaCircPump(BalboaSpaBinarySensor): From d6725715a14351ae6689e80880b45fa98003c663 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 8 Dec 2021 22:13:01 +0100 Subject: [PATCH 0194/2644] Use new EntityCategory in Tractive (#61289) --- homeassistant/components/tractive/binary_sensor.py | 5 +++-- homeassistant/components/tractive/sensor.py | 10 +++------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/tractive/binary_sensor.py b/homeassistant/components/tractive/binary_sensor.py index 453b5cf5b0c..001eb013a35 100644 --- a/homeassistant/components/tractive/binary_sensor.py +++ b/homeassistant/components/tractive/binary_sensor.py @@ -9,9 +9,10 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_BATTERY_CHARGING, ENTITY_CATEGORY_DIAGNOSTIC +from homeassistant.const import ATTR_BATTERY_CHARGING 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 . import Trackables @@ -77,7 +78,7 @@ SENSOR_TYPE = BinarySensorEntityDescription( key=ATTR_BATTERY_CHARGING, name="Battery Charging", device_class=BinarySensorDeviceClass.BATTERY_CHARGING, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ) diff --git a/homeassistant/components/tractive/sensor.py b/homeassistant/components/tractive/sensor.py index 3f6c18fa07f..e19e10f6b44 100644 --- a/homeassistant/components/tractive/sensor.py +++ b/homeassistant/components/tractive/sensor.py @@ -10,14 +10,10 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_BATTERY_LEVEL, - ENTITY_CATEGORY_DIAGNOSTIC, - PERCENTAGE, - TIME_MINUTES, -) +from homeassistant.const import ATTR_BATTERY_LEVEL, PERCENTAGE, TIME_MINUTES 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 . import Trackables @@ -141,7 +137,7 @@ SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, entity_class=TractiveHardwareSensor, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), TractiveSensorEntityDescription( # Currently, only state operational and not_reporting are used From eb3fa12a6a71541937fc8c649ff0af3f8325590b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 8 Dec 2021 22:14:16 +0100 Subject: [PATCH 0195/2644] Use new EntityCategory enum in Switchbot (#61280) --- homeassistant/components/switchbot/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/sensor.py b/homeassistant/components/switchbot/sensor.py index 0a0c4b265ec..3d1eb9f4858 100644 --- a/homeassistant/components/switchbot/sensor.py +++ b/homeassistant/components/switchbot/sensor.py @@ -9,11 +9,11 @@ from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_SIGNAL_STRENGTH, - ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DATA_COORDINATOR, DOMAIN @@ -28,13 +28,13 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=DEVICE_CLASS_SIGNAL_STRENGTH, entity_registry_enabled_default=False, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), "battery": SensorEntityDescription( key="battery", native_unit_of_measurement=PERCENTAGE, device_class=DEVICE_CLASS_BATTERY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), "lightLevel": SensorEntityDescription( key="lightLevel", From b2ae018837c57bf6538d32ecf6af3d8dbc86ed27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 8 Dec 2021 22:14:43 +0100 Subject: [PATCH 0196/2644] Use new EntityCategory in Opengarage (#61285) --- homeassistant/components/opengarage/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/opengarage/sensor.py b/homeassistant/components/opengarage/sensor.py index 5a409146577..140029372ad 100644 --- a/homeassistant/components/opengarage/sensor.py +++ b/homeassistant/components/opengarage/sensor.py @@ -10,13 +10,13 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.const import ( - ENTITY_CATEGORY_DIAGNOSTIC, LENGTH_CENTIMETERS, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS, TEMP_CELSIUS, ) from homeassistant.core import callback +from homeassistant.helpers.entity import EntityCategory from .const import DOMAIN from .entity import OpenGarageEntity @@ -32,7 +32,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="rssi", device_class=SensorDeviceClass.SIGNAL_STRENGTH, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, state_class=SensorStateClass.MEASUREMENT, From 7c09cff3ad07e623e6ebb579fc3655130489abc9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 8 Dec 2021 22:15:15 +0100 Subject: [PATCH 0197/2644] Use new DeviceClass and EntityCategory enums in august (#61288) Co-authored-by: epenet --- .../components/august/binary_sensor.py | 17 +++++++---------- homeassistant/components/august/sensor.py | 16 ++++++---------- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index cf34952309b..d8c2117f4a3 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -19,15 +19,12 @@ from yalexs.util import update_lock_detail_from_activity from homeassistant.components.august import AugustData from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_CONNECTIVITY, - DEVICE_CLASS_DOOR, - DEVICE_CLASS_MOTION, - DEVICE_CLASS_OCCUPANCY, + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import callback +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.event import async_call_later from .const import ACTIVITY_UPDATE_INTERVAL, DATA_AUGUST, DOMAIN @@ -115,22 +112,22 @@ SENSOR_TYPES_DOORBELL: tuple[AugustBinarySensorEntityDescription, ...] = ( AugustBinarySensorEntityDescription( key="doorbell_ding", name="Ding", - device_class=DEVICE_CLASS_OCCUPANCY, + device_class=BinarySensorDeviceClass.OCCUPANCY, value_fn=_retrieve_ding_state, is_time_based=True, ), AugustBinarySensorEntityDescription( key="doorbell_motion", name="Motion", - device_class=DEVICE_CLASS_MOTION, + device_class=BinarySensorDeviceClass.MOTION, value_fn=_retrieve_motion_state, is_time_based=True, ), AugustBinarySensorEntityDescription( key="doorbell_online", name="Online", - device_class=DEVICE_CLASS_CONNECTIVITY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=EntityCategory.DIAGNOSTIC, value_fn=_retrieve_online_state, is_time_based=False, ), @@ -169,7 +166,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class AugustDoorBinarySensor(AugustEntityMixin, BinarySensorEntity): """Representation of an August Door binary sensor.""" - _attr_device_class = DEVICE_CLASS_DOOR + _attr_device_class = BinarySensorDeviceClass.DOOR def __init__(self, data, device, description: BinarySensorEntityDescription): """Initialize the sensor.""" diff --git a/homeassistant/components/august/sensor.py b/homeassistant/components/august/sensor.py index 744177cbef3..031e6fd9282 100644 --- a/homeassistant/components/august/sensor.py +++ b/homeassistant/components/august/sensor.py @@ -12,17 +12,13 @@ from yalexs.lock import LockDetail from homeassistant.components.august import AugustData from homeassistant.components.sensor import ( - DEVICE_CLASS_BATTERY, + SensorDeviceClass, SensorEntity, SensorEntityDescription, ) -from homeassistant.const import ( - ATTR_ENTITY_PICTURE, - ENTITY_CATEGORY_DIAGNOSTIC, - PERCENTAGE, - STATE_UNAVAILABLE, -) +from homeassistant.const import ATTR_ENTITY_PICTURE, PERCENTAGE, STATE_UNAVAILABLE from homeassistant.core import callback +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.helpers.restore_state import RestoreEntity @@ -73,14 +69,14 @@ class AugustSensorEntityDescription( SENSOR_TYPE_DEVICE_BATTERY = AugustSensorEntityDescription[LockDetail]( key="device_battery", name="Battery", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, value_fn=_retrieve_device_battery_state, ) SENSOR_TYPE_KEYPAD_BATTERY = AugustSensorEntityDescription[KeypadDetail]( key="linked_keypad_battery", name="Battery", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, value_fn=_retrieve_linked_keypad_battery_state, ) @@ -254,7 +250,7 @@ class AugustBatterySensor(AugustEntityMixin, SensorEntity, Generic[T]): """Representation of an August sensor.""" entity_description: AugustSensorEntityDescription[T] - _attr_device_class = DEVICE_CLASS_BATTERY + _attr_device_class = SensorDeviceClass.BATTERY _attr_native_unit_of_measurement = PERCENTAGE def __init__( From ebf9faac17d14014acf5b7b70c96d3f763d3cf75 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 8 Dec 2021 22:33:09 +0100 Subject: [PATCH 0198/2644] Use new SensorDeviceClass enum in atag (#61278) Co-authored-by: epenet --- homeassistant/components/atag/sensor.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/atag/sensor.py b/homeassistant/components/atag/sensor.py index 386b5999712..0f1599098a6 100644 --- a/homeassistant/components/atag/sensor.py +++ b/homeassistant/components/atag/sensor.py @@ -1,8 +1,6 @@ """Initialization of ATAG One sensor platform.""" -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.const import ( - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, PERCENTAGE, PRESSURE_BAR, TEMP_CELSIUS, @@ -38,8 +36,8 @@ class AtagSensor(AtagEntity, SensorEntity): super().__init__(coordinator, SENSORS[sensor]) self._attr_name = sensor if coordinator.data.report[self._id].sensorclass in ( - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.PRESSURE, + SensorDeviceClass.TEMPERATURE, ): self._attr_device_class = coordinator.data.report[self._id].sensorclass if coordinator.data.report[self._id].measure in ( From 3e78c28a5b07d6ff120d96c586b9eff78ac5e8ab Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 8 Dec 2021 22:34:05 +0100 Subject: [PATCH 0199/2644] Use _attr_* in android_ip_webcam (#61270) Co-authored-by: epenet --- .../android_ip_webcam/binary_sensor.py | 26 ++++---------- .../components/android_ip_webcam/sensor.py | 34 ++++++------------- .../components/android_ip_webcam/switch.py | 20 +++-------- 3 files changed, 22 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/android_ip_webcam/binary_sensor.py b/homeassistant/components/android_ip_webcam/binary_sensor.py index 377ecfec667..82d18d0ca3d 100644 --- a/homeassistant/components/android_ip_webcam/binary_sensor.py +++ b/homeassistant/components/android_ip_webcam/binary_sensor.py @@ -1,6 +1,6 @@ """Support for Android IP Webcam binary sensors.""" from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_MOTION, + BinarySensorDeviceClass, BinarySensorEntity, ) @@ -22,32 +22,18 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorEntity): """Representation of an IP Webcam binary sensor.""" + _attr_device_class = BinarySensorDeviceClass.MOTION + def __init__(self, name, host, ipcam, sensor): """Initialize the binary sensor.""" super().__init__(host, ipcam) self._sensor = sensor self._mapped_name = KEY_MAP.get(self._sensor, self._sensor) - self._name = f"{name} {self._mapped_name}" - self._state = None - self._unit = None - - @property - def name(self): - """Return the name of the binary sensor, if any.""" - return self._name - - @property - def is_on(self): - """Return true if the binary sensor is on.""" - return self._state + self._attr_name = f"{name} {self._mapped_name}" + self._attr_is_on = None async def async_update(self): """Retrieve latest state.""" state, _ = self._ipcam.export_sensor(self._sensor) - self._state = state == 1.0 - - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return DEVICE_CLASS_MOTION + self._attr_is_on = state == 1.0 diff --git a/homeassistant/components/android_ip_webcam/sensor.py b/homeassistant/components/android_ip_webcam/sensor.py index 4bef3848617..5690dab0937 100644 --- a/homeassistant/components/android_ip_webcam/sensor.py +++ b/homeassistant/components/android_ip_webcam/sensor.py @@ -40,38 +40,26 @@ class IPWebcamSensor(AndroidIPCamEntity, SensorEntity): self._sensor = sensor self._mapped_name = KEY_MAP.get(self._sensor, self._sensor) - self._name = f"{name} {self._mapped_name}" - self._state = None - self._unit = None - - @property - def name(self): - """Return the name of the sensor, if any.""" - return self._name - - @property - def native_unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return self._unit - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state + self._attr_name = f"{name} {self._mapped_name}" + self._attr_native_value = None + self._attr_native_unit_of_measurement = None async def async_update(self): """Retrieve latest state.""" if self._sensor in ("audio_connections", "video_connections"): if not self._ipcam.status_data: return - self._state = self._ipcam.status_data.get(self._sensor) - self._unit = "Connections" + self._attr_native_value = self._ipcam.status_data.get(self._sensor) + self._attr_native_unit_of_measurement = "Connections" else: - self._state, self._unit = self._ipcam.export_sensor(self._sensor) + ( + self._attr_native_value, + self._attr_native_unit_of_measurement, + ) = self._ipcam.export_sensor(self._sensor) @property def icon(self): """Return the icon for the sensor.""" - if self._sensor == "battery_level" and self._state is not None: - return icon_for_battery_level(int(self._state)) + if self._sensor == "battery_level" and self._attr_native_value is not None: + return icon_for_battery_level(int(self._attr_native_value)) return ICON_MAP.get(self._sensor, "mdi:eye") diff --git a/homeassistant/components/android_ip_webcam/switch.py b/homeassistant/components/android_ip_webcam/switch.py index bdbb37e7661..3adb958c4ff 100644 --- a/homeassistant/components/android_ip_webcam/switch.py +++ b/homeassistant/components/android_ip_webcam/switch.py @@ -39,22 +39,12 @@ class IPWebcamSettingsSwitch(AndroidIPCamEntity, SwitchEntity): self._setting = setting self._mapped_name = KEY_MAP.get(self._setting, self._setting) - self._name = f"{name} {self._mapped_name}" - self._state = False - - @property - def name(self): - """Return the name of the node.""" - return self._name + self._attr_name = f"{name} {self._mapped_name}" + self._attr_is_on = False async def async_update(self): """Get the updated status of the switch.""" - self._state = bool(self._ipcam.current_settings.get(self._setting)) - - @property - def is_on(self): - """Return the boolean response if the node is on.""" - return self._state + self._attr_is_on = bool(self._ipcam.current_settings.get(self._setting)) async def async_turn_on(self, **kwargs): """Turn device on.""" @@ -66,7 +56,7 @@ class IPWebcamSettingsSwitch(AndroidIPCamEntity, SwitchEntity): await self._ipcam.record(record=True) else: await self._ipcam.change_setting(self._setting, True) - self._state = True + self._attr_is_on = True self.async_write_ha_state() async def async_turn_off(self, **kwargs): @@ -79,7 +69,7 @@ class IPWebcamSettingsSwitch(AndroidIPCamEntity, SwitchEntity): await self._ipcam.record(record=False) else: await self._ipcam.change_setting(self._setting, False) - self._state = False + self._attr_is_on = False self.async_write_ha_state() @property From adf63d5116282307220af7d4fed8808fb83006b7 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Wed, 8 Dec 2021 21:34:47 +0000 Subject: [PATCH 0200/2644] Aurora ABB add entity category (#61231) --- homeassistant/components/aurora_abb_powerone/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/aurora_abb_powerone/sensor.py b/homeassistant/components/aurora_abb_powerone/sensor.py index 59c74e221bd..6c4f783ecf9 100644 --- a/homeassistant/components/aurora_abb_powerone/sensor.py +++ b/homeassistant/components/aurora_abb_powerone/sensor.py @@ -25,6 +25,7 @@ from homeassistant.const import ( TEMP_CELSIUS, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import EntityCategory from .aurora_device import AuroraEntity from .const import DEFAULT_ADDRESS, DOMAIN @@ -42,6 +43,7 @@ SENSOR_TYPES = [ SensorEntityDescription( key="temp", device_class=SensorDeviceClass.TEMPERATURE, + entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, name="Temperature", From af603d04272ec489d03b26cb6384d062611d0a75 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 8 Dec 2021 22:35:52 +0100 Subject: [PATCH 0201/2644] Use new DeviceClass enums in alexa (#61263) Co-authored-by: epenet --- homeassistant/components/alexa/entities.py | 40 +++++++++++++--------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 2f7f6dc996b..17cdab18df1 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -411,7 +411,7 @@ class SwitchCapabilities(AlexaEntity): def default_display_categories(self): """Return the display categories for this entity.""" device_class = self.entity.attributes.get(ATTR_DEVICE_CLASS) - if device_class == switch.DEVICE_CLASS_OUTLET: + if device_class == switch.SwitchDeviceClass.OUTLET: return [DisplayCategory.SMARTPLUG] return [DisplayCategory.SWITCH] @@ -470,20 +470,20 @@ class CoverCapabilities(AlexaEntity): def default_display_categories(self): """Return the display categories for this entity.""" device_class = self.entity.attributes.get(ATTR_DEVICE_CLASS) - if device_class in (cover.DEVICE_CLASS_GARAGE, cover.DEVICE_CLASS_GATE): + if device_class in (cover.CoverDeviceClass.GARAGE, cover.CoverDeviceClass.GATE): return [DisplayCategory.GARAGE_DOOR] - if device_class == cover.DEVICE_CLASS_DOOR: + if device_class == cover.CoverDeviceClass.DOOR: return [DisplayCategory.DOOR] if device_class in ( - cover.DEVICE_CLASS_BLIND, - cover.DEVICE_CLASS_SHADE, - cover.DEVICE_CLASS_CURTAIN, + cover.CoverDeviceClass.BLIND, + cover.CoverDeviceClass.SHADE, + cover.CoverDeviceClass.CURTAIN, ): return [DisplayCategory.INTERIOR_BLIND] if device_class in ( - cover.DEVICE_CLASS_WINDOW, - cover.DEVICE_CLASS_AWNING, - cover.DEVICE_CLASS_SHUTTER, + cover.CoverDeviceClass.WINDOW, + cover.CoverDeviceClass.AWNING, + cover.CoverDeviceClass.SHUTTER, ): return [DisplayCategory.EXTERIOR_BLIND] @@ -492,7 +492,10 @@ class CoverCapabilities(AlexaEntity): def interfaces(self): """Yield the supported interfaces.""" device_class = self.entity.attributes.get(ATTR_DEVICE_CLASS) - if device_class not in (cover.DEVICE_CLASS_GARAGE, cover.DEVICE_CLASS_GATE): + if device_class not in ( + cover.CoverDeviceClass.GARAGE, + cover.CoverDeviceClass.GATE, + ): yield AlexaPowerController(self.entity) supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) @@ -600,7 +603,7 @@ class MediaPlayerCapabilities(AlexaEntity): def default_display_categories(self): """Return the display categories for this entity.""" device_class = self.entity.attributes.get(ATTR_DEVICE_CLASS) - if device_class == media_player.DEVICE_CLASS_SPEAKER: + if device_class == media_player.MediaPlayerDeviceClass.SPEAKER: return [DisplayCategory.SPEAKER] return [DisplayCategory.TV] @@ -763,17 +766,20 @@ class BinarySensorCapabilities(AlexaEntity): """Return the type of binary sensor.""" attrs = self.entity.attributes if attrs.get(ATTR_DEVICE_CLASS) in ( - binary_sensor.DEVICE_CLASS_DOOR, - binary_sensor.DEVICE_CLASS_GARAGE_DOOR, - binary_sensor.DEVICE_CLASS_OPENING, - binary_sensor.DEVICE_CLASS_WINDOW, + binary_sensor.BinarySensorDeviceClass.DOOR, + binary_sensor.BinarySensorDeviceClass.GARAGE_DOOR, + binary_sensor.BinarySensorDeviceClass.OPENING, + binary_sensor.BinarySensorDeviceClass.WINDOW, ): return self.TYPE_CONTACT - if attrs.get(ATTR_DEVICE_CLASS) == binary_sensor.DEVICE_CLASS_MOTION: + if attrs.get(ATTR_DEVICE_CLASS) == binary_sensor.BinarySensorDeviceClass.MOTION: return self.TYPE_MOTION - if attrs.get(ATTR_DEVICE_CLASS) == binary_sensor.DEVICE_CLASS_PRESENCE: + if ( + attrs.get(ATTR_DEVICE_CLASS) + == binary_sensor.BinarySensorDeviceClass.PRESENCE + ): return self.TYPE_PRESENCE From 17cf53677c5f9112f706e2ab2cf77c5bc457625a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 8 Dec 2021 14:35:50 -0800 Subject: [PATCH 0202/2644] Rest fixes (#61296) --- homeassistant/components/pvoutput/sensor.py | 6 +---- homeassistant/components/rest/__init__.py | 6 ++--- homeassistant/components/rest/data.py | 6 ++--- homeassistant/components/rest/switch.py | 16 ++++++------ homeassistant/components/rest/utils.py | 27 --------------------- homeassistant/helpers/template.py | 15 +++++++++--- 6 files changed, 26 insertions(+), 50 deletions(-) delete mode 100644 homeassistant/components/rest/utils.py diff --git a/homeassistant/components/pvoutput/sensor.py b/homeassistant/components/pvoutput/sensor.py index 512fb75067b..1d8b3400d8b 100644 --- a/homeassistant/components/pvoutput/sensor.py +++ b/homeassistant/components/pvoutput/sensor.py @@ -25,7 +25,6 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.template import Template _LOGGER = logging.getLogger(__name__) _ENDPOINT = "https://pvoutput.org/service/r2/getstatus.jsp" @@ -60,10 +59,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= method = "GET" payload = auth = None verify_ssl = DEFAULT_VERIFY_SSL - headers = { - "X-Pvoutput-Apikey": Template(api_key, hass), - "X-Pvoutput-SystemId": Template(system_id, hass), - } + headers = {"X-Pvoutput-Apikey": api_key, "X-Pvoutput-SystemId": system_id} rest = RestData(hass, method, _ENDPOINT, auth, headers, None, payload, verify_ssl) await rest.async_update() diff --git a/homeassistant/components/rest/__init__.py b/homeassistant/components/rest/__init__.py index ba101624673..b55d9c6d844 100644 --- a/homeassistant/components/rest/__init__.py +++ b/homeassistant/components/rest/__init__.py @@ -25,7 +25,7 @@ from homeassistant.const import ( SERVICE_RELOAD, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import discovery +from homeassistant.helpers import discovery, template from homeassistant.helpers.entity_component import ( DEFAULT_SCAN_INTERVAL, EntityComponent, @@ -37,7 +37,6 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import COORDINATOR, DOMAIN, PLATFORM_IDX, REST, REST_DATA, REST_IDX from .data import RestData from .schema import CONFIG_SCHEMA # noqa: F401 -from .utils import inject_hass_in_templates_list _LOGGER = logging.getLogger(__name__) @@ -161,7 +160,8 @@ def create_rest_data_from_config(hass, config): resource_template.hass = hass resource = resource_template.async_render(parse_result=False) - inject_hass_in_templates_list(hass, [headers, params]) + template.attach(hass, headers) + template.attach(hass, params) if username and password: if config.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION: diff --git a/homeassistant/components/rest/data.py b/homeassistant/components/rest/data.py index bc98b0caf68..7c8fd61e688 100644 --- a/homeassistant/components/rest/data.py +++ b/homeassistant/components/rest/data.py @@ -3,7 +3,7 @@ import logging import httpx -from homeassistant.components.rest.utils import render_templates +from homeassistant.helpers import template from homeassistant.helpers.httpx_client import get_async_client DEFAULT_TIMEOUT = 10 @@ -52,8 +52,8 @@ class RestData: self._hass, verify_ssl=self._verify_ssl ) - rendered_headers = render_templates(self._headers, False) - rendered_params = render_templates(self._params, True) + rendered_headers = template.render_complex(self._headers, parse_result=False) + rendered_params = template.render_complex(self._params) _LOGGER.debug("Updating from %s", self._resource) try: diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index 1fd04b66559..3e5fd7e2c68 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -24,10 +24,8 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, ) +from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv - -from .utils import inject_hass_in_templates_list, render_templates _LOGGER = logging.getLogger(__name__) CONF_BODY_OFF = "body_off" @@ -92,7 +90,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= body_on.hass = hass if body_off is not None: body_off.hass = hass - inject_hass_in_templates_list(hass, [headers, params]) + + template.attach(hass, headers) + template.attach(hass, params) timeout = config.get(CONF_TIMEOUT) try: @@ -207,8 +207,8 @@ class RestSwitch(SwitchEntity): """Send a state update to the device.""" websession = async_get_clientsession(self.hass, self._verify_ssl) - rendered_headers = render_templates(self._headers, False) - rendered_params = render_templates(self._params, True) + rendered_headers = template.render_complex(self._headers, parse_result=False) + rendered_params = template.render_complex(self._params) async with async_timeout.timeout(self._timeout): req = await getattr(websession, self._method)( @@ -233,8 +233,8 @@ class RestSwitch(SwitchEntity): """Get the latest data from REST API and update the state.""" websession = async_get_clientsession(hass, self._verify_ssl) - rendered_headers = render_templates(self._headers, False) - rendered_params = render_templates(self._params, True) + rendered_headers = template.render_complex(self._headers, parse_result=False) + rendered_params = template.render_complex(self._params) async with async_timeout.timeout(self._timeout): req = await websession.get( diff --git a/homeassistant/components/rest/utils.py b/homeassistant/components/rest/utils.py deleted file mode 100644 index f3fdba651ac..00000000000 --- a/homeassistant/components/rest/utils.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Reusable utilities for the Rest component.""" -from __future__ import annotations - -from homeassistant.core import HomeAssistant -from homeassistant.helpers.template import Template - - -def inject_hass_in_templates_list( - hass: HomeAssistant, tpl_dict_list: list[dict[str, Template] | None] -): - """Inject hass in a list of dict of templates.""" - for tpl_dict in tpl_dict_list: - if tpl_dict is not None: - for tpl in tpl_dict.values(): - tpl.hass = hass - - -def render_templates(tpl_dict: dict[str, Template] | None, parse_result: bool): - """Render a dict of templates.""" - if tpl_dict is None: - return None - - rendered_items = {} - for item_name, template in tpl_dict.items(): - if (value := template.async_render(parse_result=parse_result)) is not None: - rendered_items[item_name] = value - return rendered_items diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index d460e7ab42b..0ba1d6bfa14 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -110,18 +110,25 @@ def attach(hass: HomeAssistant, obj: Any) -> None: def render_complex( - value: Any, variables: TemplateVarsType = None, limited: bool = False + value: Any, + variables: TemplateVarsType = None, + limited: bool = False, + parse_result: bool = True, ) -> Any: """Recursive template creator helper function.""" if isinstance(value, list): - return [render_complex(item, variables) for item in value] + return [ + render_complex(item, variables, limited, parse_result) for item in value + ] if isinstance(value, collections.abc.Mapping): return { - render_complex(key, variables): render_complex(item, variables) + render_complex(key, variables, limited, parse_result): render_complex( + item, variables, limited, parse_result + ) for key, item in value.items() } if isinstance(value, Template): - return value.async_render(variables, limited=limited) + return value.async_render(variables, limited=limited, parse_result=parse_result) return value From 1f1a29cada58c2820e9c130915aceefb85bc244c Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 9 Dec 2021 00:13:16 +0000 Subject: [PATCH 0203/2644] [ci skip] Translation update --- .../components/adax/translations/bg.json | 14 ++++- .../components/adax/translations/ca.json | 22 +++++++- .../components/adax/translations/de.json | 22 +++++++- .../components/adax/translations/en.json | 22 +++++++- .../components/adax/translations/et.json | 22 +++++++- .../components/adax/translations/hu.json | 22 +++++++- .../components/adax/translations/ja.json | 22 +++++++- .../components/adax/translations/nl.json | 22 +++++++- .../components/adax/translations/no.json | 22 +++++++- .../components/adax/translations/ru.json | 22 +++++++- .../components/adax/translations/zh-Hans.json | 5 ++ .../components/adax/translations/zh-Hant.json | 22 +++++++- .../components/apple_tv/translations/bg.json | 3 ++ .../components/apple_tv/translations/ca.json | 23 ++++++-- .../apple_tv/translations/zh-Hant.json | 23 ++++++-- .../aseko_pool_live/translations/ca.json | 20 +++++++ .../binary_sensor/translations/ca.json | 2 + .../components/cloud/translations/de.json | 2 +- .../components/elmax/translations/bg.json | 27 ++++++++++ .../components/elmax/translations/ca.json | 34 ++++++++++++ .../components/elmax/translations/de.json | 34 ++++++++++++ .../components/elmax/translations/et.json | 34 ++++++++++++ .../components/elmax/translations/hu.json | 34 ++++++++++++ .../components/elmax/translations/ja.json | 34 ++++++++++++ .../components/elmax/translations/nl.json | 34 ++++++++++++ .../components/elmax/translations/no.json | 34 ++++++++++++ .../components/elmax/translations/ru.json | 34 ++++++++++++ .../elmax/translations/zh-Hant.json | 34 ++++++++++++ .../enphase_envoy/translations/ca.json | 3 +- .../components/fronius/translations/ca.json | 7 ++- .../components/knx/translations/ca.json | 2 + .../components/nina/translations/ca.json | 27 ++++++++++ .../translations/select.bg.json | 5 ++ .../translations/select.ca.json | 52 +++++++++++++++++++ .../translations/select.zh-Hant.json | 52 +++++++++++++++++++ .../components/zwave_js/translations/ca.json | 2 + .../zwave_js/translations/zh-Hant.json | 2 + 37 files changed, 763 insertions(+), 34 deletions(-) create mode 100644 homeassistant/components/aseko_pool_live/translations/ca.json create mode 100644 homeassistant/components/elmax/translations/bg.json create mode 100644 homeassistant/components/elmax/translations/ca.json create mode 100644 homeassistant/components/elmax/translations/de.json create mode 100644 homeassistant/components/elmax/translations/et.json create mode 100644 homeassistant/components/elmax/translations/hu.json create mode 100644 homeassistant/components/elmax/translations/ja.json create mode 100644 homeassistant/components/elmax/translations/nl.json create mode 100644 homeassistant/components/elmax/translations/no.json create mode 100644 homeassistant/components/elmax/translations/ru.json create mode 100644 homeassistant/components/elmax/translations/zh-Hant.json create mode 100644 homeassistant/components/nina/translations/ca.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/select.ca.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/select.zh-Hant.json diff --git a/homeassistant/components/adax/translations/bg.json b/homeassistant/components/adax/translations/bg.json index 3d3795470ba..5cc751762da 100644 --- a/homeassistant/components/adax/translations/bg.json +++ b/homeassistant/components/adax/translations/bg.json @@ -1,13 +1,25 @@ { "config": { "abort": { - "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" }, "step": { + "cloud": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + } + }, + "local": { + "data": { + "wifi_pswd": "\u041f\u0430\u0440\u043e\u043b\u0430 \u0437\u0430 Wi-Fi", + "wifi_ssid": "Wifi ssid" + } + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", diff --git a/homeassistant/components/adax/translations/ca.json b/homeassistant/components/adax/translations/ca.json index 85ba15804ac..f2313e56af5 100644 --- a/homeassistant/components/adax/translations/ca.json +++ b/homeassistant/components/adax/translations/ca.json @@ -1,19 +1,37 @@ { "config": { "abort": { - "already_configured": "El dispositiu ja est\u00e0 configurat" + "already_configured": "El dispositiu ja est\u00e0 configurat", + "heater_not_available": "Escalfador no disponible. Intenta reiniciar l'escalfador prement '+' i 'OK' durant uns segons.", + "heater_not_found": "No s'ha trobat l'escalfador. Intenta apropar-lo a l'ordinador amb Home Assistant.", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" }, "step": { + "cloud": { + "data": { + "account_id": "ID del compte", + "password": "Contrasenya" + } + }, + "local": { + "data": { + "wifi_pswd": "Contrasenya WiFi", + "wifi_ssid": "SSID WiFi" + }, + "description": "Reinicia l'escalfador prement '+' i 'OK' fins que la pantalla mostri 'Reset'. A continuaci\u00f3 i abans de fer clic a Envia, mant\u00e9 premut el bot\u00f3 'OK' fins que el led blau comenci a parpellejar. La configuraci\u00f3 de l'escalfador pot trigar uns minuts." + }, "user": { "data": { "account_id": "ID del compte", + "connection_type": "Selecciona el tipus de connexi\u00f3", "host": "Amfitri\u00f3", "password": "Contrasenya" - } + }, + "description": "Selecciona el tipus de connexi\u00f3. La local necessita escalfadors amb Bluetooth" } } } diff --git a/homeassistant/components/adax/translations/de.json b/homeassistant/components/adax/translations/de.json index 414b373ff34..711a8b44645 100644 --- a/homeassistant/components/adax/translations/de.json +++ b/homeassistant/components/adax/translations/de.json @@ -1,19 +1,37 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "heater_not_available": "Heizger\u00e4t nicht verf\u00fcgbar. Versuche das Heizger\u00e4t zur\u00fcckzusetzen, indem du + und OK einige Sekunden lang dr\u00fcckst.", + "heater_not_found": "Heizger\u00e4t nicht gefunden. Versuche das Heizger\u00e4t n\u00e4her an den Home Assistant-Computer zu bringen.", + "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "step": { + "cloud": { + "data": { + "account_id": "Konto-ID", + "password": "Passwort" + } + }, + "local": { + "data": { + "wifi_pswd": "WLAN Passwort", + "wifi_ssid": "WLAN SSID" + }, + "description": "Setze das Heizger\u00e4t zur\u00fcck, indem du + und OK dr\u00fcckst, bis auf dem Display \"Reset\" angezeigt wird. Halte dann die OK-Taste am Heizger\u00e4t gedr\u00fcckt, bis die blaue LED zu blinken beginnt, und dr\u00fccke dann auf Senden. Das Konfigurieren des Heizger\u00e4ts kann einige Minuten dauern." + }, "user": { "data": { "account_id": "Konto-ID", + "connection_type": "Verbindungstyp ausw\u00e4hlen", "host": "Host", "password": "Passwort" - } + }, + "description": "Verbindungstyp ausw\u00e4hlen. Lokal erfordert Heizungen mit Bluetooth" } } } diff --git a/homeassistant/components/adax/translations/en.json b/homeassistant/components/adax/translations/en.json index d1ef64a52c0..637bef63ece 100644 --- a/homeassistant/components/adax/translations/en.json +++ b/homeassistant/components/adax/translations/en.json @@ -1,19 +1,37 @@ { "config": { "abort": { - "already_configured": "Device is already configured" + "already_configured": "Device is already configured", + "heater_not_available": "Heater not available. Try to reset the heater by pressing + and OK for some seconds.", + "heater_not_found": "Heater not found. Try to move the heater closer to Home Assistant computer.", + "invalid_auth": "Invalid authentication" }, "error": { "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication" }, "step": { + "cloud": { + "data": { + "account_id": "Account ID", + "password": "Password" + } + }, + "local": { + "data": { + "wifi_pswd": "Wifi password", + "wifi_ssid": "Wifi ssid" + }, + "description": "Reset the heater by pressing + and OK until display shows 'Reset'. Then press and hold OK button on the heater until the blue led starts blinking before pressing Submit. Configuring heater might take some minutes." + }, "user": { "data": { "account_id": "Account ID", + "connection_type": "Select connection type", "host": "Host", "password": "Password" - } + }, + "description": "Select connection type. Local requires heaters with bluetooth" } } } diff --git a/homeassistant/components/adax/translations/et.json b/homeassistant/components/adax/translations/et.json index c8dd855218c..414e7b1022f 100644 --- a/homeassistant/components/adax/translations/et.json +++ b/homeassistant/components/adax/translations/et.json @@ -1,19 +1,37 @@ { "config": { "abort": { - "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "heater_not_available": "K\u00fctteseade pole saadaval. Proovi k\u00fctteseadet l\u00e4htestada, vajutades m\u00f5ne sekundi jooksul + ja OK.", + "heater_not_found": "K\u00fctteseadet ei leitud. Proovi viia k\u00fctteseade Home Assistanti arvutile l\u00e4hemale.", + "invalid_auth": "Tuvastamine nurjus" }, "error": { "cannot_connect": "\u00dchendamine nurjus", "invalid_auth": "Tuvastamise viga" }, "step": { + "cloud": { + "data": { + "account_id": "Konto ID", + "password": "Salas\u00f5na" + } + }, + "local": { + "data": { + "wifi_pswd": "Wifi salas\u00f5na", + "wifi_ssid": "Wifi ssid" + }, + "description": "L\u00e4htesta k\u00fctteseade, vajutades + ja OK kuni ekraanil kuvatakse \"Reset\". Seej\u00e4rel vajuta ja hoia kerisel nuppu OK kuni sinine led hakkab vilkuma enne nupu Edasta vajutamist. K\u00fctteseadme konfigureerimine v\u00f5ib v\u00f5tta aega m\u00f5ni minut." + }, "user": { "data": { "account_id": "Konto ID", + "connection_type": "Vali \u00fchenduse t\u00fc\u00fcp", "host": "Host", "password": "Salas\u00f5na" - } + }, + "description": "Vali \u00fchenduse t\u00fc\u00fcp. Kohalik n\u00f5uab bluetoothiga k\u00fctteseadmeid" } } } diff --git a/homeassistant/components/adax/translations/hu.json b/homeassistant/components/adax/translations/hu.json index 94397487c87..9f37837420f 100644 --- a/homeassistant/components/adax/translations/hu.json +++ b/homeassistant/components/adax/translations/hu.json @@ -1,19 +1,37 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "heater_not_available": "A f\u0171t\u0151berendez\u00e9s nem \u00e1ll rendelkez\u00e9sre. Pr\u00f3b\u00e1lja meg gy\u00e1ri \u00e1llapotba vissza\u00e1ll\u00edtani a + \u00e9s az OK gomb nyomvatart\u00e1s\u00e1val n\u00e9h\u00e1ny m\u00e1sodpercig.", + "heater_not_found": "A f\u0171t\u0151berendez\u00e9s nem tal\u00e1lhat\u00f3. Pr\u00f3b\u00e1lja meg k\u00f6zelebb helyezni a Home Assistant sz\u00e1m\u00edt\u00f3g\u00e9phez.", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, "error": { "cannot_connect": "Nem siker\u00fclt csatlakozni", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, "step": { + "cloud": { + "data": { + "account_id": "Fi\u00f3k ID", + "password": "Jelsz\u00f3" + } + }, + "local": { + "data": { + "wifi_pswd": "WiFi jelsz\u00f3", + "wifi_ssid": "WiFi ssid" + }, + "description": "\u00c1ll\u00edtsa vissza a f\u0171t\u0151berendez\u00e9st a + \u00e9s az OK gomb nyomvatart\u00e1s\u00e1val, m\u00edg a kijelz\u0151n a \"Reset\" (Vissza\u00e1ll\u00edt\u00e1s) felirat nem jelenik meg. Ezut\u00e1n nyomja meg \u00e9s tartsa lenyomva a f\u0171t\u0151berendez\u00e9s OK gombj\u00e1t, am\u00edg a k\u00e9k led villogni nem kezd, majd nyomja meg a K\u00fcld\u00e9s gombot. A f\u0171t\u0151berendez\u00e9s konfigur\u00e1l\u00e1sa n\u00e9h\u00e1ny percet vehet ig\u00e9nybe." + }, "user": { "data": { "account_id": "Fi\u00f3k ID", + "connection_type": "V\u00e1lassza ki a kapcsolat t\u00edpus\u00e1t", "host": "C\u00edm", "password": "Jelsz\u00f3" - } + }, + "description": "V\u00e1lassza ki a kapcsolat t\u00edpus\u00e1t. A Helyi kapcsolathoz bluetooth-os f\u0171t\u0151berendez\u00e9sekre van sz\u00fcks\u00e9g" } } } diff --git a/homeassistant/components/adax/translations/ja.json b/homeassistant/components/adax/translations/ja.json index 880b37db94d..e432e23e081 100644 --- a/homeassistant/components/adax/translations/ja.json +++ b/homeassistant/components/adax/translations/ja.json @@ -1,19 +1,37 @@ { "config": { "abort": { - "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "heater_not_available": "\u30d2\u30fc\u30bf\u30fc\u306f\u5229\u7528\u3067\u304d\u307e\u305b\u3093\u3002\u6570\u79d2\u9593\u3001+ \u3068 OK \u3092\u62bc\u3057\u3066\u30d2\u30fc\u30bf\u30fc\u3092\u30ea\u30bb\u30c3\u30c8\u3057\u3066\u307f\u3066\u304f\u3060\u3055\u3044\u3002", + "heater_not_found": "\u30d2\u30fc\u30bf\u30fc\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002\u30d2\u30fc\u30bf\u30fc\u3092Home Assistant\u30b3\u30f3\u30d4\u30e5\u30fc\u30bf\u30fc\u306b\u8fd1\u3065\u3051\u3066\u307f\u3066\u304f\u3060\u3055\u3044\u3002", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" }, "step": { + "cloud": { + "data": { + "account_id": "\u30a2\u30ab\u30a6\u30f3\u30c8ID", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + }, + "local": { + "data": { + "wifi_pswd": "Wifi\u30d1\u30b9\u30ef\u30fc\u30c9", + "wifi_ssid": "Wifi ssid" + }, + "description": "\u30c7\u30a3\u30b9\u30d7\u30ec\u30a4\u306b\u3001 'Reset ' \u3068\u8868\u793a\u3055\u308c\u308b\u307e\u3067 + \u3068 OK \u3092\u62bc\u3057\u3066\u3001\u30d2\u30fc\u30bf\u30fc\u3092\u30ea\u30bb\u30c3\u30c8\u3057\u307e\u3059\u3002\u305d\u306e\u5f8c\u3001\u30d2\u30fc\u30bf\u30fc\u306eOK\u30dc\u30bf\u30f3\u3092\u9752\u306eLED\u304c\u70b9\u6ec5\u3057\u59cb\u3081\u308b\u307e\u3067\u62bc\u3057\u7d9a\u3051\u3066\u304b\u3089\u3001Submit\u3092\u62bc\u3057\u307e\u3059\u3002\u30d2\u30fc\u30bf\u30fc\u306e\u8a2d\u5b9a\u306b\u306f\u6570\u5206\u304b\u304b\u308b\u5834\u5408\u304c\u3042\u308a\u307e\u3059\u3002" + }, "user": { "data": { "account_id": "\u30a2\u30ab\u30a6\u30f3\u30c8ID", + "connection_type": "\u63a5\u7d9a\u30bf\u30a4\u30d7\u306e\u9078\u629e", "host": "\u30db\u30b9\u30c8", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" - } + }, + "description": "\u63a5\u7d9a\u30bf\u30a4\u30d7\u3092\u9078\u629e\u3057\u307e\u3059\u3002\u30ed\u30fc\u30ab\u30eb\u306b\u306fBluetooth\u4ed8\u304d\u306e\u30d2\u30fc\u30bf\u30fc\u304c\u5fc5\u8981\u3067\u3059" } } } diff --git a/homeassistant/components/adax/translations/nl.json b/homeassistant/components/adax/translations/nl.json index 2bec9774c0a..9622d41bf65 100644 --- a/homeassistant/components/adax/translations/nl.json +++ b/homeassistant/components/adax/translations/nl.json @@ -1,19 +1,37 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd", + "heater_not_available": "Verwarming niet aanwezig. Probeer de kachel te resetten door enkele seconden op + en OK te drukken.", + "heater_not_found": "Verwarming niet gevonden. Probeer de verwarming dichter bij de Home Assistant-computer te plaatsen.", + "invalid_auth": "Ongeldige authenticatie" }, "error": { "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie" }, "step": { + "cloud": { + "data": { + "account_id": "Account ID", + "password": "Wachtwoord" + } + }, + "local": { + "data": { + "wifi_pswd": "Wifi wachtwoord", + "wifi_ssid": "Wifi ssid" + }, + "description": "Reset de kachel door op + en OK te drukken totdat het display 'Reset' toont. Houd vervolgens de OK-knop op de verwarming ingedrukt totdat de blauwe led begint te knipperen voordat u op Verzenden drukt. Het configureren van de verwarming kan enkele minuten duren." + }, "user": { "data": { "account_id": "Account ID", + "connection_type": "Selecteer verbindingstype", "host": "Host", "password": "Wachtwoord" - } + }, + "description": "Selecteer verbindingstype. Lokaal vereist verwarming met bluetooth" } } } diff --git a/homeassistant/components/adax/translations/no.json b/homeassistant/components/adax/translations/no.json index 33c54b57093..54c154c98c2 100644 --- a/homeassistant/components/adax/translations/no.json +++ b/homeassistant/components/adax/translations/no.json @@ -1,19 +1,37 @@ { "config": { "abort": { - "already_configured": "Enheten er allerede konfigurert" + "already_configured": "Enheten er allerede konfigurert", + "heater_not_available": "Varmeapparat ikke tilgjengelig. Pr\u00f8v \u00e5 tilbakestille varmeapparatet ved \u00e5 trykke p\u00e5 + og OK i noen sekunder.", + "heater_not_found": "Fant ikke varmeovn. Pr\u00f8v \u00e5 flytte varmeren n\u00e6rmere Home Assistant-datamaskinen.", + "invalid_auth": "Ugyldig godkjenning" }, "error": { "cannot_connect": "Tilkobling mislyktes", "invalid_auth": "Ugyldig godkjenning" }, "step": { + "cloud": { + "data": { + "account_id": "Konto-ID", + "password": "Passord" + } + }, + "local": { + "data": { + "wifi_pswd": "Wifi-passord", + "wifi_ssid": "Wifi ssid" + }, + "description": "Tilbakestill varmeren ved \u00e5 trykke p\u00e5 + og OK til displayet viser 'Tilbakestill'. Trykk deretter p\u00e5 og hold inne OK-knappen p\u00e5 varmeren til den bl\u00e5 lampen begynner \u00e5 blinke f\u00f8r du trykker p\u00e5 Send. Det kan ta noen minutter \u00e5 konfigurere varmeapparatet." + }, "user": { "data": { "account_id": "Konto-ID", + "connection_type": "Velg tilkoblingstype", "host": "Vert", "password": "Passord" - } + }, + "description": "Velg tilkoblingstype. Lokalt krever varmeovner med bluetooth" } } } diff --git a/homeassistant/components/adax/translations/ru.json b/homeassistant/components/adax/translations/ru.json index d0aece78982..27596e86485 100644 --- a/homeassistant/components/adax/translations/ru.json +++ b/homeassistant/components/adax/translations/ru.json @@ -1,19 +1,37 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "heater_not_available": "\u041d\u0430\u0433\u0440\u0435\u0432\u0430\u0442\u0435\u043b\u044c \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u0435\u0440\u0435\u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0435\u0433\u043e, \u043d\u0430\u0436\u0430\u0432 \u043d\u0430 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0441\u0435\u043a\u0443\u043d\u0434 \u043a\u043d\u043e\u043f\u043a\u0438 + \u0438 \u041e\u041a.", + "heater_not_found": "\u041d\u0430\u0433\u0440\u0435\u0432\u0430\u0442\u0435\u043b\u044c \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d. \u041f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u0435\u0440\u0435\u043c\u0435\u0441\u0442\u0438\u0442\u044c \u043e\u0431\u043e\u0433\u0440\u0435\u0432\u0430\u0442\u0435\u043b\u044c \u0431\u043b\u0438\u0436\u0435 \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 Home Assistant.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, "step": { + "cloud": { + "data": { + "account_id": "ID \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + }, + "local": { + "data": { + "wifi_pswd": "\u041f\u0430\u0440\u043e\u043b\u044c Wi-Fi", + "wifi_ssid": "Wi-Fi SSID" + }, + "description": "\u0421\u0431\u0440\u043e\u0441\u044c\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0430\u0433\u0440\u0435\u0432\u0430\u0442\u0435\u043b\u044f, \u043d\u0430\u0436\u0438\u043c\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0438 + \u0438 OK, \u043f\u043e\u043a\u0430 \u043d\u0430 \u0434\u0438\u0441\u043f\u043b\u0435\u0435 \u043d\u0435 \u043f\u043e\u044f\u0432\u0438\u0442\u0441\u044f \u043d\u0430\u0434\u043f\u0438\u0441\u044c 'Reset'. \u0417\u0430\u0442\u0435\u043c \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u0438 \u0443\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0439\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 OK \u043d\u0430 \u043d\u0430\u0433\u0440\u0435\u0432\u0430\u0442\u0435\u043b\u0435, \u043f\u043e\u043a\u0430 \u0441\u0438\u043d\u0438\u0439 \u0438\u043d\u0434\u0438\u043a\u0430\u0442\u043e\u0440 \u043d\u0435 \u043d\u0430\u0447\u043d\u0435\u0442 \u043c\u0438\u0433\u0430\u0442\u044c, \u043f\u043e\u0441\u043b\u0435 \u0447\u0435\u0433\u043e \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u0437\u0434\u0435\u0441\u044c '\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c'. \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043d\u0430\u0433\u0440\u0435\u0432\u0430\u0442\u0435\u043b\u044f \u043c\u043e\u0436\u0435\u0442 \u0437\u0430\u043d\u044f\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u043c\u0438\u043d\u0443\u0442." + }, "user": { "data": { "account_id": "ID \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438", + "connection_type": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f", "host": "\u0425\u043e\u0441\u0442", "password": "\u041f\u0430\u0440\u043e\u043b\u044c" - } + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f. \u0414\u043b\u044f \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043e\u0431\u043e\u0433\u0440\u0435\u0432\u0430\u0442\u0435\u043b\u0438 \u0441 Bluetooth." } } } diff --git a/homeassistant/components/adax/translations/zh-Hans.json b/homeassistant/components/adax/translations/zh-Hans.json index 7356ec08b15..7cc89fcc775 100644 --- a/homeassistant/components/adax/translations/zh-Hans.json +++ b/homeassistant/components/adax/translations/zh-Hans.json @@ -4,6 +4,11 @@ "cannot_connect": "\u8fde\u63a5\u5931\u8d25" }, "step": { + "cloud": { + "data": { + "account_id": "\u5e10\u6237ID" + } + }, "user": { "data": { "password": "\u5bc6\u7801" diff --git a/homeassistant/components/adax/translations/zh-Hant.json b/homeassistant/components/adax/translations/zh-Hant.json index 9685227f617..0ad4bcba854 100644 --- a/homeassistant/components/adax/translations/zh-Hant.json +++ b/homeassistant/components/adax/translations/zh-Hant.json @@ -1,19 +1,37 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "heater_not_available": "\u627e\u4e0d\u5230\u52a0\u71b1\u5668\uff0c\u8acb\u8a66\u8457\u6309\u4f4f + \u8207 OK \u5e7e\u79d2\u9032\u884c\u91cd\u7f6e\u3002", + "heater_not_found": "\u627e\u4e0d\u5230\u52a0\u71b1\u5668\uff0c\u8acb\u8a66\u8457\u5c07\u52a0\u71b1\u5668\u5f80 Home Assistant \u4f3a\u670d\u5668\u9760\u8fd1\u3002", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, "step": { + "cloud": { + "data": { + "account_id": "\u5e33\u865f ID", + "password": "\u5bc6\u78bc" + } + }, + "local": { + "data": { + "wifi_pswd": "Wi-Fi \u5bc6\u78bc", + "wifi_ssid": "Wi-Fi SSID" + }, + "description": "\u6309\u4f4f + \u8207 OK \u9032\u884c\u52a0\u71b1\u5668\u91cd\u7f6e\u3001\u76f4\u5230\u986f\u793a 'Reset'\u3002\u63a5\u8457\u6309\u4f4f\u52a0\u71b1\u5668\u4e0a\u7684 OK \u6309\u9215\u3001\u76f4\u5230\u85cd\u8272 LED \u71c8\u958b\u59cb\u9583\u720d\uff0c\u518d\u6309\u4e0b\u9001\u51fa\u3002\u52a0\u71b1\u5668\u8a2d\u5b9a\u53ef\u80fd\u9700\u8981\u5e7e\u5206\u9418\u6642\u9593\u3002" + }, "user": { "data": { "account_id": "\u5e33\u865f ID", + "connection_type": "\u9078\u64c7\u9023\u7dda\u985e\u578b", "host": "\u4e3b\u6a5f\u7aef", "password": "\u5bc6\u78bc" - } + }, + "description": "\u9078\u64c7\u9023\u7dda\u985e\u578b\u3002\u672c\u5730\u7aef\u5c07\u9700\u8981\u5177\u5099\u85cd\u82bd\u52a0\u71b1\u5668" } } } diff --git a/homeassistant/components/apple_tv/translations/bg.json b/homeassistant/components/apple_tv/translations/bg.json index 9da5f90cf26..4629a79d152 100644 --- a/homeassistant/components/apple_tv/translations/bg.json +++ b/homeassistant/components/apple_tv/translations/bg.json @@ -20,6 +20,9 @@ "pin": "\u041f\u0418\u041d \u043a\u043e\u0434" } }, + "password": { + "title": "\u0418\u0437\u0438\u0441\u043a\u0432\u0430 \u0441\u0435 \u043f\u0430\u0440\u043e\u043b\u0430" + }, "reconfigure": { "title": "\u041f\u0440\u0435\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e" }, diff --git a/homeassistant/components/apple_tv/translations/ca.json b/homeassistant/components/apple_tv/translations/ca.json index 646931135e2..88c49059067 100644 --- a/homeassistant/components/apple_tv/translations/ca.json +++ b/homeassistant/components/apple_tv/translations/ca.json @@ -1,12 +1,17 @@ { "config": { "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", "already_configured_device": "El dispositiu ja est\u00e0 configurat", "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", "backoff": "En aquests moments el dispositiu no accepta sol\u00b7licituds de vinculaci\u00f3 (\u00e9s possible que hagis introdu\u00eft un codi PIN inv\u00e0lid massa vegades), torna-ho a provar m\u00e9s tard.", "device_did_not_pair": "No s'ha fet cap intent d'acabar el proc\u00e9s de vinculaci\u00f3 des del dispositiu.", + "device_not_found": "No s'ha trobat el dispositiu durant el descobriment, prova de tornar-lo a afegir.", + "inconsistent_device": "Els protocols esperats no s'han trobat durant el descobriment. Normalment aix\u00f2 indica un problema amb el DNS multicast (Zeroconf). Prova d'afegir el dispositiu de nou.", "invalid_config": "La configuraci\u00f3 d'aquest dispositiu no est\u00e0 completa. Intenta'l tornar a afegir.", "no_devices_found": "No s'han trobat dispositius a la xarxa", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", + "setup_failed": "No s'ha pogut configurar el dispositiu.", "unknown": "Error inesperat" }, "error": { @@ -16,14 +21,14 @@ "no_usable_service": "S'ha trobat un dispositiu per\u00f2 no ha pogut identificar cap manera d'establir-hi una connexi\u00f3. Si continues veient aquest missatge, prova d'especificar-ne l'adre\u00e7a IP o reinicia l'Apple TV.", "unknown": "Error inesperat" }, - "flow_title": "{name}", + "flow_title": "{name} ({type})", "step": { "confirm": { - "description": "Est\u00e0s a punt d'afegir l'Apple TV amb nom \"{name}\" a Home Assistant.\n\n **Per completar el proc\u00e9s, \u00e9s possible que hagis d'introduir alguns codis PIN.** \n\n Tingues en compte que *no* pots apagar la teva Apple TV a trav\u00e9s d'aquesta integraci\u00f3. Nom\u00e9s es desactivar\u00e0 el reproductor de Home Assistant.", + "description": "Est\u00e0s a punt d'afegir `{name}` de tipus `{type}` a Home Assistant.\n\n **Per completar el proc\u00e9s, \u00e9s possible que hagis d'introduir alguns codis PIN.** \n\nTingues en compte que *no* pots apagar la teva Apple TV a trav\u00e9s d'aquesta integraci\u00f3. Nom\u00e9s es desactivar\u00e0 el reproductor de Home Assistant.", "title": "Confirma l'addici\u00f3 de l'Apple TV" }, "pair_no_pin": { - "description": "Vinculaci\u00f3 necess\u00e0ria amb el servei `{protocol}`. Per continuar, introdueix el PIN {pin} a la teva Apple TV.", + "description": "Vinculaci\u00f3 necess\u00e0ria pel servei `{protocol}`. Introdueix el PIN {pin} al teu dispositiu per continuar.", "title": "Vinculaci\u00f3" }, "pair_with_pin": { @@ -33,8 +38,16 @@ "description": "Amb el protocol \"{protocol}\" \u00e9s necess\u00e0ria la vinculaci\u00f3. Introdueix el codi PIN que es mostra en pantalla. Els zeros a l'inici, si n'hi ha, s'han d'ometre; per exemple: introdueix 123 si el codi mostrat \u00e9s 0123.", "title": "Vinculaci\u00f3" }, + "password": { + "description": "`{protocol}` necessita una contrasenya. Encara no est\u00e0 suportada, desactiva la contrasenya per continuar.", + "title": "Contrasenya necess\u00e0ria" + }, + "protocol_disabled": { + "description": "La vinculaci\u00f3 \u00e9s necess\u00e0ria per a `{protocol}` per\u00f2 est\u00e0 desactivada al dispositiu. Revisa les possibles restriccions d'acc\u00e9s del dispositiu (per exemple, permet que tots els dispositius a la xarxa local es puguin connectar). \n\nPots continuar sense vincular aquest protocol, per\u00f2 algunes funcionalitats quedaran limitades.", + "title": "No es pot vincular" + }, "reconfigure": { - "description": "Aquesta Apple TV est\u00e0 tenint problemes de connexi\u00f3 i s'ha de tornar a configurar.", + "description": "Torna a configurar aquest dispositiu per restablir el seu funcionament.", "title": "Reconfiguraci\u00f3 de dispositiu" }, "service_problem": { @@ -45,7 +58,7 @@ "data": { "device_input": "Dispositiu" }, - "description": "Comen\u00e7a introduint el nom del dispositiu (per exemple, cuina o dormitori) o l'adre\u00e7a IP de l'Apple TV que vulguis afegir. Si autom\u00e0ticament es troben dispositius a la teva xarxa, es mostra a continuaci\u00f3. \n\n Si no veus el teu dispositiu o tens problemes, prova d'especificar l'adre\u00e7a IP del dispositiu. \n\n {devices}", + "description": "Comen\u00e7a introduint el nom del dispositiu (per exemple, cuina o dormitori) o l'adre\u00e7a IP de l'Apple TV que vulguis afegir.\n\n Si no veus el teu dispositiu o tens problemes, prova d'especificar l'adre\u00e7a IP del dispositiu.", "title": "Configuraci\u00f3 d'una nova Apple TV" } } diff --git a/homeassistant/components/apple_tv/translations/zh-Hant.json b/homeassistant/components/apple_tv/translations/zh-Hant.json index ced7a18d2a2..41732c0813e 100644 --- a/homeassistant/components/apple_tv/translations/zh-Hant.json +++ b/homeassistant/components/apple_tv/translations/zh-Hant.json @@ -1,12 +1,17 @@ { "config": { "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_configured_device": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "backoff": "\u88dd\u7f6e\u4e0d\u63a5\u53d7\u6b64\u6b21\u914d\u5c0d\u8acb\u6c42\uff08\u53ef\u80fd\u8f38\u5165\u592a\u591a\u6b21\u7121\u6548\u7684 PIN \u78bc\uff09\uff0c\u8acb\u7a0d\u5f8c\u518d\u8a66\u3002", "device_did_not_pair": "\u88dd\u7f6e\u6c92\u6709\u5617\u8a66\u914d\u5c0d\u5b8c\u6210\u904e\u7a0b\u3002", + "device_not_found": "\u641c\u5c0b\u4e0d\u5230\u88dd\u7f6e\u3001\u8acb\u8a66\u8457\u518d\u65b0\u589e\u4e00\u6b21\u3002", + "inconsistent_device": "\u641c\u5c0b\u4e0d\u5230\u9810\u671f\u7684\u901a\u8a0a\u5354\u5b9a\u3002\u901a\u5e38\u539f\u56e0\u70ba Multicast DNS (Zeroconf) \u554f\u984c\u3001\u8acb\u8a66\u8457\u518d\u65b0\u589e\u4e00\u6b21\u3002", "invalid_config": "\u6b64\u88dd\u7f6e\u8a2d\u5b9a\u4e0d\u5b8c\u6574\uff0c\u8acb\u7a0d\u5019\u518d\u8a66\u4e00\u6b21\u3002", "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", + "setup_failed": "\u88dd\u7f6e\u8a2d\u5b9a\u5931\u6557\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { @@ -16,14 +21,14 @@ "no_usable_service": "\u627e\u5230\u7684\u88dd\u7f6e\u7121\u6cd5\u8b58\u5225\u4ee5\u9032\u884c\u9023\u7dda\u3002\u5047\u5982\u6b64\u8a0a\u606f\u91cd\u8907\u767c\u751f\u3002\u8acb\u8a66\u8457\u6307\u5b9a\u7279\u5b9a IP \u4f4d\u5740\u6216\u91cd\u555f Apple TV\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, - "flow_title": "{name}", + "flow_title": "{name} ({type})", "step": { "confirm": { - "description": "\u6b63\u8981\u65b0\u589e\u540d\u70ba `{name}` \u7684 Apple TV \u81f3 Home Assistant\u3002\n\n**\u6b32\u5b8c\u6210\u6b65\u9a5f\uff0c\u5fc5\u9808\u8f38\u5165\u591a\u7d44 PIN \u78bc\u3002**\n\n\u8acb\u6ce8\u610f\uff1a\u6b64\u6574\u5408\u4e26 *\u7121\u6cd5* \u9032\u884c Apple TV \u95dc\u6a5f\u7684\u52d5\u4f5c\uff0c\u50c5\u80fd\u65bc Home Assistant \u4e2d\u95dc\u9589\u5a92\u9ad4\u64ad\u653e\u5668\u529f\u80fd\uff01", + "description": "\u6b63\u8981\u65b0\u589e\u540d\u70ba `{name}` \u7684 `{type}` \u81f3 Home Assistant\u3002\n\n**\u6b32\u5b8c\u6210\u6b65\u9a5f\uff0c\u5fc5\u9808\u8f38\u5165\u591a\u7d44 PIN \u78bc\u3002**\n\n\u8acb\u6ce8\u610f\uff1a\u6b64\u6574\u5408\u4e26 *\u7121\u6cd5* \u9032\u884c Apple TV \u95dc\u6a5f\u7684\u52d5\u4f5c\uff0c\u50c5\u80fd\u65bc Home Assistant \u4e2d\u95dc\u9589\u5a92\u9ad4\u64ad\u653e\u5668\u529f\u80fd\uff01", "title": "\u78ba\u8a8d\u65b0\u589e Apple TV" }, "pair_no_pin": { - "description": "`{protocol}` \u670d\u52d9\u9700\u8981\u9032\u884c\u914d\u5c0d\uff0c\u8acb\u8f38\u5165 Apple TV \u4e0a\u6240\u986f\u793a\u4e4b PIN {pin} \u4ee5\u7e7c\u7e8c\u3002", + "description": "`{protocol}` \u670d\u52d9\u9700\u8981\u9032\u884c\u914d\u5c0d\uff0c\u8acb\u8f38\u5165\u88dd\u7f6e\u4e0a\u6240\u986f\u793a\u4e4b PIN {pin} \u4ee5\u7e7c\u7e8c\u3002", "title": "\u914d\u5c0d\u4e2d" }, "pair_with_pin": { @@ -33,8 +38,16 @@ "description": "\u914d\u5c0d\u9700\u8981 `{protocol}` \u901a\u8a0a\u5354\u5b9a\u3002\u8acb\u8f38\u5165\u986f\u793a\u65bc\u756b\u9762\u4e0a\u7684 PIN \u78bc\uff0c\u524d\u65b9\u7684 0 \u53ef\u5ffd\u8996\u986f\u793a\u78bc\u70ba 0123\uff0c\u5247\u8f38\u5165 123\u3002", "title": "\u914d\u5c0d\u4e2d" }, + "password": { + "description": "`{protocol}` \u9700\u8981\u5bc6\u78bc\u65b9\u80fd\u9032\u884c\u3002\u4f46\u76ee\u524d\u4e26\u4e0d\u652f\u63f4\u6b64\u529f\u80fd\uff0c\u8acb\u95dc\u9589\u5f8c\u7e7c\u7e8c\u9032\u884c\u3002\u3002", + "title": "\u5fc5\u8981\u5bc6\u78bc" + }, + "protocol_disabled": { + "description": "\u914d\u5c0d\u9700\u8981 `{protocol}` \u958b\u555f\u65b9\u80fd\u9032\u884c\u3001\u4f46\u76ee\u524d\u70ba\u95dc\u9589\u72c0\u614b\u3002\u8acb\u67e5\u770b\u88dd\u5099\u4e0a\u7684\u5b58\u53d6\u9650\u5236\u8a2d\u5b9a\uff08\u4f8b\u5982\u5141\u8a31\u672c\u5730\u7aef\u7db2\u8def\u9023\u7dda\uff09\u3002\n\n\u53ef\u4ee5\u4e0d\u958b\u555f\u6b64\u5354\u5b9a\u7e7c\u7e8c\u914d\u5c0d\uff0c\u4f46\u90e8\u5206\u529f\u80fd\u5c07\u6703\u53d7\u5230\u9650\u5236\u3002", + "title": "\u7121\u6cd5\u914d\u5c0d" + }, "reconfigure": { - "description": "\u6b64 Apple TV \u906d\u9047\u5230\u4e00\u4e9b\u9023\u7dda\u554f\u984c\uff0c\u5fc5\u9808\u91cd\u65b0\u8a2d\u5b9a\u3002", + "description": "\u5fc5\u9808\u91cd\u65b0\u8a2d\u5b9a\u6b64\u88dd\u7f6e\u4ee5\u6062\u5fa9\u5176\u529f\u80fd\u3002", "title": "\u88dd\u7f6e\u91cd\u65b0\u8a2d\u5b9a" }, "service_problem": { @@ -45,7 +58,7 @@ "data": { "device_input": "\u88dd\u7f6e" }, - "description": "\u9996\u5148\u8f38\u5165\u6240\u8981\u65b0\u589e\u7684 Apple TV \u88dd\u7f6e\u540d\u7a31\uff08\u4f8b\u5982\u5eda\u623f\u6216\u81e5\u5ba4\uff09\u6216 IP \u4f4d\u5740\u3002\u5047\u5982\u65bc\u5340\u7db2\u4e0a\u627e\u5230\u4efb\u4f55\u88dd\u7f6e\uff0c\u5c07\u6703\u986f\u793a\u65bc\u4e0b\u65b9\u3002\n\n\u5047\u5982\u7121\u6cd5\u770b\u5230\u88dd\u7f6e\u6216\u906d\u9047\u4efb\u4f55\u554f\u984c\uff0c\u8acb\u8a66\u8457\u6307\u5b9a\u88dd\u7f6e\u7684 IP \u4f4d\u5740\u3002\n\n{devices}", + "description": "\u9996\u5148\u8f38\u5165\u6240\u8981\u65b0\u589e\u7684 Apple TV \u88dd\u7f6e\u540d\u7a31\uff08\u4f8b\u5982\u5eda\u623f\u6216\u81e5\u5ba4\uff09\u6216 IP \u4f4d\u5740\u3002\n\n\u5047\u5982\u7121\u6cd5\u770b\u5230\u88dd\u7f6e\u6216\u906d\u9047\u4efb\u4f55\u554f\u984c\uff0c\u8acb\u8a66\u8457\u6307\u5b9a\u88dd\u7f6e\u7684 IP \u4f4d\u5740\u3002", "title": "\u8a2d\u5b9a\u4e00\u7d44 Apple TV" } } diff --git a/homeassistant/components/aseko_pool_live/translations/ca.json b/homeassistant/components/aseko_pool_live/translations/ca.json new file mode 100644 index 00000000000..3451da0874e --- /dev/null +++ b/homeassistant/components/aseko_pool_live/translations/ca.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El compte ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "email": "Correu electr\u00f2nic", + "password": "Contrasenya" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/ca.json b/homeassistant/components/binary_sensor/translations/ca.json index 2aa89dfc134..a806bc3908e 100644 --- a/homeassistant/components/binary_sensor/translations/ca.json +++ b/homeassistant/components/binary_sensor/translations/ca.json @@ -84,6 +84,7 @@ "not_powered": "{entity_name} no est\u00e0 alimentat", "not_present": "{entity_name} no est\u00e0 present", "not_running": "{entity_name} para de funcionar", + "not_tampered": "{entity_name} deixa de detectar manipulaci\u00f3", "not_unsafe": "{entity_name} es torna segur", "occupied": "{entity_name} s'ocupa", "opened": "{entity_name} s'ha obert", @@ -94,6 +95,7 @@ "running": "{entity_name} comen\u00e7a a funcionar", "smoke": "{entity_name} ha comen\u00e7at a detectar fum", "sound": "{entity_name} ha comen\u00e7at a detectar so", + "tampered": "{entity_name} comen\u00e7a a detectar manipulaci\u00f3", "turned_off": "{entity_name} apagat", "turned_on": "{entity_name} enc\u00e8s", "unsafe": "{entity_name} es torna insegur", diff --git a/homeassistant/components/cloud/translations/de.json b/homeassistant/components/cloud/translations/de.json index 0b924c65428..20ac7ff0fab 100644 --- a/homeassistant/components/cloud/translations/de.json +++ b/homeassistant/components/cloud/translations/de.json @@ -7,7 +7,7 @@ "can_reach_cloud_auth": "Authentifizierungsserver erreichbar", "google_enabled": "Google aktiviert", "logged_in": "Angemeldet", - "relayer_connected": "Relay Verbunden", + "relayer_connected": "Relay verbunden", "remote_connected": "Remote verbunden", "remote_enabled": "Remote aktiviert", "remote_server": "Remote-Server", diff --git a/homeassistant/components/elmax/translations/bg.json b/homeassistant/components/elmax/translations/bg.json new file mode 100644 index 00000000000..58dfefd31cf --- /dev/null +++ b/homeassistant/components/elmax/translations/bg.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "bad_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "invalid_pin": "\u041f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0435\u043d\u0438\u044f\u0442 \u041f\u0418\u041d \u0435 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d", + "unknown_error": "\u0412\u044a\u0437\u043d\u0438\u043a\u043d\u0430 \u043d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "panels": { + "data": { + "panel_id": "ID \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0430", + "panel_name": "\u0418\u043c\u0435 \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0430", + "panel_pin": "\u041f\u0418\u041d \u043a\u043e\u0434" + } + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/ca.json b/homeassistant/components/elmax/translations/ca.json new file mode 100644 index 00000000000..c6be03cdac8 --- /dev/null +++ b/homeassistant/components/elmax/translations/ca.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "bad_auth": "Autenticaci\u00f3 inv\u00e0lida", + "invalid_pin": "El pin proporcionat no \u00e9s v\u00e0lid", + "network_error": "S'ha produ\u00eft un error de xarxa", + "no_panel_online": "No s'ha trobat cap panell de control d'Elmax en l\u00ednia.", + "unknown_error": "S'ha produ\u00eft un error desconegut" + }, + "step": { + "panels": { + "data": { + "panel_id": "ID del panell", + "panel_name": "Nom del panell", + "panel_pin": "Codi PIN" + }, + "description": "Selecciona quin panell vols controlar amb aquesta integraci\u00f3. Tingues en compte que el panell ha d'estar ON per poder ser configurat.", + "title": "Selecci\u00f3 del panell" + }, + "user": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "description": "Inicia sessi\u00f3 a Elmax Cloud amb les teves credencials", + "title": "Inici de sessi\u00f3" + } + } + }, + "title": "Configuraci\u00f3 d'Elmax Cloud" +} \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/de.json b/homeassistant/components/elmax/translations/de.json new file mode 100644 index 00000000000..aa66cae4b19 --- /dev/null +++ b/homeassistant/components/elmax/translations/de.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "bad_auth": "Ung\u00fcltige Authentifizierung", + "invalid_pin": "Die angegebene Pin ist ung\u00fcltig", + "network_error": "Ein Netzwerkfehler ist aufgetreten", + "no_panel_online": "Es wurde kein Elmax-Bedienfeld gefunden, das online ist.", + "unknown_error": "Ein unerwarteter Fehler ist aufgetreten" + }, + "step": { + "panels": { + "data": { + "panel_id": "Panel-ID", + "panel_name": "Panel-Name", + "panel_pin": "PIN-Code" + }, + "description": "W\u00e4hle die Zentrale aus, die du mit dieser Integration steuern m\u00f6chtest. Bitte beachte, dass die Zentrale eingeschaltet sein muss, damit sie konfiguriert werden kann.", + "title": "Panelauswahl" + }, + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername" + }, + "description": "Bitte melde dich mit deinen Zugangsdaten bei der Elmax-Cloud an", + "title": "Konto-Anmeldung" + } + } + }, + "title": "Elmax Cloud-Einrichtung" +} \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/et.json b/homeassistant/components/elmax/translations/et.json new file mode 100644 index 00000000000..333cdc8d11f --- /dev/null +++ b/homeassistant/components/elmax/translations/et.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "bad_auth": "Vigane autentimine", + "invalid_pin": "Sisestatud pin on kehtetu", + "network_error": "Ilmnes v\u00f5rgut\u00f5rge", + "no_panel_online": "V\u00f5rgus olevat Elmaxi juhtpaneeli ei leitud.", + "unknown_error": "Ilmnes ootamatu t\u00f5rge" + }, + "step": { + "panels": { + "data": { + "panel_id": "Paneeli ID", + "panel_name": "Paneeli nimi", + "panel_pin": "PIN kood" + }, + "description": "Vali millist paneeli soovid selle sidumisega juhtida. Pane t\u00e4hele, et paneel peab seadistamiseks olema SEES.", + "title": "Paneeli valik" + }, + "user": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + }, + "description": "Logi oma mandaate kasutades Elmaxi pilve sisse", + "title": "Kontole sisselogimine" + } + } + }, + "title": "Elmaxi pilve seadistamine" +} \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/hu.json b/homeassistant/components/elmax/translations/hu.json new file mode 100644 index 00000000000..620396b0de0 --- /dev/null +++ b/homeassistant/components/elmax/translations/hu.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "bad_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "invalid_pin": "A megadott PIN-k\u00f3d \u00e9rv\u00e9nytelen", + "network_error": "H\u00e1l\u00f3zati hiba t\u00f6rt\u00e9nt", + "no_panel_online": "Nem tal\u00e1lhat\u00f3 online Elmax vez\u00e9rl\u0151panel.", + "unknown_error": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "panels": { + "data": { + "panel_id": "Panel ID", + "panel_name": "Panel neve", + "panel_pin": "PIN-k\u00f3d" + }, + "description": "V\u00e1lassza ki, hogy melyik panelt szeretn\u00e9 vez\u00e9relni ezzel az integr\u00e1ci\u00f3val. A panelnek bekapcsolt \u00e1llapotban kell lennie a konfigur\u00e1l\u00e1shoz.", + "title": "Panel kiv\u00e1laszt\u00e1sa" + }, + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "description": "K\u00e9rem, jelentkezzen be az Elmax felh\u0151be a hiteles\u00edt\u0151 adataival", + "title": "Fi\u00f3k bejelentkez\u00e9s" + } + } + }, + "title": "Elmax Cloud be\u00e1ll\u00edt\u00e1sa" +} \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/ja.json b/homeassistant/components/elmax/translations/ja.json new file mode 100644 index 00000000000..639f7346254 --- /dev/null +++ b/homeassistant/components/elmax/translations/ja.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "bad_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "invalid_pin": "\u63d0\u4f9b\u3055\u308c\u305f\u30d4\u30f3\u304c\u7121\u52b9\u3067\u3059", + "network_error": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f", + "no_panel_online": "\u30aa\u30f3\u30e9\u30a4\u30f3\u306eElmax\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u30d1\u30cd\u30eb\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002", + "unknown_error": "\u4e88\u671f\u305b\u306c\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f" + }, + "step": { + "panels": { + "data": { + "panel_id": "\u30d1\u30cd\u30ebID", + "panel_name": "\u30d1\u30cd\u30eb\u540d", + "panel_pin": "PIN\u30b3\u30fc\u30c9" + }, + "description": "\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u5236\u5fa1\u3059\u308b\u30d1\u30cd\u30eb\u3092\u9078\u629e\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u3059\u308b\u306b\u306f\u3001\u30d1\u30cd\u30eb\u304c\u30aa\u30f3\u306b\u306a\u3063\u3066\u3044\u308b\u5fc5\u8981\u304c\u3042\u308b\u3053\u3068\u306b\u6ce8\u610f\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "\u30d1\u30cd\u30eb\u306e\u9078\u629e" + }, + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + }, + "description": "\u3042\u306a\u305f\u306e\u8a8d\u8a3c\u60c5\u5831\u3092\u4f7f\u7528\u3057\u3066\u3001Elmax\u30af\u30e9\u30a6\u30c9\u306b\u30ed\u30b0\u30a4\u30f3\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "\u30a2\u30ab\u30a6\u30f3\u30c8 \u30ed\u30b0\u30a4\u30f3" + } + } + }, + "title": "Elmax Cloud\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" +} \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/nl.json b/homeassistant/components/elmax/translations/nl.json new file mode 100644 index 00000000000..b0ce43d9351 --- /dev/null +++ b/homeassistant/components/elmax/translations/nl.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "bad_auth": "Ongeldige authenticatie", + "invalid_pin": "De opgegeven pincode is ongeldig", + "network_error": "Er is een netwerkfout opgetreden", + "no_panel_online": "Er is geen online Elmax-controlepaneel gevonden.", + "unknown_error": "Een onbekende fout is opgetreden." + }, + "step": { + "panels": { + "data": { + "panel_id": "Paneel-ID", + "panel_name": "Paneelnaam:", + "panel_pin": "PIN Code" + }, + "description": "Selecteer welk paneel je wilt bedienen met deze integratie. Houd er rekening mee dat het paneel AAN moet staan om te kunnen worden geconfigureerd.", + "title": "Paneelselectie" + }, + "user": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + }, + "description": "Log in op de Elmax-cloud met uw inloggegevens", + "title": "Account Login" + } + } + }, + "title": "Elmax Cloud Setup" +} \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/no.json b/homeassistant/components/elmax/translations/no.json new file mode 100644 index 00000000000..bc15c853aa4 --- /dev/null +++ b/homeassistant/components/elmax/translations/no.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "bad_auth": "Ugyldig godkjenning", + "invalid_pin": "Den angitte PIN-koden er ugyldig", + "network_error": "Det oppstod en nettverksfeil", + "no_panel_online": "Ingen online Elmax kontrollpanel ble funnet.", + "unknown_error": "En uventet feil oppstod" + }, + "step": { + "panels": { + "data": { + "panel_id": "Panel-ID", + "panel_name": "Navn p\u00e5 panel", + "panel_pin": "PIN kode" + }, + "description": "Velg hvilket panel du vil kontrollere med denne integrasjonen. V\u00e6r oppmerksom p\u00e5 at panelet m\u00e5 v\u00e6re P\u00c5 for \u00e5 kunne konfigureres.", + "title": "Valg av panel" + }, + "user": { + "data": { + "password": "Passord", + "username": "Brukernavn" + }, + "description": "Logg p\u00e5 Elmax-skyen ved \u00e5 bruke legitimasjonen din", + "title": "P\u00e5logging til konto" + } + } + }, + "title": "Elmax Cloud Setup" +} \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/ru.json b/homeassistant/components/elmax/translations/ru.json new file mode 100644 index 00000000000..70cd14bac9c --- /dev/null +++ b/homeassistant/components/elmax/translations/ru.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "bad_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "invalid_pin": "\u041f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u043d\u044b\u0439 PIN-\u043a\u043e\u0434 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d.", + "network_error": "\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u0432 \u0441\u0435\u0442\u0438.", + "no_panel_online": "\u041f\u0430\u043d\u0435\u043b\u044c \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f Elmax \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u0430.", + "unknown_error": "\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "panels": { + "data": { + "panel_id": "ID \u043f\u0430\u043d\u0435\u043b\u0438", + "panel_name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u043f\u0430\u043d\u0435\u043b\u0438", + "panel_pin": "PIN-\u043a\u043e\u0434" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u043d\u0435\u043b\u044c, \u043a\u043e\u0442\u043e\u0440\u043e\u0439 \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0443\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u044d\u0442\u043e\u0439 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438. \u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0447\u0442\u043e \u0432\u043e \u0432\u0440\u0435\u043c\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u0430\u043d\u0435\u043b\u044c \u0434\u043e\u043b\u0436\u043d\u0430 \u0431\u044b\u0442\u044c \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0430.", + "title": "\u0412\u044b\u0431\u043e\u0440 \u043f\u0430\u043d\u0435\u043b\u0438" + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "description": "\u0412\u043e\u0439\u0434\u0438\u0442\u0435 \u0432 Elmax Cloud, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0441\u0432\u043e\u0438 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", + "title": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f" + } + } + }, + "title": "Elmax Cloud" +} \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/zh-Hant.json b/homeassistant/components/elmax/translations/zh-Hant.json new file mode 100644 index 00000000000..4012a5b55ad --- /dev/null +++ b/homeassistant/components/elmax/translations/zh-Hant.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "bad_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "invalid_pin": "\u6240\u63d0\u4f9b\u7684 PIN \u7121\u6548\u3002", + "network_error": "\u767c\u751f\u7db2\u8def\u932f\u8aa4", + "no_panel_online": "\u627e\u4e0d\u5230 Elmax \u63a7\u5236\u9762\u677f\u3002", + "unknown_error": "\u767c\u751f\u672a\u77e5\u932f\u8aa4" + }, + "step": { + "panels": { + "data": { + "panel_id": "\u9762\u677f ID", + "panel_name": "\u9762\u677f\u540d\u7a31", + "panel_pin": "PIN \u78bc" + }, + "description": "\u9078\u64c7\u6574\u5408\u6240\u8981\u4f7f\u7528\u7684\u9762\u677f\u3002\u8acb\u6ce8\u610f\u3001\u9762\u677f\u5fc5\u9808\u70ba\u958b\u555f\u72c0\u614b\u65b9\u80fd\u9032\u884c\u8a2d\u5b9a\u3002", + "title": "\u9078\u64c7\u9762\u677f" + }, + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u8acb\u4f7f\u7528\u6191\u8b49\u4ee5\u767b\u5165 Elmax \u96f2\u670d\u52d9", + "title": "\u767b\u5165\u5e33\u865f" + } + } + }, + "title": "Elmax \u96f2\u670d\u52d9\u8a2d\u5b9a" +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/ca.json b/homeassistant/components/enphase_envoy/translations/ca.json index dad1f8903ba..b1f88de9b96 100644 --- a/homeassistant/components/enphase_envoy/translations/ca.json +++ b/homeassistant/components/enphase_envoy/translations/ca.json @@ -16,7 +16,8 @@ "host": "Amfitri\u00f3", "password": "Contrasenya", "username": "Nom d'usuari" - } + }, + "description": "Per als models m\u00e9s nous, introdueix el nom d'usuari `envoy` sense contrasenya. Per als models m\u00e9s antics, introdueix el nom d'usuari `installer` sense contrasenya. Per a la resta de models, introdueix un nom d'usuari i una contrasenya v\u00e0lids." } } } diff --git a/homeassistant/components/fronius/translations/ca.json b/homeassistant/components/fronius/translations/ca.json index 3bc7a2cea27..1c543140627 100644 --- a/homeassistant/components/fronius/translations/ca.json +++ b/homeassistant/components/fronius/translations/ca.json @@ -1,13 +1,18 @@ { "config": { "abort": { - "already_configured": "El dispositiu ja est\u00e0 configurat" + "already_configured": "El dispositiu ja est\u00e0 configurat", + "invalid_host": "Nom de l'amfitri\u00f3 o adre\u00e7a IP inv\u00e0lids" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "unknown": "Error inesperat" }, + "flow_title": "{device}", "step": { + "confirm_discovery": { + "description": "Vols afegir {device} a Home Assistant?" + }, "user": { "data": { "host": "Amfitri\u00f3" diff --git a/homeassistant/components/knx/translations/ca.json b/homeassistant/components/knx/translations/ca.json index fef4093de94..031d4652373 100644 --- a/homeassistant/components/knx/translations/ca.json +++ b/homeassistant/components/knx/translations/ca.json @@ -12,6 +12,7 @@ "data": { "host": "Amfitri\u00f3", "individual_address": "Adre\u00e7a individual de la connexi\u00f3", + "local_ip": "IP local (deixa-ho en blanc si no n'est\u00e0s segur/a)", "port": "Port", "route_back": "Encaminament de retorn / Mode NAT" }, @@ -54,6 +55,7 @@ "tunnel": { "data": { "host": "Amfitri\u00f3", + "local_ip": "IP local (deixa-ho en blanc si no n'est\u00e0s segur/a)", "port": "Port", "route_back": "Encaminament de retorn / Mode NAT" } diff --git a/homeassistant/components/nina/translations/ca.json b/homeassistant/components/nina/translations/ca.json new file mode 100644 index 00000000000..41d274d779d --- /dev/null +++ b/homeassistant/components/nina/translations/ca.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "no_selection": "Seleccioneu almenys una ciutat o comtat", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "_a_to_d": "Ciutat/comtat (A-D)", + "_e_to_h": "Ciutat/comtat (E-H)", + "_i_to_l": "Ciutat/comtat (I-L)", + "_m_to_q": "Ciutat/comtat (M-Q)", + "_r_to_u": "Ciutat/comtat (R-U)", + "_v_to_z": "Ciutat/comtat (V-Z)", + "corona_filter": "Elimina els avisos de corona", + "slots": "Nombre d'avisos m\u00e0xims per ciutat/comtat" + }, + "title": "Selecciona ciutat/comtat" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/select.bg.json b/homeassistant/components/yamaha_musiccast/translations/select.bg.json index 3d6f452fc29..cee1a2a5a7c 100644 --- a/homeassistant/components/yamaha_musiccast/translations/select.bg.json +++ b/homeassistant/components/yamaha_musiccast/translations/select.bg.json @@ -5,8 +5,12 @@ }, "yamaha_musiccast__zone_equalizer_mode": { "auto": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e", + "bypass": "\u0411\u0430\u0439\u043f\u0430\u0441", "manual": "\u0420\u044a\u0447\u043d\u043e" }, + "yamaha_musiccast__zone_link_control": { + "speed": "\u0421\u043a\u043e\u0440\u043e\u0441\u0442" + }, "yamaha_musiccast__zone_sleep": { "120 min": "120 \u043c\u0438\u043d\u0443\u0442\u0438", "30 min": "30 \u043c\u0438\u043d\u0443\u0442\u0438", @@ -27,6 +31,7 @@ }, "yamaha_musiccast__zone_tone_control_mode": { "auto": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e", + "bypass": "\u0411\u0430\u0439\u043f\u0430\u0441", "manual": "\u0420\u044a\u0447\u043d\u043e" } } diff --git a/homeassistant/components/yamaha_musiccast/translations/select.ca.json b/homeassistant/components/yamaha_musiccast/translations/select.ca.json new file mode 100644 index 00000000000..cca1e3dd75a --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/select.ca.json @@ -0,0 +1,52 @@ +{ + "state": { + "yamaha_musiccast__dimmer": { + "auto": "Auto" + }, + "yamaha_musiccast__zone_equalizer_mode": { + "auto": "Auto", + "bypass": "Pont", + "manual": "Manual" + }, + "yamaha_musiccast__zone_link_audio_delay": { + "audio_sync": "Sincronitzaci\u00f3 d'\u00e0udio", + "audio_sync_off": "Sincronitzaci\u00f3 d'\u00e0udio OFF", + "audio_sync_on": "Sincronitzaci\u00f3 d'\u00e0udio ON", + "balanced": "Equilibrat", + "lip_sync": "Sincronitzaci\u00f3 Lip" + }, + "yamaha_musiccast__zone_link_audio_quality": { + "compressed": "Amb compressi\u00f3", + "uncompressed": "Sense compressi\u00f3" + }, + "yamaha_musiccast__zone_link_control": { + "speed": "Velocitat", + "stability": "Estabilitat", + "standard": "Est\u00e0ndard" + }, + "yamaha_musiccast__zone_sleep": { + "120 min": "120 minuts", + "30 min": "30 minuts", + "60 min": "60 minuts", + "90 min": "90 minuts", + "off": "OFF" + }, + "yamaha_musiccast__zone_surr_decoder_type": { + "auto": "Auto", + "dolby_pl": "Dolby ProLogic", + "dolby_pl2x_game": "Dolby ProLogic 2x Game", + "dolby_pl2x_movie": "Dolby ProLogic 2x Movie", + "dolby_pl2x_music": "Dolby ProLogic 2x Music", + "dolby_surround": "Dolby Surround", + "dts_neo6_cinema": "DTS Neo:6 Cinema", + "dts_neo6_music": "DTS Neo:6 Music", + "dts_neural_x": "DTS Neural:X", + "toggle": "Commuta" + }, + "yamaha_musiccast__zone_tone_control_mode": { + "auto": "Auto", + "bypass": "Pont", + "manual": "Manual" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/select.zh-Hant.json b/homeassistant/components/yamaha_musiccast/translations/select.zh-Hant.json new file mode 100644 index 00000000000..b7d9051069e --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/select.zh-Hant.json @@ -0,0 +1,52 @@ +{ + "state": { + "yamaha_musiccast__dimmer": { + "auto": "\u81ea\u52d5" + }, + "yamaha_musiccast__zone_equalizer_mode": { + "auto": "\u81ea\u52d5", + "bypass": "\u5ffd\u7565", + "manual": "\u624b\u52d5" + }, + "yamaha_musiccast__zone_link_audio_delay": { + "audio_sync": "\u97f3\u6548\u540c\u6b65", + "audio_sync_off": "\u97f3\u6548\u540c\u6b65\u95dc\u9589", + "audio_sync_on": "\u97f3\u6548\u540c\u6b65\u958b\u555f", + "balanced": "\u5e73\u8861", + "lip_sync": "\u5507\u5f62\u540c\u6b65" + }, + "yamaha_musiccast__zone_link_audio_quality": { + "compressed": "\u58d3\u7e2e", + "uncompressed": "\u672a\u58d3\u7e2e" + }, + "yamaha_musiccast__zone_link_control": { + "speed": "\u6548\u80fd", + "stability": "\u7a69\u5b9a", + "standard": "\u6a19\u6e96" + }, + "yamaha_musiccast__zone_sleep": { + "120 min": "120 \u5206\u9418", + "30 min": "30 \u5206\u9418", + "60 min": "60 \u5206\u9418", + "90 min": "90 \u5206\u9418", + "off": "\u95dc\u9589" + }, + "yamaha_musiccast__zone_surr_decoder_type": { + "auto": "\u81ea\u52d5", + "dolby_pl": "Dolby ProLogic", + "dolby_pl2x_game": "Dolby ProLogic 2x \u904a\u6232\u6a21\u5f0f", + "dolby_pl2x_movie": "Dolby ProLogic 2x \u96fb\u5f71\u6a21\u5f0f", + "dolby_pl2x_music": "Dolby ProLogic 2x \u97f3\u6a02\u6a21\u5f0f", + "dolby_surround": "Dolby \u74b0\u7e5e\u97f3\u6548", + "dts_neo6_cinema": "DTS Neo:6 \u5287\u5834\u6a21\u5f0f", + "dts_neo6_music": "DTS Neo:6 \u97f3\u6a02\u6a21\u5f0f", + "dts_neural_x": "DTS Neural:X", + "toggle": "\u958b\u95dc" + }, + "yamaha_musiccast__zone_tone_control_mode": { + "auto": "\u81ea\u52d5", + "bypass": "\u5ffd\u7565", + "manual": "\u624b\u52d5" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/ca.json b/homeassistant/components/zwave_js/translations/ca.json index d0861d44f89..5e0f834ef19 100644 --- a/homeassistant/components/zwave_js/translations/ca.json +++ b/homeassistant/components/zwave_js/translations/ca.json @@ -33,6 +33,7 @@ "s2_unauthenticated_key": "Clau d'S2 no autenticat", "usb_path": "Ruta del dispositiu USB" }, + "description": "El complement generar\u00e0 claus de seguretat si aquests camps es deixen buits.", "title": "Introdueix la configuraci\u00f3 del complement Z-Wave JS" }, "hassio_confirm": { @@ -119,6 +120,7 @@ "s2_unauthenticated_key": "Clau d'S2 no autenticat", "usb_path": "Ruta del dispositiu USB" }, + "description": "El complement generar\u00e0 claus de seguretat si aquests camps es deixen buits.", "title": "Introdueix la configuraci\u00f3 del complement Z-Wave JS" }, "install_addon": { diff --git a/homeassistant/components/zwave_js/translations/zh-Hant.json b/homeassistant/components/zwave_js/translations/zh-Hant.json index 7b495ed0ca0..5088766395f 100644 --- a/homeassistant/components/zwave_js/translations/zh-Hant.json +++ b/homeassistant/components/zwave_js/translations/zh-Hant.json @@ -33,6 +33,7 @@ "s2_unauthenticated_key": "S2 \u672a\u9a57\u8b49\u5bc6\u9470", "usb_path": "USB \u88dd\u7f6e\u8def\u5f91" }, + "description": "\u5047\u5982\u6b04\u4f4d\u4fdd\u6301\u7a7a\u767d\u3001\u9644\u52a0\u5143\u4ef6\u5c07\u6703\u7522\u751f\u4e00\u7d44\u5b89\u5168\u5bc6\u9470\u3002", "title": "\u8f38\u5165 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u8a2d\u5b9a" }, "hassio_confirm": { @@ -119,6 +120,7 @@ "s2_unauthenticated_key": "S2 \u672a\u9a57\u8b49\u5bc6\u9470", "usb_path": "USB \u88dd\u7f6e\u8def\u5f91" }, + "description": "\u5047\u5982\u6b04\u4f4d\u4fdd\u6301\u7a7a\u767d\u3001\u9644\u52a0\u5143\u4ef6\u5c07\u6703\u7522\u751f\u4e00\u7d44\u5b89\u5168\u5bc6\u9470\u3002", "title": "\u8f38\u5165 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u8a2d\u5b9a" }, "install_addon": { From a13ae85982084632df2f6d75031574a9d4a8716c Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Thu, 9 Dec 2021 01:49:35 +0100 Subject: [PATCH 0204/2644] Introduce only_supervisor for @websocket_api.ws_require_user() (#61298) --- homeassistant/components/hassio/__init__.py | 5 ++-- .../components/recorder/websocket_api.py | 4 ++-- .../components/websocket_api/decorators.py | 6 +++++ homeassistant/const.py | 3 +++ .../components/recorder/test_websocket_api.py | 20 +++++++++------- .../websocket_api/test_decorators.py | 23 +++++++++++++++++++ tests/conftest.py | 22 +++++++++++++++++- 7 files changed, 70 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 614ea928828..78927d2f322 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -20,6 +20,7 @@ from homeassistant.const import ( ATTR_MANUFACTURER, ATTR_NAME, EVENT_CORE_CONFIG_UPDATE, + HASSIO_USER_NAME, SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP, Platform, @@ -440,11 +441,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: # Migrate old name if user.name == "Hass.io": - await hass.auth.async_update_user(user, name="Supervisor") + await hass.auth.async_update_user(user, name=HASSIO_USER_NAME) if refresh_token is None: user = await hass.auth.async_create_system_user( - "Supervisor", group_ids=[GROUP_ID_ADMIN] + HASSIO_USER_NAME, group_ids=[GROUP_ID_ADMIN] ) refresh_token = await hass.auth.async_create_refresh_token(user) data["hassio_user"] = user.id diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index f6d4d57a7e5..aec7905615f 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -113,7 +113,7 @@ def ws_info( connection.send_result(msg["id"], recorder_info) -@websocket_api.require_admin +@websocket_api.ws_require_user(only_supervisor=True) @websocket_api.websocket_command({vol.Required("type"): "backup/start"}) @websocket_api.async_response async def ws_backup_start( @@ -131,7 +131,7 @@ async def ws_backup_start( connection.send_result(msg["id"]) -@websocket_api.require_admin +@websocket_api.ws_require_user(only_supervisor=True) @websocket_api.websocket_command({vol.Required("type"): "backup/end"}) @websocket_api.async_response async def ws_backup_end( diff --git a/homeassistant/components/websocket_api/decorators.py b/homeassistant/components/websocket_api/decorators.py index eff82a8c71d..296271c7cfd 100644 --- a/homeassistant/components/websocket_api/decorators.py +++ b/homeassistant/components/websocket_api/decorators.py @@ -8,6 +8,7 @@ from typing import Any import voluptuous as vol +from homeassistant.const import HASSIO_USER_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import Unauthorized @@ -70,6 +71,7 @@ def ws_require_user( allow_system_user: bool = True, only_active_user: bool = True, only_inactive_user: bool = False, + only_supervisor: bool = False, ) -> Callable[[const.WebSocketCommandHandler], const.WebSocketCommandHandler]: """Decorate function validating login user exist in current WS connection. @@ -111,6 +113,10 @@ def ws_require_user( output_error("only_inactive_user", "Not allowed as active user") return + if only_supervisor and connection.user.name != HASSIO_USER_NAME: + output_error("only_supervisor", "Only allowed as Supervisor") + return + return func(hass, connection, msg) return check_current_user diff --git a/homeassistant/const.py b/homeassistant/const.py index c4e90dc2d57..cb33c20b14b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -756,3 +756,6 @@ ENTITY_CATEGORIES: Final[list[str]] = [ CAST_APP_ID_HOMEASSISTANT_MEDIA: Final = "B45F4572" # The ID of the Home Assistant Lovelace Cast App CAST_APP_ID_HOMEASSISTANT_LOVELACE: Final = "A078F6B0" + +# User used by Supervisor +HASSIO_USER_NAME = "Supervisor" diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 994d1c677af..2a9f737e9a5 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -360,9 +360,11 @@ async def test_recorder_info_migration_queue_exhausted(hass, hass_ws_client): assert response["result"]["thread_running"] is True -async def test_backup_start_no_recorder(hass, hass_ws_client): +async def test_backup_start_no_recorder( + hass, hass_ws_client, hass_supervisor_access_token +): """Test getting backup start when recorder is not present.""" - client = await hass_ws_client() + client = await hass_ws_client(hass, hass_supervisor_access_token) await client.send_json({"id": 1, "type": "backup/start"}) response = await client.receive_json() @@ -370,9 +372,9 @@ async def test_backup_start_no_recorder(hass, hass_ws_client): assert response["error"]["code"] == "unknown_command" -async def test_backup_start_timeout(hass, hass_ws_client): +async def test_backup_start_timeout(hass, hass_ws_client, hass_supervisor_access_token): """Test getting backup start when recorder is not present.""" - client = await hass_ws_client() + client = await hass_ws_client(hass, hass_supervisor_access_token) await async_init_recorder_component(hass) # Ensure there are no queued events @@ -388,9 +390,9 @@ async def test_backup_start_timeout(hass, hass_ws_client): await client.send_json({"id": 2, "type": "backup/end"}) -async def test_backup_end(hass, hass_ws_client): +async def test_backup_end(hass, hass_ws_client, hass_supervisor_access_token): """Test backup start.""" - client = await hass_ws_client() + client = await hass_ws_client(hass, hass_supervisor_access_token) await async_init_recorder_component(hass) # Ensure there are no queued events @@ -405,9 +407,11 @@ async def test_backup_end(hass, hass_ws_client): assert response["success"] -async def test_backup_end_without_start(hass, hass_ws_client): +async def test_backup_end_without_start( + hass, hass_ws_client, hass_supervisor_access_token +): """Test backup start.""" - client = await hass_ws_client() + client = await hass_ws_client(hass, hass_supervisor_access_token) await async_init_recorder_component(hass) # Ensure there are no queued events diff --git a/tests/components/websocket_api/test_decorators.py b/tests/components/websocket_api/test_decorators.py index 45d761f6fed..4fbc1ae1a21 100644 --- a/tests/components/websocket_api/test_decorators.py +++ b/tests/components/websocket_api/test_decorators.py @@ -66,3 +66,26 @@ async def test_async_response_request_context(hass, websocket_client): assert msg["id"] == 7 assert not msg["success"] assert msg["error"]["code"] == "not_found" + + +async def test_supervisor_only(hass, websocket_client): + """Test that only the Supervisor can make requests.""" + + @websocket_api.ws_require_user(only_supervisor=True) + @websocket_api.websocket_command({"type": "test-require-supervisor-user"}) + def require_supervisor_request(hass, connection, msg): + connection.send_result(msg["id"]) + + websocket_api.async_register_command(hass, require_supervisor_request) + + await websocket_client.send_json( + { + "id": 5, + "type": "test-require-supervisor-user", + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert not msg["success"] + assert msg["error"]["code"] == "only_supervisor" diff --git a/tests/conftest.py b/tests/conftest.py index 88651a0ec3f..56be04edeeb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -26,7 +26,7 @@ from homeassistant.components.websocket_api.auth import ( TYPE_AUTH_REQUIRED, ) from homeassistant.components.websocket_api.http import URL -from homeassistant.const import ATTR_NOW, EVENT_TIME_CHANGED +from homeassistant.const import ATTR_NOW, EVENT_TIME_CHANGED, HASSIO_USER_NAME from homeassistant.helpers import config_entry_oauth2_flow, event from homeassistant.setup import async_setup_component from homeassistant.util import location @@ -405,6 +405,26 @@ def hass_read_only_access_token(hass, hass_read_only_user, local_auth): return hass.auth.async_create_access_token(refresh_token) +@pytest.fixture +def hass_supervisor_user(hass, local_auth): + """Return the Home Assistant Supervisor user.""" + admin_group = hass.loop.run_until_complete( + hass.auth.async_get_group(GROUP_ID_ADMIN) + ) + return MockUser( + name=HASSIO_USER_NAME, groups=[admin_group], system_generated=True + ).add_to_hass(hass) + + +@pytest.fixture +def hass_supervisor_access_token(hass, hass_supervisor_user, local_auth): + """Return a Home Assistant Supervisor access token.""" + refresh_token = hass.loop.run_until_complete( + hass.auth.async_create_refresh_token(hass_supervisor_user) + ) + return hass.auth.async_create_access_token(refresh_token) + + @pytest.fixture def legacy_auth(hass): """Load legacy API password provider.""" From 9ffa3b21f679d86352b6e41ff150ebba48c9add2 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 8 Dec 2021 21:49:40 -0800 Subject: [PATCH 0205/2644] Display nest media events using local time (#61143) --- homeassistant/components/nest/media_source.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/nest/media_source.py b/homeassistant/components/nest/media_source.py index 8fd7d384e36..b02b9b1870e 100644 --- a/homeassistant/components/nest/media_source.py +++ b/homeassistant/components/nest/media_source.py @@ -46,6 +46,7 @@ from homeassistant.components.nest.device_info import NestDeviceInfo from homeassistant.components.nest.events import MEDIA_SOURCE_EVENT_TITLE_MAP from homeassistant.core import HomeAssistant from homeassistant.helpers.template import DATE_STR_FORMAT +from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -250,7 +251,7 @@ def _browse_event( media_content_type=MEDIA_TYPE_IMAGE, title=CLIP_TITLE_FORMAT.format( event_name=MEDIA_SOURCE_EVENT_TITLE_MAP.get(event.event_type, "Event"), - event_time=event.timestamp.strftime(DATE_STR_FORMAT), + event_time=dt_util.as_local(event.timestamp).strftime(DATE_STR_FORMAT), ), can_play=True, can_expand=False, From 4933189ad94652a0dec7de7f865e3e7e78c1180a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 8 Dec 2021 20:21:11 -1000 Subject: [PATCH 0206/2644] Fix lookin failing to setup during firmware updates (#61305) --- homeassistant/components/lookin/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lookin/__init__.py b/homeassistant/components/lookin/__init__.py index f749621beaf..5e603027a50 100644 --- a/homeassistant/components/lookin/__init__.py +++ b/homeassistant/components/lookin/__init__.py @@ -1,6 +1,7 @@ """The lookin integration.""" from __future__ import annotations +import asyncio from datetime import timedelta import logging @@ -37,7 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: lookin_device = await lookin_protocol.get_info() devices = await lookin_protocol.get_devices() - except aiohttp.ClientError as ex: + except (asyncio.TimeoutError, aiohttp.ClientError) as ex: raise ConfigEntryNotReady from ex meteo_coordinator: DataUpdateCoordinator = DataUpdateCoordinator( From 8f238d6b5304f2091af5e6dad38716be9c4ef2fd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 8 Dec 2021 22:36:41 -0800 Subject: [PATCH 0207/2644] Fix rova timezone (#61302) --- homeassistant/components/rova/sensor.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rova/sensor.py b/homeassistant/components/rova/sensor.py index ca9f201b302..60d0fbe6df0 100644 --- a/homeassistant/components/rova/sensor.py +++ b/homeassistant/components/rova/sensor.py @@ -20,6 +20,7 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle +from homeassistant.util.dt import get_time_zone, now # Config for rova requests. CONF_ZIP_CODE = "zip_code" @@ -140,10 +141,12 @@ class RovaData: self.data = {} for item in items: - date = datetime.strptime(item["Date"], "%Y-%m-%dT%H:%M:%S") + date = datetime.strptime(item["Date"], "%Y-%m-%dT%H:%M:%S").replace( + tzinfo=get_time_zone("Europe/Amsterdam") + ) code = item["GarbageTypeCode"].lower() - if code not in self.data and date > datetime.now(): + if code not in self.data and date > now(): self.data[code] = date _LOGGER.debug("Updated Rova calendar: %s", self.data) From 2e659232050facf5618a8d233c3c4b859cef8c40 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 8 Dec 2021 22:50:30 -0800 Subject: [PATCH 0208/2644] Fix CO2signal error handling (#61311) --- homeassistant/components/co2signal/__init__.py | 3 --- homeassistant/components/co2signal/config_flow.py | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/co2signal/__init__.py b/homeassistant/components/co2signal/__init__.py index a87aed64564..56be5bca57b 100644 --- a/homeassistant/components/co2signal/__init__.py +++ b/homeassistant/components/co2signal/__init__.py @@ -134,9 +134,6 @@ def get_data(hass: HomeAssistant, config: dict) -> CO2SignalResponse: _LOGGER.exception("Unexpected exception") raise UnknownError from err - except Exception as err: - _LOGGER.exception("Unexpected exception") - raise UnknownError from err else: if "error" in data: diff --git a/homeassistant/components/co2signal/config_flow.py b/homeassistant/components/co2signal/config_flow.py index e7f94e4d603..fb4e48c66e8 100644 --- a/homeassistant/components/co2signal/config_flow.py +++ b/homeassistant/components/co2signal/config_flow.py @@ -10,7 +10,7 @@ from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CON from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv -from . import APIRatelimitExceeded, CO2Error, InvalidAuth, UnknownError, get_data +from . import APIRatelimitExceeded, CO2Error, InvalidAuth, get_data from .const import CONF_COUNTRY_CODE, DOMAIN from .util import get_extra_name @@ -172,7 +172,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "invalid_auth" except APIRatelimitExceeded: errors["base"] = "api_ratelimit" - except UnknownError: + except Exception: # pylint: disable=broad-except errors["base"] = "unknown" else: return self.async_create_entry( From a63900a5a88aad59c2a359a6aeeee2fb90a2af72 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 9 Dec 2021 08:26:37 +0100 Subject: [PATCH 0209/2644] Use new CoverDeviceClass in bond (#61322) Co-authored-by: epenet --- homeassistant/components/bond/cover.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bond/cover.py b/homeassistant/components/bond/cover.py index 2c5f0fe66c3..57be80c0fd7 100644 --- a/homeassistant/components/bond/cover.py +++ b/homeassistant/components/bond/cover.py @@ -6,13 +6,13 @@ from typing import Any from bond_api import Action, BPUPSubscriptions, DeviceType from homeassistant.components.cover import ( - DEVICE_CLASS_SHADE, SUPPORT_CLOSE, SUPPORT_CLOSE_TILT, SUPPORT_OPEN, SUPPORT_OPEN_TILT, SUPPORT_STOP, SUPPORT_STOP_TILT, + CoverDeviceClass, CoverEntity, ) from homeassistant.config_entries import ConfigEntry @@ -47,7 +47,7 @@ async def async_setup_entry( class BondCover(BondEntity, CoverEntity): """Representation of a Bond cover.""" - _attr_device_class = DEVICE_CLASS_SHADE + _attr_device_class = CoverDeviceClass.SHADE def __init__( self, hub: BondHub, device: BondDevice, bpup_subs: BPUPSubscriptions From a8d0a545535206803278417343e4d1a437e2781d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 9 Dec 2021 08:28:08 +0100 Subject: [PATCH 0210/2644] Use new DeviceClass enums in bmw (#61321) Co-authored-by: epenet --- .../bmw_connected_drive/binary_sensor.py | 23 ++++++++----------- .../components/bmw_connected_drive/sensor.py | 9 +++++--- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index 37c0271a034..8110b535716 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -15,12 +15,7 @@ from bimmer_connected.vehicle_status import ( ) from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_BATTERY_CHARGING, - DEVICE_CLASS_LIGHT, - DEVICE_CLASS_LOCK, - DEVICE_CLASS_OPENING, - DEVICE_CLASS_PLUG, - DEVICE_CLASS_PROBLEM, + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -158,42 +153,42 @@ SENSOR_TYPES: tuple[BMWBinarySensorEntityDescription, ...] = ( BMWBinarySensorEntityDescription( key="lids", name="Doors", - device_class=DEVICE_CLASS_OPENING, + device_class=BinarySensorDeviceClass.OPENING, icon="mdi:car-door-lock", value_fn=_are_doors_closed, ), BMWBinarySensorEntityDescription( key="windows", name="Windows", - device_class=DEVICE_CLASS_OPENING, + device_class=BinarySensorDeviceClass.OPENING, icon="mdi:car-door", value_fn=_are_windows_closed, ), BMWBinarySensorEntityDescription( key="door_lock_state", name="Door lock state", - device_class=DEVICE_CLASS_LOCK, + device_class=BinarySensorDeviceClass.LOCK, icon="mdi:car-key", value_fn=_are_doors_locked, ), BMWBinarySensorEntityDescription( key="lights_parking", name="Parking lights", - device_class=DEVICE_CLASS_LIGHT, + device_class=BinarySensorDeviceClass.LIGHT, icon="mdi:car-parking-lights", value_fn=_are_parking_lights_on, ), BMWBinarySensorEntityDescription( key="condition_based_services", name="Condition based services", - device_class=DEVICE_CLASS_PROBLEM, + device_class=BinarySensorDeviceClass.PROBLEM, icon="mdi:wrench", value_fn=_are_problems_detected, ), BMWBinarySensorEntityDescription( key="check_control_messages", name="Control messages", - device_class=DEVICE_CLASS_PROBLEM, + device_class=BinarySensorDeviceClass.PROBLEM, icon="mdi:car-tire-alert", value_fn=_check_control_messages, ), @@ -201,14 +196,14 @@ SENSOR_TYPES: tuple[BMWBinarySensorEntityDescription, ...] = ( BMWBinarySensorEntityDescription( key="charging_status", name="Charging status", - device_class=DEVICE_CLASS_BATTERY_CHARGING, + device_class=BinarySensorDeviceClass.BATTERY_CHARGING, icon="mdi:ev-station", value_fn=_is_vehicle_charging, ), BMWBinarySensorEntityDescription( key="connection_status", name="Connection status", - device_class=DEVICE_CLASS_PLUG, + device_class=BinarySensorDeviceClass.PLUG, icon="mdi:car-electric", value_fn=_is_vehicle_plugged_in, ), diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 3eda7ccd5b0..241ee3a83b6 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -8,11 +8,14 @@ from typing import cast from bimmer_connected.vehicle import ConnectedDriveVehicle -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_UNIT_SYSTEM_IMPERIAL, - DEVICE_CLASS_BATTERY, LENGTH_KILOMETERS, LENGTH_MILES, PERCENTAGE, @@ -61,7 +64,7 @@ SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = { key="charging_level_hv", unit_metric=PERCENTAGE, unit_imperial=PERCENTAGE, - device_class=DEVICE_CLASS_BATTERY, + device_class=SensorDeviceClass.BATTERY, ), # --- Specific --- "mileage": BMWSensorEntityDescription( From 0c96b27ab2bedfc3b2f86bfa56e2cad93c892761 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 9 Dec 2021 08:28:37 +0100 Subject: [PATCH 0211/2644] Use new SensorDeviceClass enum in bmp280 (#61320) Co-authored-by: epenet --- homeassistant/components/bmp280/sensor.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bmp280/sensor.py b/homeassistant/components/bmp280/sensor.py index 21ab71e5ce6..3721bffaf68 100644 --- a/homeassistant/components/bmp280/sensor.py +++ b/homeassistant/components/bmp280/sensor.py @@ -8,9 +8,8 @@ from busio import I2C import voluptuous as vol from homeassistant.components.sensor import ( - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, PLATFORM_SCHEMA, + SensorDeviceClass, SensorEntity, ) from homeassistant.const import CONF_NAME, PRESSURE_HPA, TEMP_CELSIUS @@ -87,7 +86,7 @@ class Bmp280TemperatureSensor(Bmp280Sensor): def __init__(self, bmp280: Adafruit_BMP280_I2C, name: str) -> None: """Initialize the entity.""" super().__init__( - bmp280, f"{name} Temperature", TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE + bmp280, f"{name} Temperature", TEMP_CELSIUS, SensorDeviceClass.TEMPERATURE ) @Throttle(MIN_TIME_BETWEEN_UPDATES) @@ -112,7 +111,7 @@ class Bmp280PressureSensor(Bmp280Sensor): def __init__(self, bmp280: Adafruit_BMP280_I2C, name: str) -> None: """Initialize the entity.""" super().__init__( - bmp280, f"{name} Pressure", PRESSURE_HPA, DEVICE_CLASS_PRESSURE + bmp280, f"{name} Pressure", PRESSURE_HPA, SensorDeviceClass.PRESSURE ) @Throttle(MIN_TIME_BETWEEN_UPDATES) From c7eae8b0bc73dc00038053e275b941a0ee32ac94 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 9 Dec 2021 08:29:01 +0100 Subject: [PATCH 0212/2644] Use new SensorDeviceClass enum in bme280 (#61319) Co-authored-by: epenet --- homeassistant/components/bme280/const.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/bme280/const.py b/homeassistant/components/bme280/const.py index e217c0df29e..1bb0828dd1e 100644 --- a/homeassistant/components/bme280/const.py +++ b/homeassistant/components/bme280/const.py @@ -3,14 +3,8 @@ from __future__ import annotations from datetime import timedelta -from homeassistant.components.sensor import SensorEntityDescription -from homeassistant.const import ( - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, - PERCENTAGE, - TEMP_CELSIUS, -) +from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription +from homeassistant.const import PERCENTAGE, TEMP_CELSIUS # Common DOMAIN = "bme280" @@ -34,19 +28,19 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key=SENSOR_TEMP, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( key=SENSOR_HUMID, name="Humidity", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=SENSOR_PRESS, name="Pressure", native_unit_of_measurement="mb", - device_class=DEVICE_CLASS_PRESSURE, + device_class=SensorDeviceClass.PRESSURE, ), ) SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] From cf440f245250f7e602dbedcfc0085ee82e2e9ac8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 9 Dec 2021 08:29:29 +0100 Subject: [PATCH 0213/2644] Use SensorDeviceClass enum in beewi_smartclim (#61313) Co-authored-by: epenet --- .../components/beewi_smartclim/sensor.py | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/beewi_smartclim/sensor.py b/homeassistant/components/beewi_smartclim/sensor.py index 9ec81956c56..afb51734a9e 100644 --- a/homeassistant/components/beewi_smartclim/sensor.py +++ b/homeassistant/components/beewi_smartclim/sensor.py @@ -2,16 +2,12 @@ from beewi_smartclim import BeewiSmartClimPoller # pylint: disable=import-error import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import ( - CONF_MAC, - CONF_NAME, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_TEMPERATURE, - PERCENTAGE, - TEMP_CELSIUS, +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorDeviceClass, + SensorEntity, ) +from homeassistant.const import CONF_MAC, CONF_NAME, PERCENTAGE, TEMP_CELSIUS import homeassistant.helpers.config_validation as cv # Default values @@ -19,9 +15,9 @@ DEFAULT_NAME = "BeeWi SmartClim" # Sensor config SENSOR_TYPES = [ - [DEVICE_CLASS_TEMPERATURE, "Temperature", TEMP_CELSIUS], - [DEVICE_CLASS_HUMIDITY, "Humidity", PERCENTAGE], - [DEVICE_CLASS_BATTERY, "Battery", PERCENTAGE], + [SensorDeviceClass.TEMPERATURE, "Temperature", TEMP_CELSIUS], + [SensorDeviceClass.HUMIDITY, "Humidity", PERCENTAGE], + [SensorDeviceClass.BATTERY, "Battery", PERCENTAGE], ] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -71,9 +67,9 @@ class BeewiSmartclimSensor(SensorEntity): """Fetch new state data from the poller.""" self._poller.update_sensor() self._attr_native_value = None - if self._device == DEVICE_CLASS_TEMPERATURE: + if self._device == SensorDeviceClass.TEMPERATURE: self._attr_native_value = self._poller.get_temperature() - if self._device == DEVICE_CLASS_HUMIDITY: + if self._device == SensorDeviceClass.HUMIDITY: self._attr_native_value = self._poller.get_humidity() - if self._device == DEVICE_CLASS_BATTERY: + if self._device == SensorDeviceClass.BATTERY: self._attr_native_value = self._poller.get_battery() From 6d61a4678db2848dda321936792fea5fa59c204b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 9 Dec 2021 08:29:47 +0100 Subject: [PATCH 0214/2644] Ues new SensorDeviceClass in bme680 (#61318) Co-authored-by: epenet --- homeassistant/components/bme680/sensor.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/bme680/sensor.py b/homeassistant/components/bme680/sensor.py index 6c32639fdb4..84a1b613a23 100644 --- a/homeassistant/components/bme680/sensor.py +++ b/homeassistant/components/bme680/sensor.py @@ -11,15 +11,13 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, + SensorDeviceClass, SensorEntity, SensorEntityDescription, ) from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_NAME, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, PERCENTAGE, TEMP_CELSIUS, ) @@ -65,19 +63,19 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key=SENSOR_TEMP, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( key=SENSOR_HUMID, name="Humidity", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=SENSOR_PRESS, name="Pressure", native_unit_of_measurement="mb", - device_class=DEVICE_CLASS_PRESSURE, + device_class=SensorDeviceClass.PRESSURE, ), SensorEntityDescription( key=SENSOR_GAS, From d7756efe55f899073815f827763d8dbb07d48f2e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 9 Dec 2021 08:39:33 +0100 Subject: [PATCH 0215/2644] Use DeviceClass and StateClass enums in broadlink (#61326) Co-authored-by: epenet --- homeassistant/components/broadlink/sensor.py | 41 ++++++++------------ homeassistant/components/broadlink/switch.py | 7 ++-- 2 files changed, 20 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/broadlink/sensor.py b/homeassistant/components/broadlink/sensor.py index 8f739deeaa8..6eb0314fd65 100644 --- a/homeassistant/components/broadlink/sensor.py +++ b/homeassistant/components/broadlink/sensor.py @@ -6,18 +6,11 @@ import logging import voluptuous as vol from homeassistant.components.sensor import ( - DEVICE_CLASS_CURRENT, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_POWER, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_VOLTAGE, PLATFORM_SCHEMA, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.const import ( CONF_HOST, @@ -41,8 +34,8 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="temperature", name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="air_quality", @@ -52,13 +45,13 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="humidity", name="Humidity", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="light", name="Light", - device_class=DEVICE_CLASS_ILLUMINANCE, + device_class=SensorDeviceClass.ILLUMINANCE, ), SensorEntityDescription( key="noise", @@ -68,36 +61,36 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="power", name="Current power", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="volt", name="Voltage", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="current", name="Current", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="overload", name="Overload", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="totalconsum", name="Total consumption", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), ) diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index d8fc9632321..a2a988c8531 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -6,9 +6,8 @@ from broadlink.exceptions import BroadlinkException import voluptuous as vol from homeassistant.components.switch import ( - DEVICE_CLASS_OUTLET, - DEVICE_CLASS_SWITCH, PLATFORM_SCHEMA, + SwitchDeviceClass, SwitchEntity, ) from homeassistant.const import ( @@ -136,7 +135,7 @@ class BroadlinkSwitch(BroadlinkEntity, SwitchEntity, RestoreEntity, ABC): """Representation of a Broadlink switch.""" _attr_assumed_state = True - _attr_device_class = DEVICE_CLASS_SWITCH + _attr_device_class = SwitchDeviceClass.SWITCH def __init__(self, device, command_on, command_off): """Initialize the switch.""" @@ -269,7 +268,7 @@ class BroadlinkBG1Slot(BroadlinkSwitch): self._attr_is_on = self._coordinator.data[f"pwr{slot}"] self._attr_name = f"{device.name} S{slot}" - self._attr_device_class = DEVICE_CLASS_OUTLET + self._attr_device_class = SwitchDeviceClass.OUTLET self._attr_unique_id = f"{device.unique_id}-s{slot}" def _update_state(self, data): From f4f13b70742fab5e4ae1c27bb3302c69ee9d4e63 Mon Sep 17 00:00:00 2001 From: Yehuda Davis Date: Thu, 9 Dec 2021 02:40:45 -0500 Subject: [PATCH 0216/2644] Fix regression in Tuya cover is_closed logic (#61303) --- homeassistant/components/tuya/cover.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/cover.py b/homeassistant/components/tuya/cover.py index fd1f2aae972..275d28bae17 100644 --- a/homeassistant/components/tuya/cover.py +++ b/homeassistant/components/tuya/cover.py @@ -297,7 +297,7 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity): is not None ): return self.entity_description.current_state_inverse is not ( - current_state in (False, "fully_close") + current_state in (True, "fully_close") ) if (position := self.current_cover_position) is not None: From 54e312e1f78938d78786ee64955d6b54e206d673 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 8 Dec 2021 23:58:23 -0800 Subject: [PATCH 0217/2644] Fix hue groups inheritance (#61308) --- homeassistant/components/hue/v2/group.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hue/v2/group.py b/homeassistant/components/hue/v2/group.py index ae77ab38539..312fef6629f 100644 --- a/homeassistant/components/hue/v2/group.py +++ b/homeassistant/components/hue/v2/group.py @@ -7,7 +7,6 @@ from aiohue.v2 import HueBridgeV2 from aiohue.v2.controllers.events import EventType from aiohue.v2.controllers.groups import GroupedLight, Room, Zone -from homeassistant.components.group.light import LightGroup from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -18,6 +17,7 @@ from homeassistant.components.light import ( COLOR_MODE_ONOFF, COLOR_MODE_XY, SUPPORT_TRANSITION, + LightEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -73,11 +73,12 @@ async def async_setup_entry( ) -class GroupedHueLight(HueBaseEntity, LightGroup): +class GroupedHueLight(HueBaseEntity, LightEntity): """Representation of a Grouped Hue light.""" # Entities for Hue groups are disabled by default _attr_entity_registry_enabled_default = False + _attr_icon = "mdi:lightbulb-group" def __init__( self, bridge: HueBridge, resource: GroupedLight, group: Room | Zone From 4c542336d569a7d0f618c78dad2e0261323b271c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Thu, 9 Dec 2021 09:04:19 +0100 Subject: [PATCH 0218/2644] Add sensors to Mill local heaters (#61247) Co-authored-by: Franck Nijhof --- homeassistant/components/mill/sensor.py | 59 +++++++++++++++++++++++++ tests/components/mill/test_init.py | 2 + 2 files changed, 61 insertions(+) diff --git a/homeassistant/components/mill/sensor.py b/homeassistant/components/mill/sensor.py index 44d08e828f3..2a1d14d159e 100644 --- a/homeassistant/components/mill/sensor.py +++ b/homeassistant/components/mill/sensor.py @@ -12,9 +12,11 @@ from homeassistant.components.sensor import ( from homeassistant.const import ( CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, + CONF_IP_ADDRESS, CONF_USERNAME, ENERGY_KILO_WATT_HOUR, PERCENTAGE, + POWER_WATT, TEMP_CELSIUS, ) from homeassistant.core import callback @@ -90,10 +92,43 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), ) +LOCAL_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="control_signal", + native_unit_of_measurement=PERCENTAGE, + name="Control signal", + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="current_power", + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=POWER_WATT, + name="Current power", + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="raw_ambient_temperature", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + name="Uncalibrated temperature", + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), +) + async def async_setup_entry(hass, entry, async_add_entities): """Set up the Mill sensor.""" if entry.data.get(CONNECTION_TYPE) == LOCAL: + mill_data_coordinator = hass.data[DOMAIN][LOCAL][entry.data[CONF_IP_ADDRESS]] + + async_add_entities( + LocalMillSensor( + mill_data_coordinator, + entity_description, + ) + for entity_description in LOCAL_SENSOR_TYPES + ) return mill_data_coordinator = hass.data[DOMAIN][CLOUD][entry.data[CONF_USERNAME]] @@ -154,3 +189,27 @@ class MillSensor(CoordinatorEntity, SensorEntity): def _update_attr(self, device): self._available = device.available self._attr_native_value = getattr(device, self.entity_description.key) + + +class LocalMillSensor(CoordinatorEntity, SensorEntity): + """Representation of a Mill Sensor device.""" + + def __init__(self, coordinator, entity_description): + """Initialize the sensor.""" + super().__init__(coordinator) + + self.entity_description = entity_description + self._attr_name = ( + f"{coordinator.mill_data_connection.name} {entity_description.name}" + ) + self._update_attr() + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._update_attr() + self.async_write_ha_state() + + @callback + def _update_attr(self) -> None: + self._attr_native_value = self.coordinator.data[self.entity_description.key] diff --git a/tests/components/mill/test_init.py b/tests/components/mill/test_init.py index f92b4689ebf..fe2facef50d 100644 --- a/tests/components/mill/test_init.py +++ b/tests/components/mill/test_init.py @@ -78,6 +78,8 @@ async def test_setup_with_local_config(hass): "ambient_temperature": 20, "set_temperature": 22, "current_power": 0, + "control_signal": 0, + "raw_ambient_temperature": 19, }, ) as mock_fetch, patch( "mill_local.Mill.connect", From a7b631d650be628d7eed07c9c73b308057630673 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 9 Dec 2021 09:12:54 +0100 Subject: [PATCH 0219/2644] Use DeviceClass and StateClass enums in bbox (#61309) Co-authored-by: epenet --- homeassistant/components/bbox/sensor.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/bbox/sensor.py b/homeassistant/components/bbox/sensor.py index fc9c8982733..5ab897de797 100644 --- a/homeassistant/components/bbox/sensor.py +++ b/homeassistant/components/bbox/sensor.py @@ -10,15 +10,15 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, - STATE_CLASS_MEASUREMENT, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.const import ( CONF_MONITORED_VARIABLES, CONF_NAME, DATA_RATE_MEGABITS_PER_SECOND, - DEVICE_CLASS_TIMESTAMP, ) import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle @@ -49,14 +49,14 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="current_down_bandwidth", name="Currently Used Download Bandwidth", native_unit_of_measurement=DATA_RATE_MEGABITS_PER_SECOND, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, icon="mdi:download", ), SensorEntityDescription( key="current_up_bandwidth", name="Currently Used Upload Bandwidth", native_unit_of_measurement=DATA_RATE_MEGABITS_PER_SECOND, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, icon="mdi:upload", ), SensorEntityDescription( @@ -120,7 +120,7 @@ class BboxUptimeSensor(SensorEntity): """Bbox uptime sensor.""" _attr_attribution = ATTRIBUTION - _attr_device_class = DEVICE_CLASS_TIMESTAMP + _attr_device_class = SensorDeviceClass.TIMESTAMP def __init__(self, bbox_data, name, description: SensorEntityDescription): """Initialize the sensor.""" From 730208028f1dc37cc5da5208d391bcce61de3b98 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 9 Dec 2021 09:13:23 +0100 Subject: [PATCH 0220/2644] Use SensorDeviceClass enum in bh1750 (#61314) Co-authored-by: epenet --- homeassistant/components/bh1750/sensor.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bh1750/sensor.py b/homeassistant/components/bh1750/sensor.py index ad5ca13684a..8eab85e5f1d 100644 --- a/homeassistant/components/bh1750/sensor.py +++ b/homeassistant/components/bh1750/sensor.py @@ -6,8 +6,12 @@ from i2csense.bh1750 import BH1750 # pylint: disable=import-error import smbus import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import CONF_NAME, DEVICE_CLASS_ILLUMINANCE, LIGHT_LUX +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorDeviceClass, + SensorEntity, +) +from homeassistant.const import CONF_NAME, LIGHT_LUX import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -96,7 +100,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class BH1750Sensor(SensorEntity): """Implementation of the BH1750 sensor.""" - _attr_device_class = DEVICE_CLASS_ILLUMINANCE + _attr_device_class = SensorDeviceClass.ILLUMINANCE def __init__(self, bh1750_sensor, name, unit, multiplier=1.0): """Initialize the sensor.""" From 23f21bd27a13d0513e38b478deb14d1a1140b1d1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 9 Dec 2021 09:13:50 +0100 Subject: [PATCH 0221/2644] Use new DeviceClass enums in blink (#61315) Co-authored-by: epenet --- homeassistant/components/blink/binary_sensor.py | 7 +++---- homeassistant/components/blink/sensor.py | 15 +++++++-------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/blink/binary_sensor.py b/homeassistant/components/blink/binary_sensor.py index 6be284e2197..6e5dd58c368 100644 --- a/homeassistant/components/blink/binary_sensor.py +++ b/homeassistant/components/blink/binary_sensor.py @@ -2,8 +2,7 @@ from __future__ import annotations from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_MOTION, + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -14,7 +13,7 @@ BINARY_SENSORS_TYPES: tuple[BinarySensorEntityDescription, ...] = ( BinarySensorEntityDescription( key=TYPE_BATTERY, name="Battery", - device_class=DEVICE_CLASS_BATTERY, + device_class=BinarySensorDeviceClass.BATTERY, ), BinarySensorEntityDescription( key=TYPE_CAMERA_ARMED, @@ -23,7 +22,7 @@ BINARY_SENSORS_TYPES: tuple[BinarySensorEntityDescription, ...] = ( BinarySensorEntityDescription( key=TYPE_MOTION_DETECTED, name="Motion Detected", - device_class=DEVICE_CLASS_MOTION, + device_class=BinarySensorDeviceClass.MOTION, ), ) diff --git a/homeassistant/components/blink/sensor.py b/homeassistant/components/blink/sensor.py index d2122b59cd8..8a5947ec8bf 100644 --- a/homeassistant/components/blink/sensor.py +++ b/homeassistant/components/blink/sensor.py @@ -3,13 +3,12 @@ from __future__ import annotations import logging -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription -from homeassistant.const import ( - DEVICE_CLASS_SIGNAL_STRENGTH, - DEVICE_CLASS_TEMPERATURE, - SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - TEMP_FAHRENHEIT, +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, ) +from homeassistant.const import SIGNAL_STRENGTH_DECIBELS_MILLIWATT, TEMP_FAHRENHEIT from .const import DOMAIN, TYPE_TEMPERATURE, TYPE_WIFI_STRENGTH @@ -20,13 +19,13 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key=TYPE_TEMPERATURE, name="Temperature", native_unit_of_measurement=TEMP_FAHRENHEIT, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( key=TYPE_WIFI_STRENGTH, name="Wifi Signal", native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, ), ) From f7f50563dddb6f4b6193774dc6375f7c48d48a98 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 9 Dec 2021 09:14:16 +0100 Subject: [PATCH 0222/2644] Use new DeviceClass enums in bloomsky (#61316) Co-authored-by: epenet --- homeassistant/components/bloomsky/binary_sensor.py | 4 ++-- homeassistant/components/bloomsky/sensor.py | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/bloomsky/binary_sensor.py b/homeassistant/components/bloomsky/binary_sensor.py index 2ef4586f537..2b8cdd57c95 100644 --- a/homeassistant/components/bloomsky/binary_sensor.py +++ b/homeassistant/components/bloomsky/binary_sensor.py @@ -2,8 +2,8 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_MOISTURE, PLATFORM_SCHEMA, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.const import CONF_MONITORED_CONDITIONS @@ -11,7 +11,7 @@ import homeassistant.helpers.config_validation as cv from . import DOMAIN -SENSOR_TYPES = {"Rain": DEVICE_CLASS_MOISTURE, "Night": None} +SENSOR_TYPES = {"Rain": BinarySensorDeviceClass.MOISTURE, "Night": None} PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { diff --git a/homeassistant/components/bloomsky/sensor.py b/homeassistant/components/bloomsky/sensor.py index 9a14145d1d8..f0bc31a337a 100644 --- a/homeassistant/components/bloomsky/sensor.py +++ b/homeassistant/components/bloomsky/sensor.py @@ -1,11 +1,14 @@ """Support the sensor of a BloomSky weather station.""" import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorDeviceClass, + SensorEntity, +) from homeassistant.const import ( AREA_SQUARE_METERS, CONF_MONITORED_CONDITIONS, - DEVICE_CLASS_TEMPERATURE, ELECTRIC_POTENTIAL_MILLIVOLT, PERCENTAGE, PRESSURE_INHG, @@ -47,7 +50,7 @@ SENSOR_UNITS_METRIC = { # Device class SENSOR_DEVICE_CLASS = { - "Temperature": DEVICE_CLASS_TEMPERATURE, + "Temperature": SensorDeviceClass.TEMPERATURE, } # Which sensors to format numerically From 76bdf9bc251669155545587b90a0e5c6f12897e9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 9 Dec 2021 09:16:14 +0100 Subject: [PATCH 0223/2644] Use new DeviceClass enums in bosch_shc (#61324) Co-authored-by: epenet --- .../components/bosch_shc/binary_sensor.py | 16 +++++++--------- homeassistant/components/bosch_shc/cover.py | 4 ++-- homeassistant/components/bosch_shc/sensor.py | 14 +++++--------- 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/bosch_shc/binary_sensor.py b/homeassistant/components/bosch_shc/binary_sensor.py index 29e45ba2359..341be87a939 100644 --- a/homeassistant/components/bosch_shc/binary_sensor.py +++ b/homeassistant/components/bosch_shc/binary_sensor.py @@ -3,9 +3,7 @@ from boschshcpy import SHCBatteryDevice, SHCSession, SHCShutterContact from boschshcpy.device import SHCDevice from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_DOOR, - DEVICE_CLASS_WINDOW, + BinarySensorDeviceClass, BinarySensorEntity, ) @@ -57,13 +55,13 @@ class ShutterContactSensor(SHCEntity, BinarySensorEntity): """Initialize an SHC shutter contact sensor..""" super().__init__(device, parent_id, entry_id) switcher = { - "ENTRANCE_DOOR": DEVICE_CLASS_DOOR, - "REGULAR_WINDOW": DEVICE_CLASS_WINDOW, - "FRENCH_WINDOW": DEVICE_CLASS_DOOR, - "GENERIC": DEVICE_CLASS_WINDOW, + "ENTRANCE_DOOR": BinarySensorDeviceClass.DOOR, + "REGULAR_WINDOW": BinarySensorDeviceClass.WINDOW, + "FRENCH_WINDOW": BinarySensorDeviceClass.DOOR, + "GENERIC": BinarySensorDeviceClass.WINDOW, } self._attr_device_class = switcher.get( - self._device.device_class, DEVICE_CLASS_WINDOW + self._device.device_class, BinarySensorDeviceClass.WINDOW ) @property @@ -75,7 +73,7 @@ class ShutterContactSensor(SHCEntity, BinarySensorEntity): class BatterySensor(SHCEntity, BinarySensorEntity): """Representation of an SHC battery reporting sensor.""" - _attr_device_class = DEVICE_CLASS_BATTERY + _attr_device_class = BinarySensorDeviceClass.BATTERY def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: """Initialize an SHC battery reporting sensor.""" diff --git a/homeassistant/components/bosch_shc/cover.py b/homeassistant/components/bosch_shc/cover.py index 91543e586bf..c08984ca0c2 100644 --- a/homeassistant/components/bosch_shc/cover.py +++ b/homeassistant/components/bosch_shc/cover.py @@ -3,11 +3,11 @@ from boschshcpy import SHCSession, SHCShutterControl from homeassistant.components.cover import ( ATTR_POSITION, - DEVICE_CLASS_SHUTTER, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION, SUPPORT_STOP, + CoverDeviceClass, CoverEntity, ) @@ -37,7 +37,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class ShutterControlCover(SHCEntity, CoverEntity): """Representation of a SHC shutter control device.""" - _attr_device_class = DEVICE_CLASS_SHUTTER + _attr_device_class = CoverDeviceClass.SHUTTER _attr_supported_features = ( SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP | SUPPORT_SET_POSITION ) diff --git a/homeassistant/components/bosch_shc/sensor.py b/homeassistant/components/bosch_shc/sensor.py index 6ea4f3c7065..33d18ebbfd4 100644 --- a/homeassistant/components/bosch_shc/sensor.py +++ b/homeassistant/components/bosch_shc/sensor.py @@ -2,13 +2,9 @@ from boschshcpy import SHCSession from boschshcpy.device import SHCDevice -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_TEMPERATURE, ENERGY_KILO_WATT_HOUR, PERCENTAGE, POWER_WATT, @@ -146,7 +142,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class TemperatureSensor(SHCEntity, SensorEntity): """Representation of an SHC temperature reporting sensor.""" - _attr_device_class = DEVICE_CLASS_TEMPERATURE + _attr_device_class = SensorDeviceClass.TEMPERATURE _attr_native_unit_of_measurement = TEMP_CELSIUS def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: @@ -164,7 +160,7 @@ class TemperatureSensor(SHCEntity, SensorEntity): class HumiditySensor(SHCEntity, SensorEntity): """Representation of an SHC humidity reporting sensor.""" - _attr_device_class = DEVICE_CLASS_HUMIDITY + _attr_device_class = SensorDeviceClass.HUMIDITY _attr_native_unit_of_measurement = PERCENTAGE def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: @@ -267,7 +263,7 @@ class PurityRatingSensor(SHCEntity, SensorEntity): class PowerSensor(SHCEntity, SensorEntity): """Representation of an SHC power reporting sensor.""" - _attr_device_class = DEVICE_CLASS_POWER + _attr_device_class = SensorDeviceClass.POWER _attr_native_unit_of_measurement = POWER_WATT def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: @@ -285,7 +281,7 @@ class PowerSensor(SHCEntity, SensorEntity): class EnergySensor(SHCEntity, SensorEntity): """Representation of an SHC energy reporting sensor.""" - _attr_device_class = DEVICE_CLASS_ENERGY + _attr_device_class = SensorDeviceClass.ENERGY _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: From 9d25961bf30d8a76acd5c14fe96678811c56d3c9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 9 Dec 2021 09:16:37 +0100 Subject: [PATCH 0224/2644] Use new MediaPlayerDeviceClass enum in braviatv (#61325) Co-authored-by: epenet --- homeassistant/components/braviatv/media_player.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/braviatv/media_player.py b/homeassistant/components/braviatv/media_player.py index 5451738cec6..69df0b245b8 100644 --- a/homeassistant/components/braviatv/media_player.py +++ b/homeassistant/components/braviatv/media_player.py @@ -3,7 +3,10 @@ from __future__ import annotations from typing import Final -from homeassistant.components.media_player import DEVICE_CLASS_TV, MediaPlayerEntity +from homeassistant.components.media_player import ( + MediaPlayerDeviceClass, + MediaPlayerEntity, +) from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -68,7 +71,7 @@ class BraviaTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): """Representation of a Bravia TV Media Player.""" coordinator: BraviaTVCoordinator - _attr_device_class = DEVICE_CLASS_TV + _attr_device_class = MediaPlayerDeviceClass.TV _attr_supported_features = SUPPORT_BRAVIA def __init__( From 48188725912c51646d38b58d4a0d2752fa1dcfbc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 9 Dec 2021 09:17:50 +0100 Subject: [PATCH 0225/2644] Use new enums in brother (#61327) Co-authored-by: epenet --- homeassistant/components/brother/sensor.py | 106 ++++++++++----------- 1 file changed, 51 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/brother/sensor.py b/homeassistant/components/brother/sensor.py index be46bc33b6b..a589ea0bd77 100644 --- a/homeassistant/components/brother/sensor.py +++ b/homeassistant/components/brother/sensor.py @@ -6,19 +6,15 @@ from datetime import datetime from typing import Any, cast from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_HOST, - DEVICE_CLASS_TIMESTAMP, - ENTITY_CATEGORY_DIAGNOSTIC, - PERCENTAGE, -) +from homeassistant.const import CONF_HOST, PERCENTAGE from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -173,190 +169,190 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( key=ATTR_STATUS, icon="mdi:printer", name=ATTR_STATUS.title(), - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), BrotherSensorEntityDescription( key=ATTR_PAGE_COUNTER, icon="mdi:file-document-outline", name=ATTR_PAGE_COUNTER.replace("_", " ").title(), native_unit_of_measurement=UNIT_PAGES, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), BrotherSensorEntityDescription( key=ATTR_BW_COUNTER, icon="mdi:file-document-outline", name=ATTR_BW_COUNTER.replace("_", " ").title(), native_unit_of_measurement=UNIT_PAGES, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), BrotherSensorEntityDescription( key=ATTR_COLOR_COUNTER, icon="mdi:file-document-outline", name=ATTR_COLOR_COUNTER.replace("_", " ").title(), native_unit_of_measurement=UNIT_PAGES, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), BrotherSensorEntityDescription( key=ATTR_DUPLEX_COUNTER, icon="mdi:file-document-outline", name=ATTR_DUPLEX_COUNTER.replace("_", " ").title(), native_unit_of_measurement=UNIT_PAGES, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), BrotherSensorEntityDescription( key=ATTR_DRUM_REMAINING_LIFE, icon="mdi:chart-donut", name=ATTR_DRUM_REMAINING_LIFE.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), BrotherSensorEntityDescription( key=ATTR_BLACK_DRUM_REMAINING_LIFE, icon="mdi:chart-donut", name=ATTR_BLACK_DRUM_REMAINING_LIFE.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), BrotherSensorEntityDescription( key=ATTR_CYAN_DRUM_REMAINING_LIFE, icon="mdi:chart-donut", name=ATTR_CYAN_DRUM_REMAINING_LIFE.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), BrotherSensorEntityDescription( key=ATTR_MAGENTA_DRUM_REMAINING_LIFE, icon="mdi:chart-donut", name=ATTR_MAGENTA_DRUM_REMAINING_LIFE.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), BrotherSensorEntityDescription( key=ATTR_YELLOW_DRUM_REMAINING_LIFE, icon="mdi:chart-donut", name=ATTR_YELLOW_DRUM_REMAINING_LIFE.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), BrotherSensorEntityDescription( key=ATTR_BELT_UNIT_REMAINING_LIFE, icon="mdi:current-ac", name=ATTR_BELT_UNIT_REMAINING_LIFE.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), BrotherSensorEntityDescription( key=ATTR_FUSER_REMAINING_LIFE, icon="mdi:water-outline", name=ATTR_FUSER_REMAINING_LIFE.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), BrotherSensorEntityDescription( key=ATTR_LASER_REMAINING_LIFE, icon="mdi:spotlight-beam", name=ATTR_LASER_REMAINING_LIFE.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), BrotherSensorEntityDescription( key=ATTR_PF_KIT_1_REMAINING_LIFE, icon="mdi:printer-3d", name=ATTR_PF_KIT_1_REMAINING_LIFE.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), BrotherSensorEntityDescription( key=ATTR_PF_KIT_MP_REMAINING_LIFE, icon="mdi:printer-3d", name=ATTR_PF_KIT_MP_REMAINING_LIFE.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), BrotherSensorEntityDescription( key=ATTR_BLACK_TONER_REMAINING, icon="mdi:printer-3d-nozzle", name=ATTR_BLACK_TONER_REMAINING.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), BrotherSensorEntityDescription( key=ATTR_CYAN_TONER_REMAINING, icon="mdi:printer-3d-nozzle", name=ATTR_CYAN_TONER_REMAINING.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), BrotherSensorEntityDescription( key=ATTR_MAGENTA_TONER_REMAINING, icon="mdi:printer-3d-nozzle", name=ATTR_MAGENTA_TONER_REMAINING.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), BrotherSensorEntityDescription( key=ATTR_YELLOW_TONER_REMAINING, icon="mdi:printer-3d-nozzle", name=ATTR_YELLOW_TONER_REMAINING.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), BrotherSensorEntityDescription( key=ATTR_BLACK_INK_REMAINING, icon="mdi:printer-3d-nozzle", name=ATTR_BLACK_INK_REMAINING.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), BrotherSensorEntityDescription( key=ATTR_CYAN_INK_REMAINING, icon="mdi:printer-3d-nozzle", name=ATTR_CYAN_INK_REMAINING.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), BrotherSensorEntityDescription( key=ATTR_MAGENTA_INK_REMAINING, icon="mdi:printer-3d-nozzle", name=ATTR_MAGENTA_INK_REMAINING.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), BrotherSensorEntityDescription( key=ATTR_YELLOW_INK_REMAINING, icon="mdi:printer-3d-nozzle", name=ATTR_YELLOW_INK_REMAINING.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), BrotherSensorEntityDescription( key=ATTR_UPTIME, name=ATTR_UPTIME.title(), entity_registry_enabled_default=False, - device_class=DEVICE_CLASS_TIMESTAMP, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=SensorDeviceClass.TIMESTAMP, + entity_category=EntityCategory.DIAGNOSTIC, entity_class=BrotherPrinterUptimeSensor, ), ) From 172591031dce626b3678ccb227c0731c166bbca8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 9 Dec 2021 09:18:20 +0100 Subject: [PATCH 0226/2644] Use new CoverDeviceClass enum in brunt (#61328) Co-authored-by: epenet --- homeassistant/components/brunt/cover.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/brunt/cover.py b/homeassistant/components/brunt/cover.py index cc0ecd0feab..faa4653d3be 100644 --- a/homeassistant/components/brunt/cover.py +++ b/homeassistant/components/brunt/cover.py @@ -10,10 +10,10 @@ from brunt import BruntClientAsync, Thing from homeassistant.components.cover import ( ATTR_POSITION, - DEVICE_CLASS_SHADE, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION, + CoverDeviceClass, CoverEntity, ) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -101,7 +101,7 @@ class BruntDevice(CoordinatorEntity, CoverEntity): self._remove_update_listener = None self._attr_name = self._thing.NAME - self._attr_device_class = DEVICE_CLASS_SHADE + self._attr_device_class = CoverDeviceClass.SHADE self._attr_supported_features = COVER_FEATURES self._attr_attribution = ATTRIBUTION self._attr_device_info = DeviceInfo( From bdbecc6c051a21e2e0f7d9c041606c75174124d4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 9 Dec 2021 09:18:48 +0100 Subject: [PATCH 0227/2644] Use new SensorDeviceClass enum in buienradar (#61329) Co-authored-by: epenet --- homeassistant/components/buienradar/sensor.py | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index 0affe1e2c62..0f1044cec96 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -21,7 +21,11 @@ from buienradar.constants import ( WINDSPEED, ) -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, @@ -29,7 +33,6 @@ from homeassistant.const import ( CONF_LONGITUDE, CONF_NAME, DEGREE, - DEVICE_CLASS_TEMPERATURE, IRRADIATION_WATTS_PER_SQUARE_METER, LENGTH_KILOMETERS, LENGTH_MILLIMETERS, @@ -107,7 +110,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="feeltemperature", name="Feel temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( key="humidity", @@ -119,13 +122,13 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="temperature", name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( key="groundtemperature", name="Ground temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( key="windspeed", @@ -209,61 +212,61 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="temperature_1d", name="Temperature 1d", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( key="temperature_2d", name="Temperature 2d", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( key="temperature_3d", name="Temperature 3d", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( key="temperature_4d", name="Temperature 4d", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( key="temperature_5d", name="Temperature 5d", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( key="mintemp_1d", name="Minimum temperature 1d", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( key="mintemp_2d", name="Minimum temperature 2d", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( key="mintemp_3d", name="Minimum temperature 3d", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( key="mintemp_4d", name="Minimum temperature 4d", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( key="mintemp_5d", name="Minimum temperature 5d", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( key="rain_1d", From dfc85fe372f4b26cc03e255f9262594ee0519245 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 9 Dec 2021 09:20:10 +0100 Subject: [PATCH 0228/2644] Use new SensorDeviceClass enum in canary (#61330) Co-authored-by: epenet --- homeassistant/components/canary/sensor.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/canary/sensor.py b/homeassistant/components/canary/sensor.py index dbd94d98d58..cf2b0970311 100644 --- a/homeassistant/components/canary/sensor.py +++ b/homeassistant/components/canary/sensor.py @@ -5,13 +5,9 @@ from typing import Final from canary.api import Device, Location, SensorType -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_SIGNAL_STRENGTH, - DEVICE_CLASS_TEMPERATURE, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, TEMP_CELSIUS, @@ -38,17 +34,17 @@ CANARY_FLEX: Final = "Canary Flex" # Sensor types are defined like so: # sensor type name, unit_of_measurement, icon, device class, products supported SENSOR_TYPES: Final[list[SensorTypeItem]] = [ - ("temperature", TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE, [CANARY_PRO]), - ("humidity", PERCENTAGE, None, DEVICE_CLASS_HUMIDITY, [CANARY_PRO]), + ("temperature", TEMP_CELSIUS, None, SensorDeviceClass.TEMPERATURE, [CANARY_PRO]), + ("humidity", PERCENTAGE, None, SensorDeviceClass.HUMIDITY, [CANARY_PRO]), ("air_quality", None, "mdi:weather-windy", None, [CANARY_PRO]), ( "wifi", SIGNAL_STRENGTH_DECIBELS_MILLIWATT, None, - DEVICE_CLASS_SIGNAL_STRENGTH, + SensorDeviceClass.SIGNAL_STRENGTH, [CANARY_FLEX], ), - ("battery", PERCENTAGE, None, DEVICE_CLASS_BATTERY, [CANARY_FLEX]), + ("battery", PERCENTAGE, None, SensorDeviceClass.BATTERY, [CANARY_FLEX]), ] STATE_AIR_QUALITY_NORMAL: Final = "normal" From fb7bab2a5db35f53495d4ecd34cb6200c3bc4ef8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 9 Dec 2021 09:20:56 +0100 Subject: [PATCH 0229/2644] Use new SensorDeviceClass in cert_expiry (#61331) Co-authored-by: epenet --- homeassistant/components/cert_expiry/sensor.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py index 0aa67993180..bad82289017 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -5,14 +5,13 @@ from datetime import datetime, timedelta import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import ( - CONF_HOST, - CONF_PORT, - DEVICE_CLASS_TIMESTAMP, - EVENT_HOMEASSISTANT_START, +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorDeviceClass, + SensorEntity, ) +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_START from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_call_later @@ -78,7 +77,7 @@ class CertExpiryEntity(CoordinatorEntity): class SSLCertificateTimestamp(CertExpiryEntity, SensorEntity): """Implementation of the Cert Expiry timestamp sensor.""" - _attr_device_class = DEVICE_CLASS_TIMESTAMP + _attr_device_class = SensorDeviceClass.TIMESTAMP def __init__(self, coordinator) -> None: """Initialize a Cert Expiry timestamp sensor.""" From dff29639bdb8f80872fd4f37e3414113d6a02729 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 9 Dec 2021 09:22:02 +0100 Subject: [PATCH 0230/2644] Use new SensorStateClass enum in co2signal (#61333) Co-authored-by: epenet --- homeassistant/components/co2signal/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index 53a02afb560..6b176c2cf5f 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -10,8 +10,8 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components.sensor import ( PLATFORM_SCHEMA, - STATE_CLASS_MEASUREMENT, SensorEntity, + SensorStateClass, ) from homeassistant.const import ( ATTR_ATTRIBUTION, @@ -84,7 +84,7 @@ async def async_setup_entry(hass, entry, async_add_entities): class CO2Sensor(update_coordinator.CoordinatorEntity[CO2SignalResponse], SensorEntity): """Implementation of the CO2Signal sensor.""" - _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_state_class = SensorStateClass.MEASUREMENT _attr_icon = "mdi:molecule-co2" def __init__( From 59878968b2ce25b0f59a4c2f7f0e0eacf287bbd5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 9 Dec 2021 09:22:52 +0100 Subject: [PATCH 0231/2644] Use new DeviceClass and StateClass enums in comfoconnect (#61334) Co-authored-by: epenet --- .../components/comfoconnect/sensor.py | 74 +++++++++---------- 1 file changed, 35 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/comfoconnect/sensor.py b/homeassistant/components/comfoconnect/sensor.py index 14590fc1445..2503fb27583 100644 --- a/homeassistant/components/comfoconnect/sensor.py +++ b/homeassistant/components/comfoconnect/sensor.py @@ -29,17 +29,13 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.const import ( CONF_RESOURCES, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_TEMPERATURE, ENERGY_KILO_WATT_HOUR, PERCENTAGE, POWER_WATT, @@ -96,8 +92,8 @@ class ComfoconnectSensorEntityDescription( SENSOR_TYPES = ( ComfoconnectSensorEntityDescription( key=ATTR_CURRENT_TEMPERATURE, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, name="Inside temperature", native_unit_of_measurement=TEMP_CELSIUS, sensor_id=SENSOR_TEMPERATURE_EXTRACT, @@ -105,16 +101,16 @@ SENSOR_TYPES = ( ), ComfoconnectSensorEntityDescription( key=ATTR_CURRENT_HUMIDITY, - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, name="Inside humidity", native_unit_of_measurement=PERCENTAGE, sensor_id=SENSOR_HUMIDITY_EXTRACT, ), ComfoconnectSensorEntityDescription( key=ATTR_CURRENT_RMOT, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, name="Current RMOT", native_unit_of_measurement=TEMP_CELSIUS, sensor_id=SENSOR_CURRENT_RMOT, @@ -122,8 +118,8 @@ SENSOR_TYPES = ( ), ComfoconnectSensorEntityDescription( key=ATTR_OUTSIDE_TEMPERATURE, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, name="Outside temperature", native_unit_of_measurement=TEMP_CELSIUS, sensor_id=SENSOR_TEMPERATURE_OUTDOOR, @@ -131,16 +127,16 @@ SENSOR_TYPES = ( ), ComfoconnectSensorEntityDescription( key=ATTR_OUTSIDE_HUMIDITY, - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, name="Outside humidity", native_unit_of_measurement=PERCENTAGE, sensor_id=SENSOR_HUMIDITY_OUTDOOR, ), ComfoconnectSensorEntityDescription( key=ATTR_SUPPLY_TEMPERATURE, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, name="Supply temperature", native_unit_of_measurement=TEMP_CELSIUS, sensor_id=SENSOR_TEMPERATURE_SUPPLY, @@ -148,15 +144,15 @@ SENSOR_TYPES = ( ), ComfoconnectSensorEntityDescription( key=ATTR_SUPPLY_HUMIDITY, - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, name="Supply humidity", native_unit_of_measurement=PERCENTAGE, sensor_id=SENSOR_HUMIDITY_SUPPLY, ), ComfoconnectSensorEntityDescription( key=ATTR_SUPPLY_FAN_SPEED, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, name="Supply fan speed", native_unit_of_measurement="rpm", icon="mdi:fan-plus", @@ -164,7 +160,7 @@ SENSOR_TYPES = ( ), ComfoconnectSensorEntityDescription( key=ATTR_SUPPLY_FAN_DUTY, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, name="Supply fan duty", native_unit_of_measurement=PERCENTAGE, icon="mdi:fan-plus", @@ -172,7 +168,7 @@ SENSOR_TYPES = ( ), ComfoconnectSensorEntityDescription( key=ATTR_EXHAUST_FAN_SPEED, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, name="Exhaust fan speed", native_unit_of_measurement="rpm", icon="mdi:fan-minus", @@ -180,7 +176,7 @@ SENSOR_TYPES = ( ), ComfoconnectSensorEntityDescription( key=ATTR_EXHAUST_FAN_DUTY, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, name="Exhaust fan duty", native_unit_of_measurement=PERCENTAGE, icon="mdi:fan-minus", @@ -188,8 +184,8 @@ SENSOR_TYPES = ( ), ComfoconnectSensorEntityDescription( key=ATTR_EXHAUST_TEMPERATURE, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, name="Exhaust temperature", native_unit_of_measurement=TEMP_CELSIUS, sensor_id=SENSOR_TEMPERATURE_EXHAUST, @@ -197,15 +193,15 @@ SENSOR_TYPES = ( ), ComfoconnectSensorEntityDescription( key=ATTR_EXHAUST_HUMIDITY, - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, name="Exhaust humidity", native_unit_of_measurement=PERCENTAGE, sensor_id=SENSOR_HUMIDITY_EXHAUST, ), ComfoconnectSensorEntityDescription( key=ATTR_AIR_FLOW_SUPPLY, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, name="Supply airflow", native_unit_of_measurement=VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR, icon="mdi:fan-plus", @@ -213,7 +209,7 @@ SENSOR_TYPES = ( ), ComfoconnectSensorEntityDescription( key=ATTR_AIR_FLOW_EXHAUST, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, name="Exhaust airflow", native_unit_of_measurement=VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR, icon="mdi:fan-minus", @@ -221,7 +217,7 @@ SENSOR_TYPES = ( ), ComfoconnectSensorEntityDescription( key=ATTR_BYPASS_STATE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, name="Bypass state", native_unit_of_measurement=PERCENTAGE, icon="mdi:camera-iris", @@ -236,32 +232,32 @@ SENSOR_TYPES = ( ), ComfoconnectSensorEntityDescription( key=ATTR_POWER_CURRENT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, name="Power usage", native_unit_of_measurement=POWER_WATT, sensor_id=SENSOR_POWER_CURRENT, ), ComfoconnectSensorEntityDescription( key=ATTR_POWER_TOTAL, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, name="Energy total", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, sensor_id=SENSOR_POWER_TOTAL, ), ComfoconnectSensorEntityDescription( key=ATTR_PREHEATER_POWER_CURRENT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, name="Preheater power usage", native_unit_of_measurement=POWER_WATT, sensor_id=SENSOR_PREHEATER_POWER_CURRENT, ), ComfoconnectSensorEntityDescription( key=ATTR_PREHEATER_POWER_TOTAL, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, name="Preheater energy total", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, sensor_id=SENSOR_PREHEATER_POWER_TOTAL, From 8df0bc9d57c0b0e2c987e4ccaab2d392e9dcf88e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 9 Dec 2021 09:24:07 +0100 Subject: [PATCH 0232/2644] Use new BinarySensorDeviceClass enum in concord232 (#61335) Co-authored-by: epenet --- .../components/concord232/binary_sensor.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/concord232/binary_sensor.py b/homeassistant/components/concord232/binary_sensor.py index 7fd1d9748b3..3f21489de20 100644 --- a/homeassistant/components/concord232/binary_sensor.py +++ b/homeassistant/components/concord232/binary_sensor.py @@ -7,12 +7,9 @@ import requests import voluptuous as vol from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_MOTION, - DEVICE_CLASS_OPENING, - DEVICE_CLASS_SAFETY, - DEVICE_CLASS_SMOKE, DEVICE_CLASSES, PLATFORM_SCHEMA, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.const import CONF_HOST, CONF_PORT @@ -89,14 +86,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): def get_opening_type(zone): """Return the result of the type guessing from name.""" if "MOTION" in zone["name"]: - return DEVICE_CLASS_MOTION + return BinarySensorDeviceClass.MOTION if "KEY" in zone["name"]: - return DEVICE_CLASS_SAFETY + return BinarySensorDeviceClass.SAFETY if "SMOKE" in zone["name"]: - return DEVICE_CLASS_SMOKE + return BinarySensorDeviceClass.SMOKE if "WATER" in zone["name"]: return "water" - return DEVICE_CLASS_OPENING + return BinarySensorDeviceClass.OPENING class Concord232ZoneSensor(BinarySensorEntity): From c5ffeb7809bdf1393b0a98f9e8fec32fa04ff213 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 9 Dec 2021 09:40:12 +0100 Subject: [PATCH 0233/2644] Use new DeviceClass and EntityCategory enums in cloud (#61332) Co-authored-by: epenet --- homeassistant/components/cloud/binary_sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/cloud/binary_sensor.py b/homeassistant/components/cloud/binary_sensor.py index a27364c715f..b537c447120 100644 --- a/homeassistant/components/cloud/binary_sensor.py +++ b/homeassistant/components/cloud/binary_sensor.py @@ -2,11 +2,11 @@ import asyncio from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_CONNECTIVITY, + BinarySensorDeviceClass, BinarySensorEntity, ) -from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import EntityCategory from .const import DISPATCHER_REMOTE_UPDATE, DOMAIN @@ -26,10 +26,10 @@ class CloudRemoteBinary(BinarySensorEntity): """Representation of an Cloud Remote UI Connection binary sensor.""" _attr_name = "Remote UI" - _attr_device_class = DEVICE_CLASS_CONNECTIVITY + _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY _attr_should_poll = False _attr_unique_id = "cloud-remote-ui-connectivity" - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_entity_category = EntityCategory.DIAGNOSTIC def __init__(self, cloud): """Initialize the binary sensor.""" From 11ee0fb1d0f233e8543a2a86d50a98d47d7c906a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 9 Dec 2021 10:17:34 +0100 Subject: [PATCH 0234/2644] Upgrade tailscale to 0.1.4 (#61338) --- homeassistant/components/tailscale/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tailscale/manifest.json b/homeassistant/components/tailscale/manifest.json index e1b2435b989..4d47e397b76 100644 --- a/homeassistant/components/tailscale/manifest.json +++ b/homeassistant/components/tailscale/manifest.json @@ -3,7 +3,7 @@ "name": "Tailscale", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tailscale", - "requirements": ["tailscale==0.1.3"], + "requirements": ["tailscale==0.1.4"], "codeowners": ["@frenck"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index eb70fef8bfa..6144e14bf6e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2281,7 +2281,7 @@ systembridge==2.2.3 tahoma-api==0.0.16 # homeassistant.components.tailscale -tailscale==0.1.3 +tailscale==0.1.4 # homeassistant.components.tank_utility tank_utility==1.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 183e515e702..35aae4e6b15 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1364,7 +1364,7 @@ surepy==0.7.2 systembridge==2.2.3 # homeassistant.components.tailscale -tailscale==0.1.3 +tailscale==0.1.4 # homeassistant.components.tellduslive tellduslive==0.10.11 From 22f71a89e021cf3d877367b9023e4020ee552758 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Thu, 9 Dec 2021 10:28:51 +0100 Subject: [PATCH 0235/2644] Use new SensorDeviceClass and SensorStateClass in velbus (#61339) --- homeassistant/components/velbus/sensor.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/velbus/sensor.py b/homeassistant/components/velbus/sensor.py index 34642dd3bf1..86e9a606d36 100644 --- a/homeassistant/components/velbus/sensor.py +++ b/homeassistant/components/velbus/sensor.py @@ -4,16 +4,11 @@ from __future__ import annotations from velbusaio.channels import ButtonCounter, LightSensor, SensorNumber, Temperature from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_TEMPERATURE, -) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -58,19 +53,19 @@ class VelbusSensor(VelbusEntity, SensorEntity): self._attr_name = f"{self._attr_name}-counter" # define the device class if self._is_counter: - self._attr_device_class = DEVICE_CLASS_ENERGY + self._attr_device_class = SensorDeviceClass.ENERGY elif channel.is_counter_channel(): - self._attr_device_class = DEVICE_CLASS_POWER + self._attr_device_class = SensorDeviceClass.POWER elif channel.is_temperature(): - self._attr_device_class = DEVICE_CLASS_TEMPERATURE + self._attr_device_class = SensorDeviceClass.TEMPERATURE # define the icon if self._is_counter: self._attr_icon = "mdi:counter" # the state class if self._is_counter: - self._attr_state_class = STATE_CLASS_TOTAL_INCREASING + self._attr_state_class = SensorStateClass.TOTAL_INCREASING else: - self._attr_state_class = STATE_CLASS_MEASUREMENT + self._attr_state_class = SensorStateClass.MEASUREMENT # unit if self._is_counter: self._attr_native_unit_of_measurement = channel.get_counter_unit() From 8e58ea8397560a83585386566f805856120af0f3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 9 Dec 2021 10:49:19 +0100 Subject: [PATCH 0236/2644] Correct state class for Tasmota sensors (#61236) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/tasmota/sensor.py | 146 +++++++++++++-------- 1 file changed, 94 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index 678d3eaf4fa..45ff93b5945 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -10,25 +10,15 @@ from hatasmota.models import DiscoveryHashType from homeassistant.components import sensor from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_CO2, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_POWER, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_SIGNAL_STRENGTH, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_TIMESTAMP, ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, @@ -63,28 +53,54 @@ STATE_CLASS = "state_class" ICON = "icon" # A Tasmota sensor type may be mapped to either a device class or an icon, not both -SENSOR_DEVICE_CLASS_ICON_MAP = { - hc.SENSOR_AMBIENT: {DEVICE_CLASS: DEVICE_CLASS_ILLUMINANCE}, - hc.SENSOR_APPARENT_POWERUSAGE: {DEVICE_CLASS: DEVICE_CLASS_POWER}, - hc.SENSOR_BATTERY: { - DEVICE_CLASS: DEVICE_CLASS_BATTERY, - STATE_CLASS: STATE_CLASS_MEASUREMENT, +SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { + hc.SENSOR_AMBIENT: { + DEVICE_CLASS: SensorDeviceClass.ILLUMINANCE, + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_APPARENT_POWERUSAGE: { + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_BATTERY: { + DEVICE_CLASS: SensorDeviceClass.BATTERY, + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_CCT: { + ICON: "mdi:temperature-kelvin", + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_CO2: { + DEVICE_CLASS: SensorDeviceClass.CO2, + STATE_CLASS: SensorStateClass.MEASUREMENT, }, - hc.SENSOR_CCT: {ICON: "mdi:temperature-kelvin"}, - hc.SENSOR_CO2: {DEVICE_CLASS: DEVICE_CLASS_CO2}, hc.SENSOR_COLOR_BLUE: {ICON: "mdi:palette"}, hc.SENSOR_COLOR_GREEN: {ICON: "mdi:palette"}, hc.SENSOR_COLOR_RED: {ICON: "mdi:palette"}, - hc.SENSOR_CURRENT: {ICON: "mdi:alpha-a-circle-outline"}, - hc.SENSOR_DEWPOINT: {ICON: "mdi:weather-rainy"}, - hc.SENSOR_DISTANCE: {ICON: "mdi:leak"}, - hc.SENSOR_ECO2: {ICON: "mdi:molecule-co2"}, - hc.SENSOR_FREQUENCY: {ICON: "mdi:current-ac"}, - hc.SENSOR_HUMIDITY: { - DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, - STATE_CLASS: STATE_CLASS_MEASUREMENT, + hc.SENSOR_CURRENT: { + ICON: "mdi:alpha-a-circle-outline", + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_DEWPOINT: { + ICON: "mdi:weather-rainy", + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_DISTANCE: { + ICON: "mdi:leak", + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_ECO2: {ICON: "mdi:molecule-co2"}, + hc.SENSOR_FREQUENCY: { + DEVICE_CLASS: SensorDeviceClass.FREQUENCY, + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_HUMIDITY: { + DEVICE_CLASS: SensorDeviceClass.HUMIDITY, + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_ILLUMINANCE: { + DEVICE_CLASS: SensorDeviceClass.ILLUMINANCE, + STATE_CLASS: SensorStateClass.MEASUREMENT, }, - hc.SENSOR_ILLUMINANCE: {DEVICE_CLASS: DEVICE_CLASS_ILLUMINANCE}, hc.SENSOR_STATUS_IP: {ICON: "mdi:ip-network"}, hc.SENSOR_STATUS_LINK_COUNT: {ICON: "mdi:counter"}, hc.SENSOR_MOISTURE: {ICON: "mdi:cup-water"}, @@ -95,40 +111,66 @@ SENSOR_DEVICE_CLASS_ICON_MAP = { hc.SENSOR_PB1: {ICON: "mdi:flask"}, hc.SENSOR_PB2_5: {ICON: "mdi:flask"}, hc.SENSOR_PB5: {ICON: "mdi:flask"}, - hc.SENSOR_PM10: {ICON: "mdi:air-filter"}, - hc.SENSOR_PM1: {ICON: "mdi:air-filter"}, - hc.SENSOR_PM2_5: {ICON: "mdi:air-filter"}, - hc.SENSOR_POWERFACTOR: {ICON: "mdi:alpha-f-circle-outline"}, - hc.SENSOR_POWERUSAGE: {DEVICE_CLASS: DEVICE_CLASS_POWER}, + hc.SENSOR_PM10: { + DEVICE_CLASS: SensorDeviceClass.PM10, + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_PM1: { + DEVICE_CLASS: SensorDeviceClass.PM1, + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_PM2_5: { + DEVICE_CLASS: SensorDeviceClass.PM25, + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_POWERFACTOR: { + ICON: "mdi:alpha-f-circle-outline", + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_POWERUSAGE: { + DEVICE_CLASS: SensorDeviceClass.POWER, + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, hc.SENSOR_PRESSURE: { - DEVICE_CLASS: DEVICE_CLASS_PRESSURE, - STATE_CLASS: STATE_CLASS_MEASUREMENT, + DEVICE_CLASS: SensorDeviceClass.PRESSURE, + STATE_CLASS: SensorStateClass.MEASUREMENT, }, hc.SENSOR_PRESSUREATSEALEVEL: { - DEVICE_CLASS: DEVICE_CLASS_PRESSURE, - STATE_CLASS: STATE_CLASS_MEASUREMENT, + DEVICE_CLASS: SensorDeviceClass.PRESSURE, + STATE_CLASS: SensorStateClass.MEASUREMENT, }, hc.SENSOR_PROXIMITY: {ICON: "mdi:ruler"}, - hc.SENSOR_REACTIVE_POWERUSAGE: {DEVICE_CLASS: DEVICE_CLASS_POWER}, - hc.SENSOR_STATUS_LAST_RESTART_TIME: {DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP}, + hc.SENSOR_REACTIVE_POWERUSAGE: { + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_STATUS_LAST_RESTART_TIME: {DEVICE_CLASS: SensorDeviceClass.TIMESTAMP}, hc.SENSOR_STATUS_RESTART_REASON: {ICON: "mdi:information-outline"}, - hc.SENSOR_STATUS_SIGNAL: {DEVICE_CLASS: DEVICE_CLASS_SIGNAL_STRENGTH}, - hc.SENSOR_STATUS_RSSI: {ICON: "mdi:access-point"}, + hc.SENSOR_STATUS_SIGNAL: { + DEVICE_CLASS: SensorDeviceClass.SIGNAL_STRENGTH, + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_STATUS_RSSI: { + ICON: "mdi:access-point", + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, hc.SENSOR_STATUS_SSID: {ICON: "mdi:access-point-network"}, hc.SENSOR_TEMPERATURE: { - DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - STATE_CLASS: STATE_CLASS_MEASUREMENT, + DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, + STATE_CLASS: SensorStateClass.MEASUREMENT, }, - hc.SENSOR_TODAY: {DEVICE_CLASS: DEVICE_CLASS_ENERGY}, + hc.SENSOR_TODAY: {DEVICE_CLASS: SensorDeviceClass.ENERGY}, hc.SENSOR_TOTAL: { - DEVICE_CLASS: DEVICE_CLASS_ENERGY, - STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + DEVICE_CLASS: SensorDeviceClass.ENERGY, + STATE_CLASS: SensorStateClass.TOTAL_INCREASING, }, hc.SENSOR_TOTAL_START_TIME: {ICON: "mdi:progress-clock"}, hc.SENSOR_TVOC: {ICON: "mdi:air-filter"}, - hc.SENSOR_VOLTAGE: {ICON: "mdi:alpha-v-circle-outline"}, - hc.SENSOR_WEIGHT: {ICON: "mdi:scale"}, - hc.SENSOR_YESTERDAY: {DEVICE_CLASS: DEVICE_CLASS_ENERGY}, + hc.SENSOR_VOLTAGE: { + ICON: "mdi:alpha-v-circle-outline", + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + hc.SENSOR_WEIGHT: {ICON: "mdi:scale", STATE_CLASS: SensorStateClass.MEASUREMENT}, + hc.SENSOR_YESTERDAY: {DEVICE_CLASS: SensorDeviceClass.ENERGY}, } SENSOR_UNIT_MAP = { @@ -208,7 +250,7 @@ class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, SensorEntity): @callback def sensor_state_updated(self, state: Any, **kwargs: Any) -> None: """Handle state updates.""" - if self.device_class == DEVICE_CLASS_TIMESTAMP: + if self.device_class == SensorDeviceClass.TIMESTAMP: self._state_timestamp = state else: self._state = state @@ -261,7 +303,7 @@ class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, SensorEntity): @property def native_value(self) -> datetime | str | None: """Return the state of the entity.""" - if self._state_timestamp and self.device_class == DEVICE_CLASS_TIMESTAMP: + if self._state_timestamp and self.device_class == SensorDeviceClass.TIMESTAMP: return self._state_timestamp return self._state From cfb10029209396ae8594df8378e0b4c6599fc3ff Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 9 Dec 2021 11:07:58 +0100 Subject: [PATCH 0237/2644] Use new DeviceClass and StateClass enums in daikin (#61340) Co-authored-by: epenet --- homeassistant/components/daikin/sensor.py | 29 ++++++++++------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/daikin/sensor.py b/homeassistant/components/daikin/sensor.py index 2ca91aa2780..cf91d65b4a6 100644 --- a/homeassistant/components/daikin/sensor.py +++ b/homeassistant/components/daikin/sensor.py @@ -7,15 +7,12 @@ from dataclasses import dataclass from pydaikin.daikin_base import Appliance from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.const import ( - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_TEMPERATURE, ENERGY_KILO_WATT_HOUR, FREQUENCY_HERTZ, PERCENTAGE, @@ -52,39 +49,39 @@ SENSOR_TYPES: tuple[DaikinSensorEntityDescription, ...] = ( DaikinSensorEntityDescription( key=ATTR_INSIDE_TEMPERATURE, name="Inside Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, value_func=lambda device: device.inside_temperature, ), DaikinSensorEntityDescription( key=ATTR_OUTSIDE_TEMPERATURE, name="Outside Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, value_func=lambda device: device.outside_temperature, ), DaikinSensorEntityDescription( key=ATTR_HUMIDITY, name="Humidity", - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, value_func=lambda device: device.humidity, ), DaikinSensorEntityDescription( key=ATTR_TARGET_HUMIDITY, name="Target Humidity", - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, value_func=lambda device: device.humidity, ), DaikinSensorEntityDescription( key=ATTR_TOTAL_POWER, name="Total Power Consumption", - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, native_unit_of_measurement=POWER_KILO_WATT, value_func=lambda device: round(device.current_total_power_consumption, 2), ), @@ -92,7 +89,7 @@ SENSOR_TYPES: tuple[DaikinSensorEntityDescription, ...] = ( key=ATTR_COOL_ENERGY, name="Cool Energy Consumption", icon="mdi:snowflake", - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value_func=lambda device: round(device.last_hour_cool_energy_consumption, 2), ), @@ -100,7 +97,7 @@ SENSOR_TYPES: tuple[DaikinSensorEntityDescription, ...] = ( key=ATTR_HEAT_ENERGY, name="Heat Energy Consumption", icon="mdi:fire", - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value_func=lambda device: round(device.last_hour_heat_energy_consumption, 2), ), From f4d66f67d5e7868362b3e5814b34e029d1dca3d6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 9 Dec 2021 11:10:26 +0100 Subject: [PATCH 0238/2644] Use new DeviceClass and StateClass enums in darksky (#61342) Co-authored-by: epenet --- homeassistant/components/darksky/sensor.py | 43 ++++++++++------------ 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/darksky/sensor.py b/homeassistant/components/darksky/sensor.py index 6d1711b4d0e..d6bcf404e85 100644 --- a/homeassistant/components/darksky/sensor.py +++ b/homeassistant/components/darksky/sensor.py @@ -11,11 +11,11 @@ from requests.exceptions import ConnectionError as ConnectError, HTTPError, Time import voluptuous as vol from homeassistant.components.sensor import ( - DEVICE_CLASS_TEMPERATURE, PLATFORM_SCHEMA, - STATE_CLASS_MEASUREMENT, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.const import ( ATTR_ATTRIBUTION, @@ -26,9 +26,6 @@ from homeassistant.const import ( CONF_NAME, CONF_SCAN_INTERVAL, DEGREE, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_OZONE, - DEVICE_CLASS_PRESSURE, LENGTH_CENTIMETERS, LENGTH_INCHES, LENGTH_KILOMETERS, @@ -181,8 +178,8 @@ SENSOR_TYPES: dict[str, DarkskySensorEntityDescription] = { "temperature": DarkskySensorEntityDescription( key="temperature", name="Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, si_unit=TEMP_CELSIUS, us_unit=TEMP_FAHRENHEIT, ca_unit=TEMP_CELSIUS, @@ -193,8 +190,8 @@ SENSOR_TYPES: dict[str, DarkskySensorEntityDescription] = { "apparent_temperature": DarkskySensorEntityDescription( key="apparent_temperature", name="Apparent Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, si_unit=TEMP_CELSIUS, us_unit=TEMP_FAHRENHEIT, ca_unit=TEMP_CELSIUS, @@ -205,8 +202,8 @@ SENSOR_TYPES: dict[str, DarkskySensorEntityDescription] = { "dew_point": DarkskySensorEntityDescription( key="dew_point", name="Dew Point", - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, si_unit=TEMP_CELSIUS, us_unit=TEMP_FAHRENHEIT, ca_unit=TEMP_CELSIUS, @@ -261,8 +258,8 @@ SENSOR_TYPES: dict[str, DarkskySensorEntityDescription] = { "humidity": DarkskySensorEntityDescription( key="humidity", name="Humidity", - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, si_unit=PERCENTAGE, us_unit=PERCENTAGE, ca_unit=PERCENTAGE, @@ -273,7 +270,7 @@ SENSOR_TYPES: dict[str, DarkskySensorEntityDescription] = { "pressure": DarkskySensorEntityDescription( key="pressure", name="Pressure", - device_class=DEVICE_CLASS_PRESSURE, + device_class=SensorDeviceClass.PRESSURE, si_unit=PRESSURE_MBAR, us_unit=PRESSURE_MBAR, ca_unit=PRESSURE_MBAR, @@ -295,7 +292,7 @@ SENSOR_TYPES: dict[str, DarkskySensorEntityDescription] = { "ozone": DarkskySensorEntityDescription( key="ozone", name="Ozone", - device_class=DEVICE_CLASS_OZONE, + device_class=SensorDeviceClass.OZONE, si_unit="DU", us_unit="DU", ca_unit="DU", @@ -306,7 +303,7 @@ SENSOR_TYPES: dict[str, DarkskySensorEntityDescription] = { "apparent_temperature_max": DarkskySensorEntityDescription( key="apparent_temperature_max", name="Daily High Apparent Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, si_unit=TEMP_CELSIUS, us_unit=TEMP_FAHRENHEIT, ca_unit=TEMP_CELSIUS, @@ -317,7 +314,7 @@ SENSOR_TYPES: dict[str, DarkskySensorEntityDescription] = { "apparent_temperature_high": DarkskySensorEntityDescription( key="apparent_temperature_high", name="Daytime High Apparent Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, si_unit=TEMP_CELSIUS, us_unit=TEMP_FAHRENHEIT, ca_unit=TEMP_CELSIUS, @@ -328,7 +325,7 @@ SENSOR_TYPES: dict[str, DarkskySensorEntityDescription] = { "apparent_temperature_min": DarkskySensorEntityDescription( key="apparent_temperature_min", name="Daily Low Apparent Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, si_unit=TEMP_CELSIUS, us_unit=TEMP_FAHRENHEIT, ca_unit=TEMP_CELSIUS, @@ -339,7 +336,7 @@ SENSOR_TYPES: dict[str, DarkskySensorEntityDescription] = { "apparent_temperature_low": DarkskySensorEntityDescription( key="apparent_temperature_low", name="Overnight Low Apparent Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, si_unit=TEMP_CELSIUS, us_unit=TEMP_FAHRENHEIT, ca_unit=TEMP_CELSIUS, @@ -350,7 +347,7 @@ SENSOR_TYPES: dict[str, DarkskySensorEntityDescription] = { "temperature_max": DarkskySensorEntityDescription( key="temperature_max", name="Daily High Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, si_unit=TEMP_CELSIUS, us_unit=TEMP_FAHRENHEIT, ca_unit=TEMP_CELSIUS, @@ -361,7 +358,7 @@ SENSOR_TYPES: dict[str, DarkskySensorEntityDescription] = { "temperature_high": DarkskySensorEntityDescription( key="temperature_high", name="Daytime High Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, si_unit=TEMP_CELSIUS, us_unit=TEMP_FAHRENHEIT, ca_unit=TEMP_CELSIUS, @@ -372,7 +369,7 @@ SENSOR_TYPES: dict[str, DarkskySensorEntityDescription] = { "temperature_min": DarkskySensorEntityDescription( key="temperature_min", name="Daily Low Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, si_unit=TEMP_CELSIUS, us_unit=TEMP_FAHRENHEIT, ca_unit=TEMP_CELSIUS, @@ -383,7 +380,7 @@ SENSOR_TYPES: dict[str, DarkskySensorEntityDescription] = { "temperature_low": DarkskySensorEntityDescription( key="temperature_low", name="Overnight Low Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, si_unit=TEMP_CELSIUS, us_unit=TEMP_FAHRENHEIT, ca_unit=TEMP_CELSIUS, From 5b8f8772d2557e7b2bb3c945ce4515a96bf500d1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 9 Dec 2021 11:39:57 +0100 Subject: [PATCH 0239/2644] Use _attr_* in delijn (#61344) Co-authored-by: epenet --- homeassistant/components/delijn/sensor.py | 78 +++++++++-------------- 1 file changed, 29 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/delijn/sensor.py b/homeassistant/components/delijn/sensor.py index 310e6a7f7fc..6db89e95675 100644 --- a/homeassistant/components/delijn/sensor.py +++ b/homeassistant/components/delijn/sensor.py @@ -5,8 +5,12 @@ from pydelijn.api import Passages from pydelijn.common import HttpException import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, DEVICE_CLASS_TIMESTAMP +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorDeviceClass, + SensorEntity, +) +from homeassistant.const import CONF_API_KEY from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -60,72 +64,48 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class DeLijnPublicTransportSensor(SensorEntity): """Representation of a Ruter sensor.""" - _attr_device_class = DEVICE_CLASS_TIMESTAMP + _attr_attribution = ATTRIBUTION + _attr_device_class = SensorDeviceClass.TIMESTAMP + _attr_icon = "mdi:bus" def __init__(self, line): """Initialize the sensor.""" self.line = line - self._attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} - self._name = None - self._state = None - self._available = True + self._attr_extra_state_attributes = {} async def async_update(self): """Get the latest data from the De Lijn API.""" try: await self.line.get_passages() - self._name = await self.line.get_stopname() + self._attr_name = await self.line.get_stopname() except HttpException: - self._available = False + self._attr_available = False _LOGGER.error("De Lijn http error") return - self._attributes["stopname"] = self._name + self._attr_extra_state_attributes["stopname"] = self._attr_name if not self.line.passages: - self._available = False + self._attr_available = False return try: first = self.line.passages[0] - if first["due_at_realtime"] is not None: - first_passage = first["due_at_realtime"] - else: + if (first_passage := first["due_at_realtime"]) is None: first_passage = first["due_at_schedule"] - self._state = first_passage - self._attributes["line_number_public"] = first["line_number_public"] - self._attributes["line_transport_type"] = first["line_transport_type"] - self._attributes["final_destination"] = first["final_destination"] - self._attributes["due_at_schedule"] = first["due_at_schedule"] - self._attributes["due_at_realtime"] = first["due_at_realtime"] - self._attributes["is_realtime"] = first["is_realtime"] - self._attributes["next_passages"] = self.line.passages - self._available = True + self._attr_native_value = first_passage + self._attr_extra_state_attributes.update( + { + "line_number_public": first["line_number_public"], + "line_transport_type": first["line_transport_type"], + "final_destination": first["final_destination"], + "due_at_schedule": first["due_at_schedule"], + "due_at_realtime": first["due_at_realtime"], + "is_realtime": first["is_realtime"], + "next_passages": self.line.passages, + } + ) + self._attr_available = True except (KeyError) as error: _LOGGER.error("Invalid data received from De Lijn: %s", error) - self._available = False - - @property - def available(self): - """Return True if entity is available.""" - return self._available - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state - - @property - def icon(self): - """Return the icon of the sensor.""" - return "mdi:bus" - - @property - def extra_state_attributes(self): - """Return attributes for the sensor.""" - return self._attributes + self._attr_available = False From 1ed490ce62177ddc076b75b99ee1dfe59bb71747 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 9 Dec 2021 11:43:48 +0100 Subject: [PATCH 0240/2644] Fix date/datetime support for templates (#61088) Co-authored-by: Paulus Schoutsen --- homeassistant/components/template/sensor.py | 65 +++++++- tests/components/template/test_sensor.py | 166 ++++++++++++++++++++ 2 files changed, 229 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index a31e49db570..18ae8af8569 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -1,6 +1,8 @@ """Allows the creation of a sensor that breaks out state_attributes.""" from __future__ import annotations +from datetime import date, datetime +import logging from typing import Any import voluptuous as vol @@ -12,6 +14,7 @@ from homeassistant.components.sensor import ( ENTITY_ID_FORMAT, PLATFORM_SCHEMA, STATE_CLASSES_SCHEMA, + SensorDeviceClass, SensorEntity, ) from homeassistant.const import ( @@ -32,6 +35,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.util import dt as dt_util from .const import ( CONF_ATTRIBUTE_TEMPLATES, @@ -85,6 +89,7 @@ LEGACY_SENSOR_SCHEMA = vol.All( } ), ) +_LOGGER = logging.getLogger(__name__) def extra_validation_checks(val): @@ -179,6 +184,32 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) +@callback +def _async_parse_date_datetime( + value: str, entity_id: str, device_class: SensorDeviceClass | str | None +) -> datetime | date | None: + """Parse datetime.""" + if device_class == SensorDeviceClass.TIMESTAMP: + if (parsed_timestamp := dt_util.parse_datetime(value)) is None: + _LOGGER.warning("%s rendered invalid timestamp: %s", entity_id, value) + return None + + if parsed_timestamp.tzinfo is None: + _LOGGER.warning( + "%s rendered timestamp without timezone: %s", entity_id, value + ) + return None + + return parsed_timestamp + + # Date device class + if (parsed_date := dt_util.parse_date(value)) is not None: + return parsed_date + + _LOGGER.warning("%s rendered invalid date %s", entity_id, value) + return None + + class SensorTemplate(TemplateEntity, SensorEntity): """Representation of a Template Sensor.""" @@ -227,7 +258,20 @@ class SensorTemplate(TemplateEntity, SensorEntity): @callback def _update_state(self, result): super()._update_state(result) - self._attr_native_value = None if isinstance(result, TemplateError) else result + if isinstance(result, TemplateError): + self._attr_native_value = None + return + + if result is None or self.device_class not in ( + SensorDeviceClass.DATE, + SensorDeviceClass.TIMESTAMP, + ): + self._attr_native_value = result + return + + self._attr_native_value = _async_parse_date_datetime( + result, self.entity_id, self.device_class + ) class TriggerSensorEntity(TriggerEntity, SensorEntity): @@ -237,7 +281,7 @@ class TriggerSensorEntity(TriggerEntity, SensorEntity): extra_template_keys = (CONF_STATE,) @property - def native_value(self) -> str | None: + def native_value(self) -> str | datetime | date | None: """Return state of the sensor.""" return self._rendered.get(CONF_STATE) @@ -245,3 +289,20 @@ class TriggerSensorEntity(TriggerEntity, SensorEntity): def state_class(self) -> str | None: """Sensor state class.""" return self._config.get(CONF_STATE_CLASS) + + @callback + def _process_data(self) -> None: + """Process new data.""" + super()._process_data() + + if ( + state := self._rendered.get(CONF_STATE) + ) is None or self.device_class not in ( + SensorDeviceClass.DATE, + SensorDeviceClass.TIMESTAMP, + ): + return + + self._rendered[CONF_STATE] = _async_parse_date_datetime( + state, self.entity_id, self.device_class + ) diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index 189fe3653f2..0352080bed8 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -1094,3 +1094,169 @@ async def test_trigger_entity_available(hass): state = hass.states.get("sensor.maybe_available") assert state.state == "unavailable" + + +async def test_trigger_entity_device_class_parsing_works(hass): + """Test trigger entity device class parsing works.""" + assert await async_setup_component( + hass, + "template", + { + "template": [ + { + "trigger": {"platform": "event", "event_type": "test_event"}, + "sensor": [ + { + "name": "Date entity", + "state": "{{ now().date() }}", + "device_class": "date", + }, + { + "name": "Timestamp entity", + "state": "{{ now() }}", + "device_class": "timestamp", + }, + ], + }, + ], + }, + ) + + await hass.async_block_till_done() + + now = dt_util.now() + + with patch("homeassistant.util.dt.now", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + + date_state = hass.states.get("sensor.date_entity") + assert date_state is not None + assert date_state.state == now.date().isoformat() + + ts_state = hass.states.get("sensor.timestamp_entity") + assert ts_state is not None + assert ts_state.state == now.isoformat(timespec="seconds") + + +async def test_trigger_entity_device_class_errors_works(hass): + """Test trigger entity device class errors works.""" + assert await async_setup_component( + hass, + "template", + { + "template": [ + { + "trigger": {"platform": "event", "event_type": "test_event"}, + "sensor": [ + { + "name": "Date entity", + "state": "invalid", + "device_class": "date", + }, + { + "name": "Timestamp entity", + "state": "invalid", + "device_class": "timestamp", + }, + ], + }, + ], + }, + ) + + await hass.async_block_till_done() + + now = dt_util.now() + + with patch("homeassistant.util.dt.now", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + + date_state = hass.states.get("sensor.date_entity") + assert date_state is not None + assert date_state.state == STATE_UNKNOWN + + ts_state = hass.states.get("sensor.timestamp_entity") + assert ts_state is not None + assert ts_state.state == STATE_UNKNOWN + + +async def test_entity_device_class_parsing_works(hass): + """Test entity device class parsing works.""" + now = dt_util.now() + + with patch("homeassistant.util.dt.now", return_value=now): + assert await async_setup_component( + hass, + "template", + { + "template": [ + { + "sensor": [ + { + "name": "Date entity", + "state": "{{ now().date() }}", + "device_class": "date", + }, + { + "name": "Timestamp entity", + "state": "{{ now() }}", + "device_class": "timestamp", + }, + ], + }, + ], + }, + ) + await hass.async_block_till_done() + + date_state = hass.states.get("sensor.date_entity") + assert date_state is not None + assert date_state.state == now.date().isoformat() + + ts_state = hass.states.get("sensor.timestamp_entity") + assert ts_state is not None + assert ts_state.state == now.isoformat(timespec="seconds") + + +async def test_entity_device_class_errors_works(hass): + """Test entity device class errors works.""" + assert await async_setup_component( + hass, + "template", + { + "template": [ + { + "sensor": [ + { + "name": "Date entity", + "state": "invalid", + "device_class": "date", + }, + { + "name": "Timestamp entity", + "state": "invalid", + "device_class": "timestamp", + }, + ], + }, + ], + }, + ) + + await hass.async_block_till_done() + + now = dt_util.now() + + with patch("homeassistant.util.dt.now", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + + date_state = hass.states.get("sensor.date_entity") + assert date_state is not None + assert date_state.state == STATE_UNKNOWN + + ts_state = hass.states.get("sensor.timestamp_entity") + assert ts_state is not None + assert ts_state.state == STATE_UNKNOWN From 50e034d4f0fda752ccf60c1286443a63b84fa653 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 9 Dec 2021 11:52:37 +0100 Subject: [PATCH 0241/2644] Use new SensorDeviceClass enum in dht (#61349) Co-authored-by: epenet --- homeassistant/components/dht/sensor.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/dht/sensor.py b/homeassistant/components/dht/sensor.py index 7c4ae5610f7..0b9248a44e8 100644 --- a/homeassistant/components/dht/sensor.py +++ b/homeassistant/components/dht/sensor.py @@ -10,6 +10,7 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, + SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, @@ -18,8 +19,6 @@ from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_PIN, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_TEMPERATURE, PERCENTAGE, TEMP_CELSIUS, ) @@ -44,14 +43,14 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key=SENSOR_TEMPERATURE, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_HUMIDITY, name="Humidity", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), ) From 487c44e11d2001a86593b9380b35ede32004b0ec Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 9 Dec 2021 11:55:42 +0100 Subject: [PATCH 0242/2644] Use _attr_* in dte_energy_bridge (#61353) Co-authored-by: epenet --- .../components/dte_energy_bridge/sensor.py | 42 +++++-------------- 1 file changed, 11 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/dte_energy_bridge/sensor.py b/homeassistant/components/dte_energy_bridge/sensor.py index d443047a171..9dd7cd79ac7 100644 --- a/homeassistant/components/dte_energy_bridge/sensor.py +++ b/homeassistant/components/dte_energy_bridge/sensor.py @@ -7,10 +7,10 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, - STATE_CLASS_MEASUREMENT, SensorEntity, + SensorStateClass, ) -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, POWER_KILO_WATT import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -46,7 +46,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class DteEnergyBridgeSensor(SensorEntity): """Implementation of the DTE Energy Bridge sensors.""" - _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_icon = ICON + _attr_native_unit_of_measurement = POWER_KILO_WATT + _attr_state_class = SensorStateClass.MEASUREMENT def __init__(self, ip_address, name, version): """Initialize the sensor.""" @@ -57,29 +59,7 @@ class DteEnergyBridgeSensor(SensorEntity): elif self._version == 2: self._url = f"http://{ip_address}:8888/zigbee/se/instantaneousdemand" - self._name = name - self._unit_of_measurement = "kW" - self._state = None - - @property - def name(self): - """Return the name of th sensor.""" - return self._name - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state - - @property - def native_unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return ICON + self._attr_name = name def update(self): """Get the energy usage data from the DTE energy bridge.""" @@ -87,7 +67,7 @@ class DteEnergyBridgeSensor(SensorEntity): response = requests.get(self._url, timeout=5) except (requests.exceptions.RequestException, ValueError): _LOGGER.warning( - "Could not update status for DTE Energy Bridge (%s)", self._name + "Could not update status for DTE Energy Bridge (%s)", self._attr_name ) return @@ -95,7 +75,7 @@ class DteEnergyBridgeSensor(SensorEntity): _LOGGER.warning( "Invalid status_code from DTE Energy Bridge: %s (%s)", response.status_code, - self._name, + self._attr_name, ) return @@ -105,7 +85,7 @@ class DteEnergyBridgeSensor(SensorEntity): _LOGGER.warning( 'Invalid response from DTE Energy Bridge: "%s" (%s)', response.text, - self._name, + self._attr_name, ) return @@ -118,6 +98,6 @@ class DteEnergyBridgeSensor(SensorEntity): # values in the format 000000.000 kW, but the scaling is Watts # NOT kWatts if self._version == 1 and "." in response_split[0]: - self._state = val + self._attr_native_value = val else: - self._state = val / 1000 + self._attr_native_value = val / 1000 From 7389cdaf055148fc427408f9ab66b22c08673d92 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 9 Dec 2021 12:03:45 +0100 Subject: [PATCH 0243/2644] Use new SensorStateClass enum in dsmr_reader (#61352) Co-authored-by: epenet --- .../components/dsmr_reader/definitions.py | 60 +++++++++---------- 1 file changed, 28 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/dsmr_reader/definitions.py b/homeassistant/components/dsmr_reader/definitions.py index 4645aef9a7a..259f0880abd 100644 --- a/homeassistant/components/dsmr_reader/definitions.py +++ b/homeassistant/components/dsmr_reader/definitions.py @@ -5,11 +5,7 @@ from collections.abc import Callable from dataclasses import dataclass from typing import Final -from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, - SensorEntityDescription, -) +from homeassistant.components.sensor import SensorEntityDescription, SensorStateClass from homeassistant.const import ( CURRENCY_EURO, DEVICE_CLASS_CURRENT, @@ -57,42 +53,42 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( name="Low tariff usage", device_class=DEVICE_CLASS_ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/reading/electricity_returned_1", name="Low tariff returned", device_class=DEVICE_CLASS_ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/reading/electricity_delivered_2", name="High tariff usage", device_class=DEVICE_CLASS_ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/reading/electricity_returned_2", name="High tariff returned", device_class=DEVICE_CLASS_ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/reading/electricity_currently_delivered", name="Current power usage", device_class=DEVICE_CLASS_POWER, native_unit_of_measurement=POWER_KILO_WATT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), DSMRReaderSensorEntityDescription( key="dsmr/reading/electricity_currently_returned", name="Current power return", device_class=DEVICE_CLASS_POWER, native_unit_of_measurement=POWER_KILO_WATT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), DSMRReaderSensorEntityDescription( key="dsmr/reading/phase_currently_delivered_l1", @@ -100,7 +96,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( entity_registry_enabled_default=False, device_class=DEVICE_CLASS_POWER, native_unit_of_measurement=POWER_KILO_WATT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), DSMRReaderSensorEntityDescription( key="dsmr/reading/phase_currently_delivered_l2", @@ -108,7 +104,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( entity_registry_enabled_default=False, device_class=DEVICE_CLASS_POWER, native_unit_of_measurement=POWER_KILO_WATT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), DSMRReaderSensorEntityDescription( key="dsmr/reading/phase_currently_delivered_l3", @@ -116,7 +112,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( entity_registry_enabled_default=False, device_class=DEVICE_CLASS_POWER, native_unit_of_measurement=POWER_KILO_WATT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), DSMRReaderSensorEntityDescription( key="dsmr/reading/phase_currently_returned_l1", @@ -124,7 +120,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( entity_registry_enabled_default=False, device_class=DEVICE_CLASS_POWER, native_unit_of_measurement=POWER_KILO_WATT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), DSMRReaderSensorEntityDescription( key="dsmr/reading/phase_currently_returned_l2", @@ -132,7 +128,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( entity_registry_enabled_default=False, device_class=DEVICE_CLASS_POWER, native_unit_of_measurement=POWER_KILO_WATT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), DSMRReaderSensorEntityDescription( key="dsmr/reading/phase_currently_returned_l3", @@ -140,7 +136,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( entity_registry_enabled_default=False, device_class=DEVICE_CLASS_POWER, native_unit_of_measurement=POWER_KILO_WATT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), DSMRReaderSensorEntityDescription( key="dsmr/reading/extra_device_delivered", @@ -148,7 +144,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( entity_registry_enabled_default=False, icon="mdi:fire", native_unit_of_measurement=VOLUME_CUBIC_METERS, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/reading/phase_voltage_l1", @@ -156,7 +152,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( entity_registry_enabled_default=False, device_class=DEVICE_CLASS_VOLTAGE, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), DSMRReaderSensorEntityDescription( key="dsmr/reading/phase_voltage_l2", @@ -164,7 +160,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( entity_registry_enabled_default=False, device_class=DEVICE_CLASS_VOLTAGE, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), DSMRReaderSensorEntityDescription( key="dsmr/reading/phase_voltage_l3", @@ -172,7 +168,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( entity_registry_enabled_default=False, device_class=DEVICE_CLASS_VOLTAGE, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), DSMRReaderSensorEntityDescription( key="dsmr/reading/phase_power_current_l1", @@ -180,7 +176,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( entity_registry_enabled_default=False, device_class=DEVICE_CLASS_CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), DSMRReaderSensorEntityDescription( key="dsmr/reading/phase_power_current_l2", @@ -188,7 +184,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( entity_registry_enabled_default=False, device_class=DEVICE_CLASS_CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), DSMRReaderSensorEntityDescription( key="dsmr/reading/phase_power_current_l3", @@ -196,7 +192,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( entity_registry_enabled_default=False, device_class=DEVICE_CLASS_CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), DSMRReaderSensorEntityDescription( key="dsmr/reading/timestamp", @@ -210,14 +206,14 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( name="Gas usage", device_class=DEVICE_CLASS_GAS, native_unit_of_measurement=VOLUME_CUBIC_METERS, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/consumption/gas/currently_delivered", name="Current gas usage", device_class=DEVICE_CLASS_GAS, native_unit_of_measurement=VOLUME_CUBIC_METERS, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), DSMRReaderSensorEntityDescription( key="dsmr/consumption/gas/read_at", @@ -231,42 +227,42 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( name="Low tariff usage", device_class=DEVICE_CLASS_ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity2", name="High tariff usage", device_class=DEVICE_CLASS_ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity1_returned", name="Low tariff return", device_class=DEVICE_CLASS_ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity2_returned", name="High tariff return", device_class=DEVICE_CLASS_ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity_merged", name="Power usage total", device_class=DEVICE_CLASS_ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity_returned_merged", name="Power return total", device_class=DEVICE_CLASS_ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity1_cost", From c5815ef936dae5263df5b327cffde0ac144d0d99 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 9 Dec 2021 12:04:25 +0100 Subject: [PATCH 0244/2644] Use new CoverDeviceClass enum in dynalite (#61354) Co-authored-by: epenet --- homeassistant/components/dynalite/cover.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/dynalite/cover.py b/homeassistant/components/dynalite/cover.py index 3e6c738f066..930ced4ff54 100644 --- a/homeassistant/components/dynalite/cover.py +++ b/homeassistant/components/dynalite/cover.py @@ -1,17 +1,13 @@ """Support for the Dynalite channels as covers.""" -from homeassistant.components.cover import ( - DEVICE_CLASS_SHUTTER, - DEVICE_CLASSES, - CoverEntity, -) +from homeassistant.components.cover import DEVICE_CLASSES, CoverDeviceClass, CoverEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from .dynalitebase import DynaliteBase, async_setup_entry_base -DEFAULT_COVER_CLASS = DEVICE_CLASS_SHUTTER +DEFAULT_COVER_CLASS = CoverDeviceClass.SHUTTER async def async_setup_entry( From 50940844b89735c0bcc6f1e47ddbef41b422a9d0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 9 Dec 2021 12:05:09 +0100 Subject: [PATCH 0245/2644] Use new enums in devolo_home_network (#61348) Co-authored-by: epenet --- homeassistant/components/devolo_home_network/sensor.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/devolo_home_network/sensor.py b/homeassistant/components/devolo_home_network/sensor.py index b0f68ae280e..08d61cd6eff 100644 --- a/homeassistant/components/devolo_home_network/sensor.py +++ b/homeassistant/components/devolo_home_network/sensor.py @@ -8,13 +8,13 @@ from typing import Any from devolo_plc_api.device import Device from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -44,7 +44,7 @@ class DevoloSensorEntityDescription( SENSOR_TYPES: dict[str, DevoloSensorEntityDescription] = { CONNECTED_PLC_DEVICES: DevoloSensorEntityDescription( key=CONNECTED_PLC_DEVICES, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:lan", name="Connected PLC devices", @@ -57,12 +57,12 @@ SENSOR_TYPES: dict[str, DevoloSensorEntityDescription] = { entity_registry_enabled_default=True, icon="mdi:wifi", name="Connected Wifi clients", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, value_func=lambda data: len(data["connected_stations"]), ), NEIGHBORING_WIFI_NETWORKS: DevoloSensorEntityDescription( key=NEIGHBORING_WIFI_NETWORKS, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:wifi-marker", name="Neighboring Wifi networks", From bcdeb06a4e3bb838531f602ac69087b282b0a9be Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 9 Dec 2021 12:06:04 +0100 Subject: [PATCH 0246/2644] Use new MediaPlayerDeviceClass enum in directv (#61351) Co-authored-by: epenet --- homeassistant/components/directv/media_player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/directv/media_player.py b/homeassistant/components/directv/media_player.py index 0f2dcabd552..b965eb4e7ea 100644 --- a/homeassistant/components/directv/media_player.py +++ b/homeassistant/components/directv/media_player.py @@ -6,7 +6,7 @@ import logging from directv import DIRECTV from homeassistant.components.media_player import ( - DEVICE_CLASS_RECEIVER, + MediaPlayerDeviceClass, MediaPlayerEntity, ) from homeassistant.components.media_player.const import ( @@ -96,7 +96,7 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity): self._attr_unique_id = self._device_id self._attr_name = name - self._attr_device_class = DEVICE_CLASS_RECEIVER + self._attr_device_class = MediaPlayerDeviceClass.RECEIVER self._attr_available = False self._is_recorded = None From b1a8e0b7963423b17f4f18ad4953698b2a00e6cf Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 9 Dec 2021 12:53:05 +0100 Subject: [PATCH 0247/2644] Use new enums in devolo_home_control (#61345) Co-authored-by: epenet --- .../devolo_home_control/binary_sensor.py | 30 +++++------- .../components/devolo_home_control/cover.py | 4 +- .../components/devolo_home_control/sensor.py | 46 ++++++++----------- 3 files changed, 34 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/devolo_home_control/binary_sensor.py b/homeassistant/components/devolo_home_control/binary_sensor.py index 4fadc8b5f46..fad6b91f155 100644 --- a/homeassistant/components/devolo_home_control/binary_sensor.py +++ b/homeassistant/components/devolo_home_control/binary_sensor.py @@ -5,30 +5,24 @@ from devolo_home_control_api.devices.zwave import Zwave from devolo_home_control_api.homecontrol import HomeControl from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_DOOR, - DEVICE_CLASS_HEAT, - DEVICE_CLASS_MOISTURE, - DEVICE_CLASS_MOTION, - DEVICE_CLASS_PROBLEM, - DEVICE_CLASS_SAFETY, - DEVICE_CLASS_SMOKE, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN from .devolo_device import DevoloDeviceEntity DEVICE_CLASS_MAPPING = { - "Water alarm": DEVICE_CLASS_MOISTURE, - "Home Security": DEVICE_CLASS_MOTION, - "Smoke Alarm": DEVICE_CLASS_SMOKE, - "Heat Alarm": DEVICE_CLASS_HEAT, - "door": DEVICE_CLASS_DOOR, - "overload": DEVICE_CLASS_SAFETY, + "Water alarm": BinarySensorDeviceClass.MOISTURE, + "Home Security": BinarySensorDeviceClass.MOTION, + "Smoke Alarm": BinarySensorDeviceClass.SMOKE, + "Heat Alarm": BinarySensorDeviceClass.HEAT, + "door": BinarySensorDeviceClass.DOOR, + "overload": BinarySensorDeviceClass.SAFETY, } @@ -95,12 +89,12 @@ class DevoloBinaryDeviceEntity(DevoloDeviceEntity, BinarySensorEntity): self._value = self._binary_sensor_property.state - if self._attr_device_class == DEVICE_CLASS_SAFETY: - self._attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + if self._attr_device_class == BinarySensorDeviceClass.SAFETY: + self._attr_entity_category = EntityCategory.DIAGNOSTIC if element_uid.startswith("devolo.WarningBinaryFI:"): - self._attr_device_class = DEVICE_CLASS_PROBLEM - self._attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + self._attr_device_class = BinarySensorDeviceClass.PROBLEM + self._attr_entity_category = EntityCategory.DIAGNOSTIC self._attr_entity_registry_enabled_default = False @property diff --git a/homeassistant/components/devolo_home_control/cover.py b/homeassistant/components/devolo_home_control/cover.py index 7a1a93596d3..0f82847660c 100644 --- a/homeassistant/components/devolo_home_control/cover.py +++ b/homeassistant/components/devolo_home_control/cover.py @@ -7,10 +7,10 @@ from devolo_home_control_api.devices.zwave import Zwave from devolo_home_control_api.homecontrol import HomeControl from homeassistant.components.cover import ( - DEVICE_CLASS_BLIND, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION, + CoverDeviceClass, CoverEntity, ) from homeassistant.config_entries import ConfigEntry @@ -55,7 +55,7 @@ class DevoloCoverDeviceEntity(DevoloMultiLevelSwitchDeviceEntity, CoverEntity): element_uid=element_uid, ) - self._attr_device_class = DEVICE_CLASS_BLIND + self._attr_device_class = CoverDeviceClass.BLIND self._attr_supported_features = ( SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION ) diff --git a/homeassistant/components/devolo_home_control/sensor.py b/homeassistant/components/devolo_home_control/sensor.py index c342f691b00..ab6ef87fab2 100644 --- a/homeassistant/components/devolo_home_control/sensor.py +++ b/homeassistant/components/devolo_home_control/sensor.py @@ -5,43 +5,37 @@ from devolo_home_control_api.devices.zwave import Zwave from devolo_home_control_api.homecontrol import HomeControl from homeassistant.components.sensor import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_POWER, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_VOLTAGE, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE +from homeassistant.const import PERCENTAGE from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN from .devolo_device import DevoloDeviceEntity DEVICE_CLASS_MAPPING = { - "battery": DEVICE_CLASS_BATTERY, - "temperature": DEVICE_CLASS_TEMPERATURE, - "light": DEVICE_CLASS_ILLUMINANCE, - "humidity": DEVICE_CLASS_HUMIDITY, - "current": DEVICE_CLASS_POWER, - "total": DEVICE_CLASS_ENERGY, - "voltage": DEVICE_CLASS_VOLTAGE, + "battery": SensorDeviceClass.BATTERY, + "temperature": SensorDeviceClass.TEMPERATURE, + "light": SensorDeviceClass.ILLUMINANCE, + "humidity": SensorDeviceClass.HUMIDITY, + "current": SensorDeviceClass.POWER, + "total": SensorDeviceClass.ENERGY, + "voltage": SensorDeviceClass.VOLTAGE, } STATE_CLASS_MAPPING = { - "battery": STATE_CLASS_MEASUREMENT, - "temperature": STATE_CLASS_MEASUREMENT, - "light": STATE_CLASS_MEASUREMENT, - "humidity": STATE_CLASS_MEASUREMENT, - "current": STATE_CLASS_MEASUREMENT, - "total": STATE_CLASS_TOTAL_INCREASING, - "voltage": STATE_CLASS_MEASUREMENT, + "battery": SensorStateClass.MEASUREMENT, + "temperature": SensorStateClass.MEASUREMENT, + "light": SensorStateClass.MEASUREMENT, + "humidity": SensorStateClass.MEASUREMENT, + "current": SensorStateClass.MEASUREMENT, + "total": SensorStateClass.TOTAL_INCREASING, + "voltage": SensorStateClass.MEASUREMENT, } @@ -147,7 +141,7 @@ class DevoloBatteryEntity(DevoloMultiLevelDeviceEntity): self._attr_device_class = DEVICE_CLASS_MAPPING.get("battery") self._attr_state_class = STATE_CLASS_MAPPING.get("battery") - self._attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + self._attr_entity_category = EntityCategory.DIAGNOSTIC self._attr_native_unit_of_measurement = PERCENTAGE self._value = device_instance.battery_level @@ -179,7 +173,7 @@ class DevoloConsumptionEntity(DevoloMultiLevelDeviceEntity): ) if consumption == "total": - self._attr_state_class = STATE_CLASS_TOTAL_INCREASING + self._attr_state_class = SensorStateClass.TOTAL_INCREASING self._value = getattr( device_instance.consumption_property[element_uid], consumption From f5c77ef5d02cb8e7eaa02f8e012c9722fd13a6b4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 9 Dec 2021 12:59:29 +0100 Subject: [PATCH 0248/2644] Use _attr_* in abode (#61357) Co-authored-by: epenet --- homeassistant/components/abode/__init__.py | 13 +++---------- .../components/abode/alarm_control_panel.py | 4 +--- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index 3da4155dafd..0d6ab95a318 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -8,7 +8,6 @@ from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol from homeassistant.const import ( - ATTR_ATTRIBUTION, ATTR_DATE, ATTR_DEVICE_ID, ATTR_ENTITY_ID, @@ -248,17 +247,13 @@ def setup_abode_events(hass): class AbodeEntity(Entity): """Representation of an Abode entity.""" + _attr_attribution = ATTRIBUTION + def __init__(self, data): """Initialize Abode entity.""" self._data = data - self._available = True self._attr_should_poll = data.polling - @property - def available(self): - """Return the available state.""" - return self._available - async def async_added_to_hass(self): """Subscribe to Abode connection status updates.""" await self.hass.async_add_executor_job( @@ -277,7 +272,7 @@ class AbodeEntity(Entity): def _update_connection_status(self): """Update the entity available property.""" - self._available = self._data.abode.events.connected + self._attr_available = self._data.abode.events.connected self.schedule_update_ha_state() @@ -315,7 +310,6 @@ class AbodeDevice(AbodeEntity): def extra_state_attributes(self): """Return the state attributes.""" return { - ATTR_ATTRIBUTION: ATTRIBUTION, "device_id": self._device.device_id, "battery_low": self._device.battery_low, "no_response": self._device.no_response, @@ -347,7 +341,6 @@ class AbodeAutomation(AbodeEntity): self._attr_name = automation.name self._attr_unique_id = automation.automation_id self._attr_extra_state_attributes = { - ATTR_ATTRIBUTION: ATTRIBUTION, "type": "CUE automation", } diff --git a/homeassistant/components/abode/alarm_control_panel.py b/homeassistant/components/abode/alarm_control_panel.py index 0cc1b500ff4..947e729db3a 100644 --- a/homeassistant/components/abode/alarm_control_panel.py +++ b/homeassistant/components/abode/alarm_control_panel.py @@ -5,14 +5,13 @@ from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_HOME, ) from homeassistant.const import ( - ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, ) from . import AbodeDevice -from .const import ATTRIBUTION, DOMAIN +from .const import DOMAIN ICON = "mdi:security" @@ -61,7 +60,6 @@ class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanelEntity): def extra_state_attributes(self): """Return the state attributes.""" return { - ATTR_ATTRIBUTION: ATTRIBUTION, "device_id": self._device.device_id, "battery_backup": self._device.battery, "cellular_backup": self._device.is_cellular, From 2e99fbc1f32c1a3f2368b87016c0bc490e91ac9a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 9 Dec 2021 13:22:22 +0100 Subject: [PATCH 0249/2644] Use new BinarySensorDeviceClass enum in digital_ocean (#61350) Co-authored-by: epenet --- homeassistant/components/digital_ocean/binary_sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/digital_ocean/binary_sensor.py b/homeassistant/components/digital_ocean/binary_sensor.py index af74e1ffb13..f2222a03d73 100644 --- a/homeassistant/components/digital_ocean/binary_sensor.py +++ b/homeassistant/components/digital_ocean/binary_sensor.py @@ -4,8 +4,8 @@ import logging import voluptuous as vol from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_MOVING, PLATFORM_SCHEMA, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.const import ATTR_ATTRIBUTION @@ -74,7 +74,7 @@ class DigitalOceanBinarySensor(BinarySensorEntity): @property def device_class(self): """Return the class of this sensor.""" - return DEVICE_CLASS_MOVING + return BinarySensorDeviceClass.MOVING @property def extra_state_attributes(self): From fa38a2d0bfa68735db0aa35782c0787f625afdf5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 9 Dec 2021 13:23:01 +0100 Subject: [PATCH 0250/2644] Use new enums in deconz (#61343) Co-authored-by: epenet --- .../components/deconz/binary_sensor.py | 27 +++++------- homeassistant/components/deconz/cover.py | 9 ++-- homeassistant/components/deconz/number.py | 4 +- homeassistant/components/deconz/sensor.py | 41 ++++++++----------- 4 files changed, 34 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 475c631bdf8..32b444b115d 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -17,21 +17,16 @@ from pydeconz.sensor import ( ) from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_GAS, - DEVICE_CLASS_MOISTURE, - DEVICE_CLASS_MOTION, - DEVICE_CLASS_OPENING, - DEVICE_CLASS_SMOKE, - DEVICE_CLASS_TAMPER, - DEVICE_CLASS_VIBRATION, DOMAIN, + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_TEMPERATURE, ENTITY_CATEGORY_DIAGNOSTIC +from homeassistant.const import ATTR_TEMPERATURE 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 .const import ATTR_DARK, ATTR_ON @@ -56,27 +51,27 @@ ATTR_VIBRATIONSTRENGTH = "vibrationstrength" ENTITY_DESCRIPTIONS = { CarbonMonoxide: BinarySensorEntityDescription( key="carbonmonoxide", - device_class=DEVICE_CLASS_GAS, + device_class=BinarySensorDeviceClass.GAS, ), Fire: BinarySensorEntityDescription( key="fire", - device_class=DEVICE_CLASS_SMOKE, + device_class=BinarySensorDeviceClass.SMOKE, ), OpenClose: BinarySensorEntityDescription( key="openclose", - device_class=DEVICE_CLASS_OPENING, + device_class=BinarySensorDeviceClass.OPENING, ), Presence: BinarySensorEntityDescription( key="presence", - device_class=DEVICE_CLASS_MOTION, + device_class=BinarySensorDeviceClass.MOTION, ), Vibration: BinarySensorEntityDescription( key="vibration", - device_class=DEVICE_CLASS_VIBRATION, + device_class=BinarySensorDeviceClass.VIBRATION, ), Water: BinarySensorEntityDescription( key="water", - device_class=DEVICE_CLASS_MOISTURE, + device_class=BinarySensorDeviceClass.MOISTURE, ), } @@ -186,8 +181,8 @@ class DeconzTampering(DeconzDevice, BinarySensorEntity): TYPE = DOMAIN _device: PydeconzSensor - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC - _attr_device_class = DEVICE_CLASS_TAMPER + _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_device_class = BinarySensorDeviceClass.TAMPER def __init__(self, device: PydeconzSensor, gateway: DeconzGateway) -> None: """Initialize deCONZ binary sensor.""" diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index 324452c4aa6..d3696783933 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -10,8 +10,6 @@ from pydeconz.light import Cover from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, - DEVICE_CLASS_DAMPER, - DEVICE_CLASS_SHADE, DOMAIN, SUPPORT_CLOSE, SUPPORT_CLOSE_TILT, @@ -21,6 +19,7 @@ from homeassistant.components.cover import ( SUPPORT_SET_TILT_POSITION, SUPPORT_STOP, SUPPORT_STOP_TILT, + CoverDeviceClass, CoverEntity, ) from homeassistant.config_entries import ConfigEntry @@ -32,9 +31,9 @@ from .deconz_device import DeconzDevice from .gateway import DeconzGateway, get_gateway_from_config_entry DEVICE_CLASS = { - "Level controllable output": DEVICE_CLASS_DAMPER, - "Window covering controller": DEVICE_CLASS_SHADE, - "Window covering device": DEVICE_CLASS_SHADE, + "Level controllable output": CoverDeviceClass.DAMPER, + "Window covering controller": CoverDeviceClass.SHADE, + "Window covering device": CoverDeviceClass.SHADE, } diff --git a/homeassistant/components/deconz/number.py b/homeassistant/components/deconz/number.py index 300a10f9a27..fff70b9f7b5 100644 --- a/homeassistant/components/deconz/number.py +++ b/homeassistant/components/deconz/number.py @@ -13,9 +13,9 @@ from homeassistant.components.number import ( NumberEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_CONFIG 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 .deconz_device import DeconzDevice @@ -37,7 +37,7 @@ class DeconzNumberEntityDescription( ): """Class describing deCONZ number entities.""" - entity_category = ENTITY_CATEGORY_CONFIG + entity_category = EntityCategory.CONFIG ENTITY_DESCRIPTIONS = { diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index e530e33e654..c74867df5bb 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -22,24 +22,16 @@ from pydeconz.sensor import ( from homeassistant.components.sensor import ( DOMAIN, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_TEMPERATURE, ATTR_VOLTAGE, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_POWER, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, ENERGY_KILO_WATT_HOUR, - ENTITY_CATEGORY_DIAGNOSTIC, LIGHT_LUX, PERCENTAGE, POWER_WATT, @@ -51,6 +43,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -79,15 +72,15 @@ ATTR_EVENT_ID = "event_id" ENTITY_DESCRIPTIONS = { Battery: SensorEntityDescription( key="battery", - device_class=DEVICE_CLASS_BATTERY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), Consumption: SensorEntityDescription( key="consumption", - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), Daylight: SensorEntityDescription( @@ -97,31 +90,31 @@ ENTITY_DESCRIPTIONS = { ), Humidity: SensorEntityDescription( key="humidity", - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, ), LightLevel: SensorEntityDescription( key="lightlevel", - device_class=DEVICE_CLASS_ILLUMINANCE, + device_class=SensorDeviceClass.ILLUMINANCE, native_unit_of_measurement=LIGHT_LUX, ), Power: SensorEntityDescription( key="power", - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_WATT, ), Pressure: SensorEntityDescription( key="pressure", - device_class=DEVICE_CLASS_PRESSURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PRESSURE_HPA, ), Temperature: SensorEntityDescription( key="temperature", - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, ), } From 2db1013620c0ef756e66652b738cacc7149ce0e1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 9 Dec 2021 15:38:32 +0100 Subject: [PATCH 0251/2644] Use new DeviceClass enums in blebox (#61361) Co-authored-by: epenet --- homeassistant/components/blebox/const.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/blebox/const.py b/homeassistant/components/blebox/const.py index f5eba403c75..401aa0a8771 100644 --- a/homeassistant/components/blebox/const.py +++ b/homeassistant/components/blebox/const.py @@ -1,16 +1,15 @@ """Constants for the BleBox devices integration.""" from homeassistant.components.cover import ( - DEVICE_CLASS_DOOR, - DEVICE_CLASS_GATE, - DEVICE_CLASS_SHUTTER, STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING, + CoverDeviceClass, ) -from homeassistant.components.switch import DEVICE_CLASS_SWITCH -from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS +from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.components.switch import SwitchDeviceClass +from homeassistant.const import TEMP_CELSIUS DOMAIN = "blebox" PRODUCT = "product" @@ -24,11 +23,11 @@ UNSUPPORTED_VERSION = "unsupported_version" UNKNOWN = "unknown" BLEBOX_TO_HASS_DEVICE_CLASSES = { - "shutter": DEVICE_CLASS_SHUTTER, - "gatebox": DEVICE_CLASS_DOOR, - "gate": DEVICE_CLASS_GATE, - "relay": DEVICE_CLASS_SWITCH, - "temperature": DEVICE_CLASS_TEMPERATURE, + "shutter": CoverDeviceClass.SHUTTER, + "gatebox": CoverDeviceClass.DOOR, + "gate": CoverDeviceClass.GATE, + "relay": SwitchDeviceClass.SWITCH, + "temperature": SensorDeviceClass.TEMPERATURE, } BLEBOX_TO_HASS_COVER_STATES = { From d64ae20f94ca686d92e6af6d08267a7c830ce542 Mon Sep 17 00:00:00 2001 From: bsmappee <58250533+bsmappee@users.noreply.github.com> Date: Thu, 9 Dec 2021 20:00:23 +0100 Subject: [PATCH 0252/2644] add missing unit of measurement in Smappee (#61365) --- homeassistant/components/smappee/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/smappee/sensor.py b/homeassistant/components/smappee/sensor.py index 276248fd6ae..ccae097e53f 100644 --- a/homeassistant/components/smappee/sensor.py +++ b/homeassistant/components/smappee/sensor.py @@ -250,6 +250,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): description=SmappeeSensorEntityDescription( key="load", name=measurement.name, + native_unit_of_measurement=POWER_WATT, sensor_id=measurement_id, device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, From 497f036af6c9f78f36a2aeca000439afd9aff312 Mon Sep 17 00:00:00 2001 From: bigbadblunt Date: Thu, 9 Dec 2021 21:12:40 +0000 Subject: [PATCH 0253/2644] Add default value for signal_repetitions in cover (#61393) --- homeassistant/components/rfxtrx/cover.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/rfxtrx/cover.py b/homeassistant/components/rfxtrx/cover.py index 0244e3aa8a0..4dc89577542 100644 --- a/homeassistant/components/rfxtrx/cover.py +++ b/homeassistant/components/rfxtrx/cover.py @@ -65,7 +65,7 @@ async def async_setup_entry( entity = RfxtrxCover( event.device, device_id, - signal_repetitions=entity_info[CONF_SIGNAL_REPETITIONS], + signal_repetitions=entity_info.get(CONF_SIGNAL_REPETITIONS, 1), venetian_blind_mode=entity_info.get(CONF_VENETIAN_BLIND_MODE), ) entities.append(entity) From f512bacfc7a6db5fae6b11f8c63132112d793c16 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 9 Dec 2021 22:20:06 +0100 Subject: [PATCH 0254/2644] Use new SensorDeviceClass enum in emonitor (#61385) Co-authored-by: epenet --- homeassistant/components/emonitor/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/emonitor/sensor.py b/homeassistant/components/emonitor/sensor.py index 5d4f1983ac1..a0b0e3c1fd9 100644 --- a/homeassistant/components/emonitor/sensor.py +++ b/homeassistant/components/emonitor/sensor.py @@ -2,7 +2,7 @@ from aioemonitor.monitor import EmonitorChannel -from homeassistant.components.sensor import DEVICE_CLASS_POWER, SensorEntity +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.const import POWER_WATT from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity import DeviceInfo @@ -37,7 +37,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class EmonitorPowerSensor(CoordinatorEntity, SensorEntity): """Representation of an Emonitor power sensor entity.""" - _attr_device_class = DEVICE_CLASS_POWER + _attr_device_class = SensorDeviceClass.POWER _attr_native_unit_of_measurement = POWER_WATT def __init__(self, coordinator: DataUpdateCoordinator, channel_number: int) -> None: From ea3e08c04181c75a6b46f4b23de3f14ff7174e56 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 9 Dec 2021 22:35:53 +0100 Subject: [PATCH 0255/2644] Improve type checking for rfxtrx (#58837) --- homeassistant/components/rfxtrx/__init__.py | 72 ++++++++++++++----- .../components/rfxtrx/binary_sensor.py | 65 +++++++++-------- .../components/rfxtrx/config_flow.py | 43 +++++++---- homeassistant/components/rfxtrx/cover.py | 37 ++++++---- homeassistant/components/rfxtrx/light.py | 13 ++-- homeassistant/components/rfxtrx/sensor.py | 2 +- homeassistant/components/rfxtrx/switch.py | 22 ++++-- 7 files changed, 166 insertions(+), 88 deletions(-) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 7fc743d021f..e380e1734e6 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -1,9 +1,12 @@ """Support for RFXtrx devices.""" +from __future__ import annotations + import asyncio import binascii import copy import functools import logging +from typing import NamedTuple import RFXtrx as rfxtrxmod import async_timeout @@ -49,6 +52,14 @@ SIGNAL_EVENT = f"{DOMAIN}_event" _LOGGER = logging.getLogger(__name__) +class DeviceTuple(NamedTuple): + """Representation of a device in rfxtrx.""" + + packettype: str + subtype: str + id_string: str + + def _bytearray_string(data): val = cv.string(data) try: @@ -225,7 +236,7 @@ async def async_setup_internal(hass, entry: config_entries.ConfigEntry): hass.services.async_register(DOMAIN, SERVICE_SEND, send, schema=SERVICE_SEND_SCHEMA) -def get_rfx_object(packetid): +def get_rfx_object(packetid: str) -> rfxtrxmod.RFXtrxEvent | None: """Return the RFXObject with the packetid.""" try: binarypacket = bytearray.fromhex(packetid) @@ -246,10 +257,10 @@ def get_rfx_object(packetid): return obj -def get_pt2262_deviceid(device_id, nb_data_bits): +def get_pt2262_deviceid(device_id: str, nb_data_bits: int | None) -> bytes | None: """Extract and return the address bits from a Lighting4/PT2262 packet.""" if nb_data_bits is None: - return + return None try: data = bytearray.fromhex(device_id) @@ -262,7 +273,7 @@ def get_pt2262_deviceid(device_id, nb_data_bits): return binascii.hexlify(data) -def get_pt2262_cmd(device_id, data_bits): +def get_pt2262_cmd(device_id: str, data_bits: int) -> str | None: """Extract and return the data bits from a Lighting4/PT2262 packet.""" try: data = bytearray.fromhex(device_id) @@ -274,7 +285,9 @@ def get_pt2262_cmd(device_id, data_bits): return hex(data[-1] & mask) -def get_device_data_bits(device, devices): +def get_device_data_bits( + device: rfxtrxmod.RFXtrxDevice, devices: dict[DeviceTuple, dict] +) -> int | None: """Deduce data bits for device based on a cache of device bits.""" data_bits = None if device.packettype == DEVICE_PACKET_TYPE_LIGHTING4: @@ -286,7 +299,7 @@ def get_device_data_bits(device, devices): return data_bits -def find_possible_pt2262_device(device_ids, device_id): +def find_possible_pt2262_device(device_ids: list[str], device_id: str) -> str | None: """Look for the device which id matches the given device_id parameter.""" for dev_id in device_ids: if len(dev_id) == len(device_id): @@ -313,9 +326,11 @@ def find_possible_pt2262_device(device_ids, device_id): return None -def get_device_id(device, data_bits=None): +def get_device_id( + device: rfxtrxmod.RFXtrxDevice, data_bits: int | None = None +) -> DeviceTuple: """Calculate a device id for device.""" - id_string = device.id_string + id_string: str = device.id_string if ( data_bits and device.packettype == DEVICE_PACKET_TYPE_LIGHTING4 @@ -323,7 +338,7 @@ def get_device_id(device, data_bits=None): ): id_string = masked_id.decode("ASCII") - return (f"{device.packettype:x}", f"{device.subtype:x}", id_string) + return DeviceTuple(f"{device.packettype:x}", f"{device.subtype:x}", id_string) def connect_auto_add(hass, entry_data, callback_fun): @@ -340,7 +355,15 @@ class RfxtrxEntity(RestoreEntity): Contains the common logic for Rfxtrx lights and switches. """ - def __init__(self, device, device_id, event=None): + _device: rfxtrxmod.RFXtrxDevice + _event: rfxtrxmod.RFXtrxEvent | None + + def __init__( + self, + device: rfxtrxmod.RFXtrxDevice, + device_id: DeviceTuple, + event: rfxtrxmod.RFXtrxEvent | None = None, + ) -> None: """Initialize the device.""" self._name = f"{device.type_string} {device.id_string}" self._device = device @@ -405,21 +428,28 @@ class RfxtrxEntity(RestoreEntity): name=f"{self._device.type_string} {self._device.id_string}", ) - def _event_applies(self, event, device_id): + def _event_applies(self, event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple): """Check if event applies to me.""" - if "Command" in event.values and event.values["Command"] in COMMAND_GROUP_LIST: - (group_id, _, _) = event.device.id_string.partition(":") - return group_id == self._group_id + if isinstance(event, rfxtrxmod.ControlEvent): + if ( + "Command" in event.values + and event.values["Command"] in COMMAND_GROUP_LIST + ): + device: rfxtrxmod.RFXtrxDevice = event.device + (group_id, _, _) = device.id_string.partition(":") + return group_id == self._group_id # Otherwise, the event only applies to the matching device. return device_id == self._device_id - def _apply_event(self, event): + def _apply_event(self, event: rfxtrxmod.RFXtrxEvent) -> None: """Apply a received event.""" self._event = event @callback - def _handle_event(self, event, device_id): + def _handle_event( + self, event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple + ) -> None: """Handle a reception of data, overridden by other classes.""" @@ -429,11 +459,17 @@ class RfxtrxCommandEntity(RfxtrxEntity): Contains the common logic for Rfxtrx lights and switches. """ - def __init__(self, device, device_id, signal_repetitions=1, event=None): + def __init__( + self, + device: rfxtrxmod.RFXtrxDevice, + device_id: DeviceTuple, + signal_repetitions: int = 1, + event: rfxtrxmod.RFXtrxEvent | None = None, + ) -> None: """Initialzie a switch or light device.""" super().__init__(device, device_id, event=event) self.signal_repetitions = signal_repetitions - self._state = None + self._state: bool | None = None async def _async_send(self, fun, *args): rfx_object = self.hass.data[DOMAIN][DATA_RFXOBJECT] diff --git a/homeassistant/components/rfxtrx/binary_sensor.py b/homeassistant/components/rfxtrx/binary_sensor.py index cc11e94c526..d7c0ea306d8 100644 --- a/homeassistant/components/rfxtrx/binary_sensor.py +++ b/homeassistant/components/rfxtrx/binary_sensor.py @@ -17,10 +17,11 @@ from homeassistant.const import ( CONF_DEVICES, STATE_ON, ) -from homeassistant.core import callback +from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.helpers import event as evt from . import ( + DeviceTuple, RfxtrxEntity, connect_auto_add, find_possible_pt2262_device, @@ -83,7 +84,7 @@ SENSOR_TYPES = ( SENSOR_TYPES_DICT = {desc.key: desc for desc in SENSOR_TYPES} -def supported(event): +def supported(event: rfxtrxmod.RFXtrxEvent): """Return whether an event supports binary_sensor.""" if isinstance(event, rfxtrxmod.ControlEvent): return True @@ -103,8 +104,8 @@ async def async_setup_entry( """Set up platform.""" sensors = [] - device_ids = set() - pt2262_devices = [] + device_ids: set[DeviceTuple] = set() + pt2262_devices: list[str] = [] discovery_info = config_entry.data @@ -127,25 +128,29 @@ async def async_setup_entry( continue device_ids.add(device_id) - if event.device.packettype == DEVICE_PACKET_TYPE_LIGHTING4: - find_possible_pt2262_device(pt2262_devices, event.device.id_string) - pt2262_devices.append(event.device.id_string) + device: rfxtrxmod.RFXtrxDevice = event.device - device = RfxtrxBinarySensor( - event.device, + if device.packettype == DEVICE_PACKET_TYPE_LIGHTING4: + find_possible_pt2262_device(pt2262_devices, device.id_string) + pt2262_devices.append(device.id_string) + + entity = RfxtrxBinarySensor( + device, device_id, - get_sensor_description(event.device.type_string), + get_sensor_description(device.type_string), entity_info.get(CONF_OFF_DELAY), entity_info.get(CONF_DATA_BITS), entity_info.get(CONF_COMMAND_ON), entity_info.get(CONF_COMMAND_OFF), ) - sensors.append(device) + sensors.append(entity) async_add_entities(sensors) @callback - def binary_sensor_update(event, device_id): + def binary_sensor_update( + event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple + ) -> None: """Call for control updates from the RFXtrx gateway.""" if not supported(event): return @@ -179,22 +184,22 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): def __init__( self, - device, - device_id, - entity_description, - off_delay=None, - data_bits=None, - cmd_on=None, - cmd_off=None, - event=None, - ): + device: rfxtrxmod.RFXtrxDevice, + device_id: DeviceTuple, + entity_description: BinarySensorEntityDescription, + off_delay: float | None = None, + data_bits: int | None = None, + cmd_on: int | None = None, + cmd_off: int | None = None, + event: rfxtrxmod.RFXtrxEvent | None = None, + ) -> None: """Initialize the RFXtrx sensor.""" super().__init__(device, device_id, event=event) self.entity_description = entity_description self._data_bits = data_bits self._off_delay = off_delay - self._state = None - self._delay_listener = None + self._state: bool | None = None + self._delay_listener: CALLBACK_TYPE | None = None self._cmd_on = cmd_on self._cmd_off = cmd_off @@ -220,11 +225,12 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): """Return true if the sensor state is True.""" return self._state - def _apply_event_lighting4(self, event): + def _apply_event_lighting4(self, event: rfxtrxmod.RFXtrxEvent): """Apply event for a lighting 4 device.""" if self._data_bits is not None: - cmd = get_pt2262_cmd(event.device.id_string, self._data_bits) - cmd = int(cmd, 16) + cmdstr = get_pt2262_cmd(event.device.id_string, self._data_bits) + assert cmdstr + cmd = int(cmdstr, 16) if cmd == self._cmd_on: self._state = True elif cmd == self._cmd_off: @@ -232,7 +238,8 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): else: self._state = True - def _apply_event_standard(self, event): + def _apply_event_standard(self, event: rfxtrxmod.RFXtrxEvent): + assert isinstance(event, (rfxtrxmod.SensorEvent, rfxtrxmod.ControlEvent)) if event.values.get("Command") in COMMAND_ON_LIST: self._state = True elif event.values.get("Command") in COMMAND_OFF_LIST: @@ -242,7 +249,7 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): elif event.values.get("Sensor Status") in SENSOR_STATUS_OFF: self._state = False - def _apply_event(self, event): + def _apply_event(self, event: rfxtrxmod.RFXtrxEvent): """Apply command from rfxtrx.""" super()._apply_event(event) if event.device.packettype == DEVICE_PACKET_TYPE_LIGHTING4: @@ -251,7 +258,7 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): self._apply_event_standard(event) @callback - def _handle_event(self, event, device_id): + def _handle_event(self, event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple): """Check if event applies to me and update.""" if not self._event_applies(event, device_id): return diff --git a/homeassistant/components/rfxtrx/config_flow.py b/homeassistant/components/rfxtrx/config_flow.py index cf1069a7907..1ec74c2415a 100644 --- a/homeassistant/components/rfxtrx/config_flow.py +++ b/homeassistant/components/rfxtrx/config_flow.py @@ -1,6 +1,9 @@ """Config flow for RFXCOM RFXtrx integration.""" +from __future__ import annotations + import copy import os +from typing import TypedDict, cast import RFXtrx as rfxtrxmod import serial @@ -22,6 +25,8 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.device_registry import ( + DeviceEntry, + DeviceRegistry, async_entries_for_config_entry, async_get_registry as async_get_device_registry, ) @@ -30,7 +35,7 @@ from homeassistant.helpers.entity_registry import ( async_get_registry as async_get_entity_registry, ) -from . import DOMAIN, get_device_id, get_rfx_object +from . import DOMAIN, DeviceTuple, get_device_id, get_rfx_object from .binary_sensor import supported as binary_supported from .const import ( CONF_AUTOMATIC_ADD, @@ -53,6 +58,13 @@ CONF_EVENT_CODE = "event_code" CONF_MANUAL_PATH = "Enter Manually" +class DeviceData(TypedDict): + """Dict data representing a device entry.""" + + event_code: str + device_id: DeviceTuple + + def none_or_int(value, base): """Check if strin is one otherwise convert to int.""" if value is None: @@ -63,16 +75,17 @@ def none_or_int(value, base): class OptionsFlow(config_entries.OptionsFlow): """Handle Rfxtrx options.""" + _device_registry: DeviceRegistry + _device_entries: list[DeviceEntry] + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize rfxtrx options flow.""" self._config_entry = config_entry self._global_options = None self._selected_device = None - self._selected_device_entry_id = None - self._selected_device_event_code = None - self._selected_device_object = None - self._device_entries = None - self._device_registry = None + self._selected_device_entry_id: str | None = None + self._selected_device_event_code: str | None = None + self._selected_device_object: rfxtrxmod.RFXtrxEvent | None = None async def async_step_init(self, user_input=None): """Manage the options.""" @@ -173,6 +186,8 @@ class OptionsFlow(config_entries.OptionsFlow): errors = {} if user_input is not None: + assert self._selected_device_object + assert self._selected_device_event_code device_id = get_device_id( self._selected_device_object.device, data_bits=user_input.get(CONF_DATA_BITS), @@ -399,20 +414,18 @@ class OptionsFlow(config_entries.OptionsFlow): return data[CONF_EVENT_CODE] - def _get_device_data(self, entry_id): + def _get_device_data(self, entry_id) -> DeviceData: """Get event code based on device identifier.""" - event_code = None - device_id = None + event_code: str entry = self._device_registry.async_get(entry_id) - device_id = next(iter(entry.identifiers))[1:] + assert entry + device_id = cast(DeviceTuple, next(iter(entry.identifiers))[1:]) for packet_id, entity_info in self._config_entry.data[CONF_DEVICES].items(): if tuple(entity_info.get(CONF_DEVICE_ID)) == device_id: - event_code = packet_id + event_code = cast(str, packet_id) break - - data = {CONF_EVENT_CODE: event_code, CONF_DEVICE_ID: device_id} - - return data + assert event_code + return DeviceData(event_code=event_code, device_id=device_id) @callback def update_config_data(self, global_options=None, devices=None): diff --git a/homeassistant/components/rfxtrx/cover.py b/homeassistant/components/rfxtrx/cover.py index 4dc89577542..c1c009c930c 100644 --- a/homeassistant/components/rfxtrx/cover.py +++ b/homeassistant/components/rfxtrx/cover.py @@ -1,6 +1,10 @@ """Support for RFXtrx covers.""" +from __future__ import annotations + import logging +import RFXtrx as rfxtrxmod + from homeassistant.components.cover import ( SUPPORT_CLOSE, SUPPORT_CLOSE_TILT, @@ -15,6 +19,7 @@ from homeassistant.core import callback from . import ( DEFAULT_SIGNAL_REPETITIONS, + DeviceTuple, RfxtrxCommandEntity, connect_auto_add, get_device_id, @@ -33,7 +38,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -def supported(event): +def supported(event: rfxtrxmod.RFXtrxEvent): """Return whether an event supports cover.""" return event.device.known_to_be_rollershutter @@ -45,7 +50,7 @@ async def async_setup_entry( ): """Set up config entry.""" discovery_info = config_entry.data - device_ids = set() + device_ids: set[DeviceTuple] = set() entities = [] for packet_id, entity_info in discovery_info[CONF_DEVICES].items(): @@ -73,7 +78,7 @@ async def async_setup_entry( async_add_entities(entities) @callback - def cover_update(event, device_id): + def cover_update(event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple) -> None: """Handle cover updates from the RFXtrx gateway.""" if not supported(event): return @@ -81,12 +86,13 @@ async def async_setup_entry( if device_id in device_ids: return device_ids.add(device_id) + device: rfxtrxmod.RFXtrxDevice = event.device _LOGGER.info( "Added cover (Device ID: %s Class: %s Sub: %s, Event: %s)", - event.device.id_string.lower(), - event.device.__class__.__name__, - event.device.subtype, + device.id_string.lower(), + device.__class__.__name__, + device.subtype, "".join(f"{x:02x}" for x in event.data), ) @@ -102,14 +108,16 @@ async def async_setup_entry( class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): """Representation of a RFXtrx cover.""" + _device: rfxtrxmod.RollerTrolDevice | rfxtrxmod.RfyDevice | rfxtrxmod.LightingDevice + def __init__( self, - device, - device_id, - signal_repetitions, - event=None, - venetian_blind_mode=None, - ): + device: rfxtrxmod.RFXtrxDevice, + device_id: DeviceTuple, + signal_repetitions: int, + event: rfxtrxmod.RFXtrxEvent = None, + venetian_blind_mode: bool | None = None, + ) -> None: """Initialize the RFXtrx cover device.""" super().__init__(device, device_id, signal_repetitions, event) self._venetian_blind_mode = venetian_blind_mode @@ -191,8 +199,9 @@ class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): self._state = True self.async_write_ha_state() - def _apply_event(self, event): + def _apply_event(self, event: rfxtrxmod.RFXtrxEvent): """Apply command from rfxtrx.""" + assert isinstance(event, rfxtrxmod.ControlEvent) super()._apply_event(event) if event.values["Command"] in COMMAND_ON_LIST: self._state = True @@ -200,7 +209,7 @@ class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): self._state = False @callback - def _handle_event(self, event, device_id): + def _handle_event(self, event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple): """Check if event applies to me and update.""" if device_id != self._device_id: return diff --git a/homeassistant/components/rfxtrx/light.py b/homeassistant/components/rfxtrx/light.py index c67213ed6f8..bf88ff86368 100644 --- a/homeassistant/components/rfxtrx/light.py +++ b/homeassistant/components/rfxtrx/light.py @@ -1,4 +1,6 @@ """Support for RFXtrx lights.""" +from __future__ import annotations + import logging import RFXtrx as rfxtrxmod @@ -13,6 +15,7 @@ from homeassistant.core import callback from . import ( DEFAULT_SIGNAL_REPETITIONS, + DeviceTuple, RfxtrxCommandEntity, connect_auto_add, get_device_id, @@ -30,7 +33,7 @@ _LOGGER = logging.getLogger(__name__) SUPPORT_RFXTRX = SUPPORT_BRIGHTNESS -def supported(event): +def supported(event: rfxtrxmod.RFXtrxEvent): """Return whether an event supports light.""" return ( isinstance(event.device, rfxtrxmod.LightingDevice) @@ -45,7 +48,7 @@ async def async_setup_entry( ): """Set up config entry.""" discovery_info = config_entry.data - device_ids = set() + device_ids: set[DeviceTuple] = set() # Add switch from config file entities = [] @@ -72,7 +75,7 @@ async def async_setup_entry( async_add_entities(entities) @callback - def light_update(event, device_id): + def light_update(event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple): """Handle light updates from the RFXtrx gateway.""" if not supported(event): return @@ -103,6 +106,7 @@ class RfxtrxLight(RfxtrxCommandEntity, LightEntity): """Representation of a RFXtrx light.""" _brightness = 0 + _device: rfxtrxmod.LightingDevice async def async_added_to_hass(self): """Restore RFXtrx device state (ON/OFF).""" @@ -149,8 +153,9 @@ class RfxtrxLight(RfxtrxCommandEntity, LightEntity): self._brightness = 0 self.async_write_ha_state() - def _apply_event(self, event): + def _apply_event(self, event: rfxtrxmod.RFXtrxEvent): """Apply command from rfxtrx.""" + assert isinstance(event, rfxtrxmod.ControlEvent) super()._apply_event(event) if event.values["Command"] in COMMAND_ON_LIST: self._state = True diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index c13e499bbf0..bf292adeee8 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -238,7 +238,7 @@ async def async_setup_entry( event.device, data_bits=entity_info.get(CONF_DATA_BITS) ) for data_type in set(event.values) & set(SENSOR_TYPES_DICT): - data_id = (*device_id, data_type) + data_id = (*device_id, str(data_type)) if data_id in data_ids: continue data_ids.add(data_id) diff --git a/homeassistant/components/rfxtrx/switch.py b/homeassistant/components/rfxtrx/switch.py index 21e9e06b802..d0ec83f3c66 100644 --- a/homeassistant/components/rfxtrx/switch.py +++ b/homeassistant/components/rfxtrx/switch.py @@ -1,4 +1,6 @@ """Support for RFXtrx switches.""" +from __future__ import annotations + import logging import RFXtrx as rfxtrxmod @@ -10,6 +12,7 @@ from homeassistant.core import callback from . import ( DEFAULT_SIGNAL_REPETITIONS, DOMAIN, + DeviceTuple, RfxtrxCommandEntity, connect_auto_add, get_device_id, @@ -44,7 +47,7 @@ async def async_setup_entry( ): """Set up config entry.""" discovery_info = config_entry.data - device_ids = set() + device_ids: set[DeviceTuple] = set() # Add switch from config file entities = [] @@ -79,16 +82,18 @@ async def async_setup_entry( return device_ids.add(device_id) + device: rfxtrxmod.RFXtrxDevice = event.device + _LOGGER.info( "Added switch (Device ID: %s Class: %s Sub: %s, Event: %s)", - event.device.id_string.lower(), - event.device.__class__.__name__, - event.device.subtype, + device.id_string.lower(), + device.__class__.__name__, + device.subtype, "".join(f"{x:02x}" for x in event.data), ) entity = RfxtrxSwitch( - event.device, device_id, DEFAULT_SIGNAL_REPETITIONS, event=event + device, device_id, DEFAULT_SIGNAL_REPETITIONS, event=event ) async_add_entities([entity]) @@ -108,8 +113,9 @@ class RfxtrxSwitch(RfxtrxCommandEntity, SwitchEntity): if old_state is not None: self._state = old_state.state == STATE_ON - def _apply_event(self, event): + def _apply_event(self, event: rfxtrxmod.RFXtrxEvent) -> None: """Apply command from rfxtrx.""" + assert isinstance(event, rfxtrxmod.ControlEvent) super()._apply_event(event) if event.values["Command"] in COMMAND_ON_LIST: self._state = True @@ -117,7 +123,9 @@ class RfxtrxSwitch(RfxtrxCommandEntity, SwitchEntity): self._state = False @callback - def _handle_event(self, event, device_id): + def _handle_event( + self, event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple + ) -> None: """Check if event applies to me and update.""" if self._event_applies(event, device_id): self._apply_event(event) From 2b7c2d902e7c76d709497dcbcb4ab6e674bb9f8b Mon Sep 17 00:00:00 2001 From: einarhauks Date: Thu, 9 Dec 2021 22:08:29 +0000 Subject: [PATCH 0256/2644] Update tesla-wall-connector to v1.0.1 (#61392) --- homeassistant/components/tesla_wall_connector/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tesla_wall_connector/manifest.json b/homeassistant/components/tesla_wall_connector/manifest.json index 08d52b3016b..8e86fa3d2f8 100644 --- a/homeassistant/components/tesla_wall_connector/manifest.json +++ b/homeassistant/components/tesla_wall_connector/manifest.json @@ -3,7 +3,7 @@ "name": "Tesla Wall Connector", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tesla_wall_connector", - "requirements": ["tesla-wall-connector==1.0.0"], + "requirements": ["tesla-wall-connector==1.0.1"], "dhcp": [ { "hostname": "teslawallconnector_*", diff --git a/requirements_all.txt b/requirements_all.txt index 6144e14bf6e..048388e9898 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2311,7 +2311,7 @@ temperusb==1.5.3 tesla-powerwall==0.3.12 # homeassistant.components.tesla_wall_connector -tesla-wall-connector==1.0.0 +tesla-wall-connector==1.0.1 # homeassistant.components.tensorflow # tf-models-official==2.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 35aae4e6b15..b6f8a1b9b43 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1373,7 +1373,7 @@ tellduslive==0.10.11 tesla-powerwall==0.3.12 # homeassistant.components.tesla_wall_connector -tesla-wall-connector==1.0.0 +tesla-wall-connector==1.0.1 # homeassistant.components.tolo tololib==0.1.0b3 From b556bd1d5802e5fdce36bee426c0e7c020e66df8 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 9 Dec 2021 15:11:41 -0700 Subject: [PATCH 0257/2644] Consolidate SimpliSafe config flow forms into one (#61402) --- .../components/simplisafe/config_flow.py | 59 +++++++++---------- .../components/simplisafe/strings.json | 8 +-- .../simplisafe/translations/en.json | 25 +------- .../components/simplisafe/test_config_flow.py | 21 ------- 4 files changed, 31 insertions(+), 82 deletions(-) diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index 8f8ec6cdc16..53fcb92f71a 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -24,7 +24,7 @@ from .const import CONF_USER_ID, DOMAIN, LOGGER CONF_AUTH_CODE = "auth_code" -STEP_INPUT_AUTH_CODE_SCHEMA = vol.Schema( +STEP_USER_SCHEMA = vol.Schema( { vol.Required(CONF_AUTH_CODE): cv.string, } @@ -54,8 +54,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self._errors: dict[str, Any] = {} - self._oauth_values: SimpliSafeOAuthValues = async_get_simplisafe_oauth_values() + self._oauth_values: SimpliSafeOAuthValues | None = None self._reauth: bool = False self._username: str | None = None @@ -67,19 +66,34 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Define the config flow to handle options.""" return SimpliSafeOptionsFlowHandler(config_entry) - async def async_step_input_auth_code( + def _async_show_form(self, *, errors: dict[str, Any] | None = None) -> FlowResult: + """Show the form.""" + self._oauth_values = async_get_simplisafe_oauth_values() + + return self.async_show_form( + step_id="user", + data_schema=STEP_USER_SCHEMA, + errors=errors or {}, + description_placeholders={CONF_URL: self._oauth_values.auth_url}, + ) + + async def async_step_reauth(self, config: ConfigType) -> FlowResult: + """Handle configuration by re-auth.""" + self._username = config.get(CONF_USERNAME) + self._reauth = True + return await self.async_step_user() + + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: - """Handle the input of a SimpliSafe OAuth authorization code.""" + """Handle the start of the config flow.""" if user_input is None: - return self.async_show_form( - step_id="input_auth_code", data_schema=STEP_INPUT_AUTH_CODE_SCHEMA - ) + return self._async_show_form() if TYPE_CHECKING: assert self._oauth_values - self._errors = {} + errors = {} session = aiohttp_client.async_get_clientsession(self.hass) try: @@ -89,13 +103,13 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): session=session, ) except InvalidCredentialsError: - self._errors = {"base": "invalid_auth"} + errors = {"base": "invalid_auth"} except SimplipyError as err: LOGGER.error("Unknown error while logging into SimpliSafe: %s", err) - self._errors = {"base": "unknown"} + errors = {"base": "unknown"} - if self._errors: - return await self.async_step_user() + if errors: + return self._async_show_form(errors=errors) data = {CONF_USER_ID: simplisafe.user_id, CONF_TOKEN: simplisafe.refresh_token} unique_id = str(simplisafe.user_id) @@ -122,25 +136,6 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() return self.async_create_entry(title=unique_id, data=data) - async def async_step_reauth(self, config: ConfigType) -> FlowResult: - """Handle configuration by re-auth.""" - self._username = config.get(CONF_USERNAME) - self._reauth = True - return await self.async_step_user() - - async def async_step_user( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle the start of the config flow.""" - if user_input is None: - return self.async_show_form( - step_id="user", - errors=self._errors, - description_placeholders={CONF_URL: self._oauth_values.auth_url}, - ) - - return await self.async_step_input_auth_code() - class SimpliSafeOptionsFlowHandler(config_entries.OptionsFlow): """Handle a SimpliSafe options flow.""" diff --git a/homeassistant/components/simplisafe/strings.json b/homeassistant/components/simplisafe/strings.json index 55a916bfe6b..08632cd754a 100644 --- a/homeassistant/components/simplisafe/strings.json +++ b/homeassistant/components/simplisafe/strings.json @@ -1,15 +1,11 @@ { "config": { "step": { - "input_auth_code": { - "title": "Finish Authorization", - "description": "Input the authorization code from the SimpliSafe web app URL:", + "user": { + "description": "Starting in 2021, SimpliSafe has moved to a new authentication mechanism via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\n1. Click [here]({url}) to open the SimpliSafe web app and input your credentials.\n\n2. When the login process is complete, return here and input the authorization code below.", "data": { "auth_code": "Authorization Code" } - }, - "user": { - "description": "Starting in 2021, SimpliSafe has moved to a new authentication mechanism via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. When the process is complete, return here and click Submit." } }, "error": { diff --git a/homeassistant/components/simplisafe/translations/en.json b/homeassistant/components/simplisafe/translations/en.json index 66843f86d27..3c5dd9261bc 100644 --- a/homeassistant/components/simplisafe/translations/en.json +++ b/homeassistant/components/simplisafe/translations/en.json @@ -12,32 +12,11 @@ "unknown": "Unexpected error" }, "step": { - "input_auth_code": { + "user": { "data": { "auth_code": "Authorization Code" }, - "description": "Input the authorization code from the SimpliSafe web app URL:", - "title": "Finish Authorization" - }, - "mfa": { - "description": "Check your email for a link from SimpliSafe. After verifying the link, return here to complete the installation of the integration.", - "title": "SimpliSafe Multi-Factor Authentication" - }, - "reauth_confirm": { - "data": { - "password": "Password" - }, - "description": "Your access has expired or been revoked. Enter your password to re-link your account.", - "title": "Reauthenticate Integration" - }, - "user": { - "data": { - "code": "Code (used in Home Assistant UI)", - "password": "Password", - "username": "Email" - }, - "description": "Starting in 2021, SimpliSafe has moved to a new authentication mechanism via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. When the process is complete, return here and click Submit.", - "title": "Fill in your information." + "description": "Starting in 2021, SimpliSafe has moved to a new authentication mechanism via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\n1. Click [here]({url}) to open the SimpliSafe web app and input your credentials.\n\n2. When the login process is complete, return here and input the authorization code below." } } }, diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index 99943497556..0597ad377cf 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -53,9 +53,6 @@ async def test_duplicate_error(hass, mock_async_from_auth): assert result["step_id"] == "user" assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} ) @@ -73,9 +70,6 @@ async def test_invalid_credentials(hass, mock_async_from_auth): assert result["step_id"] == "user" assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} ) @@ -131,9 +125,6 @@ async def test_step_reauth_old_format(hass, mock_async_from_auth): with patch( "homeassistant.components.simplisafe.async_setup_entry", return_value=True ), patch("homeassistant.config_entries.ConfigEntries.async_reload"): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} ) @@ -166,9 +157,6 @@ async def test_step_reauth_new_format(hass, mock_async_from_auth): with patch( "homeassistant.components.simplisafe.async_setup_entry", return_value=True ), patch("homeassistant.config_entries.ConfigEntries.async_reload"): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} ) @@ -205,9 +193,6 @@ async def test_step_reauth_wrong_account(hass, api, mock_async_from_auth): with patch( "homeassistant.components.simplisafe.async_setup_entry", return_value=True ), patch("homeassistant.config_entries.ConfigEntries.async_reload"): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} ) @@ -229,9 +214,6 @@ async def test_step_user(hass, mock_async_from_auth): with patch( "homeassistant.components.simplisafe.async_setup_entry", return_value=True ), patch("homeassistant.config_entries.ConfigEntries.async_reload"): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} ) @@ -252,9 +234,6 @@ async def test_unknown_error(hass, mock_async_from_auth): assert result["step_id"] == "user" assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} ) From 3ae16caf6a9a5bd329171a707e59e15a0bd1fea2 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 9 Dec 2021 16:12:19 -0600 Subject: [PATCH 0258/2644] Fix Sonos radio handling during polling (#61401) --- homeassistant/components/sonos/speaker.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 05787a354f0..e12166d7795 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -994,9 +994,9 @@ class SonosSpeaker: @soco_error() def update_media(self, event: SonosEvent | None = None) -> None: """Update information about currently playing media.""" - variables = event and event.variables + variables = event.variables if event else {} - if variables and "transport_state" in variables: + if "transport_state" in variables: # If the transport has an error then transport_state will # not be set new_status = variables["transport_state"] @@ -1012,7 +1012,7 @@ class SonosSpeaker: update_position = new_status != self.media.playback_status self.media.playback_status = new_status - if variables and "transport_state" in variables: + if "transport_state" in variables: self.media.play_mode = variables["current_play_mode"] track_uri = ( variables["enqueued_transport_uri"] or variables["current_track_uri"] @@ -1060,7 +1060,7 @@ class SonosSpeaker: self.media.title = source self.media.source_name = source - def update_media_radio(self, variables: dict | None) -> None: + def update_media_radio(self, variables: dict) -> None: """Update state when streaming radio.""" self.media.clear_position() radio_title = None From 1f97603f7162d0ca98cfa1afeae83710c65777fa Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 9 Dec 2021 14:40:05 -0800 Subject: [PATCH 0259/2644] Bump frontend to 20211209.0 (#61406) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 6d419276029..c9739cd0302 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20211206.0" + "home-assistant-frontend==20211209.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index cf1a20e1925..b203f005e64 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ ciso8601==2.2.0 cryptography==35.0.0 emoji==1.5.0 hass-nabucasa==0.50.0 -home-assistant-frontend==20211206.0 +home-assistant-frontend==20211209.0 httpx==0.21.0 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index 048388e9898..c0c6048a684 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -828,7 +828,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211206.0 +home-assistant-frontend==20211209.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b6f8a1b9b43..9e9bc7dc4c2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -524,7 +524,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211206.0 +home-assistant-frontend==20211209.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 58174eaa4e77b8f0fded3b2e7e479e58d8a4d1eb Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 9 Dec 2021 15:41:13 -0700 Subject: [PATCH 0260/2644] Assign docs URL to a placeholder in SimpliSafe config flow (#61410) --- homeassistant/components/simplisafe/config_flow.py | 10 +++++++++- homeassistant/components/simplisafe/strings.json | 2 +- .../components/simplisafe/translations/en.json | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index 53fcb92f71a..3a3d1963e0e 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -23,6 +23,11 @@ from homeassistant.helpers.typing import ConfigType from .const import CONF_USER_ID, DOMAIN, LOGGER CONF_AUTH_CODE = "auth_code" +CONF_DOCS_URL = "docs_url" + +AUTH_DOCS_URL = ( + "http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code" +) STEP_USER_SCHEMA = vol.Schema( { @@ -74,7 +79,10 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=STEP_USER_SCHEMA, errors=errors or {}, - description_placeholders={CONF_URL: self._oauth_values.auth_url}, + description_placeholders={ + CONF_URL: self._oauth_values.auth_url, + CONF_DOCS_URL: AUTH_DOCS_URL, + }, ) async def async_step_reauth(self, config: ConfigType) -> FlowResult: diff --git a/homeassistant/components/simplisafe/strings.json b/homeassistant/components/simplisafe/strings.json index 08632cd754a..a0ff28fd689 100644 --- a/homeassistant/components/simplisafe/strings.json +++ b/homeassistant/components/simplisafe/strings.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "description": "Starting in 2021, SimpliSafe has moved to a new authentication mechanism via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\n1. Click [here]({url}) to open the SimpliSafe web app and input your credentials.\n\n2. When the login process is complete, return here and input the authorization code below.", + "description": "SimpliSafe authenticates with Home Assistant via the SimpliSafe web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation]({docs_url}) before starting.\n\n1. Click [here]({url}) to open the SimpliSafe web app and input your credentials.\n\n2. When the login process is complete, return here and input the authorization code below.", "data": { "auth_code": "Authorization Code" } diff --git a/homeassistant/components/simplisafe/translations/en.json b/homeassistant/components/simplisafe/translations/en.json index 3c5dd9261bc..5829c68301f 100644 --- a/homeassistant/components/simplisafe/translations/en.json +++ b/homeassistant/components/simplisafe/translations/en.json @@ -16,7 +16,7 @@ "data": { "auth_code": "Authorization Code" }, - "description": "Starting in 2021, SimpliSafe has moved to a new authentication mechanism via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\n1. Click [here]({url}) to open the SimpliSafe web app and input your credentials.\n\n2. When the login process is complete, return here and input the authorization code below." + "description": "SimpliSafe authenticates with Home Assistant via the SimpliSafe web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation]({docs_url}) before starting.\n\n1. Click [here]({url}) to open the SimpliSafe web app and input your credentials.\n\n2. When the login process is complete, return here and input the authorization code below." } } }, From 4f9d4872a75d3d9b87b0c16ea9b09db49bb83aa8 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Fri, 10 Dec 2021 00:35:20 +0100 Subject: [PATCH 0261/2644] Fix unique_id of S0 meters connected to Fronius inverters (#61408) --- homeassistant/components/fronius/sensor.py | 11 +- tests/components/fronius/__init__.py | 28 ++-- .../fixtures/primo_s0/GetAPIVersion.json | 5 + .../fixtures/primo_s0/GetInverterInfo.json | 33 +++++ .../GetInverterRealtimeData_Device_1.json | 64 +++++++++ .../GetInverterRealtimeData_Device_2.json | 64 +++++++++ .../fixtures/primo_s0/GetLoggerInfo.json | 29 ++++ .../primo_s0/GetMeterRealtimeData.json | 31 ++++ .../primo_s0/GetOhmPilotRealtimeData.json | 17 +++ .../primo_s0/GetPowerFlowRealtimeData.json | 45 ++++++ .../primo_s0/GetStorageRealtimeData.json | 14 ++ tests/components/fronius/test_sensor.py | 136 +++++++++++++++++- 12 files changed, 462 insertions(+), 15 deletions(-) create mode 100644 tests/components/fronius/fixtures/primo_s0/GetAPIVersion.json create mode 100644 tests/components/fronius/fixtures/primo_s0/GetInverterInfo.json create mode 100644 tests/components/fronius/fixtures/primo_s0/GetInverterRealtimeData_Device_1.json create mode 100644 tests/components/fronius/fixtures/primo_s0/GetInverterRealtimeData_Device_2.json create mode 100644 tests/components/fronius/fixtures/primo_s0/GetLoggerInfo.json create mode 100644 tests/components/fronius/fixtures/primo_s0/GetMeterRealtimeData.json create mode 100644 tests/components/fronius/fixtures/primo_s0/GetOhmPilotRealtimeData.json create mode 100644 tests/components/fronius/fixtures/primo_s0/GetPowerFlowRealtimeData.json create mode 100644 tests/components/fronius/fixtures/primo_s0/GetStorageRealtimeData.json diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index 348ef91cd7d..31d3d77fd17 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -784,15 +784,22 @@ class MeterSensor(_FroniusSensorEntity): self._entity_id_prefix = f"meter_{solar_net_id}" super().__init__(coordinator, key, solar_net_id) meter_data = self._device_data() + # S0 meters connected directly to inverters respond "n.a." as serial number + # `model` contains the inverter id: "S0 Meter at inverter 1" + if (meter_uid := meter_data["serial"]["value"]) == "n.a.": + meter_uid = ( + f"{coordinator.solar_net.solar_net_device_id}:" + f'{meter_data["model"]["value"]}' + ) self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, meter_data["serial"]["value"])}, + identifiers={(DOMAIN, meter_uid)}, manufacturer=meter_data["manufacturer"]["value"], model=meter_data["model"]["value"], name=meter_data["model"]["value"], via_device=(DOMAIN, coordinator.solar_net.solar_net_device_id), ) - self._attr_unique_id = f'{meter_data["serial"]["value"]}-{key}' + self._attr_unique_id = f"{meter_uid}-{key}" class OhmpilotSensor(_FroniusSensorEntity): diff --git a/tests/components/fronius/__init__.py b/tests/components/fronius/__init__.py index 683575a11c8..7930f6c01f4 100644 --- a/tests/components/fronius/__init__.py +++ b/tests/components/fronius/__init__.py @@ -1,4 +1,6 @@ """Tests for the Fronius integration.""" +from __future__ import annotations + from homeassistant.components.fronius.const import DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST @@ -10,16 +12,16 @@ from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture from tests.test_util.aiohttp import AiohttpClientMocker MOCK_HOST = "http://fronius" -MOCK_UID = "123.4567890" # has to match mocked logger unique_id +MOCK_UID = "123.4567890" async def setup_fronius_integration( - hass: HomeAssistant, is_logger: bool = True + hass: HomeAssistant, is_logger: bool = True, unique_id: str = MOCK_UID ) -> ConfigEntry: """Create the Fronius integration.""" entry = MockConfigEntry( domain=DOMAIN, - unique_id=MOCK_UID, + unique_id=unique_id, # has to match mocked logger unique_id data={ CONF_HOST: MOCK_HOST, "is_logger": is_logger, @@ -35,9 +37,10 @@ def mock_responses( aioclient_mock: AiohttpClientMocker, host: str = MOCK_HOST, fixture_set: str = "symo", + inverter_ids: list[str | int] = [1], night: bool = False, ) -> None: - """Mock responses for Fronius Symo inverter with meter.""" + """Mock responses for Fronius devices.""" aioclient_mock.clear_requests() _night = "_night" if night else "" @@ -45,14 +48,15 @@ def mock_responses( f"{host}/solar_api/GetAPIVersion.cgi", text=load_fixture(f"{fixture_set}/GetAPIVersion.json", "fronius"), ) - aioclient_mock.get( - f"{host}/solar_api/v1/GetInverterRealtimeData.cgi?Scope=Device&" - "DeviceId=1&DataCollection=CommonInverterData", - text=load_fixture( - f"{fixture_set}/GetInverterRealtimeData_Device_1{_night}.json", - "fronius", - ), - ) + for inverter_id in inverter_ids: + aioclient_mock.get( + f"{host}/solar_api/v1/GetInverterRealtimeData.cgi?Scope=Device&" + f"DeviceId={inverter_id}&DataCollection=CommonInverterData", + text=load_fixture( + f"{fixture_set}/GetInverterRealtimeData_Device_{inverter_id}{_night}.json", + "fronius", + ), + ) aioclient_mock.get( f"{host}/solar_api/v1/GetInverterInfo.cgi", text=load_fixture(f"{fixture_set}/GetInverterInfo.json", "fronius"), diff --git a/tests/components/fronius/fixtures/primo_s0/GetAPIVersion.json b/tests/components/fronius/fixtures/primo_s0/GetAPIVersion.json new file mode 100644 index 00000000000..2051b4d58e3 --- /dev/null +++ b/tests/components/fronius/fixtures/primo_s0/GetAPIVersion.json @@ -0,0 +1,5 @@ +{ + "APIVersion" : 1, + "BaseURL" : "/solar_api/v1/", + "CompatibilityRange" : "1.6-3" +} diff --git a/tests/components/fronius/fixtures/primo_s0/GetInverterInfo.json b/tests/components/fronius/fixtures/primo_s0/GetInverterInfo.json new file mode 100644 index 00000000000..5ac293653c0 --- /dev/null +++ b/tests/components/fronius/fixtures/primo_s0/GetInverterInfo.json @@ -0,0 +1,33 @@ +{ + "Body" : { + "Data" : { + "1" : { + "CustomName" : "Primo 5.0-1", + "DT" : 76, + "ErrorCode" : 0, + "PVPower" : 5160, + "Show" : 1, + "StatusCode" : 7, + "UniqueID" : "123456" + }, + "2" : { + "CustomName" : "Primo 3.0-1", + "DT" : 81, + "ErrorCode" : 0, + "PVPower" : 3240, + "Show" : 1, + "StatusCode" : 7, + "UniqueID" : "234567" + } + } + }, + "Head" : { + "RequestArguments" : {}, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2021-12-09T15:34:06-03:00" + } +} diff --git a/tests/components/fronius/fixtures/primo_s0/GetInverterRealtimeData_Device_1.json b/tests/components/fronius/fixtures/primo_s0/GetInverterRealtimeData_Device_1.json new file mode 100644 index 00000000000..e54366a5008 --- /dev/null +++ b/tests/components/fronius/fixtures/primo_s0/GetInverterRealtimeData_Device_1.json @@ -0,0 +1,64 @@ +{ + "Body" : { + "Data" : { + "DAY_ENERGY" : { + "Unit" : "Wh", + "Value" : 22504 + }, + "DeviceStatus" : { + "ErrorCode" : 0, + "LEDColor" : 2, + "LEDState" : 0, + "MgmtTimerRemainingTime" : -1, + "StateToReset" : false, + "StatusCode" : 7 + }, + "FAC" : { + "Unit" : "Hz", + "Value" : 60 + }, + "IAC" : { + "Unit" : "A", + "Value" : 3.8500000000000001 + }, + "IDC" : { + "Unit" : "A", + "Value" : 4.2300000000000004 + }, + "PAC" : { + "Unit" : "W", + "Value" : 862 + }, + "TOTAL_ENERGY" : { + "Unit" : "Wh", + "Value" : 17114940 + }, + "UAC" : { + "Unit" : "V", + "Value" : 223.90000000000001 + }, + "UDC" : { + "Unit" : "V", + "Value" : 452.30000000000001 + }, + "YEAR_ENERGY" : { + "Unit" : "Wh", + "Value" : 7532755.5 + } + } + }, + "Head" : { + "RequestArguments" : { + "DataCollection" : "CommonInverterData", + "DeviceClass" : "Inverter", + "DeviceId" : "1", + "Scope" : "Device" + }, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2021-12-09T15:34:08-03:00" + } +} diff --git a/tests/components/fronius/fixtures/primo_s0/GetInverterRealtimeData_Device_2.json b/tests/components/fronius/fixtures/primo_s0/GetInverterRealtimeData_Device_2.json new file mode 100644 index 00000000000..dd1e22c0a7a --- /dev/null +++ b/tests/components/fronius/fixtures/primo_s0/GetInverterRealtimeData_Device_2.json @@ -0,0 +1,64 @@ +{ + "Body" : { + "Data" : { + "DAY_ENERGY" : { + "Unit" : "Wh", + "Value" : 14237 + }, + "DeviceStatus" : { + "ErrorCode" : 0, + "LEDColor" : 2, + "LEDState" : 0, + "MgmtTimerRemainingTime" : -1, + "StateToReset" : false, + "StatusCode" : 7 + }, + "FAC" : { + "Unit" : "Hz", + "Value" : 60.009999999999998 + }, + "IAC" : { + "Unit" : "A", + "Value" : 1.3200000000000001 + }, + "IDC" : { + "Unit" : "A", + "Value" : 0.96999999999999997 + }, + "PAC" : { + "Unit" : "W", + "Value" : 296 + }, + "TOTAL_ENERGY" : { + "Unit" : "Wh", + "Value" : 5796010 + }, + "UAC" : { + "Unit" : "V", + "Value" : 223.59999999999999 + }, + "UDC" : { + "Unit" : "V", + "Value" : 329.5 + }, + "YEAR_ENERGY" : { + "Unit" : "Wh", + "Value" : 3596193.25 + } + } + }, + "Head" : { + "RequestArguments" : { + "DataCollection" : "CommonInverterData", + "DeviceClass" : "Inverter", + "DeviceId" : "2", + "Scope" : "Device" + }, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2021-12-09T15:36:15-03:00" + } +} diff --git a/tests/components/fronius/fixtures/primo_s0/GetLoggerInfo.json b/tests/components/fronius/fixtures/primo_s0/GetLoggerInfo.json new file mode 100644 index 00000000000..1fb0bbc8577 --- /dev/null +++ b/tests/components/fronius/fixtures/primo_s0/GetLoggerInfo.json @@ -0,0 +1,29 @@ +{ + "Body" : { + "LoggerInfo" : { + "CO2Factor" : 0.52999997138977051, + "CO2Unit" : "kg", + "CashCurrency" : "BRL", + "CashFactor" : 1, + "DefaultLanguage" : "en", + "DeliveryFactor" : 1, + "HWVersion" : "2.4E", + "PlatformID" : "wilma", + "ProductID" : "fronius-datamanager-card", + "SWVersion" : "3.18.7-1", + "TimezoneLocation" : "Sao_Paulo", + "TimezoneName" : "-03", + "UTCOffset" : 4294956496, + "UniqueID" : "123.4567890" + } + }, + "Head" : { + "RequestArguments" : {}, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2021-12-09T15:34:09-03:00" + } +} diff --git a/tests/components/fronius/fixtures/primo_s0/GetMeterRealtimeData.json b/tests/components/fronius/fixtures/primo_s0/GetMeterRealtimeData.json new file mode 100644 index 00000000000..aa308bb3b69 --- /dev/null +++ b/tests/components/fronius/fixtures/primo_s0/GetMeterRealtimeData.json @@ -0,0 +1,31 @@ +{ + "Body" : { + "Data" : { + "0" : { + "Details" : { + "Manufacturer" : "Fronius", + "Model" : "S0 Meter at inverter 1", + "Serial" : "n.a." + }, + "Enable" : 1, + "EnergyReal_WAC_Minus_Relative" : 191.25, + "Meter_Location_Current" : 1, + "PowerReal_P_Sum" : -2216.7486858112229, + "TimeStamp" : 1639074843, + "Visible" : 1 + } + } + }, + "Head" : { + "RequestArguments" : { + "DeviceClass" : "Meter", + "Scope" : "System" + }, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2021-12-09T15:34:04-03:00" + } +} diff --git a/tests/components/fronius/fixtures/primo_s0/GetOhmPilotRealtimeData.json b/tests/components/fronius/fixtures/primo_s0/GetOhmPilotRealtimeData.json new file mode 100644 index 00000000000..4562b45efb0 --- /dev/null +++ b/tests/components/fronius/fixtures/primo_s0/GetOhmPilotRealtimeData.json @@ -0,0 +1,17 @@ +{ + "Body" : { + "Data" : {} + }, + "Head" : { + "RequestArguments" : { + "DeviceClass" : "OhmPilot", + "Scope" : "System" + }, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2021-12-09T15:34:05-03:00" + } +} diff --git a/tests/components/fronius/fixtures/primo_s0/GetPowerFlowRealtimeData.json b/tests/components/fronius/fixtures/primo_s0/GetPowerFlowRealtimeData.json new file mode 100644 index 00000000000..4bbee2aec28 --- /dev/null +++ b/tests/components/fronius/fixtures/primo_s0/GetPowerFlowRealtimeData.json @@ -0,0 +1,45 @@ +{ + "Body" : { + "Data" : { + "Inverters" : { + "1" : { + "DT" : 76, + "E_Day" : 22502, + "E_Total" : 17114930, + "E_Year" : 7532753.5, + "P" : 886 + }, + "2" : { + "DT" : 81, + "E_Day" : 14222, + "E_Total" : 5795989.5, + "E_Year" : 3596179.75, + "P" : 948 + } + }, + "Site" : { + "E_Day" : 36724, + "E_Total" : 22910919.5, + "E_Year" : 11128933.25, + "Meter_Location" : "load", + "Mode" : "vague-meter", + "P_Akku" : null, + "P_Grid" : 384.93491437299008, + "P_Load" : -2218.9349143729901, + "P_PV" : 1834, + "rel_Autonomy" : 82.652266550064084, + "rel_SelfConsumption" : 100 + }, + "Version" : "12" + } + }, + "Head" : { + "RequestArguments" : {}, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2021-12-09T15:34:06-03:00" + } +} diff --git a/tests/components/fronius/fixtures/primo_s0/GetStorageRealtimeData.json b/tests/components/fronius/fixtures/primo_s0/GetStorageRealtimeData.json new file mode 100644 index 00000000000..8743a2c6d68 --- /dev/null +++ b/tests/components/fronius/fixtures/primo_s0/GetStorageRealtimeData.json @@ -0,0 +1,14 @@ +{ + "Body" : { + "Data" : {} + }, + "Head" : { + "RequestArguments" : {}, + "Status" : { + "Code" : 255, + "Reason" : "GetStorageRealtimeData request is not supported by this device.", + "UserMessage" : "" + }, + "Timestamp" : "2021-12-09T15:34:05-03:00" + } +} diff --git a/tests/components/fronius/test_sensor.py b/tests/components/fronius/test_sensor.py index cf371a47471..2e48faf606a 100644 --- a/tests/components/fronius/test_sensor.py +++ b/tests/components/fronius/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the Fronius sensor platform.""" +from homeassistant.components.fronius.const import DOMAIN from homeassistant.components.fronius.coordinator import ( FroniusInverterUpdateCoordinator, FroniusMeterUpdateCoordinator, @@ -6,6 +7,7 @@ from homeassistant.components.fronius.coordinator import ( ) from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import STATE_UNKNOWN +from homeassistant.helpers import device_registry as dr from homeassistant.util import dt from . import enable_all_entities, mock_responses, setup_fronius_integration @@ -371,7 +373,9 @@ async def test_gen24_storage(hass, aioclient_mock): assert state.state == str(expected_state) mock_responses(aioclient_mock, fixture_set="gen24_storage") - config_entry = await setup_fronius_integration(hass, is_logger=False) + config_entry = await setup_fronius_integration( + hass, is_logger=False, unique_id="12345678" + ) assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 36 await enable_all_entities( @@ -469,3 +473,133 @@ async def test_gen24_storage(hass, aioclient_mock): assert_state("sensor.temperature_cell_fronius_storage_0_http_fronius", 21.5) assert_state("sensor.capacity_designed_fronius_storage_0_http_fronius", 16588) assert_state("sensor.voltage_dc_fronius_storage_0_http_fronius", 0.0) + + # Devices + device_registry = dr.async_get(hass) + + solar_net = device_registry.async_get_device( + identifiers={(DOMAIN, "solar_net_12345678")} + ) + assert solar_net.configuration_url == "http://fronius" + assert solar_net.manufacturer == "Fronius" + assert solar_net.name == "SolarNet" + + inverter_1 = device_registry.async_get_device(identifiers={(DOMAIN, "12345678")}) + assert inverter_1.manufacturer == "Fronius" + assert inverter_1.model == "Gen24" + assert inverter_1.name == "Gen24 Storage" + + meter = device_registry.async_get_device(identifiers={(DOMAIN, "1234567890")}) + assert meter.manufacturer == "Fronius" + assert meter.model == "Smart Meter TS 65A-3" + assert meter.name == "Smart Meter TS 65A-3" + + ohmpilot = device_registry.async_get_device(identifiers={(DOMAIN, "23456789")}) + assert ohmpilot.manufacturer == "Fronius" + assert ohmpilot.model == "Ohmpilot 6" + assert ohmpilot.name == "Ohmpilot" + assert ohmpilot.sw_version == "1.0.25-3" + + storage = device_registry.async_get_device( + identifiers={(DOMAIN, "P030T020Z2001234567 ")} + ) + assert storage.manufacturer == "BYD" + assert storage.model == "BYD Battery-Box Premium HV" + assert storage.name == "BYD Battery-Box Premium HV" + + +async def test_primo_s0(hass, aioclient_mock): + """Test Fronius Primo dual inverter with S0 meter entities.""" + + def assert_state(entity_id, expected_state): + state = hass.states.get(entity_id) + assert state + assert state.state == str(expected_state) + + mock_responses(aioclient_mock, fixture_set="primo_s0", inverter_ids=[1, 2]) + config_entry = await setup_fronius_integration(hass, is_logger=True) + + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 30 + await enable_all_entities( + hass, config_entry.entry_id, FroniusMeterUpdateCoordinator.default_interval + ) + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 41 + # logger + assert_state("sensor.cash_factor_fronius_logger_info_0_http_fronius", 1) + assert_state("sensor.co2_factor_fronius_logger_info_0_http_fronius", 0.53) + assert_state("sensor.delivery_factor_fronius_logger_info_0_http_fronius", 1) + # inverter 1 + assert_state("sensor.energy_total_fronius_inverter_1_http_fronius", 17114940) + assert_state("sensor.energy_day_fronius_inverter_1_http_fronius", 22504) + assert_state("sensor.voltage_dc_fronius_inverter_1_http_fronius", 452.3) + assert_state("sensor.power_ac_fronius_inverter_1_http_fronius", 862) + assert_state("sensor.error_code_fronius_inverter_1_http_fronius", 0) + assert_state("sensor.current_dc_fronius_inverter_1_http_fronius", 4.23) + assert_state("sensor.status_code_fronius_inverter_1_http_fronius", 7) + assert_state("sensor.energy_year_fronius_inverter_1_http_fronius", 7532755.5) + assert_state("sensor.current_ac_fronius_inverter_1_http_fronius", 3.85) + assert_state("sensor.voltage_ac_fronius_inverter_1_http_fronius", 223.9) + assert_state("sensor.frequency_ac_fronius_inverter_1_http_fronius", 60) + assert_state("sensor.led_color_fronius_inverter_1_http_fronius", 2) + assert_state("sensor.led_state_fronius_inverter_1_http_fronius", 0) + # inverter 2 + assert_state("sensor.energy_total_fronius_inverter_2_http_fronius", 5796010) + assert_state("sensor.energy_day_fronius_inverter_2_http_fronius", 14237) + assert_state("sensor.voltage_dc_fronius_inverter_2_http_fronius", 329.5) + assert_state("sensor.power_ac_fronius_inverter_2_http_fronius", 296) + assert_state("sensor.error_code_fronius_inverter_2_http_fronius", 0) + assert_state("sensor.current_dc_fronius_inverter_2_http_fronius", 0.97) + assert_state("sensor.status_code_fronius_inverter_2_http_fronius", 7) + assert_state("sensor.energy_year_fronius_inverter_2_http_fronius", 3596193.25) + assert_state("sensor.current_ac_fronius_inverter_2_http_fronius", 1.32) + assert_state("sensor.voltage_ac_fronius_inverter_2_http_fronius", 223.6) + assert_state("sensor.frequency_ac_fronius_inverter_2_http_fronius", 60.01) + assert_state("sensor.led_color_fronius_inverter_2_http_fronius", 2) + assert_state("sensor.led_state_fronius_inverter_2_http_fronius", 0) + # meter + assert_state("sensor.meter_location_fronius_meter_0_http_fronius", 1) + assert_state("sensor.power_real_fronius_meter_0_http_fronius", -2216.7487) + # power_flow + assert_state("sensor.power_load_fronius_power_flow_0_http_fronius", -2218.9349) + assert_state( + "sensor.power_battery_fronius_power_flow_0_http_fronius", STATE_UNKNOWN + ) + assert_state("sensor.meter_mode_fronius_power_flow_0_http_fronius", "vague-meter") + assert_state("sensor.power_photovoltaics_fronius_power_flow_0_http_fronius", 1834) + assert_state("sensor.power_grid_fronius_power_flow_0_http_fronius", 384.9349) + assert_state( + "sensor.relative_self_consumption_fronius_power_flow_0_http_fronius", 100 + ) + assert_state("sensor.relative_autonomy_fronius_power_flow_0_http_fronius", 82.6523) + assert_state("sensor.energy_total_fronius_power_flow_0_http_fronius", 22910919.5) + assert_state("sensor.energy_day_fronius_power_flow_0_http_fronius", 36724) + assert_state("sensor.energy_year_fronius_power_flow_0_http_fronius", 11128933.25) + + # Devices + device_registry = dr.async_get(hass) + + solar_net = device_registry.async_get_device( + identifiers={(DOMAIN, "solar_net_123.4567890")} + ) + assert solar_net.configuration_url == "http://fronius" + assert solar_net.manufacturer == "Fronius" + assert solar_net.model == "fronius-datamanager-card" + assert solar_net.name == "SolarNet" + assert solar_net.sw_version == "3.18.7-1" + + inverter_1 = device_registry.async_get_device(identifiers={(DOMAIN, "123456")}) + assert inverter_1.manufacturer == "Fronius" + assert inverter_1.model == "Primo 5.0-1" + assert inverter_1.name == "Primo 5.0-1" + + inverter_2 = device_registry.async_get_device(identifiers={(DOMAIN, "234567")}) + assert inverter_2.manufacturer == "Fronius" + assert inverter_2.model == "Primo 3.0-1" + assert inverter_2.name == "Primo 3.0-1" + + meter = device_registry.async_get_device( + identifiers={(DOMAIN, "solar_net_123.4567890:S0 Meter at inverter 1")} + ) + assert meter.manufacturer == "Fronius" + assert meter.model == "S0 Meter at inverter 1" + assert meter.name == "S0 Meter at inverter 1" From 0aaf9459ab96023cddfdb55776beb4b1f9b3d901 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 10 Dec 2021 00:13:29 +0000 Subject: [PATCH 0262/2644] [ci skip] Translation update --- .../components/adax/translations/cs.json | 8 ++- .../components/adax/translations/id.json | 22 +++++++- .../components/adax/translations/it.json | 22 +++++++- .../components/adax/translations/lt.json | 29 +++++++++++ .../components/adax/translations/tr.json | 22 +++++++- .../components/apple_tv/translations/it.json | 23 ++++++-- .../components/apple_tv/translations/lt.json | 9 ++++ .../components/apple_tv/translations/tr.json | 23 ++++++-- .../aseko_pool_live/translations/it.json | 20 +++++++ .../aseko_pool_live/translations/lt.json | 15 ++++++ .../components/balboa/translations/lt.json | 10 ++++ .../components/elmax/translations/cs.json | 12 +++++ .../components/elmax/translations/id.json | 34 ++++++++++++ .../components/elmax/translations/it.json | 34 ++++++++++++ .../components/elmax/translations/lt.json | 23 ++++++++ .../components/elmax/translations/tr.json | 34 ++++++++++++ .../enphase_envoy/translations/it.json | 3 +- .../enphase_envoy/translations/tr.json | 3 +- .../components/fronius/translations/lt.json | 7 +++ .../components/hue/translations/lt.json | 8 +++ .../components/jellyfin/translations/lt.json | 12 +++++ .../components/konnected/translations/lt.json | 7 +++ .../components/nina/translations/it.json | 27 ++++++++++ .../components/nina/translations/lt.json | 9 ++++ .../components/ridwell/translations/lt.json | 11 ++++ .../simplisafe/translations/en.json | 26 +++++++++- .../tesla_wall_connector/translations/lt.json | 7 +++ .../components/tolo/translations/lt.json | 10 ++++ .../tolo/translations/select.lt.json | 8 +++ .../translations/lt.json | 11 ++++ .../components/wallbox/translations/lt.json | 12 +++++ .../wled/translations/select.lt.json | 8 +++ .../translations/select.it.json | 52 +++++++++++++++++++ .../translations/select.lt.json | 29 +++++++++++ .../translations/select.tr.json | 52 +++++++++++++++++++ .../components/zwave_js/translations/it.json | 2 + .../components/zwave_js/translations/tr.json | 2 + 37 files changed, 625 insertions(+), 21 deletions(-) create mode 100644 homeassistant/components/adax/translations/lt.json create mode 100644 homeassistant/components/apple_tv/translations/lt.json create mode 100644 homeassistant/components/aseko_pool_live/translations/it.json create mode 100644 homeassistant/components/aseko_pool_live/translations/lt.json create mode 100644 homeassistant/components/balboa/translations/lt.json create mode 100644 homeassistant/components/elmax/translations/cs.json create mode 100644 homeassistant/components/elmax/translations/id.json create mode 100644 homeassistant/components/elmax/translations/it.json create mode 100644 homeassistant/components/elmax/translations/lt.json create mode 100644 homeassistant/components/elmax/translations/tr.json create mode 100644 homeassistant/components/fronius/translations/lt.json create mode 100644 homeassistant/components/jellyfin/translations/lt.json create mode 100644 homeassistant/components/konnected/translations/lt.json create mode 100644 homeassistant/components/nina/translations/it.json create mode 100644 homeassistant/components/nina/translations/lt.json create mode 100644 homeassistant/components/ridwell/translations/lt.json create mode 100644 homeassistant/components/tesla_wall_connector/translations/lt.json create mode 100644 homeassistant/components/tolo/translations/lt.json create mode 100644 homeassistant/components/tolo/translations/select.lt.json create mode 100644 homeassistant/components/trafikverket_weatherstation/translations/lt.json create mode 100644 homeassistant/components/wallbox/translations/lt.json create mode 100644 homeassistant/components/wled/translations/select.lt.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/select.it.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/select.lt.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/select.tr.json diff --git a/homeassistant/components/adax/translations/cs.json b/homeassistant/components/adax/translations/cs.json index 1d090f44de2..4dc53056df0 100644 --- a/homeassistant/components/adax/translations/cs.json +++ b/homeassistant/components/adax/translations/cs.json @@ -1,13 +1,19 @@ { "config": { "abort": { - "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" }, "step": { + "cloud": { + "data": { + "password": "Heslo" + } + }, "user": { "data": { "account_id": "ID \u00fa\u010dtu", diff --git a/homeassistant/components/adax/translations/id.json b/homeassistant/components/adax/translations/id.json index e554913bdc8..864a8ed623d 100644 --- a/homeassistant/components/adax/translations/id.json +++ b/homeassistant/components/adax/translations/id.json @@ -1,19 +1,37 @@ { "config": { "abort": { - "already_configured": "Perangkat sudah dikonfigurasi" + "already_configured": "Perangkat sudah dikonfigurasi", + "heater_not_available": "Pemanas tidak tersedia. Cobalah untuk mengatur ulang pemanas dengan menekan + dan OK selama beberapa detik.", + "heater_not_found": "Pemanas tidak ditemukan. Coba dekatkan pemanas ke komputer Home Assistant.", + "invalid_auth": "Autentikasi tidak valid" }, "error": { "cannot_connect": "Gagal terhubung", "invalid_auth": "Autentikasi tidak valid" }, "step": { + "cloud": { + "data": { + "account_id": "ID Akun", + "password": "Kata Sandi" + } + }, + "local": { + "data": { + "wifi_pswd": "Kata sandi Wi-Fi", + "wifi_ssid": "SSID Wi-Fi" + }, + "description": "Atur ulang pemanas dengan menekan + dan OK hingga muncul tampilan 'Reset'. Kemudian tekan dan tahan tombol OK pada pemanas sampai led biru mulai berkedip sebelum menekan Submit. Mengonfigurasi pemanas mungkin memerlukan waktu beberapa menit." + }, "user": { "data": { "account_id": "ID Akun", + "connection_type": "Pilih jenis koneksi", "host": "Host", "password": "Kata Sandi" - } + }, + "description": "Pilih jenis koneksi. Lokal membutuhkan pemanas dengan bluetooth" } } } diff --git a/homeassistant/components/adax/translations/it.json b/homeassistant/components/adax/translations/it.json index c0ccf6aff05..b331b8a5f65 100644 --- a/homeassistant/components/adax/translations/it.json +++ b/homeassistant/components/adax/translations/it.json @@ -1,19 +1,37 @@ { "config": { "abort": { - "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "heater_not_available": "Riscaldatore non disponibile. Prova a resettare il riscaldatore premendo + e OK per alcuni secondi.", + "heater_not_found": "Riscaldatore non trovato. Prova ad avvicinare il riscaldatore al computer Home Assistant.", + "invalid_auth": "Autenticazione non valida" }, "error": { "cannot_connect": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida" }, "step": { + "cloud": { + "data": { + "account_id": "ID account", + "password": "Password" + } + }, + "local": { + "data": { + "wifi_pswd": "Password WiFi", + "wifi_ssid": "SSID Wifi" + }, + "description": "Ripristinare il riscaldatore premendo + e OK finch\u00e9 il display non mostra 'Reset'. Quindi premere e tenere premuto il pulsante OK sul riscaldatore fino a quando il led blu inizia a lampeggiare prima di premere Invia. La configurazione del riscaldatore potrebbe richiedere alcuni minuti." + }, "user": { "data": { "account_id": "ID account", + "connection_type": "Seleziona il tipo di connessione", "host": "Host", "password": "Password" - } + }, + "description": "Seleziona il tipo di connessione. Locale richiede riscaldatori con bluetooth" } } } diff --git a/homeassistant/components/adax/translations/lt.json b/homeassistant/components/adax/translations/lt.json new file mode 100644 index 00000000000..5faf196f952 --- /dev/null +++ b/homeassistant/components/adax/translations/lt.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "heater_not_available": "\u0160ildytuvas nepasiekiamas. Pabandykite i\u0161 naujo nustatyti \u0161ildytuv\u0105 paspausdami + ir OK kelias sekundes.", + "heater_not_found": "\u0160ildytuvas nerastas. Pabandykite \u0161ildytuv\u0105 laikyti ar\u010diau Home Assistant serverio" + }, + "step": { + "cloud": { + "data": { + "account_id": "Paskyros ID", + "password": "Slapta\u017eodis" + } + }, + "local": { + "data": { + "wifi_pswd": "Wifi slapta\u017eodis", + "wifi_ssid": "Wifi ssid" + }, + "description": "I\u0161 naujo nustatykite \u0161ildytuv\u0105 spausdami + ir OK, kol ekrane pasirodys \u201eReset\u201c. Tada paspauskite ir palaikykite OK mygtuk\u0105 ant \u0161ildytuvo, kol prad\u0117s mirks\u0117ti m\u0117lyna lemput\u0117, prie\u0161 paspausdami Patvirtinti. \u0160ildytuvo konfig\u016bravimas gali u\u017etrukti kelet\u0105 minu\u010di\u0173." + }, + "user": { + "data": { + "connection_type": "Pasirinkite ry\u0161io tip\u0105" + }, + "description": "Pasirinkite ry\u0161io tip\u0105. Vietiniams reikalingi \u0161ildytuvai su \u201eBluetooth\u201c." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adax/translations/tr.json b/homeassistant/components/adax/translations/tr.json index 2407831bd5b..56406fed6ec 100644 --- a/homeassistant/components/adax/translations/tr.json +++ b/homeassistant/components/adax/translations/tr.json @@ -1,19 +1,37 @@ { "config": { "abort": { - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "heater_not_available": "Is\u0131t\u0131c\u0131 mevcut de\u011fil. + ve OK tu\u015flar\u0131na birka\u00e7 saniye basarak \u0131s\u0131t\u0131c\u0131y\u0131 s\u0131f\u0131rlamay\u0131 deneyin.", + "heater_not_found": "Is\u0131t\u0131c\u0131 bulunamad\u0131. Is\u0131t\u0131c\u0131y\u0131 Home Assistant bilgisayar\u0131na yakla\u015ft\u0131rmay\u0131 deneyin.", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" }, "step": { + "cloud": { + "data": { + "account_id": "Hesap Kimli\u011fi", + "password": "Parola" + } + }, + "local": { + "data": { + "wifi_pswd": "Kablosuz a\u011f parolas\u0131", + "wifi_ssid": "Wifi ssid" + }, + "description": "Ekranda 'S\u0131f\u0131rla' g\u00f6r\u00fcnene kadar + ve OK tu\u015flar\u0131na basarak \u0131s\u0131t\u0131c\u0131y\u0131 s\u0131f\u0131rlay\u0131n. Ard\u0131ndan G\u00f6nder'e basmadan \u00f6nce mavi led yan\u0131p s\u00f6nmeye ba\u015flayana kadar \u0131s\u0131t\u0131c\u0131daki OK d\u00fc\u011fmesini bas\u0131l\u0131 tutun. Is\u0131t\u0131c\u0131y\u0131 yap\u0131land\u0131rmak birka\u00e7 dakika s\u00fcrebilir." + }, "user": { "data": { "account_id": "Hesap Kimli\u011fi", + "connection_type": "Ba\u011flant\u0131 t\u00fcr\u00fcn\u00fc se\u00e7in", "host": "Sunucu", "password": "Parola" - } + }, + "description": "Ba\u011flant\u0131 t\u00fcr\u00fcn\u00fc se\u00e7in. Yerel Bluetooth'lu \u0131s\u0131t\u0131c\u0131lar gerektirir" } } } diff --git a/homeassistant/components/apple_tv/translations/it.json b/homeassistant/components/apple_tv/translations/it.json index 8faf1be4326..2162fad6765 100644 --- a/homeassistant/components/apple_tv/translations/it.json +++ b/homeassistant/components/apple_tv/translations/it.json @@ -1,12 +1,17 @@ { "config": { "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "already_configured_device": "Il dispositivo \u00e8 gi\u00e0 configurato", "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", "backoff": "Il dispositivo non accetta richieste di abbinamento in questo momento (potresti aver inserito un codice PIN non valido troppe volte), riprova pi\u00f9 tardi.", "device_did_not_pair": "Nessun tentativo di completare il processo di abbinamento \u00e8 stato effettuato dal dispositivo.", + "device_not_found": "Il dispositivo non \u00e8 stato trovato durante il rilevamento, prova ad aggiungerlo di nuovo.", + "inconsistent_device": "I protocolli previsti non sono stati trovati durante il rilevamento. Questo normalmente indica un problema con DNS multicast (Zeroconf). Prova ad aggiungere di nuovo il dispositivo.", "invalid_config": "La configurazione per questo dispositivo \u00e8 incompleta. Prova ad aggiungerlo di nuovo.", "no_devices_found": "Nessun dispositivo trovato sulla rete", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", + "setup_failed": "Impossibile configurare il dispositivo.", "unknown": "Errore imprevisto" }, "error": { @@ -16,14 +21,14 @@ "no_usable_service": "\u00c8 stato trovato un dispositivo ma non \u00e8 stato possibile identificare alcun modo per stabilire una connessione ad esso. Se continui a vedere questo messaggio, prova a specificarne l'indirizzo IP o a riavviare l'Apple TV.", "unknown": "Errore imprevisto" }, - "flow_title": "{name}", + "flow_title": "{name} ({type})", "step": { "confirm": { - "description": "Stai per aggiungere l'Apple TV denominata \"{name}\" a Home Assistant. \n\n **Per completare la procedura, potrebbe essere necessario inserire pi\u00f9 codici PIN.** \n\nTieni presente che *non* sarai in grado di spegnere la tua Apple TV con questa integrazione. Solo il lettore multimediale in Home Assistant si spegner\u00e0!", + "description": "Stai per aggiungere `{name}` di tipo `{type}` a Home Assistant. \n\n **Per completare il processo, potrebbe essere necessario inserire pi\u00f9 codici PIN.** \n\nTieni presente che *non* sarai in grado di spegnere la tua Apple TV con questa integrazione. Solo il lettore multimediale in Home Assistant si spegner\u00e0!", "title": "Conferma l'aggiunta di Apple TV" }, "pair_no_pin": { - "description": "L'abbinamento \u00e8 richiesto per il servizio \"{protocol}\". Inserisci il PIN {pin} sulla tua Apple TV per continuare.", + "description": "L'associazione \u00e8 necessaria per il servizio `{protocol}`. Inserisci il PIN {pin} sul tuo dispositivo per continuare.", "title": "Abbinamento" }, "pair_with_pin": { @@ -33,8 +38,16 @@ "description": "L'abbinamento \u00e8 richiesto per il protocollo \"{protocol}\". Immettere il codice PIN visualizzato sullo schermo. Gli zeri iniziali devono essere omessi, ovvero immettere 123 se il codice visualizzato \u00e8 0123.", "title": "Abbinamento" }, + "password": { + "description": "\u00c8 richiesta una password da `{protocol}`. Questo non \u00e8 ancora supportato, disabilita la password per continuare.", + "title": "Password richiesta" + }, + "protocol_disabled": { + "description": "L'associazione \u00e8 necessaria per `{protocol}` ma \u00e8 disabilitata sul dispositivo. Si prega di rivedere le potenziali restrizioni di accesso (ad esempio consentire a tutti i dispositivi sulla rete locale di connettersi) sul dispositivo. \n\n Puoi continuare senza associare questo protocollo, ma alcune funzionalit\u00e0 saranno limitate.", + "title": "Associazione non possibile" + }, "reconfigure": { - "description": "Questa Apple TV sta riscontrando alcune difficolt\u00e0 di connessione e deve essere riconfigurata.", + "description": "Riconfigurare questo dispositivo per ripristinarne la funzionalit\u00e0.", "title": "Riconfigurazione del dispositivo" }, "service_problem": { @@ -45,7 +58,7 @@ "data": { "device_input": "Dispositivo" }, - "description": "Inizia inserendo il nome del dispositivo (es. Cucina o Camera da letto) o l'indirizzo IP dell'Apple TV che desideri aggiungere. Se sono stati rilevati automaticamente dei dispositivi sulla rete, verranno visualizzati di seguito. \n\n Se non riesci a vedere il tuo dispositivo o riscontri problemi, prova a specificare l'indirizzo IP del dispositivo. \n\n {devices}", + "description": "Inizia inserendo il nome del dispositivo (es. Cucina o Camera da letto) o l'indirizzo IP dell'Apple TV che desideri aggiungere. \n\n Se non riesci a vedere il tuo dispositivo o riscontri problemi, prova a specificare l'indirizzo IP del dispositivo.", "title": "Configura una nuova Apple TV" } } diff --git a/homeassistant/components/apple_tv/translations/lt.json b/homeassistant/components/apple_tv/translations/lt.json new file mode 100644 index 00000000000..133261e6fa4 --- /dev/null +++ b/homeassistant/components/apple_tv/translations/lt.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "password": { + "title": "Reikalingas slapta\u017eodis" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/tr.json b/homeassistant/components/apple_tv/translations/tr.json index 007397de8d5..4b9c2a4ca07 100644 --- a/homeassistant/components/apple_tv/translations/tr.json +++ b/homeassistant/components/apple_tv/translations/tr.json @@ -1,12 +1,17 @@ { "config": { "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "already_configured_device": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", "backoff": "Cihaz \u015fu anda e\u015fle\u015ftirme isteklerini kabul etmiyor (\u00e7ok say\u0131da ge\u00e7ersiz PIN kodu girmi\u015f olabilirsiniz), daha sonra tekrar deneyin.", "device_did_not_pair": "Cihazdan e\u015fle\u015ftirme i\u015flemini bitirmek i\u00e7in herhangi bir giri\u015fimde bulunulmad\u0131.", + "device_not_found": "Cihaz ke\u015fif s\u0131ras\u0131nda bulunamad\u0131, l\u00fctfen tekrar eklemeyi deneyin.", + "inconsistent_device": "Ke\u015fif s\u0131ras\u0131nda beklenen protokoller bulunamad\u0131. Bu normalde \u00e7ok noktaya yay\u0131n DNS (Zeroconf) ile ilgili bir sorunu g\u00f6sterir. L\u00fctfen cihaz\u0131 tekrar eklemeyi deneyin.", "invalid_config": "Bu ayg\u0131t\u0131n yap\u0131land\u0131rmas\u0131 tamamlanmad\u0131. L\u00fctfen tekrar eklemeyi deneyin.", "no_devices_found": "A\u011fda cihaz bulunamad\u0131", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", + "setup_failed": "Cihaz kurulumu ba\u015far\u0131s\u0131z.", "unknown": "Beklenmeyen hata" }, "error": { @@ -16,14 +21,14 @@ "no_usable_service": "Bir ayg\u0131t bulundu, ancak ba\u011flant\u0131 kurman\u0131n herhangi bir yolunu tan\u0131mlayamad\u0131. Bu iletiyi g\u00f6rmeye devam ederseniz, IP adresini belirtmeye veya Apple TV'nizi yeniden ba\u015flatmaya \u00e7al\u0131\u015f\u0131n.", "unknown": "Beklenmeyen hata" }, - "flow_title": "{name}", + "flow_title": "{name} ( {type} )", "step": { "confirm": { - "description": "{name} ` adl\u0131 Apple TV'yi Ev Asistan\u0131na eklemek \u00fczeresiniz. \n\n **\u0130\u015flemi tamamlamak i\u00e7in birden fazla PIN kodu girmeniz gerekebilir.** \n\n Bu entegrasyonla Apple TV'nizi *kapatamayaca\u011f\u0131n\u0131z\u0131* l\u00fctfen unutmay\u0131n. Yaln\u0131zca Home Assistant'taki medya oynat\u0131c\u0131 kapanacak!", + "description": "Home Assistant'a {type} ` t\u00fcr\u00fcnde ` {name} ` eklemek \u00fczeresiniz. \n\n **\u0130\u015flemi tamamlamak i\u00e7in birden fazla PIN kodu girmeniz gerekebilir.** \n\n Bu entegrasyonla Apple TV'nizi *kapatamayaca\u011f\u0131n\u0131z\u0131* l\u00fctfen unutmay\u0131n. Yaln\u0131zca Home Assistant'taki medya oynat\u0131c\u0131 kapanacak!", "title": "Apple TV eklemeyi onaylay\u0131n" }, "pair_no_pin": { - "description": "{protocol} ` hizmeti i\u00e7in e\u015fle\u015ftirme gereklidir. Devam etmek i\u00e7in l\u00fctfen Apple TV'nizde PIN {pin}", + "description": "{protocol} ` hizmeti i\u00e7in e\u015fle\u015ftirme gereklidir. Devam etmek i\u00e7in l\u00fctfen cihaz\u0131n\u0131za PIN {pin} giriniz.", "title": "E\u015fle\u015ftirme" }, "pair_with_pin": { @@ -33,8 +38,16 @@ "description": "{protocol} ` protokol\u00fc i\u00e7in e\u015fle\u015ftirme gereklidir. L\u00fctfen ekranda g\u00f6r\u00fcnt\u00fclenen PIN kodunu girin. Ba\u015ftaki s\u0131f\u0131rlar atlanmal\u0131d\u0131r, yani g\u00f6r\u00fcnt\u00fclenen kod 0123 ise 123 girin.", "title": "E\u015fle\u015ftirme" }, + "password": { + "description": "{protocol} ` taraf\u0131ndan bir \u015fifre gerekiyor. Bu hen\u00fcz desteklenmiyor, l\u00fctfen devam etmek i\u00e7in \u015fifreyi devre d\u0131\u015f\u0131 b\u0131rak\u0131n.", + "title": "\u015eifre gerekli" + }, + "protocol_disabled": { + "description": "{protocol} ` i\u00e7in e\u015fle\u015ftirme gerekli ancak cihazda devre d\u0131\u015f\u0131 b\u0131rak\u0131ld\u0131. L\u00fctfen cihazdaki olas\u0131 eri\u015fim k\u0131s\u0131tlamalar\u0131n\u0131 g\u00f6zden ge\u00e7irin (\u00f6rne\u011fin, yerel a\u011fdaki t\u00fcm cihazlar\u0131n ba\u011flanmas\u0131na izin verin). \n\n Bu protokol\u00fc e\u015fle\u015ftirmeden devam edebilirsiniz, ancak baz\u0131 i\u015flevler s\u0131n\u0131rl\u0131 olacakt\u0131r.", + "title": "E\u015fle\u015ftirme m\u00fcmk\u00fcn de\u011fil" + }, "reconfigure": { - "description": "Bu Apple TV baz\u0131 ba\u011flant\u0131 sorunlar\u0131 ya\u015f\u0131yor ve yeniden yap\u0131land\u0131r\u0131lmas\u0131 gerekiyor.", + "description": "\u0130\u015flevselli\u011fini geri y\u00fcklemek i\u00e7in bu cihaz\u0131 yeniden yap\u0131land\u0131r\u0131n.", "title": "Cihaz\u0131n yeniden yap\u0131land\u0131r\u0131lmas\u0131" }, "service_problem": { @@ -45,7 +58,7 @@ "data": { "device_input": "Cihaz" }, - "description": "Eklemek istedi\u011finiz Apple TV'nin cihaz ad\u0131n\u0131 (\u00f6rn. Mutfak veya Yatak Odas\u0131) veya IP adresini girerek ba\u015flay\u0131n. A\u011f\u0131n\u0131zda otomatik olarak herhangi bir cihaz bulunduysa, bunlar a\u015fa\u011f\u0131da g\u00f6sterilmi\u015ftir. \n\n Cihaz\u0131n\u0131z\u0131 g\u00f6remiyorsan\u0131z veya herhangi bir sorun ya\u015f\u0131yorsan\u0131z, cihaz\u0131n IP adresini belirtmeyi deneyin. \n\n {devices}", + "description": "Eklemek istedi\u011finiz Apple TV'nin cihaz ad\u0131n\u0131 (\u00f6rn. Mutfak veya Yatak Odas\u0131) veya IP adresini girerek ba\u015flay\u0131n. \n\n Cihaz\u0131n\u0131z\u0131 g\u00f6remiyorsan\u0131z veya herhangi bir sorun ya\u015f\u0131yorsan\u0131z, cihaz\u0131n IP adresini belirtmeyi deneyin.", "title": "Yeni bir Apple TV kurun" } } diff --git a/homeassistant/components/aseko_pool_live/translations/it.json b/homeassistant/components/aseko_pool_live/translations/it.json new file mode 100644 index 00000000000..30f36eb67ca --- /dev/null +++ b/homeassistant/components/aseko_pool_live/translations/it.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "email": "E-mail", + "password": "Password" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aseko_pool_live/translations/lt.json b/homeassistant/components/aseko_pool_live/translations/lt.json new file mode 100644 index 00000000000..838d40511e4 --- /dev/null +++ b/homeassistant/components/aseko_pool_live/translations/lt.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepavyko prisijungti" + }, + "step": { + "user": { + "data": { + "email": "Elektroninis pa\u0161tas", + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/balboa/translations/lt.json b/homeassistant/components/balboa/translations/lt.json new file mode 100644 index 00000000000..5d5f99e0971 --- /dev/null +++ b/homeassistant/components/balboa/translations/lt.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "\u012erenginys paruo\u0161tas naudojimui" + }, + "error": { + "cannot_connect": "Nepavyko prisijungti" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/cs.json b/homeassistant/components/elmax/translations/cs.json new file mode 100644 index 00000000000..338ff5d9a44 --- /dev/null +++ b/homeassistant/components/elmax/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/id.json b/homeassistant/components/elmax/translations/id.json new file mode 100644 index 00000000000..e4670a00e12 --- /dev/null +++ b/homeassistant/components/elmax/translations/id.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "bad_auth": "Autentikasi tidak valid", + "invalid_pin": "PIN yang diberikan tidak valid", + "network_error": "Terjadi kesalahan jaringan", + "no_panel_online": "Tidak ada panel kontrol Elmax online yang ditemukan.", + "unknown_error": "Terjadi kesalahan tak terduga" + }, + "step": { + "panels": { + "data": { + "panel_id": "ID Panel", + "panel_name": "Nama Panel", + "panel_pin": "Kode PIN" + }, + "description": "Pilih panel mana yang ingin dikontrol dengan integrasi ini. Perhatikan bahwa panel harus AKTIF agar dapat dikonfigurasi.", + "title": "Pemilihan panel" + }, + "user": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "description": "Masuk ke cloud Elmax menggunakan kredensial Anda", + "title": "Akun Masuk" + } + } + }, + "title": "Penyiapan Elmax Cloud" +} \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/it.json b/homeassistant/components/elmax/translations/it.json new file mode 100644 index 00000000000..c80e64c81c9 --- /dev/null +++ b/homeassistant/components/elmax/translations/it.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "bad_auth": "Autenticazione non valida", + "invalid_pin": "Il pin fornito non \u00e8 valido", + "network_error": "Si \u00e8 verificato un errore di rete", + "no_panel_online": "Non \u00e8 stato trovato alcun pannello di controllo Elmax online.", + "unknown_error": "Si \u00e8 verificato un errore imprevisto" + }, + "step": { + "panels": { + "data": { + "panel_id": "ID pannello", + "panel_name": "Nome del pannello", + "panel_pin": "Codice PIN" + }, + "description": "Seleziona quale pannello vuoi controllare con questa integrazione. Si prega di notare che il pannello deve essere acceso per essere configurato.", + "title": "Selezione del pannello" + }, + "user": { + "data": { + "password": "Password", + "username": "Nome utente" + }, + "description": "Effettua il login al cloud di Elmax utilizzando le tue credenziali", + "title": "Accesso all'account" + } + } + }, + "title": "Configurazione di Elmax Cloud" +} \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/lt.json b/homeassistant/components/elmax/translations/lt.json new file mode 100644 index 00000000000..d59749831b1 --- /dev/null +++ b/homeassistant/components/elmax/translations/lt.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u012erenginis paruo\u0161tas naudojimui" + }, + "step": { + "panels": { + "data": { + "panel_pin": "PIN kodas" + } + }, + "user": { + "data": { + "password": "Slapta\u017eodis", + "username": "Prisijungimo vardas" + }, + "description": "Prisijunkite prie \"Elmax\" debesies naudodami savo prisijungimo duomenis", + "title": "Paskyros prisijungimas" + } + } + }, + "title": "Elmax Cloud nustatymai" +} \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/tr.json b/homeassistant/components/elmax/translations/tr.json new file mode 100644 index 00000000000..a6f8a434c8c --- /dev/null +++ b/homeassistant/components/elmax/translations/tr.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "bad_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "invalid_pin": "Sa\u011flanan pin ge\u00e7ersiz", + "network_error": "Bir a\u011f hatas\u0131 olu\u015ftu", + "no_panel_online": "\u00c7evrimi\u00e7i Elmax kontrol paneli bulunamad\u0131.", + "unknown_error": "Beklenmeyen bir hata olu\u015ftu" + }, + "step": { + "panels": { + "data": { + "panel_id": "Panel Kimli\u011fi", + "panel_name": "Panel Ad\u0131", + "panel_pin": "PIN Kodu" + }, + "description": "Bu entegrasyon ile hangi paneli kontrol etmek istedi\u011finizi se\u00e7in. L\u00fctfen konfig\u00fcre edilebilmesi i\u00e7in panelin A\u00c7IK olmas\u0131 gerekti\u011fini unutmay\u0131n.", + "title": "Panel se\u00e7imi" + }, + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "L\u00fctfen kimlik bilgilerinizi kullanarak Elmax bulutuna giri\u015f yap\u0131n", + "title": "Hesap Giri\u015fi" + } + } + }, + "title": "Elmax Bulut Kurulumu" +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/it.json b/homeassistant/components/enphase_envoy/translations/it.json index 8fd9ab9ce25..98d8520d6ce 100644 --- a/homeassistant/components/enphase_envoy/translations/it.json +++ b/homeassistant/components/enphase_envoy/translations/it.json @@ -16,7 +16,8 @@ "host": "Host", "password": "Password", "username": "Nome utente" - } + }, + "description": "Per i modelli pi\u00f9 recenti, inserisci il nome utente \"envoy\" senza password. Per i modelli pi\u00f9 vecchi, inserisci il nome utente \"installer\" senza password. Per tutti gli altri modelli, inserisci un nome utente e una password validi." } } } diff --git a/homeassistant/components/enphase_envoy/translations/tr.json b/homeassistant/components/enphase_envoy/translations/tr.json index 4bcc08e0672..c1ab59f3716 100644 --- a/homeassistant/components/enphase_envoy/translations/tr.json +++ b/homeassistant/components/enphase_envoy/translations/tr.json @@ -16,7 +16,8 @@ "host": "Sunucu", "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" - } + }, + "description": "Daha yeni modeller i\u00e7in, parola olmadan 'envoy' kullan\u0131c\u0131 ad\u0131n\u0131 girin. Daha eski modeller i\u00e7in, parola olmadan 'installer' kullan\u0131c\u0131 ad\u0131n\u0131 girin. Di\u011fer t\u00fcm modeller i\u00e7in ge\u00e7erli bir kullan\u0131c\u0131 ad\u0131 ve \u015fifre girin." } } } diff --git a/homeassistant/components/fronius/translations/lt.json b/homeassistant/components/fronius/translations/lt.json new file mode 100644 index 00000000000..826d1cae85f --- /dev/null +++ b/homeassistant/components/fronius/translations/lt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u012erenginys paruo\u0161tas naudojimui" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/translations/lt.json b/homeassistant/components/hue/translations/lt.json index 1e12894085b..6838078d19d 100644 --- a/homeassistant/components/hue/translations/lt.json +++ b/homeassistant/components/hue/translations/lt.json @@ -7,5 +7,13 @@ } } } + }, + "device_automation": { + "trigger_subtype": { + "1": "Pirmasis mygtukas", + "2": "Antras mygtukas", + "3": "Tre\u010dias mygtukas", + "4": "Ketvirtas mygtukas" + } } } \ No newline at end of file diff --git a/homeassistant/components/jellyfin/translations/lt.json b/homeassistant/components/jellyfin/translations/lt.json new file mode 100644 index 00000000000..e729d845e5c --- /dev/null +++ b/homeassistant/components/jellyfin/translations/lt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis", + "username": "Prisijungimo vardas" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/translations/lt.json b/homeassistant/components/konnected/translations/lt.json new file mode 100644 index 00000000000..9db781c2dc9 --- /dev/null +++ b/homeassistant/components/konnected/translations/lt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cannot_connect": "Nepavyko prisijungti" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nina/translations/it.json b/homeassistant/components/nina/translations/it.json new file mode 100644 index 00000000000..ee7668cf9ce --- /dev/null +++ b/homeassistant/components/nina/translations/it.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "no_selection": "Seleziona almeno una citt\u00e0/provincia", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "_a_to_d": "Citt\u00e0/provincia (A-D)", + "_e_to_h": "Citt\u00e0/provincia (E-H)", + "_i_to_l": "Citt\u00e0/provincia (I-L)", + "_m_to_q": "Citt\u00e0/provincia (M-Q)", + "_r_to_u": "Citt\u00e0/provincia (R-U)", + "_v_to_z": "Citt\u00e0/provincia (V-Z)", + "corona_filter": "Rimuovere gli avvisi Corona", + "slots": "Numero massimo di avvisi per citt\u00e0/provincia" + }, + "title": "Seleziona citt\u00e0/provincia" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nina/translations/lt.json b/homeassistant/components/nina/translations/lt.json new file mode 100644 index 00000000000..fad1f16a66a --- /dev/null +++ b/homeassistant/components/nina/translations/lt.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Pasirinkti miest\u0105 / apskrit\u012f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ridwell/translations/lt.json b/homeassistant/components/ridwell/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/ridwell/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/en.json b/homeassistant/components/simplisafe/translations/en.json index 5829c68301f..a3481ecf2c7 100644 --- a/homeassistant/components/simplisafe/translations/en.json +++ b/homeassistant/components/simplisafe/translations/en.json @@ -12,11 +12,33 @@ "unknown": "Unexpected error" }, "step": { - "user": { + "input_auth_code": { "data": { "auth_code": "Authorization Code" }, - "description": "SimpliSafe authenticates with Home Assistant via the SimpliSafe web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation]({docs_url}) before starting.\n\n1. Click [here]({url}) to open the SimpliSafe web app and input your credentials.\n\n2. When the login process is complete, return here and input the authorization code below." + "description": "Input the authorization code from the SimpliSafe web app URL:", + "title": "Finish Authorization" + }, + "mfa": { + "description": "Check your email for a link from SimpliSafe. After verifying the link, return here to complete the installation of the integration.", + "title": "SimpliSafe Multi-Factor Authentication" + }, + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "Your access has expired or been revoked. Enter your password to re-link your account.", + "title": "Reauthenticate Integration" + }, + "user": { + "data": { + "auth_code": "Authorization Code", + "code": "Code (used in Home Assistant UI)", + "password": "Password", + "username": "Email" + }, + "description": "SimpliSafe authenticates with Home Assistant via the SimpliSafe web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation]({docs_url}) before starting.\n\n1. Click [here]({url}) to open the SimpliSafe web app and input your credentials.\n\n2. When the login process is complete, return here and input the authorization code below.", + "title": "Fill in your information." } } }, diff --git a/homeassistant/components/tesla_wall_connector/translations/lt.json b/homeassistant/components/tesla_wall_connector/translations/lt.json new file mode 100644 index 00000000000..826d1cae85f --- /dev/null +++ b/homeassistant/components/tesla_wall_connector/translations/lt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u012erenginys paruo\u0161tas naudojimui" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/lt.json b/homeassistant/components/tolo/translations/lt.json new file mode 100644 index 00000000000..5d5f99e0971 --- /dev/null +++ b/homeassistant/components/tolo/translations/lt.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "\u012erenginys paruo\u0161tas naudojimui" + }, + "error": { + "cannot_connect": "Nepavyko prisijungti" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/select.lt.json b/homeassistant/components/tolo/translations/select.lt.json new file mode 100644 index 00000000000..3d151a35c9f --- /dev/null +++ b/homeassistant/components/tolo/translations/select.lt.json @@ -0,0 +1,8 @@ +{ + "state": { + "tolo__lamp_mode": { + "automatic": "Automatinis", + "manual": "Rankinis" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_weatherstation/translations/lt.json b/homeassistant/components/trafikverket_weatherstation/translations/lt.json new file mode 100644 index 00000000000..87076aabdf1 --- /dev/null +++ b/homeassistant/components/trafikverket_weatherstation/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Prisijungimo vardas" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/lt.json b/homeassistant/components/wallbox/translations/lt.json new file mode 100644 index 00000000000..1cf4189b1ba --- /dev/null +++ b/homeassistant/components/wallbox/translations/lt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "Slapta\u017eodis", + "username": "Prisijungimo vardas" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/translations/select.lt.json b/homeassistant/components/wled/translations/select.lt.json new file mode 100644 index 00000000000..e617ff3e8d1 --- /dev/null +++ b/homeassistant/components/wled/translations/select.lt.json @@ -0,0 +1,8 @@ +{ + "state": { + "wled__live_override": { + "0": "I\u0161jungta", + "1": "\u012ejungta" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/select.it.json b/homeassistant/components/yamaha_musiccast/translations/select.it.json new file mode 100644 index 00000000000..0640173398f --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/select.it.json @@ -0,0 +1,52 @@ +{ + "state": { + "yamaha_musiccast__dimmer": { + "auto": "Automatico" + }, + "yamaha_musiccast__zone_equalizer_mode": { + "auto": "Automatico", + "bypass": "Bypass", + "manual": "Manuale" + }, + "yamaha_musiccast__zone_link_audio_delay": { + "audio_sync": "Sincronizzazione audio", + "audio_sync_off": "Sincronizzazione audio disattivata", + "audio_sync_on": "Sincronizzazione audio attivata", + "balanced": "Bilanciato", + "lip_sync": "Sincronizzazione labiale" + }, + "yamaha_musiccast__zone_link_audio_quality": { + "compressed": "Compresso", + "uncompressed": "Non compresso" + }, + "yamaha_musiccast__zone_link_control": { + "speed": "Velocit\u00e0", + "stability": "Stabilit\u00e0", + "standard": "Standard" + }, + "yamaha_musiccast__zone_sleep": { + "120 min": "120 minuti", + "30 min": "30 minuti", + "60 min": "60 minuti", + "90 min": "90 minuti", + "off": "Spento" + }, + "yamaha_musiccast__zone_surr_decoder_type": { + "auto": "Automatico", + "dolby_pl": "Dolby ProLogic", + "dolby_pl2x_game": "Dolby ProLogic 2x Gioco", + "dolby_pl2x_movie": "Dolby ProLogic 2x Film", + "dolby_pl2x_music": "Dolby ProLogic 2x Musica", + "dolby_surround": "Dolby Surround", + "dts_neo6_cinema": "DTS Neo:6 Cinema", + "dts_neo6_music": "DTS Neo:6 Musica", + "dts_neural_x": "DTS Neural:X", + "toggle": "Attiva/disattiva" + }, + "yamaha_musiccast__zone_tone_control_mode": { + "auto": "Automatico", + "bypass": "Bypass", + "manual": "Manuale" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/select.lt.json b/homeassistant/components/yamaha_musiccast/translations/select.lt.json new file mode 100644 index 00000000000..28f2a709a97 --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/select.lt.json @@ -0,0 +1,29 @@ +{ + "state": { + "yamaha_musiccast__dimmer": { + "auto": "Automatinis" + }, + "yamaha_musiccast__zone_equalizer_mode": { + "auto": "Automatinis", + "manual": "Rankinis" + }, + "yamaha_musiccast__zone_link_control": { + "speed": "Greitis", + "standard": "Standartas" + }, + "yamaha_musiccast__zone_sleep": { + "120 min": "120 minu\u010di\u0173", + "30 min": "30 minu\u010di\u0173", + "60 min": "60 minu\u010di\u0173", + "90 min": "90 minu\u010di\u0173", + "off": "I\u0161jungta" + }, + "yamaha_musiccast__zone_surr_decoder_type": { + "auto": "Automatinis" + }, + "yamaha_musiccast__zone_tone_control_mode": { + "auto": "Automatinis", + "manual": "Rankinis" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/select.tr.json b/homeassistant/components/yamaha_musiccast/translations/select.tr.json new file mode 100644 index 00000000000..4033d44be25 --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/select.tr.json @@ -0,0 +1,52 @@ +{ + "state": { + "yamaha_musiccast__dimmer": { + "auto": "Otomatik" + }, + "yamaha_musiccast__zone_equalizer_mode": { + "auto": "Otomatik", + "bypass": "Atlatma", + "manual": "Manuel" + }, + "yamaha_musiccast__zone_link_audio_delay": { + "audio_sync": "Ses Senkronizasyonu", + "audio_sync_off": "Ses Senkronizasyonu Kapal\u0131", + "audio_sync_on": "Ses Senkronizasyonu A\u00e7\u0131k", + "balanced": "Dengeli", + "lip_sync": "Dudak Senkronizasyonu" + }, + "yamaha_musiccast__zone_link_audio_quality": { + "compressed": "S\u0131k\u0131\u015ft\u0131r\u0131lm\u0131\u015f", + "uncompressed": "S\u0131k\u0131\u015ft\u0131r\u0131lmam\u0131\u015f" + }, + "yamaha_musiccast__zone_link_control": { + "speed": "H\u0131z", + "stability": "Stabilite", + "standard": "Standart" + }, + "yamaha_musiccast__zone_sleep": { + "120 min": "120 Dakika", + "30 min": "30 Dakika", + "60 min": "60 Dakika", + "90 min": "90 Dakika", + "off": "Kapal\u0131" + }, + "yamaha_musiccast__zone_surr_decoder_type": { + "auto": "Otomatik", + "dolby_pl": "Dolby ProLogic", + "dolby_pl2x_game": "Dolby ProLogic 2x Oyun", + "dolby_pl2x_movie": "Dolby ProLogic 2x Film", + "dolby_pl2x_music": "Dolby ProLogic 2x M\u00fczik", + "dolby_surround": "Dolby Surround", + "dts_neo6_cinema": "DTS Neo:6 Sinema", + "dts_neo6_music": "DTS Neo:6 M\u00fczik", + "dts_neural_x": "DTS Neural:X", + "toggle": "De\u011fi\u015ftir" + }, + "yamaha_musiccast__zone_tone_control_mode": { + "auto": "Otomatik", + "bypass": "Atlatma", + "manual": "Manuel" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/it.json b/homeassistant/components/zwave_js/translations/it.json index af3416ed9a9..310aa56fcc4 100644 --- a/homeassistant/components/zwave_js/translations/it.json +++ b/homeassistant/components/zwave_js/translations/it.json @@ -33,6 +33,7 @@ "s2_unauthenticated_key": "Chiave S2 non autenticata", "usb_path": "Percorso del dispositivo USB" }, + "description": "Il componente aggiuntivo generer\u00e0 chiavi di sicurezza se questi campi vengono lasciati vuoti.", "title": "Accedi alla configurazione del componente aggiuntivo Z-Wave JS" }, "hassio_confirm": { @@ -119,6 +120,7 @@ "s2_unauthenticated_key": "Chiave S2 non autenticata", "usb_path": "Percorso del dispositivo USB" }, + "description": "Il componente aggiuntivo generer\u00e0 chiavi di sicurezza se questi campi vengono lasciati vuoti.", "title": "Entra nella configurazione del componente aggiuntivo Z-Wave JS" }, "install_addon": { diff --git a/homeassistant/components/zwave_js/translations/tr.json b/homeassistant/components/zwave_js/translations/tr.json index f77b7f1f0f4..efaa87cc48d 100644 --- a/homeassistant/components/zwave_js/translations/tr.json +++ b/homeassistant/components/zwave_js/translations/tr.json @@ -33,6 +33,7 @@ "s2_unauthenticated_key": "S2 Kimli\u011fi Do\u011frulanmam\u0131\u015f Anahtar", "usb_path": "USB Ayg\u0131t Yolu" }, + "description": "Bu alanlar bo\u015f b\u0131rak\u0131l\u0131rsa eklenti g\u00fcvenlik anahtarlar\u0131 \u00fcretecektir.", "title": "Z-Wave JS eklenti yap\u0131land\u0131rmas\u0131na girin" }, "hassio_confirm": { @@ -119,6 +120,7 @@ "s2_unauthenticated_key": "S2 Kimli\u011fi Do\u011frulanmam\u0131\u015f Anahtar", "usb_path": "USB Cihaz Yolu" }, + "description": "Bu alanlar bo\u015f b\u0131rak\u0131l\u0131rsa eklenti g\u00fcvenlik anahtarlar\u0131 \u00fcretecektir.", "title": "Z-Wave JS eklenti yap\u0131land\u0131rmas\u0131na girin" }, "install_addon": { From 27d1063dec4f136664a3f86476a7bd149ff0f279 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 10 Dec 2021 08:19:53 +0100 Subject: [PATCH 0263/2644] Revert "Skip duplicated data when calculating fossil energy consumption (#60599)" (#61323) This reverts commit 159506262a25f362bafec3d0b48e4b0045f096d7. --- .../components/energy/websocket_api.py | 4 +- tests/components/energy/test_websocket_api.py | 217 +----------------- 2 files changed, 2 insertions(+), 219 deletions(-) diff --git a/homeassistant/components/energy/websocket_api.py b/homeassistant/components/energy/websocket_api.py index d243faae89f..cdc7599b55b 100644 --- a/homeassistant/components/energy/websocket_api.py +++ b/homeassistant/components/energy/websocket_api.py @@ -274,16 +274,14 @@ async def ws_get_fossil_energy_consumption( ) -> dict[datetime, float]: """Combine multiple statistics, returns a dict indexed by start time.""" result: defaultdict[datetime, float] = defaultdict(float) - seen: defaultdict[datetime, set[str]] = defaultdict(set) for statistics_id, stat in stats.items(): if statistics_id not in statistic_ids: continue for period in stat: - if period["sum"] is None or statistics_id in seen[period["start"]]: + if period["sum"] is None: continue result[period["start"]] += period["sum"] - seen[period["start"]].add(statistics_id) return {key: result[key] for key in sorted(result)} diff --git a/tests/components/energy/test_websocket_api.py b/tests/components/energy/test_websocket_api.py index c1dc195a63e..46c6a5c0fa6 100644 --- a/tests/components/energy/test_websocket_api.py +++ b/tests/components/energy/test_websocket_api.py @@ -1,10 +1,9 @@ """Test the Energy websocket API.""" -from unittest.mock import AsyncMock, Mock, patch +from unittest.mock import AsyncMock, Mock import pytest from homeassistant.components.energy import data, is_configured -from homeassistant.components.recorder import statistics from homeassistant.components.recorder.statistics import async_add_external_statistics from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -964,220 +963,6 @@ async def test_fossil_energy_consumption(hass, hass_ws_client): } -@pytest.mark.freeze_time("2021-08-01 00:00:00+00:00") -async def test_fossil_energy_consumption_duplicate(hass, hass_ws_client): - """Test fossil_energy_consumption with co2 sensor data.""" - now = dt_util.utcnow() - later = dt_util.as_utc(dt_util.parse_datetime("2022-09-01 00:00:00")) - - await hass.async_add_executor_job(init_recorder_component, hass) - await async_setup_component(hass, "history", {}) - await async_setup_component(hass, "sensor", {}) - - period1 = dt_util.as_utc(dt_util.parse_datetime("2021-09-01 00:00:00")) - period2 = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 23:00:00")) - period2_day_start = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 00:00:00")) - period3 = dt_util.as_utc(dt_util.parse_datetime("2021-10-01 00:00:00")) - period4 = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 23:00:00")) - period4_day_start = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 00:00:00")) - - external_energy_statistics_1 = ( - { - "start": period1, - "last_reset": None, - "state": 0, - "sum": 2, - }, - { - "start": period2, - "last_reset": None, - "state": 1, - "sum": 3, - }, - { - "start": period3, - "last_reset": None, - "state": 2, - "sum": 4, - }, - { - "start": period4, - "last_reset": None, - "state": 3, - "sum": 5, - }, - { - "start": period4, - "last_reset": None, - "state": 3, - "sum": 5, - }, - ) - external_energy_metadata_1 = { - "has_mean": False, - "has_sum": True, - "name": "Total imported energy", - "source": "test", - "statistic_id": "test:total_energy_import_tariff_1", - "unit_of_measurement": "kWh", - } - external_energy_statistics_2 = ( - { - "start": period1, - "last_reset": None, - "state": 0, - "sum": 20, - }, - { - "start": period2, - "last_reset": None, - "state": 1, - "sum": 30, - }, - { - "start": period3, - "last_reset": None, - "state": 2, - "sum": 40, - }, - { - "start": period4, - "last_reset": None, - "state": 3, - "sum": 50, - }, - { - "start": period4, - "last_reset": None, - "state": 3, - "sum": 50, - }, - ) - external_energy_metadata_2 = { - "has_mean": False, - "has_sum": True, - "name": "Total imported energy", - "source": "test", - "statistic_id": "test:total_energy_import_tariff_2", - "unit_of_measurement": "kWh", - } - external_co2_statistics = ( - { - "start": period1, - "last_reset": None, - "mean": 10, - }, - { - "start": period2, - "last_reset": None, - "mean": 30, - }, - { - "start": period3, - "last_reset": None, - "mean": 60, - }, - { - "start": period4, - "last_reset": None, - "mean": 90, - }, - ) - external_co2_metadata = { - "has_mean": True, - "has_sum": False, - "name": "Fossil percentage", - "source": "test", - "statistic_id": "test:fossil_percentage", - "unit_of_measurement": "%", - } - - with patch.object( - statistics, "_statistics_exists", return_value=False - ), patch.object( - statistics, "_insert_statistics", wraps=statistics._insert_statistics - ) as insert_statistics_mock: - async_add_external_statistics( - hass, external_energy_metadata_1, external_energy_statistics_1 - ) - async_add_external_statistics( - hass, external_energy_metadata_2, external_energy_statistics_2 - ) - async_add_external_statistics( - hass, external_co2_metadata, external_co2_statistics - ) - await async_wait_recording_done_without_instance(hass) - assert insert_statistics_mock.call_count == 14 - - client = await hass_ws_client() - await client.send_json( - { - "id": 1, - "type": "energy/fossil_energy_consumption", - "start_time": now.isoformat(), - "end_time": later.isoformat(), - "energy_statistic_ids": [ - "test:total_energy_import_tariff_1", - "test:total_energy_import_tariff_2", - ], - "co2_statistic_id": "test:fossil_percentage", - "period": "hour", - } - ) - response = await client.receive_json() - assert response["success"] - assert response["result"] == { - period2.isoformat(): pytest.approx((33.0 - 22.0) * 0.3), - period3.isoformat(): pytest.approx((44.0 - 33.0) * 0.6), - period4.isoformat(): pytest.approx((55.0 - 44.0) * 0.9), - } - - await client.send_json( - { - "id": 2, - "type": "energy/fossil_energy_consumption", - "start_time": now.isoformat(), - "end_time": later.isoformat(), - "energy_statistic_ids": [ - "test:total_energy_import_tariff_1", - "test:total_energy_import_tariff_2", - ], - "co2_statistic_id": "test:fossil_percentage", - "period": "day", - } - ) - response = await client.receive_json() - assert response["success"] - assert response["result"] == { - period2_day_start.isoformat(): pytest.approx((33.0 - 22.0) * 0.3), - period3.isoformat(): pytest.approx((44.0 - 33.0) * 0.6), - period4_day_start.isoformat(): pytest.approx((55.0 - 44.0) * 0.9), - } - - await client.send_json( - { - "id": 3, - "type": "energy/fossil_energy_consumption", - "start_time": now.isoformat(), - "end_time": later.isoformat(), - "energy_statistic_ids": [ - "test:total_energy_import_tariff_1", - "test:total_energy_import_tariff_2", - ], - "co2_statistic_id": "test:fossil_percentage", - "period": "month", - } - ) - response = await client.receive_json() - assert response["success"] - assert response["result"] == { - period1.isoformat(): pytest.approx((33.0 - 22.0) * 0.3), - period3.isoformat(): pytest.approx( - ((44.0 - 33.0) * 0.6) + ((55.0 - 44.0) * 0.9) - ), - } - - async def test_fossil_energy_consumption_checks(hass, hass_ws_client): """Test fossil_energy_consumption parameter validation.""" client = await hass_ws_client(hass) From 8383da6a5e99777d0650f66e0330ec057f195482 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 9 Dec 2021 21:28:35 -1000 Subject: [PATCH 0264/2644] Fix older v1 dimmable flux_led bulbs not turning on (#61414) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index ca6dc5a5a1d..59d5ec46110 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Magic Home", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.26.3"], + "requirements": ["flux_led==0.26.5"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index c0c6048a684..721afc9eee7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -667,7 +667,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.26.3 +flux_led==0.26.5 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9e9bc7dc4c2..fcce704eee7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -408,7 +408,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.26.3 +flux_led==0.26.5 # homeassistant.components.homekit fnvhash==0.1.0 From 8d483e2206036a8a10ae36d43e39fbc34b226b7f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 9 Dec 2021 21:29:27 -1000 Subject: [PATCH 0265/2644] Fix flux_led discovery with older models (#61413) --- .../components/flux_led/config_flow.py | 9 +++++- tests/components/flux_led/__init__.py | 18 ++++++++--- tests/components/flux_led/test_config_flow.py | 30 +++++++++++++++++++ tests/components/flux_led/test_light.py | 28 ++++++++--------- tests/components/flux_led/test_number.py | 8 ++--- tests/components/flux_led/test_switch.py | 2 +- 6 files changed, 71 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/flux_led/config_flow.py b/homeassistant/components/flux_led/config_flow.py index b6efc763b6d..d0190730ed7 100644 --- a/homeassistant/components/flux_led/config_flow.py +++ b/homeassistant/components/flux_led/config_flow.py @@ -238,7 +238,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FluxLEDDiscovery: """Try to connect.""" self._async_abort_entries_match({CONF_HOST: host}) - if device := await async_discover_device(self.hass, host): + if (device := await async_discover_device(self.hass, host)) and device[ + ATTR_MODEL_DESCRIPTION + ]: + # Older models do not return enough information + # to build the model description via UDP so we have + # to fallback to making a tcp connection to avoid + # identifying the device as the chip model number + # AKA `HF-LPB100-ZJ200` return device bulb = async_wifi_bulb_for_host(host) try: diff --git a/tests/components/flux_led/__init__.py b/tests/components/flux_led/__init__.py index 764c33686b7..6bfe02990a2 100644 --- a/tests/components/flux_led/__init__.py +++ b/tests/components/flux_led/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from contextlib import contextmanager import datetime from typing import Callable from unittest.mock import AsyncMock, MagicMock, patch @@ -162,11 +163,20 @@ def _patch_discovery(device=None, no_device=False): async def _discovery(*args, **kwargs): if no_device: raise OSError - return [FLUX_DISCOVERY] + return [] if no_device else [device or FLUX_DISCOVERY] - return patch( - "homeassistant.components.flux_led.AIOBulbScanner.async_scan", new=_discovery - ) + @contextmanager + def _patcher(): + with patch( + "homeassistant.components.flux_led.AIOBulbScanner.async_scan", + new=_discovery, + ), patch( + "homeassistant.components.flux_led.AIOBulbScanner.getBulbInfo", + return_value=[] if no_device else [device or FLUX_DISCOVERY], + ): + yield + + return _patcher() def _patch_wifibulb(device=None, no_device=False): diff --git a/tests/components/flux_led/test_config_flow.py b/tests/components/flux_led/test_config_flow.py index 4c956358818..a546120ae41 100644 --- a/tests/components/flux_led/test_config_flow.py +++ b/tests/components/flux_led/test_config_flow.py @@ -31,6 +31,7 @@ from . import ( DEFAULT_ENTRY_TITLE, DHCP_DISCOVERY, FLUX_DISCOVERY, + FLUX_DISCOVERY_PARTIAL, IP_ADDRESS, MAC_ADDRESS, MODULE, @@ -435,6 +436,35 @@ async def test_discovered_by_dhcp_no_udp_response(hass): assert mock_async_setup_entry.called +async def test_discovered_by_dhcp_partial_udp_response_fallback_tcp(hass): + """Test we can setup when discovered from dhcp but part of the udp response is missing.""" + + with _patch_discovery(no_device=True), _patch_wifibulb(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with _patch_discovery(device=FLUX_DISCOVERY_PARTIAL), _patch_wifibulb(), patch( + f"{MODULE}.async_setup", return_value=True + ) as mock_async_setup, patch( + f"{MODULE}.async_setup_entry", return_value=True + ) as mock_async_setup_entry: + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["data"] == { + CONF_HOST: IP_ADDRESS, + CONF_NAME: DEFAULT_ENTRY_TITLE, + } + assert mock_async_setup.called + assert mock_async_setup_entry.called + + async def test_discovered_by_dhcp_no_udp_response_or_tcp_response(hass): """Test we can setup when discovered from dhcp but no udp response or tcp response.""" diff --git a/tests/components/flux_led/test_light.py b/tests/components/flux_led/test_light.py index 6f08ae8a307..92ea0fd8d39 100644 --- a/tests/components/flux_led/test_light.py +++ b/tests/components/flux_led/test_light.py @@ -80,7 +80,7 @@ async def test_light_unique_id(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) bulb = _mocked_bulb() - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -100,7 +100,7 @@ async def test_light_goes_unavailable_and_recovers(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) bulb = _mocked_bulb() - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -190,7 +190,7 @@ async def test_rgb_light(hass: HomeAssistant) -> None: bulb.raw_state = bulb.raw_state._replace(model_num=0x33) # RGB only model bulb.color_modes = {FLUX_COLOR_MODE_RGB} bulb.color_mode = FLUX_COLOR_MODE_RGB - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(no_device=True), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -294,7 +294,7 @@ async def test_rgb_light_auto_on(hass: HomeAssistant) -> None: bulb.raw_state = bulb.raw_state._replace(model_num=0x33) # RGB only model bulb.color_modes = {FLUX_COLOR_MODE_RGB} bulb.color_mode = FLUX_COLOR_MODE_RGB - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -407,7 +407,7 @@ async def test_rgb_cct_light(hass: HomeAssistant) -> None: bulb.raw_state = bulb.raw_state._replace(model_num=0x35) # RGB & CCT model bulb.color_modes = {FLUX_COLOR_MODE_RGB, FLUX_COLOR_MODE_CCT} bulb.color_mode = FLUX_COLOR_MODE_RGB - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -526,7 +526,7 @@ async def test_rgbw_light(hass: HomeAssistant) -> None: bulb = _mocked_bulb() bulb.color_modes = {FLUX_COLOR_MODE_RGBW} bulb.color_mode = FLUX_COLOR_MODE_RGBW - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -628,7 +628,7 @@ async def test_rgb_or_w_light(hass: HomeAssistant) -> None: bulb = _mocked_bulb() bulb.color_modes = FLUX_COLOR_MODES_RGB_W bulb.color_mode = FLUX_COLOR_MODE_RGB - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -739,7 +739,7 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None: bulb.raw_state = bulb.raw_state._replace(warm_white=1, cool_white=2) bulb.color_modes = {FLUX_COLOR_MODE_RGBWW, FLUX_COLOR_MODE_CCT} bulb.color_mode = FLUX_COLOR_MODE_RGBWW - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -879,7 +879,7 @@ async def test_white_light(hass: HomeAssistant) -> None: bulb.protocol = None bulb.color_modes = {FLUX_COLOR_MODE_DIM} bulb.color_mode = FLUX_COLOR_MODE_DIM - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -930,7 +930,7 @@ async def test_no_color_modes(hass: HomeAssistant) -> None: bulb.protocol = None bulb.color_modes = set() bulb.color_mode = None - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -974,7 +974,7 @@ async def test_rgb_light_custom_effects(hass: HomeAssistant) -> None: bulb = _mocked_bulb() bulb.color_modes = {FLUX_COLOR_MODE_RGB} bulb.color_mode = FLUX_COLOR_MODE_RGB - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -1056,7 +1056,7 @@ async def test_rgb_light_custom_effects_invalid_colors( bulb = _mocked_bulb() bulb.color_modes = {FLUX_COLOR_MODE_RGB} bulb.color_mode = FLUX_COLOR_MODE_RGB - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -1085,7 +1085,7 @@ async def test_rgb_light_custom_effect_via_service( bulb = _mocked_bulb() bulb.color_modes = {FLUX_COLOR_MODE_RGB} bulb.color_mode = FLUX_COLOR_MODE_RGB - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -1230,7 +1230,7 @@ async def test_addressable_light(hass: HomeAssistant) -> None: bulb.raw_state = bulb.raw_state._replace(model_num=0x33) # RGB only model bulb.color_modes = {FLUX_COLOR_MODE_ADDRESSABLE} bulb.color_mode = FLUX_COLOR_MODE_ADDRESSABLE - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() diff --git a/tests/components/flux_led/test_number.py b/tests/components/flux_led/test_number.py index 11df6daae4a..325307f1f32 100644 --- a/tests/components/flux_led/test_number.py +++ b/tests/components/flux_led/test_number.py @@ -41,7 +41,7 @@ async def test_number_unique_id(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) bulb = _mocked_bulb() - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -64,7 +64,7 @@ async def test_rgb_light_effect_speed(hass: HomeAssistant) -> None: bulb.color_modes = {FLUX_COLOR_MODE_RGB} bulb.color_mode = FLUX_COLOR_MODE_RGB - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -129,7 +129,7 @@ async def test_original_addressable_light_effect_speed(hass: HomeAssistant) -> N bulb.color_mode = FLUX_COLOR_MODE_RGB bulb.effect = "7 colors change gradually" bulb.speed = 50 - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -186,7 +186,7 @@ async def test_addressable_light_effect_speed(hass: HomeAssistant) -> None: bulb.color_mode = FLUX_COLOR_MODE_RGB bulb.effect = "RBM 1" bulb.speed = 50 - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() diff --git a/tests/components/flux_led/test_switch.py b/tests/components/flux_led/test_switch.py index 852e1efd49e..b569d51e13a 100644 --- a/tests/components/flux_led/test_switch.py +++ b/tests/components/flux_led/test_switch.py @@ -35,7 +35,7 @@ async def test_switch_on_off(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) switch = _mocked_switch() - with _patch_discovery(device=switch), _patch_wifibulb(device=switch): + with _patch_discovery(), _patch_wifibulb(device=switch): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() From 9a46d238f75fddb1c4c602260ae39dfcab6457ab Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 10 Dec 2021 00:30:15 -0700 Subject: [PATCH 0266/2644] Bump simplisafe-python to 2021.12.1 (#61412) --- homeassistant/components/simplisafe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 954c39efce1..0b6cb385be6 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,7 +3,7 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==2021.12.0"], + "requirements": ["simplisafe-python==2021.12.1"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 721afc9eee7..a2c093cb4e5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2155,7 +2155,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==2021.12.0 +simplisafe-python==2021.12.1 # homeassistant.components.sisyphus sisyphus-control==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fcce704eee7..9fe243edb5b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1285,7 +1285,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==2021.12.0 +simplisafe-python==2021.12.1 # homeassistant.components.slack slackclient==2.5.0 From fe17c9ffb62a72199a76c8b24e2bf7c0da8c2729 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 9 Dec 2021 21:35:07 -1000 Subject: [PATCH 0267/2644] Fix lookin set temperature when device is off (#61411) --- homeassistant/components/lookin/climate.py | 23 ++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/homeassistant/components/lookin/climate.py b/homeassistant/components/lookin/climate.py index cab4b0968eb..356b57453bc 100644 --- a/homeassistant/components/lookin/climate.py +++ b/homeassistant/components/lookin/climate.py @@ -10,6 +10,7 @@ from aiolookin import Climate, MeteoSensor, SensorID from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( + ATTR_HVAC_MODE, FAN_AUTO, FAN_HIGH, FAN_LOW, @@ -151,6 +152,28 @@ class ConditionerEntity(LookinCoordinatorEntity, ClimateEntity): if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return self._climate.temp_celsius = int(temperature) + lookin_index = LOOKIN_HVAC_MODE_IDX_TO_HASS + if hvac_mode := kwargs.get(ATTR_HVAC_MODE): + self._climate.hvac_mode = HASS_TO_LOOKIN_HVAC_MODE[hvac_mode] + elif self._climate.hvac_mode == lookin_index.index(HVAC_MODE_OFF): + # + # If the device is off, and the user didn't specify an HVAC mode + # (which is the default when using the HA UI), the device won't turn + # on without having an HVAC mode passed. + # + # We picked the hvac mode based on the current temp if its available + # since only some units support auto, but most support either heat + # or cool otherwise we set auto since we don't have a way to make + # an educated guess. + # + meteo_data: MeteoSensor = self._meteo_coordinator.data + current_temp = meteo_data.temperature + if not current_temp: + self._climate.hvac_mode = lookin_index.index(HVAC_MODE_AUTO) + elif current_temp >= self._climate.temp_celsius: + self._climate.hvac_mode = lookin_index.index(HVAC_MODE_COOL) + else: + self._climate.hvac_mode = lookin_index.index(HVAC_MODE_HEAT) await self._async_update_conditioner() async def async_set_fan_mode(self, fan_mode: str) -> None: From df608b56a5947d64f4cff2d22f5ba970155277a6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 10 Dec 2021 08:40:53 +0100 Subject: [PATCH 0268/2644] Use new enums in energy (#61386) Co-authored-by: epenet --- homeassistant/components/energy/sensor.py | 34 ++++++++++----------- homeassistant/components/energy/validate.py | 29 ++++++++++-------- 2 files changed, 33 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py index 0800b02330e..636c6dc32ae 100644 --- a/homeassistant/components/energy/sensor.py +++ b/homeassistant/components/energy/sensor.py @@ -10,11 +10,9 @@ from typing import Any, Final, Literal, TypeVar, cast from homeassistant.components.sensor import ( ATTR_LAST_RESET, ATTR_STATE_CLASS, - DEVICE_CLASS_MONETARY, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, + SensorStateClass, ) from homeassistant.components.sensor.recorder import reset_detected from homeassistant.const import ( @@ -22,7 +20,6 @@ from homeassistant.const import ( ENERGY_KILO_WATT_HOUR, ENERGY_MEGA_WATT_HOUR, ENERGY_WATT_HOUR, - ENTITY_CATEGORY_SYSTEM, VOLUME_CUBIC_METERS, ) from homeassistant.core import ( @@ -32,6 +29,7 @@ from homeassistant.core import ( split_entity_id, valid_entity_id, ) +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -41,9 +39,9 @@ from .const import DOMAIN from .data import EnergyManager, async_get_manager SUPPORTED_STATE_CLASSES = [ - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL, - STATE_CLASS_TOTAL_INCREASING, + SensorStateClass.MEASUREMENT, + SensorStateClass.TOTAL, + SensorStateClass.TOTAL_INCREASING, ] VALID_ENERGY_UNITS = [ENERGY_WATT_HOUR, ENERGY_KILO_WATT_HOUR, ENERGY_MEGA_WATT_HOUR] VALID_ENERGY_UNITS_GAS = [VOLUME_CUBIC_METERS] + VALID_ENERGY_UNITS @@ -215,7 +213,7 @@ class EnergyCostSensor(SensorEntity): utility. """ - _attr_entity_category = ENTITY_CATEGORY_SYSTEM + _attr_entity_category = EntityCategory.SYSTEM _wrong_state_class_reported = False _wrong_unit_reported = False @@ -231,8 +229,8 @@ class EnergyCostSensor(SensorEntity): self.entity_id = ( f"{config[adapter.entity_energy_key]}_{adapter.entity_id_suffix}" ) - self._attr_device_class = DEVICE_CLASS_MONETARY - self._attr_state_class = STATE_CLASS_TOTAL + self._attr_device_class = SensorDeviceClass.MONETARY + self._attr_state_class = SensorStateClass.TOTAL self._config = config self._last_energy_sensor_state: State | None = None # add_finished is set when either of async_added_to_hass or add_to_platform_abort @@ -267,9 +265,9 @@ class EnergyCostSensor(SensorEntity): ) return - # last_reset must be set if the sensor is STATE_CLASS_MEASUREMENT + # last_reset must be set if the sensor is SensorStateClass.MEASUREMENT if ( - state_class == STATE_CLASS_MEASUREMENT + state_class == SensorStateClass.MEASUREMENT and ATTR_LAST_RESET not in energy_state.attributes ): return @@ -337,14 +335,16 @@ class EnergyCostSensor(SensorEntity): ) return - if state_class != STATE_CLASS_TOTAL_INCREASING and energy_state.attributes.get( - ATTR_LAST_RESET - ) != self._last_energy_sensor_state.attributes.get(ATTR_LAST_RESET): + if ( + state_class != SensorStateClass.TOTAL_INCREASING + and energy_state.attributes.get(ATTR_LAST_RESET) + != self._last_energy_sensor_state.attributes.get(ATTR_LAST_RESET) + ): # Energy meter was reset, reset cost sensor too energy_state_copy = copy.copy(energy_state) energy_state_copy.state = "0.0" self._reset(energy_state_copy) - elif state_class == STATE_CLASS_TOTAL_INCREASING and reset_detected( + elif state_class == SensorStateClass.TOTAL_INCREASING and reset_detected( self.hass, cast(str, self._config[self._adapter.entity_energy_key]), energy, diff --git a/homeassistant/components/energy/validate.py b/homeassistant/components/energy/validate.py index d77ea75d36c..d9cd9a73aa0 100644 --- a/homeassistant/components/energy/validate.py +++ b/homeassistant/components/energy/validate.py @@ -21,19 +21,22 @@ from homeassistant.core import HomeAssistant, callback, valid_entity_id from . import data from .const import DOMAIN -ENERGY_USAGE_DEVICE_CLASSES = (sensor.DEVICE_CLASS_ENERGY,) +ENERGY_USAGE_DEVICE_CLASSES = (sensor.SensorDeviceClass.ENERGY,) ENERGY_USAGE_UNITS = { - sensor.DEVICE_CLASS_ENERGY: (ENERGY_KILO_WATT_HOUR, ENERGY_WATT_HOUR) + sensor.SensorDeviceClass.ENERGY: (ENERGY_KILO_WATT_HOUR, ENERGY_WATT_HOUR) } ENERGY_PRICE_UNITS = tuple( f"/{unit}" for units in ENERGY_USAGE_UNITS.values() for unit in units ) ENERGY_UNIT_ERROR = "entity_unexpected_unit_energy" ENERGY_PRICE_UNIT_ERROR = "entity_unexpected_unit_energy_price" -GAS_USAGE_DEVICE_CLASSES = (sensor.DEVICE_CLASS_ENERGY, sensor.DEVICE_CLASS_GAS) +GAS_USAGE_DEVICE_CLASSES = ( + sensor.SensorDeviceClass.ENERGY, + sensor.SensorDeviceClass.GAS, +) GAS_USAGE_UNITS = { - sensor.DEVICE_CLASS_ENERGY: (ENERGY_WATT_HOUR, ENERGY_KILO_WATT_HOUR), - sensor.DEVICE_CLASS_GAS: (VOLUME_CUBIC_METERS, VOLUME_CUBIC_FEET), + sensor.SensorDeviceClass.ENERGY: (ENERGY_WATT_HOUR, ENERGY_KILO_WATT_HOUR), + sensor.SensorDeviceClass.GAS: (VOLUME_CUBIC_METERS, VOLUME_CUBIC_FEET), } GAS_PRICE_UNITS = tuple( f"/{unit}" for units in GAS_USAGE_UNITS.values() for unit in units @@ -141,9 +144,9 @@ def _async_validate_usage_stat( state_class = state.attributes.get(sensor.ATTR_STATE_CLASS) allowed_state_classes = [ - sensor.STATE_CLASS_MEASUREMENT, - sensor.STATE_CLASS_TOTAL, - sensor.STATE_CLASS_TOTAL_INCREASING, + sensor.SensorStateClass.MEASUREMENT, + sensor.SensorStateClass.TOTAL, + sensor.SensorStateClass.TOTAL_INCREASING, ] if state_class not in allowed_state_classes: result.append( @@ -155,7 +158,7 @@ def _async_validate_usage_stat( ) if ( - state_class == sensor.STATE_CLASS_MEASUREMENT + state_class == sensor.SensorStateClass.MEASUREMENT and sensor.ATTR_LAST_RESET not in state.attributes ): result.append( @@ -221,9 +224,9 @@ def _async_validate_cost_stat( state_class = state.attributes.get("state_class") supported_state_classes = [ - sensor.STATE_CLASS_MEASUREMENT, - sensor.STATE_CLASS_TOTAL, - sensor.STATE_CLASS_TOTAL_INCREASING, + sensor.SensorStateClass.MEASUREMENT, + sensor.SensorStateClass.TOTAL, + sensor.SensorStateClass.TOTAL_INCREASING, ] if state_class not in supported_state_classes: result.append( @@ -231,7 +234,7 @@ def _async_validate_cost_stat( ) if ( - state_class == sensor.STATE_CLASS_MEASUREMENT + state_class == sensor.SensorStateClass.MEASUREMENT and sensor.ATTR_LAST_RESET not in state.attributes ): result.append( From 9c28727aa04553fbc6723bb9fafc3663dd2366dd Mon Sep 17 00:00:00 2001 From: bsmappee <58250533+bsmappee@users.noreply.github.com> Date: Fri, 10 Dec 2021 08:42:33 +0100 Subject: [PATCH 0269/2644] Remove energy entity again in Smappee local integration (#61373) --- homeassistant/components/smappee/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/smappee/sensor.py b/homeassistant/components/smappee/sensor.py index ccae097e53f..595cc4da02d 100644 --- a/homeassistant/components/smappee/sensor.py +++ b/homeassistant/components/smappee/sensor.py @@ -320,7 +320,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ), ) for actuator_id, actuator in service_location.actuators.items() - if actuator.type == "SWITCH" + if actuator.type == "SWITCH" and not service_location.local_polling ] ) From 49a5c7b2cc13777667cd46aee95b904905a61414 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Fri, 10 Dec 2021 08:46:44 +0100 Subject: [PATCH 0270/2644] Use find_coordinates in waze_travel_time (#61400) --- .../waze_travel_time/config_flow.py | 1 - .../components/waze_travel_time/helpers.py | 63 ++----------------- .../components/waze_travel_time/sensor.py | 18 ++---- 3 files changed, 8 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/waze_travel_time/config_flow.py b/homeassistant/components/waze_travel_time/config_flow.py index 54097ad37bd..d040f595fd6 100644 --- a/homeassistant/components/waze_travel_time/config_flow.py +++ b/homeassistant/components/waze_travel_time/config_flow.py @@ -172,7 +172,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): or await self.hass.async_add_executor_job( is_valid_config_entry, self.hass, - _LOGGER, user_input[CONF_ORIGIN], user_input[CONF_DESTINATION], user_input[CONF_REGION], diff --git a/homeassistant/components/waze_travel_time/helpers.py b/homeassistant/components/waze_travel_time/helpers.py index 26e529b8e93..67d8b5674b2 100644 --- a/homeassistant/components/waze_travel_time/helpers.py +++ b/homeassistant/components/waze_travel_time/helpers.py @@ -1,70 +1,15 @@ """Helpers for Waze Travel Time integration.""" -import re - from WazeRouteCalculator import WazeRouteCalculator, WRCError -from homeassistant.components.waze_travel_time.const import ENTITY_ID_PATTERN -from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE -from homeassistant.helpers import location +from homeassistant.helpers.location import find_coordinates -def is_valid_config_entry(hass, logger, origin, destination, region): +def is_valid_config_entry(hass, origin, destination, region): """Return whether the config entry data is valid.""" - origin = resolve_location(hass, logger, origin) - destination = resolve_location(hass, logger, destination) + origin = find_coordinates(hass, origin) + destination = find_coordinates(hass, destination) try: WazeRouteCalculator(origin, destination, region).calc_all_routes_info() except WRCError: return False return True - - -def resolve_location(hass, logger, loc): - """Resolve a location.""" - if re.fullmatch(ENTITY_ID_PATTERN, loc): - return get_location_from_entity(hass, logger, loc) - - return resolve_zone(hass, loc) - - -def get_location_from_entity(hass, logger, entity_id): - """Get the location from the entity_id.""" - if (state := hass.states.get(entity_id)) is None: - logger.error("Unable to find entity %s", entity_id) - return None - - # Check if the entity has location attributes. - if location.has_location(state): - logger.debug("Getting %s location", entity_id) - return _get_location_from_attributes(state) - - # Check if device is inside a zone. - zone_state = hass.states.get(f"zone.{state.state}") - if location.has_location(zone_state): - logger.debug( - "%s is in %s, getting zone location", entity_id, zone_state.entity_id - ) - return _get_location_from_attributes(zone_state) - - # If zone was not found in state then use the state as the location. - if entity_id.startswith("sensor."): - return state.state - - # When everything fails just return nothing. - return None - - -def resolve_zone(hass, friendly_name): - """Get a lat/long from a zones friendly_name.""" - states = hass.states.all() - for state in states: - if state.domain == "zone" and state.name == friendly_name: - return _get_location_from_attributes(state) - - return friendly_name - - -def _get_location_from_attributes(state): - """Get the lat/long string from an states attributes.""" - attr = state.attributes - return f"{attr.get(ATTR_LATITUDE)},{attr.get(ATTR_LONGITUDE)}" diff --git a/homeassistant/components/waze_travel_time/sensor.py b/homeassistant/components/waze_travel_time/sensor.py index 9df95daf9ee..3c6dba586d1 100644 --- a/homeassistant/components/waze_travel_time/sensor.py +++ b/homeassistant/components/waze_travel_time/sensor.py @@ -25,6 +25,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.location import find_coordinates from homeassistant.helpers.typing import DiscoveryInfoType from .const import ( @@ -50,7 +51,6 @@ from .const import ( UNITS, VEHICLE_TYPES, ) -from .helpers import get_location_from_entity, resolve_zone _LOGGER = logging.getLogger(__name__) @@ -237,24 +237,14 @@ class WazeTravelTime(SensorEntity): _LOGGER.debug("Fetching Route for %s", self._attr_name) # Get origin latitude and longitude from entity_id. if self._origin_entity_id is not None: - self._waze_data.origin = get_location_from_entity( - self.hass, _LOGGER, self._origin_entity_id - ) + self._waze_data.origin = find_coordinates(self.hass, self._origin_entity_id) # Get destination latitude and longitude from entity_id. if self._destination_entity_id is not None: - self._waze_data.destination = get_location_from_entity( - self.hass, _LOGGER, self._destination_entity_id + self._waze_data.destination = find_coordinates( + self.hass, self._destination_entity_id ) - # Get origin from zone name. - self._waze_data.origin = resolve_zone(self.hass, self._waze_data.origin) - - # Get destination from zone name. - self._waze_data.destination = resolve_zone( - self.hass, self._waze_data.destination - ) - self._waze_data.update() From f7bdbd9fdd72950f162ae4df493e8ada073c0d03 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 10 Dec 2021 08:53:23 +0100 Subject: [PATCH 0271/2644] Use new SensorDeviceClass in dsmr-reader (#61371) Co-authored-by: epenet --- .../components/dsmr_reader/definitions.py | 92 +++++++++---------- 1 file changed, 45 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/dsmr_reader/definitions.py b/homeassistant/components/dsmr_reader/definitions.py index 259f0880abd..9f4ee7ed918 100644 --- a/homeassistant/components/dsmr_reader/definitions.py +++ b/homeassistant/components/dsmr_reader/definitions.py @@ -5,15 +5,13 @@ from collections.abc import Callable from dataclasses import dataclass from typing import Final -from homeassistant.components.sensor import SensorEntityDescription, SensorStateClass +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntityDescription, + SensorStateClass, +) from homeassistant.const import ( CURRENCY_EURO, - DEVICE_CLASS_CURRENT, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_GAS, - DEVICE_CLASS_POWER, - DEVICE_CLASS_TIMESTAMP, - DEVICE_CLASS_VOLTAGE, ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, @@ -51,42 +49,42 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( DSMRReaderSensorEntityDescription( key="dsmr/reading/electricity_delivered_1", name="Low tariff usage", - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/reading/electricity_returned_1", name="Low tariff returned", - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/reading/electricity_delivered_2", name="High tariff usage", - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/reading/electricity_returned_2", name="High tariff returned", - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/reading/electricity_currently_delivered", name="Current power usage", - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, native_unit_of_measurement=POWER_KILO_WATT, state_class=SensorStateClass.MEASUREMENT, ), DSMRReaderSensorEntityDescription( key="dsmr/reading/electricity_currently_returned", name="Current power return", - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, native_unit_of_measurement=POWER_KILO_WATT, state_class=SensorStateClass.MEASUREMENT, ), @@ -94,7 +92,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( key="dsmr/reading/phase_currently_delivered_l1", name="Current power usage L1", entity_registry_enabled_default=False, - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, native_unit_of_measurement=POWER_KILO_WATT, state_class=SensorStateClass.MEASUREMENT, ), @@ -102,7 +100,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( key="dsmr/reading/phase_currently_delivered_l2", name="Current power usage L2", entity_registry_enabled_default=False, - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, native_unit_of_measurement=POWER_KILO_WATT, state_class=SensorStateClass.MEASUREMENT, ), @@ -110,7 +108,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( key="dsmr/reading/phase_currently_delivered_l3", name="Current power usage L3", entity_registry_enabled_default=False, - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, native_unit_of_measurement=POWER_KILO_WATT, state_class=SensorStateClass.MEASUREMENT, ), @@ -118,7 +116,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( key="dsmr/reading/phase_currently_returned_l1", name="Current power return L1", entity_registry_enabled_default=False, - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, native_unit_of_measurement=POWER_KILO_WATT, state_class=SensorStateClass.MEASUREMENT, ), @@ -126,7 +124,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( key="dsmr/reading/phase_currently_returned_l2", name="Current power return L2", entity_registry_enabled_default=False, - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, native_unit_of_measurement=POWER_KILO_WATT, state_class=SensorStateClass.MEASUREMENT, ), @@ -134,7 +132,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( key="dsmr/reading/phase_currently_returned_l3", name="Current power return L3", entity_registry_enabled_default=False, - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, native_unit_of_measurement=POWER_KILO_WATT, state_class=SensorStateClass.MEASUREMENT, ), @@ -150,7 +148,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( key="dsmr/reading/phase_voltage_l1", name="Current voltage L1", entity_registry_enabled_default=False, - device_class=DEVICE_CLASS_VOLTAGE, + device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, state_class=SensorStateClass.MEASUREMENT, ), @@ -158,7 +156,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( key="dsmr/reading/phase_voltage_l2", name="Current voltage L2", entity_registry_enabled_default=False, - device_class=DEVICE_CLASS_VOLTAGE, + device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, state_class=SensorStateClass.MEASUREMENT, ), @@ -166,7 +164,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( key="dsmr/reading/phase_voltage_l3", name="Current voltage L3", entity_registry_enabled_default=False, - device_class=DEVICE_CLASS_VOLTAGE, + device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, state_class=SensorStateClass.MEASUREMENT, ), @@ -174,7 +172,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( key="dsmr/reading/phase_power_current_l1", name="Phase power current L1", entity_registry_enabled_default=False, - device_class=DEVICE_CLASS_CURRENT, + device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=SensorStateClass.MEASUREMENT, ), @@ -182,7 +180,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( key="dsmr/reading/phase_power_current_l2", name="Phase power current L2", entity_registry_enabled_default=False, - device_class=DEVICE_CLASS_CURRENT, + device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=SensorStateClass.MEASUREMENT, ), @@ -190,7 +188,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( key="dsmr/reading/phase_power_current_l3", name="Phase power current L3", entity_registry_enabled_default=False, - device_class=DEVICE_CLASS_CURRENT, + device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=SensorStateClass.MEASUREMENT, ), @@ -198,20 +196,20 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( key="dsmr/reading/timestamp", name="Telegram timestamp", entity_registry_enabled_default=False, - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, state=dt_util.parse_datetime, ), DSMRReaderSensorEntityDescription( key="dsmr/consumption/gas/delivered", name="Gas usage", - device_class=DEVICE_CLASS_GAS, + device_class=SensorDeviceClass.GAS, native_unit_of_measurement=VOLUME_CUBIC_METERS, state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/consumption/gas/currently_delivered", name="Current gas usage", - device_class=DEVICE_CLASS_GAS, + device_class=SensorDeviceClass.GAS, native_unit_of_measurement=VOLUME_CUBIC_METERS, state_class=SensorStateClass.MEASUREMENT, ), @@ -219,48 +217,48 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( key="dsmr/consumption/gas/read_at", name="Gas meter read", entity_registry_enabled_default=False, - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, state=dt_util.parse_datetime, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity1", name="Low tariff usage", - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity2", name="High tariff usage", - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity1_returned", name="Low tariff return", - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity2_returned", name="High tariff return", - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity_merged", name="Power usage total", - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity_returned_merged", name="Power return total", - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, ), @@ -406,37 +404,37 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( DSMRReaderSensorEntityDescription( key="dsmr/current-month/electricity1", name="Current month low tariff usage", - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), DSMRReaderSensorEntityDescription( key="dsmr/current-month/electricity2", name="Current month high tariff usage", - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), DSMRReaderSensorEntityDescription( key="dsmr/current-month/electricity1_returned", name="Current month low tariff returned", - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), DSMRReaderSensorEntityDescription( key="dsmr/current-month/electricity2_returned", name="Current month high tariff returned", - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), DSMRReaderSensorEntityDescription( key="dsmr/current-month/electricity_merged", name="Current month power usage total", - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), DSMRReaderSensorEntityDescription( key="dsmr/current-month/electricity_returned_merged", name="Current month power return total", - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), DSMRReaderSensorEntityDescription( @@ -484,37 +482,37 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( DSMRReaderSensorEntityDescription( key="dsmr/current-year/electricity1", name="Current year low tariff usage", - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), DSMRReaderSensorEntityDescription( key="dsmr/current-year/electricity2", name="Current year high tariff usage", - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), DSMRReaderSensorEntityDescription( key="dsmr/current-year/electricity1_returned", name="Current year low tariff returned", - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), DSMRReaderSensorEntityDescription( key="dsmr/current-year/electricity2_returned", name="Current year high tariff usage", - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), DSMRReaderSensorEntityDescription( key="dsmr/current-year/electricity_merged", name="Current year power usage total", - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), DSMRReaderSensorEntityDescription( key="dsmr/current-year/electricity_returned_merged", name="Current year power returned total", - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), DSMRReaderSensorEntityDescription( From 728f5116278666a6ba078c824df0a7e1449c928b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 10 Dec 2021 08:55:57 +0100 Subject: [PATCH 0272/2644] Use new DeviceClass and StateClass enums in ecobee (#61372) Co-authored-by: epenet --- .../components/ecobee/binary_sensor.py | 4 ++-- homeassistant/components/ecobee/humidifier.py | 5 ++--- homeassistant/components/ecobee/sensor.py | 18 +++++++----------- 3 files changed, 11 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/ecobee/binary_sensor.py b/homeassistant/components/ecobee/binary_sensor.py index 94c4ade7398..ae0d395d58a 100644 --- a/homeassistant/components/ecobee/binary_sensor.py +++ b/homeassistant/components/ecobee/binary_sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_OCCUPANCY, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.helpers.entity import DeviceInfo @@ -97,7 +97,7 @@ class EcobeeBinarySensor(BinarySensorEntity): @property def device_class(self): """Return the class of this sensor, from DEVICE_CLASSES.""" - return DEVICE_CLASS_OCCUPANCY + return BinarySensorDeviceClass.OCCUPANCY async def async_update(self): """Get the latest state of the sensor.""" diff --git a/homeassistant/components/ecobee/humidifier.py b/homeassistant/components/ecobee/humidifier.py index b660a75363f..a98b3712dd3 100644 --- a/homeassistant/components/ecobee/humidifier.py +++ b/homeassistant/components/ecobee/humidifier.py @@ -3,11 +3,10 @@ from __future__ import annotations from datetime import timedelta -from homeassistant.components.humidifier import HumidifierEntity +from homeassistant.components.humidifier import HumidifierDeviceClass, HumidifierEntity from homeassistant.components.humidifier.const import ( DEFAULT_MAX_HUMIDITY, DEFAULT_MIN_HUMIDITY, - DEVICE_CLASS_HUMIDIFIER, MODE_AUTO, SUPPORT_MODES, ) @@ -97,7 +96,7 @@ class EcobeeHumidifier(HumidifierEntity): @property def device_class(self): """Return the device class type.""" - return DEVICE_CLASS_HUMIDIFIER + return HumidifierDeviceClass.HUMIDIFIER @property def is_on(self): diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index cfdf861e7bc..690a0110477 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -4,16 +4,12 @@ from __future__ import annotations from pyecobee.const import ECOBEE_STATE_CALIBRATING, ECOBEE_STATE_UNKNOWN from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) -from homeassistant.const import ( - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_TEMPERATURE, - PERCENTAGE, - TEMP_FAHRENHEIT, -) +from homeassistant.const import PERCENTAGE, TEMP_FAHRENHEIT from homeassistant.helpers.entity import DeviceInfo from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER @@ -23,15 +19,15 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="temperature", name="Temperature", native_unit_of_measurement=TEMP_FAHRENHEIT, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="humidity", name="Humidity", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), ) From f59966f8ee1a3a5e5680bb7467bd9954866e8a5d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 10 Dec 2021 08:57:31 +0100 Subject: [PATCH 0273/2644] Use new DeviceClass enums in econet (#61375) Co-authored-by: epenet --- homeassistant/components/econet/binary_sensor.py | 13 +++++-------- homeassistant/components/econet/sensor.py | 11 +++-------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/econet/binary_sensor.py b/homeassistant/components/econet/binary_sensor.py index cb7945e0815..6cee3383d23 100644 --- a/homeassistant/components/econet/binary_sensor.py +++ b/homeassistant/components/econet/binary_sensor.py @@ -4,10 +4,7 @@ from __future__ import annotations from pyeconet.equipment import EquipmentType from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_LOCK, - DEVICE_CLASS_OPENING, - DEVICE_CLASS_POWER, - DEVICE_CLASS_SOUND, + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -19,22 +16,22 @@ BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( BinarySensorEntityDescription( key="shutoff_valve_open", name="shutoff_valve", - device_class=DEVICE_CLASS_OPENING, + device_class=BinarySensorDeviceClass.OPENING, ), BinarySensorEntityDescription( key="running", name="running", - device_class=DEVICE_CLASS_POWER, + device_class=BinarySensorDeviceClass.POWER, ), BinarySensorEntityDescription( key="screen_locked", name="screen_locked", - device_class=DEVICE_CLASS_LOCK, + device_class=BinarySensorDeviceClass.LOCK, ), BinarySensorEntityDescription( key="beep_enabled", name="beep_enabled", - device_class=DEVICE_CLASS_SOUND, + device_class=BinarySensorDeviceClass.SOUND, ), ) diff --git a/homeassistant/components/econet/sensor.py b/homeassistant/components/econet/sensor.py index bbcf54003e8..f4c37841b17 100644 --- a/homeassistant/components/econet/sensor.py +++ b/homeassistant/components/econet/sensor.py @@ -1,13 +1,8 @@ """Support for Rheem EcoNet water heaters.""" from pyeconet.equipment import EquipmentType -from homeassistant.components.sensor import SensorEntity -from homeassistant.const import ( - DEVICE_CLASS_SIGNAL_STRENGTH, - ENERGY_KILO_WATT_HOUR, - PERCENTAGE, - VOLUME_GALLONS, -) +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.const import ENERGY_KILO_WATT_HOUR, PERCENTAGE, VOLUME_GALLONS from . import EcoNetEntity from .const import DOMAIN, EQUIPMENT @@ -44,7 +39,7 @@ SENSOR_NAMES_TO_UNIT_OF_MEASUREMENT = { WATER_USAGE_TODAY: VOLUME_GALLONS, POWER_USAGE_TODAY: None, # Depends on unit type ALERT_COUNT: None, - WIFI_SIGNAL: DEVICE_CLASS_SIGNAL_STRENGTH, + WIFI_SIGNAL: SensorDeviceClass.SIGNAL_STRENGTH, RUNNING_STATE: None, # This is just a string } From 347d1fc6bd3fdbcf3446f9010925d51f45a5d9ce Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 10 Dec 2021 08:57:55 +0100 Subject: [PATCH 0274/2644] Use new SensorDeviceClass enum in eddystone_temperature (#61376) Co-authored-by: epenet --- homeassistant/components/eddystone_temperature/sensor.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/eddystone_temperature/sensor.py b/homeassistant/components/eddystone_temperature/sensor.py index 1eee0b47272..66b0ce6731e 100644 --- a/homeassistant/components/eddystone_temperature/sensor.py +++ b/homeassistant/components/eddystone_temperature/sensor.py @@ -10,10 +10,13 @@ import logging from beacontools import BeaconScanner, EddystoneFilter, EddystoneTLMFrame import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorDeviceClass, + SensorEntity, +) from homeassistant.const import ( CONF_NAME, - DEVICE_CLASS_TEMPERATURE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN, @@ -121,7 +124,7 @@ class EddystoneTemp(SensorEntity): @property def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" - return DEVICE_CLASS_TEMPERATURE + return SensorDeviceClass.TEMPERATURE @property def native_unit_of_measurement(self): From 393107c855dbe6f596426859bf9696410b18c727 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 10 Dec 2021 08:59:24 +0100 Subject: [PATCH 0275/2644] Use new DeviceClass and StateClass enums in efergy (#61377) Co-authored-by: epenet --- homeassistant/components/efergy/sensor.py | 47 +++++++++++------------ 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/efergy/sensor.py b/homeassistant/components/efergy/sensor.py index 1f0db12f449..e2d71716f24 100644 --- a/homeassistant/components/efergy/sensor.py +++ b/homeassistant/components/efergy/sensor.py @@ -10,19 +10,16 @@ import voluptuous as vol from homeassistant.components.efergy import EfergyEntity from homeassistant.components.sensor import ( PLATFORM_SCHEMA, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_CURRENCY, CONF_MONITORED_VARIABLES, CONF_TYPE, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_MONETARY, - DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR, POWER_WATT, ) @@ -40,39 +37,39 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="instant_readings", name="Power Usage", - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, native_unit_of_measurement=POWER_WATT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="energy_day", name="Daily Consumption", - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, entity_registry_enabled_default=False, ), SensorEntityDescription( key="energy_week", name="Weekly Consumption", - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, entity_registry_enabled_default=False, ), SensorEntityDescription( key="energy_month", name="Monthly Consumption", - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="energy_year", name="Yearly Consumption", - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, entity_registry_enabled_default=False, ), SensorEntityDescription( @@ -83,36 +80,36 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="cost_day", name="Daily Energy Cost", - device_class=DEVICE_CLASS_MONETARY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.MONETARY, + state_class=SensorStateClass.TOTAL_INCREASING, entity_registry_enabled_default=False, ), SensorEntityDescription( key="cost_week", name="Weekly Energy Cost", - device_class=DEVICE_CLASS_MONETARY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.MONETARY, + state_class=SensorStateClass.TOTAL_INCREASING, entity_registry_enabled_default=False, ), SensorEntityDescription( key="cost_month", name="Monthly Energy Cost", - device_class=DEVICE_CLASS_MONETARY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.MONETARY, + state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="cost_year", name="Yearly Energy Cost", - device_class=DEVICE_CLASS_MONETARY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.MONETARY, + state_class=SensorStateClass.TOTAL_INCREASING, entity_registry_enabled_default=False, ), SensorEntityDescription( key=CONF_CURRENT_VALUES, name="Power Usage", - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, native_unit_of_measurement=POWER_WATT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), ) From 9f9e2db5105eab1f46590a6b8d6a5b5eff4ccb51 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 10 Dec 2021 09:01:58 +0100 Subject: [PATCH 0276/2644] Use new BinarySensorDeviceClass enum in egardia (#61378) Co-authored-by: epenet --- homeassistant/components/egardia/binary_sensor.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/egardia/binary_sensor.py b/homeassistant/components/egardia/binary_sensor.py index 61f43301fd9..cf621277faf 100644 --- a/homeassistant/components/egardia/binary_sensor.py +++ b/homeassistant/components/egardia/binary_sensor.py @@ -1,7 +1,6 @@ """Interfaces with Egardia/Woonveilig alarm control panel.""" from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_MOTION, - DEVICE_CLASS_OPENING, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.const import STATE_OFF, STATE_ON @@ -9,9 +8,9 @@ from homeassistant.const import STATE_OFF, STATE_ON from . import ATTR_DISCOVER_DEVICES, EGARDIA_DEVICE EGARDIA_TYPE_TO_DEVICE_CLASS = { - "IR Sensor": DEVICE_CLASS_MOTION, - "Door Contact": DEVICE_CLASS_OPENING, - "IR": DEVICE_CLASS_MOTION, + "IR Sensor": BinarySensorDeviceClass.MOTION, + "Door Contact": BinarySensorDeviceClass.OPENING, + "IR": BinarySensorDeviceClass.MOTION, } From 5559c751a9eb08ef31c7f25d3034165b65fb81f1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 10 Dec 2021 09:02:21 +0100 Subject: [PATCH 0277/2644] Use new EntityCategory enum in elgato (#61379) Co-authored-by: epenet --- homeassistant/components/elgato/button.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/elgato/button.py b/homeassistant/components/elgato/button.py index 4e77f05e415..5e53ecfad40 100644 --- a/homeassistant/components/elgato/button.py +++ b/homeassistant/components/elgato/button.py @@ -7,9 +7,8 @@ from elgato import Elgato, ElgatoError, Info from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN @@ -39,7 +38,7 @@ class ElgatoIdentifyButton(ButtonEntity): key="identify", name="Identify", icon="mdi:help", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ) self._attr_unique_id = f"{info.serial_number}_{self.entity_description.key}" From b5cd13a13405beabb54e40361662e53a3c9a738e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 10 Dec 2021 09:04:07 +0100 Subject: [PATCH 0278/2644] Use new SensorStateClass in eliqonline (#61380) Co-authored-by: epenet --- homeassistant/components/eliqonline/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/eliqonline/sensor.py b/homeassistant/components/eliqonline/sensor.py index ecd6e4ad4bb..0ddb123d5f8 100644 --- a/homeassistant/components/eliqonline/sensor.py +++ b/homeassistant/components/eliqonline/sensor.py @@ -8,8 +8,8 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, - STATE_CLASS_MEASUREMENT, SensorEntity, + SensorStateClass, ) from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, POWER_WATT from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -58,7 +58,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class EliqSensor(SensorEntity): """Implementation of an ELIQ Online sensor.""" - _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_state_class = SensorStateClass.MEASUREMENT def __init__(self, api, channel_id, name): """Initialize the sensor.""" From 46326a47de5f00faa7449d9913b64f4c76c530ba Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 10 Dec 2021 09:06:23 +0100 Subject: [PATCH 0279/2644] Use new SensorDeviceClass enum in ebusd (#61374) Co-authored-by: epenet --- homeassistant/components/ebusd/const.py | 58 +++++++++++++++---------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/ebusd/const.py b/homeassistant/components/ebusd/const.py index 3d4ab508ca2..ac5ca313f2f 100644 --- a/homeassistant/components/ebusd/const.py +++ b/homeassistant/components/ebusd/const.py @@ -1,6 +1,6 @@ """Constants for ebus component.""" +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( - DEVICE_CLASS_TEMPERATURE, ENERGY_KILO_WATT_HOUR, PERCENTAGE, PRESSURE_BAR, @@ -20,21 +20,21 @@ SENSOR_TYPES = { TEMP_CELSIUS, None, 0, - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, ], "MaxFlowTemperatureDesired": [ "Hc1MaxFlowTempDesired", TEMP_CELSIUS, None, 0, - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, ], "MinFlowTemperatureDesired": [ "Hc1MinFlowTempDesired", TEMP_CELSIUS, None, 0, - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, ], "PumpStatus": ["Hc1PumpStatus", None, "mdi:toggle-switch", 2, None], "HCSummerTemperatureLimit": [ @@ -42,28 +42,28 @@ SENSOR_TYPES = { TEMP_CELSIUS, "mdi:weather-sunny", 0, - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, ], "HolidayTemperature": [ "HolidayTemp", TEMP_CELSIUS, None, 0, - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, ], "HWTemperatureDesired": [ "HwcTempDesired", TEMP_CELSIUS, None, 0, - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, ], "HWActualTemperature": [ "HwcStorageTemp", TEMP_CELSIUS, None, 0, - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, ], "HWTimerMonday": ["hwcTimer.Monday", None, "mdi:timer-outline", 1, None], "HWTimerTuesday": ["hwcTimer.Tuesday", None, "mdi:timer-outline", 1, None], @@ -80,35 +80,35 @@ SENSOR_TYPES = { TEMP_CELSIUS, "mdi:weather-night", 0, - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, ], "Zone1DayTemperature": [ "z1DayTemp", TEMP_CELSIUS, "mdi:weather-sunny", 0, - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, ], "Zone1HolidayTemperature": [ "z1HolidayTemp", TEMP_CELSIUS, None, 0, - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, ], "Zone1RoomTemperature": [ "z1RoomTemp", TEMP_CELSIUS, None, 0, - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, ], "Zone1ActualRoomTemperatureDesired": [ "z1ActualRoomTempDesired", TEMP_CELSIUS, None, 0, - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, ], "Zone1TimerMonday": ["z1Timer.Monday", None, "mdi:timer-outline", 1, None], "Zone1TimerTuesday": ["z1Timer.Tuesday", None, "mdi:timer-outline", 1, None], @@ -129,7 +129,7 @@ SENSOR_TYPES = { TEMP_CELSIUS, "mdi:weather-snowy", 0, - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, ], "PowerEnergyConsumptionLastMonth": [ "PrEnergySumHcLastMonth", @@ -147,8 +147,20 @@ SENSOR_TYPES = { ], }, "ehp": { - "HWTemperature": ["HwcTemp", TEMP_CELSIUS, None, 4, DEVICE_CLASS_TEMPERATURE], - "OutsideTemp": ["OutsideTemp", TEMP_CELSIUS, None, 4, DEVICE_CLASS_TEMPERATURE], + "HWTemperature": [ + "HwcTemp", + TEMP_CELSIUS, + None, + 4, + SensorDeviceClass.TEMPERATURE, + ], + "OutsideTemp": [ + "OutsideTemp", + TEMP_CELSIUS, + None, + 4, + SensorDeviceClass.TEMPERATURE, + ], }, "bai": { "HotWaterTemperature": [ @@ -156,28 +168,28 @@ SENSOR_TYPES = { TEMP_CELSIUS, None, 4, - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, ], "StorageTemperature": [ "StorageTemp", TEMP_CELSIUS, None, 4, - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, ], "DesiredStorageTemperature": [ "StorageTempDesired", TEMP_CELSIUS, None, 0, - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, ], "OutdoorsTemperature": [ "OutdoorstempSensor", TEMP_CELSIUS, None, 4, - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, ], "WaterPreasure": ["WaterPressure", PRESSURE_BAR, "mdi:pipe", 4, None], "AverageIgnitionTime": [ @@ -206,7 +218,7 @@ SENSOR_TYPES = { TEMP_CELSIUS, None, 4, - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, ], "CentralHeatingPump": ["WP", None, "mdi:toggle-switch", 2, None], "HeatingSwitch": ["HeatingSwitch", None, "mdi:toggle-switch", 2, None], @@ -215,14 +227,14 @@ SENSOR_TYPES = { TEMP_CELSIUS, None, 0, - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, ], "FlowTemperature": [ "FlowTemp", TEMP_CELSIUS, None, 4, - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, ], "Flame": ["Flame", None, "mdi:toggle-switch", 2, None], "PowerEnergyConsumptionHeatingCircuit": [ From 980f22244c270a40cda50381c587f786abf36bdb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 10 Dec 2021 09:07:17 +0100 Subject: [PATCH 0280/2644] Use new DeviceClass and StateClass enums in emoncms (#61381) Co-authored-by: epenet --- homeassistant/components/emoncms/sensor.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/emoncms/sensor.py b/homeassistant/components/emoncms/sensor.py index 68a28cf2846..2db0f0373c9 100644 --- a/homeassistant/components/emoncms/sensor.py +++ b/homeassistant/components/emoncms/sensor.py @@ -8,9 +8,9 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, + SensorStateClass, ) from homeassistant.const import ( CONF_API_KEY, @@ -19,8 +19,6 @@ from homeassistant.const import ( CONF_UNIT_OF_MEASUREMENT, CONF_URL, CONF_VALUE_TEMPLATE, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, POWER_WATT, STATE_UNKNOWN, ) @@ -156,11 +154,11 @@ class EmonCmsSensor(SensorEntity): self._elem = elem if unit_of_measurement == "kWh": - self._attr_device_class = DEVICE_CLASS_ENERGY - self._attr_state_class = STATE_CLASS_TOTAL_INCREASING + self._attr_device_class = SensorDeviceClass.ENERGY + self._attr_state_class = SensorStateClass.TOTAL_INCREASING elif unit_of_measurement == "W": - self._attr_device_class = DEVICE_CLASS_POWER - self._attr_state_class = STATE_CLASS_MEASUREMENT + self._attr_device_class = SensorDeviceClass.POWER + self._attr_state_class = SensorStateClass.MEASUREMENT if self._value_template is not None: self._state = self._value_template.render_with_possible_json_value( From 63efb809eefffa5b6acf9deae379d3892d525373 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 10 Dec 2021 09:10:36 +0100 Subject: [PATCH 0281/2644] Use new DeviceClass and StateClass enums in enocean (#61387) Co-authored-by: epenet --- homeassistant/components/enocean/sensor.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/enocean/sensor.py b/homeassistant/components/enocean/sensor.py index 9c4c9df73e9..657c4ed4ecd 100644 --- a/homeassistant/components/enocean/sensor.py +++ b/homeassistant/components/enocean/sensor.py @@ -5,17 +5,15 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, - STATE_CLASS_MEASUREMENT, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_ID, CONF_NAME, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_TEMPERATURE, PERCENTAGE, POWER_WATT, STATE_CLOSED, @@ -44,8 +42,8 @@ SENSOR_DESC_TEMPERATURE = SensorEntityDescription( name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, icon="mdi:thermometer", - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ) SENSOR_DESC_HUMIDITY = SensorEntityDescription( @@ -53,8 +51,8 @@ SENSOR_DESC_HUMIDITY = SensorEntityDescription( name="Humidity", native_unit_of_measurement=PERCENTAGE, icon="mdi:water-percent", - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ) SENSOR_DESC_POWER = SensorEntityDescription( @@ -62,8 +60,8 @@ SENSOR_DESC_POWER = SensorEntityDescription( name="Power", native_unit_of_measurement=POWER_WATT, icon="mdi:power-plug", - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ) SENSOR_DESC_WINDOWHANDLE = SensorEntityDescription( From d143aa06e390ba84af0e20a89e01d7c1ec99f0b0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 10 Dec 2021 09:12:24 +0100 Subject: [PATCH 0282/2644] Correct device class for Tasmota dewpoint sensor (#61420) --- homeassistant/components/tasmota/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index 45ff93b5945..961a89cfb31 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -81,6 +81,7 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { STATE_CLASS: SensorStateClass.MEASUREMENT, }, hc.SENSOR_DEWPOINT: { + DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ICON: "mdi:weather-rainy", STATE_CLASS: SensorStateClass.MEASUREMENT, }, From 45425f118cb2e91084b81444659b9be245f1a096 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 10 Dec 2021 09:12:38 +0100 Subject: [PATCH 0283/2644] Use new DeviceClass and StateClass in enphase_envoy (#61388) Co-authored-by: epenet --- .../components/enphase_envoy/const.py | 41 ++++++++----------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/enphase_envoy/const.py b/homeassistant/components/enphase_envoy/const.py index 7e278d83b86..747a4886f15 100644 --- a/homeassistant/components/enphase_envoy/const.py +++ b/homeassistant/components/enphase_envoy/const.py @@ -2,16 +2,11 @@ from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntityDescription, + SensorStateClass, ) -from homeassistant.const import ( - DEVICE_CLASS_ENERGY, - ENERGY_WATT_HOUR, - POWER_WATT, - Platform, -) +from homeassistant.const import ENERGY_WATT_HOUR, POWER_WATT, Platform DOMAIN = "enphase_envoy" @@ -26,60 +21,60 @@ SENSORS = ( key="production", name="Current Power Production", native_unit_of_measurement=POWER_WATT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="daily_production", name="Today's Energy Production", native_unit_of_measurement=ENERGY_WATT_HOUR, - state_class=STATE_CLASS_TOTAL_INCREASING, - device_class=DEVICE_CLASS_ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, ), SensorEntityDescription( key="seven_days_production", name="Last Seven Days Energy Production", native_unit_of_measurement=ENERGY_WATT_HOUR, - state_class=STATE_CLASS_MEASUREMENT, - device_class=DEVICE_CLASS_ENERGY, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.ENERGY, ), SensorEntityDescription( key="lifetime_production", name="Lifetime Energy Production", native_unit_of_measurement=ENERGY_WATT_HOUR, - state_class=STATE_CLASS_TOTAL_INCREASING, - device_class=DEVICE_CLASS_ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, ), SensorEntityDescription( key="consumption", name="Current Power Consumption", native_unit_of_measurement=POWER_WATT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="daily_consumption", name="Today's Energy Consumption", native_unit_of_measurement=ENERGY_WATT_HOUR, - state_class=STATE_CLASS_MEASUREMENT, - device_class=DEVICE_CLASS_ENERGY, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.ENERGY, ), SensorEntityDescription( key="seven_days_consumption", name="Last Seven Days Energy Consumption", native_unit_of_measurement=ENERGY_WATT_HOUR, - state_class=STATE_CLASS_MEASUREMENT, - device_class=DEVICE_CLASS_ENERGY, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.ENERGY, ), SensorEntityDescription( key="lifetime_consumption", name="Lifetime Energy Consumption", native_unit_of_measurement=ENERGY_WATT_HOUR, - state_class=STATE_CLASS_TOTAL_INCREASING, - device_class=DEVICE_CLASS_ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, ), SensorEntityDescription( key="inverters", name="Inverter", native_unit_of_measurement=POWER_WATT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), ) From f12261b575ec3f0eef71c78217560543136fda03 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 10 Dec 2021 09:13:07 +0100 Subject: [PATCH 0284/2644] Use SensorDeviceClass in envirophat (#61389) Co-authored-by: epenet --- homeassistant/components/envirophat/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/envirophat/sensor.py b/homeassistant/components/envirophat/sensor.py index cff3e95f355..ac51d7310ca 100644 --- a/homeassistant/components/envirophat/sensor.py +++ b/homeassistant/components/envirophat/sensor.py @@ -9,13 +9,13 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, + SensorDeviceClass, SensorEntity, SensorEntityDescription, ) from homeassistant.const import ( CONF_DISPLAY_OPTIONS, CONF_NAME, - DEVICE_CLASS_TEMPERATURE, ELECTRIC_POTENTIAL_VOLT, PRESSURE_HPA, TEMP_CELSIUS, @@ -88,7 +88,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="temperature", name="temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( key="pressure", From 8c39eade5e46bfdea9a28372fa53a4865c62bea0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 10 Dec 2021 09:14:06 +0100 Subject: [PATCH 0285/2644] Use SensorDeviceClass in environment_canada (#61390) Co-authored-by: epenet --- homeassistant/components/environment_canada/sensor.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py index 0d33a166254..e71411913c9 100644 --- a/homeassistant/components/environment_canada/sensor.py +++ b/homeassistant/components/environment_canada/sensor.py @@ -4,12 +4,15 @@ import re import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorDeviceClass, + SensorEntity, +) from homeassistant.const import ( ATTR_LOCATION, CONF_LATITUDE, CONF_LONGITUDE, - DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, ) import homeassistant.helpers.config_validation as cv @@ -135,7 +138,7 @@ class ECSensor(CoordinatorEntity, SensorEntity): "humidex", ): self._unit = TEMP_CELSIUS - self._device_class = DEVICE_CLASS_TEMPERATURE + self._device_class = SensorDeviceClass.TEMPERATURE else: self._unit = sensor_data.get("unit") From dd6b179549860060ec40b4c8d76ebe0df843ca56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Fri, 10 Dec 2021 09:24:59 +0100 Subject: [PATCH 0286/2644] Mill, use native_value (#61382) --- homeassistant/components/mill/sensor.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/mill/sensor.py b/homeassistant/components/mill/sensor.py index 2a1d14d159e..dd2aedabf28 100644 --- a/homeassistant/components/mill/sensor.py +++ b/homeassistant/components/mill/sensor.py @@ -202,14 +202,8 @@ class LocalMillSensor(CoordinatorEntity, SensorEntity): self._attr_name = ( f"{coordinator.mill_data_connection.name} {entity_description.name}" ) - self._update_attr() - @callback - def _handle_coordinator_update(self) -> None: - """Handle updated data from the coordinator.""" - self._update_attr() - self.async_write_ha_state() - - @callback - def _update_attr(self) -> None: - self._attr_native_value = self.coordinator.data[self.entity_description.key] + @property + def native_value(self): + """Return the native value of the sensor.""" + return self.coordinator.data[self.entity_description.key] From 412e5310966c814012b7fafc3886ad513e399a83 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 10 Dec 2021 09:54:58 +0100 Subject: [PATCH 0287/2644] Use new DeviceClass enums in ezviz (#61383) Co-authored-by: epenet --- homeassistant/components/ezviz/binary_sensor.py | 7 +++---- homeassistant/components/ezviz/sensor.py | 16 ++++++++-------- homeassistant/components/ezviz/switch.py | 4 ++-- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/ezviz/binary_sensor.py b/homeassistant/components/ezviz/binary_sensor.py index e7d8be80509..942ceeecdb2 100644 --- a/homeassistant/components/ezviz/binary_sensor.py +++ b/homeassistant/components/ezviz/binary_sensor.py @@ -2,8 +2,7 @@ from __future__ import annotations from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_MOTION, - DEVICE_CLASS_UPDATE, + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -20,7 +19,7 @@ PARALLEL_UPDATES = 1 BINARY_SENSOR_TYPES: dict[str, BinarySensorEntityDescription] = { "Motion_Trigger": BinarySensorEntityDescription( key="Motion_Trigger", - device_class=DEVICE_CLASS_MOTION, + device_class=BinarySensorDeviceClass.MOTION, ), "alarm_schedules_enabled": BinarySensorEntityDescription( key="alarm_schedules_enabled" @@ -28,7 +27,7 @@ BINARY_SENSOR_TYPES: dict[str, BinarySensorEntityDescription] = { "encrypted": BinarySensorEntityDescription(key="encrypted"), "upgrade_available": BinarySensorEntityDescription( key="upgrade_available", - device_class=DEVICE_CLASS_UPDATE, + device_class=BinarySensorDeviceClass.UPDATE, ), } diff --git a/homeassistant/components/ezviz/sensor.py b/homeassistant/components/ezviz/sensor.py index 5197982a2c5..a7334a3d18b 100644 --- a/homeassistant/components/ezviz/sensor.py +++ b/homeassistant/components/ezviz/sensor.py @@ -1,10 +1,13 @@ """Support for Ezviz sensors.""" from __future__ import annotations -from homeassistant.components.binary_sensor import DEVICE_CLASS_MOTION -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE +from homeassistant.const import PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -19,7 +22,7 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { "battery_level": SensorEntityDescription( key="battery_level", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_BATTERY, + device_class=SensorDeviceClass.BATTERY, ), "alarm_sound_mod": SensorEntityDescription(key="alarm_sound_mod"), "detection_sensibility": SensorEntityDescription(key="detection_sensibility"), @@ -32,10 +35,7 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { "supported_channels": SensorEntityDescription(key="supported_channels"), "local_ip": SensorEntityDescription(key="local_ip"), "wan_ip": SensorEntityDescription(key="wan_ip"), - "PIR_Status": SensorEntityDescription( - key="PIR_Status", - device_class=DEVICE_CLASS_MOTION, - ), + "PIR_Status": SensorEntityDescription(key="PIR_Status"), "last_alarm_type_code": SensorEntityDescription(key="last_alarm_type_code"), "last_alarm_type_name": SensorEntityDescription(key="last_alarm_type_name"), } diff --git a/homeassistant/components/ezviz/switch.py b/homeassistant/components/ezviz/switch.py index 0324d508f7f..ea8f1e83f70 100644 --- a/homeassistant/components/ezviz/switch.py +++ b/homeassistant/components/ezviz/switch.py @@ -6,7 +6,7 @@ from typing import Any from pyezviz.constants import DeviceSwitchType from pyezviz.exceptions import HTTPError, PyEzvizError -from homeassistant.components.switch import DEVICE_CLASS_SWITCH, SwitchEntity +from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -40,7 +40,7 @@ class EzvizSwitch(EzvizEntity, SwitchEntity): """Representation of a Ezviz sensor.""" coordinator: EzvizDataUpdateCoordinator - ATTR_DEVICE_CLASS = DEVICE_CLASS_SWITCH + _attr_device_class = SwitchDeviceClass.SWITCH def __init__( self, coordinator: EzvizDataUpdateCoordinator, serial: str, switch: str From e50c00ea06b3784ff71f02f705c3530d9ad452d8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 10 Dec 2021 09:56:20 +0100 Subject: [PATCH 0288/2644] Use new enums in esphome (#61391) Co-authored-by: epenet --- homeassistant/components/esphome/__init__.py | 18 +++++++------- homeassistant/components/esphome/sensor.py | 25 ++++++++++---------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 8e7f2524a35..4e2e7c02aaa 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -13,7 +13,7 @@ from aioesphomeapi import ( APIIntEnum, APIVersion, DeviceInfo as EsphomeDeviceInfo, - EntityCategory, + EntityCategory as EsphomeEntityCategory, EntityInfo, EntityState, HomeassistantServiceCall, @@ -33,8 +33,6 @@ from homeassistant.const import ( CONF_MODE, CONF_PASSWORD, CONF_PORT, - ENTITY_CATEGORY_CONFIG, - ENTITY_CATEGORY_DIAGNOSTIC, EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import Event, HomeAssistant, ServiceCall, State, callback @@ -43,7 +41,7 @@ from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv import homeassistant.helpers.device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import DeviceInfo, Entity, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.json import JSONEncoder @@ -647,11 +645,13 @@ class EsphomeEnumMapper(Generic[_EnumT, _ValT]): ICON_SCHEMA = vol.Schema(cv.icon) -ENTITY_CATEGORIES: EsphomeEnumMapper[EntityCategory, str | None] = EsphomeEnumMapper( +ENTITY_CATEGORIES: EsphomeEnumMapper[ + EsphomeEntityCategory, EntityCategory | None +] = EsphomeEnumMapper( { - EntityCategory.NONE: None, - EntityCategory.CONFIG: ENTITY_CATEGORY_CONFIG, - EntityCategory.DIAGNOSTIC: ENTITY_CATEGORY_DIAGNOSTIC, + EsphomeEntityCategory.NONE: None, + EsphomeEntityCategory.CONFIG: EntityCategory.CONFIG, + EsphomeEntityCategory.DIAGNOSTIC: EntityCategory.DIAGNOSTIC, } ) @@ -799,7 +799,7 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): return not self._static_info.disabled_by_default @property - def entity_category(self) -> str | None: + def entity_category(self) -> EntityCategory | None: """Return the category of the entity, if any.""" if not self._static_info.entity_category: return None diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index c3f9eff6060..45a73a5e5af 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -7,18 +7,17 @@ import math from aioesphomeapi import ( SensorInfo, SensorState, - SensorStateClass, + SensorStateClass as EsphomeSensorStateClass, TextSensorInfo, TextSensorState, ) from aioesphomeapi.model import LastResetType from homeassistant.components.sensor import ( - DEVICE_CLASS_TIMESTAMP, DEVICE_CLASSES, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -61,11 +60,13 @@ async def async_setup_entry( # pylint: disable=invalid-overridden-method -_STATE_CLASSES: EsphomeEnumMapper[SensorStateClass, str | None] = EsphomeEnumMapper( +_STATE_CLASSES: EsphomeEnumMapper[ + EsphomeSensorStateClass, SensorStateClass | None +] = EsphomeEnumMapper( { - SensorStateClass.NONE: None, - SensorStateClass.MEASUREMENT: STATE_CLASS_MEASUREMENT, - SensorStateClass.TOTAL_INCREASING: STATE_CLASS_TOTAL_INCREASING, + EsphomeSensorStateClass.NONE: None, + EsphomeSensorStateClass.MEASUREMENT: SensorStateClass.MEASUREMENT, + EsphomeSensorStateClass.TOTAL_INCREASING: SensorStateClass.TOTAL_INCREASING, } ) @@ -85,7 +86,7 @@ class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity): return None if self._state.missing_state: return None - if self.device_class == DEVICE_CLASS_TIMESTAMP: + if self.device_class == SensorDeviceClass.TIMESTAMP: return dt.utc_from_timestamp(self._state.state) return f"{self._state.state:.{self._static_info.accuracy_decimals}f}" @@ -104,18 +105,18 @@ class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity): return self._static_info.device_class @property - def state_class(self) -> str | None: + def state_class(self) -> SensorStateClass | None: """Return the state class of this entity.""" if not self._static_info.state_class: return None state_class = self._static_info.state_class reset_type = self._static_info.last_reset_type if ( - state_class == SensorStateClass.MEASUREMENT + state_class == EsphomeSensorStateClass.MEASUREMENT and reset_type == LastResetType.AUTO ): # Legacy, last_reset_type auto was the equivalent to the TOTAL_INCREASING state class - return STATE_CLASS_TOTAL_INCREASING + return SensorStateClass.TOTAL_INCREASING return _STATE_CLASSES.from_esphome(self._static_info.state_class) From b5c55280820901086e90eca10d726de9ea231faa Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 10 Dec 2021 10:13:21 +0100 Subject: [PATCH 0289/2644] Optimise state attributes in delijn (#61424) --- homeassistant/components/delijn/sensor.py | 25 +++++++++++++---------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/delijn/sensor.py b/homeassistant/components/delijn/sensor.py index 6db89e95675..0abb94e6def 100644 --- a/homeassistant/components/delijn/sensor.py +++ b/homeassistant/components/delijn/sensor.py @@ -36,6 +36,15 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) +AUTO_ATTRIBUTES = ( + "line_number_public", + "line_transport_type", + "final_destination", + "due_at_schedule", + "due_at_realtime", + "is_realtime", +) + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Create the sensor.""" @@ -94,17 +103,11 @@ class DeLijnPublicTransportSensor(SensorEntity): if (first_passage := first["due_at_realtime"]) is None: first_passage = first["due_at_schedule"] self._attr_native_value = first_passage - self._attr_extra_state_attributes.update( - { - "line_number_public": first["line_number_public"], - "line_transport_type": first["line_transport_type"], - "final_destination": first["final_destination"], - "due_at_schedule": first["due_at_schedule"], - "due_at_realtime": first["due_at_realtime"], - "is_realtime": first["is_realtime"], - "next_passages": self.line.passages, - } - ) + + for key in AUTO_ATTRIBUTES: + self._attr_extra_state_attributes[key] = first[key] + self._attr_extra_state_attributes["next_passages"] = self.line.passages + self._attr_available = True except (KeyError) as error: _LOGGER.error("Invalid data received from De Lijn: %s", error) From 281b5e1c228b89915456164611409571542004e5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 10 Dec 2021 11:13:44 +0100 Subject: [PATCH 0290/2644] Speed up demo lock tests (#61425) --- homeassistant/components/demo/lock.py | 6 ++- tests/components/demo/test_lock.py | 54 ++++++++++++++++----------- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/demo/lock.py b/homeassistant/components/demo/lock.py index af61c0f6111..c46e9f5eebe 100644 --- a/homeassistant/components/demo/lock.py +++ b/homeassistant/components/demo/lock.py @@ -10,6 +10,8 @@ from homeassistant.const import ( STATE_UNLOCKING, ) +LOCK_UNLOCK_DELAY = 2 # Used to give a realistic lock/unlock experience in frontend + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Demo lock platform.""" @@ -72,7 +74,7 @@ class DemoLock(LockEntity): """Lock the device.""" self._state = STATE_LOCKING self.async_write_ha_state() - await asyncio.sleep(2) + await asyncio.sleep(LOCK_UNLOCK_DELAY) if self._jam_on_operation: self._state = STATE_JAMMED else: @@ -83,7 +85,7 @@ class DemoLock(LockEntity): """Unlock the device.""" self._state = STATE_UNLOCKING self.async_write_ha_state() - await asyncio.sleep(2) + await asyncio.sleep(LOCK_UNLOCK_DELAY) self._state = STATE_UNLOCKED self.async_write_ha_state() diff --git a/tests/components/demo/test_lock.py b/tests/components/demo/test_lock.py index 15e4e14524d..3301bf5f406 100644 --- a/tests/components/demo/test_lock.py +++ b/tests/components/demo/test_lock.py @@ -1,9 +1,9 @@ """The tests for the Demo lock platform.""" -import asyncio +from unittest.mock import patch import pytest -from homeassistant.components.demo import DOMAIN +from homeassistant.components.demo import DOMAIN, lock as demo_lock from homeassistant.components.lock import ( DOMAIN as LOCK_DOMAIN, SERVICE_LOCK, @@ -15,10 +15,10 @@ from homeassistant.components.lock import ( STATE_UNLOCKED, STATE_UNLOCKING, ) -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID, EVENT_STATE_CHANGED from homeassistant.setup import async_setup_component -from tests.common import async_mock_service +from tests.common import async_capture_events, async_mock_service FRONT = "lock.front_door" KITCHEN = "lock.kitchen_door" @@ -35,54 +35,64 @@ async def setup_comp(hass): await hass.async_block_till_done() +@patch.object(demo_lock, "LOCK_UNLOCK_DELAY", 0) async def test_locking(hass): """Test the locking of a lock.""" state = hass.states.get(KITCHEN) assert state.state == STATE_UNLOCKED + await hass.async_block_till_done() + state_changes = async_capture_events(hass, EVENT_STATE_CHANGED) await hass.services.async_call( LOCK_DOMAIN, SERVICE_LOCK, {ATTR_ENTITY_ID: KITCHEN}, blocking=False ) + await hass.async_block_till_done() - await asyncio.sleep(1) - state = hass.states.get(KITCHEN) - assert state.state == STATE_LOCKING - await asyncio.sleep(2) - state = hass.states.get(KITCHEN) - assert state.state == STATE_LOCKED + assert state_changes[0].data["entity_id"] == KITCHEN + assert state_changes[0].data["new_state"].state == STATE_LOCKING + + assert state_changes[1].data["entity_id"] == KITCHEN + assert state_changes[1].data["new_state"].state == STATE_LOCKED +@patch.object(demo_lock, "LOCK_UNLOCK_DELAY", 0) async def test_unlocking(hass): """Test the unlocking of a lock.""" state = hass.states.get(FRONT) assert state.state == STATE_LOCKED + await hass.async_block_till_done() + state_changes = async_capture_events(hass, EVENT_STATE_CHANGED) await hass.services.async_call( LOCK_DOMAIN, SERVICE_UNLOCK, {ATTR_ENTITY_ID: FRONT}, blocking=False ) - await asyncio.sleep(1) - state = hass.states.get(FRONT) - assert state.state == STATE_UNLOCKING - await asyncio.sleep(2) - state = hass.states.get(FRONT) - assert state.state == STATE_UNLOCKED + await hass.async_block_till_done() + + assert state_changes[0].data["entity_id"] == FRONT + assert state_changes[0].data["new_state"].state == STATE_UNLOCKING + + assert state_changes[1].data["entity_id"] == FRONT + assert state_changes[1].data["new_state"].state == STATE_UNLOCKED +@patch.object(demo_lock, "LOCK_UNLOCK_DELAY", 0) async def test_jammed_when_locking(hass): """Test the locking of a lock jams.""" state = hass.states.get(POORLY_INSTALLED) assert state.state == STATE_UNLOCKED + await hass.async_block_till_done() + state_changes = async_capture_events(hass, EVENT_STATE_CHANGED) await hass.services.async_call( LOCK_DOMAIN, SERVICE_LOCK, {ATTR_ENTITY_ID: POORLY_INSTALLED}, blocking=False ) + await hass.async_block_till_done() - await asyncio.sleep(1) - state = hass.states.get(POORLY_INSTALLED) - assert state.state == STATE_LOCKING - await asyncio.sleep(2) - state = hass.states.get(POORLY_INSTALLED) - assert state.state == STATE_JAMMED + assert state_changes[0].data["entity_id"] == POORLY_INSTALLED + assert state_changes[0].data["new_state"].state == STATE_LOCKING + + assert state_changes[1].data["entity_id"] == POORLY_INSTALLED + assert state_changes[1].data["new_state"].state == STATE_JAMMED async def test_opening_mocked(hass): From da7c5f4722d5b10309c88941128f8adde97866ad Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 10 Dec 2021 12:48:47 +0100 Subject: [PATCH 0291/2644] Break long lines in discovery_info (#61431) --- homeassistant/components/dhcp/__init__.py | 6 ++++-- homeassistant/components/mqtt/__init__.py | 3 ++- homeassistant/components/ssdp/__init__.py | 16 ++++++++++++---- homeassistant/components/usb/__init__.py | 3 ++- homeassistant/components/zeroconf/__init__.py | 6 ++++-- 5 files changed, 24 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index aa3fb2118cb..28204943606 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -75,7 +75,8 @@ class DhcpServiceInfo(BaseServiceInfo): Deprecated, and will be removed in version 2022.6. """ report( - f"accessed discovery_info['{name}'] instead of discovery_info.{name}; this will fail in version 2022.6", + f"accessed discovery_info['{name}'] instead of discovery_info.{name}; " + "this will fail in version 2022.6", exclude_integrations={DOMAIN}, error_if_core=False, ) @@ -88,7 +89,8 @@ class DhcpServiceInfo(BaseServiceInfo): Deprecated, and will be removed in version 2022.6. """ report( - f"accessed discovery_info.get('{name}') instead of discovery_info.{name}; this will fail in version 2022.6", + f"accessed discovery_info.get('{name}') instead of discovery_info.{name}; " + "this will fail in version 2022.6", exclude_integrations={DOMAIN}, error_if_core=False, ) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index ae7b60c4454..7e93c26a887 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -268,7 +268,8 @@ class MqttServiceInfo(BaseServiceInfo): Deprecated, and will be removed in version 2022.6. """ report( - f"accessed discovery_info['{name}'] instead of discovery_info.{name}; this will fail in version 2022.6", + f"accessed discovery_info['{name}'] instead of discovery_info.{name}; " + "this will fail in version 2022.6", exclude_integrations={DOMAIN}, error_if_core=False, ) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index f7ccff153ac..ea4740be77a 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -110,7 +110,10 @@ class SsdpServiceInfo( Deprecated, and will be removed in version 2022.6. """ report( - f"accessed discovery_info['{name}'] instead of discovery_info.{name}; this will fail in version 2022.6", + f"accessed discovery_info['{name}'] instead of discovery_info.{name}, " + f"discovery_info.upnp['{name}'] " + f"or discovery_info.ssdp_headers['{name}']; " + "this will fail in version 2022.6", exclude_integrations={DOMAIN}, error_if_core=False, ) @@ -128,7 +131,10 @@ class SsdpServiceInfo( Deprecated, and will be removed in version 2022.6. """ report( - f"accessed discovery_info.get('{name}') instead of discovery_info.{name}; this will fail in version 2022.6", + f"accessed discovery_info.get('{name}') instead of discovery_info.{name}, " + f"discovery_info.upnp.get('{name}') " + f"or discovery_info.ssdp_headers.get('{name}'); " + "this will fail in version 2022.6", exclude_integrations={DOMAIN}, error_if_core=False, ) @@ -143,8 +149,10 @@ class SsdpServiceInfo( Deprecated, and will be removed in version 2022.6. """ report( - f"accessed discovery_info.__contains__('{name}') instead of discovery_info.upnp.__contains__('{name}') " - f"or discovery_info.ssdp_headers.__contains__('{name}'); this will fail in version 2022.6", + f"accessed discovery_info.__contains__('{name}') " + f"instead of discovery_info.upnp.__contains__('{name}') " + f"or discovery_info.ssdp_headers.__contains__('{name}'); " + "this will fail in version 2022.6", exclude_integrations={DOMAIN}, error_if_core=False, ) diff --git a/homeassistant/components/usb/__init__.py b/homeassistant/components/usb/__init__.py index ec3ffa0405a..38377aadd9f 100644 --- a/homeassistant/components/usb/__init__.py +++ b/homeassistant/components/usb/__init__.py @@ -51,7 +51,8 @@ class UsbServiceInfo(BaseServiceInfo): Deprecated, and will be removed in version 2022.6. """ report( - f"accessed discovery_info['{name}'] instead of discovery_info.{name}; this will fail in version 2022.6", + f"accessed discovery_info['{name}'] instead of discovery_info.{name}; " + "this will fail in version 2022.6", exclude_integrations={DOMAIN}, error_if_core=False, ) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 221a4d6b834..deeb42367cd 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -114,7 +114,8 @@ class ZeroconfServiceInfo(BaseServiceInfo): Deprecated, and will be removed in version 2022.6. """ report( - f"accessed discovery_info['{name}'] instead of discovery_info.{name}; this will fail in version 2022.6", + f"accessed discovery_info['{name}'] instead of discovery_info.{name}; " + "this will fail in version 2022.6", exclude_integrations={DOMAIN}, error_if_core=False, ) @@ -127,7 +128,8 @@ class ZeroconfServiceInfo(BaseServiceInfo): Deprecated, and will be removed in version 2022.6. """ report( - f"accessed discovery_info.get('{name}') instead of discovery_info.{name}; this will fail in version 2022.6", + f"accessed discovery_info.get('{name}') instead of discovery_info.{name}; " + "this will fail in version 2022.6", exclude_integrations={DOMAIN}, error_if_core=False, ) From 5538d5d59dee7246a0953582dc62f2b0cbbf61cf Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 10 Dec 2021 14:48:10 +0100 Subject: [PATCH 0292/2644] Use new DeviceClass enums in flipr (#61439) Co-authored-by: epenet --- homeassistant/components/flipr/binary_sensor.py | 6 +++--- homeassistant/components/flipr/sensor.py | 15 +++++++-------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/flipr/binary_sensor.py b/homeassistant/components/flipr/binary_sensor.py index 400c1562088..54f90248802 100644 --- a/homeassistant/components/flipr/binary_sensor.py +++ b/homeassistant/components/flipr/binary_sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_PROBLEM, + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -14,12 +14,12 @@ BINARY_SENSORS_TYPES: tuple[BinarySensorEntityDescription, ...] = ( BinarySensorEntityDescription( key="ph_status", name="PH Status", - device_class=DEVICE_CLASS_PROBLEM, + device_class=BinarySensorDeviceClass.PROBLEM, ), BinarySensorEntityDescription( key="chlorine_status", name="Chlorine Status", - device_class=DEVICE_CLASS_PROBLEM, + device_class=BinarySensorDeviceClass.PROBLEM, ), ) diff --git a/homeassistant/components/flipr/sensor.py b/homeassistant/components/flipr/sensor.py index 527742539c5..9fcef425a26 100644 --- a/homeassistant/components/flipr/sensor.py +++ b/homeassistant/components/flipr/sensor.py @@ -1,13 +1,12 @@ """Sensor platform for the Flipr's pool_sensor.""" from __future__ import annotations -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription -from homeassistant.const import ( - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_TIMESTAMP, - ELECTRIC_POTENTIAL_MILLIVOLT, - TEMP_CELSIUS, +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, ) +from homeassistant.const import ELECTRIC_POTENTIAL_MILLIVOLT, TEMP_CELSIUS from . import FliprEntity from .const import DOMAIN @@ -27,13 +26,13 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="temperature", name="Water Temp", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, ), SensorEntityDescription( key="date_time", name="Last Measured", - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, ), SensorEntityDescription( key="red_ox", From 172237f4f1b13ec8f17850fd52e4e1c722dc2edd Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 10 Dec 2021 14:48:48 +0100 Subject: [PATCH 0293/2644] Use new SensorDeviceClass enum in foobot (#61442) Co-authored-by: epenet --- homeassistant/components/foobot/sensor.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/foobot/sensor.py b/homeassistant/components/foobot/sensor.py index 21510771cd5..5b252716d92 100644 --- a/homeassistant/components/foobot/sensor.py +++ b/homeassistant/components/foobot/sensor.py @@ -9,7 +9,11 @@ import aiohttp from foobot_async import FoobotClient import voluptuous as vol -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) from homeassistant.const import ( ATTR_TEMPERATURE, ATTR_TIME, @@ -18,7 +22,6 @@ from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, CONF_TOKEN, CONF_USERNAME, - DEVICE_CLASS_TEMPERATURE, PERCENTAGE, TEMP_CELSIUS, TIME_SECONDS, @@ -54,7 +57,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="tmp", name=ATTR_TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( key="hum", From 6677dd8507e7f5acdd5d5b647f4c7a5da4e421db Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 10 Dec 2021 14:50:21 +0100 Subject: [PATCH 0294/2644] Use new DeviceClass enums in fibaro (#61437) Co-authored-by: epenet --- .../components/fibaro/binary_sensor.py | 19 ++++++++++--------- homeassistant/components/fibaro/sensor.py | 14 +++++--------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/fibaro/binary_sensor.py b/homeassistant/components/fibaro/binary_sensor.py index 96c688bac16..d6382e6f433 100644 --- a/homeassistant/components/fibaro/binary_sensor.py +++ b/homeassistant/components/fibaro/binary_sensor.py @@ -1,10 +1,7 @@ """Support for Fibaro binary sensors.""" from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_DOOR, - DEVICE_CLASS_MOTION, - DEVICE_CLASS_SMOKE, - DEVICE_CLASS_WINDOW, DOMAIN, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.const import CONF_DEVICE_CLASS, CONF_ICON @@ -13,11 +10,15 @@ from . import FIBARO_DEVICES, FibaroDevice SENSOR_TYPES = { "com.fibaro.floodSensor": ["Flood", "mdi:water", "flood"], - "com.fibaro.motionSensor": ["Motion", "mdi:run", DEVICE_CLASS_MOTION], - "com.fibaro.doorSensor": ["Door", "mdi:window-open", DEVICE_CLASS_DOOR], - "com.fibaro.windowSensor": ["Window", "mdi:window-open", DEVICE_CLASS_WINDOW], - "com.fibaro.smokeSensor": ["Smoke", "mdi:smoking", DEVICE_CLASS_SMOKE], - "com.fibaro.FGMS001": ["Motion", "mdi:run", DEVICE_CLASS_MOTION], + "com.fibaro.motionSensor": ["Motion", "mdi:run", BinarySensorDeviceClass.MOTION], + "com.fibaro.doorSensor": ["Door", "mdi:window-open", BinarySensorDeviceClass.DOOR], + "com.fibaro.windowSensor": [ + "Window", + "mdi:window-open", + BinarySensorDeviceClass.WINDOW, + ], + "com.fibaro.smokeSensor": ["Smoke", "mdi:smoking", BinarySensorDeviceClass.SMOKE], + "com.fibaro.FGMS001": ["Motion", "mdi:run", BinarySensorDeviceClass.MOTION], "com.fibaro.heatDetector": ["Heat", "mdi:fire", "heat"], } diff --git a/homeassistant/components/fibaro/sensor.py b/homeassistant/components/fibaro/sensor.py index a4b4e744af7..b96d0b23ecc 100644 --- a/homeassistant/components/fibaro/sensor.py +++ b/homeassistant/components/fibaro/sensor.py @@ -1,13 +1,9 @@ """Support for Fibaro sensors.""" from contextlib import suppress -from homeassistant.components.sensor import DOMAIN, SensorEntity +from homeassistant.components.sensor import DOMAIN, SensorDeviceClass, SensorEntity from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, - DEVICE_CLASS_CO2, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_TEMPERATURE, LIGHT_LUX, PERCENTAGE, TEMP_CELSIUS, @@ -21,7 +17,7 @@ SENSOR_TYPES = { "Temperature", None, None, - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, ], "com.fibaro.smokeSensor": [ "Smoke", @@ -34,15 +30,15 @@ SENSOR_TYPES = { CONCENTRATION_PARTS_PER_MILLION, None, None, - DEVICE_CLASS_CO2, + SensorDeviceClass.CO2, ], "com.fibaro.humiditySensor": [ "Humidity", PERCENTAGE, None, - DEVICE_CLASS_HUMIDITY, + SensorDeviceClass.HUMIDITY, ], - "com.fibaro.lightSensor": ["Light", LIGHT_LUX, None, DEVICE_CLASS_ILLUMINANCE], + "com.fibaro.lightSensor": ["Light", LIGHT_LUX, None, SensorDeviceClass.ILLUMINANCE], } From 4d282eca6d9c9b0dd4d5798c3bcd42b4def319ca Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 10 Dec 2021 14:52:33 +0100 Subject: [PATCH 0295/2644] Use new DeviceClass constants in flo (#61440) Co-authored-by: epenet --- homeassistant/components/flo/binary_sensor.py | 6 +++--- homeassistant/components/flo/sensor.py | 14 +++++--------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/flo/binary_sensor.py b/homeassistant/components/flo/binary_sensor.py index 88675e571e7..2118635d9ad 100644 --- a/homeassistant/components/flo/binary_sensor.py +++ b/homeassistant/components/flo/binary_sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_PROBLEM, + BinarySensorDeviceClass, BinarySensorEntity, ) @@ -37,7 +37,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class FloPendingAlertsBinarySensor(FloEntity, BinarySensorEntity): """Binary sensor that reports on if there are any pending system alerts.""" - _attr_device_class = DEVICE_CLASS_PROBLEM + _attr_device_class = BinarySensorDeviceClass.PROBLEM def __init__(self, device): """Initialize the pending alerts binary sensor.""" @@ -63,7 +63,7 @@ class FloPendingAlertsBinarySensor(FloEntity, BinarySensorEntity): class FloWaterDetectedBinarySensor(FloEntity, BinarySensorEntity): """Binary sensor that reports if water is detected (for leak detectors).""" - _attr_device_class = DEVICE_CLASS_PROBLEM + _attr_device_class = BinarySensorDeviceClass.PROBLEM def __init__(self, device): """Initialize the pending alerts binary sensor.""" diff --git a/homeassistant/components/flo/sensor.py b/homeassistant/components/flo/sensor.py index b64ed9ee3e4..c98f99f74ce 100644 --- a/homeassistant/components/flo/sensor.py +++ b/homeassistant/components/flo/sensor.py @@ -1,12 +1,8 @@ """Support for Flo Water Monitor sensors.""" from __future__ import annotations -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.const import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, PERCENTAGE, PRESSURE_PSI, TEMP_FAHRENHEIT, @@ -114,7 +110,7 @@ class FloCurrentFlowRateSensor(FloEntity, SensorEntity): class FloTemperatureSensor(FloEntity, SensorEntity): """Monitors the temperature.""" - _attr_device_class = DEVICE_CLASS_TEMPERATURE + _attr_device_class = SensorDeviceClass.TEMPERATURE _attr_native_unit_of_measurement = TEMP_FAHRENHEIT def __init__(self, name, device): @@ -133,7 +129,7 @@ class FloTemperatureSensor(FloEntity, SensorEntity): class FloHumiditySensor(FloEntity, SensorEntity): """Monitors the humidity.""" - _attr_device_class = DEVICE_CLASS_HUMIDITY + _attr_device_class = SensorDeviceClass.HUMIDITY _attr_native_unit_of_measurement = PERCENTAGE def __init__(self, device): @@ -152,7 +148,7 @@ class FloHumiditySensor(FloEntity, SensorEntity): class FloPressureSensor(FloEntity, SensorEntity): """Monitors the water pressure.""" - _attr_device_class = DEVICE_CLASS_PRESSURE + _attr_device_class = SensorDeviceClass.PRESSURE _attr_native_unit_of_measurement = PRESSURE_PSI def __init__(self, device): @@ -171,7 +167,7 @@ class FloPressureSensor(FloEntity, SensorEntity): class FloBatterySensor(FloEntity, SensorEntity): """Monitors the battery level for battery-powered leak detectors.""" - _attr_device_class = DEVICE_CLASS_BATTERY + _attr_device_class = SensorDeviceClass.BATTERY _attr_native_unit_of_measurement = PERCENTAGE def __init__(self, device): From 731c70a0e71a3c75c13827d8b674d6e2b4e95dc7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 10 Dec 2021 14:52:54 +0100 Subject: [PATCH 0296/2644] Use new enums in forecast_solar (#61443) Co-authored-by: epenet --- .../components/forecast_solar/const.py | 32 ++++++++----------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/forecast_solar/const.py b/homeassistant/components/forecast_solar/const.py index 97caba531e6..63d5bd10084 100644 --- a/homeassistant/components/forecast_solar/const.py +++ b/homeassistant/components/forecast_solar/const.py @@ -3,14 +3,8 @@ from __future__ import annotations from datetime import timedelta -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT -from homeassistant.const import ( - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_TIMESTAMP, - ENERGY_KILO_WATT_HOUR, - POWER_WATT, -) +from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass +from homeassistant.const import ENERGY_KILO_WATT_HOUR, POWER_WATT from .models import ForecastSolarSensorEntityDescription @@ -26,32 +20,32 @@ SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( key="energy_production_today", name="Estimated Energy Production - Today", state=lambda estimate: estimate.energy_production_today / 1000, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), ForecastSolarSensorEntityDescription( key="energy_production_tomorrow", name="Estimated Energy Production - Tomorrow", state=lambda estimate: estimate.energy_production_tomorrow / 1000, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), ForecastSolarSensorEntityDescription( key="power_highest_peak_time_today", name="Highest Power Peak Time - Today", - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, ), ForecastSolarSensorEntityDescription( key="power_highest_peak_time_tomorrow", name="Highest Power Peak Time - Tomorrow", - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, ), ForecastSolarSensorEntityDescription( key="power_production_now", name="Estimated Power Production - Now", - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, state=lambda estimate: estimate.power_production_now, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_WATT, ), ForecastSolarSensorEntityDescription( @@ -60,7 +54,7 @@ SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( estimate.now() + timedelta(hours=1) ), name="Estimated Power Production - Next Hour", - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, native_unit_of_measurement=POWER_WATT, ), @@ -70,7 +64,7 @@ SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( estimate.now() + timedelta(hours=12) ), name="Estimated Power Production - Next 12 Hours", - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, native_unit_of_measurement=POWER_WATT, ), @@ -80,7 +74,7 @@ SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( estimate.now() + timedelta(hours=24) ), name="Estimated Power Production - Next 24 Hours", - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, native_unit_of_measurement=POWER_WATT, ), @@ -88,14 +82,14 @@ SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( key="energy_current_hour", name="Estimated Energy Production - This Hour", state=lambda estimate: estimate.energy_current_hour / 1000, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), ForecastSolarSensorEntityDescription( key="energy_next_hour", state=lambda estimate: estimate.sum_energy_production(1) / 1000, name="Estimated Energy Production - Next Hour", - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), ) From eb27da3cd4cc5e57f7f9ae741c0e06e693aecdad Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 10 Dec 2021 14:53:08 +0100 Subject: [PATCH 0297/2644] Use new SensorDeviceClass enum in freebox (#61444) Co-authored-by: epenet --- homeassistant/components/freebox/sensor.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index 016434ac89f..6286fef71e5 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -4,13 +4,13 @@ from __future__ import annotations import logging from typing import Any -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - DATA_RATE_KILOBYTES_PER_SECOND, - DEVICE_CLASS_TEMPERATURE, - TEMP_CELSIUS, +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, ) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import DATA_RATE_KILOBYTES_PER_SECOND, TEMP_CELSIUS from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo @@ -42,7 +42,7 @@ async def async_setup_entry( key=sensor_name, name=f"Freebox {sensor_name}", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), ) for sensor_name in router.sensors_temperature From f1979f8b68035f98bd493fb7c34b98176e7bedde Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 10 Dec 2021 14:53:58 +0100 Subject: [PATCH 0298/2644] Use new enums in fjaraskupan (#61438) Co-authored-by: epenet --- .../components/fjaraskupan/binary_sensor.py | 6 +++--- homeassistant/components/fjaraskupan/number.py | 6 +++--- homeassistant/components/fjaraskupan/sensor.py | 17 +++++++---------- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/fjaraskupan/binary_sensor.py b/homeassistant/components/fjaraskupan/binary_sensor.py index 9af93eaf9c0..9f24a3d39d2 100644 --- a/homeassistant/components/fjaraskupan/binary_sensor.py +++ b/homeassistant/components/fjaraskupan/binary_sensor.py @@ -7,7 +7,7 @@ from dataclasses import dataclass from fjaraskupan import Device, State from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_PROBLEM, + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -34,13 +34,13 @@ SENSORS = ( EntityDescription( key="grease-filter", name="Grease Filter", - device_class=DEVICE_CLASS_PROBLEM, + device_class=BinarySensorDeviceClass.PROBLEM, is_on=lambda state: state.grease_filter_full, ), EntityDescription( key="carbon-filter", name="Carbon Filter", - device_class=DEVICE_CLASS_PROBLEM, + device_class=BinarySensorDeviceClass.PROBLEM, is_on=lambda state: state.carbon_filter_full, ), ) diff --git a/homeassistant/components/fjaraskupan/number.py b/homeassistant/components/fjaraskupan/number.py index 66f719abd6f..eecb0b3b8e1 100644 --- a/homeassistant/components/fjaraskupan/number.py +++ b/homeassistant/components/fjaraskupan/number.py @@ -5,9 +5,9 @@ from fjaraskupan import Device, State from homeassistant.components.number import NumberEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_CONFIG, TIME_MINUTES +from homeassistant.const import TIME_MINUTES from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import DeviceInfo, Entity, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -40,7 +40,7 @@ class PeriodicVentingTime(CoordinatorEntity[State], NumberEntity): _attr_max_value: float = 59 _attr_min_value: float = 0 _attr_step: float = 1 - _attr_entity_category = ENTITY_CATEGORY_CONFIG + _attr_entity_category = EntityCategory.CONFIG _attr_unit_of_measurement = TIME_MINUTES def __init__( diff --git a/homeassistant/components/fjaraskupan/sensor.py b/homeassistant/components/fjaraskupan/sensor.py index 1821008a1d7..8c19b3e3cec 100644 --- a/homeassistant/components/fjaraskupan/sensor.py +++ b/homeassistant/components/fjaraskupan/sensor.py @@ -4,17 +4,14 @@ from __future__ import annotations from fjaraskupan import Device, State from homeassistant.components.sensor import ( - DEVICE_CLASS_SIGNAL_STRENGTH, - STATE_CLASS_MEASUREMENT, + SensorDeviceClass, SensorEntity, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ENTITY_CATEGORY_DIAGNOSTIC, - SIGNAL_STRENGTH_DECIBELS_MILLIWATT, -) +from homeassistant.const import SIGNAL_STRENGTH_DECIBELS_MILLIWATT from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import DeviceInfo, Entity, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import ( @@ -56,11 +53,11 @@ class RssiSensor(CoordinatorEntity[State], SensorEntity): self._attr_unique_id = f"{device.address}-signal-strength" self._attr_device_info = device_info self._attr_name = f"{device_info['name']} Signal Strength" - self._attr_device_class = DEVICE_CLASS_SIGNAL_STRENGTH - self._attr_state_class = STATE_CLASS_MEASUREMENT + self._attr_device_class = SensorDeviceClass.SIGNAL_STRENGTH + self._attr_state_class = SensorStateClass.MEASUREMENT self._attr_native_unit_of_measurement = SIGNAL_STRENGTH_DECIBELS_MILLIWATT self._attr_entity_registry_enabled_default = False - self._attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + self._attr_entity_category = EntityCategory.DIAGNOSTIC @property def native_value(self) -> StateType: From 25838e97e0b02b4dd9ef3761724c17719d3e01cb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 10 Dec 2021 14:54:41 +0100 Subject: [PATCH 0299/2644] Use BinarySensorDeviceClass in ffmpeg (#61436) Co-authored-by: epenet --- homeassistant/components/ffmpeg_motion/binary_sensor.py | 4 ++-- homeassistant/components/ffmpeg_noise/binary_sensor.py | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ffmpeg_motion/binary_sensor.py b/homeassistant/components/ffmpeg_motion/binary_sensor.py index ecbf6f3b1ae..292ec35fbff 100644 --- a/homeassistant/components/ffmpeg_motion/binary_sensor.py +++ b/homeassistant/components/ffmpeg_motion/binary_sensor.py @@ -3,8 +3,8 @@ import haffmpeg.sensor as ffmpeg_sensor import voluptuous as vol from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_MOTION, PLATFORM_SCHEMA, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.components.ffmpeg import ( @@ -115,4 +115,4 @@ class FFmpegMotion(FFmpegBinarySensor): @property def device_class(self): """Return the class of this sensor, from DEVICE_CLASSES.""" - return DEVICE_CLASS_MOTION + return BinarySensorDeviceClass.MOTION diff --git a/homeassistant/components/ffmpeg_noise/binary_sensor.py b/homeassistant/components/ffmpeg_noise/binary_sensor.py index 6c84c5973f1..7745d6fae2a 100644 --- a/homeassistant/components/ffmpeg_noise/binary_sensor.py +++ b/homeassistant/components/ffmpeg_noise/binary_sensor.py @@ -2,7 +2,10 @@ import haffmpeg.sensor as ffmpeg_sensor import voluptuous as vol -from homeassistant.components.binary_sensor import DEVICE_CLASS_SOUND, PLATFORM_SCHEMA +from homeassistant.components.binary_sensor import ( + PLATFORM_SCHEMA, + BinarySensorDeviceClass, +) from homeassistant.components.ffmpeg import ( CONF_EXTRA_ARGUMENTS, CONF_INITIAL_STATE, @@ -78,4 +81,4 @@ class FFmpegNoise(FFmpegBinarySensor): @property def device_class(self): """Return the class of this sensor, from DEVICE_CLASSES.""" - return DEVICE_CLASS_SOUND + return BinarySensorDeviceClass.SOUND From 0d9d6d572728b33343f8d170afaf4c61ddb147c2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 10 Dec 2021 14:55:33 +0100 Subject: [PATCH 0300/2644] Use new SensorStateClass enum in flunearyou (#61441) Co-authored-by: epenet --- homeassistant/components/flunearyou/sensor.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/flunearyou/sensor.py b/homeassistant/components/flunearyou/sensor.py index 0017d868964..8a232d3d103 100644 --- a/homeassistant/components/flunearyou/sensor.py +++ b/homeassistant/components/flunearyou/sensor.py @@ -5,9 +5,9 @@ from collections.abc import Mapping from typing import Any, Union, cast from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_STATE, CONF_LATITUDE, CONF_LONGITUDE @@ -58,49 +58,49 @@ USER_SENSOR_DESCRIPTIONS = ( name="Avian Flu Symptoms", icon="mdi:alert", native_unit_of_measurement="reports", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_TYPE_USER_DENGUE, name="Dengue Fever Symptoms", icon="mdi:alert", native_unit_of_measurement="reports", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_TYPE_USER_FLU, name="Flu Symptoms", icon="mdi:alert", native_unit_of_measurement="reports", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_TYPE_USER_LEPTO, name="Leptospirosis Symptoms", icon="mdi:alert", native_unit_of_measurement="reports", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_TYPE_USER_NO_SYMPTOMS, name="No Symptoms", icon="mdi:alert", native_unit_of_measurement="reports", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_TYPE_USER_SYMPTOMS, name="Flu-like Symptoms", icon="mdi:alert", native_unit_of_measurement="reports", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_TYPE_USER_TOTAL, name="Total Symptoms", icon="mdi:alert", native_unit_of_measurement="reports", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), ) From 44b7c0e65cf898dfb7afc281c235d30d9e579b07 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 10 Dec 2021 14:58:23 +0100 Subject: [PATCH 0301/2644] Use new enums in fritzbox (#61447) Co-authored-by: epenet --- .../components/fritzbox/binary_sensor.py | 4 +- homeassistant/components/fritzbox/sensor.py | 42 ++++++++----------- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/fritzbox/binary_sensor.py b/homeassistant/components/fritzbox/binary_sensor.py index b0f5e63d424..e83a2a67472 100644 --- a/homeassistant/components/fritzbox/binary_sensor.py +++ b/homeassistant/components/fritzbox/binary_sensor.py @@ -8,7 +8,7 @@ from typing import Final from pyfritzhome.fritzhomedevice import FritzhomeDevice from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_WINDOW, + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -40,7 +40,7 @@ BINARY_SENSOR_TYPES: Final[tuple[FritzBinarySensorEntityDescription, ...]] = ( FritzBinarySensorEntityDescription( key="alarm", name="Alarm", - device_class=DEVICE_CLASS_WINDOW, + device_class=BinarySensorDeviceClass.WINDOW, suitable=lambda device: device.has_alarm, # type: ignore[no-any-return] is_on=lambda device: device.alert_state, # type: ignore[no-any-return] ), diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index 0ee7c3e8563..473e68ed5da 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -10,26 +10,20 @@ from pyfritzhome.fritzhomedevice import FritzhomeDevice from homeassistant.components.climate.const import PRESET_COMFORT, PRESET_ECO from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_TIMESTAMP, ENERGY_KILO_WATT_HOUR, - ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, POWER_WATT, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.util.dt import utc_from_timestamp @@ -58,9 +52,9 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( key="temperature", name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, suitable=lambda device: ( device.has_temperature_sensor and not device.has_thermostat ), @@ -70,8 +64,8 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( key="humidity", name="Humidity", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, suitable=lambda device: device.rel_humidity is not None, native_value=lambda device: device.rel_humidity, # type: ignore[no-any-return] ), @@ -79,8 +73,8 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( key="battery", name="Battery", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_BATTERY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=SensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, suitable=lambda device: device.battery_level is not None, native_value=lambda device: device.battery_level, # type: ignore[no-any-return] ), @@ -88,8 +82,8 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( key="power_consumption", name="Power Consumption", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, suitable=lambda device: device.has_powermeter, # type: ignore[no-any-return] native_value=lambda device: device.power / 1000 if device.power else 0.0, ), @@ -97,8 +91,8 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( key="total_energy", name="Total Energy", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, suitable=lambda device: device.has_powermeter, # type: ignore[no-any-return] native_value=lambda device: device.energy / 1000 if device.energy else 0.0, ), @@ -107,7 +101,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( key="comfort_temperature", name="Comfort Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, suitable=lambda device: device.has_thermostat and device.comfort_temperature is not None, native_value=lambda device: device.comfort_temperature, # type: ignore[no-any-return] @@ -116,7 +110,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( key="eco_temperature", name="Eco Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, suitable=lambda device: device.has_thermostat and device.eco_temperature is not None, native_value=lambda device: device.eco_temperature, # type: ignore[no-any-return] @@ -125,7 +119,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( key="nextchange_temperature", name="Next Scheduled Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, suitable=lambda device: device.has_thermostat and device.nextchange_temperature is not None, native_value=lambda device: device.nextchange_temperature, # type: ignore[no-any-return] @@ -133,7 +127,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( FritzSensorEntityDescription( key="nextchange_time", name="Next Scheduled Change Time", - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, suitable=lambda device: device.has_thermostat and device.nextchange_endperiod is not None, native_value=lambda device: utc_from_timestamp(device.nextchange_endperiod), From 80b65c679f1ed36c59643ce09491197b36a0084e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 10 Dec 2021 14:58:34 +0100 Subject: [PATCH 0302/2644] Use new enums in fritz (#61446) Co-authored-by: epenet --- .../components/fritz/binary_sensor.py | 18 ++++++------- homeassistant/components/fritz/sensor.py | 27 +++++++++---------- homeassistant/components/fritz/switch.py | 11 ++++---- 3 files changed, 26 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/fritz/binary_sensor.py b/homeassistant/components/fritz/binary_sensor.py index 594c1721be4..a54390cf260 100644 --- a/homeassistant/components/fritz/binary_sensor.py +++ b/homeassistant/components/fritz/binary_sensor.py @@ -4,15 +4,13 @@ from __future__ import annotations import logging from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_CONNECTIVITY, - DEVICE_CLASS_PLUG, - DEVICE_CLASS_UPDATE, + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .common import FritzBoxBaseEntity, FritzBoxTools @@ -25,20 +23,20 @@ SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( BinarySensorEntityDescription( key="is_connected", name="Connection", - device_class=DEVICE_CLASS_CONNECTIVITY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=EntityCategory.DIAGNOSTIC, ), BinarySensorEntityDescription( key="is_linked", name="Link", - device_class=DEVICE_CLASS_PLUG, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.PLUG, + entity_category=EntityCategory.DIAGNOSTIC, ), BinarySensorEntityDescription( key="firmware_update", name="Firmware Update", - device_class=DEVICE_CLASS_UPDATE, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.UPDATE, + entity_category=EntityCategory.DIAGNOSTIC, ), ) diff --git a/homeassistant/components/fritz/sensor.py b/homeassistant/components/fritz/sensor.py index 869e135c2c8..9f3bc0fd7c1 100644 --- a/homeassistant/components/fritz/sensor.py +++ b/homeassistant/components/fritz/sensor.py @@ -17,21 +17,20 @@ from fritzconnection.core.exceptions import ( from fritzconnection.lib.fritzstatus import FritzStatus from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DATA_GIGABYTES, DATA_RATE_KILOBITS_PER_SECOND, DATA_RATE_KILOBYTES_PER_SECOND, - DEVICE_CLASS_TIMESTAMP, - ENTITY_CATEGORY_DIAGNOSTIC, SIGNAL_STRENGTH_DECIBELS, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import utcnow @@ -165,21 +164,21 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = ( FritzSensorEntityDescription( key="device_uptime", name="Device Uptime", - device_class=DEVICE_CLASS_TIMESTAMP, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=SensorDeviceClass.TIMESTAMP, + entity_category=EntityCategory.DIAGNOSTIC, value_fn=_retrieve_device_uptime_state, ), FritzSensorEntityDescription( key="connection_uptime", name="Connection Uptime", - device_class=DEVICE_CLASS_TIMESTAMP, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=SensorDeviceClass.TIMESTAMP, + entity_category=EntityCategory.DIAGNOSTIC, value_fn=_retrieve_connection_uptime_state, ), FritzSensorEntityDescription( key="kb_s_sent", name="Upload Throughput", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=DATA_RATE_KILOBYTES_PER_SECOND, icon="mdi:upload", value_fn=_retrieve_kb_s_sent_state, @@ -187,7 +186,7 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = ( FritzSensorEntityDescription( key="kb_s_received", name="Download Throughput", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=DATA_RATE_KILOBYTES_PER_SECOND, icon="mdi:download", value_fn=_retrieve_kb_s_received_state, @@ -197,7 +196,7 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = ( name="Max Connection Upload Throughput", native_unit_of_measurement=DATA_RATE_KILOBITS_PER_SECOND, icon="mdi:upload", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, value_fn=_retrieve_max_kb_s_sent_state, ), FritzSensorEntityDescription( @@ -205,13 +204,13 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = ( name="Max Connection Download Throughput", native_unit_of_measurement=DATA_RATE_KILOBITS_PER_SECOND, icon="mdi:download", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, value_fn=_retrieve_max_kb_s_received_state, ), FritzSensorEntityDescription( key="gb_sent", name="GB sent", - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=DATA_GIGABYTES, icon="mdi:upload", value_fn=_retrieve_gb_sent_state, @@ -219,7 +218,7 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = ( FritzSensorEntityDescription( key="gb_received", name="GB received", - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=DATA_GIGABYTES, icon="mdi:download", value_fn=_retrieve_gb_received_state, diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index 4a740a26c33..c8341710d47 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -18,10 +18,9 @@ import xmltodict from homeassistant.components.network import async_get_source_ip from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import Entity, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify @@ -445,7 +444,7 @@ class FritzBoxPortSwitch(FritzBoxBaseSwitch, SwitchEntity): self.connection_type = connection_type self.port_mapping = port_mapping # dict in the format as it comes from fritzconnection. eg: {'NewRemoteHost': '0.0.0.0', 'NewExternalPort': 22, 'NewProtocol': 'TCP', 'NewInternalPort': 22, 'NewInternalClient': '192.168.178.31', 'NewEnabled': True, 'NewPortMappingDescription': 'Beast SSH ', 'NewLeaseDuration': 0} self._idx = idx # needed for update routine - self._attr_entity_category = ENTITY_CATEGORY_CONFIG + self._attr_entity_category = EntityCategory.CONFIG if port_mapping is None: return @@ -524,7 +523,7 @@ class FritzBoxDeflectionSwitch(FritzBoxBaseSwitch, SwitchEntity): self.dict_of_deflection = dict_of_deflection self._attributes = {} self.id = int(self.dict_of_deflection["DeflectionId"]) - self._attr_entity_category = ENTITY_CATEGORY_CONFIG + self._attr_entity_category = EntityCategory.CONFIG switch_info = SwitchInfo( description=f"Call deflection {self.id}", @@ -594,7 +593,7 @@ class FritzBoxProfileSwitch(FritzDeviceBase, SwitchEntity): self._attr_is_on: bool = False self._name = f"{device.hostname} Internet Access" self._attr_unique_id = f"{self._mac}_internet_access" - self._attr_entity_category = ENTITY_CATEGORY_CONFIG + self._attr_entity_category = EntityCategory.CONFIG async def async_process_update(self) -> None: """Update device.""" @@ -655,7 +654,7 @@ class FritzBoxWifiSwitch(FritzBoxBaseSwitch, SwitchEntity): self._fritzbox_tools = fritzbox_tools self._attributes = {} - self._attr_entity_category = ENTITY_CATEGORY_CONFIG + self._attr_entity_category = EntityCategory.CONFIG self._network_num = network_num switch_info = SwitchInfo( From f77c4485b6d678d9e0e000d9eeee81d46d35cf3e Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 10 Dec 2021 16:40:31 +0100 Subject: [PATCH 0303/2644] Interim fix (#61435) --- homeassistant/components/shelly/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 8d78c148b51..cead5baf006 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -69,6 +69,7 @@ from .utils import ( BLOCK_PLATFORMS: Final = [ Platform.BINARY_SENSOR, Platform.BUTTON, + Platform.CLIMATE, Platform.COVER, Platform.LIGHT, Platform.SENSOR, @@ -76,7 +77,6 @@ BLOCK_PLATFORMS: Final = [ ] BLOCK_SLEEPING_PLATFORMS: Final = [ Platform.BINARY_SENSOR, - Platform.CLIMATE, Platform.SENSOR, ] RPC_PLATFORMS: Final = [ From e0cb7dad31a86beeef197407ac05d7aa0030f2e6 Mon Sep 17 00:00:00 2001 From: Yehuda Davis Date: Fri, 10 Dec 2021 12:19:33 -0500 Subject: [PATCH 0304/2644] Fix Tuya cover open/close commands (#61369) Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- homeassistant/components/tuya/cover.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/tuya/cover.py b/homeassistant/components/tuya/cover.py index 275d28bae17..572d440f937 100644 --- a/homeassistant/components/tuya/cover.py +++ b/homeassistant/components/tuya/cover.py @@ -315,11 +315,18 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity): {"code": self.entity_description.key, "value": value} ] - if (self.entity_description.set_position) is not None: + if ( + self.entity_description.set_position is not None + and self._set_position_type is not None + ): commands.append( { "code": self.entity_description.set_position, - "value": 0, + "value": round( + self._set_position_type.remap_value_from( + 100, 0, 100, reverse=True + ), + ), } ) @@ -327,7 +334,7 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity): def close_cover(self, **kwargs: Any) -> None: """Close cover.""" - value: bool | str = True + value: bool | str = False if self.device.function[self.entity_description.key].type == "Enum": value = "close" @@ -335,11 +342,18 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity): {"code": self.entity_description.key, "value": value} ] - if (self.entity_description.set_position) is not None: + if ( + self.entity_description.set_position is not None + and self._set_position_type is not None + ): commands.append( { "code": self.entity_description.set_position, - "value": 100, + "value": round( + self._set_position_type.remap_value_from( + 0, 0, 100, reverse=True + ), + ), } ) From aa36dde14897a4309b7fd27fcb2775912ebf96e3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 10 Dec 2021 18:59:27 +0100 Subject: [PATCH 0305/2644] Correct rest sensor configured to generate timestamps (#61429) --- homeassistant/components/rest/sensor.py | 13 ++++- homeassistant/components/sensor/helpers.py | 38 +++++++++++++ homeassistant/components/template/sensor.py | 34 +---------- tests/components/rest/test_sensor.py | 63 +++++++++++++++++++++ tests/components/sensor/test_helpers.py | 38 +++++++++++++ tests/components/sensor/test_init.py | 2 +- 6 files changed, 155 insertions(+), 33 deletions(-) create mode 100644 homeassistant/components/sensor/helpers.py create mode 100644 tests/components/sensor/test_helpers.py diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index 9f8c33ad6df..422ce84cc46 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -11,8 +11,10 @@ from homeassistant.components.sensor import ( CONF_STATE_CLASS, DOMAIN as SENSOR_DOMAIN, PLATFORM_SCHEMA, + SensorDeviceClass, SensorEntity, ) +from homeassistant.components.sensor.helpers import async_parse_date_datetime from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_FORCE_UPDATE, @@ -186,4 +188,13 @@ class RestSensor(RestEntity, SensorEntity): value, None ) - self._state = value + if value is None or self.device_class not in ( + SensorDeviceClass.DATE, + SensorDeviceClass.TIMESTAMP, + ): + self._state = value + return + + self._state = async_parse_date_datetime( + value, self.entity_id, self.device_class + ) diff --git a/homeassistant/components/sensor/helpers.py b/homeassistant/components/sensor/helpers.py new file mode 100644 index 00000000000..a3f5e3827bf --- /dev/null +++ b/homeassistant/components/sensor/helpers.py @@ -0,0 +1,38 @@ +"""Helpers for sensor entities.""" +from __future__ import annotations + +from datetime import date, datetime +import logging + +from homeassistant.core import callback +from homeassistant.util import dt as dt_util + +from . import SensorDeviceClass + +_LOGGER = logging.getLogger(__name__) + + +@callback +def async_parse_date_datetime( + value: str, entity_id: str, device_class: SensorDeviceClass | str | None +) -> datetime | date | None: + """Parse datetime string to a data or datetime.""" + if device_class == SensorDeviceClass.TIMESTAMP: + if (parsed_timestamp := dt_util.parse_datetime(value)) is None: + _LOGGER.warning("%s rendered invalid timestamp: %s", entity_id, value) + return None + + if parsed_timestamp.tzinfo is None: + _LOGGER.warning( + "%s rendered timestamp without timezone: %s", entity_id, value + ) + return None + + return parsed_timestamp + + # Date device class + if (parsed_date := dt_util.parse_date(value)) is not None: + return parsed_date + + _LOGGER.warning("%s rendered invalid date %s", entity_id, value) + return None diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index 18ae8af8569..18d0be616d4 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -2,7 +2,6 @@ from __future__ import annotations from datetime import date, datetime -import logging from typing import Any import voluptuous as vol @@ -17,6 +16,7 @@ from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, ) +from homeassistant.components.sensor.helpers import async_parse_date_datetime from homeassistant.const import ( ATTR_ENTITY_ID, CONF_DEVICE_CLASS, @@ -35,7 +35,6 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.entity import async_generate_entity_id -from homeassistant.util import dt as dt_util from .const import ( CONF_ATTRIBUTE_TEMPLATES, @@ -89,7 +88,6 @@ LEGACY_SENSOR_SCHEMA = vol.All( } ), ) -_LOGGER = logging.getLogger(__name__) def extra_validation_checks(val): @@ -184,32 +182,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -@callback -def _async_parse_date_datetime( - value: str, entity_id: str, device_class: SensorDeviceClass | str | None -) -> datetime | date | None: - """Parse datetime.""" - if device_class == SensorDeviceClass.TIMESTAMP: - if (parsed_timestamp := dt_util.parse_datetime(value)) is None: - _LOGGER.warning("%s rendered invalid timestamp: %s", entity_id, value) - return None - - if parsed_timestamp.tzinfo is None: - _LOGGER.warning( - "%s rendered timestamp without timezone: %s", entity_id, value - ) - return None - - return parsed_timestamp - - # Date device class - if (parsed_date := dt_util.parse_date(value)) is not None: - return parsed_date - - _LOGGER.warning("%s rendered invalid date %s", entity_id, value) - return None - - class SensorTemplate(TemplateEntity, SensorEntity): """Representation of a Template Sensor.""" @@ -269,7 +241,7 @@ class SensorTemplate(TemplateEntity, SensorEntity): self._attr_native_value = result return - self._attr_native_value = _async_parse_date_datetime( + self._attr_native_value = async_parse_date_datetime( result, self.entity_id, self.device_class ) @@ -303,6 +275,6 @@ class TriggerSensorEntity(TriggerEntity, SensorEntity): ): return - self._rendered[CONF_STATE] = _async_parse_date_datetime( + self._rendered[CONF_STATE] = async_parse_date_datetime( state, self.entity_id, self.device_class ) diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index d37fb047f8f..fb826eefd78 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -16,6 +16,7 @@ from homeassistant.const import ( CONTENT_TYPE_JSON, DATA_MEGABYTES, DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, SERVICE_RELOAD, STATE_UNKNOWN, TEMP_CELSIUS, @@ -218,6 +219,68 @@ async def test_setup_get(hass): assert state.attributes[sensor.ATTR_STATE_CLASS] == sensor.STATE_CLASS_MEASUREMENT +@respx.mock +async def test_setup_timestamp(hass, caplog): + """Test setup with valid configuration.""" + respx.get("http://localhost").respond( + status_code=HTTPStatus.OK, json={"key": "2021-11-11 11:39Z"} + ) + assert await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": "rest", + "resource": "http://localhost", + "method": "GET", + "value_template": "{{ value_json.key }}", + "device_class": DEVICE_CLASS_TIMESTAMP, + "state_class": sensor.STATE_CLASS_MEASUREMENT, + } + }, + ) + await async_setup_component(hass, "homeassistant", {}) + + await hass.async_block_till_done() + assert len(hass.states.async_all("sensor")) == 1 + + state = hass.states.get("sensor.rest_sensor") + assert state.state == "2021-11-11T11:39:00+00:00" + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_TIMESTAMP + assert "sensor.rest_sensor rendered invalid timestamp" not in caplog.text + assert "sensor.rest_sensor rendered timestamp without timezone" not in caplog.text + + # Bad response: Not a timestamp + respx.get("http://localhost").respond( + status_code=HTTPStatus.OK, json={"key": "invalid time stamp"} + ) + await hass.services.async_call( + "homeassistant", + "update_entity", + {ATTR_ENTITY_ID: ["sensor.rest_sensor"]}, + blocking=True, + ) + state = hass.states.get("sensor.rest_sensor") + assert state.state == "unknown" + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_TIMESTAMP + assert "sensor.rest_sensor rendered invalid timestamp" in caplog.text + + # Bad response: No timezone + respx.get("http://localhost").respond( + status_code=HTTPStatus.OK, json={"key": "2021-10-11 11:39"} + ) + await hass.services.async_call( + "homeassistant", + "update_entity", + {ATTR_ENTITY_ID: ["sensor.rest_sensor"]}, + blocking=True, + ) + state = hass.states.get("sensor.rest_sensor") + assert state.state == "unknown" + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_TIMESTAMP + assert "sensor.rest_sensor rendered timestamp without timezone" in caplog.text + + @respx.mock async def test_setup_get_templated_headers_params(hass): """Test setup with valid configuration.""" diff --git a/tests/components/sensor/test_helpers.py b/tests/components/sensor/test_helpers.py new file mode 100644 index 00000000000..d43443b85ba --- /dev/null +++ b/tests/components/sensor/test_helpers.py @@ -0,0 +1,38 @@ +"""The test for sensor helpers.""" +from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.components.sensor.helpers import async_parse_date_datetime + + +def test_async_parse_datetime(caplog): + """Test async_parse_date_datetime.""" + entity_id = "sensor.timestamp" + device_class = SensorDeviceClass.TIMESTAMP + assert ( + async_parse_date_datetime( + "2021-12-12 12:12Z", entity_id, device_class + ).isoformat() + == "2021-12-12T12:12:00+00:00" + ) + assert not caplog.text + + # No timezone + assert ( + async_parse_date_datetime("2021-12-12 12:12", entity_id, device_class) is None + ) + assert "sensor.timestamp rendered timestamp without timezone" in caplog.text + + # Invalid timestamp + assert async_parse_date_datetime("12 past 12", entity_id, device_class) is None + assert "sensor.timestamp rendered invalid timestamp: 12 past 12" in caplog.text + + device_class = SensorDeviceClass.DATE + caplog.clear() + assert ( + async_parse_date_datetime("2021-12-12", entity_id, device_class).isoformat() + == "2021-12-12" + ) + assert not caplog.text + + # Invalid date + assert async_parse_date_datetime("December 12th", entity_id, device_class) is None + assert "sensor.timestamp rendered invalid date December 12th" in caplog.text diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 67f750ece96..d5deee41679 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -1,4 +1,4 @@ -"""The test for sensor device automation.""" +"""The test for sensor entity.""" from datetime import date, datetime, timezone import pytest From dc5888ab4af85bf62d01a8dcd891a400232d6a99 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 10 Dec 2021 19:09:29 +0100 Subject: [PATCH 0306/2644] Correct recorder.statistics.get_last_statistics (#61421) --- .../components/recorder/statistics.py | 43 +++++++++++++++---- homeassistant/components/sensor/recorder.py | 4 +- tests/components/recorder/test_statistics.py | 33 ++++++++++---- 3 files changed, 62 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 2b60e6fbf00..02c00722e72 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -834,8 +834,12 @@ def statistics_during_period( return _reduce_statistics_per_month(result) -def get_last_statistics( - hass: HomeAssistant, number_of_stats: int, statistic_id: str, convert_units: bool +def _get_last_statistics( + hass: HomeAssistant, + number_of_stats: int, + statistic_id: str, + convert_units: bool, + table: type[Statistics | StatisticsShortTerm], ) -> dict[str, list[dict]]: """Return the last number_of_stats statistics for a given statistic_id.""" statistic_ids = [statistic_id] @@ -845,16 +849,19 @@ def get_last_statistics( if not metadata: return {} - baked_query = hass.data[STATISTICS_SHORT_TERM_BAKERY]( - lambda session: session.query(*QUERY_STATISTICS_SHORT_TERM) - ) + if table == StatisticsShortTerm: + bakery = STATISTICS_SHORT_TERM_BAKERY + base_query = QUERY_STATISTICS_SHORT_TERM + else: + bakery = STATISTICS_BAKERY + base_query = QUERY_STATISTICS + + baked_query = hass.data[bakery](lambda session: session.query(*base_query)) baked_query += lambda q: q.filter_by(metadata_id=bindparam("metadata_id")) metadata_id = metadata[statistic_id][0] - baked_query += lambda q: q.order_by( - StatisticsShortTerm.metadata_id, StatisticsShortTerm.start.desc() - ) + baked_query += lambda q: q.order_by(table.metadata_id, table.start.desc()) baked_query += lambda q: q.limit(bindparam("number_of_stats")) @@ -874,11 +881,29 @@ def get_last_statistics( statistic_ids, metadata, convert_units, - StatisticsShortTerm, + table, None, ) +def get_last_statistics( + hass: HomeAssistant, number_of_stats: int, statistic_id: str, convert_units: bool +) -> dict[str, list[dict]]: + """Return the last number_of_stats statistics for a statistic_id.""" + return _get_last_statistics( + hass, number_of_stats, statistic_id, convert_units, Statistics + ) + + +def get_last_short_term_statistics( + hass: HomeAssistant, number_of_stats: int, statistic_id: str, convert_units: bool +) -> dict[str, list[dict]]: + """Return the last number_of_stats short term statistics for a statistic_id.""" + return _get_last_statistics( + hass, number_of_stats, statistic_id, convert_units, StatisticsShortTerm + ) + + def _statistics_at_time( session: scoped_session, metadata_ids: set[int], diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 25cb81ded12..3cfe8d45b70 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -517,7 +517,9 @@ def _compile_statistics( # noqa: C901 last_reset = old_last_reset = None new_state = old_state = None _sum = 0.0 - last_stats = statistics.get_last_statistics(hass, 1, entity_id, False) + last_stats = statistics.get_last_short_term_statistics( + hass, 1, entity_id, False + ) if entity_id in last_stats: # We have compiled history for this sensor before, use that as a starting point last_reset = old_last_reset = last_stats[entity_id][0]["last_reset"] diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index d510d6ef612..c4dd33ce840 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -14,6 +14,7 @@ from homeassistant.components.recorder.models import ( ) from homeassistant.components.recorder.statistics import ( async_add_external_statistics, + get_last_short_term_statistics, get_last_statistics, get_metadata, list_statistic_ids, @@ -40,7 +41,7 @@ def test_compile_hourly_statistics(hass_recorder): for kwargs in ({}, {"statistic_ids": ["sensor.test1"]}): stats = statistics_during_period(hass, zero, period="5minute", **kwargs) assert stats == {} - stats = get_last_statistics(hass, 0, "sensor.test1", True) + stats = get_last_short_term_statistics(hass, 0, "sensor.test1", True) assert stats == {} recorder.do_adhoc_statistics(start=zero) @@ -91,20 +92,20 @@ def test_compile_hourly_statistics(hass_recorder): ) assert stats == {} - # Test get_last_statistics - stats = get_last_statistics(hass, 0, "sensor.test1", True) + # Test get_last_short_term_statistics + stats = get_last_short_term_statistics(hass, 0, "sensor.test1", True) assert stats == {} - stats = get_last_statistics(hass, 1, "sensor.test1", True) + stats = get_last_short_term_statistics(hass, 1, "sensor.test1", True) assert stats == {"sensor.test1": [{**expected_2, "statistic_id": "sensor.test1"}]} - stats = get_last_statistics(hass, 2, "sensor.test1", True) + stats = get_last_short_term_statistics(hass, 2, "sensor.test1", True) assert stats == {"sensor.test1": expected_stats1[::-1]} - stats = get_last_statistics(hass, 3, "sensor.test1", True) + stats = get_last_short_term_statistics(hass, 3, "sensor.test1", True) assert stats == {"sensor.test1": expected_stats1[::-1]} - stats = get_last_statistics(hass, 1, "sensor.test3", True) + stats = get_last_short_term_statistics(hass, 1, "sensor.test3", True) assert stats == {} @@ -236,7 +237,7 @@ def test_rename_entity(hass_recorder): for kwargs in ({}, {"statistic_ids": ["sensor.test1"]}): stats = statistics_during_period(hass, zero, period="5minute", **kwargs) assert stats == {} - stats = get_last_statistics(hass, 0, "sensor.test1", True) + stats = get_last_short_term_statistics(hass, 0, "sensor.test1", True) assert stats == {} recorder.do_adhoc_statistics(start=zero) @@ -392,6 +393,22 @@ def test_external_statistics(hass_recorder, caplog): }, ) } + last_stats = get_last_statistics(hass, 1, "test:total_energy_import", True) + assert last_stats == { + "test:total_energy_import": [ + { + "statistic_id": "test:total_energy_import", + "start": period2.isoformat(), + "end": (period2 + timedelta(hours=1)).isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(1.0), + "sum": approx(3.0), + }, + ] + } # Update the previously inserted statistics external_statistics = { From e5b04cedf3060e3555eae07c5d814e08568f694c Mon Sep 17 00:00:00 2001 From: Anton Malko Date: Fri, 10 Dec 2021 21:52:51 +0300 Subject: [PATCH 0307/2644] Add media_player platform to Lookin (#61337) Co-authored-by: J. Nick Koston --- .coveragerc | 1 + homeassistant/components/lookin/__init__.py | 16 +- homeassistant/components/lookin/climate.py | 31 ++- homeassistant/components/lookin/const.py | 4 +- homeassistant/components/lookin/entity.py | 52 +---- homeassistant/components/lookin/manifest.json | 2 +- .../components/lookin/media_player.py | 213 ++++++++++++++++++ homeassistant/components/lookin/sensor.py | 10 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 10 files changed, 264 insertions(+), 69 deletions(-) create mode 100644 homeassistant/components/lookin/media_player.py diff --git a/.coveragerc b/.coveragerc index 7ea4fa34a13..32a9d586c52 100644 --- a/.coveragerc +++ b/.coveragerc @@ -604,6 +604,7 @@ omit = homeassistant/components/lookin/models.py homeassistant/components/lookin/sensor.py homeassistant/components/lookin/climate.py + homeassistant/components/lookin/media_player.py homeassistant/components/luci/device_tracker.py homeassistant/components/luftdaten/__init__.py homeassistant/components/luftdaten/sensor.py diff --git a/homeassistant/components/lookin/__init__.py b/homeassistant/components/lookin/__init__.py index 5e603027a50..2dd58c0982b 100644 --- a/homeassistant/components/lookin/__init__.py +++ b/homeassistant/components/lookin/__init__.py @@ -10,9 +10,9 @@ from aiolookin import ( LookInHttpProtocol, LookinUDPSubscriptions, MeteoSensor, - SensorID, start_lookin_udp, ) +from aiolookin.models import UDPCommandType, UDPEvent from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST @@ -53,22 +53,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await meteo_coordinator.async_config_entry_first_refresh() @callback - def _async_meteo_push_update(msg: dict[str, str]) -> None: + def _async_meteo_push_update(event: UDPEvent) -> None: """Process an update pushed via UDP.""" - if int(msg["event_id"]): - return - LOGGER.debug("Processing push message for meteo sensor: %s", msg) + LOGGER.debug("Processing push message for meteo sensor: %s", event) meteo: MeteoSensor = meteo_coordinator.data - meteo.update_from_value(msg["value"]) + meteo.update_from_value(event.value) meteo_coordinator.async_set_updated_data(meteo) lookin_udp_subs = LookinUDPSubscriptions() entry.async_on_unload( - lookin_udp_subs.subscribe_sensor( - lookin_device.id, SensorID.Meteo, None, _async_meteo_push_update + lookin_udp_subs.subscribe_event( + lookin_device.id, UDPCommandType.meteo, None, _async_meteo_push_update ) ) - entry.async_on_unload(await start_lookin_udp(lookin_udp_subs)) + entry.async_on_unload(await start_lookin_udp(lookin_udp_subs, lookin_device.id)) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = LookinData( lookin_udp_subs=lookin_udp_subs, diff --git a/homeassistant/components/lookin/climate.py b/homeassistant/components/lookin/climate.py index 356b57453bc..74e888745b6 100644 --- a/homeassistant/components/lookin/climate.py +++ b/homeassistant/components/lookin/climate.py @@ -6,7 +6,8 @@ from datetime import timedelta import logging from typing import Any, Final, cast -from aiolookin import Climate, MeteoSensor, SensorID +from aiolookin import Climate, MeteoSensor +from aiolookin.models import UDPCommandType, UDPEvent from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -116,6 +117,7 @@ async def async_setup_entry( class ConditionerEntity(LookinCoordinatorEntity, ClimateEntity): """An aircon or heat pump.""" + _attr_current_humidity: float | None = None # type: ignore _attr_temperature_unit = TEMP_CELSIUS _attr_supported_features: int = SUPPORT_FLAGS _attr_fan_modes: list[str] = LOOKIN_FAN_MODE_IDX_TO_HASS @@ -198,6 +200,7 @@ class ConditionerEntity(LookinCoordinatorEntity, ClimateEntity): def _async_update_from_data(self) -> None: """Update attrs from data.""" meteo_data: MeteoSensor = self._meteo_coordinator.data + self._attr_current_temperature = meteo_data.temperature self._attr_current_humidity = int(meteo_data.humidity) self._attr_target_temperature = self._climate.temp_celsius @@ -205,6 +208,12 @@ class ConditionerEntity(LookinCoordinatorEntity, ClimateEntity): self._attr_swing_mode = LOOKIN_SWING_MODE_IDX_TO_HASS[self._climate.swing_mode] self._attr_hvac_mode = LOOKIN_HVAC_MODE_IDX_TO_HASS[self._climate.hvac_mode] + @callback + def _async_update_meteo_from_value(self, event: UDPEvent) -> None: + """Update temperature and humidity from UDP event.""" + self._attr_current_temperature = float(int(event.value[:4], 16)) / 10 + self._attr_current_humidity = float(int(event.value[-4:], 16)) / 10 + @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" @@ -212,20 +221,28 @@ class ConditionerEntity(LookinCoordinatorEntity, ClimateEntity): super()._handle_coordinator_update() @callback - def _async_push_update(self, msg: dict[str, str]) -> None: + def _async_push_update(self, event: UDPEvent) -> None: """Process an update pushed via UDP.""" - LOGGER.debug("Processing push message for %s: %s", self.entity_id, msg) - self._climate.update_from_status(msg["value"]) + LOGGER.debug("Processing push message for %s: %s", self.entity_id, event) + self._climate.update_from_status(event.value) self.coordinator.async_set_updated_data(self._climate) async def async_added_to_hass(self) -> None: """Call when the entity is added to hass.""" self.async_on_remove( - self._lookin_udp_subs.subscribe_sensor( - self._lookin_device.id, SensorID.IR, self._uuid, self._async_push_update + self._lookin_udp_subs.subscribe_event( + self._lookin_device.id, + UDPCommandType.ir, + self._uuid, + self._async_push_update, ) ) self.async_on_remove( - self._meteo_coordinator.async_add_listener(self._handle_coordinator_update) + self._lookin_udp_subs.subscribe_event( + self._lookin_device.id, + UDPCommandType.meteo, + None, + self._async_update_meteo_from_value, + ) ) return await super().async_added_to_hass() diff --git a/homeassistant/components/lookin/const.py b/homeassistant/components/lookin/const.py index d9b1141aa97..c919dc30a79 100644 --- a/homeassistant/components/lookin/const.py +++ b/homeassistant/components/lookin/const.py @@ -5,5 +5,7 @@ from typing import Final from homeassistant.const import Platform +MODEL_NAMES: Final = ["LOOKin Remote", "LOOKin Remote", "LOOKin Remote2"] + DOMAIN: Final = "lookin" -PLATFORMS: Final = [Platform.CLIMATE, Platform.SENSOR] +PLATFORMS: Final = [Platform.CLIMATE, Platform.MEDIA_PLAYER, Platform.SENSOR] diff --git a/homeassistant/components/lookin/entity.py b/homeassistant/components/lookin/entity.py index ad532889771..c444407d5ae 100644 --- a/homeassistant/components/lookin/entity.py +++ b/homeassistant/components/lookin/entity.py @@ -4,13 +4,13 @@ from __future__ import annotations from aiolookin import POWER_CMD, POWER_OFF_CMD, POWER_ON_CMD, Climate, Remote from aiolookin.models import Device -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, ) -from .const import DOMAIN +from .const import DOMAIN, MODEL_NAMES from .models import LookinData @@ -20,7 +20,7 @@ def _lookin_device_to_device_info(lookin_device: Device) -> DeviceInfo: identifiers={(DOMAIN, lookin_device.id)}, name=lookin_device.name, manufacturer="LOOKin", - model="LOOKin Remote2", + model=MODEL_NAMES[lookin_device.model], sw_version=lookin_device.firmware, ) @@ -46,19 +46,6 @@ class LookinDeviceMixIn: self._lookin_udp_subs = lookin_data.lookin_udp_subs -class LookinDeviceEntity(LookinDeviceMixIn, Entity): - """A lookin device entity on the device itself.""" - - _attr_should_poll = False - - def __init__(self, lookin_data: LookinData) -> None: - """Init the lookin device entity.""" - self._set_lookin_device_attrs(lookin_data) - self._attr_device_info = _lookin_device_to_device_info( - lookin_data.lookin_device - ) - - class LookinDeviceCoordinatorEntity(LookinDeviceMixIn, CoordinatorEntity): """A lookin device entity on the device itself that uses the coordinator.""" @@ -89,34 +76,6 @@ class LookinEntityMixIn: self._function_names = {function.name for function in self._device.functions} -class LookinEntity(LookinDeviceMixIn, LookinEntityMixIn, Entity): - """A base class for lookin entities.""" - - _attr_should_poll = False - _attr_assumed_state = True - - def __init__( - self, - uuid: str, - device: Remote | Climate, - lookin_data: LookinData, - ) -> None: - """Init the base entity.""" - self._set_lookin_device_attrs(lookin_data) - self._set_lookin_entity_attrs(uuid, device, lookin_data) - self._attr_device_info = _lookin_controlled_device_to_device_info( - self._lookin_device, uuid, device - ) - self._attr_unique_id = uuid - self._attr_name = device.name - - async def _async_send_command(self, command: str) -> None: - """Send command from saved IR device.""" - await self._lookin_protocol.send_command( - uuid=self._uuid, command=command, signal="FF" - ) - - class LookinCoordinatorEntity(LookinDeviceMixIn, LookinEntityMixIn, CoordinatorEntity): """A lookin device entity for an external device that uses the coordinator.""" @@ -147,17 +106,18 @@ class LookinCoordinatorEntity(LookinDeviceMixIn, LookinEntityMixIn, CoordinatorE ) -class LookinPowerEntity(LookinEntity): +class LookinPowerEntity(LookinCoordinatorEntity): """A Lookin entity that has a power on and power off command.""" def __init__( self, + coordinator: DataUpdateCoordinator, uuid: str, device: Remote | Climate, lookin_data: LookinData, ) -> None: """Init the power entity.""" - super().__init__(uuid, device, lookin_data) + super().__init__(coordinator, uuid, device, lookin_data) self._power_on_command: str = POWER_CMD self._power_off_command: str = POWER_CMD if POWER_ON_CMD in self._function_names: diff --git a/homeassistant/components/lookin/manifest.json b/homeassistant/components/lookin/manifest.json index 7260985654a..903f6b84d50 100644 --- a/homeassistant/components/lookin/manifest.json +++ b/homeassistant/components/lookin/manifest.json @@ -3,7 +3,7 @@ "name": "LOOKin", "documentation": "https://www.home-assistant.io/integrations/lookin/", "codeowners": ["@ANMalko"], - "requirements": ["aiolookin==0.0.4"], + "requirements": ["aiolookin==0.1.0"], "zeroconf": ["_lookin._tcp.local."], "config_flow": true, "iot_class": "local_push" diff --git a/homeassistant/components/lookin/media_player.py b/homeassistant/components/lookin/media_player.py new file mode 100644 index 00000000000..9aa105e8f87 --- /dev/null +++ b/homeassistant/components/lookin/media_player.py @@ -0,0 +1,213 @@ +"""The lookin integration light platform.""" +from __future__ import annotations + +from collections.abc import Callable, Coroutine +from datetime import timedelta +import logging +from typing import Any, cast + +from aiolookin import Remote +from aiolookin.models import UDPCommandType, UDPEvent + +from homeassistant.components.media_player import ( + DEVICE_CLASS_RECEIVER, + DEVICE_CLASS_TV, + MediaPlayerEntity, +) +from homeassistant.components.media_player.const import ( + SUPPORT_NEXT_TRACK, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_STEP, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_ON, STATE_STANDBY +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN +from .entity import LookinPowerEntity +from .models import LookinData + +LOGGER = logging.getLogger(__name__) + +_TYPE_TO_DEVICE_CLASS = {"01": DEVICE_CLASS_TV, "02": DEVICE_CLASS_RECEIVER} + +_FUNCTION_NAME_TO_FEATURE = { + "power": SUPPORT_TURN_OFF, + "poweron": SUPPORT_TURN_ON, + "poweroff": SUPPORT_TURN_OFF, + "mute": SUPPORT_VOLUME_MUTE, + "volup": SUPPORT_VOLUME_STEP, + "chup": SUPPORT_NEXT_TRACK, + "chdown": SUPPORT_PREVIOUS_TRACK, +} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the media_player platform for lookin from a config entry.""" + lookin_data: LookinData = hass.data[DOMAIN][config_entry.entry_id] + entities = [] + + for remote in lookin_data.devices: + if remote["Type"] not in _TYPE_TO_DEVICE_CLASS: + continue + uuid = remote["UUID"] + + def _wrap_async_update( + uuid: str, + ) -> Callable[[], Coroutine[None, Any, Remote]]: + """Create a function to capture the uuid cell variable.""" + + async def _async_update() -> Remote: + return await lookin_data.lookin_protocol.get_remote(uuid) + + return _async_update + + coordinator = DataUpdateCoordinator( + hass, + LOGGER, + name=f"{config_entry.title} {uuid}", + update_method=_wrap_async_update(uuid), + update_interval=timedelta( + seconds=60 + ), # Updates are pushed (fallback is polling) + ) + await coordinator.async_refresh() + device: Remote = coordinator.data + + entities.append( + LookinMedia( + uuid=uuid, + device=device, + lookin_data=lookin_data, + device_class=_TYPE_TO_DEVICE_CLASS[remote["Type"]], + coordinator=coordinator, + ) + ) + + async_add_entities(entities) + + +class LookinMedia(LookinPowerEntity, MediaPlayerEntity): + """A lookin media player.""" + + _attr_should_poll = False + + def __init__( + self, + uuid: str, + device: Remote, + lookin_data: LookinData, + device_class: str, + coordinator: DataUpdateCoordinator, + ) -> None: + """Init the lookin media player.""" + self._attr_device_class = device_class + self._attr_supported_features: int = 0 + self._attr_state = None + self._attr_is_volume_muted: bool = False + super().__init__(coordinator, uuid, device, lookin_data) + for function_name, feature in _FUNCTION_NAME_TO_FEATURE.items(): + if function_name in self._function_names: + self._attr_supported_features |= feature + self._attr_name = self._remote.name + self._async_update_from_data() + + @property + def _remote(self) -> Remote: + return cast(Remote, self.coordinator.data) + + async def async_volume_up(self) -> None: + """Turn volume up for media player.""" + await self._async_send_command("volup") + + async def async_volume_down(self) -> None: + """Turn volume down for media player.""" + await self._async_send_command("voldown") + + async def async_media_previous_track(self) -> None: + """Send previous track command.""" + await self._async_send_command("chdown") + + async def async_media_next_track(self) -> None: + """Send next track command.""" + await self._async_send_command("chup") + + async def async_mute_volume(self, mute: bool) -> None: + """Mute the volume.""" + await self._async_send_command("mute") + self._attr_is_volume_muted = not self.is_volume_muted + self.async_write_ha_state() + + async def async_turn_off(self) -> None: + """Turn the media player off.""" + await self._async_send_command(self._power_off_command) + self._attr_state = STATE_STANDBY + self.async_write_ha_state() + + async def async_turn_on(self) -> None: + """Turn the media player on.""" + await self._async_send_command(self._power_on_command) + self._attr_state = STATE_ON + self.async_write_ha_state() + + def _update_from_status(self, status: str) -> None: + """Update media property from status. + + 00F0 + 0 - 0/1 on/off + 0 - sourse + F - volume, 0 - muted, 1 - volume up, F - volume down + 0 - not used + """ + if len(status) != 4: + return + state = status[0] + mute = status[2] + + self._attr_state = STATE_STANDBY if state == "1" else STATE_ON + self._attr_is_volume_muted = mute == "0" + + def _async_push_update(self, event: UDPEvent) -> None: + """Process an update pushed via UDP.""" + LOGGER.debug("Processing push message for %s: %s", self.entity_id, event) + self._update_from_status(event.value) + self.coordinator.async_set_updated_data(self._remote) + self.async_write_ha_state() + + async def _async_push_update_device(self, event: UDPEvent) -> None: + """Process an update pushed via UDP.""" + LOGGER.debug("Processing push message for %s: %s", self.entity_id, event) + await self.coordinator.async_refresh() + self._attr_name = self._remote.name + + async def async_added_to_hass(self) -> None: + """Call when the entity is added to hass.""" + self.async_on_remove( + self._lookin_udp_subs.subscribe_event( + self._lookin_device.id, + UDPCommandType.ir, + self._uuid, + self._async_push_update, + ) + ) + self.async_on_remove( + self._lookin_udp_subs.subscribe_event( + self._lookin_device.id, + UDPCommandType.data, + self._uuid, + self._async_push_update_device, + ) + ) + + def _async_update_from_data(self) -> None: + """Update attrs from data.""" + self._update_from_status(self._remote.status) diff --git a/homeassistant/components/lookin/sensor.py b/homeassistant/components/lookin/sensor.py index b320f5d537a..7b3972b7ce3 100644 --- a/homeassistant/components/lookin/sensor.py +++ b/homeassistant/components/lookin/sensor.py @@ -48,9 +48,13 @@ async def async_setup_entry( """Set up lookin sensors from the config entry.""" lookin_data: LookinData = hass.data[DOMAIN][config_entry.entry_id] - async_add_entities( - [LookinSensorEntity(description, lookin_data) for description in SENSOR_TYPES] - ) + if lookin_data.lookin_device.model >= 2: + async_add_entities( + [ + LookinSensorEntity(description, lookin_data) + for description in SENSOR_TYPES + ] + ) class LookinSensorEntity(LookinDeviceCoordinatorEntity, SensorEntity): diff --git a/requirements_all.txt b/requirements_all.txt index a2c093cb4e5..117cedd7e69 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -213,7 +213,7 @@ aiolifx_effects==0.2.2 aiolip==1.1.6 # homeassistant.components.lookin -aiolookin==0.0.4 +aiolookin==0.1.0 # homeassistant.components.lyric aiolyric==1.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9fe243edb5b..2f292840940 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -146,7 +146,7 @@ aiokafka==0.6.0 aiolip==1.1.6 # homeassistant.components.lookin -aiolookin==0.0.4 +aiolookin==0.1.0 # homeassistant.components.lyric aiolyric==1.0.8 From d73311075fbbbe5a886eaf1e7568e8e5387f0583 Mon Sep 17 00:00:00 2001 From: MattWestb <49618193+MattWestb@users.noreply.github.com> Date: Fri, 10 Dec 2021 20:07:53 +0100 Subject: [PATCH 0308/2644] Add 2 new CN-Hysen TRVs (#61002) Adding CN-Hysen "_TZE200_pvvbommb" and "_TZE200_4eeyebrt" TRVs --- homeassistant/components/zha/climate.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index 9ef7e8fdebc..f7a1d1815db 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -614,6 +614,8 @@ class CentralitePearl(ZenWithinThermostat): "_TZE200_cwnjrr72", "_TZE200_b6wax7g0", "_TZE200_2atgpdho", + "_TZE200_pvvbommb", + "_TZE200_4eeyebrt", "_TYST11_ckud7u2l", "_TYST11_ywdxldoj", "_TYST11_cwnjrr72", From d4e5c1832ec8b4191a512d2ab2ab60822259563b Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 10 Dec 2021 14:29:46 -0500 Subject: [PATCH 0309/2644] Bump ZHA quirks to 0.0.65 (#61458) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index daeb90be801..960bb55e004 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,7 +7,7 @@ "bellows==0.29.0", "pyserial==3.5", "pyserial-asyncio==0.5", - "zha-quirks==0.0.64", + "zha-quirks==0.0.65", "zigpy-deconz==0.14.0", "zigpy==0.42.0", "zigpy-xbee==0.14.0", diff --git a/requirements_all.txt b/requirements_all.txt index 117cedd7e69..721bfa3a0ae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2495,7 +2495,7 @@ zengge==0.2 zeroconf==0.37.0 # homeassistant.components.zha -zha-quirks==0.0.64 +zha-quirks==0.0.65 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2f292840940..c543bba974e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1488,7 +1488,7 @@ youless-api==0.15 zeroconf==0.37.0 # homeassistant.components.zha -zha-quirks==0.0.64 +zha-quirks==0.0.65 # homeassistant.components.zha zigpy-deconz==0.14.0 From 6d542613226def75fc90f9b9b6f9f900d660fad0 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 10 Dec 2021 15:28:47 -0600 Subject: [PATCH 0310/2644] Remove external library discovery call in Sonos (#61461) Co-authored-by: J. Nick Koston --- homeassistant/components/sonos/config_flow.py | 11 +-- tests/components/sonos/test_config_flow.py | 90 +++++++++++++++++-- tests/components/sonos/test_init.py | 26 ++++-- 3 files changed, 107 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/sonos/config_flow.py b/homeassistant/components/sonos/config_flow.py index e6d2bb337b8..07add8e6d7c 100644 --- a/homeassistant/components/sonos/config_flow.py +++ b/homeassistant/components/sonos/config_flow.py @@ -1,22 +1,19 @@ """Config flow for SONOS.""" import dataclasses -import soco - from homeassistant import config_entries -from homeassistant.components import zeroconf +from homeassistant.components import ssdp, zeroconf from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.config_entry_flow import DiscoveryFlowHandler -from .const import DATA_SONOS_DISCOVERY_MANAGER, DOMAIN +from .const import DATA_SONOS_DISCOVERY_MANAGER, DOMAIN, UPNP_ST from .helpers import hostname_to_uid async def _async_has_devices(hass: HomeAssistant) -> bool: - """Return if there are devices that can be discovered.""" - result = await hass.async_add_executor_job(soco.discover) - return bool(result) + """Return if Sonos devices have been seen recently with SSDP.""" + return bool(await ssdp.async_get_discovery_info_by_st(hass, UPNP_ST)) class SonosDiscoveryFlowHandler(DiscoveryFlowHandler): diff --git a/tests/components/sonos/test_config_flow.py b/tests/components/sonos/test_config_flow.py index eee644abeba..aa2ce6cc0be 100644 --- a/tests/components/sonos/test_config_flow.py +++ b/tests/components/sonos/test_config_flow.py @@ -4,14 +4,36 @@ from __future__ import annotations from unittest.mock import MagicMock, patch from homeassistant import config_entries, core -from homeassistant.components import zeroconf +from homeassistant.components import ssdp, zeroconf +from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.sonos.const import DATA_SONOS_DISCOVERY_MANAGER, DOMAIN +from homeassistant.const import CONF_HOSTS +from homeassistant.setup import async_setup_component -@patch("homeassistant.components.sonos.config_flow.soco.discover", return_value=True) -async def test_user_form(discover_mock: MagicMock, hass: core.HomeAssistant): +async def test_user_form( + hass: core.HomeAssistant, zeroconf_payload: zeroconf.ZeroconfServiceInfo +): """Test we get the user initiated form.""" + # Ensure config flow will fail if no devices discovered yet + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == "abort" + assert result["reason"] == "no_devices_found" + + # Initiate a discovery to allow config entry creation + await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf_payload, + ) + + # Ensure config flow succeeds after discovery result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -37,7 +59,22 @@ async def test_user_form(discover_mock: MagicMock, hass: core.HomeAssistant): assert len(mock_setup_entry.mock_calls) == 1 -async def test_zeroconf_form(hass: core.HomeAssistant, zeroconf_payload): +async def test_user_form_already_created(hass: core.HomeAssistant): + """Ensure we abort a flow if the entry is already created from config.""" + config = {DOMAIN: {MP_DOMAIN: {CONF_HOSTS: "192.168.4.2"}}} + await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "abort" + assert result["reason"] == "single_instance_allowed" + + +async def test_zeroconf_form( + hass: core.HomeAssistant, zeroconf_payload: zeroconf.ZeroconfServiceInfo +): """Test we pass Zeroconf discoveries to the manager.""" mock_manager = hass.data[DATA_SONOS_DISCOVERY_MANAGER] = MagicMock() @@ -71,6 +108,47 @@ async def test_zeroconf_form(hass: core.HomeAssistant, zeroconf_payload): assert len(mock_manager.mock_calls) == 2 +async def test_ssdp_discovery(hass: core.HomeAssistant, soco): + """Test that SSDP discoveries create a config flow.""" + + await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=ssdp.SsdpServiceInfo( + ssdp_location=f"http://{soco.ip_address}/", + ssdp_st="urn:schemas-upnp-org:device:ZonePlayer:1", + ssdp_usn=f"uuid:{soco.uid}_MR::urn:schemas-upnp-org:service:GroupRenderingControl:1", + upnp={ + ssdp.ATTR_UPNP_UDN: f"uuid:{soco.uid}", + }, + ), + ) + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + flow = flows[0] + + with patch( + "homeassistant.components.sonos.async_setup", + return_value=True, + ) as mock_setup, patch( + "homeassistant.components.sonos.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + flow["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert result["title"] == "Sonos" + assert result["data"] == {} + + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_zeroconf_sonos_v1(hass: core.HomeAssistant): """Test we pass sonos devices to the discovery manager with v1 firmware devices.""" @@ -121,7 +199,9 @@ async def test_zeroconf_sonos_v1(hass: core.HomeAssistant): assert len(mock_manager.mock_calls) == 2 -async def test_zeroconf_form_not_sonos(hass: core.HomeAssistant, zeroconf_payload): +async def test_zeroconf_form_not_sonos( + hass: core.HomeAssistant, zeroconf_payload: zeroconf.ZeroconfServiceInfo +): """Test we abort on non-sonos devices.""" mock_manager = hass.data[DATA_SONOS_DISCOVERY_MANAGER] = MagicMock() diff --git a/tests/components/sonos/test_init.py b/tests/components/sonos/test_init.py index 71f89f6d880..02897c523c1 100644 --- a/tests/components/sonos/test_init.py +++ b/tests/components/sonos/test_init.py @@ -1,16 +1,26 @@ """Tests for the Sonos config flow.""" from unittest.mock import patch -from homeassistant import config_entries, data_entry_flow -from homeassistant.components import sonos +from homeassistant import config_entries, core, data_entry_flow +from homeassistant.components import sonos, zeroconf from homeassistant.setup import async_setup_component -async def test_creating_entry_sets_up_media_player(hass): +async def test_creating_entry_sets_up_media_player( + hass: core.HomeAssistant, zeroconf_payload: zeroconf.ZeroconfServiceInfo +): """Test setting up Sonos loads the media player.""" + + # Initiate a discovery to allow a user config flow + await hass.config_entries.flow.async_init( + sonos.DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf_payload, + ) + with patch( "homeassistant.components.sonos.media_player.async_setup_entry", - ) as mock_setup, patch("soco.discover", return_value=True): + ) as mock_setup: result = await hass.config_entries.flow.async_init( sonos.DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -26,12 +36,12 @@ async def test_creating_entry_sets_up_media_player(hass): assert len(mock_setup.mock_calls) == 1 -async def test_configuring_sonos_creates_entry(hass): +async def test_configuring_sonos_creates_entry(hass: core.HomeAssistant): """Test that specifying config will create an entry.""" with patch( "homeassistant.components.sonos.async_setup_entry", return_value=True, - ) as mock_setup, patch("soco.discover", return_value=True): + ) as mock_setup: await async_setup_component( hass, sonos.DOMAIN, @@ -42,12 +52,12 @@ async def test_configuring_sonos_creates_entry(hass): assert len(mock_setup.mock_calls) == 1 -async def test_not_configuring_sonos_not_creates_entry(hass): +async def test_not_configuring_sonos_not_creates_entry(hass: core.HomeAssistant): """Test that no config will not create an entry.""" with patch( "homeassistant.components.sonos.async_setup_entry", return_value=True, - ) as mock_setup, patch("soco.discover", return_value=True): + ) as mock_setup: await async_setup_component(hass, sonos.DOMAIN, {}) await hass.async_block_till_done() From 0d36b07d10e9010391bfa734399f531741cb1c37 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sat, 11 Dec 2021 00:11:34 +0100 Subject: [PATCH 0311/2644] Small fix for device triggers and events on Hue integration (#61462) --- .../components/hue/v2/device_trigger.py | 20 +++++++++++++++++-- .../components/hue/test_device_trigger_v2.py | 14 ++++++++++--- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/hue/v2/device_trigger.py b/homeassistant/components/hue/v2/device_trigger.py index b33b7540cb8..7a194bef746 100644 --- a/homeassistant/components/hue/v2/device_trigger.py +++ b/homeassistant/components/hue/v2/device_trigger.py @@ -40,6 +40,19 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( } ) +DEFAULT_BUTTON_EVENT_TYPES = ( + # all except `DOUBLE_SHORT_RELEASE` + ButtonEvent.INITIAL_PRESS, + ButtonEvent.REPEAT, + ButtonEvent.SHORT_RELEASE, + ButtonEvent.LONG_RELEASE, +) + +DEVICE_SPECIFIC_EVENT_TYPES = { + # device specific overrides of specific supported button events + "Hue tap switch": (ButtonEvent.INITIAL_PRESS,), +} + async def async_validate_trigger_config( bridge: "HueBridge", @@ -84,10 +97,13 @@ async def async_get_triggers(bridge: "HueBridge", device_entry: DeviceEntry): hue_dev_id = get_hue_device_id(device_entry) # extract triggers from all button resources of this Hue device triggers = [] + model_id = api.devices[hue_dev_id].product_data.product_name for resource in api.devices.get_sensors(hue_dev_id): if resource.type != ResourceTypes.BUTTON: continue - for event_type in (x.value for x in ButtonEvent if x != ButtonEvent.UNKNOWN): + for event_type in DEVICE_SPECIFIC_EVENT_TYPES.get( + model_id, DEFAULT_BUTTON_EVENT_TYPES + ): triggers.append( { CONF_DEVICE_ID: device_entry.id, @@ -95,7 +111,7 @@ async def async_get_triggers(bridge: "HueBridge", device_entry: DeviceEntry): CONF_PLATFORM: "device", CONF_TYPE: event_type, CONF_SUBTYPE: resource.metadata.control_id, - CONF_UNIQUE_ID: device_entry.id, + CONF_UNIQUE_ID: resource.id, } ) return triggers diff --git a/tests/components/hue/test_device_trigger_v2.py b/tests/components/hue/test_device_trigger_v2.py index bda963552c7..e155b0adb6d 100644 --- a/tests/components/hue/test_device_trigger_v2.py +++ b/tests/components/hue/test_device_trigger_v2.py @@ -70,12 +70,20 @@ async def test_get_triggers(hass, mock_bridge_v2, v2_resources_test_data, device "platform": "device", "domain": hue.DOMAIN, "device_id": hue_wall_switch_device.id, - "unique_id": hue_wall_switch_device.id, + "unique_id": resource_id, "type": event_type, "subtype": control_id, } - for event_type in (x.value for x in ButtonEvent if x != ButtonEvent.UNKNOWN) - for control_id in range(1, 3) + for event_type in ( + ButtonEvent.INITIAL_PRESS, + ButtonEvent.LONG_RELEASE, + ButtonEvent.REPEAT, + ButtonEvent.SHORT_RELEASE, + ) + for control_id, resource_id in ( + (1, "c658d3d8-a013-4b81-8ac6-78b248537e70"), + (2, "be1eb834-bdf5-4d26-8fba-7b1feaa83a9d"), + ) ), ] From 5e750a06258a74cedab5d20de30a588d995c1b15 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 11 Dec 2021 00:12:44 +0000 Subject: [PATCH 0312/2644] [ci skip] Translation update --- homeassistant/components/climate/translations/hu.json | 2 +- homeassistant/components/coolmaster/translations/hu.json | 2 +- homeassistant/components/simplisafe/translations/ca.json | 3 ++- homeassistant/components/simplisafe/translations/de.json | 3 ++- homeassistant/components/simplisafe/translations/et.json | 3 ++- homeassistant/components/simplisafe/translations/hu.json | 3 ++- homeassistant/components/simplisafe/translations/ja.json | 1 + homeassistant/components/simplisafe/translations/no.json | 1 + homeassistant/components/simplisafe/translations/ru.json | 3 ++- homeassistant/components/simplisafe/translations/tr.json | 3 ++- homeassistant/components/simplisafe/translations/zh-Hant.json | 1 + 11 files changed, 17 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/climate/translations/hu.json b/homeassistant/components/climate/translations/hu.json index a31792fc016..f0fe007c7c5 100644 --- a/homeassistant/components/climate/translations/hu.json +++ b/homeassistant/components/climate/translations/hu.json @@ -18,7 +18,7 @@ "_": { "auto": "Automatikus", "cool": "H\u0171t\u00e9s", - "dry": "Sz\u00e1raz", + "dry": "P\u00e1r\u00e1tlan\u00edt\u00e1s", "fan_only": "Csak ventil\u00e1tor", "heat": "F\u0171t\u00e9s", "heat_cool": "F\u0171t\u00e9s/H\u0171t\u00e9s", diff --git a/homeassistant/components/coolmaster/translations/hu.json b/homeassistant/components/coolmaster/translations/hu.json index 5f6f2eb2824..3770d56ee89 100644 --- a/homeassistant/components/coolmaster/translations/hu.json +++ b/homeassistant/components/coolmaster/translations/hu.json @@ -8,7 +8,7 @@ "user": { "data": { "cool": "T\u00e1mogatott a h\u0171t\u00e9si m\u00f3d(ok)", - "dry": "T\u00e1mogassa a p\u00e1r\u00e1tlan\u00edt\u00f3 m\u00f3d(ok)", + "dry": "P\u00e1r\u00e1tlan\u00edt\u00e1s ", "fan_only": "T\u00e1mogaott csak ventil\u00e1tor m\u00f3d(ok)", "heat": "T\u00e1mogatott f\u0171t\u00e9si m\u00f3d(ok)", "heat_cool": "T\u00e1mogatott f\u0171t\u00e9si/h\u0171t\u00e9si m\u00f3d(ok)", diff --git a/homeassistant/components/simplisafe/translations/ca.json b/homeassistant/components/simplisafe/translations/ca.json index 66dd5c6ddf9..3ab643b534f 100644 --- a/homeassistant/components/simplisafe/translations/ca.json +++ b/homeassistant/components/simplisafe/translations/ca.json @@ -32,11 +32,12 @@ }, "user": { "data": { + "auth_code": "Codi d'autoritzaci\u00f3", "code": "Codi (utilitzat a la UI de Home Assistant)", "password": "Contrasenya", "username": "Correu electr\u00f2nic" }, - "description": "A partir del 2021, SimpliSafe ha passat a un nou mecanisme d'autenticaci\u00f3 a trav\u00e9s de la seva aplicaci\u00f3 web. A causa de les limitacions t\u00e8cniques, hi ha un pas manual al final d'aquest proc\u00e9s; assegura't de llegir la [documentaci\u00f3](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) abans de comen\u00e7ar. \n\nQuan estiguis a punt, fes clic a [aqu\u00ed]({url}) per obrir l'aplicaci\u00f3 web SimpliSafe i introduir les credencials. Quan el proc\u00e9s s'hagi completat, torna aqu\u00ed i fes clic a Envia.", + "description": "SimpliSafe s'autentica amb Home Assistant a trav\u00e9s de la seva aplicaci\u00f3 web. A causa de les limitacions t\u00e8cniques, hi ha un pas manual al final d'aquest proc\u00e9s; assegura't de llegir la [documentaci\u00f3]({docs_url}) abans de comen\u00e7ar.\n\n1. Fes clic [aqu\u00ed]({url}) per obrir l'aplicaci\u00f3 web de SimpliSafe i introdueix les teves credencials.\n\n2. Quan el proc\u00e9s d'inici de sessi\u00f3 s'hagi completat, torna aqu\u00ed i introdueix a sota el codi d'autoritzaci\u00f3.", "title": "Introdueix la teva informaci\u00f3" } } diff --git a/homeassistant/components/simplisafe/translations/de.json b/homeassistant/components/simplisafe/translations/de.json index 5ccd44c8379..1ee4802e77f 100644 --- a/homeassistant/components/simplisafe/translations/de.json +++ b/homeassistant/components/simplisafe/translations/de.json @@ -32,11 +32,12 @@ }, "user": { "data": { + "auth_code": "Autorisierungscode", "code": "Code (wird in der Benutzeroberfl\u00e4che von Home Assistant verwendet)", "password": "Passwort", "username": "E-Mail" }, - "description": "Ab 2021 hat SimpliSafe auf einen neuen Authentifizierungsmechanismus \u00fcber seine Web-App umgestellt. Aufgrund technischer Einschr\u00e4nkungen gibt es am Ende dieses Prozesses einen manuellen Schritt; bitte stelle sicher, dass du die [Dokumentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) liest, bevor du beginnst.\n\nWenn du bereit bist, klicke [hier]({url}), um die SimpliSafe-Webanwendung zu \u00f6ffnen und deine Anmeldedaten einzugeben. Wenn der Vorgang abgeschlossen ist, kehre hierher zur\u00fcck und klicke auf Senden.", + "description": "SimpliSafe authentifiziert sich bei Home Assistant \u00fcber die SimpliSafe Web-App. Aufgrund technischer Beschr\u00e4nkungen gibt es am Ende dieses Prozesses einen manuellen Schritt; bitte stelle sicher, dass du die [Dokumentation]({docs_url}) liest, bevor du beginnst.\n\n1. Klicke [hier]({url}), um die SimpliSafe-Webanwendung zu \u00f6ffnen und deine Anmeldedaten einzugeben.\n\n2. Wenn der Anmeldevorgang abgeschlossen ist, kehre hierher zur\u00fcck und gib unten stehenden Autorisierungscode ein.", "title": "Gib deine Informationen ein" } } diff --git a/homeassistant/components/simplisafe/translations/et.json b/homeassistant/components/simplisafe/translations/et.json index 7bbba70281a..97e59415123 100644 --- a/homeassistant/components/simplisafe/translations/et.json +++ b/homeassistant/components/simplisafe/translations/et.json @@ -32,11 +32,12 @@ }, "user": { "data": { + "auth_code": "Tuvastuskood", "code": "Kood (kasutatakse Home Assistant'i kasutajaliideses)", "password": "Salas\u00f5na", "username": "E-post" }, - "description": "Alates 2021. aastast on SimpliSafe oma veebirakenduse kaudu \u00fcle l\u00e4inud uuele tuvastusmehhanismile. Tehniliste piirangute t\u00f5ttu on selle protsessi l\u00f5pus k\u00e4sitsi tehtud samm; palun loe enne alustamist l\u00e4bi [dokumentatsioon] (http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code). \n\n Kui oled valmis, kl\u00f5psa SimpliSafe'i veebirakenduse avamiseks ja oma mandaadi sisestamiseks {url} Kui protsess on l\u00f5pule j\u00f5udnud, naase siia ja kl\u00f5psa nuppu Esita.", + "description": "SimpliSafe autendib Home Assistantiga SimpliSafe'i veebirakenduse kaudu. Tehniliste piirangute t\u00f5ttu on selle protsessi l\u00f5pus k\u00e4sitsi samm; enne alustamist loe kindlasti [dokumentatsioon]( {docs_url} \n\n 1. SimpliSafe'i veebirakenduse avamiseks ja oma mandaatide sisestamiseks kl\u00f5psa [siin]( {url} \n\n 2. Kui sisselogimisprotsess on l\u00f5ppenud, naase siia ja sisesta alltoodud tuvastuskood.", "title": "Sisesta oma teave." } } diff --git a/homeassistant/components/simplisafe/translations/hu.json b/homeassistant/components/simplisafe/translations/hu.json index dc2b4ba7e1e..69881266c4f 100644 --- a/homeassistant/components/simplisafe/translations/hu.json +++ b/homeassistant/components/simplisafe/translations/hu.json @@ -32,11 +32,12 @@ }, "user": { "data": { + "auth_code": "Enged\u00e9lyez\u00e9si k\u00f3d", "code": "K\u00f3d (a Home Assistant felhaszn\u00e1l\u00f3i fel\u00fclet\u00e9n haszn\u00e1latos)", "password": "Jelsz\u00f3", "username": "E-mail" }, - "description": "2021-t\u0151l kezd\u0151d\u0151en a SimpliSafe egy \u00faj hiteles\u00edt\u00e9si mechanizmusra v\u00e1ltott a webalkalmaz\u00e1son kereszt\u00fcl. A technikai korl\u00e1toz\u00e1sok miatt a folyamat v\u00e9g\u00e9n van egy k\u00e9zi l\u00e9p\u00e9s; k\u00e9rj\u00fck, indul\u00e1s el\u0151tt olvassa el a [dokument\u00e1ci\u00f3t](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code).\n\nHa k\u00e9szen \u00e1ll, kattintson [ide]({url}) a SimpliSafe webalkalmaz\u00e1s megnyit\u00e1s\u00e1hoz \u00e9s a hiteles\u00edt\u0151 adatok bevitel\u00e9hez. Amikor a folyamat befejez\u0151d\u00f6tt, t\u00e9rjen vissza ide, \u00e9s kattintson a K\u00fcld\u00e9s gombra.", + "description": "A SimpliSafe rendszere webalkalmaz\u00e1son kereszt\u00fcl hiteles\u00edt\u00e9si mag\u00e1t. A technikai korl\u00e1toz\u00e1sok miatt a folyamat v\u00e9g\u00e9n van egy k\u00e9zi l\u00e9p\u00e9s; k\u00e9rj\u00fck, indul\u00e1s el\u0151tt olvassa el a [dokument\u00e1ci\u00f3t]({docs_url}).\n\n1. Ha k\u00e9szen \u00e1ll, kattintson [ide]({url}) a SimpliSafe webalkalmaz\u00e1s megnyit\u00e1s\u00e1hoz \u00e9s a hiteles\u00edt\u0151 adatok bevitel\u00e9hez. \n\n2. Amikor a folyamat befejez\u0151d\u00f6tt, t\u00e9rjen vissza ide, \u00e9s \u00edrja be a k\u00f3dot.", "title": "T\u00f6ltse ki az adatait" } } diff --git a/homeassistant/components/simplisafe/translations/ja.json b/homeassistant/components/simplisafe/translations/ja.json index 32e8e4f777e..925ae801a0d 100644 --- a/homeassistant/components/simplisafe/translations/ja.json +++ b/homeassistant/components/simplisafe/translations/ja.json @@ -32,6 +32,7 @@ }, "user": { "data": { + "auth_code": "\u8a8d\u8a3c\u30b3\u30fc\u30c9", "code": "\u30b3\u30fc\u30c9(Home Assistant UI\u3067\u4f7f\u7528)", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "E\u30e1\u30fc\u30eb" diff --git a/homeassistant/components/simplisafe/translations/no.json b/homeassistant/components/simplisafe/translations/no.json index 0b4d11029d7..8d9b8b5df50 100644 --- a/homeassistant/components/simplisafe/translations/no.json +++ b/homeassistant/components/simplisafe/translations/no.json @@ -32,6 +32,7 @@ }, "user": { "data": { + "auth_code": "Autorisasjonskode", "code": "Kode (brukt i Home Assistant brukergrensesnittet)", "password": "Passord", "username": "E-post" diff --git a/homeassistant/components/simplisafe/translations/ru.json b/homeassistant/components/simplisafe/translations/ru.json index 099637e422c..306a0180d88 100644 --- a/homeassistant/components/simplisafe/translations/ru.json +++ b/homeassistant/components/simplisafe/translations/ru.json @@ -32,11 +32,12 @@ }, "user": { "data": { + "auth_code": "\u041a\u043e\u0434 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438", "code": "\u041a\u043e\u0434 (\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0432 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0435 Home Assistant)", "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" }, - "description": "\u041d\u0430\u0447\u0438\u043d\u0430\u044f \u0441 2021 \u0433\u043e\u0434\u0430 SimpliSafe \u043f\u0435\u0440\u0435\u0448\u043b\u0430 \u043d\u0430 \u043d\u043e\u0432\u044b\u0439 \u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0447\u0435\u0440\u0435\u0437 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u0435 \u0432\u0435\u0431-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435. \u0418\u0437-\u0437\u0430 \u0442\u0435\u0445\u043d\u0438\u0447\u0435\u0441\u043a\u0438\u0445 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u0439, \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u0435 \u044d\u0442\u043e\u0433\u043e \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0430 \u043e\u0441\u0443\u0449\u0435\u0441\u0442\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u043c \u0432\u0440\u0443\u0447\u043d\u0443\u044e. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) \u043f\u0435\u0440\u0435\u0434 \u0437\u0430\u043f\u0443\u0441\u043a\u043e\u043c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438. \n\n\u041a\u043e\u0433\u0434\u0430 \u0412\u044b \u0431\u0443\u0434\u0435\u0442\u0435 \u0433\u043e\u0442\u043e\u0432\u044b, \u043d\u0430\u0436\u043c\u0438\u0442\u0435 [\u0441\u044e\u0434\u0430]({url}), \u0447\u0442\u043e\u0431\u044b \u043e\u0442\u043a\u0440\u044b\u0442\u044c \u0432\u0435\u0431-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 SimpliSafe \u0438 \u0432\u0432\u0435\u0441\u0442\u0438 \u0441\u0432\u043e\u0438 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435. \u041a\u043e\u0433\u0434\u0430 \u043f\u0440\u043e\u0446\u0435\u0441\u0441 \u0431\u0443\u0434\u0435\u0442 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d, \u0432\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 ''\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c''.", + "description": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0432\u0435\u0431-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 SimpliSafe. \u0418\u0437-\u0437\u0430 \u0442\u0435\u0445\u043d\u0438\u0447\u0435\u0441\u043a\u0438\u0445 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u0439, \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u0435 \u044d\u0442\u043e\u0433\u043e \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0430 \u043e\u0441\u0443\u0449\u0435\u0441\u0442\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u043c \u0432\u0440\u0443\u0447\u043d\u0443\u044e. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u043f\u0435\u0440\u0435\u0434 \u043d\u0430\u0447\u0430\u043b\u043e\u043c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438. \n\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e [\u0441\u0441\u044b\u043b\u043a\u0435]({url}), \u0447\u0442\u043e\u0431\u044b \u043e\u0442\u043a\u0440\u044b\u0442\u044c \u0432\u0435\u0431-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 SimpliSafe \u0438 \u0432\u0432\u0435\u0441\u0442\u0438 \u0441\u0432\u043e\u0438 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435. \n\n2. \u041a\u043e\u0433\u0434\u0430 \u043f\u0440\u043e\u0446\u0435\u0441\u0441 \u0432\u0445\u043e\u0434\u0430 \u0431\u0443\u0434\u0435\u0442 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d, \u0432\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", "title": "SimpliSafe" } } diff --git a/homeassistant/components/simplisafe/translations/tr.json b/homeassistant/components/simplisafe/translations/tr.json index 09b2b61d267..721502ca0b1 100644 --- a/homeassistant/components/simplisafe/translations/tr.json +++ b/homeassistant/components/simplisafe/translations/tr.json @@ -32,11 +32,12 @@ }, "user": { "data": { + "auth_code": "Yetkilendirme Kodu", "code": "Kod (Home Assistant kullan\u0131c\u0131 aray\u00fcz\u00fcnde kullan\u0131l\u0131r)", "password": "Parola", "username": "E-posta" }, - "description": "2021'den itibaren SimpliSafe, web uygulamas\u0131 arac\u0131l\u0131\u011f\u0131yla yeni bir kimlik do\u011frulama mekanizmas\u0131na ge\u00e7ti. Teknik s\u0131n\u0131rlamalar nedeniyle bu i\u015flemin sonunda manuel bir ad\u0131m vard\u0131r; l\u00fctfen ba\u015flamadan \u00f6nce [belgeleri](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) okudu\u011funuzdan emin olun. \n\n Haz\u0131r oldu\u011funuzda SimpliSafe web uygulamas\u0131n\u0131 a\u00e7mak ve kimlik bilgilerinizi girmek i\u00e7in [buray\u0131]( {url} \u0130\u015flem tamamland\u0131\u011f\u0131nda buraya d\u00f6n\u00fcn ve G\u00f6nder'e t\u0131klay\u0131n.", + "description": "SimpliSafe, SimpliSafe web uygulamas\u0131 arac\u0131l\u0131\u011f\u0131yla Home Assistant ile kimlik do\u011frulamas\u0131 yapar. Teknik s\u0131n\u0131rlamalar nedeniyle bu i\u015flemin sonunda manuel bir ad\u0131m vard\u0131r; l\u00fctfen ba\u015flamadan \u00f6nce [belgeleri]( {docs_url} \n\n 1. SimpliSafe web uygulamas\u0131n\u0131 a\u00e7mak ve kimlik bilgilerinizi girmek i\u00e7in [buraya]( {url} \n\n 2. Oturum a\u00e7ma i\u015flemi tamamland\u0131\u011f\u0131nda buraya d\u00f6n\u00fcn ve yetkilendirme kodunu a\u015fa\u011f\u0131ya girin.", "title": "Bilgilerinizi doldurun." } } diff --git a/homeassistant/components/simplisafe/translations/zh-Hant.json b/homeassistant/components/simplisafe/translations/zh-Hant.json index 59931520202..d967899b1dd 100644 --- a/homeassistant/components/simplisafe/translations/zh-Hant.json +++ b/homeassistant/components/simplisafe/translations/zh-Hant.json @@ -32,6 +32,7 @@ }, "user": { "data": { + "auth_code": "\u8a8d\u8b49\u78bc", "code": "\u9a57\u8b49\u78bc\uff08\u4f7f\u7528\u65bc Home Assistant UI\uff09", "password": "\u5bc6\u78bc", "username": "\u96fb\u5b50\u90f5\u4ef6" From 74d5cbd3a9471824223f446e59fcb0cf9201657a Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Sat, 11 Dec 2021 06:02:17 +0100 Subject: [PATCH 0313/2644] Add events and device triggers to LCN (#58745) --- homeassistant/components/lcn/__init__.py | 86 +++- homeassistant/components/lcn/const.py | 29 ++ .../components/lcn/device_trigger.py | 105 +++++ homeassistant/components/lcn/helpers.py | 2 +- homeassistant/components/lcn/manifest.json | 2 +- homeassistant/components/lcn/strings.json | 10 + .../components/lcn/translations/en.json | 10 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/lcn/conftest.py | 26 +- tests/components/lcn/test_device_trigger.py | 409 ++++++++++++++++++ tests/components/lcn/test_events.py | 153 +++++++ tests/components/lcn/test_init.py | 1 + 13 files changed, 831 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/lcn/device_trigger.py create mode 100644 homeassistant/components/lcn/strings.json create mode 100644 homeassistant/components/lcn/translations/en.json create mode 100644 tests/components/lcn/test_device_trigger.py create mode 100644 tests/components/lcn/test_events.py diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index d019c156f37..b0b231cb9e9 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Callable +from functools import partial import logging import pypck @@ -9,6 +10,7 @@ import pypck from homeassistant import config_entries from homeassistant.const import ( CONF_ADDRESS, + CONF_DEVICE_ID, CONF_DOMAIN, CONF_IP_ADDRESS, CONF_NAME, @@ -18,6 +20,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.typing import ConfigType @@ -119,6 +122,13 @@ async def async_setup_entry( # forward config_entry to components hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + # register for LCN bus messages + device_registry = dr.async_get(hass) + input_received = partial( + async_host_input_received, hass, config_entry, device_registry + ) + lcn_connection.register_for_inputs(input_received) + # register service calls for service_name, service in SERVICES: if not hass.services.has_service(DOMAIN, service_name): @@ -150,6 +160,80 @@ async def async_unload_entry( return unload_ok +def async_host_input_received( + hass: HomeAssistant, + config_entry: config_entries.ConfigEntry, + device_registry: dr.DeviceRegistry, + inp: pypck.inputs.Input, +) -> None: + """Process received input object (command) from LCN bus.""" + if not isinstance(inp, pypck.inputs.ModInput): + return + + lcn_connection = hass.data[DOMAIN][config_entry.entry_id][CONNECTION] + logical_address = lcn_connection.physical_to_logical(inp.physical_source_addr) + address = ( + logical_address.seg_id, + logical_address.addr_id, + logical_address.is_group, + ) + identifiers = {(DOMAIN, generate_unique_id(config_entry.entry_id, address))} + device = device_registry.async_get_device(identifiers, set()) + if device is None: + return + + if isinstance(inp, pypck.inputs.ModStatusAccessControl): + _async_fire_access_control_event(hass, device, address, inp) + elif isinstance(inp, pypck.inputs.ModSendKeysHost): + _async_fire_send_keys_event(hass, device, address, inp) + + +def _async_fire_access_control_event( + hass: HomeAssistant, device: dr.DeviceEntry, address: AddressType, inp: InputType +) -> None: + """Fire access control event (transponder, transmitter, fingerprint).""" + event_data = { + "segment_id": address[0], + "module_id": address[1], + "code": inp.code, + } + + if device is not None: + event_data.update({CONF_DEVICE_ID: device.id}) + + if inp.periphery == pypck.lcn_defs.AccessControlPeriphery.TRANSMITTER: + event_data.update( + {"level": inp.level, "key": inp.key, "action": inp.action.value} + ) + + event_name = f"lcn_{inp.periphery.value.lower()}" + hass.bus.async_fire(event_name, event_data) + + +def _async_fire_send_keys_event( + hass: HomeAssistant, device: dr.DeviceEntry, address: AddressType, inp: InputType +) -> None: + """Fire send_keys event.""" + for table, action in enumerate(inp.actions): + if action == pypck.lcn_defs.SendKeyCommand.DONTSEND: + continue + + for key, selected in enumerate(inp.keys): + if not selected: + continue + event_data = { + "segment_id": address[0], + "module_id": address[1], + "key": pypck.lcn_defs.Key(table * 8 + key).name.lower(), + "action": action.name.lower(), + } + + if device is not None: + event_data.update({CONF_DEVICE_ID: device.id}) + + hass.bus.async_fire("lcn_send_keys", event_data) + + class LcnEntity(Entity): """Parent class for all entities associated with the LCN component.""" @@ -183,7 +267,7 @@ class LcnEntity(Entity): def device_info(self) -> DeviceInfo | None: """Return device specific attributes.""" address = f"{'g' if self.address[2] else 'm'}{self.address[0]:03d}{self.address[1]:03d}" - model = f"LCN {get_device_model(self.config[CONF_DOMAIN], self.config[CONF_DOMAIN_DATA])}" + model = f"LCN resource ({get_device_model(self.config[CONF_DOMAIN], self.config[CONF_DOMAIN_DATA])})" return { "identifiers": {(DOMAIN, self.unique_id)}, diff --git a/homeassistant/components/lcn/const.py b/homeassistant/components/lcn/const.py index f2059827e84..ef0ad6481f4 100644 --- a/homeassistant/components/lcn/const.py +++ b/homeassistant/components/lcn/const.py @@ -192,6 +192,35 @@ RELVARREF = ["CURRENT", "PROG"] SENDKEYCOMMANDS = ["HIT", "MAKE", "BREAK", "DONTSEND"] +SENDKEYS = [ + "A1", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "B1", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "C1", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", +] + +KEY_ACTIONS = ["HIT", "MAKE", "BREAK"] + TIME_UNITS = [ "SECONDS", "SECOND", diff --git a/homeassistant/components/lcn/device_trigger.py b/homeassistant/components/lcn/device_trigger.py new file mode 100644 index 00000000000..b21c3b820af --- /dev/null +++ b/homeassistant/components/lcn/device_trigger.py @@ -0,0 +1,105 @@ +"""Provides device triggers for LCN.""" +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant.components.automation import ( + AutomationActionType, + AutomationTriggerInfo, +) +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA +from homeassistant.components.homeassistant.triggers import event +from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE +from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers.typing import ConfigType + +from . import DOMAIN +from .const import KEY_ACTIONS, SENDKEYS + +TRIGGER_TYPES = {"transmitter", "transponder", "fingerprint", "send_keys"} + +LCN_DEVICE_TRIGGER_BASE_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( + {vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES)} +) + +ACCESS_CONTROL_SCHEMA = {vol.Optional("code"): vol.All(vol.Lower, cv.string)} +TRANSMITTER_SCHEMA = { + **ACCESS_CONTROL_SCHEMA, + vol.Optional("level"): cv.positive_int, + vol.Optional("key"): cv.positive_int, + vol.Optional("action"): vol.In([action.lower() for action in KEY_ACTIONS]), +} + +SENDKEYS_SCHEMA = { + vol.Optional("key"): vol.In([key.lower() for key in SENDKEYS]), + vol.Optional("action"): vol.In([action.lower() for action in KEY_ACTIONS]), +} + +TRIGGER_SCHEMA = vol.Any( + LCN_DEVICE_TRIGGER_BASE_SCHEMA.extend(ACCESS_CONTROL_SCHEMA), + LCN_DEVICE_TRIGGER_BASE_SCHEMA.extend(TRANSMITTER_SCHEMA), + LCN_DEVICE_TRIGGER_BASE_SCHEMA.extend(SENDKEYS_SCHEMA), +) + +TYPE_SCHEMAS = { + "transmitter": {"extra_fields": vol.Schema(TRANSMITTER_SCHEMA)}, + "transponder": {"extra_fields": vol.Schema(ACCESS_CONTROL_SCHEMA)}, + "fingerprint": {"extra_fields": vol.Schema(ACCESS_CONTROL_SCHEMA)}, + "send_keys": {"extra_fields": vol.Schema(SENDKEYS_SCHEMA)}, +} + + +async def async_get_triggers( + hass: HomeAssistant, device_id: str +) -> list[dict[str, Any]]: + """List device triggers for LCN devices.""" + device_registry = dr.async_get(hass) + device = device_registry.async_get(device_id) + + if device.model.startswith(("LCN host", "LCN group", "LCN resource")): # type: ignore[union-attr] + return [] + + base_trigger = { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_DEVICE_ID: device_id, + } + + return [{**base_trigger, CONF_TYPE: type_} for type_ in TRIGGER_TYPES] + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: AutomationTriggerInfo, +) -> CALLBACK_TYPE: + """Attach a trigger.""" + event_data = { + CONF_DEVICE_ID: config[CONF_DEVICE_ID], + **{ + key: config[key] + for key in ("code", "level", "key", "action") + if key in config + }, + } + + event_config = event.TRIGGER_SCHEMA( + { + event.CONF_PLATFORM: "event", + event.CONF_EVENT_TYPE: f"lcn_{config[CONF_TYPE]}", + event.CONF_EVENT_DATA: event_data, + } + ) + + return await event.async_attach_trigger( + hass, event_config, action, automation_info, platform_type="device" + ) + + +async def async_get_trigger_capabilities(hass: HomeAssistant, config: dict) -> dict: + """List trigger capabilities.""" + return TYPE_SCHEMAS.get(config[CONF_TYPE], {}) diff --git a/homeassistant/components/lcn/helpers.py b/homeassistant/components/lcn/helpers.py index b879c2d3f72..74f135c4d1e 100644 --- a/homeassistant/components/lcn/helpers.py +++ b/homeassistant/components/lcn/helpers.py @@ -256,7 +256,7 @@ def register_lcn_host_device(hass: HomeAssistant, config_entry: ConfigEntry) -> identifiers={(DOMAIN, config_entry.entry_id)}, manufacturer="Issendorff", name=config_entry.title, - model="PCHK", + model=f"LCN host ({config_entry.data[CONF_IP_ADDRESS]}:{config_entry.data[CONF_PORT]})", ) diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json index 1adc407d692..4b06f7075f6 100644 --- a/homeassistant/components/lcn/manifest.json +++ b/homeassistant/components/lcn/manifest.json @@ -3,7 +3,7 @@ "name": "LCN", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/lcn", - "requirements": ["pypck==0.7.10"], + "requirements": ["pypck==0.7.11"], "codeowners": ["@alengwenus"], "iot_class": "local_push" } diff --git a/homeassistant/components/lcn/strings.json b/homeassistant/components/lcn/strings.json new file mode 100644 index 00000000000..078172ff34a --- /dev/null +++ b/homeassistant/components/lcn/strings.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "trigger_type": { + "transmitter": "transmitter code received", + "transponder": "transpoder code received", + "fingerprint": "fingerprint code received", + "send_keys": "send keys received" + } + } +} diff --git a/homeassistant/components/lcn/translations/en.json b/homeassistant/components/lcn/translations/en.json new file mode 100644 index 00000000000..9a27b35a4d4 --- /dev/null +++ b/homeassistant/components/lcn/translations/en.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "trigger_type": { + "fingerprint": "fingerprint code received", + "send_keys": "send keys received", + "transmitter": "transmitter code received", + "transponder": "transpoder code received" + } + } +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 721bfa3a0ae..aa8200cf48d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1726,7 +1726,7 @@ pyownet==0.10.0.post1 pypca==0.0.7 # homeassistant.components.lcn -pypck==0.7.10 +pypck==0.7.11 # homeassistant.components.pjlink pypjlink2==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c543bba974e..f70f8f281d1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1072,7 +1072,7 @@ pyowm==3.2.0 pyownet==0.10.0.post1 # homeassistant.components.lcn -pypck==0.7.10 +pypck==0.7.11 # homeassistant.components.plaato pyplaato==0.0.15 diff --git a/tests/components/lcn/conftest.py b/tests/components/lcn/conftest.py index 81c2fdc68e4..aebae09547a 100644 --- a/tests/components/lcn/conftest.py +++ b/tests/components/lcn/conftest.py @@ -9,10 +9,12 @@ from pypck.module import GroupConnection, ModuleConnection import pytest from homeassistant.components.lcn.const import DOMAIN +from homeassistant.components.lcn.helpers import generate_unique_id from homeassistant.const import CONF_HOST +from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry, load_fixture +from tests.common import MockConfigEntry, async_mock_service, load_fixture class MockModuleConnection(ModuleConnection): @@ -39,6 +41,13 @@ class MockGroupConnection(GroupConnection): class MockPchkConnectionManager(PchkConnectionManager): """Fake connection handler.""" + return_value = None + + def __init__(self, *args, **kwargs): + """Initialize MockPchkCOnnectionManager.""" + super().__init__(*args, **kwargs) + self.__class__.return_value = self + async def async_connect(self, timeout=30): """Mock establishing a connection to PCHK.""" self.authentication_completed_future.set_result(True) @@ -75,6 +84,12 @@ def create_config_entry(name): return entry +@pytest.fixture +def calls(hass): + """Track calls to a mock service.""" + return async_mock_service(hass, "test", "automation") + + @pytest.fixture(name="entry") def create_config_entry_pchk(): """Return one specific config entry.""" @@ -101,3 +116,12 @@ async def setup_component(hass): await async_setup_component(hass, DOMAIN, config_data) await hass.async_block_till_done() + + +def get_device(hass, entry, address): + """Get LCN device for specified address.""" + device_registry = dr.async_get(hass) + identifiers = {(DOMAIN, generate_unique_id(entry.entry_id, address))} + device = device_registry.async_get_device(identifiers) + assert device + return device diff --git a/tests/components/lcn/test_device_trigger.py b/tests/components/lcn/test_device_trigger.py new file mode 100644 index 00000000000..00138594a5c --- /dev/null +++ b/tests/components/lcn/test_device_trigger.py @@ -0,0 +1,409 @@ +"""Tests for LCN device triggers.""" +from unittest.mock import patch + +from pypck.inputs import ModSendKeysHost, ModStatusAccessControl +from pypck.lcn_addr import LcnAddr +from pypck.lcn_defs import AccessControlPeriphery, KeyAction, SendKeyCommand +import voluptuous_serialize + +from homeassistant.components import automation +from homeassistant.components.lcn import device_trigger +from homeassistant.components.lcn.const import DOMAIN, KEY_ACTIONS, SENDKEYS +from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE +from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.setup import async_setup_component + +from .conftest import MockPchkConnectionManager, get_device, init_integration + +from tests.common import assert_lists_same, async_get_device_automations + + +@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) +async def test_get_triggers_module_device(hass, entry): + """Test we get the expected triggers from a LCN module device.""" + await init_integration(hass, entry) + device = get_device(hass, entry, (0, 7, False)) + + expected_triggers = [ + { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_TYPE: "transmitter", + CONF_DEVICE_ID: device.id, + }, + { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_TYPE: "transponder", + CONF_DEVICE_ID: device.id, + }, + { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_TYPE: "fingerprint", + CONF_DEVICE_ID: device.id, + }, + { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_TYPE: "send_keys", + CONF_DEVICE_ID: device.id, + }, + ] + + triggers = await async_get_device_automations(hass, "trigger", device.id) + assert_lists_same(triggers, expected_triggers) + + +@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) +async def test_get_triggers_non_module_device(hass, entry): + """Test we get the expected triggers from a LCN non-module device.""" + not_included_types = ("transmitter", "transponder", "fingerprint", "send_keys") + + await init_integration(hass, entry) + device_registry = dr.async_get(hass) + for device_id in device_registry.devices: + device = device_registry.async_get(device_id) + if device.model.startswith(("LCN host", "LCN group", "LCN resource")): + triggers = await async_get_device_automations(hass, "trigger", device_id) + for trigger in triggers: + assert trigger[CONF_TYPE] not in not_included_types + + +@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) +async def test_if_fires_on_transponder_event(hass, calls, entry): + """Test for transponder event triggers firing.""" + await init_integration(hass, entry) + address = (0, 7, False) + device = get_device(hass, entry, address) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_DEVICE_ID: device.id, + CONF_TYPE: "transponder", + }, + "action": { + "service": "test.automation", + "data_template": { + "test": "test_trigger_transponder", + "code": "{{ trigger.event.data.code }}", + }, + }, + }, + ] + }, + ) + + inp = ModStatusAccessControl( + LcnAddr(*address), + periphery=AccessControlPeriphery.TRANSPONDER, + code="aabbcc", + ) + + lcn_connection = MockPchkConnectionManager.return_value + await lcn_connection.async_process_input(inp) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].data == { + "test": "test_trigger_transponder", + "code": "aabbcc", + } + + +@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) +async def test_if_fires_on_fingerprint_event(hass, calls, entry): + """Test for fingerprint event triggers firing.""" + await init_integration(hass, entry) + address = (0, 7, False) + device = get_device(hass, entry, address) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_DEVICE_ID: device.id, + CONF_TYPE: "fingerprint", + }, + "action": { + "service": "test.automation", + "data_template": { + "test": "test_trigger_fingerprint", + "code": "{{ trigger.event.data.code }}", + }, + }, + }, + ] + }, + ) + + inp = ModStatusAccessControl( + LcnAddr(*address), + periphery=AccessControlPeriphery.FINGERPRINT, + code="aabbcc", + ) + + lcn_connection = MockPchkConnectionManager.return_value + await lcn_connection.async_process_input(inp) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].data == { + "test": "test_trigger_fingerprint", + "code": "aabbcc", + } + + +@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) +async def test_if_fires_on_transmitter_event(hass, calls, entry): + """Test for transmitter event triggers firing.""" + await init_integration(hass, entry) + address = (0, 7, False) + device = get_device(hass, entry, address) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_DEVICE_ID: device.id, + CONF_TYPE: "transmitter", + }, + "action": { + "service": "test.automation", + "data_template": { + "test": "test_trigger_transmitter", + "code": "{{ trigger.event.data.code }}", + "level": "{{ trigger.event.data.level }}", + "key": "{{ trigger.event.data.key }}", + "action": "{{ trigger.event.data.action }}", + }, + }, + }, + ] + }, + ) + + inp = ModStatusAccessControl( + LcnAddr(*address), + periphery=AccessControlPeriphery.TRANSMITTER, + code="aabbcc", + level=0, + key=0, + action=KeyAction.HIT, + ) + + lcn_connection = MockPchkConnectionManager.return_value + await lcn_connection.async_process_input(inp) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].data == { + "test": "test_trigger_transmitter", + "code": "aabbcc", + "level": 0, + "key": 0, + "action": "hit", + } + + +@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) +async def test_if_fires_on_send_keys_event(hass, calls, entry): + """Test for send_keys event triggers firing.""" + await init_integration(hass, entry) + address = (0, 7, False) + device = get_device(hass, entry, address) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_DEVICE_ID: device.id, + CONF_TYPE: "send_keys", + }, + "action": { + "service": "test.automation", + "data_template": { + "test": "test_trigger_send_keys", + "key": "{{ trigger.event.data.key }}", + "action": "{{ trigger.event.data.action }}", + }, + }, + }, + ] + }, + ) + + inp = ModSendKeysHost( + LcnAddr(*address), + actions=[SendKeyCommand.HIT, SendKeyCommand.DONTSEND, SendKeyCommand.DONTSEND], + keys=[True, False, False, False, False, False, False, False], + ) + + lcn_connection = MockPchkConnectionManager.return_value + await lcn_connection.async_process_input(inp) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].data == { + "test": "test_trigger_send_keys", + "key": "a1", + "action": "hit", + } + + +@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) +async def test_get_transponder_trigger_capabilities(hass, entry): + """Test we get the expected capabilities from a transponder device trigger.""" + await init_integration(hass, entry) + address = (0, 7, False) + device = get_device(hass, entry, address) + + capabilities = await device_trigger.async_get_trigger_capabilities( + hass, + { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_TYPE: "transponder", + CONF_DEVICE_ID: device.id, + }, + ) + assert capabilities and "extra_fields" in capabilities + + assert voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) == [{"name": "code", "optional": True, "type": "string", "lower": True}] + + +@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) +async def test_get_fingerprint_trigger_capabilities(hass, entry): + """Test we get the expected capabilities from a fingerprint device trigger.""" + await init_integration(hass, entry) + address = (0, 7, False) + device = get_device(hass, entry, address) + + capabilities = await device_trigger.async_get_trigger_capabilities( + hass, + { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_TYPE: "fingerprint", + CONF_DEVICE_ID: device.id, + }, + ) + assert capabilities and "extra_fields" in capabilities + + assert voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) == [{"name": "code", "optional": True, "type": "string", "lower": True}] + + +@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) +async def test_get_transmitter_trigger_capabilities(hass, entry): + """Test we get the expected capabilities from a transmitter device trigger.""" + await init_integration(hass, entry) + address = (0, 7, False) + device = get_device(hass, entry, address) + + capabilities = await device_trigger.async_get_trigger_capabilities( + hass, + { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_TYPE: "transmitter", + CONF_DEVICE_ID: device.id, + }, + ) + assert capabilities and "extra_fields" in capabilities + + assert voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) == [ + {"name": "code", "type": "string", "optional": True, "lower": True}, + {"name": "level", "type": "integer", "optional": True, "valueMin": 0}, + {"name": "key", "type": "integer", "optional": True, "valueMin": 0}, + { + "name": "action", + "type": "select", + "optional": True, + "options": [("hit", "hit"), ("make", "make"), ("break", "break")], + }, + ] + + +@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) +async def test_get_send_keys_trigger_capabilities(hass, entry): + """Test we get the expected capabilities from a send_keys device trigger.""" + await init_integration(hass, entry) + address = (0, 7, False) + device = get_device(hass, entry, address) + + capabilities = await device_trigger.async_get_trigger_capabilities( + hass, + { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_TYPE: "send_keys", + CONF_DEVICE_ID: device.id, + }, + ) + assert capabilities and "extra_fields" in capabilities + + assert voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) == [ + { + "name": "key", + "type": "select", + "optional": True, + "options": [(send_key.lower(), send_key.lower()) for send_key in SENDKEYS], + }, + { + "name": "action", + "type": "select", + "options": [ + (key_action.lower(), key_action.lower()) for key_action in KEY_ACTIONS + ], + "optional": True, + }, + ] + + +@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) +async def test_unknown_trigger_capabilities(hass, entry): + """Test we get empty capabilities if trigger is unknown.""" + await init_integration(hass, entry) + address = (0, 7, False) + device = get_device(hass, entry, address) + + capabilities = await device_trigger.async_get_trigger_capabilities( + hass, + { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_TYPE: "dummy", + CONF_DEVICE_ID: device.id, + }, + ) + assert capabilities == {} diff --git a/tests/components/lcn/test_events.py b/tests/components/lcn/test_events.py new file mode 100644 index 00000000000..f977d586dad --- /dev/null +++ b/tests/components/lcn/test_events.py @@ -0,0 +1,153 @@ +"""Tests for LCN events.""" +from unittest.mock import patch + +from pypck.inputs import Input, ModSendKeysHost, ModStatusAccessControl +from pypck.lcn_addr import LcnAddr +from pypck.lcn_defs import AccessControlPeriphery, KeyAction, SendKeyCommand + +from .conftest import MockPchkConnectionManager, init_integration + +from tests.common import async_capture_events + + +@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) +async def test_fire_transponder_event(hass, entry): + """Test the transponder event is fired.""" + await init_integration(hass, entry) + + events = async_capture_events(hass, "lcn_transponder") + + inp = ModStatusAccessControl( + LcnAddr(0, 7, False), + periphery=AccessControlPeriphery.TRANSPONDER, + code="aabbcc", + ) + + lcn_connection = MockPchkConnectionManager.return_value + await lcn_connection.async_process_input(inp) + await hass.async_block_till_done() + + assert len(events) == 1 + assert events[0].event_type == "lcn_transponder" + assert events[0].data["code"] == "aabbcc" + + +@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) +async def test_fire_fingerprint_event(hass, entry): + """Test the fingerprint event is fired.""" + await init_integration(hass, entry) + + events = async_capture_events(hass, "lcn_fingerprint") + + inp = ModStatusAccessControl( + LcnAddr(0, 7, False), + periphery=AccessControlPeriphery.FINGERPRINT, + code="aabbcc", + ) + + lcn_connection = MockPchkConnectionManager.return_value + await lcn_connection.async_process_input(inp) + await hass.async_block_till_done() + + assert len(events) == 1 + assert events[0].event_type == "lcn_fingerprint" + assert events[0].data["code"] == "aabbcc" + + +@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) +async def test_fire_transmitter_event(hass, entry): + """Test the transmitter event is fired.""" + await init_integration(hass, entry) + + events = async_capture_events(hass, "lcn_transmitter") + + inp = ModStatusAccessControl( + LcnAddr(0, 7, False), + periphery=AccessControlPeriphery.TRANSMITTER, + code="aabbcc", + level=0, + key=0, + action=KeyAction.HIT, + ) + + lcn_connection = MockPchkConnectionManager.return_value + await lcn_connection.async_process_input(inp) + await hass.async_block_till_done() + + assert len(events) == 1 + assert events[0].event_type == "lcn_transmitter" + assert events[0].data["code"] == "aabbcc" + assert events[0].data["level"] == 0 + assert events[0].data["key"] == 0 + assert events[0].data["action"] == "hit" + + +@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) +async def test_fire_sendkeys_event(hass, entry): + """Test the send_keys event is fired.""" + await init_integration(hass, entry) + + events = async_capture_events(hass, "lcn_send_keys") + + inp = ModSendKeysHost( + LcnAddr(0, 7, False), + actions=[SendKeyCommand.HIT, SendKeyCommand.MAKE, SendKeyCommand.DONTSEND], + keys=[True, True, False, False, False, False, False, False], + ) + + lcn_connection = MockPchkConnectionManager.return_value + await lcn_connection.async_process_input(inp) + await hass.async_block_till_done() + + assert len(events) == 4 + assert events[0].event_type == "lcn_send_keys" + assert events[0].data["key"] == "a1" + assert events[0].data["action"] == "hit" + assert events[1].event_type == "lcn_send_keys" + assert events[1].data["key"] == "a2" + assert events[1].data["action"] == "hit" + assert events[2].event_type == "lcn_send_keys" + assert events[2].data["key"] == "b1" + assert events[2].data["action"] == "make" + assert events[3].event_type == "lcn_send_keys" + assert events[3].data["key"] == "b2" + assert events[3].data["action"] == "make" + + +@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) +async def test_dont_fire_on_non_module_input(hass, entry): + """Test for no event is fired if a non-module input is received.""" + await init_integration(hass, entry) + + inp = Input() + lcn_connection = MockPchkConnectionManager.return_value + + for event_name in ( + "lcn_transponder", + "lcn_fingerprint", + "lcn_transmitter", + "lcn_send_keys", + ): + events = async_capture_events(hass, event_name) + await lcn_connection.async_process_input(inp) + await hass.async_block_till_done() + assert len(events) == 0 + + +@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) +async def test_dont_fire_on_unknown_module(hass, entry): + """Test for no event is fired if an input from an unknown module is received.""" + await init_integration(hass, entry) + + inp = ModStatusAccessControl( + LcnAddr(0, 10, False), # unknown module + periphery=AccessControlPeriphery.FINGERPRINT, + code="aabbcc", + ) + + lcn_connection = MockPchkConnectionManager.return_value + + events = async_capture_events(hass, "lcn_transmitter") + await lcn_connection.async_process_input(inp) + await hass.async_block_till_done() + assert len(events) == 0 diff --git a/tests/components/lcn/test_init.py b/tests/components/lcn/test_init.py index e4fb5beef0d..40f655dd695 100644 --- a/tests/components/lcn/test_init.py +++ b/tests/components/lcn/test_init.py @@ -19,6 +19,7 @@ from .conftest import MockPchkConnectionManager, init_integration, setup_compone async def test_async_setup_entry(hass, entry): """Test a successful setup entry and unload of entry.""" await init_integration(hass, entry) + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert entry.state == ConfigEntryState.LOADED From 3eabd69666f8a8f08670f75abd95260441a3f5dc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 10 Dec 2021 20:19:54 -1000 Subject: [PATCH 0314/2644] Fix exception in color_rgb_to_rgbww (#61466) --- homeassistant/util/color.py | 2 +- tests/util/test_color.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 2daccf28915..3d4f7122ad0 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -450,7 +450,7 @@ def color_rgb_to_rgbww( w_r, w_g, w_b = color_temperature_to_rgb(color_temp_kelvin) # Find the ratio of the midpoint white in the input rgb channels - white_level = min(r / w_r, g / w_g, b / w_b) + white_level = min(r / w_r, g / w_g, b / w_b if w_b else 0) # Subtract the white portion from the rgb channels. rgb = (r - w_r * white_level, g - w_g * white_level, b - w_b * white_level) diff --git a/tests/util/test_color.py b/tests/util/test_color.py index db9ad988aee..d806a941965 100644 --- a/tests/util/test_color.py +++ b/tests/util/test_color.py @@ -366,3 +366,38 @@ def test_get_color_in_voluptuous(): schema("not a color") assert schema("red") == (255, 0, 0) + + +def test_color_rgb_to_rgbww(): + """Test color_rgb_to_rgbww conversions.""" + assert color_util.color_rgb_to_rgbww(255, 255, 255, 154, 370) == ( + 0, + 54, + 98, + 255, + 255, + ) + assert color_util.color_rgb_to_rgbww(255, 255, 255, 100, 1000) == ( + 255, + 255, + 255, + 0, + 0, + ) + assert color_util.color_rgb_to_rgbww(255, 255, 255, 1, 1000) == ( + 0, + 118, + 241, + 255, + 255, + ) + assert color_util.color_rgb_to_rgbww(128, 128, 128, 154, 370) == ( + 0, + 27, + 49, + 128, + 128, + ) + assert color_util.color_rgb_to_rgbww(64, 64, 64, 154, 370) == (0, 14, 25, 64, 64) + assert color_util.color_rgb_to_rgbww(32, 64, 16, 154, 370) == (9, 64, 0, 38, 38) + assert color_util.color_rgb_to_rgbww(0, 0, 0, 154, 370) == (0, 0, 0, 0, 0) From 61865f45933f3bcff042c3b5fed4d96d2eeae1a1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 11 Dec 2021 00:12:37 -1000 Subject: [PATCH 0315/2644] Pickup screenlogic codeowner (#61477) - I am using this in production and already doing some work on it --- CODEOWNERS | 2 +- homeassistant/components/screenlogic/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index e0c7b99865e..e858209c30c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -455,7 +455,7 @@ homeassistant/components/samsungtv/* @escoand @chemelli74 homeassistant/components/scene/* @home-assistant/core homeassistant/components/schluter/* @prairieapps homeassistant/components/scrape/* @fabaff -homeassistant/components/screenlogic/* @dieselrabbit +homeassistant/components/screenlogic/* @dieselrabbit @bdraco homeassistant/components/script/* @home-assistant/core homeassistant/components/search/* @home-assistant/core homeassistant/components/select/* @home-assistant/core diff --git a/homeassistant/components/screenlogic/manifest.json b/homeassistant/components/screenlogic/manifest.json index b6134216049..92f9e774183 100644 --- a/homeassistant/components/screenlogic/manifest.json +++ b/homeassistant/components/screenlogic/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/screenlogic", "requirements": ["screenlogicpy==0.5.3"], - "codeowners": ["@dieselrabbit"], + "codeowners": ["@dieselrabbit", "@bdraco"], "dhcp": [ { "hostname": "pentair: *", From d78914d138fcfafde22deb5456d3931fc15c3b92 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 11 Dec 2021 00:13:05 -1000 Subject: [PATCH 0316/2644] Drop nmap_tracker code owner (#61476) - I am no longer using this in production --- CODEOWNERS | 1 - homeassistant/components/nmap_tracker/manifest.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index e858209c30c..26db874a0b8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -356,7 +356,6 @@ homeassistant/components/nightscout/* @marciogranzotto homeassistant/components/nilu/* @hfurubotten homeassistant/components/nina/* @DeerMaximum homeassistant/components/nissan_leaf/* @filcole -homeassistant/components/nmap_tracker/* @bdraco homeassistant/components/nmbs/* @thibmaek homeassistant/components/no_ip/* @fabaff homeassistant/components/noaa_tides/* @jdelaney72 diff --git a/homeassistant/components/nmap_tracker/manifest.json b/homeassistant/components/nmap_tracker/manifest.json index bbd15834ef2..e17270a62a0 100644 --- a/homeassistant/components/nmap_tracker/manifest.json +++ b/homeassistant/components/nmap_tracker/manifest.json @@ -8,7 +8,7 @@ "getmac==0.8.2", "mac-vendor-lookup==0.1.11" ], - "codeowners": ["@bdraco"], + "codeowners": [], "iot_class": "local_polling", "config_flow": true } From 773ada5fe416bdbf45c88b32dab47c4676b1ca02 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 11 Dec 2021 00:14:18 -1000 Subject: [PATCH 0317/2644] Pickup codeowner for lookin (#61474) - I am now using these devices in production --- CODEOWNERS | 2 +- homeassistant/components/lookin/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 26db874a0b8..7f011d3f182 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -294,7 +294,7 @@ homeassistant/components/litterrobot/* @natekspencer homeassistant/components/local_ip/* @issacg homeassistant/components/logger/* @home-assistant/core homeassistant/components/logi_circle/* @evanjd -homeassistant/components/lookin/* @ANMalko +homeassistant/components/lookin/* @ANMalko @bdraco homeassistant/components/lovelace/* @home-assistant/frontend homeassistant/components/luci/* @mzdrale homeassistant/components/luftdaten/* @fabaff diff --git a/homeassistant/components/lookin/manifest.json b/homeassistant/components/lookin/manifest.json index 903f6b84d50..d63961b5cfa 100644 --- a/homeassistant/components/lookin/manifest.json +++ b/homeassistant/components/lookin/manifest.json @@ -2,7 +2,7 @@ "domain": "lookin", "name": "LOOKin", "documentation": "https://www.home-assistant.io/integrations/lookin/", - "codeowners": ["@ANMalko"], + "codeowners": ["@ANMalko", "@bdraco"], "requirements": ["aiolookin==0.1.0"], "zeroconf": ["_lookin._tcp.local."], "config_flow": true, From 41bac5ccbb666f33bcfe70b1cf11ef0044850b1d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 11 Dec 2021 00:20:58 -1000 Subject: [PATCH 0318/2644] Fix non-threadsafe call to async_fire in telegram_bot (#61465) Fixes https://github.com/home-assistant/core/issues/53255#issuecomment-888111478 --- homeassistant/components/telegram_bot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 7fd83141b7d..c79b8c5a033 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -575,7 +575,7 @@ class TelegramNotificationService: } if message_tag is not None: event_data[ATTR_MESSAGE_TAG] = message_tag - self.hass.bus.async_fire(EVENT_TELEGRAM_SENT, event_data) + self.hass.bus.fire(EVENT_TELEGRAM_SENT, event_data) elif not isinstance(out, bool): _LOGGER.warning( "Update last message: out_type:%s, out=%s", type(out), out From 5907f6690c5fb4963a32b9f08343e7812d1a24e0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 11 Dec 2021 00:39:32 -1000 Subject: [PATCH 0319/2644] Fix missing color modes for Magic Home Ceiling Light CCT (0xE1) (#61478) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 59d5ec46110..9e26a128ef3 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Magic Home", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.26.5"], + "requirements": ["flux_led==0.26.7"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index aa8200cf48d..be76ee7ace5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -667,7 +667,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.26.5 +flux_led==0.26.7 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f70f8f281d1..c66f80cd792 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -408,7 +408,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.26.5 +flux_led==0.26.7 # homeassistant.components.homekit fnvhash==0.1.0 From a17031630f3f9f1b6ec87007378e9dc1d142a2b9 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sat, 11 Dec 2021 11:06:39 -0500 Subject: [PATCH 0320/2644] Use Platform enum in ZHA (#61016) --- .../components/zha/alarm_control_panel.py | 8 +- homeassistant/components/zha/binary_sensor.py | 7 +- homeassistant/components/zha/climate.py | 14 +- homeassistant/components/zha/core/const.py | 37 ++--- .../components/zha/core/registries.py | 111 +++++++-------- homeassistant/components/zha/cover.py | 13 +- .../components/zha/device_tracker.py | 7 +- homeassistant/components/zha/fan.py | 9 +- homeassistant/components/zha/light.py | 13 +- homeassistant/components/zha/lock.py | 12 +- homeassistant/components/zha/number.py | 7 +- homeassistant/components/zha/sensor.py | 8 +- homeassistant/components/zha/siren.py | 6 +- homeassistant/components/zha/switch.py | 10 +- .../zha/test_alarm_control_panel.py | 30 ++-- tests/components/zha/test_binary_sensor.py | 5 +- tests/components/zha/test_climate.py | 134 +++++++++--------- tests/components/zha/test_cover.py | 36 ++--- tests/components/zha/test_device_tracker.py | 8 +- tests/components/zha/test_discover.py | 39 ++--- tests/components/zha/test_fan.py | 31 ++-- tests/components/zha/test_gateway.py | 8 +- tests/components/zha/test_light.py | 37 +++-- tests/components/zha/test_lock.py | 14 +- tests/components/zha/test_number.py | 10 +- tests/components/zha/test_sensor.py | 8 +- tests/components/zha/test_siren.py | 11 +- tests/components/zha/test_switch.py | 15 +- 28 files changed, 332 insertions(+), 316 deletions(-) diff --git a/homeassistant/components/zha/alarm_control_panel.py b/homeassistant/components/zha/alarm_control_panel.py index 1ba5f1b73f8..c76e26baaf6 100644 --- a/homeassistant/components/zha/alarm_control_panel.py +++ b/homeassistant/components/zha/alarm_control_panel.py @@ -4,7 +4,6 @@ import functools from zigpy.zcl.clusters.security import IasAce from homeassistant.components.alarm_control_panel import ( - DOMAIN, FORMAT_TEXT, SUPPORT_ALARM_ARM_AWAY, SUPPORT_ALARM_ARM_HOME, @@ -19,6 +18,7 @@ from homeassistant.const import ( STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, + Platform, ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -43,7 +43,9 @@ from .core.helpers import async_get_zha_config_value from .core.registries import ZHA_ENTITIES from .entity import ZhaEntity -STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) +STRICT_MATCH = functools.partial( + ZHA_ENTITIES.strict_match, Platform.ALARM_CONTROL_PANEL +) IAS_ACE_STATE_MAP = { IasAce.PanelStatus.Panel_Disarmed: STATE_ALARM_DISARMED, @@ -56,7 +58,7 @@ IAS_ACE_STATE_MAP = { async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Zigbee Home Automation alarm control panel from config entry.""" - entities_to_create = hass.data[DATA_ZHA][DOMAIN] + entities_to_create = hass.data[DATA_ZHA][Platform.ALARM_CONTROL_PANEL] unsub = async_dispatcher_connect( hass, diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index e6f03a8a848..7e083a07d72 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -10,10 +10,9 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_OPENING, DEVICE_CLASS_SMOKE, DEVICE_CLASS_VIBRATION, - DOMAIN, BinarySensorEntity, ) -from homeassistant.const import STATE_ON +from homeassistant.const import STATE_ON, Platform from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -42,12 +41,12 @@ CLASS_MAPPING = { 0x002D: DEVICE_CLASS_VIBRATION, } -STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) +STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.BINARY_SENSOR) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Zigbee Home Automation binary sensor from config entry.""" - entities_to_create = hass.data[DATA_ZHA][DOMAIN] + entities_to_create = hass.data[DATA_ZHA][Platform.BINARY_SENSOR] unsub = async_dispatcher_connect( hass, diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index f7a1d1815db..7cb2c0f16e4 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -21,7 +21,6 @@ from homeassistant.components.climate.const import ( CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, CURRENT_HVAC_OFF, - DOMAIN, FAN_AUTO, FAN_ON, HVAC_MODE_COOL, @@ -40,7 +39,12 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, ) -from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS, TEMP_CELSIUS +from homeassistant.const import ( + ATTR_TEMPERATURE, + PRECISION_TENTHS, + TEMP_CELSIUS, + Platform, +) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import async_track_time_interval @@ -73,8 +77,8 @@ ATTR_UNOCCP_HEAT_SETPT = "unoccupied_heating_setpoint" ATTR_UNOCCP_COOL_SETPT = "unoccupied_cooling_setpoint" -STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) -MULTI_MATCH = functools.partial(ZHA_ENTITIES.multipass_match, DOMAIN) +STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.CLIMATE) +MULTI_MATCH = functools.partial(ZHA_ENTITIES.multipass_match, Platform.CLIMATE) RUNNING_MODE = {0x00: HVAC_MODE_OFF, 0x03: HVAC_MODE_COOL, 0x04: HVAC_MODE_HEAT} @@ -152,7 +156,7 @@ ZCL_TEMP = 100 async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Zigbee Home Automation sensor from config entry.""" - entities_to_create = hass.data[DATA_ZHA][DOMAIN] + entities_to_create = hass.data[DATA_ZHA][Platform.CLIMATE] unsub = async_dispatcher_connect( hass, SIGNAL_ADD_ENTITIES, diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 813f268bbe5..876d4c4e9f5 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -12,18 +12,7 @@ import zigpy_xbee.zigbee.application import zigpy_zigate.zigbee.application import zigpy_znp.zigbee.application -from homeassistant.components.alarm_control_panel import DOMAIN as ALARM -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR -from homeassistant.components.climate import DOMAIN as CLIMATE -from homeassistant.components.cover import DOMAIN as COVER -from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER -from homeassistant.components.fan import DOMAIN as FAN -from homeassistant.components.light import DOMAIN as LIGHT -from homeassistant.components.lock import DOMAIN as LOCK -from homeassistant.components.number import DOMAIN as NUMBER -from homeassistant.components.sensor import DOMAIN as SENSOR -from homeassistant.components.siren import DOMAIN as SIREN -from homeassistant.components.switch import DOMAIN as SWITCH +from homeassistant.const import Platform import homeassistant.helpers.config_validation as cv from .typing import CALLABLE_T @@ -111,18 +100,18 @@ CLUSTER_TYPE_IN = "in" CLUSTER_TYPE_OUT = "out" PLATFORMS = ( - ALARM, - BINARY_SENSOR, - CLIMATE, - COVER, - DEVICE_TRACKER, - FAN, - LIGHT, - LOCK, - NUMBER, - SENSOR, - SIREN, - SWITCH, + Platform.ALARM_CONTROL_PANEL, + Platform.BINARY_SENSOR, + Platform.CLIMATE, + Platform.COVER, + Platform.DEVICE_TRACKER, + Platform.FAN, + Platform.LIGHT, + Platform.LOCK, + Platform.NUMBER, + Platform.SENSOR, + Platform.SIREN, + Platform.SWITCH, ) CONF_ALARM_MASTER_CODE = "alarm_master_code" diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index f624ef9289d..36a77b841b6 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -11,25 +11,14 @@ from zigpy import zcl import zigpy.profiles.zha import zigpy.profiles.zll -from homeassistant.components.alarm_control_panel import DOMAIN as ALARM -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR -from homeassistant.components.climate import DOMAIN as CLIMATE -from homeassistant.components.cover import DOMAIN as COVER -from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER -from homeassistant.components.fan import DOMAIN as FAN -from homeassistant.components.light import DOMAIN as LIGHT -from homeassistant.components.lock import DOMAIN as LOCK -from homeassistant.components.number import DOMAIN as NUMBER -from homeassistant.components.sensor import DOMAIN as SENSOR -from homeassistant.components.siren import DOMAIN as SIREN -from homeassistant.components.switch import DOMAIN as SWITCH +from homeassistant.const import Platform # importing channels updates registries from . import channels as zha_channels # noqa: F401 pylint: disable=unused-import from .decorators import CALLABLE_T, DictRegistry, SetRegistry from .typing import ChannelType -GROUP_ENTITY_DOMAINS = [LIGHT, SWITCH, FAN] +GROUP_ENTITY_DOMAINS = [Platform.LIGHT, Platform.SWITCH, Platform.FAN] PHILLIPS_REMOTE_CLUSTER = 0xFC00 SMARTTHINGS_ACCELERATION_CLUSTER = 0xFC02 @@ -64,34 +53,34 @@ REMOTE_DEVICE_TYPES = collections.defaultdict(list, REMOTE_DEVICE_TYPES) SINGLE_INPUT_CLUSTER_DEVICE_CLASS = { # this works for now but if we hit conflicts we can break it out to # a different dict that is keyed by manufacturer - SMARTTHINGS_ACCELERATION_CLUSTER: BINARY_SENSOR, - SMARTTHINGS_HUMIDITY_CLUSTER: SENSOR, - VOC_LEVEL_CLUSTER: SENSOR, - zcl.clusters.closures.DoorLock.cluster_id: LOCK, - zcl.clusters.closures.WindowCovering.cluster_id: COVER, - zcl.clusters.general.BinaryInput.cluster_id: BINARY_SENSOR, - zcl.clusters.general.AnalogInput.cluster_id: SENSOR, - zcl.clusters.general.AnalogOutput.cluster_id: NUMBER, - zcl.clusters.general.MultistateInput.cluster_id: SENSOR, - zcl.clusters.general.OnOff.cluster_id: SWITCH, - zcl.clusters.general.PowerConfiguration.cluster_id: SENSOR, - zcl.clusters.hvac.Fan.cluster_id: FAN, - zcl.clusters.measurement.CarbonDioxideConcentration.cluster_id: SENSOR, - zcl.clusters.measurement.CarbonMonoxideConcentration.cluster_id: SENSOR, - zcl.clusters.measurement.FormaldehydeConcentration.cluster_id: SENSOR, - zcl.clusters.measurement.IlluminanceMeasurement.cluster_id: SENSOR, - zcl.clusters.measurement.OccupancySensing.cluster_id: BINARY_SENSOR, - zcl.clusters.measurement.PressureMeasurement.cluster_id: SENSOR, - zcl.clusters.measurement.RelativeHumidity.cluster_id: SENSOR, - zcl.clusters.measurement.SoilMoisture.cluster_id: SENSOR, - zcl.clusters.measurement.LeafWetness.cluster_id: SENSOR, - zcl.clusters.measurement.TemperatureMeasurement.cluster_id: SENSOR, - zcl.clusters.security.IasZone.cluster_id: BINARY_SENSOR, + SMARTTHINGS_ACCELERATION_CLUSTER: Platform.BINARY_SENSOR, + SMARTTHINGS_HUMIDITY_CLUSTER: Platform.SENSOR, + VOC_LEVEL_CLUSTER: Platform.SENSOR, + zcl.clusters.closures.DoorLock.cluster_id: Platform.LOCK, + zcl.clusters.closures.WindowCovering.cluster_id: Platform.COVER, + zcl.clusters.general.BinaryInput.cluster_id: Platform.BINARY_SENSOR, + zcl.clusters.general.AnalogInput.cluster_id: Platform.SENSOR, + zcl.clusters.general.AnalogOutput.cluster_id: Platform.NUMBER, + zcl.clusters.general.MultistateInput.cluster_id: Platform.SENSOR, + zcl.clusters.general.OnOff.cluster_id: Platform.SWITCH, + zcl.clusters.general.PowerConfiguration.cluster_id: Platform.SENSOR, + zcl.clusters.hvac.Fan.cluster_id: Platform.FAN, + zcl.clusters.measurement.CarbonDioxideConcentration.cluster_id: Platform.SENSOR, + zcl.clusters.measurement.CarbonMonoxideConcentration.cluster_id: Platform.SENSOR, + zcl.clusters.measurement.FormaldehydeConcentration.cluster_id: Platform.SENSOR, + zcl.clusters.measurement.IlluminanceMeasurement.cluster_id: Platform.SENSOR, + zcl.clusters.measurement.OccupancySensing.cluster_id: Platform.BINARY_SENSOR, + zcl.clusters.measurement.PressureMeasurement.cluster_id: Platform.SENSOR, + zcl.clusters.measurement.RelativeHumidity.cluster_id: Platform.SENSOR, + zcl.clusters.measurement.SoilMoisture.cluster_id: Platform.SENSOR, + zcl.clusters.measurement.LeafWetness.cluster_id: Platform.SENSOR, + zcl.clusters.measurement.TemperatureMeasurement.cluster_id: Platform.SENSOR, + zcl.clusters.security.IasZone.cluster_id: Platform.BINARY_SENSOR, } SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = { - zcl.clusters.general.OnOff.cluster_id: BINARY_SENSOR, - zcl.clusters.security.IasAce.cluster_id: ALARM, + zcl.clusters.general.OnOff.cluster_id: Platform.BINARY_SENSOR, + zcl.clusters.security.IasAce.cluster_id: Platform.ALARM_CONTROL_PANEL, } BINDABLE_CLUSTERS = SetRegistry() @@ -99,31 +88,31 @@ CHANNEL_ONLY_CLUSTERS = SetRegistry() DEVICE_CLASS = { zigpy.profiles.zha.PROFILE_ID: { - SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE: DEVICE_TRACKER, - zigpy.profiles.zha.DeviceType.THERMOSTAT: CLIMATE, - zigpy.profiles.zha.DeviceType.COLOR_DIMMABLE_LIGHT: LIGHT, - zigpy.profiles.zha.DeviceType.COLOR_TEMPERATURE_LIGHT: LIGHT, - zigpy.profiles.zha.DeviceType.DIMMABLE_BALLAST: LIGHT, - zigpy.profiles.zha.DeviceType.DIMMABLE_LIGHT: LIGHT, - zigpy.profiles.zha.DeviceType.DIMMABLE_PLUG_IN_UNIT: LIGHT, - zigpy.profiles.zha.DeviceType.EXTENDED_COLOR_LIGHT: LIGHT, - zigpy.profiles.zha.DeviceType.LEVEL_CONTROLLABLE_OUTPUT: COVER, - zigpy.profiles.zha.DeviceType.ON_OFF_BALLAST: SWITCH, - zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT: LIGHT, - zigpy.profiles.zha.DeviceType.ON_OFF_PLUG_IN_UNIT: SWITCH, - zigpy.profiles.zha.DeviceType.SHADE: COVER, - zigpy.profiles.zha.DeviceType.SMART_PLUG: SWITCH, - zigpy.profiles.zha.DeviceType.IAS_ANCILLARY_CONTROL: ALARM, - zigpy.profiles.zha.DeviceType.IAS_WARNING_DEVICE: SIREN, + SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE: Platform.DEVICE_TRACKER, + zigpy.profiles.zha.DeviceType.THERMOSTAT: Platform.CLIMATE, + zigpy.profiles.zha.DeviceType.COLOR_DIMMABLE_LIGHT: Platform.LIGHT, + zigpy.profiles.zha.DeviceType.COLOR_TEMPERATURE_LIGHT: Platform.LIGHT, + zigpy.profiles.zha.DeviceType.DIMMABLE_BALLAST: Platform.LIGHT, + zigpy.profiles.zha.DeviceType.DIMMABLE_LIGHT: Platform.LIGHT, + zigpy.profiles.zha.DeviceType.DIMMABLE_PLUG_IN_UNIT: Platform.LIGHT, + zigpy.profiles.zha.DeviceType.EXTENDED_COLOR_LIGHT: Platform.LIGHT, + zigpy.profiles.zha.DeviceType.LEVEL_CONTROLLABLE_OUTPUT: Platform.COVER, + zigpy.profiles.zha.DeviceType.ON_OFF_BALLAST: Platform.SWITCH, + zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT: Platform.LIGHT, + zigpy.profiles.zha.DeviceType.ON_OFF_PLUG_IN_UNIT: Platform.SWITCH, + zigpy.profiles.zha.DeviceType.SHADE: Platform.COVER, + zigpy.profiles.zha.DeviceType.SMART_PLUG: Platform.SWITCH, + zigpy.profiles.zha.DeviceType.IAS_ANCILLARY_CONTROL: Platform.ALARM_CONTROL_PANEL, + zigpy.profiles.zha.DeviceType.IAS_WARNING_DEVICE: Platform.SIREN, }, zigpy.profiles.zll.PROFILE_ID: { - zigpy.profiles.zll.DeviceType.COLOR_LIGHT: LIGHT, - zigpy.profiles.zll.DeviceType.COLOR_TEMPERATURE_LIGHT: LIGHT, - zigpy.profiles.zll.DeviceType.DIMMABLE_LIGHT: LIGHT, - zigpy.profiles.zll.DeviceType.DIMMABLE_PLUGIN_UNIT: LIGHT, - zigpy.profiles.zll.DeviceType.EXTENDED_COLOR_LIGHT: LIGHT, - zigpy.profiles.zll.DeviceType.ON_OFF_LIGHT: LIGHT, - zigpy.profiles.zll.DeviceType.ON_OFF_PLUGIN_UNIT: SWITCH, + zigpy.profiles.zll.DeviceType.COLOR_LIGHT: Platform.LIGHT, + zigpy.profiles.zll.DeviceType.COLOR_TEMPERATURE_LIGHT: Platform.LIGHT, + zigpy.profiles.zll.DeviceType.DIMMABLE_LIGHT: Platform.LIGHT, + zigpy.profiles.zll.DeviceType.DIMMABLE_PLUGIN_UNIT: Platform.LIGHT, + zigpy.profiles.zll.DeviceType.EXTENDED_COLOR_LIGHT: Platform.LIGHT, + zigpy.profiles.zll.DeviceType.ON_OFF_LIGHT: Platform.LIGHT, + zigpy.profiles.zll.DeviceType.ON_OFF_PLUGIN_UNIT: Platform.SWITCH, }, } DEVICE_CLASS = collections.defaultdict(dict, DEVICE_CLASS) diff --git a/homeassistant/components/zha/cover.py b/homeassistant/components/zha/cover.py index 71c5dcca908..2febc209755 100644 --- a/homeassistant/components/zha/cover.py +++ b/homeassistant/components/zha/cover.py @@ -12,10 +12,15 @@ from homeassistant.components.cover import ( ATTR_POSITION, DEVICE_CLASS_DAMPER, DEVICE_CLASS_SHADE, - DOMAIN, CoverEntity, ) -from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING +from homeassistant.const import ( + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, + Platform, +) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -37,12 +42,12 @@ from .entity import ZhaEntity _LOGGER = logging.getLogger(__name__) -STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) +STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.COVER) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Zigbee Home Automation cover from config entry.""" - entities_to_create = hass.data[DATA_ZHA][DOMAIN] + entities_to_create = hass.data[DATA_ZHA][Platform.COVER] unsub = async_dispatcher_connect( hass, diff --git a/homeassistant/components/zha/device_tracker.py b/homeassistant/components/zha/device_tracker.py index ffb37e33b0f..c203462d44d 100644 --- a/homeassistant/components/zha/device_tracker.py +++ b/homeassistant/components/zha/device_tracker.py @@ -2,8 +2,9 @@ import functools import time -from homeassistant.components.device_tracker import DOMAIN, SOURCE_TYPE_ROUTER +from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER from homeassistant.components.device_tracker.config_entry import ScannerEntity +from homeassistant.const import Platform from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -19,12 +20,12 @@ from .core.registries import ZHA_ENTITIES from .entity import ZhaEntity from .sensor import Battery -STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) +STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.DEVICE_TRACKER) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Zigbee Home Automation device tracker from config entry.""" - entities_to_create = hass.data[DATA_ZHA][DOMAIN] + entities_to_create = hass.data[DATA_ZHA][Platform.DEVICE_TRACKER] unsub = async_dispatcher_connect( hass, diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index 0176cf1ea3b..0402e93ff8c 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -11,12 +11,11 @@ from zigpy.zcl.clusters import hvac from homeassistant.components.fan import ( ATTR_PERCENTAGE, ATTR_PRESET_MODE, - DOMAIN, SUPPORT_SET_SPEED, FanEntity, NotValidPresetModeError, ) -from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.const import STATE_UNAVAILABLE, Platform from homeassistant.core import State, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util.percentage import ( @@ -53,13 +52,13 @@ PRESET_MODES = list(NAME_TO_PRESET_MODE) DEFAULT_ON_PERCENTAGE = 50 -STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) -GROUP_MATCH = functools.partial(ZHA_ENTITIES.group_match, DOMAIN) +STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.FAN) +GROUP_MATCH = functools.partial(ZHA_ENTITIES.group_match, Platform.FAN) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Zigbee Home Automation fan from config entry.""" - entities_to_create = hass.data[DATA_ZHA][DOMAIN] + entities_to_create = hass.data[DATA_ZHA][Platform.FAN] unsub = async_dispatcher_connect( hass, diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 9398d0fde17..476bb733cc9 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -30,7 +30,12 @@ from homeassistant.components.light import ( SUPPORT_FLASH, SUPPORT_TRANSITION, ) -from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_UNAVAILABLE +from homeassistant.const import ( + ATTR_SUPPORTED_FEATURES, + STATE_ON, + STATE_UNAVAILABLE, + Platform, +) from homeassistant.core import State, callback from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.dispatcher import ( @@ -77,8 +82,8 @@ UPDATE_COLORLOOP_HUE = 0x8 FLASH_EFFECTS = {light.FLASH_SHORT: EFFECT_BLINK, light.FLASH_LONG: EFFECT_BREATHE} UNSUPPORTED_ATTRIBUTE = 0x86 -STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, light.DOMAIN) -GROUP_MATCH = functools.partial(ZHA_ENTITIES.group_match, light.DOMAIN) +STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.LIGHT) +GROUP_MATCH = functools.partial(ZHA_ENTITIES.group_match, Platform.LIGHT) PARALLEL_UPDATES = 0 SIGNAL_LIGHT_GROUP_STATE_CHANGED = "zha_light_group_state_changed" @@ -102,7 +107,7 @@ class LightColorMode(enum.IntEnum): async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Zigbee Home Automation light from config entry.""" - entities_to_create = hass.data[DATA_ZHA][light.DOMAIN] + entities_to_create = hass.data[DATA_ZHA][Platform.LIGHT] unsub = async_dispatcher_connect( hass, diff --git a/homeassistant/components/zha/lock.py b/homeassistant/components/zha/lock.py index 99f6230de0b..0c8ca756785 100644 --- a/homeassistant/components/zha/lock.py +++ b/homeassistant/components/zha/lock.py @@ -4,12 +4,8 @@ import functools import voluptuous as vol from zigpy.zcl.foundation import Status -from homeassistant.components.lock import ( - DOMAIN, - STATE_LOCKED, - STATE_UNLOCKED, - LockEntity, -) +from homeassistant.components.lock import STATE_LOCKED, STATE_UNLOCKED, LockEntity +from homeassistant.const import Platform from homeassistant.core import callback from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -27,7 +23,7 @@ from .entity import ZhaEntity # The first state is Zigbee 'Not fully locked' STATE_LIST = [STATE_UNLOCKED, STATE_LOCKED, STATE_UNLOCKED] -STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) +STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.LOCK) VALUE_TO_STATE = dict(enumerate(STATE_LIST)) @@ -39,7 +35,7 @@ SERVICE_CLEAR_LOCK_USER_CODE = "clear_lock_user_code" async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Zigbee Home Automation Door Lock from config entry.""" - entities_to_create = hass.data[DATA_ZHA][DOMAIN] + entities_to_create = hass.data[DATA_ZHA][Platform.LOCK] unsub = async_dispatcher_connect( hass, diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py index b4772e51742..0acda49c375 100644 --- a/homeassistant/components/zha/number.py +++ b/homeassistant/components/zha/number.py @@ -2,7 +2,8 @@ import functools import logging -from homeassistant.components.number import DOMAIN, NumberEntity +from homeassistant.components.number import NumberEntity +from homeassistant.const import Platform from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -19,7 +20,7 @@ from .entity import ZhaEntity _LOGGER = logging.getLogger(__name__) -STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) +STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.NUMBER) UNITS = { @@ -235,7 +236,7 @@ ICONS = { async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Zigbee Home Automation Analog Output from config entry.""" - entities_to_create = hass.data[DATA_ZHA][DOMAIN] + entities_to_create = hass.data[DATA_ZHA][Platform.NUMBER] unsub = async_dispatcher_connect( hass, diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 567d2a6065e..d4c508a8378 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -22,7 +22,6 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_POWER, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, - DOMAIN, STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL_INCREASING, SensorEntity, @@ -51,6 +50,7 @@ from homeassistant.const import ( VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR, VOLUME_GALLONS, VOLUME_LITERS, + Platform, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -99,8 +99,8 @@ BATTERY_SIZES = { } CHANNEL_ST_HUMIDITY_CLUSTER = f"channel_0x{SMARTTHINGS_HUMIDITY_CLUSTER:04x}" -STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) -MULTI_MATCH = functools.partial(ZHA_ENTITIES.multipass_match, DOMAIN) +STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.SENSOR) +MULTI_MATCH = functools.partial(ZHA_ENTITIES.multipass_match, Platform.SENSOR) async def async_setup_entry( @@ -109,7 +109,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Zigbee Home Automation sensor from config entry.""" - entities_to_create = hass.data[DATA_ZHA][DOMAIN] + entities_to_create = hass.data[DATA_ZHA][Platform.SENSOR] unsub = async_dispatcher_connect( hass, diff --git a/homeassistant/components/zha/siren.py b/homeassistant/components/zha/siren.py index 75f527cfcf1..ce558e8f1a5 100644 --- a/homeassistant/components/zha/siren.py +++ b/homeassistant/components/zha/siren.py @@ -7,7 +7,6 @@ from typing import Any from homeassistant.components.siren import ( ATTR_DURATION, - DOMAIN, SUPPORT_DURATION, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, @@ -20,6 +19,7 @@ from homeassistant.components.siren.const import ( SUPPORT_VOLUME_SET, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -45,7 +45,7 @@ from .core.registries import ZHA_ENTITIES from .core.typing import ChannelType, ZhaDeviceType from .entity import ZhaEntity -STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) +STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.SIREN) DEFAULT_DURATION = 5 # seconds @@ -55,7 +55,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Zigbee Home Automation siren from config entry.""" - entities_to_create = hass.data[DATA_ZHA][DOMAIN] + entities_to_create = hass.data[DATA_ZHA][Platform.SIREN] unsub = async_dispatcher_connect( hass, diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index 75254f631b9..da5e843c59f 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -7,8 +7,8 @@ from typing import Any from zigpy.zcl.clusters.general import OnOff from zigpy.zcl.foundation import Status -from homeassistant.components.switch import DOMAIN, SwitchEntity -from homeassistant.const import STATE_ON, STATE_UNAVAILABLE +from homeassistant.components.switch import SwitchEntity +from homeassistant.const import STATE_ON, STATE_UNAVAILABLE, Platform from homeassistant.core import State, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -23,13 +23,13 @@ from .core.const import ( from .core.registries import ZHA_ENTITIES from .entity import ZhaEntity, ZhaGroupEntity -STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) -GROUP_MATCH = functools.partial(ZHA_ENTITIES.group_match, DOMAIN) +STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.SWITCH) +GROUP_MATCH = functools.partial(ZHA_ENTITIES.group_match, Platform.SWITCH) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Zigbee Home Automation switch from config entry.""" - entities_to_create = hass.data[DATA_ZHA][DOMAIN] + entities_to_create = hass.data[DATA_ZHA][Platform.SWITCH] unsub = async_dispatcher_connect( hass, diff --git a/tests/components/zha/test_alarm_control_panel.py b/tests/components/zha/test_alarm_control_panel.py index 39063225e50..84e66f0833a 100644 --- a/tests/components/zha/test_alarm_control_panel.py +++ b/tests/components/zha/test_alarm_control_panel.py @@ -6,7 +6,6 @@ import zigpy.profiles.zha as zha import zigpy.zcl.clusters.security as security import zigpy.zcl.foundation as zcl_f -from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, STATE_ALARM_ARMED_AWAY, @@ -15,6 +14,7 @@ from homeassistant.const import ( STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, STATE_UNAVAILABLE, + Platform, ) from .common import async_enable_traffic, find_entity_id @@ -46,7 +46,7 @@ async def test_alarm_control_panel(hass, zha_device_joined_restored, zigpy_devic zha_device = await zha_device_joined_restored(zigpy_device) cluster = zigpy_device.endpoints.get(1).ias_ace - entity_id = await find_entity_id(ALARM_DOMAIN, zha_device, hass) + entity_id = await find_entity_id(Platform.ALARM_CONTROL_PANEL, zha_device, hass) assert entity_id is not None assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED await async_enable_traffic(hass, [zha_device], enabled=False) @@ -62,7 +62,10 @@ async def test_alarm_control_panel(hass, zha_device_joined_restored, zigpy_devic # arm_away from HA cluster.client_command.reset_mock() await hass.services.async_call( - ALARM_DOMAIN, "alarm_arm_away", {ATTR_ENTITY_ID: entity_id}, blocking=True + Platform.ALARM_CONTROL_PANEL, + "alarm_arm_away", + {ATTR_ENTITY_ID: entity_id}, + blocking=True, ) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY @@ -82,19 +85,22 @@ async def test_alarm_control_panel(hass, zha_device_joined_restored, zigpy_devic # trip alarm from faulty code entry cluster.client_command.reset_mock() await hass.services.async_call( - ALARM_DOMAIN, "alarm_arm_away", {ATTR_ENTITY_ID: entity_id}, blocking=True + Platform.ALARM_CONTROL_PANEL, + "alarm_arm_away", + {ATTR_ENTITY_ID: entity_id}, + blocking=True, ) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY cluster.client_command.reset_mock() await hass.services.async_call( - ALARM_DOMAIN, + Platform.ALARM_CONTROL_PANEL, "alarm_disarm", {ATTR_ENTITY_ID: entity_id, "code": "1111"}, blocking=True, ) await hass.services.async_call( - ALARM_DOMAIN, + Platform.ALARM_CONTROL_PANEL, "alarm_disarm", {ATTR_ENTITY_ID: entity_id, "code": "1111"}, blocking=True, @@ -117,7 +123,10 @@ async def test_alarm_control_panel(hass, zha_device_joined_restored, zigpy_devic # arm_home from HA cluster.client_command.reset_mock() await hass.services.async_call( - ALARM_DOMAIN, "alarm_arm_home", {ATTR_ENTITY_ID: entity_id}, blocking=True + Platform.ALARM_CONTROL_PANEL, + "alarm_arm_home", + {ATTR_ENTITY_ID: entity_id}, + blocking=True, ) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_HOME @@ -134,7 +143,10 @@ async def test_alarm_control_panel(hass, zha_device_joined_restored, zigpy_devic # arm_night from HA cluster.client_command.reset_mock() await hass.services.async_call( - ALARM_DOMAIN, "alarm_arm_night", {ATTR_ENTITY_ID: entity_id}, blocking=True + Platform.ALARM_CONTROL_PANEL, + "alarm_arm_night", + {ATTR_ENTITY_ID: entity_id}, + blocking=True, ) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_NIGHT @@ -228,7 +240,7 @@ async def reset_alarm_panel(hass, cluster, entity_id): """Reset the state of the alarm panel.""" cluster.client_command.reset_mock() await hass.services.async_call( - ALARM_DOMAIN, + Platform.ALARM_CONTROL_PANEL, "alarm_disarm", {ATTR_ENTITY_ID: entity_id, "code": "4321"}, blocking=True, diff --git a/tests/components/zha/test_binary_sensor.py b/tests/components/zha/test_binary_sensor.py index 1ab638d0b26..bfe2a3ce4f5 100644 --- a/tests/components/zha/test_binary_sensor.py +++ b/tests/components/zha/test_binary_sensor.py @@ -4,8 +4,7 @@ import zigpy.profiles.zha import zigpy.zcl.clusters.measurement as measurement import zigpy.zcl.clusters.security as security -from homeassistant.components.binary_sensor import DOMAIN -from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform from .common import ( async_enable_traffic, @@ -78,7 +77,7 @@ async def test_binary_sensor( """Test ZHA binary_sensor platform.""" zigpy_device = zigpy_device_mock(device) zha_device = await zha_device_joined_restored(zigpy_device) - entity_id = await find_entity_id(DOMAIN, zha_device, hass) + entity_id = await find_entity_id(Platform.BINARY_SENSOR, zha_device, hass) assert entity_id is not None assert hass.states.get(entity_id).state == STATE_OFF diff --git a/tests/components/zha/test_climate.py b/tests/components/zha/test_climate.py index 09da644bd29..58d5240aad7 100644 --- a/tests/components/zha/test_climate.py +++ b/tests/components/zha/test_climate.py @@ -45,14 +45,14 @@ from homeassistant.components.climate.const import ( SERVICE_SET_PRESET_MODE, SERVICE_SET_TEMPERATURE, ) -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.zha.climate import ( - DOMAIN, - HVAC_MODE_2_SYSTEM, - SEQ_OF_OPERATION, -) +from homeassistant.components.zha.climate import HVAC_MODE_2_SYSTEM, SEQ_OF_OPERATION from homeassistant.components.zha.core.const import PRESET_COMPLEX, PRESET_SCHEDULE -from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_UNKNOWN +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_TEMPERATURE, + STATE_UNKNOWN, + Platform, +) from .common import async_enable_traffic, find_entity_id, send_attributes_report from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE @@ -244,7 +244,7 @@ async def test_climate_local_temp(hass, device_climate): """Test local temperature.""" thrm_cluster = device_climate.device.endpoints[1].thermostat - entity_id = await find_entity_id(DOMAIN, device_climate, hass) + entity_id = await find_entity_id(Platform.CLIMATE, device_climate, hass) state = hass.states.get(entity_id) assert state.attributes[ATTR_CURRENT_TEMPERATURE] is None @@ -258,8 +258,8 @@ async def test_climate_hvac_action_running_state(hass, device_climate): """Test hvac action via running state.""" thrm_cluster = device_climate.device.endpoints[1].thermostat - entity_id = await find_entity_id(DOMAIN, device_climate, hass) - sensor_entity_id = await find_entity_id(SENSOR_DOMAIN, device_climate, hass) + entity_id = await find_entity_id(Platform.CLIMATE, device_climate, hass) + sensor_entity_id = await find_entity_id(Platform.SENSOR, device_climate, hass) state = hass.states.get(entity_id) assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF @@ -319,8 +319,8 @@ async def test_climate_hvac_action_running_state_zen(hass, device_climate_zen): """Test Zen hvac action via running state.""" thrm_cluster = device_climate_zen.device.endpoints[1].thermostat - entity_id = await find_entity_id(DOMAIN, device_climate_zen, hass) - sensor_entity_id = await find_entity_id(SENSOR_DOMAIN, device_climate_zen, hass) + entity_id = await find_entity_id(Platform.CLIMATE, device_climate_zen, hass) + sensor_entity_id = await find_entity_id(Platform.SENSOR, device_climate_zen, hass) state = hass.states.get(entity_id) assert ATTR_HVAC_ACTION not in state.attributes @@ -404,7 +404,7 @@ async def test_climate_hvac_action_pi_demand(hass, device_climate): """Test hvac action based on pi_heating/cooling_demand attrs.""" thrm_cluster = device_climate.device.endpoints[1].thermostat - entity_id = await find_entity_id(DOMAIN, device_climate, hass) + entity_id = await find_entity_id(Platform.CLIMATE, device_climate, hass) state = hass.states.get(entity_id) assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF @@ -451,7 +451,7 @@ async def test_hvac_mode(hass, device_climate, sys_mode, hvac_mode): """Test HVAC modee.""" thrm_cluster = device_climate.device.endpoints[1].thermostat - entity_id = await find_entity_id(DOMAIN, device_climate, hass) + entity_id = await find_entity_id(Platform.CLIMATE, device_climate, hass) state = hass.states.get(entity_id) assert state.state == HVAC_MODE_OFF @@ -489,7 +489,7 @@ async def test_hvac_modes(hass, device_climate_mock, seq_of_op, modes): device_climate = await device_climate_mock( CLIMATE, {"ctrl_seqe_of_oper": seq_of_op} ) - entity_id = await find_entity_id(DOMAIN, device_climate, hass) + entity_id = await find_entity_id(Platform.CLIMATE, device_climate, hass) state = hass.states.get(entity_id) assert set(state.attributes[ATTR_HVAC_MODES]) == modes @@ -520,10 +520,10 @@ async def test_target_temperature( manuf=MANUF_SINOPE, quirk=zhaquirks.sinope.thermostat.SinopeTechnologiesThermostat, ) - entity_id = await find_entity_id(DOMAIN, device_climate, hass) + entity_id = await find_entity_id(Platform.CLIMATE, device_climate, hass) if preset: await hass.services.async_call( - DOMAIN, + Platform.CLIMATE, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: preset}, blocking=True, @@ -556,10 +556,10 @@ async def test_target_temperature_high( manuf=MANUF_SINOPE, quirk=zhaquirks.sinope.thermostat.SinopeTechnologiesThermostat, ) - entity_id = await find_entity_id(DOMAIN, device_climate, hass) + entity_id = await find_entity_id(Platform.CLIMATE, device_climate, hass) if preset: await hass.services.async_call( - DOMAIN, + Platform.CLIMATE, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: preset}, blocking=True, @@ -592,10 +592,10 @@ async def test_target_temperature_low( manuf=MANUF_SINOPE, quirk=zhaquirks.sinope.thermostat.SinopeTechnologiesThermostat, ) - entity_id = await find_entity_id(DOMAIN, device_climate, hass) + entity_id = await find_entity_id(Platform.CLIMATE, device_climate, hass) if preset: await hass.services.async_call( - DOMAIN, + Platform.CLIMATE, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: preset}, blocking=True, @@ -620,13 +620,13 @@ async def test_set_hvac_mode(hass, device_climate, hvac_mode, sys_mode): """Test setting hvac mode.""" thrm_cluster = device_climate.device.endpoints[1].thermostat - entity_id = await find_entity_id(DOMAIN, device_climate, hass) + entity_id = await find_entity_id(Platform.CLIMATE, device_climate, hass) state = hass.states.get(entity_id) assert state.state == HVAC_MODE_OFF await hass.services.async_call( - DOMAIN, + Platform.CLIMATE, SERVICE_SET_HVAC_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_HVAC_MODE: hvac_mode}, blocking=True, @@ -645,7 +645,7 @@ async def test_set_hvac_mode(hass, device_climate, hvac_mode, sys_mode): # turn off thrm_cluster.write_attributes.reset_mock() await hass.services.async_call( - DOMAIN, + Platform.CLIMATE, SERVICE_SET_HVAC_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_HVAC_MODE: HVAC_MODE_OFF}, blocking=True, @@ -661,7 +661,7 @@ async def test_set_hvac_mode(hass, device_climate, hvac_mode, sys_mode): async def test_preset_setting(hass, device_climate_sinope): """Test preset setting.""" - entity_id = await find_entity_id(DOMAIN, device_climate_sinope, hass) + entity_id = await find_entity_id(Platform.CLIMATE, device_climate_sinope, hass) thrm_cluster = device_climate_sinope.device.endpoints[1].thermostat state = hass.states.get(entity_id) @@ -673,7 +673,7 @@ async def test_preset_setting(hass, device_climate_sinope): ] await hass.services.async_call( - DOMAIN, + Platform.CLIMATE, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_AWAY}, blocking=True, @@ -690,7 +690,7 @@ async def test_preset_setting(hass, device_climate_sinope): zcl_f.WriteAttributesResponse.deserialize(b"\x00")[0] ] await hass.services.async_call( - DOMAIN, + Platform.CLIMATE, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_AWAY}, blocking=True, @@ -707,7 +707,7 @@ async def test_preset_setting(hass, device_climate_sinope): zcl_f.WriteAttributesResponse.deserialize(b"\x01\x01\x01")[0] ] await hass.services.async_call( - DOMAIN, + Platform.CLIMATE, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_NONE}, blocking=True, @@ -724,7 +724,7 @@ async def test_preset_setting(hass, device_climate_sinope): zcl_f.WriteAttributesResponse.deserialize(b"\x00")[0] ] await hass.services.async_call( - DOMAIN, + Platform.CLIMATE, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_NONE}, blocking=True, @@ -739,14 +739,14 @@ async def test_preset_setting(hass, device_climate_sinope): async def test_preset_setting_invalid(hass, device_climate_sinope): """Test invalid preset setting.""" - entity_id = await find_entity_id(DOMAIN, device_climate_sinope, hass) + entity_id = await find_entity_id(Platform.CLIMATE, device_climate_sinope, hass) thrm_cluster = device_climate_sinope.device.endpoints[1].thermostat state = hass.states.get(entity_id) assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE await hass.services.async_call( - DOMAIN, + Platform.CLIMATE, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: "invalid_preset"}, blocking=True, @@ -760,14 +760,14 @@ async def test_preset_setting_invalid(hass, device_climate_sinope): async def test_set_temperature_hvac_mode(hass, device_climate): """Test setting HVAC mode in temperature service call.""" - entity_id = await find_entity_id(DOMAIN, device_climate, hass) + entity_id = await find_entity_id(Platform.CLIMATE, device_climate, hass) thrm_cluster = device_climate.device.endpoints[1].thermostat state = hass.states.get(entity_id) assert state.state == HVAC_MODE_OFF await hass.services.async_call( - DOMAIN, + Platform.CLIMATE, SERVICE_SET_TEMPERATURE, { ATTR_ENTITY_ID: entity_id, @@ -800,14 +800,14 @@ async def test_set_temperature_heat_cool(hass, device_climate_mock): manuf=MANUF_SINOPE, quirk=zhaquirks.sinope.thermostat.SinopeTechnologiesThermostat, ) - entity_id = await find_entity_id(DOMAIN, device_climate, hass) + entity_id = await find_entity_id(Platform.CLIMATE, device_climate, hass) thrm_cluster = device_climate.device.endpoints[1].thermostat state = hass.states.get(entity_id) assert state.state == HVAC_MODE_HEAT_COOL await hass.services.async_call( - DOMAIN, + Platform.CLIMATE, SERVICE_SET_TEMPERATURE, {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 21}, blocking=True, @@ -819,7 +819,7 @@ async def test_set_temperature_heat_cool(hass, device_climate_mock): assert thrm_cluster.write_attributes.await_count == 0 await hass.services.async_call( - DOMAIN, + Platform.CLIMATE, SERVICE_SET_TEMPERATURE, { ATTR_ENTITY_ID: entity_id, @@ -841,7 +841,7 @@ async def test_set_temperature_heat_cool(hass, device_climate_mock): } await hass.services.async_call( - DOMAIN, + Platform.CLIMATE, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_AWAY}, blocking=True, @@ -849,7 +849,7 @@ async def test_set_temperature_heat_cool(hass, device_climate_mock): thrm_cluster.write_attributes.reset_mock() await hass.services.async_call( - DOMAIN, + Platform.CLIMATE, SERVICE_SET_TEMPERATURE, { ATTR_ENTITY_ID: entity_id, @@ -886,14 +886,14 @@ async def test_set_temperature_heat(hass, device_climate_mock): manuf=MANUF_SINOPE, quirk=zhaquirks.sinope.thermostat.SinopeTechnologiesThermostat, ) - entity_id = await find_entity_id(DOMAIN, device_climate, hass) + entity_id = await find_entity_id(Platform.CLIMATE, device_climate, hass) thrm_cluster = device_climate.device.endpoints[1].thermostat state = hass.states.get(entity_id) assert state.state == HVAC_MODE_HEAT await hass.services.async_call( - DOMAIN, + Platform.CLIMATE, SERVICE_SET_TEMPERATURE, { ATTR_ENTITY_ID: entity_id, @@ -910,7 +910,7 @@ async def test_set_temperature_heat(hass, device_climate_mock): assert thrm_cluster.write_attributes.await_count == 0 await hass.services.async_call( - DOMAIN, + Platform.CLIMATE, SERVICE_SET_TEMPERATURE, {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 21}, blocking=True, @@ -926,7 +926,7 @@ async def test_set_temperature_heat(hass, device_climate_mock): } await hass.services.async_call( - DOMAIN, + Platform.CLIMATE, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_AWAY}, blocking=True, @@ -934,7 +934,7 @@ async def test_set_temperature_heat(hass, device_climate_mock): thrm_cluster.write_attributes.reset_mock() await hass.services.async_call( - DOMAIN, + Platform.CLIMATE, SERVICE_SET_TEMPERATURE, {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 22}, blocking=True, @@ -965,14 +965,14 @@ async def test_set_temperature_cool(hass, device_climate_mock): manuf=MANUF_SINOPE, quirk=zhaquirks.sinope.thermostat.SinopeTechnologiesThermostat, ) - entity_id = await find_entity_id(DOMAIN, device_climate, hass) + entity_id = await find_entity_id(Platform.CLIMATE, device_climate, hass) thrm_cluster = device_climate.device.endpoints[1].thermostat state = hass.states.get(entity_id) assert state.state == HVAC_MODE_COOL await hass.services.async_call( - DOMAIN, + Platform.CLIMATE, SERVICE_SET_TEMPERATURE, { ATTR_ENTITY_ID: entity_id, @@ -989,7 +989,7 @@ async def test_set_temperature_cool(hass, device_climate_mock): assert thrm_cluster.write_attributes.await_count == 0 await hass.services.async_call( - DOMAIN, + Platform.CLIMATE, SERVICE_SET_TEMPERATURE, {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 21}, blocking=True, @@ -1005,7 +1005,7 @@ async def test_set_temperature_cool(hass, device_climate_mock): } await hass.services.async_call( - DOMAIN, + Platform.CLIMATE, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_AWAY}, blocking=True, @@ -1013,7 +1013,7 @@ async def test_set_temperature_cool(hass, device_climate_mock): thrm_cluster.write_attributes.reset_mock() await hass.services.async_call( - DOMAIN, + Platform.CLIMATE, SERVICE_SET_TEMPERATURE, {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 22}, blocking=True, @@ -1048,14 +1048,14 @@ async def test_set_temperature_wrong_mode(hass, device_climate_mock): }, manuf=MANUF_SINOPE, ) - entity_id = await find_entity_id(DOMAIN, device_climate, hass) + entity_id = await find_entity_id(Platform.CLIMATE, device_climate, hass) thrm_cluster = device_climate.device.endpoints[1].thermostat state = hass.states.get(entity_id) assert state.state == HVAC_MODE_DRY await hass.services.async_call( - DOMAIN, + Platform.CLIMATE, SERVICE_SET_TEMPERATURE, {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 24}, blocking=True, @@ -1071,14 +1071,14 @@ async def test_set_temperature_wrong_mode(hass, device_climate_mock): async def test_occupancy_reset(hass, device_climate_sinope): """Test away preset reset.""" - entity_id = await find_entity_id(DOMAIN, device_climate_sinope, hass) + entity_id = await find_entity_id(Platform.CLIMATE, device_climate_sinope, hass) thrm_cluster = device_climate_sinope.device.endpoints[1].thermostat state = hass.states.get(entity_id) assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE await hass.services.async_call( - DOMAIN, + Platform.CLIMATE, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_AWAY}, blocking=True, @@ -1098,7 +1098,7 @@ async def test_occupancy_reset(hass, device_climate_sinope): async def test_fan_mode(hass, device_climate_fan): """Test fan mode.""" - entity_id = await find_entity_id(DOMAIN, device_climate_fan, hass) + entity_id = await find_entity_id(Platform.CLIMATE, device_climate_fan, hass) thrm_cluster = device_climate_fan.device.endpoints[1].thermostat state = hass.states.get(entity_id) @@ -1127,11 +1127,11 @@ async def test_fan_mode(hass, device_climate_fan): async def test_set_fan_mode_not_supported(hass, device_climate_fan): """Test fan setting unsupported mode.""" - entity_id = await find_entity_id(DOMAIN, device_climate_fan, hass) + entity_id = await find_entity_id(Platform.CLIMATE, device_climate_fan, hass) fan_cluster = device_climate_fan.device.endpoints[1].fan await hass.services.async_call( - DOMAIN, + Platform.CLIMATE, SERVICE_SET_FAN_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_FAN_MODE: FAN_LOW}, blocking=True, @@ -1142,14 +1142,14 @@ async def test_set_fan_mode_not_supported(hass, device_climate_fan): async def test_set_fan_mode(hass, device_climate_fan): """Test fan mode setting.""" - entity_id = await find_entity_id(DOMAIN, device_climate_fan, hass) + entity_id = await find_entity_id(Platform.CLIMATE, device_climate_fan, hass) fan_cluster = device_climate_fan.device.endpoints[1].fan state = hass.states.get(entity_id) assert state.attributes[ATTR_FAN_MODE] == FAN_AUTO await hass.services.async_call( - DOMAIN, + Platform.CLIMATE, SERVICE_SET_FAN_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_FAN_MODE: FAN_ON}, blocking=True, @@ -1159,7 +1159,7 @@ async def test_set_fan_mode(hass, device_climate_fan): fan_cluster.write_attributes.reset_mock() await hass.services.async_call( - DOMAIN, + Platform.CLIMATE, SERVICE_SET_FAN_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_FAN_MODE: FAN_AUTO}, blocking=True, @@ -1171,14 +1171,14 @@ async def test_set_fan_mode(hass, device_climate_fan): async def test_set_moes_preset(hass, device_climate_moes): """Test setting preset for moes trv.""" - entity_id = await find_entity_id(DOMAIN, device_climate_moes, hass) + entity_id = await find_entity_id(Platform.CLIMATE, device_climate_moes, hass) thrm_cluster = device_climate_moes.device.endpoints[1].thermostat state = hass.states.get(entity_id) assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE await hass.services.async_call( - DOMAIN, + Platform.CLIMATE, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_AWAY}, blocking=True, @@ -1191,7 +1191,7 @@ async def test_set_moes_preset(hass, device_climate_moes): thrm_cluster.write_attributes.reset_mock() await hass.services.async_call( - DOMAIN, + Platform.CLIMATE, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_SCHEDULE}, blocking=True, @@ -1207,7 +1207,7 @@ async def test_set_moes_preset(hass, device_climate_moes): thrm_cluster.write_attributes.reset_mock() await hass.services.async_call( - DOMAIN, + Platform.CLIMATE, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_COMFORT}, blocking=True, @@ -1223,7 +1223,7 @@ async def test_set_moes_preset(hass, device_climate_moes): thrm_cluster.write_attributes.reset_mock() await hass.services.async_call( - DOMAIN, + Platform.CLIMATE, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_ECO}, blocking=True, @@ -1239,7 +1239,7 @@ async def test_set_moes_preset(hass, device_climate_moes): thrm_cluster.write_attributes.reset_mock() await hass.services.async_call( - DOMAIN, + Platform.CLIMATE, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_BOOST}, blocking=True, @@ -1255,7 +1255,7 @@ async def test_set_moes_preset(hass, device_climate_moes): thrm_cluster.write_attributes.reset_mock() await hass.services.async_call( - DOMAIN, + Platform.CLIMATE, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_COMPLEX}, blocking=True, @@ -1271,7 +1271,7 @@ async def test_set_moes_preset(hass, device_climate_moes): thrm_cluster.write_attributes.reset_mock() await hass.services.async_call( - DOMAIN, + Platform.CLIMATE, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_NONE}, blocking=True, @@ -1286,7 +1286,7 @@ async def test_set_moes_preset(hass, device_climate_moes): async def test_set_moes_operation_mode(hass, device_climate_moes): """Test setting preset for moes trv.""" - entity_id = await find_entity_id(DOMAIN, device_climate_moes, hass) + entity_id = await find_entity_id(Platform.CLIMATE, device_climate_moes, hass) thrm_cluster = device_climate_moes.device.endpoints[1].thermostat await send_attributes_report(hass, thrm_cluster, {"operation_preset": 0}) diff --git a/tests/components/zha/test_cover.py b/tests/components/zha/test_cover.py index e002e2c26f0..45c5928797d 100644 --- a/tests/components/zha/test_cover.py +++ b/tests/components/zha/test_cover.py @@ -11,7 +11,6 @@ import zigpy.zcl.foundation as zcl_f from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, - DOMAIN, SERVICE_CLOSE_COVER, SERVICE_OPEN_COVER, SERVICE_SET_COVER_POSITION, @@ -22,6 +21,7 @@ from homeassistant.const import ( STATE_CLOSED, STATE_OPEN, STATE_UNAVAILABLE, + Platform, ) from homeassistant.core import CoreState, State @@ -116,7 +116,7 @@ async def test_cover(m1, hass, zha_device_joined_restored, zigpy_cover_device): assert cluster.read_attributes.call_count == 2 assert "current_position_lift_percentage" in cluster.read_attributes.call_args[0][0] - entity_id = await find_entity_id(DOMAIN, zha_device, hass) + entity_id = await find_entity_id(Platform.COVER, zha_device, hass) assert entity_id is not None await async_enable_traffic(hass, [zha_device], enabled=False) @@ -140,7 +140,7 @@ async def test_cover(m1, hass, zha_device_joined_restored, zigpy_cover_device): "zigpy.zcl.Cluster.request", return_value=mock_coro([0x1, zcl_f.Status.SUCCESS]) ): await hass.services.async_call( - DOMAIN, SERVICE_CLOSE_COVER, {"entity_id": entity_id}, blocking=True + Platform.COVER, SERVICE_CLOSE_COVER, {"entity_id": entity_id}, blocking=True ) assert cluster.request.call_count == 1 assert cluster.request.call_args[0][0] is False @@ -153,7 +153,7 @@ async def test_cover(m1, hass, zha_device_joined_restored, zigpy_cover_device): "zigpy.zcl.Cluster.request", return_value=mock_coro([0x0, zcl_f.Status.SUCCESS]) ): await hass.services.async_call( - DOMAIN, SERVICE_OPEN_COVER, {"entity_id": entity_id}, blocking=True + Platform.COVER, SERVICE_OPEN_COVER, {"entity_id": entity_id}, blocking=True ) assert cluster.request.call_count == 1 assert cluster.request.call_args[0][0] is False @@ -166,7 +166,7 @@ async def test_cover(m1, hass, zha_device_joined_restored, zigpy_cover_device): "zigpy.zcl.Cluster.request", return_value=mock_coro([0x5, zcl_f.Status.SUCCESS]) ): await hass.services.async_call( - DOMAIN, + Platform.COVER, SERVICE_SET_COVER_POSITION, {"entity_id": entity_id, "position": 47}, blocking=True, @@ -183,7 +183,7 @@ async def test_cover(m1, hass, zha_device_joined_restored, zigpy_cover_device): "zigpy.zcl.Cluster.request", return_value=mock_coro([0x2, zcl_f.Status.SUCCESS]) ): await hass.services.async_call( - DOMAIN, SERVICE_STOP_COVER, {"entity_id": entity_id}, blocking=True + Platform.COVER, SERVICE_STOP_COVER, {"entity_id": entity_id}, blocking=True ) assert cluster.request.call_count == 1 assert cluster.request.call_args[0][0] is False @@ -204,7 +204,7 @@ async def test_shade(hass, zha_device_joined_restored, zigpy_shade_device): cluster_on_off = zigpy_shade_device.endpoints.get(1).on_off cluster_level = zigpy_shade_device.endpoints.get(1).level - entity_id = await find_entity_id(DOMAIN, zha_device, hass) + entity_id = await find_entity_id(Platform.COVER, zha_device, hass) assert entity_id is not None await async_enable_traffic(hass, [zha_device], enabled=False) @@ -226,7 +226,7 @@ async def test_shade(hass, zha_device_joined_restored, zigpy_shade_device): # close from UI command fails with patch("zigpy.zcl.Cluster.request", side_effect=asyncio.TimeoutError): await hass.services.async_call( - DOMAIN, SERVICE_CLOSE_COVER, {"entity_id": entity_id}, blocking=True + Platform.COVER, SERVICE_CLOSE_COVER, {"entity_id": entity_id}, blocking=True ) assert cluster_on_off.request.call_count == 1 assert cluster_on_off.request.call_args[0][0] is False @@ -237,7 +237,7 @@ async def test_shade(hass, zha_device_joined_restored, zigpy_shade_device): "zigpy.zcl.Cluster.request", AsyncMock(return_value=[0x1, zcl_f.Status.SUCCESS]) ): await hass.services.async_call( - DOMAIN, SERVICE_CLOSE_COVER, {"entity_id": entity_id}, blocking=True + Platform.COVER, SERVICE_CLOSE_COVER, {"entity_id": entity_id}, blocking=True ) assert cluster_on_off.request.call_count == 1 assert cluster_on_off.request.call_args[0][0] is False @@ -249,7 +249,7 @@ async def test_shade(hass, zha_device_joined_restored, zigpy_shade_device): await send_attributes_report(hass, cluster_level, {0: 0}) with patch("zigpy.zcl.Cluster.request", side_effect=asyncio.TimeoutError): await hass.services.async_call( - DOMAIN, SERVICE_OPEN_COVER, {"entity_id": entity_id}, blocking=True + Platform.COVER, SERVICE_OPEN_COVER, {"entity_id": entity_id}, blocking=True ) assert cluster_on_off.request.call_count == 1 assert cluster_on_off.request.call_args[0][0] is False @@ -261,7 +261,7 @@ async def test_shade(hass, zha_device_joined_restored, zigpy_shade_device): "zigpy.zcl.Cluster.request", AsyncMock(return_value=[0x0, zcl_f.Status.SUCCESS]) ): await hass.services.async_call( - DOMAIN, SERVICE_OPEN_COVER, {"entity_id": entity_id}, blocking=True + Platform.COVER, SERVICE_OPEN_COVER, {"entity_id": entity_id}, blocking=True ) assert cluster_on_off.request.call_count == 1 assert cluster_on_off.request.call_args[0][0] is False @@ -271,7 +271,7 @@ async def test_shade(hass, zha_device_joined_restored, zigpy_shade_device): # set position UI command fails with patch("zigpy.zcl.Cluster.request", side_effect=asyncio.TimeoutError): await hass.services.async_call( - DOMAIN, + Platform.COVER, SERVICE_SET_COVER_POSITION, {"entity_id": entity_id, "position": 47}, blocking=True, @@ -287,7 +287,7 @@ async def test_shade(hass, zha_device_joined_restored, zigpy_shade_device): "zigpy.zcl.Cluster.request", AsyncMock(return_value=[0x5, zcl_f.Status.SUCCESS]) ): await hass.services.async_call( - DOMAIN, + Platform.COVER, SERVICE_SET_COVER_POSITION, {"entity_id": entity_id, "position": 47}, blocking=True, @@ -313,7 +313,7 @@ async def test_shade(hass, zha_device_joined_restored, zigpy_shade_device): # test cover stop with patch("zigpy.zcl.Cluster.request", side_effect=asyncio.TimeoutError): await hass.services.async_call( - DOMAIN, + Platform.COVER, SERVICE_STOP_COVER, {"entity_id": entity_id}, blocking=True, @@ -340,7 +340,7 @@ async def test_restore_state(hass, zha_device_restored, zigpy_shade_device): hass.state = CoreState.starting zha_device = await zha_device_restored(zigpy_shade_device) - entity_id = await find_entity_id(DOMAIN, zha_device, hass) + entity_id = await find_entity_id(Platform.COVER, zha_device, hass) assert entity_id is not None # test that the cover was created and that it is unavailable @@ -356,7 +356,7 @@ async def test_keen_vent(hass, zha_device_joined_restored, zigpy_keen_vent): cluster_on_off = zigpy_keen_vent.endpoints.get(1).on_off cluster_level = zigpy_keen_vent.endpoints.get(1).level - entity_id = await find_entity_id(DOMAIN, zha_device, hass) + entity_id = await find_entity_id(Platform.COVER, zha_device, hass) assert entity_id is not None await async_enable_traffic(hass, [zha_device], enabled=False) @@ -377,7 +377,7 @@ async def test_keen_vent(hass, zha_device_joined_restored, zigpy_keen_vent): with p1, p2: await hass.services.async_call( - DOMAIN, SERVICE_OPEN_COVER, {"entity_id": entity_id}, blocking=True + Platform.COVER, SERVICE_OPEN_COVER, {"entity_id": entity_id}, blocking=True ) assert cluster_on_off.request.call_count == 1 assert cluster_on_off.request.call_args[0][0] is False @@ -391,7 +391,7 @@ async def test_keen_vent(hass, zha_device_joined_restored, zigpy_keen_vent): with p1, p2: await hass.services.async_call( - DOMAIN, SERVICE_OPEN_COVER, {"entity_id": entity_id}, blocking=True + Platform.COVER, SERVICE_OPEN_COVER, {"entity_id": entity_id}, blocking=True ) await asyncio.sleep(0) assert cluster_on_off.request.call_count == 1 diff --git a/tests/components/zha/test_device_tracker.py b/tests/components/zha/test_device_tracker.py index 60dd136b9fd..e345918179e 100644 --- a/tests/components/zha/test_device_tracker.py +++ b/tests/components/zha/test_device_tracker.py @@ -6,11 +6,11 @@ import pytest import zigpy.profiles.zha import zigpy.zcl.clusters.general as general -from homeassistant.components.device_tracker import DOMAIN, SOURCE_TYPE_ROUTER +from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER from homeassistant.components.zha.core.registries import ( SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE, ) -from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNAVAILABLE +from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNAVAILABLE, Platform import homeassistant.util.dt as dt_util from .common import ( @@ -49,7 +49,7 @@ async def test_device_tracker(hass, zha_device_joined_restored, zigpy_device_dt) zha_device = await zha_device_joined_restored(zigpy_device_dt) cluster = zigpy_device_dt.endpoints.get(1).power - entity_id = await find_entity_id(DOMAIN, zha_device, hass) + entity_id = await find_entity_id(Platform.DEVICE_TRACKER, zha_device, hass) assert entity_id is not None assert hass.states.get(entity_id).state == STATE_HOME @@ -80,7 +80,7 @@ async def test_device_tracker(hass, zha_device_joined_restored, zigpy_device_dt) assert hass.states.get(entity_id).state == STATE_HOME - entity = hass.data[DOMAIN].get_entity(entity_id) + entity = hass.data[Platform.DEVICE_TRACKER].get_entity(entity_id) assert entity.is_connected is True assert entity.source_type == SOURCE_TYPE_ROUTER diff --git a/tests/components/zha/test_discover.py b/tests/components/zha/test_discover.py index d765ede0e5f..6b34590f4ad 100644 --- a/tests/components/zha/test_discover.py +++ b/tests/components/zha/test_discover.py @@ -27,6 +27,7 @@ import homeassistant.components.zha.light import homeassistant.components.zha.lock import homeassistant.components.zha.sensor import homeassistant.components.zha.switch +from homeassistant.const import Platform import homeassistant.helpers.entity_registry from .common import get_zha_gateway @@ -150,7 +151,7 @@ async def test_devices( ha_comp, ha_unique_id, ha_channels = ha_ent_info[ (test_unique_id_head, test_ent_class) ] - assert component is ha_comp + assert component is ha_comp.value # unique_id used for discover is the same for "multi entities" assert unique_id.startswith(ha_unique_id) assert {ch.name for ch in ha_channels} == set(ent_info[DEV_SIG_CHANNELS]) @@ -193,9 +194,9 @@ def test_discover_entities(m1, m2): @pytest.mark.parametrize( "device_type, component, hit", [ - (zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT, zha_const.LIGHT, True), - (zigpy.profiles.zha.DeviceType.ON_OFF_BALLAST, zha_const.SWITCH, True), - (zigpy.profiles.zha.DeviceType.SMART_PLUG, zha_const.SWITCH, True), + (zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT, Platform.LIGHT, True), + (zigpy.profiles.zha.DeviceType.ON_OFF_BALLAST, Platform.SWITCH, True), + (zigpy.profiles.zha.DeviceType.SMART_PLUG, Platform.SWITCH, True), (0xFFFF, None, False), ], ) @@ -234,7 +235,7 @@ def test_discover_by_device_type_override(): ep_mock.return_value.device_type = 0x0100 type(ep_channels).endpoint = ep_mock - overrides = {ep_channels.unique_id: {"type": zha_const.SWITCH}} + overrides = {ep_channels.unique_id: {"type": Platform.SWITCH}} get_entity_mock = mock.MagicMock( return_value=(mock.sentinel.entity_cls, mock.sentinel.claimed) ) @@ -247,7 +248,7 @@ def test_discover_by_device_type_override(): assert ep_channels.claim_channels.call_count == 1 assert ep_channels.claim_channels.call_args[0][0] is mock.sentinel.claimed assert ep_channels.async_new_entity.call_count == 1 - assert ep_channels.async_new_entity.call_args[0][0] == zha_const.SWITCH + assert ep_channels.async_new_entity.call_args[0][0] == Platform.SWITCH assert ep_channels.async_new_entity.call_args[0][1] == mock.sentinel.entity_cls @@ -268,13 +269,13 @@ def test_discover_probe_single_cluster(): "homeassistant.components.zha.core.registries.ZHA_ENTITIES.get_entity", get_entity_mock, ): - disc.PROBE.probe_single_cluster(zha_const.SWITCH, channel_mock, ep_channels) + disc.PROBE.probe_single_cluster(Platform.SWITCH, channel_mock, ep_channels) assert get_entity_mock.call_count == 1 assert ep_channels.claim_channels.call_count == 1 assert ep_channels.claim_channels.call_args[0][0] is mock.sentinel.claimed assert ep_channels.async_new_entity.call_count == 1 - assert ep_channels.async_new_entity.call_args[0][0] == zha_const.SWITCH + assert ep_channels.async_new_entity.call_args[0][0] == Platform.SWITCH assert ep_channels.async_new_entity.call_args[0][1] == mock.sentinel.entity_cls assert ep_channels.async_new_entity.call_args[0][3] == mock.sentinel.claimed @@ -320,7 +321,7 @@ async def test_discover_endpoint(device_info, channels_mock, hass): ha_comp, ha_unique_id, ha_channels = ha_ent_info[ (test_unique_id_head, test_ent_class) ] - assert component is ha_comp + assert component is ha_comp.value # unique_id used for discover is the same for "multi entities" assert unique_id.startswith(ha_unique_id) assert {ch.name for ch in ha_channels} == set(ent_info[DEV_SIG_CHANNELS]) @@ -372,11 +373,11 @@ def _test_single_input_cluster_device_class(probe_mock): disc.ProbeEndpoint().discover_by_cluster_id(ch_pool) assert probe_mock.call_count == len(ch_pool.unclaimed_channels()) probes = ( - (zha_const.LOCK, door_ch), - (zha_const.COVER, cover_ch), - (zha_const.SENSOR, multistate_ch), - (zha_const.BINARY_SENSOR, ias_ch), - (zha_const.SENSOR, analog_ch), + (Platform.LOCK, door_ch), + (Platform.COVER, cover_ch), + (Platform.SENSOR, multistate_ch), + (Platform.BINARY_SENSOR, ias_ch), + (Platform.SENSOR, analog_ch), ) for call, details in zip(probe_mock.call_args_list, probes): component, ch = details @@ -392,11 +393,11 @@ def test_single_input_cluster_device_class(): def test_single_input_cluster_device_class_by_cluster_class(): """Test SINGLE_INPUT_CLUSTER_DEVICE_CLASS matching by cluster id or class.""" mock_reg = { - zigpy.zcl.clusters.closures.DoorLock.cluster_id: zha_const.LOCK, - zigpy.zcl.clusters.closures.WindowCovering.cluster_id: zha_const.COVER, - zigpy.zcl.clusters.general.AnalogInput: zha_const.SENSOR, - zigpy.zcl.clusters.general.MultistateInput: zha_const.SENSOR, - zigpy.zcl.clusters.security.IasZone: zha_const.BINARY_SENSOR, + zigpy.zcl.clusters.closures.DoorLock.cluster_id: Platform.LOCK, + zigpy.zcl.clusters.closures.WindowCovering.cluster_id: Platform.COVER, + zigpy.zcl.clusters.general.AnalogInput: Platform.SENSOR, + zigpy.zcl.clusters.general.MultistateInput: Platform.SENSOR, + zigpy.zcl.clusters.security.IasZone: Platform.BINARY_SENSOR, } with mock.patch.dict( diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py index 212152e231d..e94c028acd8 100644 --- a/tests/components/zha/test_fan.py +++ b/tests/components/zha/test_fan.py @@ -14,7 +14,6 @@ from homeassistant.components.fan import ( ATTR_PERCENTAGE_STEP, ATTR_PRESET_MODE, ATTR_SPEED, - DOMAIN, SERVICE_SET_PRESET_MODE, SERVICE_SET_SPEED, SPEED_HIGH, @@ -23,7 +22,6 @@ from homeassistant.components.fan import ( SPEED_OFF, NotValidPresetModeError, ) -from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.zha.core.discovery import GROUP_PROBE from homeassistant.components.zha.core.group import GroupMember from homeassistant.components.zha.fan import ( @@ -38,6 +36,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNAVAILABLE, + Platform, ) from homeassistant.setup import async_setup_component @@ -151,7 +150,7 @@ async def test_fan(hass, zha_device_joined_restored, zigpy_device): zha_device = await zha_device_joined_restored(zigpy_device) cluster = zigpy_device.endpoints.get(1).fan - entity_id = await find_entity_id(DOMAIN, zha_device, hass) + entity_id = await find_entity_id(Platform.FAN, zha_device, hass) assert entity_id is not None assert hass.states.get(entity_id).state == STATE_OFF @@ -217,14 +216,14 @@ async def async_turn_on(hass, entity_id, speed=None): if value is not None } - await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data, blocking=True) + await hass.services.async_call(Platform.FAN, SERVICE_TURN_ON, data, blocking=True) async def async_turn_off(hass, entity_id): """Turn fan off.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - await hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data, blocking=True) + await hass.services.async_call(Platform.FAN, SERVICE_TURN_OFF, data, blocking=True) async def async_set_speed(hass, entity_id, speed=None): @@ -235,7 +234,7 @@ async def async_set_speed(hass, entity_id, speed=None): if value is not None } - await hass.services.async_call(DOMAIN, SERVICE_SET_SPEED, data, blocking=True) + await hass.services.async_call(Platform.FAN, SERVICE_SET_SPEED, data, blocking=True) async def async_set_preset_mode(hass, entity_id, preset_mode=None): @@ -246,7 +245,9 @@ async def async_set_preset_mode(hass, entity_id, preset_mode=None): if value is not None } - await hass.services.async_call(DOMAIN, SERVICE_SET_PRESET_MODE, data, blocking=True) + await hass.services.async_call( + Platform.FAN, SERVICE_SET_PRESET_MODE, data, blocking=True + ) @patch( @@ -282,10 +283,10 @@ async def test_zha_group_fan_entity(hass, device_fan_1, device_fan_2, coordinato entity_domains = GROUP_PROBE.determine_entity_domains(hass, zha_group) assert len(entity_domains) == 2 - assert LIGHT_DOMAIN in entity_domains - assert DOMAIN in entity_domains + assert Platform.LIGHT in entity_domains + assert Platform.FAN in entity_domains - entity_id = async_find_group_entity_id(hass, DOMAIN, zha_group) + entity_id = async_find_group_entity_id(hass, Platform.FAN, zha_group) assert hass.states.get(entity_id) is not None group_fan_cluster = zha_group.endpoint[hvac.Fan.cluster_id] @@ -396,10 +397,10 @@ async def test_zha_group_fan_entity_failure_state( entity_domains = GROUP_PROBE.determine_entity_domains(hass, zha_group) assert len(entity_domains) == 2 - assert LIGHT_DOMAIN in entity_domains - assert DOMAIN in entity_domains + assert Platform.LIGHT in entity_domains + assert Platform.FAN in entity_domains - entity_id = async_find_group_entity_id(hass, DOMAIN, zha_group) + entity_id = async_find_group_entity_id(hass, Platform.FAN, zha_group) assert hass.states.get(entity_id) is not None group_fan_cluster = zha_group.endpoint[hvac.Fan.cluster_id] @@ -450,7 +451,7 @@ async def test_fan_init( cluster.PLUGGED_ATTR_READS = plug_read zha_device = await zha_device_joined_restored(zigpy_device) - entity_id = await find_entity_id(DOMAIN, zha_device, hass) + entity_id = await find_entity_id(Platform.FAN, zha_device, hass) assert entity_id is not None assert hass.states.get(entity_id).state == expected_state assert hass.states.get(entity_id).attributes[ATTR_SPEED] == expected_speed @@ -469,7 +470,7 @@ async def test_fan_update_entity( cluster.PLUGGED_ATTR_READS = {"fan_mode": 0} zha_device = await zha_device_joined_restored(zigpy_device) - entity_id = await find_entity_id(DOMAIN, zha_device, hass) + entity_id = await find_entity_id(Platform.FAN, zha_device, hass) assert entity_id is not None assert hass.states.get(entity_id).state == STATE_OFF assert hass.states.get(entity_id).attributes[ATTR_SPEED] == SPEED_OFF diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index 6662f0c2c9f..a263a6d7ed3 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -8,9 +8,9 @@ import zigpy.profiles.zha as zha import zigpy.zcl.clusters.general as general import zigpy.zcl.clusters.lighting as lighting -from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.zha.core.group import GroupMember from homeassistant.components.zha.core.store import TOMBSTONE_LIFETIME +from homeassistant.const import Platform from .common import async_enable_traffic, async_find_group_entity_id, get_zha_gateway from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE @@ -144,7 +144,7 @@ async def test_gateway_group_methods(hass, device_light_1, device_light_2, coord for member in zha_group.members: assert member.device.ieee in member_ieee_addresses - entity_id = async_find_group_entity_id(hass, LIGHT_DOMAIN, zha_group) + entity_id = async_find_group_entity_id(hass, Platform.LIGHT, zha_group) assert hass.states.get(entity_id) is not None # test get group by name @@ -158,7 +158,7 @@ async def test_gateway_group_methods(hass, device_light_1, device_light_2, coord assert zha_gateway.async_get_group_by_name(zha_group.name) is None # the group entity should be cleaned up - assert entity_id not in hass.states.async_entity_ids(LIGHT_DOMAIN) + assert entity_id not in hass.states.async_entity_ids(Platform.LIGHT) # test creating a group with 1 member zha_group = await zha_gateway.async_create_zigpy_group( @@ -172,7 +172,7 @@ async def test_gateway_group_methods(hass, device_light_1, device_light_2, coord assert member.device.ieee in [device_light_1.ieee] # the group entity should not have been cleaned up - assert entity_id not in hass.states.async_entity_ids(LIGHT_DOMAIN) + assert entity_id not in hass.states.async_entity_ids(Platform.LIGHT) with patch("zigpy.zcl.Cluster.request", side_effect=asyncio.TimeoutError): await zha_group.members[0].async_remove_from_group() diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index d1a3b5dabb0..ee437fc63c9 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -9,10 +9,10 @@ import zigpy.zcl.clusters.general as general import zigpy.zcl.clusters.lighting as lighting import zigpy.zcl.foundation as zcl_f -from homeassistant.components.light import DOMAIN, FLASH_LONG, FLASH_SHORT +from homeassistant.components.light import FLASH_LONG, FLASH_SHORT from homeassistant.components.zha.core.group import GroupMember from homeassistant.components.zha.light import FLASH_EFFECTS -from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform import homeassistant.util.dt as dt_util from .common import ( @@ -187,7 +187,7 @@ async def test_light_refresh(hass, zigpy_device_mock, zha_device_joined_restored on_off_cluster = zigpy_device.endpoints[1].on_off on_off_cluster.PLUGGED_ATTR_READS = {"on_off": 0} zha_device = await zha_device_joined_restored(zigpy_device) - entity_id = await find_entity_id(DOMAIN, zha_device, hass) + entity_id = await find_entity_id(Platform.LIGHT, zha_device, hass) # allow traffic to flow through the gateway and device await async_enable_traffic(hass, [zha_device]) @@ -245,7 +245,7 @@ async def test_light( # create zigpy devices zigpy_device = zigpy_device_mock(device) zha_device = await zha_device_joined_restored(zigpy_device) - entity_id = await find_entity_id(DOMAIN, zha_device, hass) + entity_id = await find_entity_id(Platform.LIGHT, zha_device, hass) assert entity_id is not None @@ -327,7 +327,7 @@ async def async_test_on_off_from_hass(hass, cluster, entity_id): # turn on via UI cluster.request.reset_mock() await hass.services.async_call( - DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True + Platform.LIGHT, "turn_on", {"entity_id": entity_id}, blocking=True ) assert cluster.request.call_count == 1 assert cluster.request.await_count == 1 @@ -344,7 +344,7 @@ async def async_test_off_from_hass(hass, cluster, entity_id): # turn off via UI cluster.request.reset_mock() await hass.services.async_call( - DOMAIN, "turn_off", {"entity_id": entity_id}, blocking=True + Platform.LIGHT, "turn_off", {"entity_id": entity_id}, blocking=True ) assert cluster.request.call_count == 1 assert cluster.request.await_count == 1 @@ -362,7 +362,7 @@ async def async_test_level_on_off_from_hass( level_cluster.request.reset_mock() # turn on via UI await hass.services.async_call( - DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True + Platform.LIGHT, "turn_on", {"entity_id": entity_id}, blocking=True ) assert on_off_cluster.request.call_count == 1 assert on_off_cluster.request.await_count == 1 @@ -375,7 +375,10 @@ async def async_test_level_on_off_from_hass( level_cluster.request.reset_mock() await hass.services.async_call( - DOMAIN, "turn_on", {"entity_id": entity_id, "transition": 10}, blocking=True + Platform.LIGHT, + "turn_on", + {"entity_id": entity_id, "transition": 10}, + blocking=True, ) assert on_off_cluster.request.call_count == 1 assert on_off_cluster.request.await_count == 1 @@ -399,7 +402,10 @@ async def async_test_level_on_off_from_hass( level_cluster.request.reset_mock() await hass.services.async_call( - DOMAIN, "turn_on", {"entity_id": entity_id, "brightness": 10}, blocking=True + Platform.LIGHT, + "turn_on", + {"entity_id": entity_id, "brightness": 10}, + blocking=True, ) # the onoff cluster is now not used when brightness is present by default assert on_off_cluster.request.call_count == 0 @@ -442,7 +448,10 @@ async def async_test_flash_from_hass(hass, cluster, entity_id, flash): # turn on via UI cluster.request.reset_mock() await hass.services.async_call( - DOMAIN, "turn_on", {"entity_id": entity_id, "flash": flash}, blocking=True + Platform.LIGHT, + "turn_on", + {"entity_id": entity_id, "flash": flash}, + blocking=True, ) assert cluster.request.call_count == 1 assert cluster.request.await_count == 1 @@ -505,9 +514,9 @@ async def test_zha_group_light_entity( assert member.group == zha_group assert member.endpoint is not None - device_1_entity_id = await find_entity_id(DOMAIN, device_light_1, hass) - device_2_entity_id = await find_entity_id(DOMAIN, device_light_2, hass) - device_3_entity_id = await find_entity_id(DOMAIN, device_light_3, hass) + device_1_entity_id = await find_entity_id(Platform.LIGHT, device_light_1, hass) + device_2_entity_id = await find_entity_id(Platform.LIGHT, device_light_2, hass) + device_3_entity_id = await find_entity_id(Platform.LIGHT, device_light_3, hass) assert ( device_1_entity_id != device_2_entity_id @@ -515,7 +524,7 @@ async def test_zha_group_light_entity( ) assert device_2_entity_id != device_3_entity_id - group_entity_id = async_find_group_entity_id(hass, DOMAIN, zha_group) + group_entity_id = async_find_group_entity_id(hass, Platform.LIGHT, zha_group) assert hass.states.get(group_entity_id) is not None assert device_1_entity_id in zha_group.member_entity_ids diff --git a/tests/components/zha/test_lock.py b/tests/components/zha/test_lock.py index ca9b7961d38..c8685996c25 100644 --- a/tests/components/zha/test_lock.py +++ b/tests/components/zha/test_lock.py @@ -7,8 +7,12 @@ import zigpy.zcl.clusters.closures as closures import zigpy.zcl.clusters.general as general import zigpy.zcl.foundation as zcl_f -from homeassistant.components.lock import DOMAIN -from homeassistant.const import STATE_LOCKED, STATE_UNAVAILABLE, STATE_UNLOCKED +from homeassistant.const import ( + STATE_LOCKED, + STATE_UNAVAILABLE, + STATE_UNLOCKED, + Platform, +) from .common import async_enable_traffic, find_entity_id, send_attributes_report from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_TYPE @@ -44,7 +48,7 @@ async def test_lock(hass, lock): """Test zha lock platform.""" zha_device, cluster = lock - entity_id = await find_entity_id(DOMAIN, zha_device, hass) + entity_id = await find_entity_id(Platform.LOCK, zha_device, hass) assert entity_id is not None assert hass.states.get(entity_id).state == STATE_UNLOCKED @@ -92,7 +96,7 @@ async def async_lock(hass, cluster, entity_id): ): # lock via UI await hass.services.async_call( - DOMAIN, "lock", {"entity_id": entity_id}, blocking=True + Platform.LOCK, "lock", {"entity_id": entity_id}, blocking=True ) assert cluster.request.call_count == 1 assert cluster.request.call_args[0][0] is False @@ -106,7 +110,7 @@ async def async_unlock(hass, cluster, entity_id): ): # lock via UI await hass.services.async_call( - DOMAIN, "unlock", {"entity_id": entity_id}, blocking=True + Platform.LOCK, "unlock", {"entity_id": entity_id}, blocking=True ) assert cluster.request.call_count == 1 assert cluster.request.call_args[0][0] is False diff --git a/tests/components/zha/test_number.py b/tests/components/zha/test_number.py index c27cd9fd654..ac72a00d802 100644 --- a/tests/components/zha/test_number.py +++ b/tests/components/zha/test_number.py @@ -7,8 +7,7 @@ import zigpy.types import zigpy.zcl.clusters.general as general import zigpy.zcl.foundation as zcl_f -from homeassistant.components.number import DOMAIN -from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.const import STATE_UNAVAILABLE, Platform from homeassistant.setup import async_setup_component from .common import ( @@ -67,7 +66,7 @@ async def test_number(hass, zha_device_joined_restored, zigpy_analog_output_devi assert "engineering_units" in attr_reads assert "application_type" in attr_reads - entity_id = await find_entity_id(DOMAIN, zha_device, hass) + entity_id = await find_entity_id(Platform.NUMBER, zha_device, hass) assert entity_id is not None await async_enable_traffic(hass, [zha_device], enabled=False) @@ -110,7 +109,10 @@ async def test_number(hass, zha_device_joined_restored, zigpy_analog_output_devi ): # set value via UI await hass.services.async_call( - DOMAIN, "set_value", {"entity_id": entity_id, "value": 30.0}, blocking=True + Platform.NUMBER, + "set_value", + {"entity_id": entity_id, "value": 30.0}, + blocking=True, ) assert len(cluster.write_attributes.mock_calls) == 1 assert cluster.write_attributes.call_args == call({"present_value": 30.0}) diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index af4bb082b03..468458cdc51 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -8,7 +8,6 @@ import zigpy.zcl.clusters.homeautomation as homeautomation import zigpy.zcl.clusters.measurement as measurement import zigpy.zcl.clusters.smartenergy as smartenergy -from homeassistant.components.sensor import DOMAIN from homeassistant.components.zha.core.const import ZHA_CHANNEL_READS_PER_REQ import homeassistant.config as config_util from homeassistant.const import ( @@ -32,6 +31,7 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS, + Platform, ) from homeassistant.helpers import restore_state from homeassistant.helpers.entity_component import async_update_entity @@ -505,7 +505,7 @@ async def test_temp_uom( ) cluster = zigpy_device.endpoints[1].temperature zha_device = await zha_device_restored(zigpy_device) - entity_id = await find_entity_id(DOMAIN, zha_device, hass) + entity_id = await find_entity_id(Platform.SENSOR, zha_device, hass) if not restore: await async_enable_traffic(hass, [zha_device], enabled=False) @@ -545,7 +545,7 @@ async def test_electrical_measurement_init( ) cluster = zigpy_device.endpoints[1].in_clusters[cluster_id] zha_device = await zha_device_joined(zigpy_device) - entity_id = await find_entity_id(DOMAIN, zha_device, hass) + entity_id = await find_entity_id(Platform.SENSOR, zha_device, hass) # allow traffic to flow through the gateway and devices await async_enable_traffic(hass, [zha_device]) @@ -681,7 +681,7 @@ async def test_unsupported_attributes_sensor( await async_enable_traffic(hass, [zha_device], enabled=False) await hass.async_block_till_done() - present_entity_ids = set(await find_entity_ids(DOMAIN, zha_device, hass)) + present_entity_ids = set(await find_entity_ids(Platform.SENSOR, zha_device, hass)) assert present_entity_ids == entity_ids assert missing_entity_ids not in present_entity_ids diff --git a/tests/components/zha/test_siren.py b/tests/components/zha/test_siren.py index d9c173fd932..1414879c0f0 100644 --- a/tests/components/zha/test_siren.py +++ b/tests/components/zha/test_siren.py @@ -9,7 +9,6 @@ import zigpy.zcl.clusters.general as general import zigpy.zcl.clusters.security as security import zigpy.zcl.foundation as zcl_f -from homeassistant.components.siren import DOMAIN from homeassistant.components.siren.const import ( ATTR_DURATION, ATTR_TONE, @@ -19,7 +18,7 @@ from homeassistant.components.zha.core.const import ( WARNING_DEVICE_MODE_EMERGENCY_PANIC, WARNING_DEVICE_SOUND_MEDIUM, ) -from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform import homeassistant.util.dt as dt_util from .common import async_enable_traffic, find_entity_id @@ -52,7 +51,7 @@ async def test_siren(hass, siren): zha_device, cluster = siren assert cluster is not None - entity_id = await find_entity_id(DOMAIN, zha_device, hass) + entity_id = await find_entity_id(Platform.SIREN, zha_device, hass) assert entity_id is not None assert hass.states.get(entity_id).state == STATE_OFF @@ -73,7 +72,7 @@ async def test_siren(hass, siren): ): # turn on via UI await hass.services.async_call( - DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True + Platform.SIREN, "turn_on", {"entity_id": entity_id}, blocking=True ) assert len(cluster.request.mock_calls) == 1 assert cluster.request.call_args[0][0] is False @@ -93,7 +92,7 @@ async def test_siren(hass, siren): ): # turn off via UI await hass.services.async_call( - DOMAIN, "turn_off", {"entity_id": entity_id}, blocking=True + Platform.SIREN, "turn_off", {"entity_id": entity_id}, blocking=True ) assert len(cluster.request.mock_calls) == 1 assert cluster.request.call_args[0][0] is False @@ -113,7 +112,7 @@ async def test_siren(hass, siren): ): # turn on via UI await hass.services.async_call( - DOMAIN, + Platform.SIREN, "turn_on", { "entity_id": entity_id, diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py index 04f43344b98..879bc26db9f 100644 --- a/tests/components/zha/test_switch.py +++ b/tests/components/zha/test_switch.py @@ -6,9 +6,8 @@ import zigpy.profiles.zha as zha import zigpy.zcl.clusters.general as general import zigpy.zcl.foundation as zcl_f -from homeassistant.components.switch import DOMAIN from homeassistant.components.zha.core.group import GroupMember -from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform from .common import ( async_enable_traffic, @@ -108,7 +107,7 @@ async def test_switch(hass, zha_device_joined_restored, zigpy_device): zha_device = await zha_device_joined_restored(zigpy_device) cluster = zigpy_device.endpoints.get(1).on_off - entity_id = await find_entity_id(DOMAIN, zha_device, hass) + entity_id = await find_entity_id(Platform.SWITCH, zha_device, hass) assert entity_id is not None assert hass.states.get(entity_id).state == STATE_OFF @@ -137,7 +136,7 @@ async def test_switch(hass, zha_device_joined_restored, zigpy_device): ): # turn on via UI await hass.services.async_call( - DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True + Platform.SWITCH, "turn_on", {"entity_id": entity_id}, blocking=True ) assert len(cluster.request.mock_calls) == 1 assert cluster.request.call_args == call( @@ -151,7 +150,7 @@ async def test_switch(hass, zha_device_joined_restored, zigpy_device): ): # turn off via UI await hass.services.async_call( - DOMAIN, "turn_off", {"entity_id": entity_id}, blocking=True + Platform.SWITCH, "turn_off", {"entity_id": entity_id}, blocking=True ) assert len(cluster.request.mock_calls) == 1 assert cluster.request.call_args == call( @@ -193,7 +192,7 @@ async def test_zha_group_switch_entity( assert member.group == zha_group assert member.endpoint is not None - entity_id = async_find_group_entity_id(hass, DOMAIN, zha_group) + entity_id = async_find_group_entity_id(hass, Platform.SWITCH, zha_group) assert hass.states.get(entity_id) is not None group_cluster_on_off = zha_group.endpoint[general.OnOff.cluster_id] @@ -220,7 +219,7 @@ async def test_zha_group_switch_entity( ): # turn on via UI await hass.services.async_call( - DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True + Platform.SWITCH, "turn_on", {"entity_id": entity_id}, blocking=True ) assert len(group_cluster_on_off.request.mock_calls) == 1 assert group_cluster_on_off.request.call_args == call( @@ -235,7 +234,7 @@ async def test_zha_group_switch_entity( ): # turn off via UI await hass.services.async_call( - DOMAIN, "turn_off", {"entity_id": entity_id}, blocking=True + Platform.SWITCH, "turn_off", {"entity_id": entity_id}, blocking=True ) assert len(group_cluster_on_off.request.mock_calls) == 1 assert group_cluster_on_off.request.call_args == call( From 0abfc90870be3a3db19421779954ad2a59ee7ab7 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sat, 11 Dec 2021 17:12:33 +0100 Subject: [PATCH 0321/2644] Fix typo in Hue device triggers - use enum value (#61498) --- homeassistant/components/hue/v2/device_trigger.py | 2 +- tests/components/hue/test_device_trigger_v2.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hue/v2/device_trigger.py b/homeassistant/components/hue/v2/device_trigger.py index 7a194bef746..74863a1897e 100644 --- a/homeassistant/components/hue/v2/device_trigger.py +++ b/homeassistant/components/hue/v2/device_trigger.py @@ -109,7 +109,7 @@ async def async_get_triggers(bridge: "HueBridge", device_entry: DeviceEntry): CONF_DEVICE_ID: device_entry.id, CONF_DOMAIN: DOMAIN, CONF_PLATFORM: "device", - CONF_TYPE: event_type, + CONF_TYPE: event_type.value, CONF_SUBTYPE: resource.metadata.control_id, CONF_UNIQUE_ID: resource.id, } diff --git a/tests/components/hue/test_device_trigger_v2.py b/tests/components/hue/test_device_trigger_v2.py index e155b0adb6d..0641281b9fa 100644 --- a/tests/components/hue/test_device_trigger_v2.py +++ b/tests/components/hue/test_device_trigger_v2.py @@ -71,7 +71,7 @@ async def test_get_triggers(hass, mock_bridge_v2, v2_resources_test_data, device "domain": hue.DOMAIN, "device_id": hue_wall_switch_device.id, "unique_id": resource_id, - "type": event_type, + "type": event_type.value, "subtype": control_id, } for event_type in ( From f6ac856b8dcbed0b80f0e5051a0a78730cb749a6 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sat, 11 Dec 2021 11:50:03 -0500 Subject: [PATCH 0322/2644] Use async_on_unload from config entry in ZHA (#61015) * remove DATA_ZHA_DISPATCHERS * update typing information * fix rebase --- homeassistant/components/zha/__init__.py | 17 ++++++++-------- .../components/zha/alarm_control_panel.py | 13 ++++++++---- homeassistant/components/zha/binary_sensor.py | 13 ++++++++---- homeassistant/components/zha/climate.py | 13 ++++++++---- homeassistant/components/zha/core/const.py | 1 - homeassistant/components/zha/cover.py | 13 ++++++++---- .../components/zha/device_tracker.py | 13 ++++++++---- homeassistant/components/zha/fan.py | 20 +++++++++---------- homeassistant/components/zha/light.py | 13 ++++++++---- homeassistant/components/zha/lock.py | 12 +++++++---- homeassistant/components/zha/number.py | 13 ++++++++---- homeassistant/components/zha/sensor.py | 3 +-- homeassistant/components/zha/switch.py | 13 ++++++++---- 13 files changed, 99 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index d6578be775f..42f14e63927 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -12,6 +12,7 @@ from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.typing import ConfigType from . import api from .core import ZHAGateway @@ -27,7 +28,6 @@ from .core.const import ( CONF_ZIGPY, DATA_ZHA, DATA_ZHA_CONFIG, - DATA_ZHA_DISPATCHERS, DATA_ZHA_GATEWAY, DATA_ZHA_PLATFORM_LOADED, DATA_ZHA_SHUTDOWN_TASK, @@ -72,7 +72,7 @@ CENTICELSIUS = "C-100" _LOGGER = logging.getLogger(__name__) -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: ConfigType): """Set up ZHA from config.""" hass.data[DATA_ZHA] = {} @@ -83,7 +83,9 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, config_entry): +async def async_setup_entry( + hass: HomeAssistant, config_entry: config_entries.ConfigEntry +): """Set up ZHA. Will automatically load components to support devices found on the network. @@ -101,7 +103,6 @@ async def async_setup_entry(hass, config_entry): zha_gateway = ZHAGateway(hass, config, config_entry) await zha_gateway.async_initialize() - zha_data[DATA_ZHA_DISPATCHERS] = [] zha_data[DATA_ZHA_PLATFORM_LOADED] = [] for platform in PLATFORMS: coro = hass.config_entries.async_forward_entry_setup(config_entry, platform) @@ -131,7 +132,9 @@ async def async_setup_entry(hass, config_entry): return True -async def async_unload_entry(hass, config_entry): +async def async_unload_entry( + hass: HomeAssistant, config_entry: config_entries.ConfigEntry +): """Unload ZHA config entry.""" await hass.data[DATA_ZHA][DATA_ZHA_GATEWAY].shutdown() await hass.data[DATA_ZHA][DATA_ZHA_GATEWAY].async_update_device_storage() @@ -139,10 +142,6 @@ async def async_unload_entry(hass, config_entry): GROUP_PROBE.cleanup() api.async_unload_api(hass) - dispatchers = hass.data[DATA_ZHA].get(DATA_ZHA_DISPATCHERS, []) - for unsub_dispatcher in dispatchers: - unsub_dispatcher() - # our components don't have unload methods so no need to look at return values await asyncio.gather( *( diff --git a/homeassistant/components/zha/alarm_control_panel.py b/homeassistant/components/zha/alarm_control_panel.py index c76e26baaf6..3e83a5cd3d1 100644 --- a/homeassistant/components/zha/alarm_control_panel.py +++ b/homeassistant/components/zha/alarm_control_panel.py @@ -12,6 +12,7 @@ from homeassistant.components.alarm_control_panel import ( AlarmControlPanelEntity, ) from homeassistant.components.zha.core.typing import ZhaDeviceType +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, @@ -20,8 +21,9 @@ from homeassistant.const import ( STATE_ALARM_TRIGGERED, Platform, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .core import discovery from .core.channels.security import ( @@ -35,7 +37,6 @@ from .core.const import ( CONF_ALARM_FAILED_TRIES, CONF_ALARM_MASTER_CODE, DATA_ZHA, - DATA_ZHA_DISPATCHERS, SIGNAL_ADD_ENTITIES, ZHA_ALARM_OPTIONS, ) @@ -56,7 +57,11 @@ IAS_ACE_STATE_MAP = { } -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +): """Set up the Zigbee Home Automation alarm control panel from config entry.""" entities_to_create = hass.data[DATA_ZHA][Platform.ALARM_CONTROL_PANEL] @@ -67,7 +72,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): discovery.async_add_entities, async_add_entities, entities_to_create ), ) - hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub) + config_entry.async_on_unload(unsub) @STRICT_MATCH(channel_names=CHANNEL_IAS_ACE) diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index 7e083a07d72..c0af9a807d5 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -12,9 +12,11 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_VIBRATION, BinarySensorEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_ON, Platform -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .core import discovery from .core.const import ( @@ -24,7 +26,6 @@ from .core.const import ( CHANNEL_ON_OFF, CHANNEL_ZONE, DATA_ZHA, - DATA_ZHA_DISPATCHERS, SIGNAL_ADD_ENTITIES, SIGNAL_ATTR_UPDATED, ) @@ -44,7 +45,11 @@ CLASS_MAPPING = { STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.BINARY_SENSOR) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +): """Set up the Zigbee Home Automation binary sensor from config entry.""" entities_to_create = hass.data[DATA_ZHA][Platform.BINARY_SENSOR] @@ -55,7 +60,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): discovery.async_add_entities, async_add_entities, entities_to_create ), ) - hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub) + config_entry.async_on_unload(unsub) class BinarySensor(ZhaEntity, BinarySensorEntity): diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index 7cb2c0f16e4..9c01f6630db 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -39,14 +39,16 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_TEMPERATURE, PRECISION_TENTHS, TEMP_CELSIUS, Platform, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval import homeassistant.util.dt as dt_util @@ -55,7 +57,6 @@ from .core.const import ( CHANNEL_FAN, CHANNEL_THERMOSTAT, DATA_ZHA, - DATA_ZHA_DISPATCHERS, PRESET_COMPLEX, PRESET_SCHEDULE, SIGNAL_ADD_ENTITIES, @@ -154,7 +155,11 @@ SYSTEM_MODE_2_HVAC = { ZCL_TEMP = 100 -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +): """Set up the Zigbee Home Automation sensor from config entry.""" entities_to_create = hass.data[DATA_ZHA][Platform.CLIMATE] unsub = async_dispatcher_connect( @@ -164,7 +169,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): discovery.async_add_entities, async_add_entities, entities_to_create ), ) - hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub) + config_entry.async_on_unload(unsub) @MULTI_MATCH(channel_names=CHANNEL_THERMOSTAT, aux_channels=CHANNEL_FAN) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 876d4c4e9f5..585124c3ee9 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -165,7 +165,6 @@ DATA_ZHA = "zha" DATA_ZHA_CONFIG = "config" DATA_ZHA_BRIDGE_ID = "zha_bridge_id" DATA_ZHA_CORE_EVENTS = "zha_core_events" -DATA_ZHA_DISPATCHERS = "zha_dispatchers" DATA_ZHA_GATEWAY = "zha_gateway" DATA_ZHA_PLATFORM_LOADED = "platform_loaded" DATA_ZHA_SHUTDOWN_TASK = "zha_shutdown_task" diff --git a/homeassistant/components/zha/cover.py b/homeassistant/components/zha/cover.py index 2febc209755..8b9bf5589b3 100644 --- a/homeassistant/components/zha/cover.py +++ b/homeassistant/components/zha/cover.py @@ -14,6 +14,7 @@ from homeassistant.components.cover import ( DEVICE_CLASS_SHADE, CoverEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( STATE_CLOSED, STATE_CLOSING, @@ -21,8 +22,9 @@ from homeassistant.const import ( STATE_OPENING, Platform, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .core import discovery from .core.const import ( @@ -31,7 +33,6 @@ from .core.const import ( CHANNEL_ON_OFF, CHANNEL_SHADE, DATA_ZHA, - DATA_ZHA_DISPATCHERS, SIGNAL_ADD_ENTITIES, SIGNAL_ATTR_UPDATED, SIGNAL_SET_LEVEL, @@ -45,7 +46,11 @@ _LOGGER = logging.getLogger(__name__) STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.COVER) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +): """Set up the Zigbee Home Automation cover from config entry.""" entities_to_create = hass.data[DATA_ZHA][Platform.COVER] @@ -56,7 +61,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): discovery.async_add_entities, async_add_entities, entities_to_create ), ) - hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub) + config_entry.async_on_unload(unsub) @STRICT_MATCH(channel_names=CHANNEL_COVER) diff --git a/homeassistant/components/zha/device_tracker.py b/homeassistant/components/zha/device_tracker.py index c203462d44d..4219bfd6288 100644 --- a/homeassistant/components/zha/device_tracker.py +++ b/homeassistant/components/zha/device_tracker.py @@ -4,15 +4,16 @@ import time from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER from homeassistant.components.device_tracker.config_entry import ScannerEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .core import discovery from .core.const import ( CHANNEL_POWER_CONFIGURATION, DATA_ZHA, - DATA_ZHA_DISPATCHERS, SIGNAL_ADD_ENTITIES, SIGNAL_ATTR_UPDATED, ) @@ -23,7 +24,11 @@ from .sensor import Battery STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.DEVICE_TRACKER) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +): """Set up the Zigbee Home Automation device tracker from config entry.""" entities_to_create = hass.data[DATA_ZHA][Platform.DEVICE_TRACKER] @@ -34,7 +39,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): discovery.async_add_entities, async_add_entities, entities_to_create ), ) - hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub) + config_entry.async_on_unload(unsub) @STRICT_MATCH(channel_names=CHANNEL_POWER_CONFIGURATION) diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index 0402e93ff8c..729a0f7e618 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -15,9 +15,11 @@ from homeassistant.components.fan import ( FanEntity, NotValidPresetModeError, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_UNAVAILABLE, Platform -from homeassistant.core import State, callback +from homeassistant.core import HomeAssistant, State, 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, @@ -25,13 +27,7 @@ from homeassistant.util.percentage import ( ) from .core import discovery -from .core.const import ( - CHANNEL_FAN, - DATA_ZHA, - DATA_ZHA_DISPATCHERS, - SIGNAL_ADD_ENTITIES, - SIGNAL_ATTR_UPDATED, -) +from .core.const import CHANNEL_FAN, DATA_ZHA, SIGNAL_ADD_ENTITIES, SIGNAL_ATTR_UPDATED from .core.registries import ZHA_ENTITIES from .entity import ZhaEntity, ZhaGroupEntity @@ -56,7 +52,11 @@ STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.FAN) GROUP_MATCH = functools.partial(ZHA_ENTITIES.group_match, Platform.FAN) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +): """Set up the Zigbee Home Automation fan from config entry.""" entities_to_create = hass.data[DATA_ZHA][Platform.FAN] @@ -70,7 +70,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): update_before_add=False, ), ) - hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub) + config_entry.async_on_unload(unsub) class BaseFan(FanEntity): diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 476bb733cc9..2c3b6249cf9 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -30,18 +30,20 @@ from homeassistant.components.light import ( SUPPORT_FLASH, SUPPORT_TRANSITION, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_UNAVAILABLE, Platform, ) -from homeassistant.core import State, callback +from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval import homeassistant.util.color as color_util @@ -52,7 +54,6 @@ from .core.const import ( CHANNEL_ON_OFF, CONF_DEFAULT_LIGHT_TRANSITION, DATA_ZHA, - DATA_ZHA_DISPATCHERS, EFFECT_BLINK, EFFECT_BREATHE, EFFECT_DEFAULT_VARIANT, @@ -105,7 +106,11 @@ class LightColorMode(enum.IntEnum): COLOR_TEMP = 0x02 -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +): """Set up the Zigbee Home Automation light from config entry.""" entities_to_create = hass.data[DATA_ZHA][Platform.LIGHT] @@ -116,7 +121,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): discovery.async_add_entities, async_add_entities, entities_to_create ), ) - hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub) + config_entry.async_on_unload(unsub) class BaseLight(LogMixin, light.LightEntity): diff --git a/homeassistant/components/zha/lock.py b/homeassistant/components/zha/lock.py index 0c8ca756785..61104a8b7e9 100644 --- a/homeassistant/components/zha/lock.py +++ b/homeassistant/components/zha/lock.py @@ -5,8 +5,9 @@ import voluptuous as vol from zigpy.zcl.foundation import Status from homeassistant.components.lock import STATE_LOCKED, STATE_UNLOCKED, LockEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -14,7 +15,6 @@ from .core import discovery from .core.const import ( CHANNEL_DOORLOCK, DATA_ZHA, - DATA_ZHA_DISPATCHERS, SIGNAL_ADD_ENTITIES, SIGNAL_ATTR_UPDATED, ) @@ -33,7 +33,11 @@ SERVICE_DISABLE_LOCK_USER_CODE = "disable_lock_user_code" SERVICE_CLEAR_LOCK_USER_CODE = "clear_lock_user_code" -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: entity_platform.AddEntitiesCallback, +): """Set up the Zigbee Home Automation Door Lock from config entry.""" entities_to_create = hass.data[DATA_ZHA][Platform.LOCK] @@ -44,7 +48,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): discovery.async_add_entities, async_add_entities, entities_to_create ), ) - hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub) + config_entry.async_on_unload(unsub) platform = entity_platform.async_get_current_platform() diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py index 0acda49c375..e3de49756c2 100644 --- a/homeassistant/components/zha/number.py +++ b/homeassistant/components/zha/number.py @@ -3,15 +3,16 @@ import functools import logging from homeassistant.components.number import NumberEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .core import discovery from .core.const import ( CHANNEL_ANALOG_OUTPUT, DATA_ZHA, - DATA_ZHA_DISPATCHERS, SIGNAL_ADD_ENTITIES, SIGNAL_ATTR_UPDATED, ) @@ -234,7 +235,11 @@ ICONS = { } -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +): """Set up the Zigbee Home Automation Analog Output from config entry.""" entities_to_create = hass.data[DATA_ZHA][Platform.NUMBER] @@ -248,7 +253,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): update_before_add=False, ), ) - hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub) + config_entry.async_on_unload(unsub) @STRICT_MATCH(channel_names=CHANNEL_ANALOG_OUTPUT) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index d4c508a8378..f14e8b561c3 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -72,7 +72,6 @@ from .core.const import ( CHANNEL_TEMPERATURE, CHANNEL_THERMOSTAT, DATA_ZHA, - DATA_ZHA_DISPATCHERS, SIGNAL_ADD_ENTITIES, SIGNAL_ATTR_UPDATED, ) @@ -121,7 +120,7 @@ async def async_setup_entry( update_before_add=False, ), ) - hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub) + config_entry.async_on_unload(unsub) class Sensor(ZhaEntity, SensorEntity): diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index da5e843c59f..b67ab72fa35 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -8,15 +8,16 @@ from zigpy.zcl.clusters.general import OnOff from zigpy.zcl.foundation import Status from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_ON, STATE_UNAVAILABLE, Platform -from homeassistant.core import State, callback +from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .core import discovery from .core.const import ( CHANNEL_ON_OFF, DATA_ZHA, - DATA_ZHA_DISPATCHERS, SIGNAL_ADD_ENTITIES, SIGNAL_ATTR_UPDATED, ) @@ -27,7 +28,11 @@ STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.SWITCH) GROUP_MATCH = functools.partial(ZHA_ENTITIES.group_match, Platform.SWITCH) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +): """Set up the Zigbee Home Automation switch from config entry.""" entities_to_create = hass.data[DATA_ZHA][Platform.SWITCH] @@ -38,7 +43,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): discovery.async_add_entities, async_add_entities, entities_to_create ), ) - hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub) + config_entry.async_on_unload(unsub) class BaseSwitch(SwitchEntity): From abbde8f128314f72ea1eb8d97056c0f2266cc0c5 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sat, 11 Dec 2021 11:51:24 -0500 Subject: [PATCH 0323/2644] Clean up state class and device class usage in ZHA (#61049) * Clean up sensor and device class usage in ZHA * additional cleanup * Use EntityCategory --- homeassistant/components/zha/binary_sensor.py | 36 +++----- homeassistant/components/zha/cover.py | 7 +- homeassistant/components/zha/sensor.py | 85 ++++++++----------- tests/components/zha/test_sensor.py | 5 +- 4 files changed, 52 insertions(+), 81 deletions(-) diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index c0af9a807d5..7e2b34d3613 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -2,14 +2,7 @@ import functools from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_GAS, - DEVICE_CLASS_MOISTURE, - DEVICE_CLASS_MOTION, - DEVICE_CLASS_MOVING, - DEVICE_CLASS_OCCUPANCY, - DEVICE_CLASS_OPENING, - DEVICE_CLASS_SMOKE, - DEVICE_CLASS_VIBRATION, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -34,12 +27,12 @@ from .entity import ZhaEntity # Zigbee Cluster Library Zone Type to Home Assistant device class CLASS_MAPPING = { - 0x000D: DEVICE_CLASS_MOTION, - 0x0015: DEVICE_CLASS_OPENING, - 0x0028: DEVICE_CLASS_SMOKE, - 0x002A: DEVICE_CLASS_MOISTURE, - 0x002B: DEVICE_CLASS_GAS, - 0x002D: DEVICE_CLASS_VIBRATION, + 0x000D: BinarySensorDeviceClass.MOTION, + 0x0015: BinarySensorDeviceClass.OPENING, + 0x0028: BinarySensorDeviceClass.SMOKE, + 0x002A: BinarySensorDeviceClass.MOISTURE, + 0x002B: BinarySensorDeviceClass.GAS, + 0x002D: BinarySensorDeviceClass.VIBRATION, } STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.BINARY_SENSOR) @@ -67,13 +60,11 @@ class BinarySensor(ZhaEntity, BinarySensorEntity): """ZHA BinarySensor.""" SENSOR_ATTR = None - DEVICE_CLASS = None def __init__(self, unique_id, zha_device, channels, **kwargs): """Initialize the ZHA binary sensor.""" super().__init__(unique_id, zha_device, channels, **kwargs) self._channel = channels[0] - self._device_class = self.DEVICE_CLASS async def async_added_to_hass(self): """Run when about to be added to hass.""" @@ -95,11 +86,6 @@ class BinarySensor(ZhaEntity, BinarySensorEntity): return False return self._state - @property - def device_class(self) -> str: - """Return device class from component DEVICE_CLASSES.""" - return self._device_class - @callback def async_set_state(self, attr_id, attr_name, value): """Set the state.""" @@ -122,7 +108,7 @@ class Accelerometer(BinarySensor): """ZHA BinarySensor.""" SENSOR_ATTR = "acceleration" - DEVICE_CLASS = DEVICE_CLASS_MOVING + _attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.MOVING @STRICT_MATCH(channel_names=CHANNEL_OCCUPANCY) @@ -130,7 +116,7 @@ class Occupancy(BinarySensor): """ZHA BinarySensor.""" SENSOR_ATTR = "occupancy" - DEVICE_CLASS = DEVICE_CLASS_OCCUPANCY + _attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.OCCUPANCY @STRICT_MATCH(channel_names=CHANNEL_ON_OFF) @@ -138,7 +124,7 @@ class Opening(BinarySensor): """ZHA BinarySensor.""" SENSOR_ATTR = "on_off" - DEVICE_CLASS = DEVICE_CLASS_OPENING + _attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.OPENING @STRICT_MATCH(channel_names=CHANNEL_BINARY_INPUT) @@ -164,7 +150,7 @@ class Motion(BinarySensor): """ZHA BinarySensor.""" SENSOR_ATTR = "on_off" - DEVICE_CLASS = DEVICE_CLASS_MOTION + _attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.MOTION @STRICT_MATCH(channel_names=CHANNEL_ZONE) diff --git a/homeassistant/components/zha/cover.py b/homeassistant/components/zha/cover.py index 8b9bf5589b3..e5ff1f6e95d 100644 --- a/homeassistant/components/zha/cover.py +++ b/homeassistant/components/zha/cover.py @@ -10,8 +10,7 @@ from zigpy.zcl.foundation import Status from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, ATTR_POSITION, - DEVICE_CLASS_DAMPER, - DEVICE_CLASS_SHADE, + CoverDeviceClass, CoverEntity, ) from homeassistant.config_entries import ConfigEntry @@ -187,7 +186,7 @@ class ZhaCover(ZhaEntity, CoverEntity): class Shade(ZhaEntity, CoverEntity): """ZHA Shade.""" - _attr_device_class = DEVICE_CLASS_SHADE + _attr_device_class = CoverDeviceClass.SHADE def __init__( self, @@ -296,7 +295,7 @@ class Shade(ZhaEntity, CoverEntity): class KeenVent(Shade): """Keen vent cover.""" - _attr_device_class = DEVICE_CLASS_DAMPER + _attr_device_class = CoverDeviceClass.DAMPER async def async_open_cover(self, **kwargs): """Open the cover.""" diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index f14e8b561c3..eed85417b67 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -13,29 +13,18 @@ from homeassistant.components.climate.const import ( CURRENT_HVAC_OFF, ) from homeassistant.components.sensor import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_CO, - DEVICE_CLASS_CO2, - DEVICE_CLASS_CURRENT, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_POWER, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, - DEVICE_CLASS_ENERGY, ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, - ENTITY_CATEGORY_DIAGNOSTIC, LIGHT_LUX, PERCENTAGE, POWER_VOLT_AMPERE, @@ -54,6 +43,7 @@ from homeassistant.const import ( ) 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 homeassistant.helpers.typing import StateType @@ -128,10 +118,8 @@ class Sensor(ZhaEntity, SensorEntity): SENSOR_ATTR: int | str | None = None _decimals: int = 1 - _device_class: str | None = None _divisor: int = 1 _multiplier: int = 1 - _state_class: str | None = None _unit: str | None = None def __init__( @@ -170,16 +158,6 @@ class Sensor(ZhaEntity, SensorEntity): self._channel, SIGNAL_ATTR_UPDATED, self.async_set_state ) - @property - def device_class(self) -> str: - """Return device class from component DEVICE_CLASSES.""" - return self._device_class - - @property - def state_class(self) -> str | None: - """Return the state class of this entity, from STATE_CLASSES, if any.""" - return self._state_class - @property def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement of this entity.""" @@ -225,10 +203,10 @@ class Battery(Sensor): """Battery sensor of power configuration cluster.""" SENSOR_ATTR = "battery_percentage_remaining" - _device_class = DEVICE_CLASS_BATTERY - _state_class = STATE_CLASS_MEASUREMENT + _attr_device_class: SensorDeviceClass = SensorDeviceClass.BATTERY + _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT _unit = PERCENTAGE - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_entity_category = EntityCategory.DIAGNOSTIC @classmethod def create_entity( @@ -276,8 +254,8 @@ class ElectricalMeasurement(Sensor): """Active power measurement.""" SENSOR_ATTR = "active_power" - _device_class = DEVICE_CLASS_POWER - _state_class = STATE_CLASS_MEASUREMENT + _attr_device_class: SensorDeviceClass = SensorDeviceClass.POWER + _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT _unit = POWER_WATT _div_mul_prefix = "ac_power" @@ -322,7 +300,6 @@ class ElectricalMeasurementApparentPower( """Apparent power measurement.""" SENSOR_ATTR = "apparent_power" - _device_class = DEVICE_CLASS_POWER _unit = POWER_VOLT_AMPERE _div_mul_prefix = "ac_power" @@ -337,7 +314,7 @@ class ElectricalMeasurementRMSCurrent(ElectricalMeasurement, id_suffix="rms_curr """RMS current measurement.""" SENSOR_ATTR = "rms_current" - _device_class = DEVICE_CLASS_CURRENT + _attr_device_class: SensorDeviceClass = SensorDeviceClass.CURRENT _unit = ELECTRIC_CURRENT_AMPERE _div_mul_prefix = "ac_current" @@ -352,7 +329,7 @@ class ElectricalMeasurementRMSVoltage(ElectricalMeasurement, id_suffix="rms_volt """RMS Voltage measurement.""" SENSOR_ATTR = "rms_voltage" - _device_class = DEVICE_CLASS_CURRENT + _attr_device_class: SensorDeviceClass = SensorDeviceClass.CURRENT _unit = ELECTRIC_POTENTIAL_VOLT _div_mul_prefix = "ac_voltage" @@ -368,9 +345,9 @@ class Humidity(Sensor): """Humidity sensor.""" SENSOR_ATTR = "measured_value" - _device_class = DEVICE_CLASS_HUMIDITY + _attr_device_class: SensorDeviceClass = SensorDeviceClass.HUMIDITY + _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT _divisor = 100 - _state_class = STATE_CLASS_MEASUREMENT _unit = PERCENTAGE @@ -379,9 +356,9 @@ class SoilMoisture(Sensor): """Soil Moisture sensor.""" SENSOR_ATTR = "measured_value" - _device_class = DEVICE_CLASS_HUMIDITY + _attr_device_class: SensorDeviceClass = SensorDeviceClass.HUMIDITY + _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT _divisor = 100 - _state_class = STATE_CLASS_MEASUREMENT _unit = PERCENTAGE @@ -390,9 +367,9 @@ class LeafWetness(Sensor): """Leaf Wetness sensor.""" SENSOR_ATTR = "measured_value" - _device_class = DEVICE_CLASS_HUMIDITY + _attr_device_class: SensorDeviceClass = SensorDeviceClass.HUMIDITY + _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT _divisor = 100 - _state_class = STATE_CLASS_MEASUREMENT _unit = PERCENTAGE @@ -401,7 +378,8 @@ class Illuminance(Sensor): """Illuminance Sensor.""" SENSOR_ATTR = "measured_value" - _device_class = DEVICE_CLASS_ILLUMINANCE + _attr_device_class: SensorDeviceClass = SensorDeviceClass.ILLUMINANCE + _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT _unit = LIGHT_LUX @staticmethod @@ -415,8 +393,8 @@ class SmartEnergyMetering(Sensor): """Metering sensor.""" SENSOR_ATTR: int | str = "instantaneous_demand" - _device_class: str | None = DEVICE_CLASS_POWER - _state_class: str | None = STATE_CLASS_MEASUREMENT + _attr_device_class: SensorDeviceClass = SensorDeviceClass.POWER + _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT unit_of_measure_map = { 0x00: POWER_WATT, @@ -459,8 +437,8 @@ class SmartEnergySummation(SmartEnergyMetering, id_suffix="summation_delivered") """Smart Energy Metering summation sensor.""" SENSOR_ATTR: int | str = "current_summ_delivered" - _device_class: str | None = DEVICE_CLASS_ENERGY - _state_class: str = STATE_CLASS_TOTAL_INCREASING + _attr_device_class: SensorDeviceClass = SensorDeviceClass.ENERGY + _attr_state_class: SensorStateClass = SensorStateClass.TOTAL_INCREASING unit_of_measure_map = { 0x00: ENERGY_KILO_WATT_HOUR, @@ -492,9 +470,9 @@ class Pressure(Sensor): """Pressure sensor.""" SENSOR_ATTR = "measured_value" - _device_class = DEVICE_CLASS_PRESSURE + _attr_device_class: SensorDeviceClass = SensorDeviceClass.PRESSURE + _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT _decimals = 0 - _state_class = STATE_CLASS_MEASUREMENT _unit = PRESSURE_HPA @@ -503,9 +481,9 @@ class Temperature(Sensor): """Temperature Sensor.""" SENSOR_ATTR = "measured_value" - _device_class = DEVICE_CLASS_TEMPERATURE + _attr_device_class: SensorDeviceClass = SensorDeviceClass.TEMPERATURE + _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT _divisor = 100 - _state_class = STATE_CLASS_MEASUREMENT _unit = TEMP_CELSIUS @@ -514,7 +492,8 @@ class CarbonDioxideConcentration(Sensor): """Carbon Dioxide Concentration sensor.""" SENSOR_ATTR = "measured_value" - _device_class = DEVICE_CLASS_CO2 + _attr_device_class: SensorDeviceClass = SensorDeviceClass.CO2 + _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT _decimals = 0 _multiplier = 1e6 _unit = CONCENTRATION_PARTS_PER_MILLION @@ -525,7 +504,8 @@ class CarbonMonoxideConcentration(Sensor): """Carbon Monoxide Concentration sensor.""" SENSOR_ATTR = "measured_value" - _device_class = DEVICE_CLASS_CO + _attr_device_class: SensorDeviceClass = SensorDeviceClass.CO + _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT _decimals = 0 _multiplier = 1e6 _unit = CONCENTRATION_PARTS_PER_MILLION @@ -537,6 +517,8 @@ class VOCLevel(Sensor): """VOC Level sensor.""" SENSOR_ATTR = "measured_value" + _attr_device_class: SensorDeviceClass = SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS + _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT _decimals = 0 _multiplier = 1e6 _unit = CONCENTRATION_MICROGRAMS_PER_CUBIC_METER @@ -547,6 +529,8 @@ class PPBVOCLevel(Sensor): """VOC Level sensor.""" SENSOR_ATTR = "measured_value" + _attr_device_class: SensorDeviceClass = SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS + _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT _decimals = 0 _multiplier = 1 _unit = CONCENTRATION_PARTS_PER_BILLION @@ -557,6 +541,7 @@ class FormaldehydeConcentration(Sensor): """Formaldehyde Concentration sensor.""" SENSOR_ATTR = "measured_value" + _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT _decimals = 0 _multiplier = 1e6 _unit = CONCENTRATION_PARTS_PER_MILLION diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index 468458cdc51..3483077d7d8 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -8,6 +8,7 @@ import zigpy.zcl.clusters.homeautomation as homeautomation import zigpy.zcl.clusters.measurement as measurement import zigpy.zcl.clusters.smartenergy as smartenergy +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.components.zha.core.const import ZHA_CHANNEL_READS_PER_REQ import homeassistant.config as config_util from homeassistant.const import ( @@ -16,7 +17,6 @@ from homeassistant.const import ( CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC, - DEVICE_CLASS_ENERGY, ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, @@ -149,7 +149,8 @@ async def async_test_smart_energy_summation(hass, cluster, entity_id): assert hass.states.get(entity_id).attributes["status"] == "NO_ALARMS" assert hass.states.get(entity_id).attributes["device_type"] == "Electric Metering" assert ( - hass.states.get(entity_id).attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_ENERGY + hass.states.get(entity_id).attributes[ATTR_DEVICE_CLASS] + == SensorDeviceClass.ENERGY ) From 0736e4fde10e81f9b273811b5d9a3c02ce4fdabd Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sat, 11 Dec 2021 18:11:42 +0100 Subject: [PATCH 0324/2644] Update frontend to 20211211.0 (#61499) --- homeassistant/components/frontend/manifest.json | 10 +++------- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index c9739cd0302..994ac596527 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,9 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": [ - "home-assistant-frontend==20211209.0" - ], + "requirements": ["home-assistant-frontend==20211211.0"], "dependencies": [ "api", "auth", @@ -17,8 +15,6 @@ "system_log", "websocket_api" ], - "codeowners": [ - "@home-assistant/frontend" - ], + "codeowners": ["@home-assistant/frontend"], "quality_scale": "internal" -} \ No newline at end of file +} diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b203f005e64..a22fbb871a2 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ ciso8601==2.2.0 cryptography==35.0.0 emoji==1.5.0 hass-nabucasa==0.50.0 -home-assistant-frontend==20211209.0 +home-assistant-frontend==20211211.0 httpx==0.21.0 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index be76ee7ace5..19124429426 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -828,7 +828,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211209.0 +home-assistant-frontend==20211211.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c66f80cd792..425bc09f87c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -524,7 +524,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211209.0 +home-assistant-frontend==20211211.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From f75b325ab2bb985c4c83b03997017109aa3a0bac Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 12 Dec 2021 00:14:30 +0000 Subject: [PATCH 0325/2644] [ci skip] Translation update --- .../components/adax/translations/fr.json | 22 ++++- .../components/adax/translations/he.json | 8 +- .../components/airthings/translations/fr.json | 21 +++++ .../components/airtouch4/translations/fr.json | 6 +- .../amberelectric/translations/fr.json | 22 +++++ .../components/apple_tv/translations/fr.json | 21 ++++- .../components/arcam_fmj/translations/fr.json | 2 +- .../aseko_pool_live/translations/fr.json | 20 +++++ .../aurora_abb_powerone/translations/fr.json | 23 +++++ .../azure_devops/translations/fr.json | 2 +- .../components/balboa/translations/fr.json | 28 ++++++ .../binary_sensor/translations/fr.json | 27 ++++++ .../components/blebox/translations/fr.json | 2 +- .../components/bond/translations/fr.json | 2 +- .../components/brother/translations/fr.json | 2 +- .../components/brunt/translations/fr.json | 29 ++++++ .../components/bsblan/translations/fr.json | 2 +- .../components/button/translations/fr.json | 11 +++ .../components/canary/translations/fr.json | 2 +- .../components/cloud/translations/fr.json | 1 + .../cloudflare/translations/fr.json | 2 +- .../crownstone/translations/fr.json | 89 +++++++++++++++++-- .../components/daikin/translations/fr.json | 1 + .../components/deconz/translations/fr.json | 2 +- .../components/denonavr/translations/fr.json | 2 +- .../devolo_home_network/translations/fr.json | 8 +- .../components/directv/translations/fr.json | 2 +- .../components/dlna_dmr/translations/fr.json | 40 ++++++++- .../components/doorbird/translations/fr.json | 2 +- .../components/efergy/translations/fr.json | 11 ++- .../components/elgato/translations/fr.json | 2 +- .../components/elmax/translations/fr.json | 34 +++++++ .../components/elmax/translations/he.json | 15 ++++ .../components/emonitor/translations/fr.json | 2 +- .../enphase_envoy/translations/fr.json | 5 +- .../environment_canada/translations/fr.json | 11 ++- .../components/esphome/translations/fr.json | 11 ++- .../components/flux_led/translations/fr.json | 25 ++++++ .../forked_daapd/translations/fr.json | 2 +- .../components/fritz/translations/fr.json | 2 +- .../components/fritzbox/translations/fr.json | 2 +- .../fritzbox_callmonitor/translations/fr.json | 2 +- .../components/fronius/translations/fr.json | 25 ++++++ .../components/goalzero/translations/fr.json | 2 +- .../components/harmony/translations/fr.json | 2 +- .../components/homekit/translations/fr.json | 1 + .../homekit_controller/translations/fr.json | 2 +- .../huawei_lte/translations/fr.json | 2 +- .../components/hue/translations/fr.json | 12 ++- .../components/iotawatt/translations/fr.json | 12 +++ .../components/ipp/translations/fr.json | 2 +- .../components/isy994/translations/fr.json | 2 +- .../components/knx/translations/fr.json | 65 ++++++++++++++ .../components/kodi/translations/fr.json | 2 +- .../components/konnected/translations/fr.json | 1 + .../components/lcn/translations/ca.json | 10 +++ .../components/lcn/translations/de.json | 10 +++ .../components/lcn/translations/et.json | 10 +++ .../components/lcn/translations/fr.json | 10 +++ .../components/lcn/translations/id.json | 10 +++ .../components/lcn/translations/ja.json | 10 +++ .../components/lcn/translations/ru.json | 10 +++ .../components/lcn/translations/zh-Hant.json | 10 +++ .../components/lookin/translations/fr.json | 31 +++++++ .../lutron_caseta/translations/fr.json | 2 +- .../components/mill/translations/fr.json | 16 +++- .../modem_callerid/translations/fr.json | 11 ++- .../motion_blinds/translations/fr.json | 17 +++- .../components/motioneye/translations/fr.json | 1 + .../components/nam/translations/fr.json | 21 ++++- .../components/nest/translations/fr.json | 20 ++++- .../components/netatmo/translations/fr.json | 5 ++ .../components/netgear/translations/fr.json | 34 +++++++ .../components/nina/translations/fr.json | 27 ++++++ .../components/notion/translations/fr.json | 13 ++- .../components/nzbget/translations/fr.json | 2 +- .../components/octoprint/translations/fr.json | 29 ++++++ .../opengarage/translations/fr.json | 1 + .../ovo_energy/translations/fr.json | 2 +- .../components/plugwise/translations/fr.json | 2 +- .../components/powerwall/translations/fr.json | 2 +- .../pvpc_hourly_pricing/translations/fr.json | 6 +- .../rainmachine/translations/fr.json | 2 +- .../components/rdw/translations/fr.json | 10 ++- .../components/renault/translations/fr.json | 4 +- .../components/rfxtrx/translations/fr.json | 10 +++ .../components/ridwell/translations/fr.json | 4 +- .../components/roku/translations/fr.json | 2 +- .../components/roomba/translations/fr.json | 6 +- .../components/samsungtv/translations/fr.json | 2 +- .../components/samsungtv/translations/sv.json | 6 +- .../screenlogic/translations/fr.json | 2 +- .../components/sense/translations/fr.json | 3 +- .../components/sensor/translations/fr.json | 20 +++++ .../components/shelly/translations/fr.json | 3 + .../simplisafe/translations/fr.json | 12 ++- .../simplisafe/translations/id.json | 3 +- .../simplisafe/translations/zh-Hant.json | 2 +- .../components/smappee/translations/fr.json | 2 +- .../components/soma/translations/fr.json | 6 +- .../somfy_mylink/translations/fr.json | 2 +- .../components/sonarr/translations/fr.json | 2 +- .../components/songpal/translations/fr.json | 2 +- .../squeezebox/translations/fr.json | 2 +- .../stookalert/translations/fr.json | 7 ++ .../surepetcare/translations/fr.json | 20 +++++ .../components/switchbot/translations/fr.json | 30 +++++++ .../components/syncthru/translations/fr.json | 2 +- .../synology_dsm/translations/fr.json | 5 +- .../system_bridge/translations/fr.json | 2 +- .../components/tailscale/translations/fr.json | 26 ++++++ .../tesla_wall_connector/translations/fr.json | 30 +++++++ .../components/tolo/translations/fr.json | 23 +++++ .../tolo/translations/select.fr.json | 8 ++ .../totalconnect/translations/fr.json | 3 +- .../components/tplink/translations/fr.json | 12 ++- .../tractive/translations/sensor.fr.json | 10 +++ .../components/tradfri/translations/fr.json | 1 + .../translations/fr.json | 23 +++++ .../components/tuya/translations/fr.json | 14 ++- .../tuya/translations/select.fr.json | 38 +++++++- .../tuya/translations/sensor.fr.json | 15 ++++ .../components/unifi/translations/fr.json | 8 +- .../components/upnp/translations/fr.json | 2 +- .../components/venstar/translations/fr.json | 9 +- .../vlc_telnet/translations/fr.json | 14 ++- .../components/wallbox/translations/fr.json | 1 + .../components/watttime/translations/fr.json | 28 +++++- .../components/whirlpool/translations/fr.json | 6 ++ .../components/wilight/translations/fr.json | 2 +- .../components/withings/translations/fr.json | 2 +- .../components/wled/translations/fr.json | 2 +- .../wled/translations/select.fr.json | 9 ++ .../xiaomi_aqara/translations/fr.json | 2 +- .../xiaomi_miio/translations/fr.json | 2 +- .../yale_smart_alarm/translations/fr.json | 3 +- .../translations/select.fr.json | 52 +++++++++++ .../components/zha/translations/fr.json | 2 +- .../components/zwave_js/translations/fr.json | 16 +++- 139 files changed, 1387 insertions(+), 126 deletions(-) create mode 100644 homeassistant/components/airthings/translations/fr.json create mode 100644 homeassistant/components/amberelectric/translations/fr.json create mode 100644 homeassistant/components/aseko_pool_live/translations/fr.json create mode 100644 homeassistant/components/aurora_abb_powerone/translations/fr.json create mode 100644 homeassistant/components/balboa/translations/fr.json create mode 100644 homeassistant/components/brunt/translations/fr.json create mode 100644 homeassistant/components/button/translations/fr.json create mode 100644 homeassistant/components/elmax/translations/fr.json create mode 100644 homeassistant/components/elmax/translations/he.json create mode 100644 homeassistant/components/fronius/translations/fr.json create mode 100644 homeassistant/components/knx/translations/fr.json create mode 100644 homeassistant/components/lcn/translations/ca.json create mode 100644 homeassistant/components/lcn/translations/de.json create mode 100644 homeassistant/components/lcn/translations/et.json create mode 100644 homeassistant/components/lcn/translations/fr.json create mode 100644 homeassistant/components/lcn/translations/id.json create mode 100644 homeassistant/components/lcn/translations/ja.json create mode 100644 homeassistant/components/lcn/translations/ru.json create mode 100644 homeassistant/components/lcn/translations/zh-Hant.json create mode 100644 homeassistant/components/lookin/translations/fr.json create mode 100644 homeassistant/components/netgear/translations/fr.json create mode 100644 homeassistant/components/nina/translations/fr.json create mode 100644 homeassistant/components/octoprint/translations/fr.json create mode 100644 homeassistant/components/surepetcare/translations/fr.json create mode 100644 homeassistant/components/tailscale/translations/fr.json create mode 100644 homeassistant/components/tesla_wall_connector/translations/fr.json create mode 100644 homeassistant/components/tolo/translations/fr.json create mode 100644 homeassistant/components/tolo/translations/select.fr.json create mode 100644 homeassistant/components/tractive/translations/sensor.fr.json create mode 100644 homeassistant/components/trafikverket_weatherstation/translations/fr.json create mode 100644 homeassistant/components/tuya/translations/sensor.fr.json create mode 100644 homeassistant/components/wled/translations/select.fr.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/select.fr.json diff --git a/homeassistant/components/adax/translations/fr.json b/homeassistant/components/adax/translations/fr.json index 80164e30b54..eefd0693e24 100644 --- a/homeassistant/components/adax/translations/fr.json +++ b/homeassistant/components/adax/translations/fr.json @@ -1,19 +1,37 @@ { "config": { "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "heater_not_available": "Chauffage non disponible. Essayez de r\u00e9initialiser le chauffage en appuyant sur + et OK pendant quelques secondes.", + "heater_not_found": "Chauffage introuvable. Essayez de rapprocher le radiateur de l'ordinateur Home Assistant.", + "invalid_auth": "Authentification invalide" }, "error": { "cannot_connect": "\u00c9chec de connexion", "invalid_auth": "Authentification invalide" }, "step": { + "cloud": { + "data": { + "account_id": "Identifiant de compte", + "password": "Mot der passe" + } + }, + "local": { + "data": { + "wifi_pswd": "Mot de passe WiFi", + "wifi_ssid": "identifiant Wifi" + }, + "description": "R\u00e9initialisez le radiateur en appuyant sur + et OK jusqu'\u00e0 ce que l'\u00e9cran affiche \u00ab\u00a0Reset\u00a0\u00bb. Appuyez ensuite sur le bouton OK du radiateur et maintenez-le enfonc\u00e9 jusqu'\u00e0 ce que le voyant bleu commence \u00e0 clignoter avant d'appuyer sur Soumettre. La configuration du chauffage peut prendre quelques minutes." + }, "user": { "data": { "account_id": "identifiant de compte", + "connection_type": "S\u00e9lectionner le type de connexion", "host": "H\u00f4te", "password": "Mot de passe" - } + }, + "description": "S\u00e9lectionnez le type de connexion. Local n\u00e9cessite des radiateurs avec Bluetooth" } } } diff --git a/homeassistant/components/adax/translations/he.json b/homeassistant/components/adax/translations/he.json index 54d31cd2669..0ba47926a07 100644 --- a/homeassistant/components/adax/translations/he.json +++ b/homeassistant/components/adax/translations/he.json @@ -1,13 +1,19 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" }, "step": { + "cloud": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + } + }, "user": { "data": { "account_id": "\u05de\u05d6\u05d4\u05d4 \u05d7\u05e9\u05d1\u05d5\u05df", diff --git a/homeassistant/components/airthings/translations/fr.json b/homeassistant/components/airthings/translations/fr.json new file mode 100644 index 00000000000..1ad84e8bd99 --- /dev/null +++ b/homeassistant/components/airthings/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "description": "Connectez-vous sur {url} pour trouver vos identifiants", + "id": "ID", + "secret": "Secret" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airtouch4/translations/fr.json b/homeassistant/components/airtouch4/translations/fr.json index 33580a8eae3..8dce645e3f0 100644 --- a/homeassistant/components/airtouch4/translations/fr.json +++ b/homeassistant/components/airtouch4/translations/fr.json @@ -4,13 +4,15 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "cannot_connect": "\u00c9chec de connexion" + "cannot_connect": "\u00c9chec de connexion", + "no_units": "Impossible de trouver des groupes AirTouch 4." }, "step": { "user": { "data": { "host": "H\u00f4te" - } + }, + "title": "Configurez les d\u00e9tails de votre connexion AirTouch 4." } } } diff --git a/homeassistant/components/amberelectric/translations/fr.json b/homeassistant/components/amberelectric/translations/fr.json new file mode 100644 index 00000000000..487ceff33f3 --- /dev/null +++ b/homeassistant/components/amberelectric/translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "site": { + "data": { + "site_name": "Nom du site", + "site_nmi": "Site NMI" + }, + "description": "S\u00e9lectionnez le NMI du site que vous souhaitez ajouter", + "title": "Amber Electrique" + }, + "user": { + "data": { + "api_token": "Jeton d'API", + "site_id": "ID du site" + }, + "description": "Acc\u00e9dez \u00e0 {api_url} pour g\u00e9n\u00e9rer une cl\u00e9 API", + "title": "Amber Electrique" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/fr.json b/homeassistant/components/apple_tv/translations/fr.json index 056a98ea74f..23fed83cfc4 100644 --- a/homeassistant/components/apple_tv/translations/fr.json +++ b/homeassistant/components/apple_tv/translations/fr.json @@ -1,12 +1,17 @@ { "config": { "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "already_configured_device": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", "backoff": "L'appareil n'accepte pas les demandes d'appariement pour le moment (vous avez peut-\u00eatre saisi un code PIN non valide trop de fois), r\u00e9essayez plus tard.", "device_did_not_pair": "Aucune tentative pour terminer l'appairage n'a \u00e9t\u00e9 effectu\u00e9e \u00e0 partir de l'appareil.", + "device_not_found": "L'appareil n'a pas \u00e9t\u00e9 trouv\u00e9 lors de la d\u00e9couverte, veuillez r\u00e9essayer de l'ajouter.", + "inconsistent_device": "Les protocoles attendus n'ont pas \u00e9t\u00e9 trouv\u00e9s lors de la d\u00e9couverte. Cela indique normalement un probl\u00e8me avec le DNS multicast (Zeroconf). Veuillez r\u00e9essayer d'ajouter l'appareil.", "invalid_config": "La configuration de cet appareil est incompl\u00e8te. Veuillez r\u00e9essayer de l'ajouter.", "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", + "setup_failed": "\u00c9chec de la configuration de l'appareil.", "unknown": "Erreur inattendue" }, "error": { @@ -16,14 +21,14 @@ "no_usable_service": "Un dispositif a \u00e9t\u00e9 trouv\u00e9, mais aucun moyen d\u2019\u00e9tablir un lien avec lui. Si vous continuez \u00e0 voir ce message, essayez de sp\u00e9cifier son adresse IP ou de red\u00e9marrer votre Apple TV.", "unknown": "Erreur inattendue" }, - "flow_title": "Apple TV: {name}", + "flow_title": "{name} ({type})", "step": { "confirm": { - "description": "Vous \u00eates sur le point d'ajouter l'Apple TV nomm\u00e9e \u00ab {name} \u00bb \u00e0 Home Assistant. \n\n **Pour terminer le processus, vous devrez peut-\u00eatre saisir plusieurs codes PIN.** \n\n Veuillez noter que vous ne pourrez *pas* \u00e9teindre votre Apple TV avec cette int\u00e9gration. Seul le lecteur multim\u00e9dia de Home Assistant s'\u00e9teint!", + "description": "Vous \u00eates sur le point d'ajouter ` {name} ` de type ` {type} ` \u00e0 Home Assistant. \n\n **Pour terminer le processus, vous devrez peut-\u00eatre saisir plusieurs codes PIN.** \n\n Veuillez noter que vous ne pourrez *pas* \u00e9teindre votre Apple TV avec cette int\u00e9gration. Seul le lecteur multim\u00e9dia de Home Assistant s'\u00e9teindra !", "title": "Confirmer l'ajout d'Apple TV" }, "pair_no_pin": { - "description": "L'appairage est requis pour le service ` {protocol} `. Veuillez saisir le code PIN {pin} sur votre Apple TV pour continuer.", + "description": "L'appariement est requis pour le service ` {protocol} `. Veuillez saisir le code PIN {pin} sur votre appareil pour continuer.", "title": "Appairage" }, "pair_with_pin": { @@ -33,6 +38,14 @@ "description": "L'appairage est requis pour le protocole `{protocol}`. Veuillez saisir le code PIN affich\u00e9 \u00e0 l'\u00e9cran. Les z\u00e9ros doivent \u00eatre omis, c'est-\u00e0-dire entrer 123 si le code affich\u00e9 est 0123.", "title": "Appairage" }, + "password": { + "description": "Un mot de passe est requis par ` {protocol} `. Ceci n'est pas encore pris en charge, veuillez d\u00e9sactiver le mot de passe pour continuer.", + "title": "Mot de passe requis" + }, + "protocol_disabled": { + "description": "L'appairage est requis pour ` {protocol} ` mais il est d\u00e9sactiv\u00e9 sur l'appareil. Veuillez examiner les restrictions d'acc\u00e8s potentielles (par exemple, autoriser tous les appareils du r\u00e9seau local \u00e0 se connecter) sur l'appareil. \n\n Vous pouvez continuer sans appairer ce protocole, mais certaines fonctionnalit\u00e9s seront limit\u00e9es.", + "title": "Appairage impossible" + }, "reconfigure": { "description": "Cette Apple TV rencontre des difficult\u00e9s de connexion et doit \u00eatre reconfigur\u00e9e.", "title": "Reconfiguration de l'appareil" @@ -45,7 +58,7 @@ "data": { "device_input": "Appareil" }, - "description": "Commencez par entrer le nom de l'appareil (par exemple, Cuisine ou Chambre) ou l'adresse IP de l'Apple TV que vous souhaitez ajouter. Si des appareils ont \u00e9t\u00e9 d\u00e9tect\u00e9s automatiquement sur votre r\u00e9seau, ils sont affich\u00e9s ci-dessous. \n\n Si vous ne voyez pas votre appareil ou rencontrez des probl\u00e8mes, essayez de sp\u00e9cifier l'adresse IP de l'appareil. \n\n {devices}", + "description": "Commencez par saisir le nom de l'appareil (par exemple, cuisine ou chambre) ou l'adresse IP de l'Apple TV que vous souhaitez ajouter. \n\n Si vous ne pouvez pas voir votre appareil ou rencontrez des probl\u00e8mes, essayez de sp\u00e9cifier l'adresse IP de l'appareil.", "title": "Configurer une nouvelle Apple TV" } } diff --git a/homeassistant/components/arcam_fmj/translations/fr.json b/homeassistant/components/arcam_fmj/translations/fr.json index 938e9ab7b5d..363621cc94a 100644 --- a/homeassistant/components/arcam_fmj/translations/fr.json +++ b/homeassistant/components/arcam_fmj/translations/fr.json @@ -9,7 +9,7 @@ "one": "Vide", "other": "Vide" }, - "flow_title": "Arcam FMJ sur {host}", + "flow_title": "{host}", "step": { "confirm": { "description": "Voulez-vous ajouter Arcam FMJ sur ` {host} ` \u00e0 HomeAssistant ?" diff --git a/homeassistant/components/aseko_pool_live/translations/fr.json b/homeassistant/components/aseko_pool_live/translations/fr.json new file mode 100644 index 00000000000..d28b22f8d98 --- /dev/null +++ b/homeassistant/components/aseko_pool_live/translations/fr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "Mot de passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aurora_abb_powerone/translations/fr.json b/homeassistant/components/aurora_abb_powerone/translations/fr.json new file mode 100644 index 00000000000..d87822fb7c3 --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/translations/fr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "no_serial_ports": "Aucun port com trouv\u00e9. Besoin d'un p\u00e9riph\u00e9rique RS485 valide pour communiquer." + }, + "error": { + "cannot_connect": "Connexion impossible, veuillez v\u00e9rifier le port s\u00e9rie, l'adresse, la connexion \u00e9lectrique et que l'onduleur est allum\u00e9 (\u00e0 la lumi\u00e8re du jour)", + "cannot_open_serial_port": "Impossible d'ouvrir le port s\u00e9rie, veuillez v\u00e9rifier et r\u00e9essayer", + "invalid_serial_port": "Le port s\u00e9rie n'est pas un p\u00e9riph\u00e9rique valide ou n'a pas pu \u00eatre ouvert", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "address": "Adresse de l'onduleur", + "port": "Port adaptateur RS485 ou USB-RS485" + }, + "description": "L'onduleur doit \u00eatre connect\u00e9 via un adaptateur RS485, veuillez s\u00e9lectionner le port s\u00e9rie et l'adresse de l'onduleur comme configur\u00e9 sur le panneau LCD" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/azure_devops/translations/fr.json b/homeassistant/components/azure_devops/translations/fr.json index 27513074046..17bc6104112 100644 --- a/homeassistant/components/azure_devops/translations/fr.json +++ b/homeassistant/components/azure_devops/translations/fr.json @@ -9,7 +9,7 @@ "invalid_auth": "Authentification invalide", "project_error": "Impossible d'obtenir les informations sur le projet." }, - "flow_title": "Azure DevOps: {project_url}", + "flow_title": "{project_url}", "step": { "reauth": { "data": { diff --git a/homeassistant/components/balboa/translations/fr.json b/homeassistant/components/balboa/translations/fr.json new file mode 100644 index 00000000000..c9c24b8dee8 --- /dev/null +++ b/homeassistant/components/balboa/translations/fr.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te" + }, + "title": "Connectez-vous \u00e0 l'appareil Wi-Fi Balboa" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "sync_time": "Synchronisez l'heure de votre client Balboa Spa avec Home Assistant" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/fr.json b/homeassistant/components/binary_sensor/translations/fr.json index 74f54a30814..b1a1d7ee351 100644 --- a/homeassistant/components/binary_sensor/translations/fr.json +++ b/homeassistant/components/binary_sensor/translations/fr.json @@ -31,6 +31,8 @@ "is_not_plugged_in": "{entity_name} est d\u00e9branch\u00e9", "is_not_powered": "{entity_name} n'est pas aliment\u00e9", "is_not_present": "{entity_name} n'est pas pr\u00e9sent", + "is_not_running": "{entity_name} n'est pas en cours d'ex\u00e9cution", + "is_not_tampered": "{entity_name} ne d\u00e9tecte pas la falsification", "is_not_unsafe": "{entity_name} est en s\u00e9curit\u00e9", "is_occupied": "{entity_name} est occup\u00e9", "is_off": "{entity_name} est d\u00e9sactiv\u00e9", @@ -40,8 +42,10 @@ "is_powered": "{entity_name} est aliment\u00e9", "is_present": "{entity_name} est pr\u00e9sent", "is_problem": "{entity_name} d\u00e9tecte un probl\u00e8me", + "is_running": "{entity_name} est en cours d'ex\u00e9cution", "is_smoke": "{entity_name} d\u00e9tecte de la fum\u00e9e", "is_sound": "{entity_name} d\u00e9tecte du son", + "is_tampered": "{entity_name} d\u00e9tecte une falsification", "is_unsafe": "{entity_name} est dangereux", "is_update": "{entity_name} a une mise \u00e0 jour disponible", "is_vibration": "{entity_name} d\u00e9tecte des vibrations" @@ -52,6 +56,8 @@ "connected": "{entity_name} connect\u00e9", "gas": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter du gaz", "hot": "{entity_name} est devenu chaud", + "is_not_tampered": "{entity_name} a cess\u00e9 de d\u00e9tecter la falsification", + "is_tampered": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter une falsification", "light": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter la lumi\u00e8re", "locked": "{entity_name} verrouill\u00e9", "moist": "{entity_name} est devenu humide", @@ -77,6 +83,8 @@ "not_plugged_in": "{entity_name} d\u00e9branch\u00e9", "not_powered": "{entity_name} non aliment\u00e9", "not_present": "{entity_name} non pr\u00e9sent", + "not_running": "{entity_name} n'est plus en cours d'ex\u00e9cution", + "not_tampered": "{entity_name} a cess\u00e9 de d\u00e9tecter la falsification", "not_unsafe": "{entity_name} est devenu s\u00fbr", "occupied": "{entity_name} est devenu occup\u00e9", "opened": "{entity_name} ouvert", @@ -84,8 +92,10 @@ "powered": "{entity_name} aliment\u00e9", "present": "{entity_name} pr\u00e9sent", "problem": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter un probl\u00e8me", + "running": "{entity_name} commenc\u00e9 \u00e0 s'ex\u00e9cuter", "smoke": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter la fum\u00e9e", "sound": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter le son", + "tampered": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter une falsification", "turned_off": "{entity_name} est d\u00e9sactiv\u00e9", "turned_on": "{entity_name} est activ\u00e9", "unsafe": "{entity_name} est devenu dangereux", @@ -93,6 +103,19 @@ "vibration": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter les vibrations" } }, + "device_class": { + "cold": "froid", + "gas": "gaz", + "heat": "Chauffer", + "moisture": "humidit\u00e9", + "motion": "mouvement", + "occupancy": "occupation", + "power": "Puissance", + "problem": "Probl\u00e8me", + "smoke": "fum\u00e9e", + "sound": "son", + "vibration": "vibration" + }, "state": { "_": { "off": "Inactif", @@ -170,6 +193,10 @@ "off": "OK", "on": "Probl\u00e8me" }, + "running": { + "off": "\u00c0 l'arr\u00eat", + "on": "En marche" + }, "safety": { "off": "S\u00e9curis\u00e9", "on": "Dangereux" diff --git a/homeassistant/components/blebox/translations/fr.json b/homeassistant/components/blebox/translations/fr.json index 83983be5be1..6a5224f9239 100644 --- a/homeassistant/components/blebox/translations/fr.json +++ b/homeassistant/components/blebox/translations/fr.json @@ -9,7 +9,7 @@ "unknown": "Erreur inattendue", "unsupported_version": "L'appareil BleBox a un micrologiciel obsol\u00e8te. Veuillez d'abord le mettre \u00e0 jour." }, - "flow_title": "P\u00e9riph\u00e9rique Blebox: {name} ({host)}", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/bond/translations/fr.json b/homeassistant/components/bond/translations/fr.json index 7d2450dc9b5..f968622e214 100644 --- a/homeassistant/components/bond/translations/fr.json +++ b/homeassistant/components/bond/translations/fr.json @@ -9,7 +9,7 @@ "old_firmware": "Ancien micrologiciel non pris en charge sur l'appareil Bond - veuillez mettre \u00e0 niveau avant de continuer", "unknown": "Erreur inattendue" }, - "flow_title": "Lien : {name} ({host})", + "flow_title": "{name} ({host})", "step": { "confirm": { "data": { diff --git a/homeassistant/components/brother/translations/fr.json b/homeassistant/components/brother/translations/fr.json index d5a53b94622..ada9ca7385d 100644 --- a/homeassistant/components/brother/translations/fr.json +++ b/homeassistant/components/brother/translations/fr.json @@ -9,7 +9,7 @@ "snmp_error": "Serveur SNMP d\u00e9sactiv\u00e9 ou imprimante non prise en charge.", "wrong_host": "Nom d'h\u00f4te ou adresse IP invalide." }, - "flow_title": "Imprimante Brother: {model} {serial_number}", + "flow_title": "{model} {serial_number}", "step": { "user": { "data": { diff --git a/homeassistant/components/brunt/translations/fr.json b/homeassistant/components/brunt/translations/fr.json new file mode 100644 index 00000000000..611a57ea313 --- /dev/null +++ b/homeassistant/components/brunt/translations/fr.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Mot de passe" + }, + "description": "Veuillez saisir \u00e0 nouveau le mot de passe pour\u00a0: {username}", + "title": "R\u00e9-authentifier l'int\u00e9gration" + }, + "user": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + }, + "title": "Configurez votre int\u00e9gration Brunt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/fr.json b/homeassistant/components/bsblan/translations/fr.json index 2d1388bed18..dda5e5c293c 100644 --- a/homeassistant/components/bsblan/translations/fr.json +++ b/homeassistant/components/bsblan/translations/fr.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "\u00c9chec de connexion" }, - "flow_title": "BSB-Lan: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/button/translations/fr.json b/homeassistant/components/button/translations/fr.json new file mode 100644 index 00000000000..5e6adf70da1 --- /dev/null +++ b/homeassistant/components/button/translations/fr.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "press": "Appuyez sur le bouton {entity_name}" + }, + "trigger_type": { + "pressed": "{entity_name} a \u00e9t\u00e9 press\u00e9" + } + }, + "title": "Bouton" +} \ No newline at end of file diff --git a/homeassistant/components/canary/translations/fr.json b/homeassistant/components/canary/translations/fr.json index 9bb1761f9fb..45b06208df4 100644 --- a/homeassistant/components/canary/translations/fr.json +++ b/homeassistant/components/canary/translations/fr.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u00c9chec de connexion" }, - "flow_title": "Canary : {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/cloud/translations/fr.json b/homeassistant/components/cloud/translations/fr.json index 9bb4029fce0..76d6bce9a05 100644 --- a/homeassistant/components/cloud/translations/fr.json +++ b/homeassistant/components/cloud/translations/fr.json @@ -10,6 +10,7 @@ "relayer_connected": "Relais connect\u00e9", "remote_connected": "Contr\u00f4le \u00e0 distance connect\u00e9", "remote_enabled": "Contr\u00f4le \u00e0 distance activ\u00e9", + "remote_server": "Serveur distant", "subscription_expiration": "Expiration de l'abonnement" } } diff --git a/homeassistant/components/cloudflare/translations/fr.json b/homeassistant/components/cloudflare/translations/fr.json index 7add319cf29..73c2d76b4fc 100644 --- a/homeassistant/components/cloudflare/translations/fr.json +++ b/homeassistant/components/cloudflare/translations/fr.json @@ -10,7 +10,7 @@ "invalid_auth": "Authentification invalide", "invalid_zone": "Zone invalide" }, - "flow_title": "Cloudflare: {name}", + "flow_title": "{name}", "step": { "reauth_confirm": { "data": { diff --git a/homeassistant/components/crownstone/translations/fr.json b/homeassistant/components/crownstone/translations/fr.json index 55336a6a87f..783cd25bd49 100644 --- a/homeassistant/components/crownstone/translations/fr.json +++ b/homeassistant/components/crownstone/translations/fr.json @@ -1,20 +1,95 @@ { - "options": { + "config": { + "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "usb_setup_complete": "Configuration de la cl\u00e9 USB Crownstone termin\u00e9e.", + "usb_setup_unsuccessful": "La configuration USB de Crownstone a \u00e9chou\u00e9." + }, + "error": { + "account_not_verified": "Compte non v\u00e9rifi\u00e9. Veuillez activer votre compte via l'e-mail d'activation de Crownstone.", + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue" + }, "step": { "usb_config": { "data": { "usb_path": "Chemin du p\u00e9riph\u00e9rique USB" - } - }, - "usb_config_option": { - "data": { - "usb_path": "Chemin du p\u00e9riph\u00e9rique USB" - } + }, + "description": "S\u00e9lectionnez le port s\u00e9rie du dongle USB Crownstone ou s\u00e9lectionnez \u00ab\u00a0Ne pas utiliser USB\u00a0\u00bb si vous ne souhaitez pas configurer un dongle USB. \n\n Recherchez un appareil avec VID 10C4 et PID EA60.", + "title": "Configuration du dongle USB Crownstone" }, "usb_manual_config": { "data": { "usb_manual_path": "Chemin du p\u00e9riph\u00e9rique USB" + }, + "description": "Entrez manuellement le chemin d'un dongle USB Crownstone.", + "title": "Chemin d'acc\u00e8s manuel du dongle USB Crownstone" + }, + "usb_sphere_config": { + "data": { + "usb_sphere": "Crownstone Sphere" + }, + "description": "S\u00e9lectionnez une sph\u00e8re Crownstone o\u00f9 se trouve l\u2019USB.", + "title": "Sph\u00e8re USB Crownstone" + }, + "user": { + "data": { + "email": "Email", + "password": "Mot de passe" + }, + "title": "Compte Crownstone" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "usb_sphere_option": "Sph\u00e8re Crownstone o\u00f9 se trouve la cl\u00e9 USB", + "use_usb_option": "Utilisez un dongle USB Crownstone pour la transmission de donn\u00e9es locale" } + }, + "usb_config": { + "data": { + "usb_path": "Chemin du p\u00e9riph\u00e9rique USB" + }, + "description": "S\u00e9lectionnez le port s\u00e9rie du dongle USB Crownstone. \n\n Recherchez un appareil avec VID 10C4 et PID EA60.", + "title": "Configuration du dongle USB Crownstone" + }, + "usb_config_option": { + "data": { + "usb_path": "Chemin du p\u00e9riph\u00e9rique USB" + }, + "description": "S\u00e9lectionnez le port s\u00e9rie du dongle USB Crownstone. \n\n Recherchez un appareil avec VID 10C4 et PID EA60.", + "title": "Configuration du dongle USB Crownstone" + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "Chemin du p\u00e9riph\u00e9rique USB" + }, + "description": "Entrez manuellement le chemin d'un dongle USB Crownstone.", + "title": "Chemin d'acc\u00e8s manuel du dongle USB Crownstone" + }, + "usb_manual_config_option": { + "data": { + "usb_manual_path": "Chemin du p\u00e9riph\u00e9rique USB" + }, + "description": "Entrez manuellement le chemin d'un dongle USB Crownstone.", + "title": "Chemin d'acc\u00e8s manuel du dongle USB Crownstone" + }, + "usb_sphere_config": { + "data": { + "usb_sphere": "Crownstone Sphere" + }, + "description": "S\u00e9lectionnez une sph\u00e8re Crownstone o\u00f9 se trouve l\u2019USB.", + "title": "Sph\u00e8re USB Crownstone" + }, + "usb_sphere_config_option": { + "data": { + "usb_sphere": "Crownstone Sphere" + }, + "description": "S\u00e9lectionnez une sph\u00e8re Crownstone o\u00f9 se trouve l\u2019USB.", + "title": "Sph\u00e8re USB Crownstone" } } } diff --git a/homeassistant/components/daikin/translations/fr.json b/homeassistant/components/daikin/translations/fr.json index 8d033bdb853..3b2dcd8ce27 100644 --- a/homeassistant/components/daikin/translations/fr.json +++ b/homeassistant/components/daikin/translations/fr.json @@ -5,6 +5,7 @@ "cannot_connect": "\u00c9chec de connexion" }, "error": { + "api_password": "Authentification invalide, utilisez la cl\u00e9 API ou le mot de passe.", "cannot_connect": "\u00c9chec de connexion", "invalid_auth": "Authentification invalide", "unknown": "Erreur inattendue" diff --git a/homeassistant/components/deconz/translations/fr.json b/homeassistant/components/deconz/translations/fr.json index 48322a55659..464cc2e139e 100644 --- a/homeassistant/components/deconz/translations/fr.json +++ b/homeassistant/components/deconz/translations/fr.json @@ -11,7 +11,7 @@ "error": { "no_key": "Impossible d'obtenir une cl\u00e9 d'API" }, - "flow_title": "Passerelle deCONZ Zigbee ({host})", + "flow_title": "{host}", "step": { "hassio_confirm": { "description": "Voulez-vous configurer Home Assistant pour qu'il se connecte \u00e0 la passerelle deCONZ fournie par le module compl\u00e9mentaire Hass.io {addon} ?", diff --git a/homeassistant/components/denonavr/translations/fr.json b/homeassistant/components/denonavr/translations/fr.json index 0b7fb29a6d8..474f02f5e21 100644 --- a/homeassistant/components/denonavr/translations/fr.json +++ b/homeassistant/components/denonavr/translations/fr.json @@ -10,7 +10,7 @@ "error": { "discovery_error": "Impossible de d\u00e9couvrir un r\u00e9cepteur r\u00e9seau Denon AVR" }, - "flow_title": "R\u00e9cepteur r\u00e9seau Denon AVR: {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "Veuillez confirmer l'ajout du r\u00e9cepteur", diff --git a/homeassistant/components/devolo_home_network/translations/fr.json b/homeassistant/components/devolo_home_network/translations/fr.json index 1c0fc4827c2..49dc24e0db1 100644 --- a/homeassistant/components/devolo_home_network/translations/fr.json +++ b/homeassistant/components/devolo_home_network/translations/fr.json @@ -1,18 +1,24 @@ { "config": { "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "home_control": "L'unit\u00e9 centrale devolo Home Control ne fonctionne pas avec cette int\u00e9gration." }, "error": { "cannot_connect": "\u00c9chec de connexion", "unknown": "Erreur inattendue" }, + "flow_title": "{product} ( {name} )", "step": { "user": { "data": { "ip_address": "Adresse IP" }, "description": "Voulez-vous commencer la configuration ?" + }, + "zeroconf_confirm": { + "description": "Voulez-vous ajouter le p\u00e9riph\u00e9rique r\u00e9seau domestique devolo avec le nom d'h\u00f4te ` {host_name} ` \u00e0 Home Assistant\u00a0?", + "title": "Appareil r\u00e9seau domestique devolo d\u00e9couvert" } } } diff --git a/homeassistant/components/directv/translations/fr.json b/homeassistant/components/directv/translations/fr.json index 6d5bc24cb5a..9f227f2004e 100644 --- a/homeassistant/components/directv/translations/fr.json +++ b/homeassistant/components/directv/translations/fr.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u00c9chec de connexion" }, - "flow_title": "DirecTV: {name}", + "flow_title": "{name}", "step": { "ssdp_confirm": { "data": { diff --git a/homeassistant/components/dlna_dmr/translations/fr.json b/homeassistant/components/dlna_dmr/translations/fr.json index 4db8bec9bd6..f7a1b9cd71c 100644 --- a/homeassistant/components/dlna_dmr/translations/fr.json +++ b/homeassistant/components/dlna_dmr/translations/fr.json @@ -1,25 +1,57 @@ { "config": { "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "alternative_integration": "L'appareil est mieux pris en charge par une autre int\u00e9gration", + "cannot_connect": "\u00c9chec de connexion", + "could_not_connect": "\u00c9chec de la connexion au p\u00e9riph\u00e9rique DLNA", + "discovery_error": "\u00c9chec de la d\u00e9couverte d'un p\u00e9riph\u00e9rique DLNA correspondant", + "incomplete_config": "Il manque une variable requise dans la configuration", + "non_unique_id": "Plusieurs appareils trouv\u00e9s avec le m\u00eame identifiant unique", + "not_dmr": "L'appareil n'est pas un moteur de rendu multim\u00e9dia num\u00e9rique pris en charge" }, "error": { - "cannot_connect": "\u00c9chec de connexion" + "cannot_connect": "\u00c9chec de connexion", + "could_not_connect": "\u00c9chec de la connexion au p\u00e9riph\u00e9rique DLNA", + "not_dmr": "L'appareil n'est pas un moteur de rendu multim\u00e9dia num\u00e9rique pris en charge" }, + "flow_title": "{name}", "step": { "confirm": { "description": "Voulez-vous commencer la configuration ?" }, + "import_turn_on": { + "description": "Veuillez allumer l'appareil et cliquer sur soumettre pour continuer la migration" + }, "manual": { "data": { "url": "URL" - } + }, + "description": "URL vers un fichier XML de description d'appareil", + "title": "Connexion manuelle de l'appareil DLNA DMR" }, "user": { "data": { "host": "H\u00f4te", "url": "URL" - } + }, + "description": "Choisissez un appareil \u00e0 configurer ou laissez vide pour saisir une URL", + "title": "P\u00e9riph\u00e9riques DLNA DMR d\u00e9couverts" + } + } + }, + "options": { + "error": { + "invalid_url": "URL invalide" + }, + "step": { + "init": { + "data": { + "callback_url_override": "URL de rappel de l'\u00e9couteur d'\u00e9v\u00e9nement", + "listen_port": "Port d'\u00e9coute d'\u00e9v\u00e9nement (al\u00e9atoire s'il n'est pas d\u00e9fini)", + "poll_availability": "Sondage pour la disponibilit\u00e9 de l'appareil" + }, + "title": "Configuration du moteur de rendu multim\u00e9dia num\u00e9rique DLNA" } } } diff --git a/homeassistant/components/doorbird/translations/fr.json b/homeassistant/components/doorbird/translations/fr.json index 92961908be4..68165d762f9 100644 --- a/homeassistant/components/doorbird/translations/fr.json +++ b/homeassistant/components/doorbird/translations/fr.json @@ -10,7 +10,7 @@ "invalid_auth": "Authentification invalide", "unknown": "Erreur inattendue" }, - "flow_title": "DoorBird {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/efergy/translations/fr.json b/homeassistant/components/efergy/translations/fr.json index 1e0299533ea..2e98eca19e7 100644 --- a/homeassistant/components/efergy/translations/fr.json +++ b/homeassistant/components/efergy/translations/fr.json @@ -1,12 +1,21 @@ { "config": { "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { "cannot_connect": "\u00c9chec de connexion", "invalid_auth": "Authentification invalide", "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "api_key": "Cl\u00e9 d'API" + }, + "title": "Efergy" + } } } } \ No newline at end of file diff --git a/homeassistant/components/elgato/translations/fr.json b/homeassistant/components/elgato/translations/fr.json index 6cd1cd247a7..5f3e99b0425 100644 --- a/homeassistant/components/elgato/translations/fr.json +++ b/homeassistant/components/elgato/translations/fr.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u00c9chec de connexion" }, - "flow_title": "Elgato Key Light: {serial_number}", + "flow_title": "{serial_number}", "step": { "user": { "data": { diff --git a/homeassistant/components/elmax/translations/fr.json b/homeassistant/components/elmax/translations/fr.json new file mode 100644 index 00000000000..3c19c6975dd --- /dev/null +++ b/homeassistant/components/elmax/translations/fr.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "bad_auth": "Authentification invalide", + "invalid_pin": "Le code PIN fourni n\u2019est pas valide", + "network_error": "Une erreur r\u00e9seau s'est produite", + "no_panel_online": "Aucun panneau de contr\u00f4le Elmax en ligne n'a \u00e9t\u00e9 trouv\u00e9.", + "unknown_error": "une erreur inattendue est apparue" + }, + "step": { + "panels": { + "data": { + "panel_id": "Identifiant du panneau", + "panel_name": "Nom du panneau", + "panel_pin": "Code PIN" + }, + "description": "S\u00e9lectionnez le panneau que vous souhaitez contr\u00f4ler avec cette int\u00e9gration. Veuillez noter que le panneau doit \u00eatre allum\u00e9 pour \u00eatre configur\u00e9.", + "title": "S\u00e9lection du panneau" + }, + "user": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + }, + "description": "Veuillez vous connecter au cloud Elmax en utilisant vos informations d'identification", + "title": "Connexion au compte" + } + } + }, + "title": "Configuration d\u2019Elmax Cloud" +} \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/he.json b/homeassistant/components/elmax/translations/he.json new file mode 100644 index 00000000000..e428d0009ae --- /dev/null +++ b/homeassistant/components/elmax/translations/he.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emonitor/translations/fr.json b/homeassistant/components/emonitor/translations/fr.json index 9557160e335..aaacc8bf140 100644 --- a/homeassistant/components/emonitor/translations/fr.json +++ b/homeassistant/components/emonitor/translations/fr.json @@ -7,7 +7,7 @@ "cannot_connect": "\u00c9chec de connexion", "unknown": "Erreur inattendue" }, - "flow_title": "SiteSage {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "Voulez-vous configurer {name} ( {host} )?", diff --git a/homeassistant/components/enphase_envoy/translations/fr.json b/homeassistant/components/enphase_envoy/translations/fr.json index a369562d70c..165c54c67d1 100644 --- a/homeassistant/components/enphase_envoy/translations/fr.json +++ b/homeassistant/components/enphase_envoy/translations/fr.json @@ -9,14 +9,15 @@ "invalid_auth": "Authentification invalide", "unknown": "Erreur inattendue" }, - "flow_title": "Envoy\u00e9 {serial} ({host})", + "flow_title": "{serial} ({host})", "step": { "user": { "data": { "host": "H\u00f4te", "password": "Mot de passe", "username": "Nom d'utilisateur" - } + }, + "description": "Pour les mod\u00e8les plus r\u00e9cents, saisissez le nom d'utilisateur \u00ab\u00a0envoy\u00a0\u00bb sans mot de passe. Pour les mod\u00e8les plus anciens, entrez le nom d'utilisateur \u00ab\u00a0installer\u00a0\u00bb sans mot de passe. Pour tous les autres mod\u00e8les, entrez un nom d'utilisateur et un mot de passe valides." } } } diff --git a/homeassistant/components/environment_canada/translations/fr.json b/homeassistant/components/environment_canada/translations/fr.json index 84333da4f4a..d09b1eec095 100644 --- a/homeassistant/components/environment_canada/translations/fr.json +++ b/homeassistant/components/environment_canada/translations/fr.json @@ -1,15 +1,22 @@ { "config": { "error": { + "bad_station_id": "L'ID de station est invalide, manquant ou introuvable dans la base de donn\u00e9es d'ID de station", "cannot_connect": "\u00c9chec de connexion", + "error_response": "R\u00e9ponse d'Environnement Canada par erreur", + "too_many_attempts": "Les connexions \u00e0 Environnement Canada sont limit\u00e9es en termes de taux; R\u00e9essayez dans 60 secondes", "unknown": "Erreur inattendue" }, "step": { "user": { "data": { + "language": "Langue des informations m\u00e9t\u00e9orologiques", "latitude": "Latitude", - "longitude": "Longitude" - } + "longitude": "Longitude", + "station": "ID de la station m\u00e9t\u00e9orologique" + }, + "description": "Un ID de station ou une latitude/longitude doit \u00eatre sp\u00e9cifi\u00e9. La latitude/longitude par d\u00e9faut utilis\u00e9e sont les valeurs configur\u00e9es dans votre installation Home Assistant. La station m\u00e9t\u00e9o la plus proche des coordonn\u00e9es sera utilis\u00e9e si vous sp\u00e9cifiez des coordonn\u00e9es. Si un code de station est utilis\u00e9, il doit suivre le format : PP/code, o\u00f9 PP est la province \u00e0 deux lettres et le code est l'ID de la station. La liste des identifiants de station peut \u00eatre trouv\u00e9e ici : https://dd.weather.gc.ca/citypage_weather/docs/site_list_towns_en.csv. Les informations m\u00e9t\u00e9orologiques peuvent \u00eatre r\u00e9cup\u00e9r\u00e9es en anglais ou en fran\u00e7ais.", + "title": "Environnement Canada\u00a0: emplacement m\u00e9t\u00e9o et langue" } } } diff --git a/homeassistant/components/esphome/translations/fr.json b/homeassistant/components/esphome/translations/fr.json index 860755e97ba..7125ef9c395 100644 --- a/homeassistant/components/esphome/translations/fr.json +++ b/homeassistant/components/esphome/translations/fr.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", - "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours" + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { "connection_error": "Impossible de se connecter \u00e0 ESP. Assurez-vous que votre fichier YAML contient une ligne 'api:'.", @@ -10,7 +11,7 @@ "invalid_psk": "La cl\u00e9 de chiffrement de transport n\u2019est pas valide. Assurez-vous qu\u2019elle correspond \u00e0 ce que vous avez dans votre configuration", "resolve_error": "Impossible de r\u00e9soudre l'adresse de l'ESP. Si cette erreur persiste, veuillez d\u00e9finir une adresse IP statique: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, - "flow_title": "ESPHome: {name}", + "flow_title": "{name}", "step": { "authenticate": { "data": { @@ -28,6 +29,12 @@ }, "description": "Entrez la cl\u00e9 de chiffrement que vous avez d\u00e9finie dans votre configuration pour {name}." }, + "reauth_confirm": { + "data": { + "noise_psk": "Cl\u00e9 de chiffrement" + }, + "description": "L'appareil ESPHome {name} activ\u00e9 le cryptage de transport ou modifi\u00e9 la cl\u00e9 de cryptage. Veuillez saisir la cl\u00e9 mise \u00e0 jour." + }, "user": { "data": { "host": "H\u00f4te", diff --git a/homeassistant/components/flux_led/translations/fr.json b/homeassistant/components/flux_led/translations/fr.json index 9cb1d7dfd16..c2177a0cb1f 100644 --- a/homeassistant/components/flux_led/translations/fr.json +++ b/homeassistant/components/flux_led/translations/fr.json @@ -1,9 +1,34 @@ { "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion" + }, + "flow_title": "{model} {id} ( {ipaddr} )", "step": { + "discovery_confirm": { + "description": "Voulez-vous configurer {model} {id} ( {ipaddr} )\u00a0?" + }, "user": { "data": { "host": "H\u00f4te" + }, + "description": "Si vous laissez l'h\u00f4te vide, la d\u00e9couverte sera utilis\u00e9e pour trouver des p\u00e9riph\u00e9riques." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "custom_effect_colors": "Effet personnalis\u00e9 : liste de 1 \u00e0 16 couleurs [R, V, B]. Exemple\u00a0: [255,0,255],[60,128,0]", + "custom_effect_speed_pct": "Effet personnalis\u00e9\u00a0: vitesse en pourcentage pour l'effet qui change les couleurs.", + "custom_effect_transition": "Effet personnalis\u00e9 : Type de transition entre les couleurs.", + "mode": "Le mode de luminosit\u00e9 choisi." } } } diff --git a/homeassistant/components/forked_daapd/translations/fr.json b/homeassistant/components/forked_daapd/translations/fr.json index 2b3633f01a0..11951f50a95 100644 --- a/homeassistant/components/forked_daapd/translations/fr.json +++ b/homeassistant/components/forked_daapd/translations/fr.json @@ -12,7 +12,7 @@ "wrong_password": "Mot de passe incorrect.", "wrong_server_type": "L'int\u00e9gration forked-daapd n\u00e9cessite un serveur forked-daapd avec la version > = 27.0." }, - "flow_title": "serveur forked-daapd: {name} ( {host} )", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/fritz/translations/fr.json b/homeassistant/components/fritz/translations/fr.json index f46c47cab5e..38c8a9e802d 100644 --- a/homeassistant/components/fritz/translations/fr.json +++ b/homeassistant/components/fritz/translations/fr.json @@ -12,7 +12,7 @@ "connection_error": "\u00c9chec de connexion", "invalid_auth": "Authentification invalide" }, - "flow_title": "FRITZ!Box Tools : {name}", + "flow_title": "{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/fritzbox/translations/fr.json b/homeassistant/components/fritzbox/translations/fr.json index 1f9d5d9893b..8a75221ea88 100644 --- a/homeassistant/components/fritzbox/translations/fr.json +++ b/homeassistant/components/fritzbox/translations/fr.json @@ -10,7 +10,7 @@ "error": { "invalid_auth": "Authentification invalide" }, - "flow_title": "AVM FRITZ!Box : {name}", + "flow_title": "{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/fritzbox_callmonitor/translations/fr.json b/homeassistant/components/fritzbox_callmonitor/translations/fr.json index 99baf51d0bd..2d1cadb8a48 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/fr.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/fr.json @@ -8,7 +8,7 @@ "error": { "invalid_auth": "Authentification invalide" }, - "flow_title": "Moniteur d'appels AVM FRITZ! Box: {name}", + "flow_title": "{name}", "step": { "phonebook": { "data": { diff --git a/homeassistant/components/fronius/translations/fr.json b/homeassistant/components/fronius/translations/fr.json new file mode 100644 index 00000000000..e7ea85962bd --- /dev/null +++ b/homeassistant/components/fronius/translations/fr.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "unknown": "Erreur inattendue" + }, + "flow_title": "{device}", + "step": { + "confirm_discovery": { + "description": "Voulez-vous ajouter {device} \u00e0 Home Assistant\u00a0?" + }, + "user": { + "data": { + "host": "H\u00f4te" + }, + "description": "Configurez l'adresse IP ou le nom d'h\u00f4te local de votre appareil Fronius.", + "title": "Fronius SolarNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/goalzero/translations/fr.json b/homeassistant/components/goalzero/translations/fr.json index 469f37143a2..7def0d06402 100644 --- a/homeassistant/components/goalzero/translations/fr.json +++ b/homeassistant/components/goalzero/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/harmony/translations/fr.json b/homeassistant/components/harmony/translations/fr.json index 25b9e24eb5f..077405be95f 100644 --- a/homeassistant/components/harmony/translations/fr.json +++ b/homeassistant/components/harmony/translations/fr.json @@ -7,7 +7,7 @@ "cannot_connect": "\u00c9chec de connexion", "unknown": "Erreur inattendue" }, - "flow_title": "Logitech Harmony Hub {name}", + "flow_title": "{name}", "step": { "link": { "description": "Voulez-vous configurer {name} ( {host} ) ?", diff --git a/homeassistant/components/homekit/translations/fr.json b/homeassistant/components/homekit/translations/fr.json index ef931792193..a66192ace9a 100644 --- a/homeassistant/components/homekit/translations/fr.json +++ b/homeassistant/components/homekit/translations/fr.json @@ -29,6 +29,7 @@ }, "cameras": { "data": { + "camera_audio": "Cam\u00e9ras prenant en charge l'audio", "camera_copy": "Cam\u00e9ras prenant en charge les flux H.264 natifs" }, "description": "V\u00e9rifiez toutes les cam\u00e9ras prenant en charge les flux H.264 natifs. Si la cam\u00e9ra ne produit pas de flux H.264, le syst\u00e8me transcodera la vid\u00e9o en H.264 pour HomeKit. Le transcodage n\u00e9cessite un processeur performant et il est peu probable qu'il fonctionne sur des ordinateurs \u00e0 carte unique.", diff --git a/homeassistant/components/homekit_controller/translations/fr.json b/homeassistant/components/homekit_controller/translations/fr.json index 72d8d58517f..18f3e82aa76 100644 --- a/homeassistant/components/homekit_controller/translations/fr.json +++ b/homeassistant/components/homekit_controller/translations/fr.json @@ -18,7 +18,7 @@ "unable_to_pair": "Impossible d'appairer, veuillez r\u00e9essayer.", "unknown_error": "L'appareil a signal\u00e9 une erreur inconnue. L'appairage a \u00e9chou\u00e9." }, - "flow_title": "{name} via le protocole accessoire HomeKit", + "flow_title": "{name}", "step": { "busy_error": { "description": "Annulez l'association sur tous les contr\u00f4leurs ou essayez de red\u00e9marrer l'appareil, puis continuez \u00e0 reprendre l'association.", diff --git a/homeassistant/components/huawei_lte/translations/fr.json b/homeassistant/components/huawei_lte/translations/fr.json index d04f7e83d3f..ca360ffc077 100644 --- a/homeassistant/components/huawei_lte/translations/fr.json +++ b/homeassistant/components/huawei_lte/translations/fr.json @@ -15,7 +15,7 @@ "response_error": "Erreur inconnue de l'appareil", "unknown": "Erreur inattendue" }, - "flow_title": "Huawei LTE: {nom}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/hue/translations/fr.json b/homeassistant/components/hue/translations/fr.json index ee82b3ec4e6..63032e42dac 100644 --- a/homeassistant/components/hue/translations/fr.json +++ b/homeassistant/components/hue/translations/fr.json @@ -35,6 +35,10 @@ }, "device_automation": { "trigger_subtype": { + "1": "Premier bouton", + "2": "Deuxi\u00e8me bouton", + "3": "Troisi\u00e8me bouton", + "4": "Quatri\u00e8me bouton", "button_1": "Premier bouton", "button_2": "Deuxi\u00e8me bouton", "button_3": "Troisi\u00e8me bouton", @@ -47,11 +51,16 @@ "turn_on": "Allumer" }, "trigger_type": { + "double_short_release": "Les deux \" {subtype} \" ont \u00e9t\u00e9 rel\u00e2ch\u00e9s", + "initial_press": "Bouton \" {subtype} \" appuy\u00e9 initialement", + "long_release": "Bouton \" {subtype} \" rel\u00e2ch\u00e9 apr\u00e8s un appui long", "remote_button_long_release": "Bouton \" {subtype} \" rel\u00e2ch\u00e9 apr\u00e8s un appui long", "remote_button_short_press": "bouton \"{subtype}\" est press\u00e9", "remote_button_short_release": "Bouton \" {subtype} \" est rel\u00e2ch\u00e9", "remote_double_button_long_press": "Les deux \"{sous-type}\" ont \u00e9t\u00e9 rel\u00e2ch\u00e9s apr\u00e8s un appui long", - "remote_double_button_short_press": "Les deux \" {subtype} \" ont \u00e9t\u00e9 rel\u00e2ch\u00e9s" + "remote_double_button_short_press": "Les deux \" {subtype} \" ont \u00e9t\u00e9 rel\u00e2ch\u00e9s", + "repeat": "Bouton \" {subtype} \" maintenu enfonc\u00e9", + "short_release": "Bouton \" {subtype} \" rel\u00e2ch\u00e9 apr\u00e8s un appui court" } }, "options": { @@ -59,6 +68,7 @@ "init": { "data": { "allow_hue_groups": "Autoriser les groupes Hue", + "allow_hue_scenes": "Autoriser les sc\u00e8nes Hue", "allow_unreachable": "Autoriser les ampoules inaccessibles \u00e0 signaler correctement leur \u00e9tat" } } diff --git a/homeassistant/components/iotawatt/translations/fr.json b/homeassistant/components/iotawatt/translations/fr.json index 9cb1d7dfd16..1452beb4465 100644 --- a/homeassistant/components/iotawatt/translations/fr.json +++ b/homeassistant/components/iotawatt/translations/fr.json @@ -1,6 +1,18 @@ { "config": { + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue" + }, "step": { + "auth": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + }, + "description": "L'appareil IoTawatt n\u00e9cessite une authentification. Veuillez saisir le nom d'utilisateur et le mot de passe et cliquez sur le bouton Soumettre." + }, "user": { "data": { "host": "H\u00f4te" diff --git a/homeassistant/components/ipp/translations/fr.json b/homeassistant/components/ipp/translations/fr.json index 21805c55330..4ae1cc602f4 100644 --- a/homeassistant/components/ipp/translations/fr.json +++ b/homeassistant/components/ipp/translations/fr.json @@ -13,7 +13,7 @@ "cannot_connect": "\u00c9chec de connexion", "connection_upgrade": "Impossible de se connecter \u00e0 l'imprimante. Veuillez r\u00e9essayer avec l'option SSL / TLS coch\u00e9e." }, - "flow_title": "Imprimante: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/isy994/translations/fr.json b/homeassistant/components/isy994/translations/fr.json index 8a4e4ffa707..654df08431d 100644 --- a/homeassistant/components/isy994/translations/fr.json +++ b/homeassistant/components/isy994/translations/fr.json @@ -9,7 +9,7 @@ "invalid_host": "L'entr\u00e9e d'h\u00f4te n'\u00e9tait pas au format URL complet, par exemple http://192.168.10.100:80", "unknown": "Erreur inattendue" }, - "flow_title": "Appareils universels ISY994 {name} ( {host} )", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/knx/translations/fr.json b/homeassistant/components/knx/translations/fr.json new file mode 100644 index 00000000000..9efac4d94d0 --- /dev/null +++ b/homeassistant/components/knx/translations/fr.json @@ -0,0 +1,65 @@ +{ + "config": { + "abort": { + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9", + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + }, + "error": { + "cannot_connect": "\u00c9chec de connexion" + }, + "step": { + "manual_tunnel": { + "data": { + "host": "H\u00f4te", + "individual_address": "Adresse individuelle pour la connexion", + "local_ip": "IP locale (laisser vide en cas de doute)", + "port": "Port", + "route_back": "Retour/Mode NAT" + }, + "description": "Veuillez saisir les informations de connexion de votre p\u00e9riph\u00e9rique de tunneling." + }, + "routing": { + "data": { + "individual_address": "Adresse individuelle pour la connexion de routage", + "multicast_group": "Le groupe multicast utilis\u00e9 pour le routage", + "multicast_port": "Le port multicast utilis\u00e9 pour le routage" + }, + "description": "Veuillez configurer les options de routage." + }, + "tunnel": { + "data": { + "gateway": "Connexion tunnel KNX" + }, + "description": "Veuillez s\u00e9lectionner une passerelle dans la liste." + }, + "type": { + "data": { + "connection_type": "Type de connexion KNX" + }, + "description": "Veuillez saisir le type de connexion que nous devons utiliser pour votre connexion KNX.\n AUTOMATIQUE - L'int\u00e9gration prend en charge la connectivit\u00e9 \u00e0 votre bus KNX en effectuant un scan de passerelle.\n TUNNELING - L'int\u00e9gration se connectera \u00e0 votre bus KNX via tunneling.\n ROUTAGE - L'int\u00e9gration se connectera \u00e0 votre bus KNX via le routage." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "connection_type": "Type de connexion KNX", + "individual_address": "Adresse individuelle par d\u00e9faut", + "multicast_group": "Groupe de multidiffusion utilis\u00e9 pour le routage et la d\u00e9couverte", + "multicast_port": "Port de multidiffusion utilis\u00e9 pour le routage et la d\u00e9couverte", + "rate_limit": "Nombre maximal de t\u00e9l\u00e9grammes sortants par seconde", + "state_updater": "Activer globalement la lecture des \u00e9tats depuis le bus KNX" + } + }, + "tunnel": { + "data": { + "host": "H\u00f4te", + "local_ip": "IP locale (laisser vide en cas de doute)", + "port": "Port", + "route_back": "Retour/Mode NAT" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/fr.json b/homeassistant/components/kodi/translations/fr.json index 8e740466bc4..e94282fa393 100644 --- a/homeassistant/components/kodi/translations/fr.json +++ b/homeassistant/components/kodi/translations/fr.json @@ -12,7 +12,7 @@ "invalid_auth": "Authentification invalide", "unknown": "Erreur inattendue" }, - "flow_title": "Kodi: {name}", + "flow_title": "{name}", "step": { "credentials": { "data": { diff --git a/homeassistant/components/konnected/translations/fr.json b/homeassistant/components/konnected/translations/fr.json index 7d50c474909..2ddd335fa9e 100644 --- a/homeassistant/components/konnected/translations/fr.json +++ b/homeassistant/components/konnected/translations/fr.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "cannot_connect": "\u00c9chec de connexion", "not_konn_panel": "Non reconnu comme appareil Konnected.io", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/lcn/translations/ca.json b/homeassistant/components/lcn/translations/ca.json new file mode 100644 index 00000000000..e6d704f1668 --- /dev/null +++ b/homeassistant/components/lcn/translations/ca.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "trigger_type": { + "fingerprint": "codi d'empremta rebut", + "send_keys": "claus d'enviament rebudes", + "transmitter": "codi del transmissor rebut", + "transponder": "codi del transpoder rebut" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lcn/translations/de.json b/homeassistant/components/lcn/translations/de.json new file mode 100644 index 00000000000..e7716b1beba --- /dev/null +++ b/homeassistant/components/lcn/translations/de.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "trigger_type": { + "fingerprint": "Fingerabdruckcode empfangen", + "send_keys": "Sendeschl\u00fcssel empfangen", + "transmitter": "Sendercode empfangen", + "transponder": "Transpondercode empfangen" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lcn/translations/et.json b/homeassistant/components/lcn/translations/et.json new file mode 100644 index 00000000000..eff0c5bd79e --- /dev/null +++ b/homeassistant/components/lcn/translations/et.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "trigger_type": { + "fingerprint": "vastu v\u00f5etud s\u00f5rmej\u00e4ljekood", + "send_keys": "vastuv\u00f5etud v\u00f5tmete saatmine", + "transmitter": "saatja kood vastu v\u00f5etud", + "transponder": "saadud transpooderi kood" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lcn/translations/fr.json b/homeassistant/components/lcn/translations/fr.json new file mode 100644 index 00000000000..7a2202e58a7 --- /dev/null +++ b/homeassistant/components/lcn/translations/fr.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "trigger_type": { + "fingerprint": "code d'empreinte digitale re\u00e7u", + "send_keys": "code \u00e9metteur re\u00e7u", + "transmitter": "code \u00e9metteur re\u00e7u", + "transponder": "code transpodeur re\u00e7u" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lcn/translations/id.json b/homeassistant/components/lcn/translations/id.json new file mode 100644 index 00000000000..e265e2e357b --- /dev/null +++ b/homeassistant/components/lcn/translations/id.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "trigger_type": { + "fingerprint": "kode sidik jari diterima", + "send_keys": "kode dikirim diterima", + "transmitter": "kode pemancar diterima", + "transponder": "kode transponder diterima" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lcn/translations/ja.json b/homeassistant/components/lcn/translations/ja.json new file mode 100644 index 00000000000..b656835dcbc --- /dev/null +++ b/homeassistant/components/lcn/translations/ja.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "trigger_type": { + "fingerprint": "\u6307\u7d0b\u30b3\u30fc\u30c9\u3092\u53d7\u4fe1\u3057\u307e\u3057\u305f(fingerprint code received)", + "send_keys": "\u9001\u4fe1\u30ad\u30fc\u3092\u53d7\u4fe1\u3057\u307e\u3057\u305f(send keys received)", + "transmitter": "\u9001\u4fe1\u6a5f\u30b3\u30fc\u30c9\u53d7\u4fe1\u3057\u307e\u3057\u305f(transmitter code received)", + "transponder": "\u30c8\u30e9\u30f3\u30b9\u30dd\u30fc\u30c0\u30fc\u30b3\u30fc\u30c9\u3092\u53d7\u4fe1\u3057\u307e\u3057\u305f(transpoder code received)" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lcn/translations/ru.json b/homeassistant/components/lcn/translations/ru.json new file mode 100644 index 00000000000..0953ee96ef7 --- /dev/null +++ b/homeassistant/components/lcn/translations/ru.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "trigger_type": { + "fingerprint": "\u043f\u043e\u043b\u0443\u0447\u0435\u043d \u043a\u043e\u0434 \u043e\u0442\u043f\u0435\u0447\u0430\u0442\u043a\u0430 \u043f\u0430\u043b\u044c\u0446\u0430", + "send_keys": "\u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0430 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044b\u0445 \u043a\u043b\u044e\u0447\u0435\u0439", + "transmitter": "\u043f\u043e\u043b\u0443\u0447\u0435\u043d \u043a\u043e\u0434 \u043e\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u0442\u0447\u0438\u043a\u0430", + "transponder": "\u043f\u043e\u043b\u0443\u0447\u0435\u043d \u043a\u043e\u0434 \u043e\u0442 \u043f\u0440\u0438\u0451\u043c\u043d\u0438\u043a\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lcn/translations/zh-Hant.json b/homeassistant/components/lcn/translations/zh-Hant.json new file mode 100644 index 00000000000..d72235caabe --- /dev/null +++ b/homeassistant/components/lcn/translations/zh-Hant.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "trigger_type": { + "fingerprint": "\u5df2\u6536\u5230\u6307\u7d0b\u78bc", + "send_keys": "\u5df2\u6536\u5230\u50b3\u9001\u5bc6\u9470", + "transmitter": "\u5df2\u6536\u5230\u767c\u5c04\u5668\u78bc", + "transponder": "\u5df2\u6536\u5230\u8a62\u7b54\u5668\u78bc" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lookin/translations/fr.json b/homeassistant/components/lookin/translations/fr.json new file mode 100644 index 00000000000..7276af22624 --- /dev/null +++ b/homeassistant/components/lookin/translations/fr.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "cannot_connect": "\u00c9chec de connexion", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", + "unknown": "Erreur inattendue" + }, + "flow_title": "{name} ({host})", + "step": { + "device_name": { + "data": { + "name": "Nom" + } + }, + "discovery_confirm": { + "description": "Voulez-vous configurer {name} ( {host} )?" + }, + "user": { + "data": { + "ip_address": "Adresse IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/fr.json b/homeassistant/components/lutron_caseta/translations/fr.json index 0dcc8755173..cdf584fcc00 100644 --- a/homeassistant/components/lutron_caseta/translations/fr.json +++ b/homeassistant/components/lutron_caseta/translations/fr.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "\u00c9chec de connexion" }, - "flow_title": "Lutron Cas\u00e9ta {name} ( {host} )", + "flow_title": "{name} ( {host} )", "step": { "import_failed": { "description": "Impossible de configurer la passerelle (h\u00f4te: {host} ) import\u00e9 \u00e0 partir de configuration.yaml.", diff --git a/homeassistant/components/mill/translations/fr.json b/homeassistant/components/mill/translations/fr.json index ffcff15ade8..440ef77ea0a 100644 --- a/homeassistant/components/mill/translations/fr.json +++ b/homeassistant/components/mill/translations/fr.json @@ -7,11 +7,25 @@ "cannot_connect": "\u00c9chec de connexion" }, "step": { - "user": { + "cloud": { "data": { "password": "Mot de passe", "username": "Nom d'utilisateur" } + }, + "local": { + "data": { + "ip_address": "Adresse IP" + }, + "description": "Adresse IP locale de l'appareil." + }, + "user": { + "data": { + "connection_type": "S\u00e9lectionner le type de connexion", + "password": "Mot de passe", + "username": "Nom d'utilisateur" + }, + "description": "S\u00e9lectionnez le type de connexion. Local n\u00e9cessite des radiateurs de g\u00e9n\u00e9ration 3" } } } diff --git a/homeassistant/components/modem_callerid/translations/fr.json b/homeassistant/components/modem_callerid/translations/fr.json index 5847c82af6a..696927d66db 100644 --- a/homeassistant/components/modem_callerid/translations/fr.json +++ b/homeassistant/components/modem_callerid/translations/fr.json @@ -2,17 +2,24 @@ "config": { "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", - "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours" + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "no_devices_found": "Aucun appareil restant trouv\u00e9" }, "error": { "cannot_connect": "\u00c9chec de connexion" }, "step": { + "usb_confirm": { + "description": "Il s'agit d'une int\u00e9gration pour les appels fixes utilisant un modem vocal CX93001. Cela peut r\u00e9cup\u00e9rer les informations d'identification de l'appelant avec une option pour rejeter un appel entrant.", + "title": "Modem t\u00e9l\u00e9phonique" + }, "user": { "data": { "name": "Nom", "port": "Port" - } + }, + "description": "Il s'agit d'une int\u00e9gration pour les appels fixes utilisant un modem vocal CX93001. Cela peut r\u00e9cup\u00e9rer les informations d'identification de l'appelant avec une option pour rejeter un appel entrant.", + "title": "Modem t\u00e9l\u00e9phonique" } } } diff --git a/homeassistant/components/motion_blinds/translations/fr.json b/homeassistant/components/motion_blinds/translations/fr.json index dc883066c47..75fb1daa9be 100644 --- a/homeassistant/components/motion_blinds/translations/fr.json +++ b/homeassistant/components/motion_blinds/translations/fr.json @@ -6,13 +6,15 @@ "connection_error": "\u00c9chec de connexion" }, "error": { - "discovery_error": "Impossible de d\u00e9couvrir une Motion Gateway" + "discovery_error": "Impossible de d\u00e9couvrir une Motion Gateway", + "invalid_interface": "Interface r\u00e9seau non valide" }, "flow_title": "Stores de mouvement", "step": { "connect": { "data": { - "api_key": "Cl\u00e9 d'API" + "api_key": "Cl\u00e9 d'API", + "interface": "Interface r\u00e9seau \u00e0 utiliser" }, "description": "Vous aurez besoin de la cl\u00e9 API de 16 caract\u00e8res, voir https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key pour les instructions", "title": "Stores de mouvement" @@ -33,5 +35,16 @@ "title": "Stores de mouvement" } } + }, + "options": { + "step": { + "init": { + "data": { + "wait_for_push": "Attendre la mise \u00e0 jour de la diffusion group\u00e9e" + }, + "description": "Sp\u00e9cifiez les param\u00e8tres optionnels", + "title": "Store motoris\u00e9" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/fr.json b/homeassistant/components/motioneye/translations/fr.json index c7a27090396..db844987d88 100644 --- a/homeassistant/components/motioneye/translations/fr.json +++ b/homeassistant/components/motioneye/translations/fr.json @@ -30,6 +30,7 @@ "step": { "init": { "data": { + "stream_url_template": "Mod\u00e8le d'URL de flux", "webhook_set": "Configurer les webhooks motionEye pour signaler les \u00e9v\u00e9nements \u00e0 Home Assistant", "webhook_set_overwrite": "\u00c9craser les webhooks non reconnus" } diff --git a/homeassistant/components/nam/translations/fr.json b/homeassistant/components/nam/translations/fr.json index fbb2f4ae367..58a626f4071 100644 --- a/homeassistant/components/nam/translations/fr.json +++ b/homeassistant/components/nam/translations/fr.json @@ -2,17 +2,34 @@ "config": { "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", - "device_unsupported": "L'appareil n'est pas pris en charge." + "device_unsupported": "L'appareil n'est pas pris en charge.", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", + "reauth_unsuccessful": "La r\u00e9authentification a \u00e9chou\u00e9, veuillez supprimer l'int\u00e9gration et la reconfigurer." }, "error": { "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide", "unknown": "Erreur inattendue" }, - "flow_title": "{nom}", + "flow_title": "{host}", "step": { "confirm_discovery": { "description": "Voulez-vous configurer Nettigo Air Monitor chez {host} ?" }, + "credentials": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + }, + "description": "Veuillez saisir le nom d'utilisateur et le mot de passe." + }, + "reauth_confirm": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + }, + "description": "Veuillez saisir le nom d'utilisateur et le mot de passe corrects pour l'h\u00f4te\u00a0: {host}" + }, "user": { "data": { "host": "H\u00f4te" diff --git a/homeassistant/components/nest/translations/fr.json b/homeassistant/components/nest/translations/fr.json index 06f6897f364..adb7999d772 100644 --- a/homeassistant/components/nest/translations/fr.json +++ b/homeassistant/components/nest/translations/fr.json @@ -2,6 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", + "invalid_access_token": "Jeton d'acc\u00e8s non valide", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )", "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", @@ -12,12 +13,22 @@ "default": "Authentification r\u00e9ussie" }, "error": { + "bad_project_id": "Veuillez saisir un ID de projet Cloud valide (v\u00e9rifiez Cloud\u00a0Console)", "internal_error": "Erreur interne lors de la validation du code", "invalid_pin": "Code PIN invalide", + "subscriber_error": "Erreur d'abonn\u00e9 inconnue, voir les journaux", "timeout": "D\u00e9lai de la validation du code expir\u00e9", - "unknown": "Erreur inattendue" + "unknown": "Erreur inattendue", + "wrong_project_id": "Veuillez saisir un ID de projet Cloud valide (ID de projet d'acc\u00e8s \u00e0 l'appareil trouv\u00e9)" }, "step": { + "auth": { + "data": { + "code": "Jeton d'acc\u00e8s" + }, + "description": "Pour lier votre compte Google, [autorisez votre compte]( {url} ). \n\n Apr\u00e8s autorisation, copiez-collez le code d'authentification fourni ci-dessous.", + "title": "Associer un compte Google" + }, "init": { "data": { "flow_impl": "Fournisseur" @@ -35,6 +46,13 @@ "pick_implementation": { "title": "S\u00e9lectionner une m\u00e9thode d'authentification" }, + "pubsub": { + "data": { + "cloud_project_id": "ID de projet Google Cloud" + }, + "description": "Visitez la [Console Cloud]({url}) pour trouver votre ID de projet Google Cloud.", + "title": "Configurer Google\u00a0Cloud" + }, "reauth_confirm": { "description": "L'int\u00e9gration Nest doit r\u00e9-authentifier votre compte", "title": "R\u00e9-authentifier l'int\u00e9gration" diff --git a/homeassistant/components/netatmo/translations/fr.json b/homeassistant/components/netatmo/translations/fr.json index 3a66f224bc2..5de581d301b 100644 --- a/homeassistant/components/netatmo/translations/fr.json +++ b/homeassistant/components/netatmo/translations/fr.json @@ -4,6 +4,7 @@ "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, "create_entry": { @@ -12,6 +13,10 @@ "step": { "pick_implementation": { "title": "S\u00e9lectionner une m\u00e9thode d'authentification" + }, + "reauth_confirm": { + "description": "L'int\u00e9gration Netatmo doit r\u00e9-authentifier votre compte", + "title": "R\u00e9-authentifier l'int\u00e9gration" } } }, diff --git a/homeassistant/components/netgear/translations/fr.json b/homeassistant/components/netgear/translations/fr.json new file mode 100644 index 00000000000..f4b0af97896 --- /dev/null +++ b/homeassistant/components/netgear/translations/fr.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "config": "Erreur de connexion ou de connexion : veuillez v\u00e9rifier votre configuration" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te (facultatif)", + "password": "Mot de passe", + "port": "Port (facultatif)", + "ssl": "Utilise un certificat SSL", + "username": "Nom d'utilisateur (Optional)" + }, + "description": "H\u00f4te par d\u00e9faut\u00a0: {host}\n Port par d\u00e9faut\u00a0: {port}\n Nom d'utilisateur par d\u00e9faut\u00a0: {username}", + "title": "Netgear" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Consid\u00e9rez le temps pass\u00e9 \u00e0 la maison (secondes)" + }, + "description": "Sp\u00e9cifiez les param\u00e8tres optionnels", + "title": "Netgear" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nina/translations/fr.json b/homeassistant/components/nina/translations/fr.json new file mode 100644 index 00000000000..83f994b79a4 --- /dev/null +++ b/homeassistant/components/nina/translations/fr.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "no_selection": "Veuillez s\u00e9lectionner au moins une ville/une region", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "_a_to_d": "Ville/R\u00e9gion (A-D)", + "_e_to_h": "Ville/R\u00e9gion (E-H)", + "_i_to_l": "Ville/R\u00e9gion (I-L)", + "_m_to_q": "Ville/R\u00e9gion (M-Q)", + "_r_to_u": "Ville/R\u00e9gion (R-U)", + "_v_to_z": "Ville/R\u00e9gion (V-Z)", + "corona_filter": "Supprimer les avertissements Corona", + "slots": "Nombre maximal d'avertissements par ville/region" + }, + "title": "S\u00e9lectionnez la ville/la region" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/notion/translations/fr.json b/homeassistant/components/notion/translations/fr.json index 5979af3cf04..111fc918818 100644 --- a/homeassistant/components/notion/translations/fr.json +++ b/homeassistant/components/notion/translations/fr.json @@ -1,13 +1,22 @@ { "config": { "abort": { - "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { "invalid_auth": "Authentification invalide", - "no_devices": "Aucun appareil trouv\u00e9 sur le compte" + "no_devices": "Aucun appareil trouv\u00e9 sur le compte", + "unknown": "Erreur inattendue" }, "step": { + "reauth_confirm": { + "data": { + "password": "Mot de passe" + }, + "description": "Veuillez saisir \u00e0 nouveau le mot de passe pour {username} .", + "title": "R\u00e9-authentifier l'int\u00e9gration" + }, "user": { "data": { "password": "Mot de passe", diff --git a/homeassistant/components/nzbget/translations/fr.json b/homeassistant/components/nzbget/translations/fr.json index 15420989501..fc043e36108 100644 --- a/homeassistant/components/nzbget/translations/fr.json +++ b/homeassistant/components/nzbget/translations/fr.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u00c9chec de connexion" }, - "flow_title": "NZBGet: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/octoprint/translations/fr.json b/homeassistant/components/octoprint/translations/fr.json new file mode 100644 index 00000000000..779335b2229 --- /dev/null +++ b/homeassistant/components/octoprint/translations/fr.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "auth_failed": "\u00c9chec de la r\u00e9cup\u00e9ration de la cl\u00e9 API de l'application", + "cannot_connect": "\u00c9chec de connexion", + "unknown": "Erreur inattendue" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "unknown": "Erreur inattendue" + }, + "flow_title": "Imprimante OctoPrint\u00a0: {host}", + "progress": { + "get_api_key": "Ouvrez l'interface utilisateur d'OctoPrint et cliquez sur \u00abAutoriser\u00bb sur la demande d'acc\u00e8s pour \u00abHome Assistant\u00bb." + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "path": "Chemin d\u2019acc\u00e8s \u00e0 l\u2019application", + "port": "Num\u00e9ro de port", + "ssl": "Utiliser SSL", + "username": "Nom d'utilisateur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opengarage/translations/fr.json b/homeassistant/components/opengarage/translations/fr.json index 909f8bd9eec..571a2c68b22 100644 --- a/homeassistant/components/opengarage/translations/fr.json +++ b/homeassistant/components/opengarage/translations/fr.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "device_key": "Cl\u00e9 de l'appareil", "host": "H\u00f4te", "port": "Port", "verify_ssl": "V\u00e9rifier le certificat SSL" diff --git a/homeassistant/components/ovo_energy/translations/fr.json b/homeassistant/components/ovo_energy/translations/fr.json index 9be6b4d3c11..2c2482d9f5f 100644 --- a/homeassistant/components/ovo_energy/translations/fr.json +++ b/homeassistant/components/ovo_energy/translations/fr.json @@ -5,7 +5,7 @@ "cannot_connect": "\u00c9chec de connexion", "invalid_auth": "Authentification invalide" }, - "flow_title": "OVO Energy: {username}", + "flow_title": "{username}", "step": { "reauth": { "data": { diff --git a/homeassistant/components/plugwise/translations/fr.json b/homeassistant/components/plugwise/translations/fr.json index ce262e72f24..5370a18ba56 100644 --- a/homeassistant/components/plugwise/translations/fr.json +++ b/homeassistant/components/plugwise/translations/fr.json @@ -8,7 +8,7 @@ "invalid_auth": "Authentification invalide", "unknown": "Erreur inattendue" }, - "flow_title": "Smile: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/powerwall/translations/fr.json b/homeassistant/components/powerwall/translations/fr.json index 61e69d3dedc..a6a6edab938 100644 --- a/homeassistant/components/powerwall/translations/fr.json +++ b/homeassistant/components/powerwall/translations/fr.json @@ -10,7 +10,7 @@ "unknown": "Erreur inattendue", "wrong_version": "Votre Powerwall utilise une version logicielle qui n'est pas prise en charge. Veuillez envisager de mettre \u00e0 niveau ou de signaler ce probl\u00e8me afin qu'il puisse \u00eatre r\u00e9solu." }, - "flow_title": "Tesla Powerwall ( {ip_address} )", + "flow_title": "{ip_address}", "step": { "user": { "data": { diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/fr.json b/homeassistant/components/pvpc_hourly_pricing/translations/fr.json index e22a70092c3..afbea0852c4 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/fr.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/fr.json @@ -9,10 +9,10 @@ "name": "Nom du capteur", "power": "Puissance souscrite (kW)", "power_p3": "Puissance souscrite pour la p\u00e9riode de vall\u00e9e P3 (kW)", - "tariff": "Tarif souscrit (1, 2, ou 3 p\u00e9riodes)" + "tariff": "Tarif applicable par zone g\u00e9ographique" }, - "description": "Ce capteur utilise l'API officielle pour obtenir la [tarification horaire de l'\u00e9lectricit\u00e9 (PVPC)] (https://www.esios.ree.es/es/pvpc) en Espagne. \n Pour une explication plus pr\u00e9cise, visitez la [documentation d'int\u00e9gration] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\n S\u00e9lectionnez le tarif contract\u00e9 en fonction du nombre de p\u00e9riodes de facturation par jour: \n - 1 p\u00e9riode: normale \n - 2 p\u00e9riodes: discrimination (tarif \u00e0 la nuit) \n - 3 p\u00e9riodes: voiture \u00e9lectrique (tarif \u00e0 la nuit sur 3 p\u00e9riodes)", - "title": "S\u00e9lection tarifaire" + "description": "Ce capteur utilise l'API officielle pour obtenir [tarification horaire de l'\u00e9lectricit\u00e9 (PVPC)](https://www.esios.ree.es/es/pvpc) en Espagne.\n Pour des explications plus pr\u00e9cises, visitez les [docs d'int\u00e9gration](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", + "title": "Configuration du capteur" } } }, diff --git a/homeassistant/components/rainmachine/translations/fr.json b/homeassistant/components/rainmachine/translations/fr.json index de4e5cdc1ed..db8bac46b59 100644 --- a/homeassistant/components/rainmachine/translations/fr.json +++ b/homeassistant/components/rainmachine/translations/fr.json @@ -6,7 +6,7 @@ "error": { "invalid_auth": "Authentification invalide" }, - "flow_title": "RainMachine {ip}", + "flow_title": "{ip}", "step": { "user": { "data": { diff --git a/homeassistant/components/rdw/translations/fr.json b/homeassistant/components/rdw/translations/fr.json index 4da885d870f..b312cd2ba0a 100644 --- a/homeassistant/components/rdw/translations/fr.json +++ b/homeassistant/components/rdw/translations/fr.json @@ -1,7 +1,15 @@ { "config": { "error": { - "cannot_connect": "\u00c9chec de connexion" + "cannot_connect": "\u00c9chec de connexion", + "unknown_license_plate": "plaque d'immatriculation inconnue" + }, + "step": { + "user": { + "data": { + "license_plate": "Plaque d'immatriculation" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/renault/translations/fr.json b/homeassistant/components/renault/translations/fr.json index 8cfc294cf4a..406339b0445 100644 --- a/homeassistant/components/renault/translations/fr.json +++ b/homeassistant/components/renault/translations/fr.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", - "kamereon_no_account": "Impossible de trouver le compte Kamereon." + "kamereon_no_account": "Impossible de trouver le compte Kamereon.", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { "invalid_credentials": "Authentification invalide" @@ -18,6 +19,7 @@ "data": { "password": "Mot de passe" }, + "description": "Veuillez mettre \u00e0 jour votre mot de passe pour {username}", "title": "R\u00e9-authentifier l'int\u00e9gration" }, "user": { diff --git a/homeassistant/components/rfxtrx/translations/fr.json b/homeassistant/components/rfxtrx/translations/fr.json index 8794b3913f1..d101cfbc57a 100644 --- a/homeassistant/components/rfxtrx/translations/fr.json +++ b/homeassistant/components/rfxtrx/translations/fr.json @@ -39,6 +39,16 @@ } } }, + "device_automation": { + "action_type": { + "send_command": "Envoyer la commande\u00a0: {subtype}", + "send_status": "Envoyer la mise \u00e0 jour du statut\u00a0: {subtype}" + }, + "trigger_type": { + "command": "Commande re\u00e7ue\u00a0: {subtype}", + "status": "Statut re\u00e7u\u00a0: {subtype}" + } + }, "one": "Vide", "options": { "error": { diff --git a/homeassistant/components/ridwell/translations/fr.json b/homeassistant/components/ridwell/translations/fr.json index 3324689e3d3..09b73beb37c 100644 --- a/homeassistant/components/ridwell/translations/fr.json +++ b/homeassistant/components/ridwell/translations/fr.json @@ -13,13 +13,15 @@ "data": { "password": "Mot de passe" }, + "description": "Veuillez saisir \u00e0 nouveau le mot de passe pour {username}\u00a0:", "title": "R\u00e9-authentifier l'int\u00e9gration" }, "user": { "data": { "password": "Mot de passe", "username": "Nom d'utilisateur" - } + }, + "description": "Saisissez votre nom d'utilisateur et votre mot de passe\u00a0:" } } } diff --git a/homeassistant/components/roku/translations/fr.json b/homeassistant/components/roku/translations/fr.json index 4888ed60a1a..64a928de567 100644 --- a/homeassistant/components/roku/translations/fr.json +++ b/homeassistant/components/roku/translations/fr.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "\u00c9chec de connexion" }, - "flow_title": "Roku: {name}", + "flow_title": "{name}", "step": { "discovery_confirm": { "data": { diff --git a/homeassistant/components/roomba/translations/fr.json b/homeassistant/components/roomba/translations/fr.json index b4c06b26c9f..5cbb1090cc0 100644 --- a/homeassistant/components/roomba/translations/fr.json +++ b/homeassistant/components/roomba/translations/fr.json @@ -9,7 +9,7 @@ "error": { "cannot_connect": "\u00c9chec de connexion" }, - "flow_title": "iRobot {name} ( {host} )", + "flow_title": "{name} ({host})", "step": { "init": { "data": { @@ -34,7 +34,7 @@ "blid": "BLID", "host": "H\u00f4te" }, - "description": "Aucun Roomba ou Braava d\u00e9couvert sur votre r\u00e9seau. Le BLID est la partie du nom d'h\u00f4te du p\u00e9riph\u00e9rique apr\u00e8s `iRobot-`. Veuillez suivre les \u00e9tapes d\u00e9crites dans la documentation \u00e0 {auth_help_url}\u00b4", + "description": "Aucun Roomba ou Braava n'a \u00e9t\u00e9 d\u00e9couvert sur votre r\u00e9seau.", "title": "Se connecter manuellement \u00e0 l'appareil" }, "user": { @@ -46,7 +46,7 @@ "password": "Mot de passe" }, "description": "La r\u00e9cup\u00e9ration du BLID et du mot de passe est actuellement un processus manuel. Veuillez suivre les \u00e9tapes d\u00e9crites dans la documentation \u00e0 l'adresse: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", - "title": "Se connecter \u00e0 l'appareil" + "title": "Se connecter automatiquement \u00e0 l'appareil" } } }, diff --git a/homeassistant/components/samsungtv/translations/fr.json b/homeassistant/components/samsungtv/translations/fr.json index 03d6eb2b3c6..1d9bebb5569 100644 --- a/homeassistant/components/samsungtv/translations/fr.json +++ b/homeassistant/components/samsungtv/translations/fr.json @@ -14,7 +14,7 @@ "error": { "auth_missing": "Home Assistant n'est pas autoris\u00e9 \u00e0 se connecter \u00e0 ce t\u00e9l\u00e9viseur Samsung. Veuillez v\u00e9rifier les param\u00e8tres de votre t\u00e9l\u00e9viseur pour autoriser Home Assistant." }, - "flow_title": "Samsung TV: {model}", + "flow_title": "{device}", "step": { "confirm": { "description": "Voulez vous installer la TV {device} Samsung? Si vous n'avez jamais connect\u00e9 Home Assistant avant, vous devriez voir une fen\u00eatre contextuelle sur votre t\u00e9l\u00e9viseur demandant une authentification. Les configurations manuelles de ce t\u00e9l\u00e9viseur seront \u00e9cras\u00e9es.", diff --git a/homeassistant/components/samsungtv/translations/sv.json b/homeassistant/components/samsungtv/translations/sv.json index 835bb5e5c9b..38b3bea8a22 100644 --- a/homeassistant/components/samsungtv/translations/sv.json +++ b/homeassistant/components/samsungtv/translations/sv.json @@ -4,9 +4,11 @@ "already_configured": "Denna Samsung TV \u00e4r redan konfigurerad.", "already_in_progress": "Samsung TV-konfiguration p\u00e5g\u00e5r redan.", "auth_missing": "Home Assistant har inte beh\u00f6righet att ansluta till denna Samsung TV. Kontrollera tv:ns inst\u00e4llningar f\u00f6r att godk\u00e4nna Home Assistant.", - "not_supported": "Denna Samsung TV-enhet st\u00f6ds f\u00f6r n\u00e4rvarande inte." + "id_missing": "Denna Samsung-enhet har inget serienummer.", + "not_supported": "Denna Samsung TV-enhet st\u00f6ds f\u00f6r n\u00e4rvarande inte.", + "unknown": "Ov\u00e4ntat fel" }, - "flow_title": "Samsung TV: {model}", + "flow_title": "{device}", "step": { "confirm": { "description": "Vill du st\u00e4lla in Samsung TV {device}? Om du aldrig har anslutit Home Assistant innan du ska se ett popup-f\u00f6nster p\u00e5 tv:n och be om auktorisering. Manuella konfigurationer f\u00f6r den h\u00e4r TV:n skrivs \u00f6ver.", diff --git a/homeassistant/components/screenlogic/translations/fr.json b/homeassistant/components/screenlogic/translations/fr.json index efd9740ac31..62f26aae2b6 100644 --- a/homeassistant/components/screenlogic/translations/fr.json +++ b/homeassistant/components/screenlogic/translations/fr.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "\u00c9chec de connexion" }, - "flow_title": "ScreenLogic {nom}", + "flow_title": "{name}", "step": { "gateway_entry": { "data": { diff --git a/homeassistant/components/sense/translations/fr.json b/homeassistant/components/sense/translations/fr.json index 83ec49fd388..bdd588eae74 100644 --- a/homeassistant/components/sense/translations/fr.json +++ b/homeassistant/components/sense/translations/fr.json @@ -12,7 +12,8 @@ "user": { "data": { "email": "Email", - "password": "Mot de passe" + "password": "Mot de passe", + "timeout": "D\u00e9lai expir\u00e9" }, "title": "Connectez-vous \u00e0 votre moniteur d'\u00e9nergie Sense" } diff --git a/homeassistant/components/sensor/translations/fr.json b/homeassistant/components/sensor/translations/fr.json index d25cb4766cb..eb9bba61311 100644 --- a/homeassistant/components/sensor/translations/fr.json +++ b/homeassistant/components/sensor/translations/fr.json @@ -6,12 +6,22 @@ "is_carbon_monoxide": "Niveau actuel de concentration de monoxyde de carbone {entity_name}", "is_current": "Courant actuel pour {entity_name}", "is_energy": "\u00c9nergie actuelle pour {entity_name}", + "is_frequency": "Fr\u00e9quence actuelle de {entity_name}", + "is_gas": "Gaz actuel de {entity_name}", "is_humidity": "Humidit\u00e9 de {entity_name}", "is_illuminance": "\u00c9clairement de {entity_name}", + "is_nitrogen_dioxide": "Niveau actuel de concentration en dioxyde d'azote de {entity_name}", + "is_nitrogen_monoxide": "Niveau actuel de concentration en monoxyde d'azote de {entity_name}", + "is_nitrous_oxide": "Niveau actuel de concentration d'oxyde nitreux de {entity_name}", + "is_ozone": "Niveau de concentration d'ozone actuel de {entity_name}", + "is_pm1": "Niveau de concentration actuel de {entity_name}", + "is_pm10": "Niveau de concentration actuel de {entity_name}", + "is_pm25": "Niveau de concentration actuel de {entity_name}", "is_power": "Puissance de {entity_name}", "is_power_factor": "Facteur de puissance actuel pour {entity_name}", "is_pressure": "Pression de {entity_name}", "is_signal_strength": "Force du signal de {entity_name}", + "is_sulphur_dioxide": "Niveau de concentration actuel de {entity_name}", "is_temperature": "Temp\u00e9rature de {entity_name}", "is_value": "La valeur actuelle de {entity_name}", "is_volatile_organic_compounds": "Niveau actuel de concentration en compos\u00e9s organiques volatils de {entity_name}", @@ -23,12 +33,22 @@ "carbon_monoxide": "{entity_name} changements de concentration de monoxyde de carbone", "current": "{entity_name} changement de courant", "energy": "{entity_name} changement d'\u00e9nergie", + "frequency": "{entity_name} changements de fr\u00e9quence", + "gas": "{entity_name} changements de gaz", "humidity": "{entity_name} modification de l'humidit\u00e9", "illuminance": "{entity_name} modification de l'\u00e9clairement", + "nitrogen_dioxide": "Modifications de la concentration de dioxyde d'azote de {entity_name}", + "nitrogen_monoxide": "Modifications de la concentration de monoxyde d'azote de {entity_name}", + "nitrous_oxide": "{entity_name} concentration d'oxyde nitreux de {entity_name}", + "ozone": "{entity_name} changements de concentration d'ozone", + "pm1": "{entity_name} Modifications de la concentration de PM1", + "pm10": "{entity_name} Modifications de la concentration de PM10", + "pm25": "{entity_name} Modifications de la concentration de PM2,5", "power": "{entity_name} modification de la puissance", "power_factor": "{entity_name} changement de facteur de puissance", "pressure": "{entity_name} modification de la pression", "signal_strength": "{entity_name} modification de la force du signal", + "sulphur_dioxide": "{entity_name} changements de concentration de dioxyde de soufre", "temperature": "{entity_name} modification de temp\u00e9rature", "value": "Changements de valeur de {entity_name}", "volatile_organic_compounds": "{entity_name} changements de concentration de compos\u00e9s organiques volatils", diff --git a/homeassistant/components/shelly/translations/fr.json b/homeassistant/components/shelly/translations/fr.json index 68dc5de667a..83b66645157 100644 --- a/homeassistant/components/shelly/translations/fr.json +++ b/homeassistant/components/shelly/translations/fr.json @@ -37,7 +37,10 @@ "button4": "Quatri\u00e8me bouton" }, "trigger_type": { + "btn_down": "{sous-type} bouton en bas", + "btn_up": "{sous-type} bouton haut", "double": "{subtype} double-cliqu\u00e9", + "double_push": "{subtype} double pression", "long": "{subtype} long cliqu\u00e9", "long_push": "{subtype} appui long", "long_single": "{subtype} clic long et simple clic", diff --git a/homeassistant/components/simplisafe/translations/fr.json b/homeassistant/components/simplisafe/translations/fr.json index b1ea8441369..d0ff1347bbd 100644 --- a/homeassistant/components/simplisafe/translations/fr.json +++ b/homeassistant/components/simplisafe/translations/fr.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Ce compte SimpliSafe est d\u00e9j\u00e0 utilis\u00e9.", - "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", + "wrong_account": "Les informations d'identification d'utilisateur fournies ne correspondent pas \u00e0 ce compte SimpliSafe." }, "error": { "identifier_exists": "Compte d\u00e9j\u00e0 enregistr\u00e9", @@ -11,6 +12,13 @@ "unknown": "Erreur inattendue" }, "step": { + "input_auth_code": { + "data": { + "auth_code": "Code d'autorisation" + }, + "description": "Saisissez le code d'autorisation \u00e0 partir de l'URL de l'application Web SimpliSafe\u00a0:", + "title": "Terminer l'autorisation" + }, "mfa": { "description": "V\u00e9rifiez votre messagerie pour un lien de SimpliSafe. Apr\u00e8s avoir v\u00e9rifi\u00e9 le lien, revenez ici pour terminer l'installation de l'int\u00e9gration.", "title": "Authentification multi facteur SimpliSafe" @@ -24,10 +32,12 @@ }, "user": { "data": { + "auth_code": "Code d'autorisation", "code": "Code (utilis\u00e9 dans l'interface Home Assistant)", "password": "Mot de passe", "username": "Email" }, + "description": "SimpliSafe s'authentifie avec Home Assistant via l'application Web SimpliSafe. En raison de limitations techniques, il y a une \u00e9tape manuelle \u00e0 la fin de ce processus ; veuillez vous assurer de lire la [documentation]( {docs_url} ) avant de commencer. \n\n 1. Cliquez sur [ici]( {url} ) pour ouvrir l'application Web SimpliSafe et saisissez vos informations d'identification. \n\n 2. Une fois le processus de connexion termin\u00e9, revenez ici et saisissez le code d'autorisation ci-dessous.", "title": "Veuillez saisir vos informations" } } diff --git a/homeassistant/components/simplisafe/translations/id.json b/homeassistant/components/simplisafe/translations/id.json index 733af744e30..743af691714 100644 --- a/homeassistant/components/simplisafe/translations/id.json +++ b/homeassistant/components/simplisafe/translations/id.json @@ -32,11 +32,12 @@ }, "user": { "data": { + "auth_code": "Kode Otorisasi", "code": "Kode (digunakan di antarmuka Home Assistant)", "password": "Kata Sandi", "username": "Email" }, - "description": "Sejak tahun 2021, SimpliSafe telah berpindah ke mekanisme autentikasi baru melalui aplikasi webnya. Karena keterbatasan teknis, ada langkah manual di akhir proses ini; pastikan Anda membaca [dokumentasi] (http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) sebelum memulai.\n\nJika siap, klik [di sini]({url}) untuk membuka aplikasi web SimpliSafe dan masukkan kredensial Anda. Setelah proses selesai, kembali ke sini dan klik Kirim.", + "description": "SimpliSafe mengautentikasi dengan Home Assistant melalui aplikasi web. Karena keterbatasan teknis, ada langkah manual di akhir proses ini; pastikan Anda membaca [dokumentasi] ({docs_url}) sebelum memulai.\n\n1. Klik [di sini]({url}) untuk membuka aplikasi web SimpliSafe dan masukkan kredensial Anda. \n\n2. Setelah proses masuk selesai, kembali ke sini dan klik masukkan kode otorisasi di bawah ini.", "title": "Isi informasi Anda." } } diff --git a/homeassistant/components/simplisafe/translations/zh-Hant.json b/homeassistant/components/simplisafe/translations/zh-Hant.json index d967899b1dd..ae22f8c8f0d 100644 --- a/homeassistant/components/simplisafe/translations/zh-Hant.json +++ b/homeassistant/components/simplisafe/translations/zh-Hant.json @@ -37,7 +37,7 @@ "password": "\u5bc6\u78bc", "username": "\u96fb\u5b50\u90f5\u4ef6" }, - "description": "SimpliSafe \u81ea 2021 \u958b\u59cb\u8b8a\u66f4\u70ba\u900f\u904e Web App \u65b9\u5f0f\u7684\u8a8d\u8b49\u6a5f\u5236\u3002\u7531\u65bc\u6280\u8853\u9650\u5236\u3001\u65bc\u6b64\u904e\u7a0b\u7d50\u675f\u6642\u5c07\u6703\u6709\u4e00\u6b65\u624b\u52d5\u968e\u6bb5\uff1b\u65bc\u958b\u59cb\u524d\u3001\u8acb\u78ba\u5b9a\u53c3\u95b1 [\u76f8\u95dc\u6587\u4ef6](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code)\u3002\n\n\u6e96\u5099\u5c31\u7dd2\u5f8c\u3001\u9ede\u9078 [\u6b64\u8655]({url}) \u4ee5\u958b\u555f SimpliSafe Web App \u4e26\u8f38\u5165\u9a57\u8b49\u3002\u5b8c\u6210\u5f8c\u56de\u5230\u9019\u88e1\u9ede\u9078\u50b3\u9001\u3002", + "description": "SimpliSafe \u70ba\u900f\u904e Web App \u65b9\u5f0f\u7684\u8a8d\u8b49\u6a5f\u5236\u3002\u7531\u65bc\u6280\u8853\u9650\u5236\u3001\u65bc\u6b64\u904e\u7a0b\u7d50\u675f\u6642\u5c07\u6703\u6709\u4e00\u6b65\u624b\u52d5\u968e\u6bb5\uff1b\u65bc\u958b\u59cb\u524d\u3001\u8acb\u78ba\u5b9a\u53c3\u95b1 [\u76f8\u95dc\u6587\u4ef6]({docs_url})\u3002\n\n1. \u9ede\u9078 [\u6b64\u8655] ({url}) \u4ee5\u958b\u555f SimpliSafe Web App \u4e26\u8f38\u5165\u9a57\u8b49\u3002\n\n2. \u7576\u767b\u5165\u5b8c\u6210\u5f8c\u3001\u56de\u5230\u6b64\u8655\u65bc\u4e0b\u65b9\u8f38\u5165\u8a8d\u8b49\u78bc\u3002", "title": "\u586b\u5beb\u8cc7\u8a0a\u3002" } } diff --git a/homeassistant/components/smappee/translations/fr.json b/homeassistant/components/smappee/translations/fr.json index 4bcdd0b5c16..ba57f8c36c4 100644 --- a/homeassistant/components/smappee/translations/fr.json +++ b/homeassistant/components/smappee/translations/fr.json @@ -9,7 +9,7 @@ "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )" }, - "flow_title": "Smappee: {name}", + "flow_title": "{name}", "step": { "environment": { "data": { diff --git a/homeassistant/components/soma/translations/fr.json b/homeassistant/components/soma/translations/fr.json index 8531f839df0..c9c9bb07643 100644 --- a/homeassistant/components/soma/translations/fr.json +++ b/homeassistant/components/soma/translations/fr.json @@ -2,13 +2,13 @@ "config": { "abort": { "already_setup": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", - "authorize_url_timeout": "D\u00e9lai d'attente g\u00e9n\u00e9rant l'autorisation de l'URL.", - "connection_error": "Impossible de se connecter \u00e0 SOMA Connect.", + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", + "connection_error": "\u00c9chec de connexion", "missing_configuration": "Le composant Soma n'est pas configur\u00e9. Veuillez suivre la documentation.", "result_error": "SOMA Connect a r\u00e9pondu avec l'\u00e9tat d'erreur." }, "create_entry": { - "default": "Authentifi\u00e9 avec succ\u00e8s avec Soma." + "default": "Authentification r\u00e9ussie" }, "step": { "user": { diff --git a/homeassistant/components/somfy_mylink/translations/fr.json b/homeassistant/components/somfy_mylink/translations/fr.json index f31a3b9d86f..46781252523 100644 --- a/homeassistant/components/somfy_mylink/translations/fr.json +++ b/homeassistant/components/somfy_mylink/translations/fr.json @@ -8,7 +8,7 @@ "invalid_auth": "Authentification invalide", "unknown": "Erreur inattendue" }, - "flow_title": "Somfy MyLink {mac} ( {ip} )", + "flow_title": "{mac} ({ip})", "step": { "user": { "data": { diff --git a/homeassistant/components/sonarr/translations/fr.json b/homeassistant/components/sonarr/translations/fr.json index 154d3435da3..c4a7a8764ac 100644 --- a/homeassistant/components/sonarr/translations/fr.json +++ b/homeassistant/components/sonarr/translations/fr.json @@ -9,7 +9,7 @@ "cannot_connect": "\u00c9chec de connexion", "invalid_auth": "Authentification invalide" }, - "flow_title": "Sonarr: {name}", + "flow_title": "{name}", "step": { "reauth_confirm": { "description": "L'int\u00e9gration Sonarr doit \u00eatre r\u00e9-authentifi\u00e9e manuellement avec l'API Sonarr h\u00e9berg\u00e9e sur: {host}", diff --git a/homeassistant/components/songpal/translations/fr.json b/homeassistant/components/songpal/translations/fr.json index 84f115ec1a5..cbc1f83b94c 100644 --- a/homeassistant/components/songpal/translations/fr.json +++ b/homeassistant/components/songpal/translations/fr.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u00c9chec de connexion" }, - "flow_title": "Sony Songpal {name} ({host})", + "flow_title": "{name} ({host})", "step": { "init": { "description": "Voulez-vous configurer {name} ({host})?" diff --git a/homeassistant/components/squeezebox/translations/fr.json b/homeassistant/components/squeezebox/translations/fr.json index c416a18b49d..1dca7a14308 100644 --- a/homeassistant/components/squeezebox/translations/fr.json +++ b/homeassistant/components/squeezebox/translations/fr.json @@ -10,7 +10,7 @@ "no_server_found": "Impossible de d\u00e9couvrir automatiquement le serveur.", "unknown": "Erreur inattendue" }, - "flow_title": "Logitech Squeezebox: {host}", + "flow_title": "{host}", "step": { "edit": { "data": { diff --git a/homeassistant/components/stookalert/translations/fr.json b/homeassistant/components/stookalert/translations/fr.json index 9c74e1b5026..dce990944f6 100644 --- a/homeassistant/components/stookalert/translations/fr.json +++ b/homeassistant/components/stookalert/translations/fr.json @@ -2,6 +2,13 @@ "config": { "abort": { "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9" + }, + "step": { + "user": { + "data": { + "province": "Province" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/surepetcare/translations/fr.json b/homeassistant/components/surepetcare/translations/fr.json new file mode 100644 index 00000000000..b6704aabae1 --- /dev/null +++ b/homeassistant/components/surepetcare/translations/fr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/fr.json b/homeassistant/components/switchbot/translations/fr.json index d155457d4b6..c554326c5da 100644 --- a/homeassistant/components/switchbot/translations/fr.json +++ b/homeassistant/components/switchbot/translations/fr.json @@ -1,7 +1,37 @@ { "config": { "abort": { + "already_configured_device": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "\u00c9chec de connexion", + "no_unconfigured_devices": "Aucun p\u00e9riph\u00e9rique non configur\u00e9 trouv\u00e9.", + "switchbot_unsupported_type": "Type Switchbot non pris en charge.", + "unknown": "Erreur inattendue" + }, + "error": { "cannot_connect": "\u00c9chec de connexion" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "mac": "Adresse MAC de l'appareil", + "name": "Nom", + "password": "Mot de passe" + }, + "title": "Configurer l'appareil Switchbot" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "retry_count": "Nombre de nouvelles tentatives", + "retry_timeout": "D\u00e9lai d'attente entre les tentatives", + "scan_timeout": "Combien de temps pour rechercher les donn\u00e9es publicitaires", + "update_time": "Temps entre les mises \u00e0 jour (secondes)" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/syncthru/translations/fr.json b/homeassistant/components/syncthru/translations/fr.json index 2e1d73688e0..1936e2f4a16 100644 --- a/homeassistant/components/syncthru/translations/fr.json +++ b/homeassistant/components/syncthru/translations/fr.json @@ -8,7 +8,7 @@ "syncthru_not_supported": "L'appareil ne prend pas en charge SyncThru", "unknown_state": "\u00c9tat de l'imprimante inconnu, v\u00e9rifiez l'URL et la connectivit\u00e9 r\u00e9seau" }, - "flow_title": "Imprimante Samsung SyncThru: {nom}", + "flow_title": "{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/synology_dsm/translations/fr.json b/homeassistant/components/synology_dsm/translations/fr.json index 2589139b9ec..503db024603 100644 --- a/homeassistant/components/synology_dsm/translations/fr.json +++ b/homeassistant/components/synology_dsm/translations/fr.json @@ -12,7 +12,7 @@ "otp_failed": "\u00c9chec de l'authentification en deux \u00e9tapes, r\u00e9essayez avec un nouveau code d'acc\u00e8s", "unknown": "Erreur inattendue" }, - "flow_title": "Synology DSM {name} ({host})", + "flow_title": "{name} ({host})", "step": { "2sa": { "data": { @@ -43,7 +43,8 @@ "data": { "password": "Mot de passe", "username": "Nom d'utilisateur" - } + }, + "title": "Synology DSM R\u00e9-authentifier l'int\u00e9gration" }, "user": { "data": { diff --git a/homeassistant/components/system_bridge/translations/fr.json b/homeassistant/components/system_bridge/translations/fr.json index 99b38c44ad8..1c2a1b23c9c 100644 --- a/homeassistant/components/system_bridge/translations/fr.json +++ b/homeassistant/components/system_bridge/translations/fr.json @@ -10,7 +10,7 @@ "invalid_auth": "Authentification invalide", "unknown": "Erreur inattendue" }, - "flow_title": "Pont syst\u00e8me: {name}", + "flow_title": "{name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/tailscale/translations/fr.json b/homeassistant/components/tailscale/translations/fr.json new file mode 100644 index 00000000000..eb307c84de9 --- /dev/null +++ b/homeassistant/components/tailscale/translations/fr.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Cl\u00e9 d'API" + }, + "description": "Les jetons d'API Tailscale sont valides pendant 90 jours. Vous pouvez cr\u00e9er une nouvelle cl\u00e9 API Tailscale \u00e0 l'adresse https://login.tailscale.com/admin/settings/authkeys." + }, + "user": { + "data": { + "api_key": "Cl\u00e9 d'API", + "tailnet": "Tailnet" + }, + "description": "Pour vous authentifier avec Tailscale, vous devrez cr\u00e9er une cl\u00e9 API sur https://login.tailscale.com/admin/settings/authkeys. \n\n Un Tailnet est le nom de votre r\u00e9seau Tailscale. Vous pouvez le trouver dans le coin sup\u00e9rieur gauche du panneau d'administration Tailscale (\u00e0 c\u00f4t\u00e9 du logo Tailscale)." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla_wall_connector/translations/fr.json b/homeassistant/components/tesla_wall_connector/translations/fr.json new file mode 100644 index 00000000000..6cf4aacf7d3 --- /dev/null +++ b/homeassistant/components/tesla_wall_connector/translations/fr.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "unknown": "Erreur inattendue" + }, + "flow_title": "{serial_number} ( {host} )", + "step": { + "user": { + "data": { + "host": "H\u00f4te" + }, + "title": "Configurer le connecteur mural Tesla" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Fr\u00e9quence de mise \u00e0 jour" + }, + "title": "Configurer les options pour le connecteur mural Tesla" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/fr.json b/homeassistant/components/tolo/translations/fr.json new file mode 100644 index 00000000000..95951419dc4 --- /dev/null +++ b/homeassistant/components/tolo/translations/fr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Voulez-vous commencer la configuration ?" + }, + "user": { + "data": { + "host": "H\u00f4te" + }, + "description": "Saisissez le nom d'h\u00f4te ou l'adresse IP de votre appareil TOLO Sauna." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/select.fr.json b/homeassistant/components/tolo/translations/select.fr.json new file mode 100644 index 00000000000..24b1f41f755 --- /dev/null +++ b/homeassistant/components/tolo/translations/select.fr.json @@ -0,0 +1,8 @@ +{ + "state": { + "tolo__lamp_mode": { + "automatic": "automatique", + "manual": "Manuel" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/fr.json b/homeassistant/components/totalconnect/translations/fr.json index 6c51c724f77..a538750f455 100644 --- a/homeassistant/components/totalconnect/translations/fr.json +++ b/homeassistant/components/totalconnect/translations/fr.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "no_locations": "Aucun emplacement n'est disponible pour cet utilisateur, v\u00e9rifiez les param\u00e8tres de TotalConnect", "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { @@ -14,7 +15,7 @@ "location": "Emplacement", "usercode": "Code d'utilisateur" }, - "description": "Saisissez le code d'utilisateur de cet utilisateur \u00e0 cet emplacement", + "description": "Entrez le code utilisateur pour cet utilisateur \u00e0 l'emplacement {location_id}", "title": "Codes d'utilisateur de l'emplacement" }, "reauth_confirm": { diff --git a/homeassistant/components/tplink/translations/fr.json b/homeassistant/components/tplink/translations/fr.json index 7d1efcafeb8..0607c01c14d 100644 --- a/homeassistant/components/tplink/translations/fr.json +++ b/homeassistant/components/tplink/translations/fr.json @@ -8,14 +8,24 @@ "error": { "cannot_connect": "\u00c9chec de connexion" }, + "flow_title": "{name} {model} ( {host} )", "step": { "confirm": { "description": "Voulez-vous configurer TP-Link smart devices?" }, + "discovery_confirm": { + "description": "Voulez-vous configurer {name} {model} ( {host} )\u00a0?" + }, + "pick_device": { + "data": { + "device": "Appareil" + } + }, "user": { "data": { "host": "H\u00f4te" - } + }, + "description": "Si vous laissez l'h\u00f4te vide, la d\u00e9couverte sera utilis\u00e9e pour trouver des p\u00e9riph\u00e9riques." } } } diff --git a/homeassistant/components/tractive/translations/sensor.fr.json b/homeassistant/components/tractive/translations/sensor.fr.json new file mode 100644 index 00000000000..20a6e9b7ad4 --- /dev/null +++ b/homeassistant/components/tractive/translations/sensor.fr.json @@ -0,0 +1,10 @@ +{ + "state": { + "tractive__tracker_state": { + "not_reporting": "Ne pas signaler", + "operational": "Op\u00e9rationnel", + "system_shutdown_user": "Arr\u00eater l'h\u00f4te", + "system_startup": "D\u00e9marrage du syst\u00e8me" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/translations/fr.json b/homeassistant/components/tradfri/translations/fr.json index 2d32029c954..94796c720df 100644 --- a/homeassistant/components/tradfri/translations/fr.json +++ b/homeassistant/components/tradfri/translations/fr.json @@ -5,6 +5,7 @@ "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours" }, "error": { + "cannot_authenticate": "Impossible de s'authentifier, Gateway est-il associ\u00e9 \u00e0 un autre serveur comme par exemple Homekit\u00a0?", "cannot_connect": "\u00c9chec de connexion", "invalid_key": "\u00c9chec de l'enregistrement avec la cl\u00e9 fournie. Si cela se reproduit, essayez de red\u00e9marrer la passerelle.", "timeout": "D\u00e9lai d'attente de la validation du code expir\u00e9" diff --git a/homeassistant/components/trafikverket_weatherstation/translations/fr.json b/homeassistant/components/trafikverket_weatherstation/translations/fr.json new file mode 100644 index 00000000000..d988b3aba97 --- /dev/null +++ b/homeassistant/components/trafikverket_weatherstation/translations/fr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide", + "invalid_station": "Impossible de trouver une station m\u00e9t\u00e9o avec le nom sp\u00e9cifi\u00e9", + "more_stations": "Trouv\u00e9 plusieurs stations m\u00e9t\u00e9o avec le nom sp\u00e9cifi\u00e9" + }, + "step": { + "user": { + "data": { + "api_key": "Cl\u00e9 d'API", + "conditions": "Conditions surveill\u00e9es", + "name": "Nom d'utilisateur", + "station": "Station" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/fr.json b/homeassistant/components/tuya/translations/fr.json index f74ed38c18c..0f8dd7b6742 100644 --- a/homeassistant/components/tuya/translations/fr.json +++ b/homeassistant/components/tuya/translations/fr.json @@ -6,12 +6,15 @@ "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, "error": { - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification invalide", + "login_error": "Erreur de connexion ( {code} ): {msg}" }, "flow_title": "Configuration Tuya", "step": { "login": { "data": { + "access_id": "Identifiant d'acc\u00e8s", + "access_secret": "Access Secret", "country_code": "Code pays", "endpoint": "Zone de disponibilit\u00e9", "password": "Mot de passe", @@ -23,14 +26,17 @@ }, "user": { "data": { - "country_code": "Le code de pays de votre compte (par exemple, 1 pour les \u00c9tats-Unis ou 86 pour la Chine)", + "access_id": "Identifiant d'acc\u00e8s Tuya IoT", + "access_secret": "Tuya IoT Access Secret", + "country_code": "Pays", "password": "Mot de passe", "platform": "L'application dans laquelle votre compte est enregistr\u00e9", "region": "R\u00e9gion", - "username": "Nom d'utilisateur" + "tuya_project_type": "Type de projet cloud Tuya", + "username": "Compte" }, "description": "Saisissez vos informations d'identification Tuya.", - "title": "Tuya" + "title": "Int\u00e9gration de Tuya" } } }, diff --git a/homeassistant/components/tuya/translations/select.fr.json b/homeassistant/components/tuya/translations/select.fr.json index 6ca8d863030..3591eb2b864 100644 --- a/homeassistant/components/tuya/translations/select.fr.json +++ b/homeassistant/components/tuya/translations/select.fr.json @@ -1,13 +1,49 @@ { "state": { + "tuya__basic_anti_flickr": { + "0": "D\u00e9sactiv\u00e9", + "1": "50 Hz", + "2": "60 Hz" + }, "tuya__basic_nightvision": { + "0": "automatique", "1": "Inactif", "2": "Actif" }, + "tuya__decibel_sensitivity": { + "0": "Faible sensibilit\u00e9", + "1": "Haute sensibilit\u00e9" + }, + "tuya__fingerbot_mode": { + "click": "Appuyer", + "switch": "Interrupteur" + }, + "tuya__ipc_work_mode": { + "0": "Mode faible consommation", + "1": "Mode de travail continu" + }, + "tuya__led_type": { + "halogen": "Halog\u00e8ne", + "incandescent": "Incandescent", + "led": "LED" + }, "tuya__light_mode": { - "none": "Inactif" + "none": "Inactif", + "pos": "Indiquer l'emplacement du commutateur", + "relay": "Indiquer l\u2019\u00e9tat marche/arr\u00eat de l\u2019interrupteur" + }, + "tuya__motion_sensitivity": { + "0": "Faible sensibilit\u00e9", + "1": "Sensibilit\u00e9 moyenne", + "2": "Haute sensibilit\u00e9" + }, + "tuya__record_mode": { + "1": "Enregistrer uniquement les \u00e9v\u00e9nements", + "2": "Enregistrement continu" }, "tuya__relay_status": { + "last": "Se souvenir du dernier \u00e9tat", + "memory": "Se souvenir du dernier \u00e9tat", "off": "Inactif", "on": "Actif", "power_off": "Inactif", diff --git a/homeassistant/components/tuya/translations/sensor.fr.json b/homeassistant/components/tuya/translations/sensor.fr.json new file mode 100644 index 00000000000..795e4c5d43e --- /dev/null +++ b/homeassistant/components/tuya/translations/sensor.fr.json @@ -0,0 +1,15 @@ +{ + "state": { + "tuya__status": { + "boiling_temp": "Temp\u00e9rature de chauffage", + "cooling": "Refroidissement", + "heating": "En chauffe", + "heating_temp": "Temp\u00e9rature de chauffage", + "reserve_1": "R\u00e9serve 1", + "reserve_2": "R\u00e9serve 2", + "reserve_3": "R\u00e9serve 3", + "standby": "En veille", + "warm": "Pr\u00e9servation de la chaleur" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/translations/fr.json b/homeassistant/components/unifi/translations/fr.json index 5486b633fd4..36c9ee5bcc6 100644 --- a/homeassistant/components/unifi/translations/fr.json +++ b/homeassistant/components/unifi/translations/fr.json @@ -10,7 +10,7 @@ "service_unavailable": "\u00c9chec de connexion", "unknown_client_mac": "Aucun client disponible sur cette adresse MAC" }, - "flow_title": "UniFi Network {site} ( {host} )", + "flow_title": "{site} ({host})", "step": { "user": { "data": { @@ -34,7 +34,7 @@ "poe_clients": "Autoriser le contr\u00f4le POE des clients" }, "description": "Configurer les contr\u00f4les client \n\n Cr\u00e9ez des interrupteurs pour les num\u00e9ros de s\u00e9rie pour lesquels vous souhaitez contr\u00f4ler l'acc\u00e8s au r\u00e9seau.", - "title": "Options UniFi 2/3" + "title": "Options de r\u00e9seau UniFi 2/3" }, "device_tracker": { "data": { @@ -46,7 +46,7 @@ "track_wired_clients": "Inclure les clients du r\u00e9seau filaire" }, "description": "Configurer le suivi des appareils", - "title": "Options UniFi 1/3" + "title": "Options de r\u00e9seau UniFi 1/3" }, "init": { "data": { @@ -68,7 +68,7 @@ "allow_uptime_sensors": "Capteurs de disponibilit\u00e9 pour les clients r\u00e9seau" }, "description": "Configurer des capteurs de statistiques", - "title": "Options UniFi 3/3" + "title": "Options de r\u00e9seau UniFi 3/3" } } } diff --git a/homeassistant/components/upnp/translations/fr.json b/homeassistant/components/upnp/translations/fr.json index 574cb9f4f93..1c5a1c61309 100644 --- a/homeassistant/components/upnp/translations/fr.json +++ b/homeassistant/components/upnp/translations/fr.json @@ -9,7 +9,7 @@ "one": "Vide", "other": "Vide" }, - "flow_title": "UPnP/IGD: {name}", + "flow_title": "{name}", "step": { "init": { "one": "Vide", diff --git a/homeassistant/components/venstar/translations/fr.json b/homeassistant/components/venstar/translations/fr.json index 2be362d5603..3b3a2e23b15 100644 --- a/homeassistant/components/venstar/translations/fr.json +++ b/homeassistant/components/venstar/translations/fr.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, "error": { - "cannot_connect": "\u00c9chec de connexion" + "cannot_connect": "\u00c9chec de connexion", + "unknown": "Erreur inattendue" }, "step": { "user": { @@ -11,7 +15,8 @@ "pin": "Code PIN", "ssl": "Utilise un certificat SSL", "username": "Nom d'utilisateur" - } + }, + "title": "Connectez-vous au thermostat Venstar" } } } diff --git a/homeassistant/components/vlc_telnet/translations/fr.json b/homeassistant/components/vlc_telnet/translations/fr.json index 9ed67c043de..7dd5fabfdca 100644 --- a/homeassistant/components/vlc_telnet/translations/fr.json +++ b/homeassistant/components/vlc_telnet/translations/fr.json @@ -7,14 +7,26 @@ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", "unknown": "Erreur inattendue" }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue" + }, + "flow_title": "{host}", "step": { + "hassio_confirm": { + "description": "Voulez-vous vous connecter \u00e0 l'add-on {addon}\u00a0?" + }, "reauth_confirm": { "data": { "password": "Mot de passe" - } + }, + "description": "Veuillez saisir le mot de passe correct pour l'h\u00f4te\u00a0: {host}" }, "user": { "data": { + "host": "H\u00f4te", + "name": "Nom", "password": "Mot de passe", "port": "Port" } diff --git a/homeassistant/components/wallbox/translations/fr.json b/homeassistant/components/wallbox/translations/fr.json index 4b2eddb6ef9..dc7972fd01d 100644 --- a/homeassistant/components/wallbox/translations/fr.json +++ b/homeassistant/components/wallbox/translations/fr.json @@ -7,6 +7,7 @@ "error": { "cannot_connect": "\u00c9chec de connexion", "invalid_auth": "Authentification invalide", + "reauth_invalid": "\u00c9chec de la r\u00e9authentification\u00a0; Le num\u00e9ro de s\u00e9rie ne correspond pas \u00e0 l'original", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/watttime/translations/fr.json b/homeassistant/components/watttime/translations/fr.json index fb916a3f333..91a6c126817 100644 --- a/homeassistant/components/watttime/translations/fr.json +++ b/homeassistant/components/watttime/translations/fr.json @@ -6,24 +6,46 @@ }, "error": { "invalid_auth": "Authentification invalide", - "unknown": "Erreur inattendue" + "unknown": "Erreur inattendue", + "unknown_coordinates": "Aucune donn\u00e9e pour la latitude/longitude" }, "step": { "coordinates": { "data": { + "latitude": "Latitude", "longitude": "Longitude" - } + }, + "description": "Saisissez la latitude et la longitude \u00e0 surveiller\u00a0:" }, "location": { "data": { "location_type": "Emplacement" - } + }, + "description": "Choisissez un emplacement \u00e0 surveiller\u00a0:" }, "reauth_confirm": { "data": { "password": "Mot de passe" }, + "description": "Veuillez saisir \u00e0 nouveau le mot de passe pour {username}\u00a0:", "title": "R\u00e9-authentifier l'int\u00e9gration" + }, + "user": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + }, + "description": "Saisissez votre nom d'utilisateur et votre mot de passe\u00a0:" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Afficher l'emplacement surveill\u00e9 sur la carte" + }, + "title": "Configurer WattTime" } } } diff --git a/homeassistant/components/whirlpool/translations/fr.json b/homeassistant/components/whirlpool/translations/fr.json index 0cfccfa88ad..63e63fd1953 100644 --- a/homeassistant/components/whirlpool/translations/fr.json +++ b/homeassistant/components/whirlpool/translations/fr.json @@ -1,8 +1,14 @@ { "config": { + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue" + }, "step": { "user": { "data": { + "password": "Mot de passe", "username": "Nom d'utilisateur" } } diff --git a/homeassistant/components/wilight/translations/fr.json b/homeassistant/components/wilight/translations/fr.json index 5d54070b401..0fb00748e25 100644 --- a/homeassistant/components/wilight/translations/fr.json +++ b/homeassistant/components/wilight/translations/fr.json @@ -5,7 +5,7 @@ "not_supported_device": "Ce WiLight n'est actuellement pas pris en charge", "not_wilight_device": "Cet appareil n'est pas WiLight" }, - "flow_title": "WiLight: {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "Voulez-vous configurer WiLight {name} ? \n\n Il prend en charge: {components}", diff --git a/homeassistant/components/withings/translations/fr.json b/homeassistant/components/withings/translations/fr.json index 9f675026022..a506d491a74 100644 --- a/homeassistant/components/withings/translations/fr.json +++ b/homeassistant/components/withings/translations/fr.json @@ -12,7 +12,7 @@ "error": { "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" }, - "flow_title": "Withings: {profile}", + "flow_title": "{profile}", "step": { "pick_implementation": { "title": "S\u00e9lectionner une m\u00e9thode d'authentification" diff --git a/homeassistant/components/wled/translations/fr.json b/homeassistant/components/wled/translations/fr.json index 03255541ad9..50817265f37 100644 --- a/homeassistant/components/wled/translations/fr.json +++ b/homeassistant/components/wled/translations/fr.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u00c9chec de connexion" }, - "flow_title": "WLED: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/wled/translations/select.fr.json b/homeassistant/components/wled/translations/select.fr.json new file mode 100644 index 00000000000..47a1a07989b --- /dev/null +++ b/homeassistant/components/wled/translations/select.fr.json @@ -0,0 +1,9 @@ +{ + "state": { + "wled__live_override": { + "0": "Inactif", + "1": "Actif", + "2": "Jusqu'au red\u00e9marrage de l'appareil" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_aqara/translations/fr.json b/homeassistant/components/xiaomi_aqara/translations/fr.json index f6ca4af9d3e..31779ba80b5 100644 --- a/homeassistant/components/xiaomi_aqara/translations/fr.json +++ b/homeassistant/components/xiaomi_aqara/translations/fr.json @@ -12,7 +12,7 @@ "invalid_key": "Cl\u00e9 de passerelle non valide", "invalid_mac": "Adresse MAC non valide" }, - "flow_title": "Passerelle Xiaomi Aqara: {nom}", + "flow_title": "{name}", "step": { "select": { "data": { diff --git a/homeassistant/components/xiaomi_miio/translations/fr.json b/homeassistant/components/xiaomi_miio/translations/fr.json index 65e7f90d253..2a9d8d65c77 100644 --- a/homeassistant/components/xiaomi_miio/translations/fr.json +++ b/homeassistant/components/xiaomi_miio/translations/fr.json @@ -16,7 +16,7 @@ "unknown_device": "Le mod\u00e8le d'appareil n'est pas connu, impossible de configurer l'appareil \u00e0 l'aide du flux de configuration.", "wrong_token": "Erreur de somme de contr\u00f4le, jeton incorrect" }, - "flow_title": "Xiaomi Miio: {name}", + "flow_title": "{name}", "step": { "cloud": { "data": { diff --git a/homeassistant/components/yale_smart_alarm/translations/fr.json b/homeassistant/components/yale_smart_alarm/translations/fr.json index c2cf20086e2..6d4d968896d 100644 --- a/homeassistant/components/yale_smart_alarm/translations/fr.json +++ b/homeassistant/components/yale_smart_alarm/translations/fr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { "invalid_auth": "Authentification invalide" diff --git a/homeassistant/components/yamaha_musiccast/translations/select.fr.json b/homeassistant/components/yamaha_musiccast/translations/select.fr.json new file mode 100644 index 00000000000..f8ce2da31a7 --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/select.fr.json @@ -0,0 +1,52 @@ +{ + "state": { + "yamaha_musiccast__dimmer": { + "auto": "Auto" + }, + "yamaha_musiccast__zone_equalizer_mode": { + "auto": "Auto", + "bypass": "Bypass personnalis\u00e9", + "manual": "Manuel" + }, + "yamaha_musiccast__zone_link_audio_delay": { + "audio_sync": "Synchronisation audio", + "audio_sync_off": "Synchronisation audio d\u00e9sactiv\u00e9e", + "audio_sync_on": "Synchronisation audio activ\u00e9e", + "balanced": "\u00c9quilibr\u00e9", + "lip_sync": "Synchronisation audio" + }, + "yamaha_musiccast__zone_link_audio_quality": { + "compressed": "Compress\u00e9", + "uncompressed": "Non compress\u00e9" + }, + "yamaha_musiccast__zone_link_control": { + "speed": "Vitesse", + "stability": "Stabilit\u00e9", + "standard": "Standard" + }, + "yamaha_musiccast__zone_sleep": { + "120 min": "120 Minutes", + "30 min": "30 Minutes", + "60 min": "60 Minutes", + "90 min": "90 Minutes", + "off": "\u00c9teint" + }, + "yamaha_musiccast__zone_surr_decoder_type": { + "auto": "Auto", + "dolby_pl": "Dolby ProLogic", + "dolby_pl2x_game": "Jeu Dolby ProLogic 2x", + "dolby_pl2x_movie": "Film Dolby ProLogic 2x", + "dolby_pl2x_music": "Musique Dolby ProLogic 2x", + "dolby_surround": "Dolby Surround", + "dts_neo6_cinema": "DTS Neo:6 Cin\u00e9ma", + "dts_neo6_music": "DTS Neo:6 Musique", + "dts_neural_x": "DTS Neural:X", + "toggle": "Permuter" + }, + "yamaha_musiccast__zone_tone_control_mode": { + "auto": "Auto", + "bypass": "Bypass personnalis\u00e9", + "manual": "Manuel" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/fr.json b/homeassistant/components/zha/translations/fr.json index c8b930a14d5..fdb3a8476fe 100644 --- a/homeassistant/components/zha/translations/fr.json +++ b/homeassistant/components/zha/translations/fr.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "\u00c9chec de connexion" }, - "flow_title": "ZHA: {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "Voulez-vous configurer {name} ?" diff --git a/homeassistant/components/zwave_js/translations/fr.json b/homeassistant/components/zwave_js/translations/fr.json index 1a8aef755b5..0933879e517 100644 --- a/homeassistant/components/zwave_js/translations/fr.json +++ b/homeassistant/components/zwave_js/translations/fr.json @@ -27,8 +27,13 @@ "configure_addon": { "data": { "network_key": "Cl\u00e9 r\u00e9seau", + "s0_legacy_key": "Cl\u00e9 S0 (h\u00e9rit\u00e9e)", + "s2_access_control_key": "Cl\u00e9 de contr\u00f4le d'acc\u00e8s S2", + "s2_authenticated_key": "Cl\u00e9 d'authentification S2", + "s2_unauthenticated_key": "Cl\u00e9 non authentifi\u00e9e S2", "usb_path": "Chemin du p\u00e9riph\u00e9rique USB" }, + "description": "Le module compl\u00e9mentaire g\u00e9n\u00e9rera des cl\u00e9s de s\u00e9curit\u00e9 si ces champs sont laiss\u00e9s vides.", "title": "Entrez la configuration du module compl\u00e9mentaire Z-Wave JS" }, "hassio_confirm": { @@ -60,7 +65,12 @@ "device_automation": { "action_type": { "clear_lock_usercode": "Effacer le code utilisateur sur {entity_name}", - "ping": "Pinger l'appareil" + "ping": "Pinger l'appareil", + "refresh_value": "Actualisez la ou les valeurs de {entity_name}", + "reset_meter": "R\u00e9initialiser les compteurs sur {subtype}", + "set_config_parameter": "D\u00e9finir la valeur du param\u00e8tre de configuration {subtype}", + "set_lock_usercode": "D\u00e9finir un code utilisateur sur {entity_name}", + "set_value": "D\u00e9finir la valeur d'une valeur Z-Wave" }, "condition_type": { "config_parameter": "Valeur du param\u00e8tre de configuration {subtype}", @@ -104,9 +114,13 @@ "emulate_hardware": "\u00c9muler le mat\u00e9riel", "log_level": "Niveau du journal", "network_key": "Cl\u00e9 r\u00e9seau", + "s0_legacy_key": "Cl\u00e9 S0 (h\u00e9rit\u00e9e)", + "s2_access_control_key": "Cl\u00e9 de contr\u00f4le d'acc\u00e8s S2", "s2_authenticated_key": "Cl\u00e9 d'authentification S2", + "s2_unauthenticated_key": "Cl\u00e9 non authentifi\u00e9e S2", "usb_path": "Chemin du p\u00e9riph\u00e9rique USB" }, + "description": "Le module compl\u00e9mentaire g\u00e9n\u00e9rera des cl\u00e9s de s\u00e9curit\u00e9 si ces champs sont laiss\u00e9s vides.", "title": "Entrer dans la configuration du module compl\u00e9mentaire Z-Wave JS" }, "install_addon": { From 3198211a7fbb673bdecae73e0d739107c5a6ec0a Mon Sep 17 00:00:00 2001 From: Austin Mroczek Date: Sat, 11 Dec 2021 17:58:12 -0800 Subject: [PATCH 0326/2644] Add instant arming for totalconnect (#60156) Co-authored-by: J. Nick Koston --- .../totalconnect/alarm_control_panel.py | 47 +++++++++++ .../components/totalconnect/services.yaml | 15 ++++ .../totalconnect/test_alarm_control_panel.py | 81 +++++++++++++++++++ 3 files changed, 143 insertions(+) create mode 100644 homeassistant/components/totalconnect/services.yaml diff --git a/homeassistant/components/totalconnect/alarm_control_panel.py b/homeassistant/components/totalconnect/alarm_control_panel.py index 36f7e5297f2..c7cd6cb939a 100644 --- a/homeassistant/components/totalconnect/alarm_control_panel.py +++ b/homeassistant/components/totalconnect/alarm_control_panel.py @@ -21,12 +21,16 @@ from homeassistant.const import ( STATE_ALARM_TRIGGERED, ) from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_platform from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN _LOGGER = logging.getLogger(__name__) +SERVICE_ALARM_ARM_AWAY_INSTANT = "arm_away_instant" +SERVICE_ALARM_ARM_HOME_INSTANT = "arm_home_instant" + async def async_setup_entry(hass, entry, async_add_entities) -> None: """Set up TotalConnect alarm panels based on a config entry.""" @@ -48,6 +52,21 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: async_add_entities(alarms, True) + # Set up services + platform = entity_platform.async_get_current_platform() + + platform.async_register_entity_service( + SERVICE_ALARM_ARM_AWAY_INSTANT, + None, + "async_alarm_arm_away_instant", + ) + + platform.async_register_entity_service( + SERVICE_ALARM_ARM_HOME_INSTANT, + None, + "async_alarm_arm_home_instant", + ) + class TotalConnectAlarm(CoordinatorEntity, alarm.AlarmControlPanelEntity): """Represent an TotalConnect status.""" @@ -201,3 +220,31 @@ class TotalConnectAlarm(CoordinatorEntity, alarm.AlarmControlPanelEntity): raise HomeAssistantError( f"TotalConnect failed to arm night {self._name}." ) from error + + async def async_alarm_arm_home_instant(self, code=None): + """Send arm home instant command.""" + await self.hass.async_add_executor_job(self._arm_home_instant) + await self.coordinator.async_request_refresh() + + def _arm_home_instant(self): + """Arm home instant synchronous.""" + try: + ArmingHelper(self._partition).arm_stay_instant() + except BadResultCodeError as error: + raise HomeAssistantError( + f"TotalConnect failed to arm home instant {self._name}." + ) from error + + async def async_alarm_arm_away_instant(self, code=None): + """Send arm away instant command.""" + await self.hass.async_add_executor_job(self._arm_away_instant) + await self.coordinator.async_request_refresh() + + def _arm_away_instant(self, code=None): + """Arm away instant synchronous.""" + try: + ArmingHelper(self._partition).arm_away_instant() + except BadResultCodeError as error: + raise HomeAssistantError( + f"TotalConnect failed to arm away instant {self._name}." + ) from error diff --git a/homeassistant/components/totalconnect/services.yaml b/homeassistant/components/totalconnect/services.yaml new file mode 100644 index 00000000000..0e8f8f8e217 --- /dev/null +++ b/homeassistant/components/totalconnect/services.yaml @@ -0,0 +1,15 @@ +arm_away_instant: + name: Arm Away Instant + description: Arm Away with zero entry delay. + target: + entity: + integration: totalconnect + domain: alarm_control_panel + +arm_home_instant: + name: Arm Home Instant + description: Arm Home with zero entry delay. + target: + entity: + integration: totalconnect + domain: alarm_control_panel diff --git a/tests/components/totalconnect/test_alarm_control_panel.py b/tests/components/totalconnect/test_alarm_control_panel.py index 368ebf93a04..c623b826459 100644 --- a/tests/components/totalconnect/test_alarm_control_panel.py +++ b/tests/components/totalconnect/test_alarm_control_panel.py @@ -5,6 +5,11 @@ from unittest.mock import patch import pytest from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN +from homeassistant.components.totalconnect import DOMAIN +from homeassistant.components.totalconnect.alarm_control_panel import ( + SERVICE_ALARM_ARM_AWAY_INSTANT, + SERVICE_ALARM_ARM_HOME_INSTANT, +) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, @@ -121,6 +126,82 @@ async def test_arm_home_failure(hass: HomeAssistant) -> None: assert mock_request.call_count == 2 +async def test_arm_home_instant_success(hass: HomeAssistant) -> None: + """Test arm home instant method success.""" + responses = [RESPONSE_DISARMED, RESPONSE_ARM_SUCCESS, RESPONSE_ARMED_STAY] + with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: + await setup_platform(hass, ALARM_DOMAIN) + assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED + assert hass.states.get(ENTITY_ID_2).state == STATE_ALARM_DISARMED + assert mock_request.call_count == 1 + + await hass.services.async_call( + DOMAIN, SERVICE_ALARM_ARM_HOME_INSTANT, DATA, blocking=True + ) + assert mock_request.call_count == 2 + + async_fire_time_changed(hass, dt.utcnow() + DELAY) + await hass.async_block_till_done() + assert mock_request.call_count == 3 + assert hass.states.get(ENTITY_ID).state == STATE_ALARM_ARMED_HOME + + +async def test_arm_home_instant_failure(hass: HomeAssistant) -> None: + """Test arm home instant method failure.""" + responses = [RESPONSE_DISARMED, RESPONSE_ARM_FAILURE] + with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: + await setup_platform(hass, ALARM_DOMAIN) + assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED + assert mock_request.call_count == 1 + + with pytest.raises(HomeAssistantError) as err: + await hass.services.async_call( + DOMAIN, SERVICE_ALARM_ARM_HOME_INSTANT, DATA, blocking=True + ) + await hass.async_block_till_done() + assert f"{err.value}" == "TotalConnect failed to arm home instant test." + assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED + assert mock_request.call_count == 2 + + +async def test_arm_away_instant_success(hass: HomeAssistant) -> None: + """Test arm home instant method success.""" + responses = [RESPONSE_DISARMED, RESPONSE_ARM_SUCCESS, RESPONSE_ARMED_AWAY] + with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: + await setup_platform(hass, ALARM_DOMAIN) + assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED + assert hass.states.get(ENTITY_ID_2).state == STATE_ALARM_DISARMED + assert mock_request.call_count == 1 + + await hass.services.async_call( + DOMAIN, SERVICE_ALARM_ARM_AWAY_INSTANT, DATA, blocking=True + ) + assert mock_request.call_count == 2 + + async_fire_time_changed(hass, dt.utcnow() + DELAY) + await hass.async_block_till_done() + assert mock_request.call_count == 3 + assert hass.states.get(ENTITY_ID).state == STATE_ALARM_ARMED_AWAY + + +async def test_arm_away_instant_failure(hass: HomeAssistant) -> None: + """Test arm home instant method failure.""" + responses = [RESPONSE_DISARMED, RESPONSE_ARM_FAILURE] + with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: + await setup_platform(hass, ALARM_DOMAIN) + assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED + assert mock_request.call_count == 1 + + with pytest.raises(HomeAssistantError) as err: + await hass.services.async_call( + DOMAIN, SERVICE_ALARM_ARM_AWAY_INSTANT, DATA, blocking=True + ) + await hass.async_block_till_done() + assert f"{err.value}" == "TotalConnect failed to arm away instant test." + assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED + assert mock_request.call_count == 2 + + async def test_arm_home_invalid_usercode(hass: HomeAssistant) -> None: """Test arm home method with invalid usercode.""" responses = [RESPONSE_DISARMED, RESPONSE_USER_CODE_INVALID] From 9a1109949f7506410d1a91f2164533e88be5cb3d Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sat, 11 Dec 2021 22:14:22 -0600 Subject: [PATCH 0327/2644] Fix Sonos sub & surround switch state reporting (#61531) * Fix sub/surround states, refactor volume param handling * Lint --- homeassistant/components/sonos/speaker.py | 25 ++++++++++++----------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index e12166d7795..30b240c9fd7 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -193,7 +193,7 @@ class SonosSpeaker: self.volume: int | None = None self.muted: bool | None = None self.night_mode: bool | None = None - self.dialog_mode: bool | None = None + self.dialog_level: bool | None = None self.cross_fade: bool | None = None self.bass: int | None = None self.treble: int | None = None @@ -498,17 +498,18 @@ class SonosSpeaker: if "mute" in variables: self.muted = variables["mute"]["Master"] == "1" - if "night_mode" in variables: - self.night_mode = variables["night_mode"] == "1" + for bool_var in ( + "dialog_level", + "night_mode", + "sub_enabled", + "surround_enabled", + ): + if bool_var in variables: + setattr(self, bool_var, variables[bool_var] == "1") - if "dialog_level" in variables: - self.dialog_mode = variables["dialog_level"] == "1" - - if "bass" in variables: - self.bass = variables["bass"] - - if "treble" in variables: - self.treble = variables["treble"] + for int_var in ("bass", "treble"): + if int_var in variables: + setattr(self, int_var, variables[int_var]) self.async_write_entity_states() @@ -982,7 +983,7 @@ class SonosSpeaker: self.volume = self.soco.volume self.muted = self.soco.mute self.night_mode = self.soco.night_mode - self.dialog_mode = self.soco.dialog_mode + self.dialog_level = self.soco.dialog_mode self.bass = self.soco.bass self.treble = self.soco.treble From 388fcac68972bf32649da96d6990f86f41ab995d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 11 Dec 2021 19:57:11 -1000 Subject: [PATCH 0328/2644] Reduce cpu requirements for apple_tv mdns and discovery (#61346) Co-authored-by: jjlawren --- homeassistant/components/apple_tv/__init__.py | 18 +-- .../components/apple_tv/config_flow.py | 118 ++++++++++----- tests/components/apple_tv/common.py | 15 +- tests/components/apple_tv/conftest.py | 19 ++- tests/components/apple_tv/test_config_flow.py | 134 +++++++++++++++--- 5 files changed, 228 insertions(+), 76 deletions(-) diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index 29bc5634b38..200984ca883 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -36,6 +36,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Apple TV" +BACKOFF_TIME_LOWER_LIMIT = 15 # seconds BACKOFF_TIME_UPPER_LIMIT = 300 # Five minutes SIGNAL_CONNECTED = "apple_tv_connected" @@ -241,7 +242,11 @@ class AppleTVManager: if self.atv is None: self._connection_attempts += 1 backoff = min( - randrange(2 ** self._connection_attempts), BACKOFF_TIME_UPPER_LIMIT + max( + BACKOFF_TIME_LOWER_LIMIT, + randrange(2 ** self._connection_attempts), + ), + BACKOFF_TIME_UPPER_LIMIT, ) _LOGGER.debug("Reconnecting in %d seconds", backoff) @@ -271,17 +276,12 @@ class AppleTVManager: return atvs[0] _LOGGER.debug( - "Failed to find device %s with address %s, trying to scan", + "Failed to find device %s with address %s", self.config_entry.title, address, ) - - atvs = await scan(self.hass.loop, identifier=identifiers, protocol=protocols) - if atvs: - return atvs[0] - - _LOGGER.debug("Failed to find device %s, trying later", self.config_entry.title) - + # We no longer multicast scan for the device since as soon as async_step_zeroconf runs, + # it will update the address and reload the config entry when the device is found. return None async def _connect(self, conf): diff --git a/homeassistant/components/apple_tv/config_flow.py b/homeassistant/components/apple_tv/config_flow.py index 16a757b2ebb..878483e0ce7 100644 --- a/homeassistant/components/apple_tv/config_flow.py +++ b/homeassistant/components/apple_tv/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Apple TV integration.""" +import asyncio from collections import deque from ipaddress import ip_address import logging @@ -27,6 +28,8 @@ INPUT_PIN_SCHEMA = vol.Schema({vol.Required(CONF_PIN, default=None): int}) DEFAULT_START_OFF = False +DISCOVERY_AGGREGATION_TIME = 15 # seconds + async def device_scan(identifier, loop): """Scan for a specific device using identifier as filter.""" @@ -46,12 +49,13 @@ async def device_scan(identifier, loop): except ValueError: return None - for hosts in (_host_filter(), None): - scan_result = await scan(loop, timeout=3, hosts=hosts) - matches = [atv for atv in scan_result if _filter_device(atv)] + # If we have an address, only probe that address to avoid + # broadcast traffic on the network + scan_result = await scan(loop, timeout=3, hosts=_host_filter()) + matches = [atv for atv in scan_result if _filter_device(atv)] - if matches: - return matches[0], matches[0].all_identifiers + if matches: + return matches[0], matches[0].all_identifiers return None, None @@ -93,10 +97,12 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): existing config entry. If that's the case, the unique_id from that entry is re-used, otherwise the newly discovered identifier is used instead. """ + all_identifiers = set(self.atv.all_identifiers) for entry in self._async_current_entries(): - for identifier in self.atv.all_identifiers: - if identifier in entry.data.get(CONF_IDENTIFIERS, [entry.unique_id]): - return entry.unique_id + if all_identifiers.intersection( + entry.data.get(CONF_IDENTIFIERS, [entry.unique_id]) + ): + return entry.unique_id return self.atv.identifier async def async_step_reauth(self, user_input=None): @@ -149,22 +155,18 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> data_entry_flow.FlowResult: """Handle device found via zeroconf.""" + host = discovery_info.host + self._async_abort_entries_match({CONF_ADDRESS: host}) service_type = discovery_info.type[:-1] # Remove leading . name = discovery_info.name.replace(f".{service_type}.", "") properties = discovery_info.properties # Extract unique identifier from service - self.scan_filter = get_unique_id(service_type, name, properties) - if self.scan_filter is None: + unique_id = get_unique_id(service_type, name, properties) + if unique_id is None: return self.async_abort(reason="unknown") - # Scan for the device in order to extract _all_ unique identifiers assigned to - # it. Not doing it like this will yield multiple config flows for the same - # device, one per protocol, which is undesired. - return await self.async_find_device_wrapper(self.async_found_zeroconf_device) - - async def async_found_zeroconf_device(self, user_input=None): - """Handle device found after Zeroconf discovery.""" + # # Suppose we have a device with three services: A, B and C. Let's assume # service A is discovered by Zeroconf, triggering a device scan that also finds # service B but *not* C. An identifier is picked from one of the services and @@ -177,31 +179,63 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # since both flows really represent the same device. They will however end up # as two separate flows. # - # To solve this, all identifiers found during a device scan is stored as + # To solve this, all identifiers are stored as # "all_identifiers" in the flow context. When a new service is discovered, the # code below will check these identifiers for all active flows and abort if a # match is found. Before aborting, the original flow is updated with any # potentially new identifiers. In the example above, when service C is # discovered, the identifier of service C will be inserted into # "all_identifiers" of the original flow (making the device complete). - for flow in self._async_in_progress(): - for identifier in self.atv.all_identifiers: - if identifier not in flow["context"].get("all_identifiers", []): - continue + # + # Wait DISCOVERY_AGGREGATION_TIME for multiple services to be + # discovered via zeroconf. Once the first service is discovered + # this allows other services to be discovered inside the time + # window before triggering a scan of the device. This prevents + # multiple scans of the device at the same time since each + # apple_tv device has multiple services that are discovered by + # zeroconf. + # + await asyncio.sleep(DISCOVERY_AGGREGATION_TIME) + self._async_check_in_progress_and_set_address(host, unique_id) + + # Scan for the device in order to extract _all_ unique identifiers assigned to + # it. Not doing it like this will yield multiple config flows for the same + # device, one per protocol, which is undesired. + self.scan_filter = host + return await self.async_find_device_wrapper(self.async_found_zeroconf_device) + + @callback + def _async_check_in_progress_and_set_address(self, host: str, unique_id: str): + """Check for in-progress flows and update them with identifiers if needed. + + This code must not await between checking in progress and setting the host + or it will have a race condition where no flows move forward. + """ + for flow in self._async_in_progress(include_uninitialized=True): + context = flow["context"] + if ( + context.get("source") != config_entries.SOURCE_ZEROCONF + or context.get(CONF_ADDRESS) != host + ): + continue + if ( + "all_identifiers" in context + and unique_id not in context["all_identifiers"] + ): # Add potentially new identifiers from this device to the existing flow - identifiers = set(flow["context"]["all_identifiers"]) - identifiers.update(self.atv.all_identifiers) - flow["context"]["all_identifiers"] = list(identifiers) - - raise data_entry_flow.AbortFlow("already_in_progress") + context["all_identifiers"].append(unique_id) + raise data_entry_flow.AbortFlow("already_in_progress") + self.context[CONF_ADDRESS] = host + async def async_found_zeroconf_device(self, user_input=None): + """Handle device found after Zeroconf discovery.""" self.context["all_identifiers"] = self.atv.all_identifiers - # Also abort if an integration with this identifier already exists await self.async_set_unique_id(self.device_identifier) - self._abort_if_unique_id_configured() - + # but be sure to update the address if its changed so the scanner + # will probe the new address + self._abort_if_unique_id_configured(updates={CONF_ADDRESS: self.atv.address}) self.context["identifier"] = self.unique_id return await self.async_step_confirm() @@ -245,14 +279,22 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): else model_str(dev_info.model) ), } - - if not allow_exist: - for identifier in self.atv.all_identifiers: - for entry in self._async_current_entries(): - if identifier in entry.data.get( - CONF_IDENTIFIERS, [entry.unique_id] - ): - raise DeviceAlreadyConfigured() + all_identifiers = set(self.atv.all_identifiers) + for entry in self._async_current_entries(): + if not all_identifiers.intersection( + entry.data.get(CONF_IDENTIFIERS, [entry.unique_id]) + ): + continue + if entry.data.get(CONF_ADDRESS) != self.atv.address: + self.hass.config_entries.async_update_entry( + entry, + data={**entry.data, CONF_ADDRESS: self.atv.address}, + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + if not allow_exist: + raise DeviceAlreadyConfigured() async def async_step_confirm(self, user_input=None): """Handle user-confirmation of discovered node.""" diff --git a/tests/components/apple_tv/common.py b/tests/components/apple_tv/common.py index 687ad256f3a..ddb8c1348d9 100644 --- a/tests/components/apple_tv/common.py +++ b/tests/components/apple_tv/common.py @@ -49,10 +49,10 @@ def create_conf(name, address, *services): return atv -def mrp_service(enabled=True): +def mrp_service(enabled=True, unique_id="mrpid"): """Create example MRP service.""" return conf.ManualService( - "mrpid", + unique_id, Protocol.MRP, 5555, {}, @@ -70,3 +70,14 @@ def airplay_service(): {}, pairing_requirement=const.PairingRequirement.Mandatory, ) + + +def raop_service(): + """Create example RAOP service.""" + return conf.ManualService( + "AABBCCDDEEFF", + Protocol.RAOP, + 7000, + {}, + pairing_requirement=const.PairingRequirement.Mandatory, + ) diff --git a/tests/components/apple_tv/conftest.py b/tests/components/apple_tv/conftest.py index c8a9725610c..504e2d22e1d 100644 --- a/tests/components/apple_tv/conftest.py +++ b/tests/components/apple_tv/conftest.py @@ -96,12 +96,19 @@ def full_device(mock_scan, dmap_pin): @pytest.fixture def mrp_device(mock_scan): """Mock pyatv.scan.""" - mock_scan.result.append( - create_conf( - "127.0.0.1", - "MRP Device", - mrp_service(), - ) + mock_scan.result.extend( + [ + create_conf( + "127.0.0.1", + "MRP Device", + mrp_service(), + ), + create_conf( + "127.0.0.2", + "MRP Device 2", + mrp_service(unique_id="unrelated"), + ), + ] ) yield mock_scan diff --git a/tests/components/apple_tv/test_config_flow.py b/tests/components/apple_tv/test_config_flow.py index 39403b837ca..1d2a5a459d4 100644 --- a/tests/components/apple_tv/test_config_flow.py +++ b/tests/components/apple_tv/test_config_flow.py @@ -1,6 +1,6 @@ """Test config flow.""" -from unittest.mock import patch +from unittest.mock import ANY, patch from pyatv import exceptions from pyatv.const import PairingRequirement, Protocol @@ -8,14 +8,15 @@ import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components import zeroconf +from homeassistant.components.apple_tv import CONF_ADDRESS, config_flow from homeassistant.components.apple_tv.const import CONF_START_OFF, DOMAIN -from .common import airplay_service, create_conf, mrp_service +from .common import airplay_service, create_conf, mrp_service, raop_service from tests.common import MockConfigEntry DMAP_SERVICE = zeroconf.ZeroconfServiceInfo( - host="mock_host", + host="127.0.0.1", hostname="mock_hostname", port=None, type="_touch-able._tcp.local.", @@ -24,6 +25,23 @@ DMAP_SERVICE = zeroconf.ZeroconfServiceInfo( ) +RAOP_SERVICE = zeroconf.ZeroconfServiceInfo( + host="127.0.0.1", + hostname="mock_hostname", + port=None, + type="_raop._tcp.local.", + name="AABBCCDDEEFF@Master Bed._raop._tcp.local.", + properties={"am": "AppleTV11,1"}, +) + + +@pytest.fixture(autouse=True) +def zero_aggregation_time(): + """Prevent the aggregation time from delaying the tests.""" + with patch.object(config_flow, "DISCOVERY_AGGREGATION_TIME", 0): + yield + + @pytest.fixture(autouse=True) def use_mocked_zeroconf(mock_async_zeroconf): """Mock zeroconf in all tests.""" @@ -507,7 +525,7 @@ async def test_zeroconf_unsupported_service_aborts(hass): DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - host="mock_host", + host="127.0.0.1", hostname="mock_hostname", name="mock_name", port=None, @@ -521,11 +539,25 @@ async def test_zeroconf_unsupported_service_aborts(hass): async def test_zeroconf_add_mrp_device(hass, mrp_device, pairing): """Test add MRP device discovered by zeroconf.""" + unrelated_result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="127.0.0.2", + hostname="mock_hostname", + port=None, + name="Kitchen", + properties={"UniqueIdentifier": "unrelated", "Name": "Kitchen"}, + type="_mediaremotetv._tcp.local.", + ), + ) + assert unrelated_result["type"] == data_entry_flow.RESULT_TYPE_FORM + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - host="mock_host", + host="127.0.0.1", hostname="mock_hostname", port=None, name="Kitchen", @@ -586,6 +618,37 @@ async def test_zeroconf_add_dmap_device(hass, dmap_device, dmap_pin, pairing): } +async def test_zeroconf_ip_change(hass, mock_scan): + """Test that the config entry gets updated when the ip changes and reloads.""" + entry = MockConfigEntry( + domain="apple_tv", unique_id="mrpid", data={CONF_ADDRESS: "127.0.0.2"} + ) + unrelated_entry = MockConfigEntry( + domain="apple_tv", unique_id="unrelated", data={CONF_ADDRESS: "127.0.0.2"} + ) + unrelated_entry.add_to_hass(hass) + entry.add_to_hass(hass) + mock_scan.result = [ + create_conf("127.0.0.1", "Device", mrp_service(), airplay_service()) + ] + + with patch( + "homeassistant.components.apple_tv.async_setup_entry", return_value=True + ) as mock_async_setup: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=DMAP_SERVICE, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert len(mock_async_setup.mock_calls) == 2 + assert entry.data[CONF_ADDRESS] == "127.0.0.1" + assert unrelated_entry.data[CONF_ADDRESS] == "127.0.0.2" + + async def test_zeroconf_add_existing_aborts(hass, dmap_device): """Test start new zeroconf flow while existing flow is active aborts.""" await hass.config_entries.flow.async_init( @@ -638,7 +701,7 @@ async def test_zeroconf_abort_if_other_in_progress(hass, mock_scan): DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - host="mock_host", + host="127.0.0.1", hostname="mock_hostname", port=None, type="_airplay._tcp.local.", @@ -658,7 +721,7 @@ async def test_zeroconf_abort_if_other_in_progress(hass, mock_scan): DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - host="mock_host", + host="127.0.0.1", hostname="mock_hostname", port=None, type="_mediaremotetv._tcp.local.", @@ -681,7 +744,7 @@ async def test_zeroconf_missing_device_during_protocol_resolve( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - host="mock_host", + host="127.0.0.1", hostname="mock_hostname", port=None, type="_airplay._tcp.local.", @@ -700,7 +763,7 @@ async def test_zeroconf_missing_device_during_protocol_resolve( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - host="mock_host", + host="127.0.0.1", hostname="mock_hostname", port=None, type="_mediaremotetv._tcp.local.", @@ -733,7 +796,7 @@ async def test_zeroconf_additional_protocol_resolve_failure( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - host="mock_host", + host="127.0.0.1", hostname="mock_hostname", port=None, type="_airplay._tcp.local.", @@ -752,7 +815,7 @@ async def test_zeroconf_additional_protocol_resolve_failure( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - host="mock_host", + host="127.0.0.1", hostname="mock_hostname", port=None, type="_mediaremotetv._tcp.local.", @@ -785,7 +848,7 @@ async def test_zeroconf_pair_additionally_found_protocols( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - host="mock_host", + host="127.0.0.1", hostname="mock_hostname", port=None, type="_airplay._tcp.local.", @@ -793,9 +856,26 @@ async def test_zeroconf_pair_additionally_found_protocols( properties={"deviceid": "airplayid"}, ), ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + await hass.async_block_till_done() mock_scan.result = [ - create_conf("127.0.0.1", "Device", mrp_service(), airplay_service()) + create_conf("127.0.0.1", "Device", raop_service(), airplay_service()) + ] + + # Find the same device again, but now also with RAOP service. The first flow should + # be updated with the RAOP service. + await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=RAOP_SERVICE, + ) + await hass.async_block_till_done() + + mock_scan.result = [ + create_conf( + "127.0.0.1", "Device", raop_service(), mrp_service(), airplay_service() + ) ] # Find the same device again, but now also with MRP service. The first flow should @@ -804,7 +884,7 @@ async def test_zeroconf_pair_additionally_found_protocols( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - host="mock_host", + host="127.0.0.1", hostname="mock_hostname", port=None, type="_mediaremotetv._tcp.local.", @@ -812,29 +892,41 @@ async def test_zeroconf_pair_additionally_found_protocols( properties={"UniqueIdentifier": "mrpid", "Name": "Kitchen"}, ), ) + await hass.async_block_till_done() - # Verify that _both_ protocols are paired + # Verify that all protocols are paired result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["step_id"] == "pair_with_pin" - assert result2["description_placeholders"] == {"protocol": "MRP"} + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "pair_no_pin" + assert result2["description_placeholders"] == {"pin": ANY, "protocol": "RAOP"} + + # Verify that all protocols are paired result3 = await hass.config_entries.flow.async_configure( result["flow_id"], - {"pin": 1234}, + {}, ) + assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM assert result3["step_id"] == "pair_with_pin" - assert result3["description_placeholders"] == {"protocol": "AirPlay"} + assert result3["description_placeholders"] == {"protocol": "MRP"} result4 = await hass.config_entries.flow.async_configure( result["flow_id"], {"pin": 1234}, ) - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result4["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result4["step_id"] == "pair_with_pin" + assert result4["description_placeholders"] == {"protocol": "AirPlay"} + + result5 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"pin": 1234}, + ) + assert result5["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY # Re-configuration From 359affb856c186c96dbc86882ffcdc0af5f48ceb Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Sat, 11 Dec 2021 23:13:12 -0700 Subject: [PATCH 0329/2644] Bump pylitterbot to 2021.12.0 (#61536) --- homeassistant/components/litterrobot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index 0d76e8df09c..ab05ab111f0 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/litterrobot", "requirements": [ - "pylitterbot==2021.11.0" + "pylitterbot==2021.12.0" ], "codeowners": [ "@natekspencer" diff --git a/requirements_all.txt b/requirements_all.txt index 19124429426..38505b5fd1e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1616,7 +1616,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2021.11.0 +pylitterbot==2021.12.0 # homeassistant.components.lutron_caseta pylutron-caseta==0.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 425bc09f87c..32179d7d98c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -989,7 +989,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2021.11.0 +pylitterbot==2021.12.0 # homeassistant.components.lutron_caseta pylutron-caseta==0.11.0 From 997809c6c47993b60109273228cc38d66420121b Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sun, 12 Dec 2021 07:52:49 -0500 Subject: [PATCH 0330/2644] Refactor ZHA entity matching process (#60063) * Group multi-matches by channels * Group multi-matched by explicit groups * Registryless AnalogInput and PowerConfiguration * Refactor single cluster sensor registry * Refactor single cluster cover and lock registry * Refactor single cluster binary_sensor registry * Pylint --- homeassistant/components/zha/binary_sensor.py | 9 +- homeassistant/components/zha/climate.py | 12 ++- .../components/zha/core/registries.py | 86 +++++++------------ homeassistant/components/zha/cover.py | 8 +- homeassistant/components/zha/lock.py | 4 +- homeassistant/components/zha/sensor.py | 49 +++++++---- tests/components/zha/test_discover.py | 6 -- 7 files changed, 80 insertions(+), 94 deletions(-) diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index 7e2b34d3613..4bc7c156f37 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -36,6 +36,7 @@ CLASS_MAPPING = { } STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.BINARY_SENSOR) +MULTI_MATCH = functools.partial(ZHA_ENTITIES.multipass_match, Platform.BINARY_SENSOR) async def async_setup_entry( @@ -103,7 +104,7 @@ class BinarySensor(ZhaEntity, BinarySensorEntity): self._state = attr_value -@STRICT_MATCH(channel_names=CHANNEL_ACCELEROMETER) +@MULTI_MATCH(channel_names=CHANNEL_ACCELEROMETER) class Accelerometer(BinarySensor): """ZHA BinarySensor.""" @@ -111,7 +112,7 @@ class Accelerometer(BinarySensor): _attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.MOVING -@STRICT_MATCH(channel_names=CHANNEL_OCCUPANCY) +@MULTI_MATCH(channel_names=CHANNEL_OCCUPANCY) class Occupancy(BinarySensor): """ZHA BinarySensor.""" @@ -127,7 +128,7 @@ class Opening(BinarySensor): _attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.OPENING -@STRICT_MATCH(channel_names=CHANNEL_BINARY_INPUT) +@MULTI_MATCH(channel_names=CHANNEL_BINARY_INPUT) class BinaryInput(BinarySensor): """ZHA BinarySensor.""" @@ -153,7 +154,7 @@ class Motion(BinarySensor): _attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.MOTION -@STRICT_MATCH(channel_names=CHANNEL_ZONE) +@MULTI_MATCH(channel_names=CHANNEL_ZONE) class IASZone(BinarySensor): """ZHA IAS BinarySensor.""" diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index 9c01f6630db..3473a6d8f9e 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -172,7 +172,11 @@ async def async_setup_entry( config_entry.async_on_unload(unsub) -@MULTI_MATCH(channel_names=CHANNEL_THERMOSTAT, aux_channels=CHANNEL_FAN) +@MULTI_MATCH( + channel_names=CHANNEL_THERMOSTAT, + aux_channels=CHANNEL_FAN, + stop_on_match_group=CHANNEL_THERMOSTAT, +) class Thermostat(ZhaEntity, ClimateEntity): """Representation of a ZHA Thermostat device.""" @@ -526,7 +530,7 @@ class Thermostat(ZhaEntity, ClimateEntity): @MULTI_MATCH( channel_names={CHANNEL_THERMOSTAT, "sinope_manufacturer_specific"}, manufacturers="Sinope Technologies", - stop_on_match=True, + stop_on_match_group=CHANNEL_THERMOSTAT, ) class SinopeTechnologiesThermostat(Thermostat): """Sinope Technologies Thermostat.""" @@ -579,7 +583,7 @@ class SinopeTechnologiesThermostat(Thermostat): channel_names=CHANNEL_THERMOSTAT, aux_channels=CHANNEL_FAN, manufacturers="Zen Within", - stop_on_match=True, + stop_on_match_group=CHANNEL_THERMOSTAT, ) class ZenWithinThermostat(Thermostat): """Zen Within Thermostat implementation.""" @@ -609,7 +613,7 @@ class ZenWithinThermostat(Thermostat): aux_channels=CHANNEL_FAN, manufacturers="Centralite", models={"3157100", "3157100-E"}, - stop_on_match=True, + stop_on_match_group=CHANNEL_THERMOSTAT, ) class CentralitePearl(ZenWithinThermostat): """Centralite Pearl Thermostat implementation.""" diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 36a77b841b6..146b7d43f0f 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -4,7 +4,6 @@ from __future__ import annotations import collections from collections.abc import Callable import dataclasses -from typing import Dict, List import attr from zigpy import zcl @@ -53,29 +52,10 @@ REMOTE_DEVICE_TYPES = collections.defaultdict(list, REMOTE_DEVICE_TYPES) SINGLE_INPUT_CLUSTER_DEVICE_CLASS = { # this works for now but if we hit conflicts we can break it out to # a different dict that is keyed by manufacturer - SMARTTHINGS_ACCELERATION_CLUSTER: Platform.BINARY_SENSOR, - SMARTTHINGS_HUMIDITY_CLUSTER: Platform.SENSOR, - VOC_LEVEL_CLUSTER: Platform.SENSOR, - zcl.clusters.closures.DoorLock.cluster_id: Platform.LOCK, - zcl.clusters.closures.WindowCovering.cluster_id: Platform.COVER, - zcl.clusters.general.BinaryInput.cluster_id: Platform.BINARY_SENSOR, - zcl.clusters.general.AnalogInput.cluster_id: Platform.SENSOR, zcl.clusters.general.AnalogOutput.cluster_id: Platform.NUMBER, zcl.clusters.general.MultistateInput.cluster_id: Platform.SENSOR, zcl.clusters.general.OnOff.cluster_id: Platform.SWITCH, - zcl.clusters.general.PowerConfiguration.cluster_id: Platform.SENSOR, zcl.clusters.hvac.Fan.cluster_id: Platform.FAN, - zcl.clusters.measurement.CarbonDioxideConcentration.cluster_id: Platform.SENSOR, - zcl.clusters.measurement.CarbonMonoxideConcentration.cluster_id: Platform.SENSOR, - zcl.clusters.measurement.FormaldehydeConcentration.cluster_id: Platform.SENSOR, - zcl.clusters.measurement.IlluminanceMeasurement.cluster_id: Platform.SENSOR, - zcl.clusters.measurement.OccupancySensing.cluster_id: Platform.BINARY_SENSOR, - zcl.clusters.measurement.PressureMeasurement.cluster_id: Platform.SENSOR, - zcl.clusters.measurement.RelativeHumidity.cluster_id: Platform.SENSOR, - zcl.clusters.measurement.SoilMoisture.cluster_id: Platform.SENSOR, - zcl.clusters.measurement.LeafWetness.cluster_id: Platform.SENSOR, - zcl.clusters.measurement.TemperatureMeasurement.cluster_id: Platform.SENSOR, - zcl.clusters.security.IasZone.cluster_id: Platform.BINARY_SENSOR, } SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = { @@ -136,12 +116,10 @@ def set_or_callable(value): class MatchRule: """Match a ZHA Entity to a channel name or generic id.""" - channel_names: Callable | set[str] | str = attr.ib( - factory=frozenset, converter=set_or_callable - ) - generic_ids: Callable | set[str] | str = attr.ib( + channel_names: set[str] | str = attr.ib( factory=frozenset, converter=set_or_callable ) + generic_ids: set[str] | str = attr.ib(factory=frozenset, converter=set_or_callable) manufacturers: Callable | set[str] | str = attr.ib( factory=frozenset, converter=set_or_callable ) @@ -151,8 +129,6 @@ class MatchRule: aux_channels: Callable | set[str] | str = attr.ib( factory=frozenset, converter=set_or_callable ) - # for multi entities, stop further processing on a match for a component - stop_on_match: bool = attr.ib(default=False) @property def weight(self) -> int: @@ -238,21 +214,20 @@ class EntityClassAndChannels: claimed_channel: list[ChannelType] -RegistryDictType = Dict[str, Dict[MatchRule, CALLABLE_T]] -MultiRegistryDictType = Dict[str, Dict[MatchRule, List[CALLABLE_T]]] -GroupRegistryDictType = Dict[str, CALLABLE_T] - - class ZHAEntityRegistry: """Channel to ZHA Entity mapping.""" def __init__(self): """Initialize Registry instance.""" - self._strict_registry: RegistryDictType = collections.defaultdict(dict) - self._multi_entity_registry: MultiRegistryDictType = collections.defaultdict( - lambda: collections.defaultdict(list) + self._strict_registry: dict[ + str, dict[MatchRule, CALLABLE_T] + ] = collections.defaultdict(dict) + self._multi_entity_registry: dict[ + str, dict[int | str | None, dict[MatchRule, list[CALLABLE_T]]] + ] = collections.defaultdict( + lambda: collections.defaultdict(lambda: collections.defaultdict(list)) ) - self._group_registry: GroupRegistryDictType = {} + self._group_registry: dict[str, CALLABLE_T] = {} def get_entity( self, @@ -276,23 +251,22 @@ class ZHAEntityRegistry: manufacturer: str, model: str, channels: list[ChannelType], - components: set | None = None, ) -> tuple[dict[str, list[EntityClassAndChannels]], list[ChannelType]]: """Match ZHA Channels to potentially multiple ZHA Entity classes.""" result: dict[str, list[EntityClassAndChannels]] = collections.defaultdict(list) all_claimed: set[ChannelType] = set() - for component in components or self._multi_entity_registry: - matches = self._multi_entity_registry[component] - sorted_matches = sorted(matches, key=lambda x: x.weight, reverse=True) - for match in sorted_matches: - if match.strict_matched(manufacturer, model, channels): - claimed = match.claim_channels(channels) - for ent_class in self._multi_entity_registry[component][match]: - ent_n_channels = EntityClassAndChannels(ent_class, claimed) - result[component].append(ent_n_channels) - all_claimed |= set(claimed) - if match.stop_on_match: - break + for component, stop_match_groups in self._multi_entity_registry.items(): + for stop_match_grp, matches in stop_match_groups.items(): + sorted_matches = sorted(matches, key=lambda x: x.weight, reverse=True) + for match in sorted_matches: + if match.strict_matched(manufacturer, model, channels): + claimed = match.claim_channels(channels) + for ent_class in stop_match_groups[stop_match_grp][match]: + ent_n_channels = EntityClassAndChannels(ent_class, claimed) + result[component].append(ent_n_channels) + all_claimed |= set(claimed) + if stop_match_grp: + break return result, list(all_claimed) @@ -303,8 +277,8 @@ class ZHAEntityRegistry: def strict_match( self, component: str, - channel_names: Callable | set[str] | str = None, - generic_ids: Callable | set[str] | str = None, + channel_names: set[str] | str = None, + generic_ids: set[str] | str = None, manufacturers: Callable | set[str] | str = None, models: Callable | set[str] | str = None, aux_channels: Callable | set[str] | str = None, @@ -328,12 +302,12 @@ class ZHAEntityRegistry: def multipass_match( self, component: str, - channel_names: Callable | set[str] | str = None, - generic_ids: Callable | set[str] | str = None, + channel_names: set[str] | str = None, + generic_ids: set[str] | str = None, manufacturers: Callable | set[str] | str = None, models: Callable | set[str] | str = None, aux_channels: Callable | set[str] | str = None, - stop_on_match: bool = False, + stop_on_match_group: int | str | None = None, ) -> Callable[[CALLABLE_T], CALLABLE_T]: """Decorate a loose match rule.""" @@ -343,7 +317,6 @@ class ZHAEntityRegistry: manufacturers, models, aux_channels, - stop_on_match, ) def decorator(zha_entity: CALLABLE_T) -> CALLABLE_T: @@ -351,7 +324,10 @@ class ZHAEntityRegistry: All non empty fields of a match rule must match. """ - self._multi_entity_registry[component][rule].append(zha_entity) + # group the rules by channels + self._multi_entity_registry[component][stop_on_match_group][rule].append( + zha_entity + ) return zha_entity return decorator diff --git a/homeassistant/components/zha/cover.py b/homeassistant/components/zha/cover.py index e5ff1f6e95d..2e8c7ab45ea 100644 --- a/homeassistant/components/zha/cover.py +++ b/homeassistant/components/zha/cover.py @@ -42,7 +42,7 @@ from .entity import ZhaEntity _LOGGER = logging.getLogger(__name__) -STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.COVER) +MULTI_MATCH = functools.partial(ZHA_ENTITIES.multipass_match, Platform.COVER) async def async_setup_entry( @@ -63,7 +63,7 @@ async def async_setup_entry( config_entry.async_on_unload(unsub) -@STRICT_MATCH(channel_names=CHANNEL_COVER) +@MULTI_MATCH(channel_names=CHANNEL_COVER) class ZhaCover(ZhaEntity, CoverEntity): """Representation of a ZHA cover.""" @@ -182,7 +182,7 @@ class ZhaCover(ZhaEntity, CoverEntity): self._state = None -@STRICT_MATCH(channel_names={CHANNEL_LEVEL, CHANNEL_ON_OFF, CHANNEL_SHADE}) +@MULTI_MATCH(channel_names={CHANNEL_LEVEL, CHANNEL_ON_OFF, CHANNEL_SHADE}) class Shade(ZhaEntity, CoverEntity): """ZHA Shade.""" @@ -289,7 +289,7 @@ class Shade(ZhaEntity, CoverEntity): return -@STRICT_MATCH( +@MULTI_MATCH( channel_names={CHANNEL_LEVEL, CHANNEL_ON_OFF}, manufacturers="Keen Home Inc" ) class KeenVent(Shade): diff --git a/homeassistant/components/zha/lock.py b/homeassistant/components/zha/lock.py index 61104a8b7e9..4eb9752f355 100644 --- a/homeassistant/components/zha/lock.py +++ b/homeassistant/components/zha/lock.py @@ -23,7 +23,7 @@ from .entity import ZhaEntity # The first state is Zigbee 'Not fully locked' STATE_LIST = [STATE_UNLOCKED, STATE_LOCKED, STATE_UNLOCKED] -STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.LOCK) +MULTI_MATCH = functools.partial(ZHA_ENTITIES.multipass_match, Platform.LOCK) VALUE_TO_STATE = dict(enumerate(STATE_LIST)) @@ -86,7 +86,7 @@ async def async_setup_entry( ) -@STRICT_MATCH(channel_names=CHANNEL_DOORLOCK) +@MULTI_MATCH(channel_names=CHANNEL_DOORLOCK) class ZhaDoorLock(ZhaEntity, LockEntity): """Representation of a ZHA lock.""" diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index eed85417b67..406bcbfa7aa 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -186,19 +186,24 @@ class Sensor(ZhaEntity, SensorEntity): return round(float(value * self._multiplier) / self._divisor) -@STRICT_MATCH( +@MULTI_MATCH( channel_names=CHANNEL_ANALOG_INPUT, manufacturers="LUMI", models={"lumi.plug", "lumi.plug.maus01", "lumi.plug.mmeu01"}, + stop_on_match_group=CHANNEL_ANALOG_INPUT, +) +@MULTI_MATCH( + channel_names=CHANNEL_ANALOG_INPUT, + manufacturers="Digi", + stop_on_match_group=CHANNEL_ANALOG_INPUT, ) -@STRICT_MATCH(channel_names=CHANNEL_ANALOG_INPUT, manufacturers="Digi") class AnalogInput(Sensor): """Sensor that displays analog input values.""" SENSOR_ATTR = "present_value" -@STRICT_MATCH(channel_names=CHANNEL_POWER_CONFIGURATION) +@MULTI_MATCH(channel_names=CHANNEL_POWER_CONFIGURATION) class Battery(Sensor): """Battery sensor of power configuration cluster.""" @@ -339,8 +344,10 @@ class ElectricalMeasurementRMSVoltage(ElectricalMeasurement, id_suffix="rms_volt return False -@STRICT_MATCH(generic_ids=CHANNEL_ST_HUMIDITY_CLUSTER) -@STRICT_MATCH(channel_names=CHANNEL_HUMIDITY) +@MULTI_MATCH( + generic_ids=CHANNEL_ST_HUMIDITY_CLUSTER, stop_on_match_group=CHANNEL_HUMIDITY +) +@MULTI_MATCH(channel_names=CHANNEL_HUMIDITY, stop_on_match_group=CHANNEL_HUMIDITY) class Humidity(Sensor): """Humidity sensor.""" @@ -351,7 +358,7 @@ class Humidity(Sensor): _unit = PERCENTAGE -@STRICT_MATCH(channel_names=CHANNEL_SOIL_MOISTURE) +@MULTI_MATCH(channel_names=CHANNEL_SOIL_MOISTURE) class SoilMoisture(Sensor): """Soil Moisture sensor.""" @@ -362,7 +369,7 @@ class SoilMoisture(Sensor): _unit = PERCENTAGE -@STRICT_MATCH(channel_names=CHANNEL_LEAF_WETNESS) +@MULTI_MATCH(channel_names=CHANNEL_LEAF_WETNESS) class LeafWetness(Sensor): """Leaf Wetness sensor.""" @@ -373,7 +380,7 @@ class LeafWetness(Sensor): _unit = PERCENTAGE -@STRICT_MATCH(channel_names=CHANNEL_ILLUMINANCE) +@MULTI_MATCH(channel_names=CHANNEL_ILLUMINANCE) class Illuminance(Sensor): """Illuminance Sensor.""" @@ -465,7 +472,7 @@ class SmartEnergySummation(SmartEnergyMetering, id_suffix="summation_delivered") return round(cooked, 3) -@STRICT_MATCH(channel_names=CHANNEL_PRESSURE) +@MULTI_MATCH(channel_names=CHANNEL_PRESSURE) class Pressure(Sensor): """Pressure sensor.""" @@ -476,7 +483,7 @@ class Pressure(Sensor): _unit = PRESSURE_HPA -@STRICT_MATCH(channel_names=CHANNEL_TEMPERATURE) +@MULTI_MATCH(channel_names=CHANNEL_TEMPERATURE) class Temperature(Sensor): """Temperature Sensor.""" @@ -487,7 +494,7 @@ class Temperature(Sensor): _unit = TEMP_CELSIUS -@STRICT_MATCH(channel_names="carbon_dioxide_concentration") +@MULTI_MATCH(channel_names="carbon_dioxide_concentration") class CarbonDioxideConcentration(Sensor): """Carbon Dioxide Concentration sensor.""" @@ -499,7 +506,7 @@ class CarbonDioxideConcentration(Sensor): _unit = CONCENTRATION_PARTS_PER_MILLION -@STRICT_MATCH(channel_names="carbon_monoxide_concentration") +@MULTI_MATCH(channel_names="carbon_monoxide_concentration") class CarbonMonoxideConcentration(Sensor): """Carbon Monoxide Concentration sensor.""" @@ -511,8 +518,8 @@ class CarbonMonoxideConcentration(Sensor): _unit = CONCENTRATION_PARTS_PER_MILLION -@STRICT_MATCH(generic_ids="channel_0x042e") -@STRICT_MATCH(channel_names="voc_level") +@MULTI_MATCH(generic_ids="channel_0x042e", stop_on_match_group="voc_level") +@MULTI_MATCH(channel_names="voc_level", stop_on_match_group="voc_level") class VOCLevel(Sensor): """VOC Level sensor.""" @@ -524,7 +531,11 @@ class VOCLevel(Sensor): _unit = CONCENTRATION_MICROGRAMS_PER_CUBIC_METER -@STRICT_MATCH(channel_names="voc_level", models="lumi.airmonitor.acn01") +@MULTI_MATCH( + channel_names="voc_level", + models="lumi.airmonitor.acn01", + stop_on_match_group="voc_level", +) class PPBVOCLevel(Sensor): """VOC Level sensor.""" @@ -536,7 +547,7 @@ class PPBVOCLevel(Sensor): _unit = CONCENTRATION_PARTS_PER_BILLION -@STRICT_MATCH(channel_names="formaldehyde_concentration") +@MULTI_MATCH(channel_names="formaldehyde_concentration") class FormaldehydeConcentration(Sensor): """Formaldehyde Concentration sensor.""" @@ -547,7 +558,7 @@ class FormaldehydeConcentration(Sensor): _unit = CONCENTRATION_PARTS_PER_MILLION -@MULTI_MATCH(channel_names=CHANNEL_THERMOSTAT) +@MULTI_MATCH(channel_names=CHANNEL_THERMOSTAT, stop_on_match_group=CHANNEL_THERMOSTAT) class ThermostatHVACAction(Sensor, id_suffix="hvac_action"): """Thermostat HVAC action sensor.""" @@ -626,12 +637,12 @@ class ThermostatHVACAction(Sensor, id_suffix="hvac_action"): aux_channels=CHANNEL_FAN, manufacturers="Centralite", models={"3157100", "3157100-E"}, - stop_on_match=True, + stop_on_match_group=CHANNEL_THERMOSTAT, ) @MULTI_MATCH( channel_names=CHANNEL_THERMOSTAT, manufacturers="Zen Within", - stop_on_match=True, + stop_on_match_group=CHANNEL_THERMOSTAT, ) class ZenHVACAction(ThermostatHVACAction): """Zen Within Thermostat HVAC Action.""" diff --git a/tests/components/zha/test_discover.py b/tests/components/zha/test_discover.py index 6b34590f4ad..9480f4b1e65 100644 --- a/tests/components/zha/test_discover.py +++ b/tests/components/zha/test_discover.py @@ -367,7 +367,6 @@ def _test_single_input_cluster_device_class(probe_mock): cover_ch, multistate_ch, ias_ch, - analog_ch, ] disc.ProbeEndpoint().discover_by_cluster_id(ch_pool) @@ -385,11 +384,6 @@ def _test_single_input_cluster_device_class(probe_mock): assert call[0][1] == ch -def test_single_input_cluster_device_class(): - """Test SINGLE_INPUT_CLUSTER_DEVICE_CLASS matching by cluster id or class.""" - _test_single_input_cluster_device_class() - - def test_single_input_cluster_device_class_by_cluster_class(): """Test SINGLE_INPUT_CLUSTER_DEVICE_CLASS matching by cluster id or class.""" mock_reg = { From a691abaa501e5b63c03561a11e327582cdd98c4c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 12 Dec 2021 15:27:20 +0100 Subject: [PATCH 0331/2644] Use new enums in gogogate2 (#61515) Co-authored-by: epenet --- homeassistant/components/gogogate2/cover.py | 5 ++-- homeassistant/components/gogogate2/sensor.py | 25 ++++++++++---------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/gogogate2/cover.py b/homeassistant/components/gogogate2/cover.py index 16191304bb9..155c767eaea 100644 --- a/homeassistant/components/gogogate2/cover.py +++ b/homeassistant/components/gogogate2/cover.py @@ -9,10 +9,9 @@ from ismartgate.common import ( ) from homeassistant.components.cover import ( - DEVICE_CLASS_GARAGE, - DEVICE_CLASS_GATE, SUPPORT_CLOSE, SUPPORT_OPEN, + CoverDeviceClass, CoverEntity, ) from homeassistant.config_entries import ConfigEntry @@ -57,7 +56,7 @@ class DeviceCover(GoGoGate2Entity, CoverEntity): super().__init__(config_entry, data_update_coordinator, door, unique_id) self._attr_supported_features = SUPPORT_OPEN | SUPPORT_CLOSE self._attr_device_class = ( - DEVICE_CLASS_GATE if self.door.gate else DEVICE_CLASS_GARAGE + CoverDeviceClass.GATE if self.door.gate else CoverDeviceClass.GARAGE ) @property diff --git a/homeassistant/components/gogogate2/sensor.py b/homeassistant/components/gogogate2/sensor.py index 743c8e7efc7..29c8f360963 100644 --- a/homeassistant/components/gogogate2/sensor.py +++ b/homeassistant/components/gogogate2/sensor.py @@ -5,16 +5,15 @@ from itertools import chain from ismartgate.common import AbstractDoor, get_configured_doors -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_TEMPERATURE, - ENTITY_CATEGORY_DIAGNOSTIC, - PERCENTAGE, - TEMP_CELSIUS, +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorStateClass, ) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE, TEMP_CELSIUS from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .common import ( @@ -66,7 +65,7 @@ class DoorSensorEntity(GoGoGate2Entity, SensorEntity): class DoorSensorBattery(DoorSensorEntity): """Battery sensor entity for gogogate2 door sensor.""" - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_entity_category = EntityCategory.DIAGNOSTIC def __init__( self, @@ -77,8 +76,8 @@ class DoorSensorBattery(DoorSensorEntity): """Initialize the object.""" unique_id = sensor_unique_id(config_entry, door, "battery") super().__init__(config_entry, data_update_coordinator, door, unique_id) - self._attr_device_class = DEVICE_CLASS_BATTERY - self._attr_state_class = STATE_CLASS_MEASUREMENT + self._attr_device_class = SensorDeviceClass.BATTERY + self._attr_state_class = SensorStateClass.MEASUREMENT self._attr_native_unit_of_measurement = PERCENTAGE @property @@ -104,8 +103,8 @@ class DoorSensorTemperature(DoorSensorEntity): """Initialize the object.""" unique_id = sensor_unique_id(config_entry, door, "temperature") super().__init__(config_entry, data_update_coordinator, door, unique_id) - self._attr_device_class = DEVICE_CLASS_TEMPERATURE - self._attr_state_class = STATE_CLASS_MEASUREMENT + self._attr_device_class = SensorDeviceClass.TEMPERATURE + self._attr_state_class = SensorStateClass.MEASUREMENT self._attr_native_unit_of_measurement = TEMP_CELSIUS @property From 7711f9a39117af5cd15d81e0fc3582713df95c23 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 12 Dec 2021 07:14:44 -0800 Subject: [PATCH 0332/2644] Use relative import within component for nest media source (#61571) --- homeassistant/components/nest/media_source.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/media_source.py b/homeassistant/components/nest/media_source.py index b02b9b1870e..098d7d01b0f 100644 --- a/homeassistant/components/nest/media_source.py +++ b/homeassistant/components/nest/media_source.py @@ -41,13 +41,14 @@ from homeassistant.components.media_source.models import ( MediaSourceItem, PlayMedia, ) -from homeassistant.components.nest.const import DATA_SUBSCRIBER, DOMAIN -from homeassistant.components.nest.device_info import NestDeviceInfo -from homeassistant.components.nest.events import MEDIA_SOURCE_EVENT_TITLE_MAP from homeassistant.core import HomeAssistant from homeassistant.helpers.template import DATE_STR_FORMAT from homeassistant.util import dt as dt_util +from .const import DATA_SUBSCRIBER, DOMAIN +from .device_info import NestDeviceInfo +from .events import MEDIA_SOURCE_EVENT_TITLE_MAP + _LOGGER = logging.getLogger(__name__) MEDIA_SOURCE_TITLE = "Nest" From 94324cebea425bf14004cf0a0bcf59c466fadd83 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sun, 12 Dec 2021 12:11:37 -0500 Subject: [PATCH 0333/2644] Update HVAC action handling in ZHA climate devices (#61460) * Update HVAC action handling in ZHA climate devices * fix class name * align with class name changes * get the correct sensor entity for state assertions --- homeassistant/components/zha/climate.py | 140 ++++++++++------------- homeassistant/components/zha/sensor.py | 118 +++++++++---------- tests/components/zha/common.py | 9 +- tests/components/zha/test_climate.py | 12 +- tests/components/zha/zha_devices_list.py | 6 +- 5 files changed, 132 insertions(+), 153 deletions(-) diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index 3473a6d8f9e..c6ad3dcc89e 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -7,10 +7,11 @@ at https://home-assistant.io/components/zha.climate/ from __future__ import annotations from datetime import datetime, timedelta -import enum import functools from random import randint +from zigpy.zcl.clusters.hvac import Fan as F, Thermostat as T + from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( ATTR_HVAC_MODE, @@ -82,27 +83,6 @@ STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.CLIMATE) MULTI_MATCH = functools.partial(ZHA_ENTITIES.multipass_match, Platform.CLIMATE) RUNNING_MODE = {0x00: HVAC_MODE_OFF, 0x03: HVAC_MODE_COOL, 0x04: HVAC_MODE_HEAT} - -class ThermostatFanMode(enum.IntEnum): - """Fan channel enum for thermostat Fans.""" - - OFF = 0x00 - ON = 0x04 - AUTO = 0x05 - - -class RunningState(enum.IntFlag): - """ZCL Running state enum.""" - - HEAT = 0x0001 - COOL = 0x0002 - FAN = 0x0004 - HEAT_STAGE_2 = 0x0008 - COOL_STAGE_2 = 0x0010 - FAN_STAGE_2 = 0x0020 - FAN_STAGE_3 = 0x0040 - - SEQ_OF_OPERATION = { 0x00: (HVAC_MODE_OFF, HVAC_MODE_COOL), # cooling only 0x01: (HVAC_MODE_OFF, HVAC_MODE_COOL), # cooling with reheat @@ -116,40 +96,25 @@ SEQ_OF_OPERATION = { 0x07: (HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF), # centralite specific } - -class SystemMode(enum.IntEnum): - """ZCL System Mode attribute enum.""" - - OFF = 0x00 - HEAT_COOL = 0x01 - COOL = 0x03 - HEAT = 0x04 - AUX_HEAT = 0x05 - PRE_COOL = 0x06 - FAN_ONLY = 0x07 - DRY = 0x08 - SLEEP = 0x09 - - HVAC_MODE_2_SYSTEM = { - HVAC_MODE_OFF: SystemMode.OFF, - HVAC_MODE_HEAT_COOL: SystemMode.HEAT_COOL, - HVAC_MODE_COOL: SystemMode.COOL, - HVAC_MODE_HEAT: SystemMode.HEAT, - HVAC_MODE_FAN_ONLY: SystemMode.FAN_ONLY, - HVAC_MODE_DRY: SystemMode.DRY, + HVAC_MODE_OFF: T.SystemMode.Off, + HVAC_MODE_HEAT_COOL: T.SystemMode.Auto, + HVAC_MODE_COOL: T.SystemMode.Cool, + HVAC_MODE_HEAT: T.SystemMode.Heat, + HVAC_MODE_FAN_ONLY: T.SystemMode.Fan_only, + HVAC_MODE_DRY: T.SystemMode.Dry, } SYSTEM_MODE_2_HVAC = { - SystemMode.OFF: HVAC_MODE_OFF, - SystemMode.HEAT_COOL: HVAC_MODE_HEAT_COOL, - SystemMode.COOL: HVAC_MODE_COOL, - SystemMode.HEAT: HVAC_MODE_HEAT, - SystemMode.AUX_HEAT: HVAC_MODE_HEAT, - SystemMode.PRE_COOL: HVAC_MODE_COOL, # this is 'precooling'. is it the same? - SystemMode.FAN_ONLY: HVAC_MODE_FAN_ONLY, - SystemMode.DRY: HVAC_MODE_DRY, - SystemMode.SLEEP: HVAC_MODE_OFF, + T.SystemMode.Off: HVAC_MODE_OFF, + T.SystemMode.Auto: HVAC_MODE_HEAT_COOL, + T.SystemMode.Cool: HVAC_MODE_COOL, + T.SystemMode.Heat: HVAC_MODE_HEAT, + T.SystemMode.Emergency_Heating: HVAC_MODE_HEAT, + T.SystemMode.Pre_cooling: HVAC_MODE_COOL, # this is 'precooling'. is it the same? + T.SystemMode.Fan_only: HVAC_MODE_FAN_ONLY, + T.SystemMode.Dry: HVAC_MODE_DRY, + T.SystemMode.Sleep: HVAC_MODE_OFF, } ZCL_TEMP = 100 @@ -233,7 +198,9 @@ class Thermostat(ZhaEntity, ClimateEntity): return FAN_AUTO if self._thrm.running_state & ( - RunningState.FAN | RunningState.FAN_STAGE_2 | RunningState.FAN_STAGE_3 + T.RunningState.Fan_State_On + | T.RunningState.Fan_2nd_Stage_On + | T.RunningState.Fan_3rd_Stage_On ): return FAN_ON return FAN_AUTO @@ -259,18 +226,25 @@ class Thermostat(ZhaEntity, ClimateEntity): def _rm_rs_action(self) -> str | None: """Return the current HVAC action based on running mode and running state.""" - running_mode = self._thrm.running_mode - if running_mode == SystemMode.HEAT: + if (running_state := self._thrm.running_state) is None: + return None + if running_state & ( + T.RunningState.Heat_State_On | T.RunningState.Heat_2nd_Stage_On + ): return CURRENT_HVAC_HEAT - if running_mode == SystemMode.COOL: + if running_state & ( + T.RunningState.Cool_State_On | T.RunningState.Cool_2nd_Stage_On + ): return CURRENT_HVAC_COOL - - running_state = self._thrm.running_state - if running_state and running_state & ( - RunningState.FAN | RunningState.FAN_STAGE_2 | RunningState.FAN_STAGE_3 + if running_state & ( + T.RunningState.Fan_State_On + | T.RunningState.Fan_2nd_Stage_On + | T.RunningState.Fan_3rd_Stage_On ): return CURRENT_HVAC_FAN - if self.hvac_mode != HVAC_MODE_OFF and running_mode == SystemMode.OFF: + if running_state & T.RunningState.Idle: + return CURRENT_HVAC_IDLE + if self.hvac_mode != HVAC_MODE_OFF: return CURRENT_HVAC_IDLE return CURRENT_HVAC_OFF @@ -431,9 +405,9 @@ class Thermostat(ZhaEntity, ClimateEntity): return if fan_mode == FAN_ON: - mode = ThermostatFanMode.ON + mode = F.FanMode.On else: - mode = ThermostatFanMode.AUTO + mode = F.FanMode.Auto await self._fan.async_set_speed(mode) @@ -545,6 +519,27 @@ class SinopeTechnologiesThermostat(Thermostat): self._supported_flags |= SUPPORT_PRESET_MODE self._manufacturer_ch = self.cluster_channels["sinope_manufacturer_specific"] + @property + def _rm_rs_action(self) -> str | None: + """Return the current HVAC action based on running mode and running state.""" + + running_mode = self._thrm.running_mode + if running_mode == T.SystemMode.Heat: + return CURRENT_HVAC_HEAT + if running_mode == T.SystemMode.Cool: + return CURRENT_HVAC_COOL + + running_state = self._thrm.running_state + if running_state and running_state & ( + T.RunningState.Fan_State_On + | T.RunningState.Fan_2nd_Stage_On + | T.RunningState.Fan_3rd_Stage_On + ): + return CURRENT_HVAC_FAN + if self.hvac_mode != HVAC_MODE_OFF and running_mode == T.SystemMode.Off: + return CURRENT_HVAC_IDLE + return CURRENT_HVAC_OFF + @callback def _async_update_time(self, timestamp=None) -> None: """Update thermostat's time display.""" @@ -588,25 +583,6 @@ class SinopeTechnologiesThermostat(Thermostat): class ZenWithinThermostat(Thermostat): """Zen Within Thermostat implementation.""" - @property - def _rm_rs_action(self) -> str | None: - """Return the current HVAC action based on running mode and running state.""" - - if (running_state := self._thrm.running_state) is None: - return None - if running_state & (RunningState.HEAT | RunningState.HEAT_STAGE_2): - return CURRENT_HVAC_HEAT - if running_state & (RunningState.COOL | RunningState.COOL_STAGE_2): - return CURRENT_HVAC_COOL - if running_state & ( - RunningState.FAN | RunningState.FAN_STAGE_2 | RunningState.FAN_STAGE_3 - ): - return CURRENT_HVAC_FAN - - if self.hvac_mode != HVAC_MODE_OFF: - return CURRENT_HVAC_IDLE - return CURRENT_HVAC_OFF - @MULTI_MATCH( channel_names=CHANNEL_THERMOSTAT, diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 406bcbfa7aa..8a4301b12fb 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -51,7 +51,6 @@ from .core import discovery from .core.const import ( CHANNEL_ANALOG_INPUT, CHANNEL_ELECTRICAL_MEASUREMENT, - CHANNEL_FAN, CHANNEL_HUMIDITY, CHANNEL_ILLUMINANCE, CHANNEL_LEAF_WETNESS, @@ -587,66 +586,6 @@ class ThermostatHVACAction(Sensor, id_suffix="hvac_action"): return self._rm_rs_action return self._pi_demand_action - @property - def _rm_rs_action(self) -> str | None: - """Return the current HVAC action based on running mode and running state.""" - - running_mode = self._channel.running_mode - if running_mode == self._channel.RunningMode.Heat: - return CURRENT_HVAC_HEAT - if running_mode == self._channel.RunningMode.Cool: - return CURRENT_HVAC_COOL - - running_state = self._channel.running_state - if running_state and running_state & ( - self._channel.RunningState.Fan_State_On - | self._channel.RunningState.Fan_2nd_Stage_On - | self._channel.RunningState.Fan_3rd_Stage_On - ): - return CURRENT_HVAC_FAN - if ( - self._channel.system_mode != self._channel.SystemMode.Off - and running_mode == self._channel.SystemMode.Off - ): - return CURRENT_HVAC_IDLE - return CURRENT_HVAC_OFF - - @property - def _pi_demand_action(self) -> str | None: - """Return the current HVAC action based on pi_demands.""" - - heating_demand = self._channel.pi_heating_demand - if heating_demand is not None and heating_demand > 0: - return CURRENT_HVAC_HEAT - cooling_demand = self._channel.pi_cooling_demand - if cooling_demand is not None and cooling_demand > 0: - return CURRENT_HVAC_COOL - - if self._channel.system_mode != self._channel.SystemMode.Off: - return CURRENT_HVAC_IDLE - return CURRENT_HVAC_OFF - - @callback - def async_set_state(self, *args, **kwargs) -> None: - """Handle state update from channel.""" - self.async_write_ha_state() - - -@MULTI_MATCH( - channel_names=CHANNEL_THERMOSTAT, - aux_channels=CHANNEL_FAN, - manufacturers="Centralite", - models={"3157100", "3157100-E"}, - stop_on_match_group=CHANNEL_THERMOSTAT, -) -@MULTI_MATCH( - channel_names=CHANNEL_THERMOSTAT, - manufacturers="Zen Within", - stop_on_match_group=CHANNEL_THERMOSTAT, -) -class ZenHVACAction(ThermostatHVACAction): - """Zen Within Thermostat HVAC Action.""" - @property def _rm_rs_action(self) -> str | None: """Return the current HVAC action based on running mode and running state.""" @@ -676,6 +615,63 @@ class ZenHVACAction(ThermostatHVACAction): ): return CURRENT_HVAC_FAN + running_state = self._channel.running_state + if running_state and running_state & self._channel.RunningState.Idle: + return CURRENT_HVAC_IDLE + if self._channel.system_mode != self._channel.SystemMode.Off: return CURRENT_HVAC_IDLE return CURRENT_HVAC_OFF + + @property + def _pi_demand_action(self) -> str | None: + """Return the current HVAC action based on pi_demands.""" + + heating_demand = self._channel.pi_heating_demand + if heating_demand is not None and heating_demand > 0: + return CURRENT_HVAC_HEAT + cooling_demand = self._channel.pi_cooling_demand + if cooling_demand is not None and cooling_demand > 0: + return CURRENT_HVAC_COOL + + if self._channel.system_mode != self._channel.SystemMode.Off: + return CURRENT_HVAC_IDLE + return CURRENT_HVAC_OFF + + @callback + def async_set_state(self, *args, **kwargs) -> None: + """Handle state update from channel.""" + self.async_write_ha_state() + + +@MULTI_MATCH( + channel_names={CHANNEL_THERMOSTAT}, + manufacturers="Sinope Technologies", + stop_on_match_group=CHANNEL_THERMOSTAT, +) +class SinopeHVACAction(ThermostatHVACAction): + """Sinope Thermostat HVAC action sensor.""" + + @property + def _rm_rs_action(self) -> str | None: + """Return the current HVAC action based on running mode and running state.""" + + running_mode = self._channel.running_mode + if running_mode == self._channel.RunningMode.Heat: + return CURRENT_HVAC_HEAT + if running_mode == self._channel.RunningMode.Cool: + return CURRENT_HVAC_COOL + + running_state = self._channel.running_state + if running_state and running_state & ( + self._channel.RunningState.Fan_State_On + | self._channel.RunningState.Fan_2nd_Stage_On + | self._channel.RunningState.Fan_3rd_Stage_On + ): + return CURRENT_HVAC_FAN + if ( + self._channel.system_mode != self._channel.SystemMode.Off + and running_mode == self._channel.SystemMode.Off + ): + return CURRENT_HVAC_IDLE + return CURRENT_HVAC_OFF diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index b302869d9e4..48772d31fb6 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -106,7 +106,7 @@ async def send_attributes_report(hass, cluster: zigpy.zcl.Cluster, attributes: d await hass.async_block_till_done() -async def find_entity_id(domain, zha_device, hass): +async def find_entity_id(domain, zha_device, hass, qualifier=None): """Find the entity id under the testing. This is used to get the entity id in order to get the state from the state @@ -115,7 +115,12 @@ async def find_entity_id(domain, zha_device, hass): entities = await find_entity_ids(domain, zha_device, hass) if not entities: return None - return entities[0] + if qualifier: + for entity_id in entities: + if qualifier in entity_id: + return entity_id + else: + return entities[0] async def find_entity_ids(domain, zha_device, hass): diff --git a/tests/components/zha/test_climate.py b/tests/components/zha/test_climate.py index 58d5240aad7..4dc72b092e4 100644 --- a/tests/components/zha/test_climate.py +++ b/tests/components/zha/test_climate.py @@ -254,12 +254,14 @@ async def test_climate_local_temp(hass, device_climate): assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 21.0 -async def test_climate_hvac_action_running_state(hass, device_climate): +async def test_climate_hvac_action_running_state(hass, device_climate_sinope): """Test hvac action via running state.""" - thrm_cluster = device_climate.device.endpoints[1].thermostat - entity_id = await find_entity_id(Platform.CLIMATE, device_climate, hass) - sensor_entity_id = await find_entity_id(Platform.SENSOR, device_climate, hass) + thrm_cluster = device_climate_sinope.device.endpoints[1].thermostat + entity_id = await find_entity_id(Platform.CLIMATE, device_climate_sinope, hass) + sensor_entity_id = await find_entity_id( + Platform.SENSOR, device_climate_sinope, hass, "hvac" + ) state = hass.states.get(entity_id) assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF @@ -407,7 +409,7 @@ async def test_climate_hvac_action_pi_demand(hass, device_climate): entity_id = await find_entity_id(Platform.CLIMATE, device_climate, hass) state = hass.states.get(entity_id) - assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + assert ATTR_HVAC_ACTION not in state.attributes await send_attributes_report(hass, thrm_cluster, {0x0007: 10}) state = hass.states.get(entity_id) diff --git a/tests/components/zha/zha_devices_list.py b/tests/components/zha/zha_devices_list.py index 06d3f10556c..54f9a35610b 100644 --- a/tests/components/zha/zha_devices_list.py +++ b/tests/components/zha/zha_devices_list.py @@ -3267,7 +3267,7 @@ DEVICES = [ }, ("sensor", "00:11:22:33:44:55:66:77-1-513-hvac_action"): { DEV_SIG_CHANNELS: ["thermostat"], - DEV_SIG_ENT_MAP_CLASS: "ThermostatHVACAction", + DEV_SIG_ENT_MAP_CLASS: "SinopeHVACAction", DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_thermostat_hvac_action", }, }, @@ -3312,7 +3312,7 @@ DEVICES = [ }, ("sensor", "00:11:22:33:44:55:66:77-1-513-hvac_action"): { DEV_SIG_CHANNELS: ["thermostat"], - DEV_SIG_ENT_MAP_CLASS: "ThermostatHVACAction", + DEV_SIG_ENT_MAP_CLASS: "SinopeHVACAction", DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_thermostat_hvac_action", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { @@ -3557,7 +3557,7 @@ DEVICES = [ }, ("sensor", "00:11:22:33:44:55:66:77-1-513-hvac_action"): { DEV_SIG_CHANNELS: ["thermostat"], - DEV_SIG_ENT_MAP_CLASS: "ZenHVACAction", + DEV_SIG_ENT_MAP_CLASS: "ThermostatHVACAction", DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_77665544_thermostat_hvac_action", }, }, From 0194f0a06eadc5d873d5b852644a783462c5dbf1 Mon Sep 17 00:00:00 2001 From: Austin Mroczek Date: Sun, 12 Dec 2021 10:00:28 -0800 Subject: [PATCH 0334/2644] Fix totalconnect service schema (#61595) --- homeassistant/components/totalconnect/alarm_control_panel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/totalconnect/alarm_control_panel.py b/homeassistant/components/totalconnect/alarm_control_panel.py index c7cd6cb939a..e4436e5c041 100644 --- a/homeassistant/components/totalconnect/alarm_control_panel.py +++ b/homeassistant/components/totalconnect/alarm_control_panel.py @@ -57,13 +57,13 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: platform.async_register_entity_service( SERVICE_ALARM_ARM_AWAY_INSTANT, - None, + {}, "async_alarm_arm_away_instant", ) platform.async_register_entity_service( SERVICE_ALARM_ARM_HOME_INSTANT, - None, + {}, "async_alarm_arm_home_instant", ) From 599d5c4c411d21861270307d72aac24a73990fcd Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sun, 12 Dec 2021 19:12:49 +0100 Subject: [PATCH 0335/2644] enable grouped light if enabled in previous integration (#61582) --- homeassistant/components/hue/v2/group.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/v2/group.py b/homeassistant/components/hue/v2/group.py index 312fef6629f..010f7ab3382 100644 --- a/homeassistant/components/hue/v2/group.py +++ b/homeassistant/components/hue/v2/group.py @@ -24,7 +24,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from ..bridge import HueBridge -from ..const import DOMAIN +from ..const import CONF_ALLOW_HUE_GROUPS, DOMAIN from .entity import HueBaseEntity ALLOWED_ERRORS = [ @@ -76,8 +76,6 @@ async def async_setup_entry( class GroupedHueLight(HueBaseEntity, LightEntity): """Representation of a Grouped Hue light.""" - # Entities for Hue groups are disabled by default - _attr_entity_registry_enabled_default = False _attr_icon = "mdi:lightbulb-group" def __init__( @@ -92,6 +90,12 @@ class GroupedHueLight(HueBaseEntity, LightEntity): self.api: HueBridgeV2 = bridge.api self._attr_supported_features |= SUPPORT_TRANSITION + # Entities for Hue groups are disabled by default + # unless they were enabled in old version (legacy option) + self._attr_entity_registry_enabled_default = bridge.config_entry.data.get( + CONF_ALLOW_HUE_GROUPS, False + ) + self._update_values() async def async_added_to_hass(self) -> None: From aff74f796906a0a67f1ab02f54b2241bf924abb8 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sun, 12 Dec 2021 19:24:32 +0100 Subject: [PATCH 0336/2644] Update frontend to 20211212.0 (#61577) --- homeassistant/components/frontend/manifest.json | 10 +++++++--- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 994ac596527..4380440408f 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,9 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20211211.0"], + "requirements": [ + "home-assistant-frontend==20211212.0" + ], "dependencies": [ "api", "auth", @@ -15,6 +17,8 @@ "system_log", "websocket_api" ], - "codeowners": ["@home-assistant/frontend"], + "codeowners": [ + "@home-assistant/frontend" + ], "quality_scale": "internal" -} +} \ No newline at end of file diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a22fbb871a2..56fbc02d690 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ ciso8601==2.2.0 cryptography==35.0.0 emoji==1.5.0 hass-nabucasa==0.50.0 -home-assistant-frontend==20211211.0 +home-assistant-frontend==20211212.0 httpx==0.21.0 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index 38505b5fd1e..5d09fdacd27 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -828,7 +828,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211211.0 +home-assistant-frontend==20211212.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 32179d7d98c..7c368e7b3fd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -524,7 +524,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211211.0 +home-assistant-frontend==20211212.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 6e7de8f85d25a4f5cbdbe34e6eb99ed0998cbac7 Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Sun, 12 Dec 2021 23:09:15 +0100 Subject: [PATCH 0337/2644] Fix for failing Solarlog integration in HA 2021.12 (#61602) --- homeassistant/components/solarlog/const.py | 63 ++++++++++----------- homeassistant/components/solarlog/sensor.py | 18 ++++-- 2 files changed, 41 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/solarlog/const.py b/homeassistant/components/solarlog/const.py index 0e9e5e8e5e0..3159bc46218 100644 --- a/homeassistant/components/solarlog/const.py +++ b/homeassistant/components/solarlog/const.py @@ -4,16 +4,11 @@ from __future__ import annotations from dataclasses import dataclass from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL, + SensorDeviceClass, SensorEntityDescription, + SensorStateClass, ) from homeassistant.const import ( - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_POWER_FACTOR, - DEVICE_CLASS_TIMESTAMP, - DEVICE_CLASS_VOLTAGE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, PERCENTAGE, @@ -38,35 +33,35 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( SolarLogSensorEntityDescription( key="time", name="last update", - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, ), SolarLogSensorEntityDescription( key="power_ac", name="power AC", icon="mdi:solar-power", native_unit_of_measurement=POWER_WATT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SolarLogSensorEntityDescription( key="power_dc", name="power DC", icon="mdi:solar-power", native_unit_of_measurement=POWER_WATT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SolarLogSensorEntityDescription( key="voltage_ac", name="voltage AC", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, ), SolarLogSensorEntityDescription( key="voltage_dc", name="voltage DC", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, ), SolarLogSensorEntityDescription( key="yield_day", @@ -101,50 +96,50 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( name="yield total", icon="mdi:solar-power", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_TOTAL, + state_class=SensorStateClass.TOTAL, factor=0.001, ), SolarLogSensorEntityDescription( key="consumption_ac", name="consumption AC", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), SolarLogSensorEntityDescription( key="consumption_day", name="consumption day", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, factor=0.001, ), SolarLogSensorEntityDescription( key="consumption_yesterday", name="consumption yesterday", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, factor=0.001, ), SolarLogSensorEntityDescription( key="consumption_month", name="consumption month", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, factor=0.001, ), SolarLogSensorEntityDescription( key="consumption_year", name="consumption year", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, factor=0.001, ), SolarLogSensorEntityDescription( key="consumption_total", name="consumption total", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, factor=0.001, ), SolarLogSensorEntityDescription( @@ -152,31 +147,31 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( name="installed peak power", icon="mdi:solar-power", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, ), SolarLogSensorEntityDescription( key="alternator_loss", name="alternator loss", icon="mdi:solar-power", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), SolarLogSensorEntityDescription( key="capacity", name="capacity", icon="mdi:solar-power", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_POWER_FACTOR, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER_FACTOR, + state_class=SensorStateClass.MEASUREMENT, factor=100, ), SolarLogSensorEntityDescription( key="efficiency", name="efficiency", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_POWER_FACTOR, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER_FACTOR, + state_class=SensorStateClass.MEASUREMENT, factor=100, ), SolarLogSensorEntityDescription( @@ -184,15 +179,15 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( name="power available", icon="mdi:solar-power", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), SolarLogSensorEntityDescription( key="usage", name="usage", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_POWER_FACTOR, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER_FACTOR, + state_class=SensorStateClass.MEASUREMENT, factor=100, ), ) diff --git a/homeassistant/components/solarlog/sensor.py b/homeassistant/components/solarlog/sensor.py index 5d79efb94c9..5c4c2bfad28 100644 --- a/homeassistant/components/solarlog/sensor.py +++ b/homeassistant/components/solarlog/sensor.py @@ -1,7 +1,8 @@ """Platform for solarlog sensors.""" from homeassistant.components.sensor import SensorEntity from homeassistant.helpers import update_coordinator -from homeassistant.helpers.entity import DeviceInfo, StateType +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.util.dt import as_local from . import SolarlogData from .const import DOMAIN, SENSOR_TYPES, SolarLogSensorEntityDescription @@ -38,11 +39,16 @@ class SolarlogSensor(update_coordinator.CoordinatorEntity, SensorEntity): ) @property - def native_value(self) -> StateType: + def native_value(self): """Return the native sensor value.""" - result = getattr(self.coordinator.data, self.entity_description.key) - if self.entity_description.factor: - state = round(result * self.entity_description.factor, 3) + if self.entity_description.key == "time": + state = as_local( + getattr(self.coordinator.data, self.entity_description.key) + ) else: - state = result + result = getattr(self.coordinator.data, self.entity_description.key) + if self.entity_description.factor: + state = round(result * self.entity_description.factor, 3) + else: + state = result return state From cbe58913eaf6e5240b57f5a51c21919ccabcdd0f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 12 Dec 2021 23:09:45 +0100 Subject: [PATCH 0338/2644] Fix freedompro tests (#61012) --- tests/components/freedompro/conftest.py | 9 +++ .../freedompro/test_binary_sensor.py | 24 +++---- tests/components/freedompro/test_climate.py | 23 ++++--- tests/components/freedompro/test_cover.py | 56 +++++++++++---- tests/components/freedompro/test_fan.py | 69 +++++++++++++++---- tests/components/freedompro/test_lock.py | 44 +++++++++--- tests/components/freedompro/test_sensor.py | 20 +++--- tests/components/freedompro/test_switch.py | 45 +++++++++--- 8 files changed, 212 insertions(+), 78 deletions(-) diff --git a/tests/components/freedompro/conftest.py b/tests/components/freedompro/conftest.py index 36070c1a0d5..804dc6d1933 100644 --- a/tests/components/freedompro/conftest.py +++ b/tests/components/freedompro/conftest.py @@ -1,4 +1,8 @@ """Fixtures for Freedompro integration tests.""" +from __future__ import annotations + +from copy import deepcopy +from typing import Any from unittest.mock import patch import pytest @@ -71,3 +75,8 @@ async def init_integration_no_state(hass) -> MockConfigEntry: await hass.async_block_till_done() return entry + + +def get_states_response_for_uid(uid: str) -> list[dict[str, Any]]: + """Return a deepcopy of the device state list for specific uid.""" + return deepcopy([resp for resp in DEVICES_STATE if resp["uid"] == uid]) diff --git a/tests/components/freedompro/test_binary_sensor.py b/tests/components/freedompro/test_binary_sensor.py index 785a6b03212..459484dbc45 100644 --- a/tests/components/freedompro/test_binary_sensor.py +++ b/tests/components/freedompro/test_binary_sensor.py @@ -9,7 +9,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.util.dt import utcnow from tests.common import async_fire_time_changed -from tests.components.freedompro.const import DEVICES_STATE +from tests.components.freedompro.conftest import get_states_response_for_uid @pytest.mark.parametrize( @@ -84,20 +84,18 @@ async def test_binary_sensor_get_state( assert state.state == STATE_OFF - get_states_response = list(DEVICES_STATE) - for state_response in get_states_response: - if state_response["uid"] == uid: - if state_response["type"] == "smokeSensor": - state_response["state"]["smokeDetected"] = True - if state_response["type"] == "occupancySensor": - state_response["state"]["occupancyDetected"] = True - if state_response["type"] == "motionSensor": - state_response["state"]["motionDetected"] = True - if state_response["type"] == "contactSensor": - state_response["state"]["contactSensorState"] = True + states_response = get_states_response_for_uid(uid) + if states_response[0]["type"] == "smokeSensor": + states_response[0]["state"]["smokeDetected"] = True + elif states_response[0]["type"] == "occupancySensor": + states_response[0]["state"]["occupancyDetected"] = True + elif states_response[0]["type"] == "motionSensor": + states_response[0]["state"]["motionDetected"] = True + elif states_response[0]["type"] == "contactSensor": + states_response[0]["state"]["contactSensorState"] = True with patch( "homeassistant.components.freedompro.get_states", - return_value=get_states_response, + return_value=states_response, ): async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) diff --git a/tests/components/freedompro/test_climate.py b/tests/components/freedompro/test_climate.py index 36ec3309d24..95c13a46766 100644 --- a/tests/components/freedompro/test_climate.py +++ b/tests/components/freedompro/test_climate.py @@ -25,7 +25,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.util.dt import utcnow from tests.common import async_fire_time_changed -from tests.components.freedompro.const import DEVICES_STATE +from tests.components.freedompro.conftest import get_states_response_for_uid uid = "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*TWMYQKL3UVED4HSIIB9GXJWJZBQCXG-9VE-N2IUAIWI" @@ -64,14 +64,12 @@ async def test_climate_get_state(hass, init_integration): assert entry assert entry.unique_id == uid - get_states_response = list(DEVICES_STATE) - for state_response in get_states_response: - if state_response["uid"] == uid: - state_response["state"]["currentTemperature"] = 20 - state_response["state"]["targetTemperature"] = 21 + states_response = get_states_response_for_uid(uid) + states_response[0]["state"]["currentTemperature"] = 20 + states_response[0]["state"]["targetTemperature"] = 21 with patch( "homeassistant.components.freedompro.get_states", - return_value=get_states_response, + return_value=states_response, ): async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) await hass.async_block_till_done() @@ -172,7 +170,16 @@ async def test_climate_set_temperature(hass, init_integration): ANY, ANY, ANY, '{"heatingCoolingState": 0, "targetTemperature": 25.0}' ) - await hass.async_block_till_done() + states_response = get_states_response_for_uid(uid) + states_response[0]["state"]["currentTemperature"] = 20 + states_response[0]["state"]["targetTemperature"] = 21 + with patch( + "homeassistant.components.freedompro.get_states", + return_value=states_response, + ): + async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) + await hass.async_block_till_done() + state = hass.states.get(entity_id) assert state.attributes[ATTR_TEMPERATURE] == 21 diff --git a/tests/components/freedompro/test_cover.py b/tests/components/freedompro/test_cover.py index d0338dec82c..11dc35b374c 100644 --- a/tests/components/freedompro/test_cover.py +++ b/tests/components/freedompro/test_cover.py @@ -17,7 +17,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.util.dt import utcnow from tests.common import async_fire_time_changed -from tests.components.freedompro.const import DEVICES_STATE +from tests.components.freedompro.conftest import get_states_response_for_uid @pytest.mark.parametrize( @@ -55,13 +55,11 @@ async def test_cover_get_state( assert entry assert entry.unique_id == uid - get_states_response = list(DEVICES_STATE) - for state_response in get_states_response: - if state_response["uid"] == uid: - state_response["state"]["position"] = 100 + states_response = get_states_response_for_uid(uid) + states_response[0]["state"]["position"] = 100 with patch( "homeassistant.components.freedompro.get_states", - return_value=get_states_response, + return_value=states_response, ): async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) await hass.async_block_till_done() @@ -97,7 +95,7 @@ async def test_cover_set_position( state = hass.states.get(entity_id) assert state - assert state.state == STATE_OPEN + assert state.state == STATE_CLOSED assert state.attributes.get("friendly_name") == name entry = registry.async_get(entity_id) @@ -113,9 +111,18 @@ async def test_cover_set_position( ) mock_put_state.assert_called_once_with(ANY, ANY, ANY, '{"position": 33}') - await hass.async_block_till_done() + states_response = get_states_response_for_uid(uid) + states_response[0]["state"]["position"] = 33 + with patch( + "homeassistant.components.freedompro.get_states", + return_value=states_response, + ): + async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) + await hass.async_block_till_done() + state = hass.states.get(entity_id) assert state.state == STATE_OPEN + assert state.attributes["current_position"] == 33 @pytest.mark.parametrize( @@ -136,6 +143,16 @@ async def test_cover_close( init_integration registry = er.async_get(hass) + states_response = get_states_response_for_uid(uid) + states_response[0]["state"]["position"] = 100 + with patch( + "homeassistant.components.freedompro.get_states", + return_value=states_response, + ): + await hass.helpers.entity_component.async_update_entity(entity_id) + async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) + await hass.async_block_till_done() + state = hass.states.get(entity_id) assert state assert state.state == STATE_OPEN @@ -154,9 +171,16 @@ async def test_cover_close( ) mock_put_state.assert_called_once_with(ANY, ANY, ANY, '{"position": 0}') - await hass.async_block_till_done() + states_response[0]["state"]["position"] = 0 + with patch( + "homeassistant.components.freedompro.get_states", + return_value=states_response, + ): + async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) + await hass.async_block_till_done() + state = hass.states.get(entity_id) - assert state.state == STATE_OPEN + assert state.state == STATE_CLOSED @pytest.mark.parametrize( @@ -179,7 +203,7 @@ async def test_cover_open( state = hass.states.get(entity_id) assert state - assert state.state == STATE_OPEN + assert state.state == STATE_CLOSED assert state.attributes.get("friendly_name") == name entry = registry.async_get(entity_id) @@ -195,6 +219,14 @@ async def test_cover_open( ) mock_put_state.assert_called_once_with(ANY, ANY, ANY, '{"position": 100}') - await hass.async_block_till_done() + states_response = get_states_response_for_uid(uid) + states_response[0]["state"]["position"] = 100 + with patch( + "homeassistant.components.freedompro.get_states", + return_value=states_response, + ): + async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) + await hass.async_block_till_done() + state = hass.states.get(entity_id) assert state.state == STATE_OPEN diff --git a/tests/components/freedompro/test_fan.py b/tests/components/freedompro/test_fan.py index 6bf4bbe1e04..3404c5d17e4 100644 --- a/tests/components/freedompro/test_fan.py +++ b/tests/components/freedompro/test_fan.py @@ -13,7 +13,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.util.dt import utcnow from tests.common import async_fire_time_changed -from tests.components.freedompro.const import DEVICES_STATE +from tests.components.freedompro.conftest import get_states_response_for_uid uid = "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*ILYH1E3DWZOVMNEUIMDYMNLOW-LFRQFDPWWJOVHVDOS" @@ -42,14 +42,12 @@ async def test_fan_get_state(hass, init_integration): assert entry assert entry.unique_id == uid - get_states_response = list(DEVICES_STATE) - for state_response in get_states_response: - if state_response["uid"] == uid: - state_response["state"]["on"] = True - state_response["state"]["rotationSpeed"] = 50 + states_response = get_states_response_for_uid(uid) + states_response[0]["state"]["on"] = True + states_response[0]["state"]["rotationSpeed"] = 50 with patch( "homeassistant.components.freedompro.get_states", - return_value=get_states_response, + return_value=states_response, ): async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) await hass.async_block_till_done() @@ -72,6 +70,18 @@ async def test_fan_set_off(hass, init_integration): registry = er.async_get(hass) entity_id = "fan.bedroom" + + states_response = get_states_response_for_uid(uid) + states_response[0]["state"]["on"] = True + states_response[0]["state"]["rotationSpeed"] = 50 + with patch( + "homeassistant.components.freedompro.get_states", + return_value=states_response, + ): + await hass.helpers.entity_component.async_update_entity(entity_id) + async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) + await hass.async_block_till_done() + state = hass.states.get(entity_id) assert state assert state.state == STATE_ON @@ -91,10 +101,20 @@ async def test_fan_set_off(hass, init_integration): ) mock_put_state.assert_called_once_with(ANY, ANY, ANY, '{"on": false}') + states_response[0]["state"]["on"] = False + states_response[0]["state"]["rotationSpeed"] = 0 + with patch( + "homeassistant.components.freedompro.get_states", + return_value=states_response, + ): + await hass.helpers.entity_component.async_update_entity(entity_id) + async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) + await hass.async_block_till_done() + await hass.async_block_till_done() state = hass.states.get(entity_id) - assert state.attributes[ATTR_PERCENTAGE] == 50 - assert state.state == STATE_ON + assert state.attributes[ATTR_PERCENTAGE] == 0 + assert state.state == STATE_OFF async def test_fan_set_on(hass, init_integration): @@ -105,8 +125,8 @@ async def test_fan_set_on(hass, init_integration): entity_id = "fan.bedroom" state = hass.states.get(entity_id) assert state - assert state.state == STATE_ON - assert state.attributes[ATTR_PERCENTAGE] == 50 + assert state.state == STATE_OFF + assert state.attributes[ATTR_PERCENTAGE] == 0 assert state.attributes.get("friendly_name") == "bedroom" entry = registry.async_get(entity_id) @@ -122,7 +142,16 @@ async def test_fan_set_on(hass, init_integration): ) mock_put_state.assert_called_once_with(ANY, ANY, ANY, '{"on": true}') - await hass.async_block_till_done() + states_response = get_states_response_for_uid(uid) + states_response[0]["state"]["on"] = True + states_response[0]["state"]["rotationSpeed"] = 50 + with patch( + "homeassistant.components.freedompro.get_states", + return_value=states_response, + ): + async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) + await hass.async_block_till_done() + state = hass.states.get(entity_id) assert state.attributes[ATTR_PERCENTAGE] == 50 assert state.state == STATE_ON @@ -136,8 +165,8 @@ async def test_fan_set_percent(hass, init_integration): entity_id = "fan.bedroom" state = hass.states.get(entity_id) assert state - assert state.state == STATE_ON - assert state.attributes[ATTR_PERCENTAGE] == 50 + assert state.state == STATE_OFF + assert state.attributes[ATTR_PERCENTAGE] == 0 assert state.attributes.get("friendly_name") == "bedroom" entry = registry.async_get(entity_id) @@ -153,7 +182,17 @@ async def test_fan_set_percent(hass, init_integration): ) mock_put_state.assert_called_once_with(ANY, ANY, ANY, '{"rotationSpeed": 40}') + states_response = get_states_response_for_uid(uid) + states_response[0]["state"]["on"] = True + states_response[0]["state"]["rotationSpeed"] = 40 + with patch( + "homeassistant.components.freedompro.get_states", + return_value=states_response, + ): + async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) + await hass.async_block_till_done() + await hass.async_block_till_done() state = hass.states.get(entity_id) - assert state.attributes[ATTR_PERCENTAGE] == 50 + assert state.attributes[ATTR_PERCENTAGE] == 40 assert state.state == STATE_ON diff --git a/tests/components/freedompro/test_lock.py b/tests/components/freedompro/test_lock.py index 5c30909e081..e0d25ce91d9 100644 --- a/tests/components/freedompro/test_lock.py +++ b/tests/components/freedompro/test_lock.py @@ -12,7 +12,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.util.dt import utcnow from tests.common import async_fire_time_changed -from tests.components.freedompro.const import DEVICES_STATE +from tests.components.freedompro.conftest import get_states_response_for_uid uid = "2WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*2VAS3HTWINNZ5N6HVEIPDJ6NX85P2-AM-GSYWUCNPU0" @@ -40,13 +40,11 @@ async def test_lock_get_state(hass, init_integration): assert entry assert entry.unique_id == uid - get_states_response = list(DEVICES_STATE) - for state_response in get_states_response: - if state_response["uid"] == uid: - state_response["state"]["lock"] = 1 + states_response = get_states_response_for_uid(uid) + states_response[0]["state"]["lock"] = 1 with patch( "homeassistant.components.freedompro.get_states", - return_value=get_states_response, + return_value=states_response, ): async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) await hass.async_block_till_done() @@ -68,6 +66,17 @@ async def test_lock_set_unlock(hass, init_integration): registry = er.async_get(hass) entity_id = "lock.lock" + + states_response = get_states_response_for_uid(uid) + states_response[0]["state"]["lock"] = 1 + with patch( + "homeassistant.components.freedompro.get_states", + return_value=states_response, + ): + await hass.helpers.entity_component.async_update_entity(entity_id) + async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) + await hass.async_block_till_done() + state = hass.states.get(entity_id) assert state assert state.state == STATE_LOCKED @@ -86,9 +95,17 @@ async def test_lock_set_unlock(hass, init_integration): ) mock_put_state.assert_called_once_with(ANY, ANY, ANY, '{"lock": 0}') - await hass.async_block_till_done() + states_response = get_states_response_for_uid(uid) + states_response[0]["state"]["lock"] = 0 + with patch( + "homeassistant.components.freedompro.get_states", + return_value=states_response, + ): + async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) + await hass.async_block_till_done() + state = hass.states.get(entity_id) - assert state.state == STATE_LOCKED + assert state.state == STATE_UNLOCKED async def test_lock_set_lock(hass, init_integration): @@ -99,7 +116,7 @@ async def test_lock_set_lock(hass, init_integration): entity_id = "lock.lock" state = hass.states.get(entity_id) assert state - assert state.state == STATE_LOCKED + assert state.state == STATE_UNLOCKED assert state.attributes.get("friendly_name") == "lock" entry = registry.async_get(entity_id) @@ -115,6 +132,15 @@ async def test_lock_set_lock(hass, init_integration): ) mock_put_state.assert_called_once_with(ANY, ANY, ANY, '{"lock": 1}') + states_response = get_states_response_for_uid(uid) + states_response[0]["state"]["lock"] = 1 + with patch( + "homeassistant.components.freedompro.get_states", + return_value=states_response, + ): + async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) + await hass.async_block_till_done() + await hass.async_block_till_done() state = hass.states.get(entity_id) assert state.state == STATE_LOCKED diff --git a/tests/components/freedompro/test_sensor.py b/tests/components/freedompro/test_sensor.py index b6f809569f1..d73506e2c6f 100644 --- a/tests/components/freedompro/test_sensor.py +++ b/tests/components/freedompro/test_sensor.py @@ -8,7 +8,7 @@ from homeassistant.helpers import entity_registry as er from homeassistant.util.dt import utcnow from tests.common import async_fire_time_changed -from tests.components.freedompro.const import DEVICES_STATE +from tests.components.freedompro.conftest import get_states_response_for_uid @pytest.mark.parametrize( @@ -48,18 +48,16 @@ async def test_sensor_get_state( assert state.state == "0" - get_states_response = list(DEVICES_STATE) - for state_response in get_states_response: - if state_response["uid"] == uid: - if state_response["type"] == "lightSensor": - state_response["state"]["currentAmbientLightLevel"] = "1" - if state_response["type"] == "temperatureSensor": - state_response["state"]["currentTemperature"] = "1" - if state_response["type"] == "humiditySensor": - state_response["state"]["currentRelativeHumidity"] = "1" + states_response = get_states_response_for_uid(uid) + if states_response[0]["type"] == "lightSensor": + states_response[0]["state"]["currentAmbientLightLevel"] = "1" + elif states_response[0]["type"] == "temperatureSensor": + states_response[0]["state"]["currentTemperature"] = "1" + elif states_response[0]["type"] == "humiditySensor": + states_response[0]["state"]["currentRelativeHumidity"] = "1" with patch( "homeassistant.components.freedompro.get_states", - return_value=get_states_response, + return_value=states_response, ): async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) diff --git a/tests/components/freedompro/test_switch.py b/tests/components/freedompro/test_switch.py index 4674b684c41..6b62e028d72 100644 --- a/tests/components/freedompro/test_switch.py +++ b/tests/components/freedompro/test_switch.py @@ -8,7 +8,7 @@ from homeassistant.helpers import entity_registry as er from homeassistant.util.dt import utcnow from tests.common import async_fire_time_changed -from tests.components.freedompro.const import DEVICES_STATE +from tests.components.freedompro.conftest import get_states_response_for_uid uid = "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*1JKU1MVWHQL-Z9SCUS85VFXMRGNDCDNDDUVVDKBU31W" @@ -28,13 +28,11 @@ async def test_switch_get_state(hass, init_integration): assert entry assert entry.unique_id == uid - get_states_response = list(DEVICES_STATE) - for state_response in get_states_response: - if state_response["uid"] == uid: - state_response["state"]["on"] = True + states_response = get_states_response_for_uid(uid) + states_response[0]["state"]["on"] = True with patch( "homeassistant.components.freedompro.get_states", - return_value=get_states_response, + return_value=states_response, ): async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) await hass.async_block_till_done() @@ -56,6 +54,17 @@ async def test_switch_set_off(hass, init_integration): registry = er.async_get(hass) entity_id = "switch.irrigation_switch" + + states_response = get_states_response_for_uid(uid) + states_response[0]["state"]["on"] = True + with patch( + "homeassistant.components.freedompro.get_states", + return_value=states_response, + ): + await hass.helpers.entity_component.async_update_entity(entity_id) + async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) + await hass.async_block_till_done() + state = hass.states.get(entity_id) assert state assert state.state == STATE_ON @@ -76,9 +85,17 @@ async def test_switch_set_off(hass, init_integration): ) mock_put_state.assert_called_once_with(ANY, ANY, ANY, '{"on": false}') - await hass.async_block_till_done() + states_response = get_states_response_for_uid(uid) + states_response[0]["state"]["on"] = False + with patch( + "homeassistant.components.freedompro.get_states", + return_value=states_response, + ): + async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) + await hass.async_block_till_done() + state = hass.states.get(entity_id) - assert state.state == STATE_ON + assert state.state == STATE_OFF async def test_switch_set_on(hass, init_integration): @@ -89,7 +106,7 @@ async def test_switch_set_on(hass, init_integration): entity_id = "switch.irrigation_switch" state = hass.states.get(entity_id) assert state - assert state.state == STATE_ON + assert state.state == STATE_OFF assert state.attributes.get("friendly_name") == "Irrigation switch" entry = registry.async_get(entity_id) @@ -107,6 +124,14 @@ async def test_switch_set_on(hass, init_integration): ) mock_put_state.assert_called_once_with(ANY, ANY, ANY, '{"on": true}') - await hass.async_block_till_done() + states_response = get_states_response_for_uid(uid) + states_response[0]["state"]["on"] = True + with patch( + "homeassistant.components.freedompro.get_states", + return_value=states_response, + ): + async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) + await hass.async_block_till_done() + state = hass.states.get(entity_id) assert state.state == STATE_ON From c3e72bec0a705865a041ff682b4064c65345f6ec Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 12 Dec 2021 14:10:20 -0800 Subject: [PATCH 0339/2644] Update logic for nest media source `can_play` for events (#61537) --- homeassistant/components/nest/media_source.py | 4 ++-- tests/components/nest/test_media_source.py | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nest/media_source.py b/homeassistant/components/nest/media_source.py index 098d7d01b0f..328a9b000b3 100644 --- a/homeassistant/components/nest/media_source.py +++ b/homeassistant/components/nest/media_source.py @@ -24,7 +24,7 @@ import logging from google_nest_sdm.camera_traits import CameraClipPreviewTrait, CameraEventImageTrait from google_nest_sdm.device import Device -from google_nest_sdm.event import ImageEventBase +from google_nest_sdm.event import EventImageType, ImageEventBase from homeassistant.components.media_player.const import ( MEDIA_CLASS_DIRECTORY, @@ -254,7 +254,7 @@ def _browse_event( event_name=MEDIA_SOURCE_EVENT_TITLE_MAP.get(event.event_type, "Event"), event_time=dt_util.as_local(event.timestamp).strftime(DATE_STR_FORMAT), ), - can_play=True, + can_play=(event.event_image_type == EventImageType.CLIP_PREVIEW), can_expand=False, thumbnail=None, children=[], diff --git a/tests/components/nest/test_media_source.py b/tests/components/nest/test_media_source.py index 22ed0721eb2..f52b89c4f4d 100644 --- a/tests/components/nest/test_media_source.py +++ b/tests/components/nest/test_media_source.py @@ -231,6 +231,7 @@ async def test_camera_event(hass, auth, hass_client): assert "Person" in browse.title assert not browse.can_expand assert not browse.children + assert not browse.can_play # Resolving the event links to the media media = await media_source.async_resolve_media( @@ -302,6 +303,7 @@ async def test_event_order(hass, auth): event_timestamp_string = event_timestamp2.strftime(DATE_STR_FORMAT) assert browse.children[0].title == f"Motion @ {event_timestamp_string}" assert not browse.children[0].can_expand + assert not browse.can_play # Person event is next assert browse.children[1].domain == DOMAIN @@ -310,6 +312,7 @@ async def test_event_order(hass, auth): event_timestamp_string = event_timestamp1.strftime(DATE_STR_FORMAT) assert browse.children[1].title == f"Person @ {event_timestamp_string}" assert not browse.children[1].can_expand + assert not browse.can_play async def test_browse_invalid_device_id(hass, auth): @@ -449,6 +452,7 @@ async def test_camera_event_clip_preview(hass, auth, hass_client): assert browse.children[0].title == f"Motion @ {event_timestamp_string}" assert not browse.children[0].can_expand assert len(browse.children[0].children) == 0 + assert browse.children[0].can_play # Resolving the event links to the media media = await media_source.async_resolve_media( From 82001017856fd647ff2386efb70e4cd3afac422b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Dec 2021 17:10:40 -0500 Subject: [PATCH 0340/2644] Fix HomeKit covers with device class window and no tilt (#61566) --- .../components/homekit/type_covers.py | 17 ++++++---- tests/components/homekit/test_type_covers.py | 33 +++++++++++++++++-- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 0c889d9aee4..b9153ef1372 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -249,14 +249,17 @@ class OpeningDeviceBase(HomeAccessory): def async_update_state(self, new_state): """Update cover position and tilt after state changed.""" # update tilt + if not self._supports_tilt: + return current_tilt = new_state.attributes.get(ATTR_CURRENT_TILT_POSITION) - if isinstance(current_tilt, (float, int)): - # HomeKit sends values between -90 and 90. - # We'll have to normalize to [0,100] - current_tilt = (current_tilt / 100.0 * 180.0) - 90.0 - current_tilt = int(current_tilt) - self.char_current_tilt.set_value(current_tilt) - self.char_target_tilt.set_value(current_tilt) + if not isinstance(current_tilt, (float, int)): + return + # HomeKit sends values between -90 and 90. + # We'll have to normalize to [0,100] + current_tilt = (current_tilt / 100.0 * 180.0) - 90.0 + current_tilt = int(current_tilt) + self.char_current_tilt.set_value(current_tilt) + self.char_target_tilt.set_value(current_tilt) class OpeningDevice(OpeningDeviceBase, HomeAccessory): diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index c357598a3df..44c9365fc04 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -223,11 +223,15 @@ async def test_windowcovering_set_cover_position(hass, hk_driver, events): assert events[-1].data[ATTR_VALUE] == 75 -async def test_window_instantiate(hass, hk_driver, events): - """Test if Window accessory is instantiated correctly.""" +async def test_window_instantiate_set_position(hass, hk_driver, events): + """Test if Window accessory is instantiated correctly and can set position.""" entity_id = "cover.window" - hass.states.async_set(entity_id, None) + hass.states.async_set( + entity_id, + STATE_OPEN, + {ATTR_SUPPORTED_FEATURES: SUPPORT_SET_POSITION, ATTR_CURRENT_POSITION: 0}, + ) await hass.async_block_till_done() acc = Window(hass, hk_driver, "Window", entity_id, 2, None) await acc.run() @@ -239,6 +243,29 @@ async def test_window_instantiate(hass, hk_driver, events): assert acc.char_current_position.value == 0 assert acc.char_target_position.value == 0 + hass.states.async_set( + entity_id, + STATE_OPEN, + {ATTR_SUPPORTED_FEATURES: SUPPORT_SET_POSITION, ATTR_CURRENT_POSITION: 50}, + ) + await hass.async_block_till_done() + assert acc.char_current_position.value == 50 + assert acc.char_target_position.value == 50 + assert acc.char_position_state.value == 2 + + hass.states.async_set( + entity_id, + STATE_OPEN, + { + ATTR_SUPPORTED_FEATURES: SUPPORT_SET_POSITION, + ATTR_CURRENT_POSITION: "GARBAGE", + }, + ) + await hass.async_block_till_done() + assert acc.char_current_position.value == 50 + assert acc.char_target_position.value == 50 + assert acc.char_position_state.value == 2 + async def test_windowcovering_cover_set_tilt(hass, hk_driver, events): """Test if accessory and HA update slat tilt accordingly.""" From 4ad90b33c991f7048ba275f9cbe0bac8d2ff47c2 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sun, 12 Dec 2021 23:11:41 +0100 Subject: [PATCH 0341/2644] Fix Hue transition calculation (#61581) --- homeassistant/components/hue/scene.py | 4 ++-- homeassistant/components/hue/v2/group.py | 4 ++-- homeassistant/components/hue/v2/light.py | 8 ++++---- tests/components/hue/test_light_v2.py | 4 ++-- tests/components/hue/test_scene.py | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/hue/scene.py b/homeassistant/components/hue/scene.py index 7335d2a048e..d67a3b097c7 100644 --- a/homeassistant/components/hue/scene.py +++ b/homeassistant/components/hue/scene.py @@ -96,8 +96,8 @@ class HueSceneEntity(HueBaseEntity, SceneEntity): """Activate Hue scene.""" transition = kwargs.get("transition") if transition is not None: - # hue transition duration is in steps of 100 ms - transition = int(transition * 100) + # hue transition duration is in milliseconds + transition = int(transition * 1000) dynamic = kwargs.get("dynamic", self.is_dynamic) await self.bridge.async_request_call( self.controller.recall, diff --git a/homeassistant/components/hue/v2/group.py b/homeassistant/components/hue/v2/group.py index 010f7ab3382..08f1dc72325 100644 --- a/homeassistant/components/hue/v2/group.py +++ b/homeassistant/components/hue/v2/group.py @@ -150,8 +150,8 @@ class GroupedHueLight(HueBaseEntity, LightEntity): # Hue uses a range of [0, 100] to control brightness. brightness = float((brightness / 255) * 100) if transition is not None: - # hue transition duration is in steps of 100 ms - transition = int(transition * 100) + # hue transition duration is in milliseconds + transition = int(transition * 1000) # NOTE: a grouped_light can only handle turn on/off # To set other features, you'll have to control the attached lights diff --git a/homeassistant/components/hue/v2/light.py b/homeassistant/components/hue/v2/light.py index 42972f2242c..de5388e1220 100644 --- a/homeassistant/components/hue/v2/light.py +++ b/homeassistant/components/hue/v2/light.py @@ -158,8 +158,8 @@ class HueLight(HueBaseEntity, LightEntity): # Hue uses a range of [0, 100] to control brightness. brightness = float((brightness / 255) * 100) if transition is not None: - # hue transition duration is in steps of 100 ms - transition = int(transition * 100) + # hue transition duration is in milliseconds + transition = int(transition * 1000) await self.bridge.async_request_call( self.controller.set_state, @@ -176,8 +176,8 @@ class HueLight(HueBaseEntity, LightEntity): """Turn the light off.""" transition = kwargs.get(ATTR_TRANSITION) if transition is not None: - # hue transition duration is in steps of 100 ms - transition = int(transition * 100) + # hue transition duration is in milliseconds + transition = int(transition * 1000) await self.bridge.async_request_call( self.controller.set_state, id=self.resource.id, diff --git a/tests/components/hue/test_light_v2.py b/tests/components/hue/test_light_v2.py index 7843cab1574..362b7076a92 100644 --- a/tests/components/hue/test_light_v2.py +++ b/tests/components/hue/test_light_v2.py @@ -119,7 +119,7 @@ async def test_light_turn_on_service(hass, mock_bridge_v2, v2_resources_test_dat ) assert len(mock_bridge_v2.mock_requests) == 2 assert mock_bridge_v2.mock_requests[1]["json"]["on"]["on"] is True - assert mock_bridge_v2.mock_requests[1]["json"]["dynamics"]["duration"] == 600 + assert mock_bridge_v2.mock_requests[1]["json"]["dynamics"]["duration"] == 6000 async def test_light_turn_off_service(hass, mock_bridge_v2, v2_resources_test_data): @@ -164,7 +164,7 @@ async def test_light_turn_off_service(hass, mock_bridge_v2, v2_resources_test_da ) assert len(mock_bridge_v2.mock_requests) == 2 assert mock_bridge_v2.mock_requests[1]["json"]["on"]["on"] is False - assert mock_bridge_v2.mock_requests[1]["json"]["dynamics"]["duration"] == 600 + assert mock_bridge_v2.mock_requests[1]["json"]["dynamics"]["duration"] == 6000 async def test_light_added(hass, mock_bridge_v2): diff --git a/tests/components/hue/test_scene.py b/tests/components/hue/test_scene.py index 0f3d6255e86..1d270706c99 100644 --- a/tests/components/hue/test_scene.py +++ b/tests/components/hue/test_scene.py @@ -83,7 +83,7 @@ async def test_scene_turn_on_service(hass, mock_bridge_v2, v2_resources_test_dat assert len(mock_bridge_v2.mock_requests) == 2 assert mock_bridge_v2.mock_requests[1]["json"]["recall"] == { "action": "active", - "duration": 600, + "duration": 6000, } From cd001d024331d71cba47225677ce6a72c7c4d88f Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 12 Dec 2021 14:12:05 -0800 Subject: [PATCH 0342/2644] Only publish nest camera event messages once per thread and bump nest version (#61587) --- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nest/common.py | 3 +- tests/components/nest/test_events.py | 73 +++++++++++++++++++++ 5 files changed, 77 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 11a464dbaf1..507711c73ff 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["ffmpeg", "http", "media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.5"], + "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.6"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 5d09fdacd27..946a31d64f9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -747,7 +747,7 @@ google-cloud-pubsub==2.1.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==0.4.5 +google-nest-sdm==0.4.6 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7c368e7b3fd..926e5435048 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -470,7 +470,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.1.0 # homeassistant.components.nest -google-nest-sdm==0.4.5 +google-nest-sdm==0.4.6 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index 35183a441a5..0f1d47c687e 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -62,7 +62,7 @@ class FakeSubscriber(GoogleNestSubscriber): def set_update_callback(self, callback: Callable[[EventMessage], Awaitable[None]]): """Capture the callback set by Home Assistant.""" - self._callback = callback + self._device_manager.set_update_callback(callback) async def create_subscription(self): """Create the subscription.""" @@ -93,7 +93,6 @@ class FakeSubscriber(GoogleNestSubscriber): """Simulate a received pubsub message, invoked by tests.""" # Update device state, then invoke HomeAssistant to refresh await self._device_manager.async_handle_event(event_message) - await self._callback(event_message) async def async_setup_sdm_platform( diff --git a/tests/components/nest/test_events.py b/tests/components/nest/test_events.py index a2f5c21fdac..4767fd815d2 100644 --- a/tests/components/nest/test_events.py +++ b/tests/components/nest/test_events.py @@ -4,6 +4,8 @@ These tests fake out the subscriber/devicemanager, and are not using a real pubsub subscriber. """ +import datetime + from google_nest_sdm.device import Device from google_nest_sdm.event import EventMessage @@ -298,3 +300,74 @@ async def test_event_message_without_device_event(hass): await hass.async_block_till_done() assert len(events) == 0 + + +async def test_doorbell_event_thread(hass): + """Test a series of pubsub messages in the same thread.""" + events = async_capture_events(hass, NEST_EVENT) + subscriber = await async_setup_devices( + hass, + "sdm.devices.types.DOORBELL", + traits={ + "sdm.devices.traits.Info": { + "customName": "Front", + }, + "sdm.devices.traits.CameraLiveStream": {}, + "sdm.devices.traits.CameraClipPreview": {}, + "sdm.devices.traits.CameraPerson": {}, + }, + ) + registry = er.async_get(hass) + entry = registry.async_get("camera.front") + assert entry is not None + + event_message_data = { + "eventId": "some-event-id-ignored", + "resourceUpdate": { + "name": DEVICE_ID, + "events": { + "sdm.devices.events.CameraMotion.Motion": { + "eventSessionId": EVENT_SESSION_ID, + "eventId": "n:1", + }, + "sdm.devices.events.CameraClipPreview.ClipPreview": { + "eventSessionId": EVENT_SESSION_ID, + "previewUrl": "image-url-1", + }, + }, + }, + "eventThreadId": "CjY5Y3VKaTZwR3o4Y19YbTVfMF...", + "resourcegroup": [DEVICE_ID], + } + + # Publish message #1 that starts the event thread + timestamp1 = utcnow() + message_data_1 = event_message_data.copy() + message_data_1.update( + { + "timestamp": timestamp1.isoformat(timespec="seconds"), + "eventThreadState": "STARTED", + } + ) + await subscriber.async_receive_event(EventMessage(message_data_1, auth=None)) + + # Publish message #1 that sends a no-op update to end the event thread + timestamp2 = timestamp1 + datetime.timedelta(seconds=1) + message_data_2 = event_message_data.copy() + message_data_2.update( + { + "timestamp": timestamp2.isoformat(timespec="seconds"), + "eventThreadState": "ENDED", + } + ) + await subscriber.async_receive_event(EventMessage(message_data_2, auth=None)) + await hass.async_block_till_done() + + # The event is only published once + assert len(events) == 1 + assert events[0].data == { + "device_id": entry.device_id, + "type": "camera_motion", + "timestamp": timestamp1.replace(microsecond=0), + "nest_event_id": EVENT_SESSION_ID, + } From ed8d5c09ff138d45080e601104b379b5d6bc6378 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sun, 12 Dec 2021 23:12:35 +0100 Subject: [PATCH 0343/2644] Fix availability for 3th party Hue lights (#61603) --- homeassistant/components/hue/v2/entity.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/hue/v2/entity.py b/homeassistant/components/hue/v2/entity.py index 6dbc959fd9c..68c427fd3a5 100644 --- a/homeassistant/components/hue/v2/entity.py +++ b/homeassistant/components/hue/v2/entity.py @@ -103,6 +103,9 @@ class HueBaseEntity(Entity): if self.resource.type == ResourceTypes.ZIGBEE_CONNECTIVITY: # the zigbee connectivity sensor itself should be always available return True + if self.device.product_data.manufacturer_name != "Signify Netherlands B.V.": + # availability status for non-philips brand lights is unreliable + return True if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id): # all device-attached entities get availability from the zigbee connectivity return zigbee.status == ConnectivityServiceStatus.CONNECTED From 238de08d1642318dd4ecb520444a6c112b8db657 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Dec 2021 17:17:54 -0500 Subject: [PATCH 0344/2644] Bump aiopvapi to 1.6.19 to fix async_timeout passing loop (#61618) --- homeassistant/components/hunterdouglas_powerview/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hunterdouglas_powerview/manifest.json b/homeassistant/components/hunterdouglas_powerview/manifest.json index ad763a33bc8..ade3b25f31c 100644 --- a/homeassistant/components/hunterdouglas_powerview/manifest.json +++ b/homeassistant/components/hunterdouglas_powerview/manifest.json @@ -2,7 +2,7 @@ "domain": "hunterdouglas_powerview", "name": "Hunter Douglas PowerView", "documentation": "https://www.home-assistant.io/integrations/hunterdouglas_powerview", - "requirements": ["aiopvapi==1.6.14"], + "requirements": ["aiopvapi==1.6.19"], "codeowners": ["@bdraco"], "config_flow": true, "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 946a31d64f9..9b45bd20602 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -237,7 +237,7 @@ aionotion==3.0.2 aiopulse==0.4.3 # homeassistant.components.hunterdouglas_powerview -aiopvapi==1.6.14 +aiopvapi==1.6.19 # homeassistant.components.pvpc_hourly_pricing aiopvpc==2.2.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 926e5435048..c74d3100683 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -167,7 +167,7 @@ aionotion==3.0.2 aiopulse==0.4.3 # homeassistant.components.hunterdouglas_powerview -aiopvapi==1.6.14 +aiopvapi==1.6.19 # homeassistant.components.pvpc_hourly_pricing aiopvpc==2.2.4 From 4013c0eb2b74b1a0a5e41f308479588bccdc3369 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 12 Dec 2021 14:28:59 -0800 Subject: [PATCH 0345/2644] Bump aiohue to 3.0.3 (#61627) --- homeassistant/components/hue/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index c789755c9a3..ee337cd3d71 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==3.0.2"], + "requirements": ["aiohue==3.0.3"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 9b45bd20602..03dbf216377 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -192,7 +192,7 @@ aiohomekit==0.6.4 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.2 +aiohue==3.0.3 # homeassistant.components.imap aioimaplib==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c74d3100683..473f1a3d44b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -137,7 +137,7 @@ aiohomekit==0.6.4 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.2 +aiohue==3.0.3 # homeassistant.components.apache_kafka aiokafka==0.6.0 From 83989d7b40521bf53a188cfdbe010bf2fe37e08a Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 13 Dec 2021 00:24:46 +0100 Subject: [PATCH 0346/2644] Use relative imports [L-R] (#61575) --- .../components/light/device_action.py | 12 +++++------ .../components/media_source/local_source.py | 2 +- .../components/metoffice/config_flow.py | 2 +- .../components/mysensors/binary_sensor.py | 3 +-- homeassistant/components/mysensors/climate.py | 2 +- .../components/mysensors/config_flow.py | 16 +++++++------- homeassistant/components/mysensors/cover.py | 2 +- .../components/mysensors/device_tracker.py | 8 ++----- homeassistant/components/neato/camera.py | 2 +- homeassistant/components/neato/sensor.py | 2 +- homeassistant/components/neato/switch.py | 2 +- .../components/nexia/binary_sensor.py | 2 +- .../components/nsw_fuel_station/sensor.py | 6 ++---- homeassistant/components/nut/sensor.py | 2 +- .../components/onewire/binary_sensor.py | 2 +- homeassistant/components/onewire/sensor.py | 5 +---- homeassistant/components/onewire/switch.py | 2 +- homeassistant/components/opengarage/entity.py | 3 ++- .../components/opnsense/device_tracker.py | 3 ++- .../components/pi_hole/config_flow.py | 21 ++++++++++--------- .../components/pilight/base_class.py | 2 +- homeassistant/components/ps4/media_player.py | 2 +- homeassistant/components/recorder/history.py | 8 ++----- .../components/rfxtrx/device_trigger.py | 2 +- 24 files changed, 51 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/light/device_action.py b/homeassistant/components/light/device_action.py index a933d04066e..3d9f2790766 100644 --- a/homeassistant/components/light/device_action.py +++ b/homeassistant/components/light/device_action.py @@ -4,13 +4,6 @@ from __future__ import annotations import voluptuous as vol from homeassistant.components.device_automation import toggle_entity -from homeassistant.components.light import ( - ATTR_FLASH, - FLASH_SHORT, - SUPPORT_FLASH, - VALID_BRIGHTNESS_PCT, - VALID_FLASH, -) from homeassistant.const import ( ATTR_ENTITY_ID, CONF_DEVICE_ID, @@ -27,7 +20,12 @@ from homeassistant.helpers.typing import ConfigType, TemplateVarsType from . import ( ATTR_BRIGHTNESS_PCT, ATTR_BRIGHTNESS_STEP_PCT, + ATTR_FLASH, DOMAIN, + FLASH_SHORT, + SUPPORT_FLASH, + VALID_BRIGHTNESS_PCT, + VALID_FLASH, brightness_supported, get_supported_color_modes, ) diff --git a/homeassistant/components/media_source/local_source.py b/homeassistant/components/media_source/local_source.py index c7b25b4400c..ab2cc531e1e 100644 --- a/homeassistant/components/media_source/local_source.py +++ b/homeassistant/components/media_source/local_source.py @@ -9,11 +9,11 @@ from aiohttp import web from homeassistant.components.http import HomeAssistantView from homeassistant.components.media_player.const import MEDIA_CLASS_DIRECTORY from homeassistant.components.media_player.errors import BrowseError -from homeassistant.components.media_source.error import Unresolvable from homeassistant.core import HomeAssistant, callback from homeassistant.util import raise_if_invalid_path from .const import DOMAIN, MEDIA_CLASS_MAP, MEDIA_MIME_TYPES +from .error import Unresolvable from .models import BrowseMediaSource, MediaSource, MediaSourceItem, PlayMedia diff --git a/homeassistant/components/metoffice/config_flow.py b/homeassistant/components/metoffice/config_flow.py index 375eaa1ec1b..959680a90ec 100644 --- a/homeassistant/components/metoffice/config_flow.py +++ b/homeassistant/components/metoffice/config_flow.py @@ -5,11 +5,11 @@ import datapoint import voluptuous as vol from homeassistant import config_entries, core, exceptions -from homeassistant.components.metoffice.helpers import fetch_site from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.helpers import config_validation as cv from .const import DOMAIN +from .helpers import fetch_site _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mysensors/binary_sensor.py b/homeassistant/components/mysensors/binary_sensor.py index f94b5f71728..91dc454dae3 100644 --- a/homeassistant/components/mysensors/binary_sensor.py +++ b/homeassistant/components/mysensors/binary_sensor.py @@ -12,14 +12,13 @@ from homeassistant.components.binary_sensor import ( DOMAIN, BinarySensorEntity, ) -from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_ON from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DiscoveryInfo +from .const import MYSENSORS_DISCOVERY, DiscoveryInfo from .helpers import on_unload SENSORS = { diff --git a/homeassistant/components/mysensors/climate.py b/homeassistant/components/mysensors/climate.py index 5dd52673581..4a12b872bdc 100644 --- a/homeassistant/components/mysensors/climate.py +++ b/homeassistant/components/mysensors/climate.py @@ -17,13 +17,13 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, ) -from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY, DiscoveryInfo from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from .const import MYSENSORS_DISCOVERY, DiscoveryInfo from .helpers import on_unload DICT_HA_TO_MYS = { diff --git a/homeassistant/components/mysensors/config_flow.py b/homeassistant/components/mysensors/config_flow.py index aea15e7b8ae..5c190d6fd56 100644 --- a/homeassistant/components/mysensors/config_flow.py +++ b/homeassistant/components/mysensors/config_flow.py @@ -17,18 +17,20 @@ from homeassistant.components.mqtt import ( valid_publish_topic, valid_subscribe_topic, ) -from homeassistant.components.mysensors import ( - CONF_DEVICE, - DEFAULT_BAUD_RATE, - DEFAULT_TCP_PORT, - is_persistence_file, -) from homeassistant.config_entries import ConfigEntry from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv -from . import CONF_RETAIN, CONF_VERSION, DEFAULT_VERSION +from . import ( + CONF_DEVICE, + CONF_RETAIN, + CONF_VERSION, + DEFAULT_BAUD_RATE, + DEFAULT_TCP_PORT, + DEFAULT_VERSION, + is_persistence_file, +) from .const import ( CONF_BAUD_RATE, CONF_GATEWAY_TYPE, diff --git a/homeassistant/components/mysensors/cover.py b/homeassistant/components/mysensors/cover.py index 9219097bea4..102f7ccaca8 100644 --- a/homeassistant/components/mysensors/cover.py +++ b/homeassistant/components/mysensors/cover.py @@ -6,13 +6,13 @@ from typing import Any from homeassistant.components import mysensors from homeassistant.components.cover import ATTR_POSITION, DOMAIN, CoverEntity -from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY, DiscoveryInfo from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from .const import MYSENSORS_DISCOVERY, DiscoveryInfo from .helpers import on_unload diff --git a/homeassistant/components/mysensors/device_tracker.py b/homeassistant/components/mysensors/device_tracker.py index 1dd29dbf864..bfb4649e7cd 100644 --- a/homeassistant/components/mysensors/device_tracker.py +++ b/homeassistant/components/mysensors/device_tracker.py @@ -6,16 +6,12 @@ from typing import Any from homeassistant.components import mysensors from homeassistant.components.device_tracker import DOMAIN -from homeassistant.components.mysensors import DevId -from homeassistant.components.mysensors.const import ( - ATTR_GATEWAY_ID, - DiscoveryInfo, - GatewayId, -) from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util import slugify +from . import DevId +from .const import ATTR_GATEWAY_ID, DiscoveryInfo, GatewayId from .helpers import on_unload diff --git a/homeassistant/components/neato/camera.py b/homeassistant/components/neato/camera.py index 6d4de68b456..d19bb2bb38b 100644 --- a/homeassistant/components/neato/camera.py +++ b/homeassistant/components/neato/camera.py @@ -10,12 +10,12 @@ from pybotvac.robot import Robot from urllib3.response import HTTPResponse from homeassistant.components.camera import Camera -from homeassistant.components.neato import NeatoHub from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import NeatoHub from .const import ( NEATO_DOMAIN, NEATO_LOGIN, diff --git a/homeassistant/components/neato/sensor.py b/homeassistant/components/neato/sensor.py index 1fd6547d926..3f7b925ef7f 100644 --- a/homeassistant/components/neato/sensor.py +++ b/homeassistant/components/neato/sensor.py @@ -8,7 +8,6 @@ from typing import Any from pybotvac.exceptions import NeatoRobotException from pybotvac.robot import Robot -from homeassistant.components.neato import NeatoHub from homeassistant.components.sensor import DEVICE_CLASS_BATTERY, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE @@ -16,6 +15,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import NeatoHub from .const import NEATO_DOMAIN, NEATO_LOGIN, NEATO_ROBOTS, SCAN_INTERVAL_MINUTES _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/neato/switch.py b/homeassistant/components/neato/switch.py index b6a1302bda7..c34eea492e9 100644 --- a/homeassistant/components/neato/switch.py +++ b/homeassistant/components/neato/switch.py @@ -8,13 +8,13 @@ from typing import Any from pybotvac.exceptions import NeatoRobotException from pybotvac.robot import Robot -from homeassistant.components.neato import NeatoHub from homeassistant.config_entries import ConfigEntry from homeassistant.const import ENTITY_CATEGORY_CONFIG, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo, ToggleEntity from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import NeatoHub from .const import NEATO_DOMAIN, NEATO_LOGIN, NEATO_ROBOTS, SCAN_INTERVAL_MINUTES _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/nexia/binary_sensor.py b/homeassistant/components/nexia/binary_sensor.py index 63fb98c9a0b..cddfb27845b 100644 --- a/homeassistant/components/nexia/binary_sensor.py +++ b/homeassistant/components/nexia/binary_sensor.py @@ -1,9 +1,9 @@ """Support for Nexia / Trane XL Thermostats.""" from homeassistant.components.binary_sensor import BinarySensorEntity -from homeassistant.components.nexia.coordinator import NexiaDataUpdateCoordinator from .const import DOMAIN +from .coordinator import NexiaDataUpdateCoordinator from .entity import NexiaThermostatEntity diff --git a/homeassistant/components/nsw_fuel_station/sensor.py b/homeassistant/components/nsw_fuel_station/sensor.py index 139728a3405..5e17c127c89 100644 --- a/homeassistant/components/nsw_fuel_station/sensor.py +++ b/homeassistant/components/nsw_fuel_station/sensor.py @@ -5,10 +5,6 @@ import logging import voluptuous as vol -from homeassistant.components.nsw_fuel_station import ( - DATA_NSW_FUEL_STATION, - StationPriceData, -) from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ATTR_ATTRIBUTION, CURRENCY_CENT, VOLUME_LITERS import homeassistant.helpers.config_validation as cv @@ -17,6 +13,8 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, ) +from . import DATA_NSW_FUEL_STATION, StationPriceData + _LOGGER = logging.getLogger(__name__) ATTR_STATION_ID = "station_id" diff --git a/homeassistant/components/nut/sensor.py b/homeassistant/components/nut/sensor.py index e937e6565df..e83cf900b7e 100644 --- a/homeassistant/components/nut/sensor.py +++ b/homeassistant/components/nut/sensor.py @@ -3,7 +3,6 @@ from __future__ import annotations import logging -from homeassistant.components.nut import PyNUTData from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.const import STATE_UNKNOWN from homeassistant.helpers.entity import DeviceInfo @@ -12,6 +11,7 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, ) +from . import PyNUTData from .const import ( COORDINATOR, DOMAIN, diff --git a/homeassistant/components/onewire/binary_sensor.py b/homeassistant/components/onewire/binary_sensor.py index 945ec2344d4..0235b46baa6 100644 --- a/homeassistant/components/onewire/binary_sensor.py +++ b/homeassistant/components/onewire/binary_sensor.py @@ -10,7 +10,6 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.components.onewire.model import OWServerDeviceDescription from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_TYPE from homeassistant.core import HomeAssistant @@ -25,6 +24,7 @@ from .const import ( DOMAIN, READ_MODE_BOOL, ) +from .model import OWServerDeviceDescription from .onewire_entities import OneWireEntityDescription, OneWireProxyEntity from .onewirehub import OneWireHub diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index ad6ad74989c..b0451be953b 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -11,10 +11,6 @@ from typing import TYPE_CHECKING, Any from pi1wire import InvalidCRCException, OneWireInterface, UnsupportResponseException -from homeassistant.components.onewire.model import ( - OWDirectDeviceDescription, - OWServerDeviceDescription, -) from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -46,6 +42,7 @@ from .const import ( READ_MODE_FLOAT, READ_MODE_INT, ) +from .model import OWDirectDeviceDescription, OWServerDeviceDescription from .onewire_entities import ( OneWireBaseEntity, OneWireEntityDescription, diff --git a/homeassistant/components/onewire/switch.py b/homeassistant/components/onewire/switch.py index a7dc1335a46..a05757936f9 100644 --- a/homeassistant/components/onewire/switch.py +++ b/homeassistant/components/onewire/switch.py @@ -5,7 +5,6 @@ from dataclasses import dataclass import os from typing import TYPE_CHECKING, Any -from homeassistant.components.onewire.model import OWServerDeviceDescription from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_TYPE @@ -21,6 +20,7 @@ from .const import ( DOMAIN, READ_MODE_BOOL, ) +from .model import OWServerDeviceDescription from .onewire_entities import OneWireEntityDescription, OneWireProxyEntity from .onewirehub import OneWireHub diff --git a/homeassistant/components/opengarage/entity.py b/homeassistant/components/opengarage/entity.py index 706ff0e81be..97a60d42c07 100644 --- a/homeassistant/components/opengarage/entity.py +++ b/homeassistant/components/opengarage/entity.py @@ -1,11 +1,12 @@ """Entity for the opengarage.io component.""" -from homeassistant.components.opengarage import DOMAIN from homeassistant.core import callback from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import DOMAIN + class OpenGarageEntity(CoordinatorEntity): """Representation of a OpenGarage entity.""" diff --git a/homeassistant/components/opnsense/device_tracker.py b/homeassistant/components/opnsense/device_tracker.py index 0bce00a8e82..9a024b2b260 100644 --- a/homeassistant/components/opnsense/device_tracker.py +++ b/homeassistant/components/opnsense/device_tracker.py @@ -1,6 +1,7 @@ """Device tracker support for OPNSense routers.""" from homeassistant.components.device_tracker import DeviceScanner -from homeassistant.components.opnsense import CONF_TRACKER_INTERFACE, OPNSENSE_DATA + +from . import CONF_TRACKER_INTERFACE, OPNSENSE_DATA async def async_get_scanner(hass, config, discovery_info=None): diff --git a/homeassistant/components/pi_hole/config_flow.py b/homeassistant/components/pi_hole/config_flow.py index 5acaffd13b1..39d47ecdd74 100644 --- a/homeassistant/components/pi_hole/config_flow.py +++ b/homeassistant/components/pi_hole/config_flow.py @@ -9,16 +9,6 @@ from hole.exceptions import HoleError import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.pi_hole.const import ( - CONF_LOCATION, - CONF_STATISTICS_ONLY, - DEFAULT_LOCATION, - DEFAULT_NAME, - DEFAULT_SSL, - DEFAULT_STATISTICS_ONLY, - DEFAULT_VERIFY_SSL, - DOMAIN, -) from homeassistant.const import ( CONF_API_KEY, CONF_HOST, @@ -30,6 +20,17 @@ from homeassistant.const import ( from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession +from .const import ( + CONF_LOCATION, + CONF_STATISTICS_ONLY, + DEFAULT_LOCATION, + DEFAULT_NAME, + DEFAULT_SSL, + DEFAULT_STATISTICS_ONLY, + DEFAULT_VERIFY_SSL, + DOMAIN, +) + _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/pilight/base_class.py b/homeassistant/components/pilight/base_class.py index 97ebaef0080..517beab850f 100644 --- a/homeassistant/components/pilight/base_class.py +++ b/homeassistant/components/pilight/base_class.py @@ -1,7 +1,6 @@ """Base class for pilight.""" import voluptuous as vol -from homeassistant.components.pilight import DOMAIN, EVENT, SERVICE_NAME from homeassistant.const import ( CONF_ID, CONF_NAME, @@ -13,6 +12,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity +from . import DOMAIN, EVENT, SERVICE_NAME from .const import ( CONF_ECHO, CONF_OFF, diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index cc096e96bd5..8f3cfcb0e4b 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -19,7 +19,6 @@ from homeassistant.components.media_player.const import ( SUPPORT_TURN_OFF, SUPPORT_TURN_ON, ) -from homeassistant.components.ps4 import format_unique_id, load_games, save_games from homeassistant.const import ( ATTR_LOCKED, CONF_HOST, @@ -34,6 +33,7 @@ from homeassistant.core import callback from homeassistant.helpers import device_registry, entity_registry from homeassistant.helpers.entity import DeviceInfo +from . import format_unique_id, load_games, save_games from .const import ( ATTR_MEDIA_IMAGE_URL, DEFAULT_ALIAS, diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index 72f820a0d3b..8be60a60ac6 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -10,15 +10,11 @@ from sqlalchemy import and_, bindparam, func from sqlalchemy.ext import baked from homeassistant.components import recorder -from homeassistant.components.recorder.models import ( - States, - process_timestamp_to_utc_isoformat, -) -from homeassistant.components.recorder.util import execute, session_scope from homeassistant.core import split_entity_id import homeassistant.util.dt as dt_util -from .models import LazyState +from .models import LazyState, States, process_timestamp_to_utc_isoformat +from .util import execute, session_scope # mypy: allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/components/rfxtrx/device_trigger.py b/homeassistant/components/rfxtrx/device_trigger.py index 25c8825f6ed..9ab10ca7f2b 100644 --- a/homeassistant/components/rfxtrx/device_trigger.py +++ b/homeassistant/components/rfxtrx/device_trigger.py @@ -12,7 +12,6 @@ from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) from homeassistant.components.homeassistant.triggers import event as event_trigger -from homeassistant.components.rfxtrx.const import EVENT_RFXTRX_EVENT from homeassistant.const import ( ATTR_DEVICE_ID, CONF_DEVICE_ID, @@ -24,6 +23,7 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers.typing import ConfigType from . import DOMAIN +from .const import EVENT_RFXTRX_EVENT from .helpers import async_get_device_object CONF_SUBTYPE = "subtype" From ca12f257ca5b8c53d590772a0f0b63ea4e42edfd Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 13 Dec 2021 00:13:26 +0000 Subject: [PATCH 0347/2644] [ci skip] Translation update --- homeassistant/components/lcn/translations/hu.json | 10 ++++++++++ homeassistant/components/lcn/translations/it.json | 10 ++++++++++ homeassistant/components/lcn/translations/tr.json | 10 ++++++++++ .../components/simplisafe/translations/it.json | 3 ++- .../components/xiaomi_miio/translations/he.json | 3 ++- 5 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/lcn/translations/hu.json create mode 100644 homeassistant/components/lcn/translations/it.json create mode 100644 homeassistant/components/lcn/translations/tr.json diff --git a/homeassistant/components/lcn/translations/hu.json b/homeassistant/components/lcn/translations/hu.json new file mode 100644 index 00000000000..3e56a4a149b --- /dev/null +++ b/homeassistant/components/lcn/translations/hu.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "trigger_type": { + "fingerprint": "ujjlenyomatk\u00f3d \u00e9rkezett", + "send_keys": "kulcs/gomb \u00e9rkezett", + "transmitter": "t\u00e1vvez\u00e9rl\u0151 k\u00f3d \u00e9rkezett", + "transponder": "transzponder k\u00f3d \u00e9rkezett" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lcn/translations/it.json b/homeassistant/components/lcn/translations/it.json new file mode 100644 index 00000000000..c8f61e2570a --- /dev/null +++ b/homeassistant/components/lcn/translations/it.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "trigger_type": { + "fingerprint": "codice impronta digitale ricevuto", + "send_keys": "invia chiavi ricevute", + "transmitter": "codice trasmettitore ricevuto", + "transponder": "codice transpoder ricevuto" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lcn/translations/tr.json b/homeassistant/components/lcn/translations/tr.json new file mode 100644 index 00000000000..2c2d94611d0 --- /dev/null +++ b/homeassistant/components/lcn/translations/tr.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "trigger_type": { + "fingerprint": "al\u0131nan parmak izi kodu", + "send_keys": "al\u0131nan anahtarlar\u0131 g\u00f6nder", + "transmitter": "verici kodu al\u0131nd\u0131", + "transponder": "transpoder kodu al\u0131nd\u0131" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/it.json b/homeassistant/components/simplisafe/translations/it.json index 445e61d835b..6b7d9231360 100644 --- a/homeassistant/components/simplisafe/translations/it.json +++ b/homeassistant/components/simplisafe/translations/it.json @@ -32,11 +32,12 @@ }, "user": { "data": { + "auth_code": "Codice di autorizzazione", "code": "Codice (utilizzato nell'Interfaccia Utente di Home Assistant)", "password": "Password", "username": "E-mail" }, - "description": "A partire dal 2021, SimpliSafe \u00e8 passato a un nuovo meccanismo di autenticazione tramite la sua app web. A causa di limitazioni tecniche, alla fine di questo processo \u00e8 previsto un passaggio manuale; assicurati di leggere la [documentazione](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) prima di iniziare. \n\nQuando sei pronto, fai clic su [qui]({url}) per aprire l'app Web SimpliSafe e inserire le tue credenziali. Al termine del processo, torna qui e fai clic su Invia.", + "description": "SimpliSafe si autentica con Home Assistant tramite l'app Web SimpliSafe. A causa di limitazioni tecniche, alla fine di questo processo \u00e8 previsto un passaggio manuale; assicurati di leggere la [documentazione]({docs_url}) prima di iniziare. \n\n 1. Fare clic su [qui]({url}) per aprire l'app Web SimpliSafe e inserire le proprie credenziali. \n\n 2. Quando il processo di accesso \u00e8 completo, torna qui e inserisci il codice di autorizzazione di seguito.", "title": "Inserisci le tue informazioni." } } diff --git a/homeassistant/components/xiaomi_miio/translations/he.json b/homeassistant/components/xiaomi_miio/translations/he.json index 69d47597c5f..4bb0251d6cb 100644 --- a/homeassistant/components/xiaomi_miio/translations/he.json +++ b/homeassistant/components/xiaomi_miio/translations/he.json @@ -13,7 +13,8 @@ "cloud_login_error": "\u05dc\u05d0 \u05d4\u05d9\u05ea\u05d4 \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05e2\u05e0\u05df \u05e9\u05d9\u05d5\u05d0\u05de\u05d9 \u05de\u05d9\u05d5, \u05e0\u05d0 \u05dc\u05d1\u05d3\u05d5\u05e7 \u05d0\u05ea \u05d4\u05d0\u05d9\u05e9\u05d5\u05e8\u05d9\u05dd.", "cloud_no_devices": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05d7\u05e9\u05d1\u05d5\u05df \u05d4\u05e2\u05e0\u05df \u05d4\u05d6\u05d4 \u05e9\u05dc \u05e9\u05d9\u05d5\u05d0\u05de\u05d9 \u05de\u05d9\u05d5.", "no_device_selected": "\u05dc\u05d0 \u05e0\u05d1\u05d7\u05e8 \u05d4\u05ea\u05e7\u05df, \u05e0\u05d0 \u05d1\u05d7\u05e8 \u05d4\u05ea\u05e7\u05df \u05d0\u05d7\u05d3.", - "unknown_device": "\u05d3\u05d2\u05dd \u05d4\u05d4\u05ea\u05e7\u05df \u05d0\u05d9\u05e0\u05d5 \u05d9\u05d3\u05d5\u05e2, \u05d0\u05d9\u05df \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05d6\u05e8\u05d9\u05de\u05ea \u05ea\u05e6\u05d5\u05e8\u05d4." + "unknown_device": "\u05d3\u05d2\u05dd \u05d4\u05d4\u05ea\u05e7\u05df \u05d0\u05d9\u05e0\u05d5 \u05d9\u05d3\u05d5\u05e2, \u05d0\u05d9\u05df \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05d6\u05e8\u05d9\u05de\u05ea \u05ea\u05e6\u05d5\u05e8\u05d4.", + "wrong_token": "\u05e9\u05d2\u05d9\u05d0\u05ea \u05d1\u05d3\u05d9\u05e7\u05ea \u05e1\u05d9\u05db\u05d5\u05dd, \u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05e9\u05d2\u05d5\u05d9" }, "flow_title": "{name}", "step": { From 9784523dfb6dea9f0c1ae94bd36237a9bddf19f6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 13 Dec 2021 02:15:37 +0100 Subject: [PATCH 0348/2644] Use new SensorDeviceClass enum in gios (#61609) Co-authored-by: epenet --- homeassistant/components/gios/const.py | 41 ++++++++++---------------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/gios/const.py b/homeassistant/components/gios/const.py index 9b98b0bda26..858a756e3e3 100644 --- a/homeassistant/components/gios/const.py +++ b/homeassistant/components/gios/const.py @@ -4,17 +4,8 @@ from __future__ import annotations from datetime import timedelta from typing import Final -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT -from homeassistant.const import ( - CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - DEVICE_CLASS_AQI, - DEVICE_CLASS_CO, - DEVICE_CLASS_NITROGEN_DIOXIDE, - DEVICE_CLASS_OZONE, - DEVICE_CLASS_PM10, - DEVICE_CLASS_PM25, - DEVICE_CLASS_SULPHUR_DIOXIDE, -) +from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass +from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER from .model import GiosSensorEntityDescription @@ -47,7 +38,7 @@ SENSOR_TYPES: Final[tuple[GiosSensorEntityDescription, ...]] = ( GiosSensorEntityDescription( key=ATTR_AQI, name="AQI", - device_class=DEVICE_CLASS_AQI, + device_class=SensorDeviceClass.AQI, value=None, ), GiosSensorEntityDescription( @@ -55,48 +46,48 @@ SENSOR_TYPES: Final[tuple[GiosSensorEntityDescription, ...]] = ( name="C6H6", icon="mdi:molecule", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), GiosSensorEntityDescription( key=ATTR_CO, name="CO", - device_class=DEVICE_CLASS_CO, + device_class=SensorDeviceClass.CO, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), GiosSensorEntityDescription( key=ATTR_NO2, name="NO2", - device_class=DEVICE_CLASS_NITROGEN_DIOXIDE, + device_class=SensorDeviceClass.NITROGEN_DIOXIDE, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), GiosSensorEntityDescription( key=ATTR_O3, name="O3", - device_class=DEVICE_CLASS_OZONE, + device_class=SensorDeviceClass.OZONE, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), GiosSensorEntityDescription( key=ATTR_PM10, name="PM10", - device_class=DEVICE_CLASS_PM10, + device_class=SensorDeviceClass.PM10, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), GiosSensorEntityDescription( key=ATTR_PM25, name="PM2.5", - device_class=DEVICE_CLASS_PM25, + device_class=SensorDeviceClass.PM25, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), GiosSensorEntityDescription( key=ATTR_SO2, name="SO2", - device_class=DEVICE_CLASS_SULPHUR_DIOXIDE, + device_class=SensorDeviceClass.SULPHUR_DIOXIDE, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), ) From 704be10561fb288a142e76f3d6685f77ccc91d53 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 13 Dec 2021 02:15:58 +0100 Subject: [PATCH 0349/2644] Use new BinarySensorDeviceClass in eight_sleep (#61610) Co-authored-by: epenet --- homeassistant/components/eight_sleep/binary_sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/eight_sleep/binary_sensor.py b/homeassistant/components/eight_sleep/binary_sensor.py index cb1b2e36f79..d2ca30ab580 100644 --- a/homeassistant/components/eight_sleep/binary_sensor.py +++ b/homeassistant/components/eight_sleep/binary_sensor.py @@ -6,7 +6,7 @@ import logging from pyeight.eight import EightSleep from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_OCCUPANCY, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.core import HomeAssistant @@ -61,7 +61,7 @@ class EightHeatSensor(EightSleepBaseEntity, BinarySensorEntity): ) -> None: """Initialize the sensor.""" super().__init__(name, coordinator, eight, side, sensor) - self._attr_device_class = DEVICE_CLASS_OCCUPANCY + self._attr_device_class = BinarySensorDeviceClass.OCCUPANCY assert self._usrobj _LOGGER.debug( "Presence Sensor: %s, Side: %s, User: %s", From c060b5926c00ac0a9575b48139b1bb7eda9b2fdd Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 13 Dec 2021 05:23:34 +0100 Subject: [PATCH 0350/2644] Use new SensorDeviceClass enum in glances (#61613) Co-authored-by: epenet --- homeassistant/components/glances/const.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/glances/const.py b/homeassistant/components/glances/const.py index 50f915ef4de..a25ae1b4660 100644 --- a/homeassistant/components/glances/const.py +++ b/homeassistant/components/glances/const.py @@ -4,14 +4,8 @@ from __future__ import annotations from dataclasses import dataclass import sys -from homeassistant.components.sensor import SensorEntityDescription -from homeassistant.const import ( - DATA_GIBIBYTES, - DATA_MEBIBYTES, - DEVICE_CLASS_TEMPERATURE, - PERCENTAGE, - TEMP_CELSIUS, -) +from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription +from homeassistant.const import DATA_GIBIBYTES, DATA_MEBIBYTES, PERCENTAGE, TEMP_CELSIUS DOMAIN = "glances" CONF_VERSION = "version" @@ -150,14 +144,14 @@ SENSOR_TYPES: tuple[GlancesSensorEntityDescription, ...] = ( type="sensors", name_suffix="Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), GlancesSensorEntityDescription( key="temperature_hdd", type="sensors", name_suffix="Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), GlancesSensorEntityDescription( key="fan_speed", From c69e479bfd6cc5de01f5ad809da6a05a814badcb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 13 Dec 2021 05:24:57 +0100 Subject: [PATCH 0351/2644] Use new enums in goalzero (#61518) Co-authored-by: epenet --- .../components/goalzero/binary_sensor.py | 15 +++-- homeassistant/components/goalzero/sensor.py | 55 ++++++++----------- 2 files changed, 31 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/goalzero/binary_sensor.py b/homeassistant/components/goalzero/binary_sensor.py index 56bbe1e2261..51883db6e60 100644 --- a/homeassistant/components/goalzero/binary_sensor.py +++ b/homeassistant/components/goalzero/binary_sensor.py @@ -4,15 +4,14 @@ from __future__ import annotations from typing import cast from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_BATTERY_CHARGING, - DEVICE_CLASS_CONNECTIVITY, - DEVICE_CLASS_POWER, + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME, ENTITY_CATEGORY_DIAGNOSTIC +from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -30,18 +29,18 @@ BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( BinarySensorEntityDescription( key="app_online", name="App Online", - device_class=DEVICE_CLASS_CONNECTIVITY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=EntityCategory.DIAGNOSTIC, ), BinarySensorEntityDescription( key="isCharging", name="Charging", - device_class=DEVICE_CLASS_BATTERY_CHARGING, + device_class=BinarySensorDeviceClass.BATTERY_CHARGING, ), BinarySensorEntityDescription( key="inputDetected", name="Input Detected", - device_class=DEVICE_CLASS_POWER, + device_class=BinarySensorDeviceClass.POWER, ), ) diff --git a/homeassistant/components/goalzero/sensor.py b/homeassistant/components/goalzero/sensor.py index 6677936b35a..34af3a89ad6 100644 --- a/homeassistant/components/goalzero/sensor.py +++ b/homeassistant/components/goalzero/sensor.py @@ -4,25 +4,17 @@ from __future__ import annotations from typing import cast from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_NAME, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_CURRENT, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_SIGNAL_STRENGTH, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_VOLTAGE, ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_WATT_HOUR, - ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, POWER_WATT, SIGNAL_STRENGTH_DECIBELS, @@ -31,6 +23,7 @@ from homeassistant.const import ( TIME_SECONDS, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -42,59 +35,59 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="wattsIn", name="Watts In", - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, native_unit_of_measurement=POWER_WATT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="ampsIn", name="Amps In", - device_class=DEVICE_CLASS_CURRENT, + device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), SensorEntityDescription( key="wattsOut", name="Watts Out", - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, native_unit_of_measurement=POWER_WATT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="ampsOut", name="Amps Out", - device_class=DEVICE_CLASS_CURRENT, + device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), SensorEntityDescription( key="whOut", name="WH Out", - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_WATT_HOUR, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, entity_registry_enabled_default=False, ), SensorEntityDescription( key="whStored", name="WH Stored", - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_WATT_HOUR, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="volts", name="Volts", - device_class=DEVICE_CLASS_VOLTAGE, + device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, entity_registry_enabled_default=False, ), SensorEntityDescription( key="socPercent", name="State of Charge Percent", - device_class=DEVICE_CLASS_BATTERY, + device_class=SensorDeviceClass.BATTERY, native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( @@ -106,36 +99,36 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="temperature", name="Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="wifiStrength", name="Wifi Strength", - device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, entity_registry_enabled_default=False, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="timestamp", name="Total Run Time", native_unit_of_measurement=TIME_SECONDS, entity_registry_enabled_default=False, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="ssid", name="Wi-Fi SSID", entity_registry_enabled_default=False, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="ipAddr", name="IP Address", entity_registry_enabled_default=False, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), ) From 8f11bcf4cc20548c6b919f43e4f00470c9cc5cc2 Mon Sep 17 00:00:00 2001 From: majuss Date: Mon, 13 Dec 2021 09:07:52 +0100 Subject: [PATCH 0352/2644] Upgrade lupupy to 0.0.24 (#61598) --- homeassistant/components/lupusec/manifest.json | 3 +-- requirements_all.txt | 3 +++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lupusec/manifest.json b/homeassistant/components/lupusec/manifest.json index ce200fe196a..126fa407a37 100644 --- a/homeassistant/components/lupusec/manifest.json +++ b/homeassistant/components/lupusec/manifest.json @@ -1,9 +1,8 @@ { - "disabled": "Library has incompatible requirements.", "domain": "lupusec", "name": "Lupus Electronics LUPUSEC", "documentation": "https://www.home-assistant.io/integrations/lupusec", - "requirements": ["lupupy==0.0.21"], + "requirements": ["lupupy==0.0.24"], "codeowners": ["@majuss"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 03dbf216377..b7e5853b20c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -977,6 +977,9 @@ london-tube-status==0.2 # homeassistant.components.luftdaten luftdaten==0.7.1 +# homeassistant.components.lupusec +lupupy==0.0.24 + # homeassistant.components.lw12wifi lw12==0.9.2 From 90e52cd3ad93d680954b9d9847762618be1241ac Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 13 Dec 2021 09:39:13 +0100 Subject: [PATCH 0353/2644] Use relative imports [S-Z] (#61576) --- .../components/samsungtv/media_player.py | 5 +--- .../components/select/device_trigger.py | 2 +- .../components/select/reproduce_state.py | 2 +- homeassistant/components/sensor/recorder.py | 27 ++++++++++--------- .../components/shelly/binary_sensor.py | 2 +- homeassistant/components/shelly/climate.py | 6 ++--- homeassistant/components/soma/cover.py | 3 ++- .../components/speedtestdotnet/sensor.py | 2 +- .../streamlabswater/binary_sensor.py | 3 ++- .../components/streamlabswater/sensor.py | 3 ++- homeassistant/components/supla/cover.py | 8 ++---- homeassistant/components/supla/switch.py | 8 ++---- .../components/template/binary_sensor.py | 2 +- homeassistant/components/template/number.py | 2 +- homeassistant/components/unifi/controller.py | 2 +- homeassistant/components/wallbox/sensor.py | 2 +- .../components/webostv/media_player.py | 17 ++++++------ .../components/websocket_api/commands.py | 2 +- .../components/withings/config_flow.py | 3 ++- .../components/yamaha_musiccast/number.py | 7 ++--- .../components/yamaha_musiccast/select.py | 7 ++--- homeassistant/components/youless/sensor.py | 3 ++- .../components/zha/alarm_control_panel.py | 2 +- homeassistant/components/zwave_js/climate.py | 4 +-- homeassistant/components/zwave_js/sensor.py | 4 +-- 25 files changed, 57 insertions(+), 71 deletions(-) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 8bbb97925f5..1c4ff6c3f83 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -22,10 +22,6 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, ) -from homeassistant.components.samsungtv.bridge import ( - SamsungTVLegacyBridge, - SamsungTVWSBridge, -) from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant @@ -37,6 +33,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.script import Script from homeassistant.util import dt as dt_util +from .bridge import SamsungTVLegacyBridge, SamsungTVWSBridge from .const import ( CONF_MANUFACTURER, CONF_MODEL, diff --git a/homeassistant/components/select/device_trigger.py b/homeassistant/components/select/device_trigger.py index 2c05b59c5d5..4def656c763 100644 --- a/homeassistant/components/select/device_trigger.py +++ b/homeassistant/components/select/device_trigger.py @@ -17,7 +17,6 @@ from homeassistant.components.homeassistant.triggers.state import ( async_attach_trigger as async_attach_state_trigger, async_validate_trigger_config as async_validate_state_trigger_config, ) -from homeassistant.components.select.const import ATTR_OPTIONS from homeassistant.const import ( CONF_DEVICE_ID, CONF_DOMAIN, @@ -31,6 +30,7 @@ from homeassistant.helpers.entity import get_capability from homeassistant.helpers.typing import ConfigType from . import DOMAIN +from .const import ATTR_OPTIONS TRIGGER_TYPES = {"current_option_changed"} diff --git a/homeassistant/components/select/reproduce_state.py b/homeassistant/components/select/reproduce_state.py index d41fd5dae46..2ff65c11fa3 100644 --- a/homeassistant/components/select/reproduce_state.py +++ b/homeassistant/components/select/reproduce_state.py @@ -6,11 +6,11 @@ from collections.abc import Iterable import logging from typing import Any -from homeassistant.components.select.const import ATTR_OPTIONS from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import Context, HomeAssistant, State from . import ATTR_OPTION, DOMAIN, SERVICE_SELECT_OPTION +from .const import ATTR_OPTIONS _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 3cfe8d45b70..9b6c07a323d 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -23,18 +23,6 @@ from homeassistant.components.recorder.models import ( StatisticMetaData, StatisticResult, ) -from homeassistant.components.sensor import ( - ATTR_STATE_CLASS, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_GAS, - DEVICE_CLASS_MONETARY, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL, - STATE_CLASS_TOTAL_INCREASING, - STATE_CLASSES, -) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, @@ -65,7 +53,20 @@ import homeassistant.util.pressure as pressure_util import homeassistant.util.temperature as temperature_util import homeassistant.util.volume as volume_util -from . import ATTR_LAST_RESET, DOMAIN +from . import ( + ATTR_LAST_RESET, + ATTR_STATE_CLASS, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_GAS, + DEVICE_CLASS_MONETARY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + DOMAIN, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL, + STATE_CLASS_TOTAL_INCREASING, + STATE_CLASSES, +) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index 4a918506b00..737efad923c 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -17,12 +17,12 @@ from homeassistant.components.binary_sensor import ( STATE_ON, BinarySensorEntity, ) -from homeassistant.components.shelly.const import CONF_SLEEP_PERIOD from homeassistant.config_entries import ConfigEntry from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from .const import CONF_SLEEP_PERIOD from .entity import ( BlockAttributeDescription, RestAttributeDescription, diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index f2db157ecf2..79d233336e3 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -19,15 +19,13 @@ from homeassistant.components.climate.const import ( SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.components.shelly import BlockDeviceWrapper -from homeassistant.components.shelly.entity import ShellyBlockEntity -from homeassistant.components.shelly.utils import get_device_entry_gen from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity +from . import BlockDeviceWrapper from .const import ( AIOSHELLY_DEVICE_TIMEOUT_SEC, BLOCK, @@ -35,6 +33,8 @@ from .const import ( DOMAIN, SHTRV_01_TEMPERATURE_SETTINGS, ) +from .entity import ShellyBlockEntity +from .utils import get_device_entry_gen _LOGGER: Final = logging.getLogger(__name__) diff --git a/homeassistant/components/soma/cover.py b/homeassistant/components/soma/cover.py index 43ea60d372e..d1f5930c02b 100644 --- a/homeassistant/components/soma/cover.py +++ b/homeassistant/components/soma/cover.py @@ -5,7 +5,8 @@ import logging from requests import RequestException from homeassistant.components.cover import ATTR_POSITION, CoverEntity -from homeassistant.components.soma import API, DEVICES, DOMAIN, SomaEntity + +from . import API, DEVICES, DOMAIN, SomaEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/speedtestdotnet/sensor.py b/homeassistant/components/speedtestdotnet/sensor.py index 10071bf9054..aa4cd72f746 100644 --- a/homeassistant/components/speedtestdotnet/sensor.py +++ b/homeassistant/components/speedtestdotnet/sensor.py @@ -4,7 +4,6 @@ from __future__ import annotations from typing import Any, cast from homeassistant.components.sensor import SensorEntity -from homeassistant.components.speedtestdotnet import SpeedTestDataCoordinator from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import HomeAssistant @@ -15,6 +14,7 @@ from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import SpeedTestDataCoordinator from .const import ( ATTR_BYTES_RECEIVED, ATTR_BYTES_SENT, diff --git a/homeassistant/components/streamlabswater/binary_sensor.py b/homeassistant/components/streamlabswater/binary_sensor.py index a25f5e124e6..d464d686bd7 100644 --- a/homeassistant/components/streamlabswater/binary_sensor.py +++ b/homeassistant/components/streamlabswater/binary_sensor.py @@ -3,9 +3,10 @@ from datetime import timedelta from homeassistant.components.binary_sensor import BinarySensorEntity -from homeassistant.components.streamlabswater import DOMAIN as STREAMLABSWATER_DOMAIN from homeassistant.util import Throttle +from . import DOMAIN as STREAMLABSWATER_DOMAIN + DEPENDS = ["streamlabswater"] MIN_TIME_BETWEEN_LOCATION_UPDATES = timedelta(seconds=60) diff --git a/homeassistant/components/streamlabswater/sensor.py b/homeassistant/components/streamlabswater/sensor.py index 3af87b8a3f8..923b94a4ce2 100644 --- a/homeassistant/components/streamlabswater/sensor.py +++ b/homeassistant/components/streamlabswater/sensor.py @@ -3,10 +3,11 @@ from datetime import timedelta from homeassistant.components.sensor import SensorEntity -from homeassistant.components.streamlabswater import DOMAIN as STREAMLABSWATER_DOMAIN from homeassistant.const import VOLUME_GALLONS from homeassistant.util import Throttle +from . import DOMAIN as STREAMLABSWATER_DOMAIN + DEPENDENCIES = ["streamlabswater"] WATER_ICON = "mdi:water" diff --git a/homeassistant/components/supla/cover.py b/homeassistant/components/supla/cover.py index a4c2fc95792..c76b28e9de0 100644 --- a/homeassistant/components/supla/cover.py +++ b/homeassistant/components/supla/cover.py @@ -7,12 +7,8 @@ from homeassistant.components.cover import ( DEVICE_CLASS_GARAGE, CoverEntity, ) -from homeassistant.components.supla import ( - DOMAIN, - SUPLA_COORDINATORS, - SUPLA_SERVERS, - SuplaChannel, -) + +from . import DOMAIN, SUPLA_COORDINATORS, SUPLA_SERVERS, SuplaChannel _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/supla/switch.py b/homeassistant/components/supla/switch.py index 32ff208f10e..cabc934b52e 100644 --- a/homeassistant/components/supla/switch.py +++ b/homeassistant/components/supla/switch.py @@ -2,14 +2,10 @@ import logging from pprint import pformat -from homeassistant.components.supla import ( - DOMAIN, - SUPLA_COORDINATORS, - SUPLA_SERVERS, - SuplaChannel, -) from homeassistant.components.switch import SwitchEntity +from . import DOMAIN, SUPLA_COORDINATORS, SUPLA_SERVERS, SuplaChannel + _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index 06af6dca925..c3557ba21f4 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -15,7 +15,6 @@ from homeassistant.components.binary_sensor import ( PLATFORM_SCHEMA, BinarySensorEntity, ) -from homeassistant.components.template import TriggerUpdateCoordinator from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, @@ -39,6 +38,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.event import async_call_later +from . import TriggerUpdateCoordinator from .const import ( CONF_ATTRIBUTES, CONF_AVAILABILITY, diff --git a/homeassistant/components/template/number.py b/homeassistant/components/template/number.py index 90a944a2d33..da2272c3138 100644 --- a/homeassistant/components/template/number.py +++ b/homeassistant/components/template/number.py @@ -17,7 +17,6 @@ from homeassistant.components.number.const import ( DEFAULT_MIN_VALUE, DOMAIN as NUMBER_DOMAIN, ) -from homeassistant.components.template import TriggerUpdateCoordinator from homeassistant.const import ( CONF_ICON, CONF_NAME, @@ -31,6 +30,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.script import Script from homeassistant.helpers.template import Template, TemplateError +from . import TriggerUpdateCoordinator from .const import CONF_AVAILABILITY, DOMAIN from .template_entity import TemplateEntity from .trigger_entity import TriggerEntity diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index 98eb377dab3..209ff9980c9 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -26,7 +26,6 @@ from aiounifi.events import ( from aiounifi.websocket import STATE_DISCONNECTED, STATE_RUNNING import async_timeout -from homeassistant.components.unifi.switch import BLOCK_SWITCH, POE_SWITCH from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -70,6 +69,7 @@ from .const import ( UNIFI_WIRELESS_CLIENTS, ) from .errors import AuthenticationRequired, CannotConnect +from .switch import BLOCK_SWITCH, POE_SWITCH RETRY_TIMER = 15 CHECK_HEARTBEAT_INTERVAL = timedelta(seconds=1) diff --git a/homeassistant/components/wallbox/sensor.py b/homeassistant/components/wallbox/sensor.py index 835a8c405da..dd45c237c45 100644 --- a/homeassistant/components/wallbox/sensor.py +++ b/homeassistant/components/wallbox/sensor.py @@ -11,7 +11,6 @@ from homeassistant.components.sensor import ( SensorEntity, SensorEntityDescription, ) -from homeassistant.components.wallbox import WallboxCoordinator from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DEVICE_CLASS_BATTERY, @@ -29,6 +28,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import WallboxCoordinator from .const import ( CONF_ADDED_ENERGY_KEY, CONF_ADDED_RANGE_KEY, diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index 36480e90f12..60768b6857f 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -24,14 +24,6 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, ) -from homeassistant.components.webostv.const import ( - ATTR_PAYLOAD, - ATTR_SOUND_OUTPUT, - CONF_ON_ACTION, - CONF_SOURCES, - DOMAIN, - LIVE_TV_APP_ID, -) from homeassistant.const import ( ATTR_ENTITY_ID, CONF_CUSTOMIZE, @@ -45,6 +37,15 @@ from homeassistant.const import ( from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.script import Script +from .const import ( + ATTR_PAYLOAD, + ATTR_SOUND_OUTPUT, + CONF_ON_ACTION, + CONF_SOURCES, + DOMAIN, + LIVE_TV_APP_ID, +) + _LOGGER = logging.getLogger(__name__) SUPPORT_WEBOSTV = ( diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index a4abbd30dff..aa38f8c8e3e 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -10,7 +10,6 @@ import voluptuous as vol from homeassistant.auth.permissions.const import CAT_ENTITIES, POLICY_READ from homeassistant.bootstrap import SIGNAL_BOOTSTRAP_INTEGRATONS -from homeassistant.components.websocket_api.const import ERR_NOT_FOUND from homeassistant.const import EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL from homeassistant.core import Context, Event, HomeAssistant, callback from homeassistant.exceptions import ( @@ -33,6 +32,7 @@ from homeassistant.setup import DATA_SETUP_TIME, async_get_loaded_integrations from . import const, decorators, messages from .connection import ActiveConnection +from .const import ERR_NOT_FOUND @callback diff --git a/homeassistant/components/withings/config_flow.py b/homeassistant/components/withings/config_flow.py index 29c1e162ed4..b447973af97 100644 --- a/homeassistant/components/withings/config_flow.py +++ b/homeassistant/components/withings/config_flow.py @@ -6,11 +6,12 @@ import logging import voluptuous as vol from withings_api.common import AuthScope -from homeassistant.components.withings import const from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.util import slugify +from . import const + class WithingsFlowHandler( config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=const.DOMAIN diff --git a/homeassistant/components/yamaha_musiccast/number.py b/homeassistant/components/yamaha_musiccast/number.py index daef8bacd12..2648359f768 100644 --- a/homeassistant/components/yamaha_musiccast/number.py +++ b/homeassistant/components/yamaha_musiccast/number.py @@ -3,15 +3,12 @@ from aiomusiccast.capabilities import NumberSetter from homeassistant.components.number import NumberEntity -from homeassistant.components.yamaha_musiccast import ( - DOMAIN, - MusicCastCapabilityEntity, - MusicCastDataUpdateCoordinator, -) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import DOMAIN, MusicCastCapabilityEntity, MusicCastDataUpdateCoordinator + async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/yamaha_musiccast/select.py b/homeassistant/components/yamaha_musiccast/select.py index 6c808c3cced..68be67eac10 100644 --- a/homeassistant/components/yamaha_musiccast/select.py +++ b/homeassistant/components/yamaha_musiccast/select.py @@ -3,15 +3,12 @@ from aiomusiccast.capabilities import OptionSetter from homeassistant.components.select import SelectEntity -from homeassistant.components.yamaha_musiccast import ( - DOMAIN, - MusicCastCapabilityEntity, - MusicCastDataUpdateCoordinator, -) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import DOMAIN, MusicCastCapabilityEntity, MusicCastDataUpdateCoordinator + async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/youless/sensor.py b/homeassistant/components/youless/sensor.py index 355d4f83127..a1e094b8c70 100644 --- a/homeassistant/components/youless/sensor.py +++ b/homeassistant/components/youless/sensor.py @@ -9,7 +9,6 @@ from homeassistant.components.sensor import ( STATE_CLASS_TOTAL_INCREASING, SensorEntity, ) -from homeassistant.components.youless import DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_DEVICE, @@ -29,6 +28,8 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, ) +from . import DOMAIN + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback diff --git a/homeassistant/components/zha/alarm_control_panel.py b/homeassistant/components/zha/alarm_control_panel.py index 3e83a5cd3d1..4d4cba3599c 100644 --- a/homeassistant/components/zha/alarm_control_panel.py +++ b/homeassistant/components/zha/alarm_control_panel.py @@ -11,7 +11,6 @@ from homeassistant.components.alarm_control_panel import ( SUPPORT_ALARM_TRIGGER, AlarmControlPanelEntity, ) -from homeassistant.components.zha.core.typing import ZhaDeviceType from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, @@ -42,6 +41,7 @@ from .core.const import ( ) from .core.helpers import async_get_zha_config_value from .core.registries import ZHA_ENTITIES +from .core.typing import ZhaDeviceType from .entity import ZhaEntity STRICT_MATCH = functools.partial( diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index f9c94cc938d..0c779eeb78f 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -45,9 +45,6 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, ) -from homeassistant.components.zwave_js.discovery_data_template import ( - DynamicCurrentTempClimateDataTemplate, -) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_TEMPERATURE, @@ -62,6 +59,7 @@ from homeassistant.helpers.temperature import convert_temperature from .const import DATA_CLIENT, DOMAIN from .discovery import ZwaveDiscoveryInfo +from .discovery_data_template import DynamicCurrentTempClimateDataTemplate from .entity import ZWaveBaseEntity from .helpers import get_value_of_zwave_value diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 549df9f6264..86901b94d3d 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -24,9 +24,6 @@ from homeassistant.components.sensor import ( SensorEntity, SensorEntityDescription, ) -from homeassistant.components.zwave_js.discovery_data_template import ( - NumericSensorDataTemplateData, -) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DEVICE_CLASS_BATTERY, @@ -75,6 +72,7 @@ from .const import ( SERVICE_RESET_METER, ) from .discovery import ZwaveDiscoveryInfo +from .discovery_data_template import NumericSensorDataTemplateData from .entity import ZWaveBaseEntity from .helpers import get_device_id From ff015d4ea4116edb1253a0fa35b4ca324e60ebab Mon Sep 17 00:00:00 2001 From: Mark Adkins Date: Mon, 13 Dec 2021 06:10:31 -0500 Subject: [PATCH 0354/2644] Bump blinkpy to 0.18.0 (#61538) --- homeassistant/components/blink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/blink/manifest.json b/homeassistant/components/blink/manifest.json index 7172406d671..b90e7e845cf 100644 --- a/homeassistant/components/blink/manifest.json +++ b/homeassistant/components/blink/manifest.json @@ -2,7 +2,7 @@ "domain": "blink", "name": "Blink", "documentation": "https://www.home-assistant.io/integrations/blink", - "requirements": ["blinkpy==0.17.0"], + "requirements": ["blinkpy==0.18.0"], "codeowners": ["@fronzbot"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index b7e5853b20c..0090b6b4332 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -402,7 +402,7 @@ bizkaibus==0.1.1 blebox_uniapi==1.3.3 # homeassistant.components.blink -blinkpy==0.17.0 +blinkpy==0.18.0 # homeassistant.components.blinksticklight blinkstick==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 473f1a3d44b..ca72e4e447b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -269,7 +269,7 @@ bimmer_connected==0.8.5 blebox_uniapi==1.3.3 # homeassistant.components.blink -blinkpy==0.17.0 +blinkpy==0.18.0 # homeassistant.components.bond bond-api==0.1.15 From c8f2d4a82bcaab59bfb453793d4246c9e7e105bc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 13 Dec 2021 12:57:16 +0100 Subject: [PATCH 0355/2644] Use new enums in freedompro (#61445) * Use new enums in freedompro * Also update PLATFORMS Co-authored-by: epenet --- .../components/freedompro/__init__.py | 23 +++++++++++-------- .../components/freedompro/binary_sensor.py | 13 ++++------- homeassistant/components/freedompro/cover.py | 16 +++++-------- homeassistant/components/freedompro/sensor.py | 16 ++++++------- 4 files changed, 31 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/freedompro/__init__.py b/homeassistant/components/freedompro/__init__.py index 6a6253f2e06..327b6314a34 100644 --- a/homeassistant/components/freedompro/__init__.py +++ b/homeassistant/components/freedompro/__init__.py @@ -1,11 +1,14 @@ """Support for freedompro.""" +from __future__ import annotations + from datetime import timedelta import logging +from typing import Final from pyfreedompro import get_list, get_states from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY +from homeassistant.const import CONF_API_KEY, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -14,15 +17,15 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORMS = [ - "binary_sensor", - "climate", - "cover", - "fan", - "light", - "lock", - "sensor", - "switch", +PLATFORMS: Final[list[Platform]] = [ + Platform.BINARY_SENSOR, + Platform.CLIMATE, + Platform.COVER, + Platform.FAN, + Platform.LIGHT, + Platform.LOCK, + Platform.SENSOR, + Platform.SWITCH, ] diff --git a/homeassistant/components/freedompro/binary_sensor.py b/homeassistant/components/freedompro/binary_sensor.py index ac70824be4c..b69f942e532 100644 --- a/homeassistant/components/freedompro/binary_sensor.py +++ b/homeassistant/components/freedompro/binary_sensor.py @@ -1,9 +1,6 @@ """Support for Freedompro binary_sensor.""" from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_MOTION, - DEVICE_CLASS_OCCUPANCY, - DEVICE_CLASS_OPENING, - DEVICE_CLASS_SMOKE, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.core import callback @@ -13,10 +10,10 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN DEVICE_CLASS_MAP = { - "smokeSensor": DEVICE_CLASS_SMOKE, - "occupancySensor": DEVICE_CLASS_OCCUPANCY, - "motionSensor": DEVICE_CLASS_MOTION, - "contactSensor": DEVICE_CLASS_OPENING, + "smokeSensor": BinarySensorDeviceClass.SMOKE, + "occupancySensor": BinarySensorDeviceClass.OCCUPANCY, + "motionSensor": BinarySensorDeviceClass.MOTION, + "contactSensor": BinarySensorDeviceClass.OPENING, } DEVICE_KEY_MAP = { diff --git a/homeassistant/components/freedompro/cover.py b/homeassistant/components/freedompro/cover.py index fd6c747da46..2131069eb0e 100644 --- a/homeassistant/components/freedompro/cover.py +++ b/homeassistant/components/freedompro/cover.py @@ -5,14 +5,10 @@ from pyfreedompro import put_state from homeassistant.components.cover import ( ATTR_POSITION, - DEVICE_CLASS_BLIND, - DEVICE_CLASS_DOOR, - DEVICE_CLASS_GARAGE, - DEVICE_CLASS_GATE, - DEVICE_CLASS_WINDOW, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION, + CoverDeviceClass, CoverEntity, ) from homeassistant.const import CONF_API_KEY @@ -24,11 +20,11 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN DEVICE_CLASS_MAP = { - "windowCovering": DEVICE_CLASS_BLIND, - "gate": DEVICE_CLASS_GATE, - "garageDoor": DEVICE_CLASS_GARAGE, - "door": DEVICE_CLASS_DOOR, - "window": DEVICE_CLASS_WINDOW, + "windowCovering": CoverDeviceClass.BLIND, + "gate": CoverDeviceClass.GATE, + "garageDoor": CoverDeviceClass.GARAGE, + "door": CoverDeviceClass.DOOR, + "window": CoverDeviceClass.WINDOW, } SUPPORTED_SENSORS = {"windowCovering", "gate", "garageDoor", "door", "window"} diff --git a/homeassistant/components/freedompro/sensor.py b/homeassistant/components/freedompro/sensor.py index 74b54474dbd..04fe7ecddeb 100644 --- a/homeassistant/components/freedompro/sensor.py +++ b/homeassistant/components/freedompro/sensor.py @@ -1,10 +1,8 @@ """Support for Freedompro sensor.""" from homeassistant.components.sensor import ( - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + SensorDeviceClass, SensorEntity, + SensorStateClass, ) from homeassistant.const import LIGHT_LUX, PERCENTAGE, TEMP_CELSIUS from homeassistant.core import callback @@ -14,13 +12,13 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN DEVICE_CLASS_MAP = { - "temperatureSensor": DEVICE_CLASS_TEMPERATURE, - "humiditySensor": DEVICE_CLASS_HUMIDITY, - "lightSensor": DEVICE_CLASS_ILLUMINANCE, + "temperatureSensor": SensorDeviceClass.TEMPERATURE, + "humiditySensor": SensorDeviceClass.HUMIDITY, + "lightSensor": SensorDeviceClass.ILLUMINANCE, } STATE_CLASS_MAP = { - "temperatureSensor": STATE_CLASS_MEASUREMENT, - "humiditySensor": STATE_CLASS_MEASUREMENT, + "temperatureSensor": SensorStateClass.MEASUREMENT, + "humiditySensor": SensorStateClass.MEASUREMENT, "lightSensor": None, } UNIT_MAP = { From d6f48683a3951ff50a6af389a293f7bcb023c835 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 13 Dec 2021 14:09:49 +0100 Subject: [PATCH 0356/2644] Use platform enum (8) [Misc] (#61013) --- homeassistant/components/axis/const.py | 7 ++-- homeassistant/components/firmata/const.py | 9 ++--- homeassistant/components/hive/const.py | 30 ++++++++-------- .../components/home_plus_control/__init__.py | 4 +-- homeassistant/components/hyperion/__init__.py | 7 ++-- .../components/iaqualink/__init__.py | 12 +++---- homeassistant/components/insteon/const.py | 14 ++++---- homeassistant/components/mysensors/const.py | 24 +++++++------ .../components/panasonic_viera/__init__.py | 13 ++++--- homeassistant/components/risco/__init__.py | 3 +- .../components/switcher_kis/__init__.py | 4 +-- homeassistant/components/tasmota/const.py | 14 ++++---- homeassistant/components/vera/__init__.py | 34 +++++++++---------- homeassistant/components/vera/common.py | 11 +++--- homeassistant/components/vesync/__init__.py | 8 ++--- 15 files changed, 102 insertions(+), 92 deletions(-) diff --git a/homeassistant/components/axis/const.py b/homeassistant/components/axis/const.py index a1ce77f099b..c9267568707 100644 --- a/homeassistant/components/axis/const.py +++ b/homeassistant/components/axis/const.py @@ -1,10 +1,7 @@ """Constants for the Axis component.""" import logging -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN -from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import Platform LOGGER = logging.getLogger(__package__) @@ -22,4 +19,4 @@ DEFAULT_STREAM_PROFILE = "No stream profile" DEFAULT_TRIGGER_TIME = 0 DEFAULT_VIDEO_SOURCE = "No video source" -PLATFORMS = [BINARY_SENSOR_DOMAIN, CAMERA_DOMAIN, LIGHT_DOMAIN, SWITCH_DOMAIN] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.CAMERA, Platform.LIGHT, Platform.SWITCH] diff --git a/homeassistant/components/firmata/const.py b/homeassistant/components/firmata/const.py index 0d859363e2b..091d724229c 100644 --- a/homeassistant/components/firmata/const.py +++ b/homeassistant/components/firmata/const.py @@ -4,6 +4,7 @@ from homeassistant.const import ( CONF_LIGHTS, CONF_SENSORS, CONF_SWITCHES, + Platform, ) CONF_ARDUINO_INSTANCE_ID = "arduino_instance_id" @@ -27,8 +28,8 @@ CONF_SLEEP_TUNE = "sleep_tune" DOMAIN = "firmata" FIRMATA_MANUFACTURER = "Firmata" CONF_PLATFORM_MAP = { - CONF_BINARY_SENSORS: "binary_sensor", - CONF_LIGHTS: "light", - CONF_SENSORS: "sensor", - CONF_SWITCHES: "switch", + CONF_BINARY_SENSORS: Platform.BINARY_SENSOR, + CONF_LIGHTS: Platform.LIGHT, + CONF_SENSORS: Platform.SENSOR, + CONF_SWITCHES: Platform.SWITCH, } diff --git a/homeassistant/components/hive/const.py b/homeassistant/components/hive/const.py index f24ed0f7b24..82c07761eef 100644 --- a/homeassistant/components/hive/const.py +++ b/homeassistant/components/hive/const.py @@ -1,4 +1,6 @@ """Constants for Hive.""" +from homeassistant.const import Platform + ATTR_MODE = "mode" ATTR_TIME_PERIOD = "time_period" ATTR_ONOFF = "on_off" @@ -7,22 +9,22 @@ CONFIG_ENTRY_VERSION = 1 DEFAULT_NAME = "Hive" DOMAIN = "hive" PLATFORMS = [ - "alarm_control_panel", - "binary_sensor", - "climate", - "light", - "sensor", - "switch", - "water_heater", + Platform.ALARM_CONTROL_PANEL, + Platform.BINARY_SENSOR, + Platform.CLIMATE, + Platform.LIGHT, + Platform.SENSOR, + Platform.SWITCH, + Platform.WATER_HEATER, ] PLATFORM_LOOKUP = { - "alarm_control_panel": "alarm_control_panel", - "binary_sensor": "binary_sensor", - "climate": "climate", - "light": "light", - "sensor": "sensor", - "switch": "switch", - "water_heater": "water_heater", + Platform.ALARM_CONTROL_PANEL: "alarm_control_panel", + Platform.BINARY_SENSOR: "binary_sensor", + Platform.CLIMATE: "climate", + Platform.LIGHT: "light", + Platform.SENSOR: "sensor", + Platform.SWITCH: "switch", + Platform.WATER_HEATER: "water_heater", } SERVICE_BOOST_HOT_WATER = "boost_hot_water" SERVICE_BOOST_HEATING_ON = "boost_heating_on" diff --git a/homeassistant/components/home_plus_control/__init__.py b/homeassistant/components/home_plus_control/__init__.py index ffb055e6324..78e31c83caa 100644 --- a/homeassistant/components/home_plus_control/__init__.py +++ b/homeassistant/components/home_plus_control/__init__.py @@ -8,7 +8,7 @@ from homepluscontrol.homeplusapi import HomePlusControlApiError import voluptuous as vol from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import ( config_entry_oauth2_flow, @@ -46,7 +46,7 @@ CONFIG_SCHEMA = vol.Schema( ) # The Legrand Home+ Control platform is currently limited to "switch" entities -PLATFORMS = ["switch"] +PLATFORMS = [Platform.SWITCH] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hyperion/__init__.py b/homeassistant/components/hyperion/__init__.py index b43b25ca5ac..292c426ba19 100644 --- a/homeassistant/components/hyperion/__init__.py +++ b/homeassistant/components/hyperion/__init__.py @@ -10,11 +10,8 @@ from typing import Any, cast from awesomeversion import AwesomeVersion from hyperion import client, const as hyperion_const -from homeassistant.components.camera.const import DOMAIN as CAMERA_DOMAIN -from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr @@ -36,7 +33,7 @@ from .const import ( SIGNAL_INSTANCE_REMOVE, ) -PLATFORMS = [LIGHT_DOMAIN, SWITCH_DOMAIN, CAMERA_DOMAIN] +PLATFORMS = [Platform.LIGHT, Platform.SWITCH, Platform.CAMERA] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index 856674f1345..61f2c906c17 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -25,7 +25,7 @@ from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -135,19 +135,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: forward_setup = hass.config_entries.async_forward_entry_setup if binary_sensors: _LOGGER.debug("Got %s binary sensors: %s", len(binary_sensors), binary_sensors) - hass.async_create_task(forward_setup(entry, BINARY_SENSOR_DOMAIN)) + hass.async_create_task(forward_setup(entry, Platform.BINARY_SENSOR)) if climates: _LOGGER.debug("Got %s climates: %s", len(climates), climates) - hass.async_create_task(forward_setup(entry, CLIMATE_DOMAIN)) + hass.async_create_task(forward_setup(entry, Platform.CLIMATE)) if lights: _LOGGER.debug("Got %s lights: %s", len(lights), lights) - hass.async_create_task(forward_setup(entry, LIGHT_DOMAIN)) + hass.async_create_task(forward_setup(entry, Platform.LIGHT)) if sensors: _LOGGER.debug("Got %s sensors: %s", len(sensors), sensors) - hass.async_create_task(forward_setup(entry, SENSOR_DOMAIN)) + hass.async_create_task(forward_setup(entry, Platform.SENSOR)) if switches: _LOGGER.debug("Got %s switches: %s", len(switches), switches) - hass.async_create_task(forward_setup(entry, SWITCH_DOMAIN)) + hass.async_create_task(forward_setup(entry, Platform.SWITCH)) async def _async_systems_update(now): """Refresh internal state for all systems.""" diff --git a/homeassistant/components/insteon/const.py b/homeassistant/components/insteon/const.py index dca53d20369..d93d03847a1 100644 --- a/homeassistant/components/insteon/const.py +++ b/homeassistant/components/insteon/const.py @@ -34,15 +34,17 @@ from pyinsteon.groups import ( TEST_SENSOR, ) +from homeassistant.const import Platform + DOMAIN = "insteon" INSTEON_PLATFORMS = [ - "binary_sensor", - "climate", - "cover", - "fan", - "light", - "switch", + Platform.BINARY_SENSOR, + Platform.CLIMATE, + Platform.COVER, + Platform.FAN, + Platform.LIGHT, + Platform.SWITCH, ] X10_PLATFORMS = [ diff --git a/homeassistant/components/mysensors/const.py b/homeassistant/components/mysensors/const.py index f8e157e3622..9feca1e6369 100644 --- a/homeassistant/components/mysensors/const.py +++ b/homeassistant/components/mysensors/const.py @@ -4,6 +4,8 @@ from __future__ import annotations from collections import defaultdict from typing import Final, Literal, Tuple, TypedDict +from homeassistant.const import Platform + ATTR_DEVICES: Final = "devices" ATTR_GATEWAY_ID: Final = "gateway_id" @@ -144,15 +146,15 @@ SWITCH_TYPES: dict[SensorType, set[ValueType]] = { } -PLATFORM_TYPES: dict[str, dict[SensorType, set[ValueType]]] = { - "binary_sensor": BINARY_SENSOR_TYPES, - "climate": CLIMATE_TYPES, - "cover": COVER_TYPES, - "device_tracker": DEVICE_TRACKER_TYPES, - "light": LIGHT_TYPES, - "notify": NOTIFY_TYPES, - "sensor": SENSOR_TYPES, - "switch": SWITCH_TYPES, +PLATFORM_TYPES: dict[Platform, dict[SensorType, set[ValueType]]] = { + Platform.BINARY_SENSOR: BINARY_SENSOR_TYPES, + Platform.CLIMATE: CLIMATE_TYPES, + Platform.COVER: COVER_TYPES, + Platform.DEVICE_TRACKER: DEVICE_TRACKER_TYPES, + Platform.LIGHT: LIGHT_TYPES, + Platform.NOTIFY: NOTIFY_TYPES, + Platform.SENSOR: SENSOR_TYPES, + Platform.SWITCH: SWITCH_TYPES, } FLAT_PLATFORM_TYPES: dict[tuple[str, SensorType], set[ValueType]] = { @@ -168,6 +170,6 @@ for platform, platform_types in PLATFORM_TYPES.items(): TYPE_TO_PLATFORMS[s_type_name].append(platform) PLATFORMS_WITH_ENTRY_SUPPORT = set(PLATFORM_TYPES.keys()) - { - "notify", - "device_tracker", + Platform.NOTIFY, + Platform.DEVICE_TRACKER, } diff --git a/homeassistant/components/panasonic_viera/__init__.py b/homeassistant/components/panasonic_viera/__init__.py index 419181da2d9..7448097ea2c 100644 --- a/homeassistant/components/panasonic_viera/__init__.py +++ b/homeassistant/components/panasonic_viera/__init__.py @@ -6,10 +6,15 @@ from urllib.error import HTTPError, URLError from panasonic_viera import EncryptionRequired, Keys, RemoteControl, SOAPError import voluptuous as vol -from homeassistant.components.media_player.const import DOMAIN as MEDIA_PLAYER_DOMAIN -from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_ON +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PORT, + STATE_OFF, + STATE_ON, + Platform, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.script import Script @@ -46,7 +51,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -PLATFORMS = [MEDIA_PLAYER_DOMAIN, REMOTE_DOMAIN] +PLATFORMS = [Platform.MEDIA_PLAYER, Platform.REMOTE] async def async_setup(hass, config): diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py index 0e61d6c7aeb..2e37499b10f 100644 --- a/homeassistant/components/risco/__init__.py +++ b/homeassistant/components/risco/__init__.py @@ -11,6 +11,7 @@ from homeassistant.const import ( CONF_PIN, CONF_SCAN_INTERVAL, CONF_USERNAME, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -20,7 +21,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DATA_COORDINATOR, DEFAULT_SCAN_INTERVAL, DOMAIN, EVENTS_COORDINATOR -PLATFORMS = ["alarm_control_panel", "binary_sensor", "sensor"] +PLATFORMS = [Platform.ALARM_CONTROL_PANEL, Platform.BINARY_SENSOR, Platform.SENSOR] UNDO_UPDATE_LISTENER = "undo_update_listener" LAST_EVENT_STORAGE_VERSION = 1 LAST_EVENT_TIMESTAMP_KEY = "last_event_timestamp" diff --git a/homeassistant/components/switcher_kis/__init__.py b/homeassistant/components/switcher_kis/__init__.py index 7be726388f0..d4779e8e748 100644 --- a/homeassistant/components/switcher_kis/__init__.py +++ b/homeassistant/components/switcher_kis/__init__.py @@ -9,7 +9,7 @@ from aioswitcher.device import SwitcherBase import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_DEVICE_ID, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import CONF_DEVICE_ID, EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import ( config_validation as cv, @@ -30,7 +30,7 @@ from .const import ( ) from .utils import async_start_bridge, async_stop_bridge -PLATFORMS = ["switch", "sensor"] +PLATFORMS = [Platform.SWITCH, Platform.SENSOR] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tasmota/const.py b/homeassistant/components/tasmota/const.py index 48026f3e93d..2e38284e43d 100644 --- a/homeassistant/components/tasmota/const.py +++ b/homeassistant/components/tasmota/const.py @@ -1,4 +1,6 @@ """Constants used by multiple Tasmota modules.""" +from homeassistant.const import Platform + CONF_DISCOVERY_PREFIX = "discovery_prefix" DATA_REMOVE_DISCOVER_COMPONENT = "tasmota_discover_{}" @@ -9,12 +11,12 @@ DEFAULT_PREFIX = "tasmota/discovery" DOMAIN = "tasmota" PLATFORMS = [ - "binary_sensor", - "cover", - "fan", - "light", - "sensor", - "switch", + Platform.BINARY_SENSOR, + Platform.COVER, + Platform.FAN, + Platform.LIGHT, + Platform.SENSOR, + Platform.SWITCH, ] TASMOTA_EVENT = "tasmota_event" diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index 3b7c11b943e..bbc6902007a 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -20,6 +20,7 @@ from homeassistant.const import ( CONF_EXCLUDE, CONF_LIGHTS, EVENT_HOMEASSISTANT_STOP, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -124,7 +125,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Exclude devices unwanted by user. devices = [device for device in all_devices if device.device_id not in exclude_ids] - vera_devices = defaultdict(list) + vera_devices: defaultdict[Platform, list[veraApi.VeraDevice]] = defaultdict(list) for device in devices: device_type = map_vera_device(device, light_ids) if device_type is not None: @@ -144,10 +145,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: set_controller_data(hass, entry, controller_data) # Forward the config data to the necessary platforms. - for platform in get_configured_platforms(controller_data): - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms( + entry, platforms=get_configured_platforms(controller_data) + ) def stop_subscription(event): """Stop SubscriptionRegistry updates.""" @@ -181,24 +181,24 @@ async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): await hass.config_entries.async_reload(entry.entry_id) -def map_vera_device(vera_device: veraApi.VeraDevice, remap: list[int]) -> str: +def map_vera_device(vera_device: veraApi.VeraDevice, remap: list[int]) -> Platform: """Map vera classes to Home Assistant types.""" type_map = { - veraApi.VeraDimmer: "light", - veraApi.VeraBinarySensor: "binary_sensor", - veraApi.VeraSensor: "sensor", - veraApi.VeraArmableDevice: "switch", - veraApi.VeraLock: "lock", - veraApi.VeraThermostat: "climate", - veraApi.VeraCurtain: "cover", - veraApi.VeraSceneController: "sensor", - veraApi.VeraSwitch: "switch", + veraApi.VeraDimmer: Platform.LIGHT, + veraApi.VeraBinarySensor: Platform.BINARY_SENSOR, + veraApi.VeraSensor: Platform.SENSOR, + veraApi.VeraArmableDevice: Platform.SWITCH, + veraApi.VeraLock: Platform.LOCK, + veraApi.VeraThermostat: Platform.CLIMATE, + veraApi.VeraCurtain: Platform.COVER, + veraApi.VeraSceneController: Platform.SENSOR, + veraApi.VeraSwitch: Platform.SWITCH, } - def map_special_case(instance_class: type, entity_type: str) -> str: + def map_special_case(instance_class: type, entity_type: Platform) -> Platform: if instance_class is veraApi.VeraSwitch and vera_device.device_id in remap: - return "light" + return Platform.LIGHT return entity_type return next( diff --git a/homeassistant/components/vera/common.py b/homeassistant/components/vera/common.py index 243ee4d7594..d352c162098 100644 --- a/homeassistant/components/vera/common.py +++ b/homeassistant/components/vera/common.py @@ -2,12 +2,13 @@ from __future__ import annotations from collections import defaultdict +from collections.abc import Sequence from typing import NamedTuple import pyvera as pv -from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.event import call_later @@ -18,19 +19,19 @@ class ControllerData(NamedTuple): """Controller data.""" controller: pv.VeraController - devices: defaultdict[str, list[pv.VeraDevice]] + devices: defaultdict[Platform, list[pv.VeraDevice]] scenes: list[pv.VeraScene] config_entry: ConfigEntry -def get_configured_platforms(controller_data: ControllerData) -> set[str]: +def get_configured_platforms(controller_data: ControllerData) -> set[Platform]: """Get configured platforms for a controller.""" - platforms = [] + platforms: Sequence[Platform] = [] for platform in controller_data.devices: platforms.append(platform) if controller_data.scenes: - platforms.append(SCENE_DOMAIN) + platforms.append(Platform.SCENE) return set(platforms) diff --git a/homeassistant/components/vesync/__init__.py b/homeassistant/components/vesync/__init__.py index 48a7a577313..95370b01409 100644 --- a/homeassistant/components/vesync/__init__.py +++ b/homeassistant/components/vesync/__init__.py @@ -3,7 +3,7 @@ import logging from pyvesync import VeSync -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -56,15 +56,15 @@ async def async_setup_entry(hass, config_entry): if device_dict[VS_SWITCHES]: switches.extend(device_dict[VS_SWITCHES]) - hass.async_create_task(forward_setup(config_entry, "switch")) + hass.async_create_task(forward_setup(config_entry, Platform.SWITCH)) if device_dict[VS_FANS]: fans.extend(device_dict[VS_FANS]) - hass.async_create_task(forward_setup(config_entry, "fan")) + hass.async_create_task(forward_setup(config_entry, Platform.FAN)) if device_dict[VS_LIGHTS]: lights.extend(device_dict[VS_LIGHTS]) - hass.async_create_task(forward_setup(config_entry, "light")) + hass.async_create_task(forward_setup(config_entry, Platform.LIGHT)) async def async_new_device_discovery(service): """Discover if new devices should be added.""" From 3635946211c54ec7a5b88a7b53269e0aba13c60b Mon Sep 17 00:00:00 2001 From: Sebastian Nohn Date: Mon, 13 Dec 2021 14:14:05 +0100 Subject: [PATCH 0357/2644] Bump pillow from 8.2.0 to 8.3.2 (#61661) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- homeassistant/components/doods/manifest.json | 2 +- homeassistant/components/image/manifest.json | 2 +- homeassistant/components/proxy/manifest.json | 2 +- homeassistant/components/qrcode/manifest.json | 2 +- homeassistant/components/seven_segments/manifest.json | 2 +- homeassistant/components/sighthound/manifest.json | 2 +- homeassistant/components/tensorflow/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json index ae584af5916..44597ac8aeb 100644 --- a/homeassistant/components/doods/manifest.json +++ b/homeassistant/components/doods/manifest.json @@ -2,7 +2,7 @@ "domain": "doods", "name": "DOODS - Dedicated Open Object Detection Service", "documentation": "https://www.home-assistant.io/integrations/doods", - "requirements": ["pydoods==1.0.2", "pillow==8.2.0"], + "requirements": ["pydoods==1.0.2", "pillow==8.3.2"], "codeowners": [], "iot_class": "local_polling" } diff --git a/homeassistant/components/image/manifest.json b/homeassistant/components/image/manifest.json index 82b7e58a653..9416ea7ef9e 100644 --- a/homeassistant/components/image/manifest.json +++ b/homeassistant/components/image/manifest.json @@ -3,7 +3,7 @@ "name": "Image", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/image", - "requirements": ["pillow==8.2.0"], + "requirements": ["pillow==8.3.2"], "dependencies": ["http"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal" diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index 68c7717e16c..47982ac120e 100644 --- a/homeassistant/components/proxy/manifest.json +++ b/homeassistant/components/proxy/manifest.json @@ -2,6 +2,6 @@ "domain": "proxy", "name": "Camera Proxy", "documentation": "https://www.home-assistant.io/integrations/proxy", - "requirements": ["pillow==8.2.0"], + "requirements": ["pillow==8.3.2"], "codeowners": [] } diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index a414e197fd6..adfad7569e8 100644 --- a/homeassistant/components/qrcode/manifest.json +++ b/homeassistant/components/qrcode/manifest.json @@ -2,7 +2,7 @@ "domain": "qrcode", "name": "QR Code", "documentation": "https://www.home-assistant.io/integrations/qrcode", - "requirements": ["pillow==8.2.0", "pyzbar==0.1.7"], + "requirements": ["pillow==8.3.2", "pyzbar==0.1.7"], "codeowners": [], "iot_class": "calculated" } diff --git a/homeassistant/components/seven_segments/manifest.json b/homeassistant/components/seven_segments/manifest.json index 9a0287b2132..14dc16814a6 100644 --- a/homeassistant/components/seven_segments/manifest.json +++ b/homeassistant/components/seven_segments/manifest.json @@ -2,7 +2,7 @@ "domain": "seven_segments", "name": "Seven Segments OCR", "documentation": "https://www.home-assistant.io/integrations/seven_segments", - "requirements": ["pillow==8.2.0"], + "requirements": ["pillow==8.3.2"], "codeowners": ["@fabaff"], "iot_class": "local_polling" } diff --git a/homeassistant/components/sighthound/manifest.json b/homeassistant/components/sighthound/manifest.json index b22b645a7e8..b0febac8150 100644 --- a/homeassistant/components/sighthound/manifest.json +++ b/homeassistant/components/sighthound/manifest.json @@ -2,7 +2,7 @@ "domain": "sighthound", "name": "Sighthound", "documentation": "https://www.home-assistant.io/integrations/sighthound", - "requirements": ["pillow==8.2.0", "simplehound==0.3"], + "requirements": ["pillow==8.3.2", "simplehound==0.3"], "codeowners": ["@robmarkcole"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 8d5ea0acaa2..fbc1d848d33 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -7,7 +7,7 @@ "tf-models-official==2.3.0", "pycocotools==2.0.1", "numpy==1.21.4", - "pillow==8.2.0" + "pillow==8.3.2" ], "codeowners": [], "iot_class": "local_polling" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 56fbc02d690..3702db188e2 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ httpx==0.21.0 ifaddr==0.1.7 jinja2==3.0.3 paho-mqtt==1.6.1 -pillow==8.2.0 +pillow==8.3.2 pip>=8.0.3,<20.3 pyserial==3.5 python-slugify==4.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 0090b6b4332..ed844a0b815 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1229,7 +1229,7 @@ pilight==0.1.1 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -pillow==8.2.0 +pillow==8.3.2 # homeassistant.components.dominos pizzapi==0.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ca72e4e447b..b582a5648e8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -746,7 +746,7 @@ pilight==0.1.1 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -pillow==8.2.0 +pillow==8.3.2 # homeassistant.components.plex plexapi==4.7.1 From bceeaec2f8f9b868aecd490a4cf2cb913afc8b5f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 13 Dec 2021 14:15:36 +0100 Subject: [PATCH 0358/2644] Remove duplicated statistics rows (#61146) * Remove duplicated statistics * Fix misleading docstring * Pylint knows best * Correct test * Oops * Prevent insertion of duplicated statistics * Tweak * pylint * Add models_schema_23.py * Tweak --- .../components/recorder/migration.py | 18 +- homeassistant/components/recorder/models.py | 11 +- .../components/recorder/statistics.py | 192 +++++- homeassistant/components/recorder/util.py | 10 +- tests/components/recorder/models_schema_23.py | 582 ++++++++++++++++ tests/components/recorder/test_statistics.py | 630 +++++++++++++++++- 6 files changed, 1429 insertions(+), 14 deletions(-) create mode 100644 tests/components/recorder/models_schema_23.py diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 2e6e5a7bd12..32119b85597 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -25,7 +25,7 @@ from .models import ( StatisticsShortTerm, process_timestamp, ) -from .statistics import get_start_time +from .statistics import delete_duplicates, get_start_time from .util import session_scope _LOGGER = logging.getLogger(__name__) @@ -587,6 +587,22 @@ def _apply_update(instance, session, new_version, old_version): # noqa: C901 elif new_version == 23: # Add name column to StatisticsMeta _add_columns(session, "statistics_meta", ["name VARCHAR(255)"]) + elif new_version == 24: + # Delete duplicated statistics + delete_duplicates(instance, session) + # Recreate statistics indices to block duplicated statistics + _drop_index(connection, "statistics", "ix_statistics_statistic_id_start") + _create_index(connection, "statistics", "ix_statistics_statistic_id_start") + _drop_index( + connection, + "statistics_short_term", + "ix_statistics_short_term_statistic_id_start", + ) + _create_index( + connection, + "statistics_short_term", + "ix_statistics_short_term_statistic_id_start", + ) else: raise ValueError(f"No schema migration defined for version {new_version}") diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 6998c8e5f53..55d6f73108c 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -40,7 +40,7 @@ import homeassistant.util.dt as dt_util # pylint: disable=invalid-name Base = declarative_base() -SCHEMA_VERSION = 23 +SCHEMA_VERSION = 24 _LOGGER = logging.getLogger(__name__) @@ -289,7 +289,7 @@ class Statistics(Base, StatisticsBase): # type: ignore __table_args__ = ( # Used for fetching statistics for a certain entity at a specific time - Index("ix_statistics_statistic_id_start", "metadata_id", "start"), + Index("ix_statistics_statistic_id_start", "metadata_id", "start", unique=True), ) __tablename__ = TABLE_STATISTICS @@ -301,7 +301,12 @@ class StatisticsShortTerm(Base, StatisticsBase): # type: ignore __table_args__ = ( # Used for fetching statistics for a certain entity at a specific time - Index("ix_statistics_short_term_statistic_id_start", "metadata_id", "start"), + Index( + "ix_statistics_short_term_statistic_id_start", + "metadata_id", + "start", + unique=True, + ), ) __tablename__ = TABLE_STATISTICS_SHORT_TERM diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 02c00722e72..5310c8ed9f3 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -3,19 +3,21 @@ from __future__ import annotations from collections import defaultdict from collections.abc import Callable, Iterable +import contextlib import dataclasses from datetime import datetime, timedelta from itertools import chain, groupby +import json import logging import re from statistics import mean from typing import TYPE_CHECKING, Any, Literal from sqlalchemy import bindparam, func -from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.exc import SQLAlchemyError, StatementError from sqlalchemy.ext import baked from sqlalchemy.orm.scoping import scoped_session -from sqlalchemy.sql.expression import true +from sqlalchemy.sql.expression import literal_column, true from homeassistant.const import ( PRESSURE_PA, @@ -26,13 +28,14 @@ from homeassistant.const import ( from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry +from homeassistant.helpers.json import JSONEncoder import homeassistant.util.dt as dt_util import homeassistant.util.pressure as pressure_util import homeassistant.util.temperature as temperature_util from homeassistant.util.unit_system import UnitSystem import homeassistant.util.volume as volume_util -from .const import DATA_INSTANCE, DOMAIN +from .const import DATA_INSTANCE, DOMAIN, MAX_ROWS_TO_PURGE from .models import ( StatisticData, StatisticMetaData, @@ -114,6 +117,8 @@ QUERY_STATISTIC_META_ID = [ StatisticsMeta.statistic_id, ] +MAX_DUPLICATES = 1000000 + STATISTICS_BAKERY = "recorder_statistics_bakery" STATISTICS_META_BAKERY = "recorder_statistics_meta_bakery" STATISTICS_SHORT_TERM_BAKERY = "recorder_statistics_short_term_bakery" @@ -262,6 +267,139 @@ def _update_or_add_metadata( return metadata_id +def _find_duplicates( + session: scoped_session, table: type[Statistics | StatisticsShortTerm] +) -> tuple[list[int], list[dict]]: + """Find duplicated statistics.""" + subquery = ( + session.query( + table.start, + table.metadata_id, + literal_column("1").label("is_duplicate"), + ) + .group_by(table.metadata_id, table.start) + .having(func.count() > 1) + .subquery() + ) + query = ( + session.query(table) + .outerjoin( + subquery, + (subquery.c.metadata_id == table.metadata_id) + & (subquery.c.start == table.start), + ) + .filter(subquery.c.is_duplicate == 1) + .order_by(table.metadata_id, table.start, table.id.desc()) + .limit(MAX_ROWS_TO_PURGE) + ) + duplicates = execute(query) + original_as_dict = {} + start = None + metadata_id = None + duplicate_ids: list[int] = [] + non_identical_duplicates_as_dict: list[dict] = [] + + if not duplicates: + return (duplicate_ids, non_identical_duplicates_as_dict) + + def columns_to_dict(duplicate: type[Statistics | StatisticsShortTerm]) -> dict: + """Convert a SQLAlchemy row to dict.""" + dict_ = {} + for key in duplicate.__mapper__.c.keys(): + dict_[key] = getattr(duplicate, key) + return dict_ + + def compare_statistic_rows(row1: dict, row2: dict) -> bool: + """Compare two statistics rows, ignoring id and created.""" + ignore_keys = ["id", "created"] + keys1 = set(row1).difference(ignore_keys) + keys2 = set(row2).difference(ignore_keys) + return keys1 == keys2 and all(row1[k] == row2[k] for k in keys1) + + for duplicate in duplicates: + if start != duplicate.start or metadata_id != duplicate.metadata_id: + original_as_dict = columns_to_dict(duplicate) + start = duplicate.start + metadata_id = duplicate.metadata_id + continue + duplicate_as_dict = columns_to_dict(duplicate) + duplicate_ids.append(duplicate.id) + if not compare_statistic_rows(original_as_dict, duplicate_as_dict): + non_identical_duplicates_as_dict.append(duplicate_as_dict) + + return (duplicate_ids, non_identical_duplicates_as_dict) + + +def _delete_duplicates_from_table( + session: scoped_session, table: type[Statistics | StatisticsShortTerm] +) -> tuple[int, list[dict]]: + """Identify and delete duplicated statistics from a specified table.""" + all_non_identical_duplicates: list[dict] = [] + total_deleted_rows = 0 + while True: + duplicate_ids, non_identical_duplicates = _find_duplicates(session, table) + if not duplicate_ids: + break + all_non_identical_duplicates.extend(non_identical_duplicates) + deleted_rows = ( + session.query(table) + .filter(table.id.in_(duplicate_ids)) + .delete(synchronize_session=False) + ) + total_deleted_rows += deleted_rows + if total_deleted_rows >= MAX_DUPLICATES: + break + return (total_deleted_rows, all_non_identical_duplicates) + + +def delete_duplicates(instance: Recorder, session: scoped_session) -> None: + """Identify and delete duplicated statistics. + + A backup will be made of duplicated statistics before it is deleted. + """ + deleted_statistics_rows, non_identical_duplicates = _delete_duplicates_from_table( + session, Statistics + ) + if deleted_statistics_rows: + _LOGGER.info("Deleted %s duplicated statistics rows", deleted_statistics_rows) + + if non_identical_duplicates: + isotime = dt_util.utcnow().isoformat() + backup_file_name = f"deleted_statistics.{isotime}.json" + backup_path = instance.hass.config.path(backup_file_name) + with open(backup_path, "w", encoding="utf8") as backup_file: + json.dump( + non_identical_duplicates, + backup_file, + indent=4, + sort_keys=True, + cls=JSONEncoder, + ) + _LOGGER.warning( + "Deleted %s non identical duplicated %s rows, a backup of the deleted rows " + "has been saved to %s", + len(non_identical_duplicates), + Statistics.__tablename__, + backup_path, + ) + + if deleted_statistics_rows >= MAX_DUPLICATES: + _LOGGER.warning( + "Found more than %s duplicated statistic rows, please report at " + 'https://github.com/home-assistant/core/issues?q=is%%3Aissue+label%%3A"integration%%3A+recorder"+', + MAX_DUPLICATES - 1, + ) + + deleted_short_term_statistics_rows, _ = _delete_duplicates_from_table( + session, StatisticsShortTerm + ) + if deleted_short_term_statistics_rows: + _LOGGER.warning( + "Deleted duplicated short term statistic rows, please report at " + 'https://github.com/home-assistant/core/issues?q=is%%3Aissue+label%%3A"integration%%3A+recorder"+' + ) + + def compile_hourly_statistics( instance: Recorder, session: scoped_session, start: datetime ) -> None: @@ -411,7 +549,10 @@ def compile_statistics(instance: Recorder, start: datetime) -> bool: platform_stats.extend(platform_stat) # Insert collected statistics in the database - with session_scope(session=instance.get_session()) as session: # type: ignore + with session_scope( + session=instance.get_session(), # type: ignore + exception_filter=_filter_unique_constraint_integrity_error(instance), + ) as session: for stats in platform_stats: metadata_id = _update_or_add_metadata(instance.hass, session, stats["meta"]) _insert_statistics( @@ -1066,6 +1207,43 @@ def async_add_external_statistics( hass.data[DATA_INSTANCE].async_external_statistics(metadata, statistics) +def _filter_unique_constraint_integrity_error( + instance: Recorder, +) -> Callable[[Exception], bool]: + def _filter_unique_constraint_integrity_error(err: Exception) -> bool: + """Handle unique constraint integrity errors.""" + if not isinstance(err, StatementError): + return False + + ignore = False + if ( + instance.engine.dialect.name == "sqlite" + and "UNIQUE constraint failed" in str(err) + ): + ignore = True + if ( + instance.engine.dialect.name == "postgresql" + and hasattr(err.orig, "pgcode") + and err.orig.pgcode == "23505" + ): + ignore = True + if instance.engine.dialect.name == "mysql" and hasattr(err.orig, "args"): + with contextlib.suppress(TypeError): + if err.orig.args[0] == 1062: + ignore = True + + if ignore: + _LOGGER.warning( + "Blocked attempt to insert duplicated statistic rows, please report at " + 'https://github.com/home-assistant/core/issues?q=is%%3Aissue+label%%3A"integration%%3A+recorder"+', + exc_info=err, + ) + + return ignore + + return _filter_unique_constraint_integrity_error + + @retryable_database_job("statistics") def add_external_statistics( instance: Recorder, @@ -1073,7 +1251,11 @@ def add_external_statistics( statistics: Iterable[StatisticData], ) -> bool: """Process an add_statistics job.""" - with session_scope(session=instance.get_session()) as session: # type: ignore + + with session_scope( + session=instance.get_session(), # type: ignore + exception_filter=_filter_unique_constraint_integrity_error(instance), + ) as session: metadata_id = _update_or_add_metadata(instance.hass, session, metadata) for stat in statistics: if stat_id := _statistics_exists( diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 3900641db63..734694b8224 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -66,7 +66,10 @@ RETRYABLE_MYSQL_ERRORS = (1205, 1206, 1213) @contextmanager def session_scope( - *, hass: HomeAssistant | None = None, session: Session | None = None + *, + hass: HomeAssistant | None = None, + session: Session | None = None, + exception_filter: Callable[[Exception], bool] | None = None, ) -> Generator[Session, None, None]: """Provide a transactional scope around a series of operations.""" if session is None and hass is not None: @@ -81,11 +84,12 @@ def session_scope( if session.get_transaction(): need_rollback = True session.commit() - except Exception as err: + except Exception as err: # pylint: disable=broad-except _LOGGER.error("Error executing query: %s", err) if need_rollback: session.rollback() - raise + if not exception_filter or not exception_filter(err): + raise finally: session.close() diff --git a/tests/components/recorder/models_schema_23.py b/tests/components/recorder/models_schema_23.py new file mode 100644 index 00000000000..50839f41906 --- /dev/null +++ b/tests/components/recorder/models_schema_23.py @@ -0,0 +1,582 @@ +"""Models for SQLAlchemy. + +This file contains the model definitions for schema version 23, +used by Home Assistant Core 2021.11.0, which adds the name column +to statistics_meta. +It is used to test the schema migration logic. +""" +from __future__ import annotations + +from datetime import datetime, timedelta +import json +import logging +from typing import TypedDict, overload + +from sqlalchemy import ( + Boolean, + Column, + DateTime, + Float, + ForeignKey, + Identity, + Index, + Integer, + String, + Text, + distinct, +) +from sqlalchemy.dialects import mysql, oracle, postgresql +from sqlalchemy.ext.declarative import declared_attr +from sqlalchemy.orm import declarative_base, relationship +from sqlalchemy.orm.session import Session + +from homeassistant.const import ( + MAX_LENGTH_EVENT_CONTEXT_ID, + MAX_LENGTH_EVENT_EVENT_TYPE, + MAX_LENGTH_EVENT_ORIGIN, + MAX_LENGTH_STATE_DOMAIN, + MAX_LENGTH_STATE_ENTITY_ID, + MAX_LENGTH_STATE_STATE, +) +from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id +from homeassistant.helpers.json import JSONEncoder +import homeassistant.util.dt as dt_util + +# SQLAlchemy Schema +# pylint: disable=invalid-name +Base = declarative_base() + +SCHEMA_VERSION = 23 + +_LOGGER = logging.getLogger(__name__) + +DB_TIMEZONE = "+00:00" + +TABLE_EVENTS = "events" +TABLE_STATES = "states" +TABLE_RECORDER_RUNS = "recorder_runs" +TABLE_SCHEMA_CHANGES = "schema_changes" +TABLE_STATISTICS = "statistics" +TABLE_STATISTICS_META = "statistics_meta" +TABLE_STATISTICS_RUNS = "statistics_runs" +TABLE_STATISTICS_SHORT_TERM = "statistics_short_term" + +ALL_TABLES = [ + TABLE_STATES, + TABLE_EVENTS, + TABLE_RECORDER_RUNS, + TABLE_SCHEMA_CHANGES, + TABLE_STATISTICS, + TABLE_STATISTICS_META, + TABLE_STATISTICS_RUNS, + TABLE_STATISTICS_SHORT_TERM, +] + +DATETIME_TYPE = DateTime(timezone=True).with_variant( + mysql.DATETIME(timezone=True, fsp=6), "mysql" +) +DOUBLE_TYPE = ( + Float() + .with_variant(mysql.DOUBLE(asdecimal=False), "mysql") + .with_variant(oracle.DOUBLE_PRECISION(), "oracle") + .with_variant(postgresql.DOUBLE_PRECISION(), "postgresql") +) + + +class Events(Base): # type: ignore + """Event history data.""" + + __table_args__ = ( + # Used for fetching events at a specific time + # see logbook + Index("ix_events_event_type_time_fired", "event_type", "time_fired"), + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_EVENTS + event_id = Column(Integer, Identity(), primary_key=True) + event_type = Column(String(MAX_LENGTH_EVENT_EVENT_TYPE)) + event_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) + origin = Column(String(MAX_LENGTH_EVENT_ORIGIN)) + time_fired = Column(DATETIME_TYPE, index=True) + created = Column(DATETIME_TYPE, default=dt_util.utcnow) + context_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) + context_user_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) + context_parent_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @staticmethod + def from_event(event, event_data=None): + """Create an event database object from a native event.""" + return Events( + event_type=event.event_type, + event_data=event_data + or json.dumps(event.data, cls=JSONEncoder, separators=(",", ":")), + origin=str(event.origin.value), + time_fired=event.time_fired, + context_id=event.context.id, + context_user_id=event.context.user_id, + context_parent_id=event.context.parent_id, + ) + + def to_native(self, validate_entity_id=True): + """Convert to a native HA Event.""" + context = Context( + id=self.context_id, + user_id=self.context_user_id, + parent_id=self.context_parent_id, + ) + try: + return Event( + self.event_type, + json.loads(self.event_data), + EventOrigin(self.origin), + process_timestamp(self.time_fired), + context=context, + ) + except ValueError: + # When json.loads fails + _LOGGER.exception("Error converting to event: %s", self) + return None + + +class States(Base): # type: ignore + """State change history.""" + + __table_args__ = ( + # Used for fetching the state of entities at a specific time + # (get_states in history.py) + Index("ix_states_entity_id_last_updated", "entity_id", "last_updated"), + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_STATES + state_id = Column(Integer, Identity(), primary_key=True) + domain = Column(String(MAX_LENGTH_STATE_DOMAIN)) + entity_id = Column(String(MAX_LENGTH_STATE_ENTITY_ID)) + state = Column(String(MAX_LENGTH_STATE_STATE)) + attributes = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) + event_id = Column( + Integer, ForeignKey("events.event_id", ondelete="CASCADE"), index=True + ) + last_changed = Column(DATETIME_TYPE, default=dt_util.utcnow) + last_updated = Column(DATETIME_TYPE, default=dt_util.utcnow, index=True) + created = Column(DATETIME_TYPE, default=dt_util.utcnow) + old_state_id = Column(Integer, ForeignKey("states.state_id"), index=True) + event = relationship("Events", uselist=False) + old_state = relationship("States", remote_side=[state_id]) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @staticmethod + def from_event(event): + """Create object from a state_changed event.""" + entity_id = event.data["entity_id"] + state = event.data.get("new_state") + + dbstate = States(entity_id=entity_id) + + # State got deleted + if state is None: + dbstate.state = "" + dbstate.domain = split_entity_id(entity_id)[0] + dbstate.attributes = "{}" + dbstate.last_changed = event.time_fired + dbstate.last_updated = event.time_fired + else: + dbstate.domain = state.domain + dbstate.state = state.state + dbstate.attributes = json.dumps( + dict(state.attributes), cls=JSONEncoder, separators=(",", ":") + ) + dbstate.last_changed = state.last_changed + dbstate.last_updated = state.last_updated + + return dbstate + + def to_native(self, validate_entity_id=True): + """Convert to an HA state object.""" + try: + return State( + self.entity_id, + self.state, + json.loads(self.attributes), + process_timestamp(self.last_changed), + process_timestamp(self.last_updated), + # Join the events table on event_id to get the context instead + # as it will always be there for state_changed events + context=Context(id=None), + validate_entity_id=validate_entity_id, + ) + except ValueError: + # When json.loads fails + _LOGGER.exception("Error converting row to state: %s", self) + return None + + +class StatisticResult(TypedDict): + """Statistic result data class. + + Allows multiple datapoints for the same statistic_id. + """ + + meta: StatisticMetaData + stat: StatisticData + + +class StatisticDataBase(TypedDict): + """Mandatory fields for statistic data class.""" + + start: datetime + + +class StatisticData(StatisticDataBase, total=False): + """Statistic data class.""" + + mean: float + min: float + max: float + last_reset: datetime | None + state: float + sum: float + + +class StatisticsBase: + """Statistics base class.""" + + id = Column(Integer, Identity(), primary_key=True) + created = Column(DATETIME_TYPE, default=dt_util.utcnow) + + @declared_attr + def metadata_id(self): + """Define the metadata_id column for sub classes.""" + return Column( + Integer, + ForeignKey(f"{TABLE_STATISTICS_META}.id", ondelete="CASCADE"), + index=True, + ) + + start = Column(DATETIME_TYPE, index=True) + mean = Column(DOUBLE_TYPE) + min = Column(DOUBLE_TYPE) + max = Column(DOUBLE_TYPE) + last_reset = Column(DATETIME_TYPE) + state = Column(DOUBLE_TYPE) + sum = Column(DOUBLE_TYPE) + + @classmethod + def from_stats(cls, metadata_id: int, stats: StatisticData): + """Create object from a statistics.""" + return cls( # type: ignore + metadata_id=metadata_id, + **stats, + ) + + +class Statistics(Base, StatisticsBase): # type: ignore + """Long term statistics.""" + + duration = timedelta(hours=1) + + __table_args__ = ( + # Used for fetching statistics for a certain entity at a specific time + Index("ix_statistics_statistic_id_start", "metadata_id", "start"), + ) + __tablename__ = TABLE_STATISTICS + + +class StatisticsShortTerm(Base, StatisticsBase): # type: ignore + """Short term statistics.""" + + duration = timedelta(minutes=5) + + __table_args__ = ( + # Used for fetching statistics for a certain entity at a specific time + Index("ix_statistics_short_term_statistic_id_start", "metadata_id", "start"), + ) + __tablename__ = TABLE_STATISTICS_SHORT_TERM + + +class StatisticMetaData(TypedDict): + """Statistic meta data class.""" + + has_mean: bool + has_sum: bool + name: str | None + source: str + statistic_id: str + unit_of_measurement: str | None + + +class StatisticsMeta(Base): # type: ignore + """Statistics meta data.""" + + __table_args__ = ( + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_STATISTICS_META + id = Column(Integer, Identity(), primary_key=True) + statistic_id = Column(String(255), index=True) + source = Column(String(32)) + unit_of_measurement = Column(String(255)) + has_mean = Column(Boolean) + has_sum = Column(Boolean) + name = Column(String(255)) + + @staticmethod + def from_meta(meta: StatisticMetaData) -> StatisticsMeta: + """Create object from meta data.""" + return StatisticsMeta(**meta) + + +class RecorderRuns(Base): # type: ignore + """Representation of recorder run.""" + + __table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),) + __tablename__ = TABLE_RECORDER_RUNS + run_id = Column(Integer, Identity(), primary_key=True) + start = Column(DateTime(timezone=True), default=dt_util.utcnow) + end = Column(DateTime(timezone=True)) + closed_incorrect = Column(Boolean, default=False) + created = Column(DateTime(timezone=True), default=dt_util.utcnow) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + end = ( + f"'{self.end.isoformat(sep=' ', timespec='seconds')}'" if self.end else None + ) + return ( + f"" + ) + + def entity_ids(self, point_in_time=None): + """Return the entity ids that existed in this run. + + Specify point_in_time if you want to know which existed at that point + in time inside the run. + """ + session = Session.object_session(self) + + assert session is not None, "RecorderRuns need to be persisted" + + query = session.query(distinct(States.entity_id)).filter( + States.last_updated >= self.start + ) + + if point_in_time is not None: + query = query.filter(States.last_updated < point_in_time) + elif self.end is not None: + query = query.filter(States.last_updated < self.end) + + return [row[0] for row in query] + + def to_native(self, validate_entity_id=True): + """Return self, native format is this model.""" + return self + + +class SchemaChanges(Base): # type: ignore + """Representation of schema version changes.""" + + __tablename__ = TABLE_SCHEMA_CHANGES + change_id = Column(Integer, Identity(), primary_key=True) + schema_version = Column(Integer) + changed = Column(DateTime(timezone=True), default=dt_util.utcnow) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + +class StatisticsRuns(Base): # type: ignore + """Representation of statistics run.""" + + __tablename__ = TABLE_STATISTICS_RUNS + run_id = Column(Integer, Identity(), primary_key=True) + start = Column(DateTime(timezone=True)) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + +@overload +def process_timestamp(ts: None) -> None: + ... + + +@overload +def process_timestamp(ts: datetime) -> datetime: + ... + + +def process_timestamp(ts: datetime | None) -> datetime | None: + """Process a timestamp into datetime object.""" + if ts is None: + return None + if ts.tzinfo is None: + return ts.replace(tzinfo=dt_util.UTC) + + return dt_util.as_utc(ts) + + +@overload +def process_timestamp_to_utc_isoformat(ts: None) -> None: + ... + + +@overload +def process_timestamp_to_utc_isoformat(ts: datetime) -> str: + ... + + +def process_timestamp_to_utc_isoformat(ts: datetime | None) -> str | None: + """Process a timestamp into UTC isotime.""" + if ts is None: + return None + if ts.tzinfo == dt_util.UTC: + return ts.isoformat() + if ts.tzinfo is None: + return f"{ts.isoformat()}{DB_TIMEZONE}" + return ts.astimezone(dt_util.UTC).isoformat() + + +class LazyState(State): + """A lazy version of core State.""" + + __slots__ = [ + "_row", + "entity_id", + "state", + "_attributes", + "_last_changed", + "_last_updated", + "_context", + ] + + def __init__(self, row): # pylint: disable=super-init-not-called + """Init the lazy state.""" + self._row = row + self.entity_id = self._row.entity_id + self.state = self._row.state or "" + self._attributes = None + self._last_changed = None + self._last_updated = None + self._context = None + + @property # type: ignore + def attributes(self): + """State attributes.""" + if not self._attributes: + try: + self._attributes = json.loads(self._row.attributes) + except ValueError: + # When json.loads fails + _LOGGER.exception("Error converting row to state: %s", self._row) + self._attributes = {} + return self._attributes + + @attributes.setter + def attributes(self, value): + """Set attributes.""" + self._attributes = value + + @property # type: ignore + def context(self): + """State context.""" + if not self._context: + self._context = Context(id=None) + return self._context + + @context.setter + def context(self, value): + """Set context.""" + self._context = value + + @property # type: ignore + def last_changed(self): + """Last changed datetime.""" + if not self._last_changed: + self._last_changed = process_timestamp(self._row.last_changed) + return self._last_changed + + @last_changed.setter + def last_changed(self, value): + """Set last changed datetime.""" + self._last_changed = value + + @property # type: ignore + def last_updated(self): + """Last updated datetime.""" + if not self._last_updated: + self._last_updated = process_timestamp(self._row.last_updated) + return self._last_updated + + @last_updated.setter + def last_updated(self, value): + """Set last updated datetime.""" + self._last_updated = value + + def as_dict(self): + """Return a dict representation of the LazyState. + + Async friendly. + + To be used for JSON serialization. + """ + if self._last_changed: + last_changed_isoformat = self._last_changed.isoformat() + else: + last_changed_isoformat = process_timestamp_to_utc_isoformat( + self._row.last_changed + ) + if self._last_updated: + last_updated_isoformat = self._last_updated.isoformat() + else: + last_updated_isoformat = process_timestamp_to_utc_isoformat( + self._row.last_updated + ) + return { + "entity_id": self.entity_id, + "state": self.state, + "attributes": self._attributes or self.attributes, + "last_changed": last_changed_isoformat, + "last_updated": last_updated_isoformat, + } + + def __eq__(self, other): + """Return the comparison.""" + return ( + other.__class__ in [self.__class__, State] + and self.entity_id == other.entity_id + and self.state == other.state + and self.attributes == other.attributes + ) diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index c4dd33ce840..0f4468019e9 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -1,12 +1,18 @@ """The tests for sensor recorder platform.""" # pylint: disable=protected-access,invalid-name from datetime import timedelta +import importlib +import json +import sys from unittest.mock import patch, sentinel import pytest from pytest import approx +from sqlalchemy import create_engine +from sqlalchemy.orm import Session -from homeassistant.components.recorder import history +from homeassistant.components import recorder +from homeassistant.components.recorder import SQLITE_URL_PREFIX, history, statistics from homeassistant.components.recorder.const import DATA_INSTANCE from homeassistant.components.recorder.models import ( StatisticsShortTerm, @@ -14,18 +20,20 @@ from homeassistant.components.recorder.models import ( ) from homeassistant.components.recorder.statistics import ( async_add_external_statistics, + delete_duplicates, get_last_short_term_statistics, get_last_statistics, get_metadata, list_statistic_ids, statistics_during_period, ) +from homeassistant.components.recorder.util import session_scope from homeassistant.const import TEMP_CELSIUS from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util -from tests.common import mock_registry +from tests.common import get_test_home_assistant, mock_registry from tests.components.recorder.common import wait_recording_done @@ -650,6 +658,624 @@ def test_monthly_statistics(hass_recorder, caplog, timezone): dt_util.set_default_time_zone(dt_util.get_time_zone("UTC")) +def _create_engine_test(*args, **kwargs): + """Test version of create_engine that initializes with old schema. + + This simulates an existing db with the old schema. + """ + module = "tests.components.recorder.models_schema_23" + importlib.import_module(module) + old_models = sys.modules[module] + engine = create_engine(*args, **kwargs) + old_models.Base.metadata.create_all(engine) + with Session(engine) as session: + session.add(recorder.models.StatisticsRuns(start=statistics.get_start_time())) + session.add( + recorder.models.SchemaChanges(schema_version=old_models.SCHEMA_VERSION) + ) + session.commit() + return engine + + +def test_delete_duplicates(caplog, tmpdir): + """Test removal of duplicated statistics.""" + test_db_file = tmpdir.mkdir("sqlite").join("test_run_info.db") + dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" + + module = "tests.components.recorder.models_schema_23" + importlib.import_module(module) + old_models = sys.modules[module] + + period1 = dt_util.as_utc(dt_util.parse_datetime("2021-09-01 00:00:00")) + period2 = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 23:00:00")) + period3 = dt_util.as_utc(dt_util.parse_datetime("2021-10-01 00:00:00")) + period4 = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 23:00:00")) + + external_energy_statistics_1 = ( + { + "start": period1, + "last_reset": None, + "state": 0, + "sum": 2, + }, + { + "start": period2, + "last_reset": None, + "state": 1, + "sum": 3, + }, + { + "start": period3, + "last_reset": None, + "state": 2, + "sum": 4, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 5, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 5, + }, + ) + external_energy_metadata_1 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_1", + "unit_of_measurement": "kWh", + } + external_energy_statistics_2 = ( + { + "start": period1, + "last_reset": None, + "state": 0, + "sum": 20, + }, + { + "start": period2, + "last_reset": None, + "state": 1, + "sum": 30, + }, + { + "start": period3, + "last_reset": None, + "state": 2, + "sum": 40, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 50, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 50, + }, + ) + external_energy_metadata_2 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_2", + "unit_of_measurement": "kWh", + } + external_co2_statistics = ( + { + "start": period1, + "last_reset": None, + "mean": 10, + }, + { + "start": period2, + "last_reset": None, + "mean": 30, + }, + { + "start": period3, + "last_reset": None, + "mean": 60, + }, + { + "start": period4, + "last_reset": None, + "mean": 90, + }, + ) + external_co2_metadata = { + "has_mean": True, + "has_sum": False, + "name": "Fossil percentage", + "source": "test", + "statistic_id": "test:fossil_percentage", + "unit_of_measurement": "%", + } + + # Create some duplicated statistics with schema version 23 + with patch.object(recorder, "models", old_models), patch.object( + recorder.migration, "SCHEMA_VERSION", old_models.SCHEMA_VERSION + ), patch( + "homeassistant.components.recorder.create_engine", new=_create_engine_test + ): + hass = get_test_home_assistant() + setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) + wait_recording_done(hass) + wait_recording_done(hass) + + with session_scope(hass=hass) as session: + session.add( + recorder.models.StatisticsMeta.from_meta(external_energy_metadata_1) + ) + session.add( + recorder.models.StatisticsMeta.from_meta(external_energy_metadata_2) + ) + session.add(recorder.models.StatisticsMeta.from_meta(external_co2_metadata)) + with session_scope(hass=hass) as session: + for stat in external_energy_statistics_1: + session.add(recorder.models.Statistics.from_stats(1, stat)) + for stat in external_energy_statistics_2: + session.add(recorder.models.Statistics.from_stats(2, stat)) + for stat in external_co2_statistics: + session.add(recorder.models.Statistics.from_stats(3, stat)) + + hass.stop() + + # Test that the duplicates are removed during migration from schema 23 + hass = get_test_home_assistant() + setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) + hass.start() + wait_recording_done(hass) + wait_recording_done(hass) + hass.stop() + + assert "Deleted 2 duplicated statistics rows" in caplog.text + assert "Found non identical" not in caplog.text + assert "Found more than" not in caplog.text + assert "Found duplicated" not in caplog.text + + +@pytest.mark.freeze_time("2021-08-01 00:00:00+00:00") +def test_delete_duplicates_non_identical(caplog, tmpdir): + """Test removal of duplicated statistics.""" + test_db_file = tmpdir.mkdir("sqlite").join("test_run_info.db") + dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" + + module = "tests.components.recorder.models_schema_23" + importlib.import_module(module) + old_models = sys.modules[module] + + period1 = dt_util.as_utc(dt_util.parse_datetime("2021-09-01 00:00:00")) + period2 = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 23:00:00")) + period3 = dt_util.as_utc(dt_util.parse_datetime("2021-10-01 00:00:00")) + period4 = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 23:00:00")) + + external_energy_statistics_1 = ( + { + "start": period1, + "last_reset": None, + "state": 0, + "sum": 2, + }, + { + "start": period2, + "last_reset": None, + "state": 1, + "sum": 3, + }, + { + "start": period3, + "last_reset": None, + "state": 2, + "sum": 4, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 5, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 6, + }, + ) + external_energy_metadata_1 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_1", + "unit_of_measurement": "kWh", + } + external_energy_statistics_2 = ( + { + "start": period1, + "last_reset": None, + "state": 0, + "sum": 20, + }, + { + "start": period2, + "last_reset": None, + "state": 1, + "sum": 30, + }, + { + "start": period3, + "last_reset": None, + "state": 2, + "sum": 40, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 50, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 50, + }, + ) + external_energy_metadata_2 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_2", + "unit_of_measurement": "kWh", + } + + # Create some duplicated statistics with schema version 23 + with patch.object(recorder, "models", old_models), patch.object( + recorder.migration, "SCHEMA_VERSION", old_models.SCHEMA_VERSION + ), patch( + "homeassistant.components.recorder.create_engine", new=_create_engine_test + ): + hass = get_test_home_assistant() + setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) + wait_recording_done(hass) + wait_recording_done(hass) + + with session_scope(hass=hass) as session: + session.add( + recorder.models.StatisticsMeta.from_meta(external_energy_metadata_1) + ) + session.add( + recorder.models.StatisticsMeta.from_meta(external_energy_metadata_2) + ) + with session_scope(hass=hass) as session: + for stat in external_energy_statistics_1: + session.add(recorder.models.Statistics.from_stats(1, stat)) + for stat in external_energy_statistics_2: + session.add(recorder.models.Statistics.from_stats(2, stat)) + + hass.stop() + + # Test that the duplicates are removed during migration from schema 23 + hass = get_test_home_assistant() + hass.config.config_dir = tmpdir + setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) + hass.start() + wait_recording_done(hass) + wait_recording_done(hass) + hass.stop() + + assert "Deleted 2 duplicated statistics rows" in caplog.text + assert "Deleted 1 non identical" in caplog.text + assert "Found more than" not in caplog.text + assert "Found duplicated" not in caplog.text + + isotime = dt_util.utcnow().isoformat() + backup_file_name = f"deleted_statistics.{isotime}.json" + + with open(hass.config.path(backup_file_name)) as backup_file: + backup = json.load(backup_file) + + assert backup == [ + { + "created": "2021-08-01T00:00:00", + "id": 4, + "last_reset": None, + "max": None, + "mean": None, + "metadata_id": 1, + "min": None, + "start": "2021-10-31T23:00:00", + "state": 3.0, + "sum": 5.0, + } + ] + + +@patch.object(statistics, "MAX_DUPLICATES", 2) +def test_delete_duplicates_too_many(caplog, tmpdir): + """Test removal of duplicated statistics.""" + test_db_file = tmpdir.mkdir("sqlite").join("test_run_info.db") + dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" + + module = "tests.components.recorder.models_schema_23" + importlib.import_module(module) + old_models = sys.modules[module] + + period1 = dt_util.as_utc(dt_util.parse_datetime("2021-09-01 00:00:00")) + period2 = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 23:00:00")) + period3 = dt_util.as_utc(dt_util.parse_datetime("2021-10-01 00:00:00")) + period4 = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 23:00:00")) + + external_energy_statistics_1 = ( + { + "start": period1, + "last_reset": None, + "state": 0, + "sum": 2, + }, + { + "start": period2, + "last_reset": None, + "state": 1, + "sum": 3, + }, + { + "start": period3, + "last_reset": None, + "state": 2, + "sum": 4, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 5, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 5, + }, + ) + external_energy_metadata_1 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_1", + "unit_of_measurement": "kWh", + } + external_energy_statistics_2 = ( + { + "start": period1, + "last_reset": None, + "state": 0, + "sum": 20, + }, + { + "start": period2, + "last_reset": None, + "state": 1, + "sum": 30, + }, + { + "start": period3, + "last_reset": None, + "state": 2, + "sum": 40, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 50, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 50, + }, + ) + external_energy_metadata_2 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_2", + "unit_of_measurement": "kWh", + } + + # Create some duplicated statistics with schema version 23 + with patch.object(recorder, "models", old_models), patch.object( + recorder.migration, "SCHEMA_VERSION", old_models.SCHEMA_VERSION + ), patch( + "homeassistant.components.recorder.create_engine", new=_create_engine_test + ): + hass = get_test_home_assistant() + setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) + wait_recording_done(hass) + wait_recording_done(hass) + + with session_scope(hass=hass) as session: + session.add( + recorder.models.StatisticsMeta.from_meta(external_energy_metadata_1) + ) + session.add( + recorder.models.StatisticsMeta.from_meta(external_energy_metadata_2) + ) + with session_scope(hass=hass) as session: + for stat in external_energy_statistics_1: + session.add(recorder.models.Statistics.from_stats(1, stat)) + for stat in external_energy_statistics_2: + session.add(recorder.models.Statistics.from_stats(2, stat)) + + hass.stop() + + # Test that the duplicates are removed during migration from schema 23 + hass = get_test_home_assistant() + hass.config.config_dir = tmpdir + setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) + hass.start() + wait_recording_done(hass) + wait_recording_done(hass) + hass.stop() + + assert "Deleted 2 duplicated statistics rows" in caplog.text + assert "Found non identical" not in caplog.text + assert "Found more than 1 duplicated statistic rows" in caplog.text + assert "Found duplicated" not in caplog.text + + +@patch.object(statistics, "MAX_DUPLICATES", 2) +def test_delete_duplicates_short_term(caplog, tmpdir): + """Test removal of duplicated statistics.""" + test_db_file = tmpdir.mkdir("sqlite").join("test_run_info.db") + dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" + + module = "tests.components.recorder.models_schema_23" + importlib.import_module(module) + old_models = sys.modules[module] + + period4 = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 23:00:00")) + + external_energy_metadata_1 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_1", + "unit_of_measurement": "kWh", + } + statistic_row = { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 5, + } + + # Create some duplicated statistics with schema version 23 + with patch.object(recorder, "models", old_models), patch.object( + recorder.migration, "SCHEMA_VERSION", old_models.SCHEMA_VERSION + ), patch( + "homeassistant.components.recorder.create_engine", new=_create_engine_test + ): + hass = get_test_home_assistant() + setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) + wait_recording_done(hass) + wait_recording_done(hass) + + with session_scope(hass=hass) as session: + session.add( + recorder.models.StatisticsMeta.from_meta(external_energy_metadata_1) + ) + with session_scope(hass=hass) as session: + session.add( + recorder.models.StatisticsShortTerm.from_stats(1, statistic_row) + ) + session.add( + recorder.models.StatisticsShortTerm.from_stats(1, statistic_row) + ) + + hass.stop() + + # Test that the duplicates are removed during migration from schema 23 + hass = get_test_home_assistant() + hass.config.config_dir = tmpdir + setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) + hass.start() + wait_recording_done(hass) + wait_recording_done(hass) + hass.stop() + + assert "duplicated statistics rows" not in caplog.text + assert "Found non identical" not in caplog.text + assert "Found more than" not in caplog.text + assert "Deleted duplicated short term statistic" in caplog.text + + +def test_delete_duplicates_no_duplicates(hass_recorder, caplog): + """Test removal of duplicated statistics.""" + hass = hass_recorder() + wait_recording_done(hass) + with session_scope(hass=hass) as session: + delete_duplicates(hass.data[DATA_INSTANCE], session) + assert "duplicated statistics rows" not in caplog.text + assert "Found non identical" not in caplog.text + assert "Found more than" not in caplog.text + assert "Found duplicated" not in caplog.text + + +def test_duplicate_statistics_handle_integrity_error(hass_recorder, caplog): + """Test the recorder does not blow up if statistics is duplicated.""" + hass = hass_recorder() + wait_recording_done(hass) + + period1 = dt_util.as_utc(dt_util.parse_datetime("2021-09-01 00:00:00")) + period2 = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 23:00:00")) + + external_energy_metadata_1 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_1", + "unit_of_measurement": "kWh", + } + external_energy_statistics_1 = [ + { + "start": period1, + "last_reset": None, + "state": 3, + "sum": 5, + }, + ] + external_energy_statistics_2 = [ + { + "start": period2, + "last_reset": None, + "state": 3, + "sum": 6, + } + ] + + with patch.object( + statistics, "_statistics_exists", return_value=False + ), patch.object( + statistics, "_insert_statistics", wraps=statistics._insert_statistics + ) as insert_statistics_mock: + async_add_external_statistics( + hass, external_energy_metadata_1, external_energy_statistics_1 + ) + async_add_external_statistics( + hass, external_energy_metadata_1, external_energy_statistics_1 + ) + async_add_external_statistics( + hass, external_energy_metadata_1, external_energy_statistics_2 + ) + wait_recording_done(hass) + assert insert_statistics_mock.call_count == 3 + + with session_scope(hass=hass) as session: + tmp = session.query(recorder.models.Statistics).all() + assert len(tmp) == 2 + + assert "Blocked attempt to insert duplicated statistic rows" in caplog.text + + def record_states(hass): """Record some test states. From e48f6d548fcc21b5f38dde52c755df12b8fe02f2 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 13 Dec 2021 14:15:52 +0100 Subject: [PATCH 0359/2644] Upgrade hangups to 0.4.16 (#61678) --- homeassistant/components/hangouts/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hangouts/manifest.json b/homeassistant/components/hangouts/manifest.json index 98531fca11a..187a748e3f6 100644 --- a/homeassistant/components/hangouts/manifest.json +++ b/homeassistant/components/hangouts/manifest.json @@ -3,7 +3,7 @@ "name": "Google Hangouts", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hangouts", - "requirements": ["hangups==0.4.14"], + "requirements": ["hangups==0.4.16"], "codeowners": [], "iot_class": "cloud_push" } diff --git a/requirements_all.txt b/requirements_all.txt index ed844a0b815..578839a5b34 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -792,7 +792,7 @@ ha-philipsjs==2.7.6 habitipy==0.2.0 # homeassistant.components.hangouts -hangups==0.4.14 +hangups==0.4.16 # homeassistant.components.cloud hass-nabucasa==0.50.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b582a5648e8..910005d493f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -500,7 +500,7 @@ ha-philipsjs==2.7.6 habitipy==0.2.0 # homeassistant.components.hangouts -hangups==0.4.14 +hangups==0.4.16 # homeassistant.components.cloud hass-nabucasa==0.50.0 From c157f1a7873daf87d49ae516ae8584ef40ea30ac Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 13 Dec 2021 14:38:49 +0100 Subject: [PATCH 0360/2644] Add color mode support to zwave light (#55264) * Add color mode support to zwave light * Fix typo --- homeassistant/components/zwave/light.py | 100 ++++++++++++------------ tests/components/zwave/test_light.py | 84 +++++++++++--------- 2 files changed, 99 insertions(+), 85 deletions(-) diff --git a/homeassistant/components/zwave/light.py b/homeassistant/components/zwave/light.py index 140f601b1d9..c03ac9d229e 100644 --- a/homeassistant/components/zwave/light.py +++ b/homeassistant/components/zwave/light.py @@ -5,21 +5,20 @@ from threading import Timer from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, - ATTR_HS_COLOR, + ATTR_RGB_COLOR, + ATTR_RGBW_COLOR, ATTR_TRANSITION, - ATTR_WHITE_VALUE, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_RGB, + COLOR_MODE_RGBW, DOMAIN, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION, - SUPPORT_WHITE_VALUE, LightEntity, ) from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -import homeassistant.util.color as color_util from . import CONF_REFRESH_DELAY, CONF_REFRESH_VALUE, ZWaveDeviceEntity, const @@ -108,16 +107,6 @@ def byte_to_zwave_brightness(value): return 0 -def ct_to_hs(temp): - """Convert color temperature (mireds) to hs.""" - colorlist = list( - color_util.color_temperature_to_hs( - color_util.color_temperature_mired_to_kelvin(temp) - ) - ) - return [int(val) for val in colorlist] - - class ZwaveDimmer(ZWaveDeviceEntity, LightEntity): """Representation of a Z-Wave dimmer.""" @@ -126,6 +115,8 @@ class ZwaveDimmer(ZWaveDeviceEntity, LightEntity): ZWaveDeviceEntity.__init__(self, values, DOMAIN) self._brightness = None self._state = None + self._color_mode = None + self._supported_color_modes = set() self._supported_features = None self._delay = delay self._refresh_value = refresh @@ -161,9 +152,10 @@ class ZwaveDimmer(ZWaveDeviceEntity, LightEntity): def value_added(self): """Call when a new value is added to this entity.""" - self._supported_features = SUPPORT_BRIGHTNESS + self._supported_color_modes = {COLOR_MODE_BRIGHTNESS} + self._color_mode = COLOR_MODE_BRIGHTNESS if self.values.dimming_duration is not None: - self._supported_features |= SUPPORT_TRANSITION + self._supported_features = SUPPORT_TRANSITION def value_changed(self): """Call when a value for this entity's node has changed.""" @@ -195,6 +187,16 @@ class ZwaveDimmer(ZWaveDeviceEntity, LightEntity): """Return true if device is on.""" return self._state == STATE_ON + @property + def color_mode(self): + """Return the current color mode.""" + return self._color_mode + + @property + def supported_color_modes(self): + """Flag supported color modes.""" + return self._supported_color_modes + @property def supported_features(self): """Flag supported features.""" @@ -260,7 +262,7 @@ class ZwaveColorLight(ZwaveDimmer): def __init__(self, values, refresh, delay): """Initialize the light.""" self._color_channels = None - self._hs = None + self._rgb = None self._ct = None self._white = None @@ -268,15 +270,18 @@ class ZwaveColorLight(ZwaveDimmer): def value_added(self): """Call when a new value is added to this entity.""" - super().value_added() + if self.values.dimming_duration is not None: + self._supported_features = SUPPORT_TRANSITION - self._supported_features |= SUPPORT_COLOR + self._supported_color_modes = {COLOR_MODE_RGB} + self._color_mode = COLOR_MODE_RGB if self._zw098: - self._supported_features |= SUPPORT_COLOR_TEMP + self._supported_color_modes.add(COLOR_MODE_COLOR_TEMP) elif self._color_channels is not None and self._color_channels & ( COLOR_CHANNEL_WARM_WHITE | COLOR_CHANNEL_COLD_WHITE ): - self._supported_features |= SUPPORT_WHITE_VALUE + self._supported_color_modes = {COLOR_MODE_RGBW} + self._color_mode = COLOR_MODE_RGBW def update_properties(self): """Update internal properties based on zwave values.""" @@ -294,8 +299,7 @@ class ZwaveColorLight(ZwaveDimmer): data = self.values.color.data # RGB is always present in the openzwave color data string. - rgb = [int(data[1:3], 16), int(data[3:5], 16), int(data[5:7], 16)] - self._hs = color_util.color_RGB_to_hs(*rgb) + self._rgb = (int(data[1:3], 16), int(data[3:5], 16), int(data[5:7], 16)) # Parse remaining color channels. Openzwave appends white channels # that are present. @@ -321,13 +325,12 @@ class ZwaveColorLight(ZwaveDimmer): if self._zw098: if warm_white > 0: self._ct = TEMP_WARM_HASS - self._hs = ct_to_hs(self._ct) + self._color_mode = COLOR_MODE_COLOR_TEMP elif cold_white > 0: self._ct = TEMP_COLD_HASS - self._hs = ct_to_hs(self._ct) + self._color_mode = COLOR_MODE_COLOR_TEMP else: - # RGB color is being used. Just report midpoint. - self._ct = TEMP_MID_HASS + self._color_mode = COLOR_MODE_RGB elif self._color_channels & COLOR_CHANNEL_WARM_WHITE: self._white = warm_white @@ -341,17 +344,19 @@ class ZwaveColorLight(ZwaveDimmer): or self._color_channels & COLOR_CHANNEL_GREEN or self._color_channels & COLOR_CHANNEL_BLUE ): - self._hs = None + self._rgb = None @property - def hs_color(self): - """Return the hs color.""" - return self._hs + def rgb_color(self): + """Return the rgb color.""" + return self._rgb @property - def white_value(self): - """Return the white value of this light between 0..255.""" - return self._white + def rgbw_color(self): + """Return the rgbw color.""" + if self._rgb is None: + return None + return (*self._rgb, self._white) @property def color_temp(self): @@ -362,31 +367,28 @@ class ZwaveColorLight(ZwaveDimmer): """Turn the device on.""" rgbw = None - if ATTR_WHITE_VALUE in kwargs: - self._white = kwargs[ATTR_WHITE_VALUE] - if ATTR_COLOR_TEMP in kwargs: # Color temperature. With the AEOTEC ZW098 bulb, only two color # temperatures are supported. The warm and cold channel values # indicate brightness for warm/cold color temperature. if self._zw098: + self._color_mode = COLOR_MODE_COLOR_TEMP if kwargs[ATTR_COLOR_TEMP] > TEMP_MID_HASS: self._ct = TEMP_WARM_HASS rgbw = "#000000ff00" else: self._ct = TEMP_COLD_HASS rgbw = "#00000000ff" - elif ATTR_HS_COLOR in kwargs: - self._hs = kwargs[ATTR_HS_COLOR] - if ATTR_WHITE_VALUE not in kwargs: - # white LED must be off in order for color to work - self._white = 0 + elif ATTR_RGB_COLOR in kwargs: + self._rgb = kwargs[ATTR_RGB_COLOR] + self._white = 0 + elif ATTR_RGBW_COLOR in kwargs: + self._rgb = kwargs[ATTR_RGBW_COLOR][0:3] + self._white = kwargs[ATTR_RGBW_COLOR][3] - if ( - ATTR_WHITE_VALUE in kwargs or ATTR_HS_COLOR in kwargs - ) and self._hs is not None: + if ATTR_RGB_COLOR in kwargs or ATTR_RGBW_COLOR in kwargs: rgbw = "#" - for colorval in color_util.color_hs_to_RGB(*self._hs): + for colorval in self._rgb: rgbw += format(colorval, "02x") if self._white is not None: rgbw += format(self._white, "02x") + "00" diff --git a/tests/components/zwave/test_light.py b/tests/components/zwave/test_light.py index 9e943c54bb4..35128ccc69a 100644 --- a/tests/components/zwave/test_light.py +++ b/tests/components/zwave/test_light.py @@ -5,14 +5,14 @@ from homeassistant.components import zwave from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, - ATTR_HS_COLOR, + ATTR_RGB_COLOR, + ATTR_RGBW_COLOR, ATTR_TRANSITION, - ATTR_WHITE_VALUE, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_RGB, + COLOR_MODE_RGBW, SUPPORT_TRANSITION, - SUPPORT_WHITE_VALUE, ) from homeassistant.components.zwave import const, light @@ -38,7 +38,9 @@ def test_get_device_detects_dimmer(mock_openzwave): device = light.get_device(node=node, values=values, node_config={}) assert isinstance(device, light.ZwaveDimmer) - assert device.supported_features == SUPPORT_BRIGHTNESS + assert device.color_mode == COLOR_MODE_BRIGHTNESS + assert device.supported_features is None + assert device.supported_color_modes == {COLOR_MODE_BRIGHTNESS} def test_get_device_detects_colorlight(mock_openzwave): @@ -49,7 +51,9 @@ def test_get_device_detects_colorlight(mock_openzwave): device = light.get_device(node=node, values=values, node_config={}) assert isinstance(device, light.ZwaveColorLight) - assert device.supported_features == SUPPORT_BRIGHTNESS | SUPPORT_COLOR + assert device.color_mode == COLOR_MODE_RGB + assert device.supported_features is None + assert device.supported_color_modes == {COLOR_MODE_RGB} def test_get_device_detects_zw098(mock_openzwave): @@ -63,9 +67,9 @@ def test_get_device_detects_zw098(mock_openzwave): values = MockLightValues(primary=value) device = light.get_device(node=node, values=values, node_config={}) assert isinstance(device, light.ZwaveColorLight) - assert device.supported_features == ( - SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_COLOR_TEMP - ) + assert device.color_mode == COLOR_MODE_RGB + assert device.supported_features is None + assert device.supported_color_modes == {COLOR_MODE_COLOR_TEMP, COLOR_MODE_RGB} def test_get_device_detects_rgbw_light(mock_openzwave): @@ -79,9 +83,9 @@ def test_get_device_detects_rgbw_light(mock_openzwave): device = light.get_device(node=node, values=values, node_config={}) device.value_added() assert isinstance(device, light.ZwaveColorLight) - assert device.supported_features == ( - SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_WHITE_VALUE - ) + assert device.color_mode == COLOR_MODE_RGBW + assert device.supported_features is None + assert device.supported_color_modes == {COLOR_MODE_RGBW} def test_dimmer_turn_on(mock_openzwave): @@ -153,7 +157,9 @@ def test_dimmer_transitions(mock_openzwave): duration = MockValue(data=0, node=node) values = MockLightValues(primary=value, dimming_duration=duration) device = light.get_device(node=node, values=values, node_config={}) - assert device.supported_features == SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION + assert device.color_mode == COLOR_MODE_BRIGHTNESS + assert device.supported_features == SUPPORT_TRANSITION + assert device.supported_color_modes == {COLOR_MODE_BRIGHTNESS} # Test turn_on # Factory Default @@ -261,7 +267,7 @@ def test_dimmer_refresh_value(mock_openzwave): assert device.brightness == 118 -def test_set_hs_color(mock_openzwave): +def test_set_rgb_color(mock_openzwave): """Test setting zwave light color.""" node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) value = MockValue(data=0, node=node) @@ -273,7 +279,7 @@ def test_set_hs_color(mock_openzwave): assert color.data == "#0000000000" - device.turn_on(**{ATTR_HS_COLOR: (30, 50)}) + device.turn_on(**{ATTR_RGB_COLOR: (0xFF, 0xBF, 0x7F)}) assert color.data == "#ffbf7f0000" @@ -290,14 +296,14 @@ def test_set_white_value(mock_openzwave): assert color.data == "#0000000000" - device.turn_on(**{ATTR_WHITE_VALUE: 200}) + device.turn_on(**{ATTR_RGBW_COLOR: (0xFF, 0xFF, 0xFF, 0xC8)}) assert color.data == "#ffffffc800" def test_disable_white_if_set_color(mock_openzwave): """ - Test that _white is set to 0 if turn_on with ATTR_HS_COLOR. + Test that _white is set to 0 if turn_on with ATTR_RGB_COLOR. See Issue #13930 - many RGBW ZWave bulbs will only activate the RGB LED to produce color if _white is set to zero. @@ -312,12 +318,12 @@ def test_disable_white_if_set_color(mock_openzwave): device._white = 234 assert color.data == "#0000000000" - assert device.white_value == 234 + assert device.rgbw_color == (0, 0, 0, 234) - device.turn_on(**{ATTR_HS_COLOR: (30, 50)}) + device.turn_on(**{ATTR_RGB_COLOR: (0xFF, 0xBF, 0x7F)}) - assert device.white_value == 0 assert color.data == "#ffbf7f0000" + assert device.rgbw_color == (0xFF, 0xBF, 0x7F, 0x00) def test_zw098_set_color_temp(mock_openzwave): @@ -355,7 +361,8 @@ def test_rgb_not_supported(mock_openzwave): values = MockLightValues(primary=value, color=color, color_channels=color_channels) device = light.get_device(node=node, values=values, node_config={}) - assert device.hs_color is None + assert device.rgb_color is None + assert device.rgbw_color is None def test_no_color_value(mock_openzwave): @@ -365,7 +372,8 @@ def test_no_color_value(mock_openzwave): values = MockLightValues(primary=value) device = light.get_device(node=node, values=values, node_config={}) - assert device.hs_color is None + assert device.rgb_color is None + assert device.rgbw_color is None def test_no_color_channels_value(mock_openzwave): @@ -376,7 +384,8 @@ def test_no_color_channels_value(mock_openzwave): values = MockLightValues(primary=value, color=color) device = light.get_device(node=node, values=values, node_config={}) - assert device.hs_color is None + assert device.rgb_color is None + assert device.rgbw_color is None def test_rgb_value_changed(mock_openzwave): @@ -389,12 +398,12 @@ def test_rgb_value_changed(mock_openzwave): values = MockLightValues(primary=value, color=color, color_channels=color_channels) device = light.get_device(node=node, values=values, node_config={}) - assert device.hs_color == (0, 0) + assert device.rgb_color == (0, 0, 0) color.data = "#ffbf800000" value_changed(color) - assert device.hs_color == (29.764, 49.804) + assert device.rgb_color == (0xFF, 0xBF, 0x80) def test_rgbww_value_changed(mock_openzwave): @@ -407,14 +416,12 @@ def test_rgbww_value_changed(mock_openzwave): values = MockLightValues(primary=value, color=color, color_channels=color_channels) device = light.get_device(node=node, values=values, node_config={}) - assert device.hs_color == (0, 0) - assert device.white_value == 0 + assert device.rgbw_color == (0, 0, 0, 0) color.data = "#c86400c800" value_changed(color) - assert device.hs_color == (30, 100) - assert device.white_value == 200 + assert device.rgbw_color == (0xC8, 0x64, 0x00, 0xC8) def test_rgbcw_value_changed(mock_openzwave): @@ -427,14 +434,12 @@ def test_rgbcw_value_changed(mock_openzwave): values = MockLightValues(primary=value, color=color, color_channels=color_channels) device = light.get_device(node=node, values=values, node_config={}) - assert device.hs_color == (0, 0) - assert device.white_value == 0 + assert device.rgbw_color == (0, 0, 0, 0) color.data = "#c86400c800" value_changed(color) - assert device.hs_color == (30, 100) - assert device.white_value == 200 + assert device.rgbw_color == (0xC8, 0x64, 0x00, 0xC8) def test_ct_value_changed(mock_openzwave): @@ -451,14 +456,21 @@ def test_ct_value_changed(mock_openzwave): values = MockLightValues(primary=value, color=color, color_channels=color_channels) device = light.get_device(node=node, values=values, node_config={}) - assert device.color_temp == light.TEMP_MID_HASS + assert device.color_mode == COLOR_MODE_RGB + assert device.color_temp is None color.data = "#000000ff00" value_changed(color) + assert device.color_mode == COLOR_MODE_COLOR_TEMP assert device.color_temp == light.TEMP_WARM_HASS color.data = "#00000000ff" value_changed(color) + assert device.color_mode == COLOR_MODE_COLOR_TEMP assert device.color_temp == light.TEMP_COLD_HASS + + color.data = "#ff00000000" + value_changed(color) + assert device.color_mode == COLOR_MODE_RGB From a6a388721fe1ad86dd8aeee43e9793c32d94ab79 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 13 Dec 2021 15:04:45 +0100 Subject: [PATCH 0361/2644] Use SwitchDeviceClass in gree (#61656) Co-authored-by: epenet --- homeassistant/components/gree/switch.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/gree/switch.py b/homeassistant/components/gree/switch.py index f8a5b4c0b3d..f05f751b8b6 100644 --- a/homeassistant/components/gree/switch.py +++ b/homeassistant/components/gree/switch.py @@ -1,7 +1,7 @@ """Support for interface with a Gree climate systems.""" from __future__ import annotations -from homeassistant.components.switch import DEVICE_CLASS_SWITCH, SwitchEntity +from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -47,7 +47,7 @@ class GreePanelLightSwitchEntity(GreeEntity, SwitchEntity): @property def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" - return DEVICE_CLASS_SWITCH + return SwitchDeviceClass.SWITCH @property def is_on(self) -> bool: @@ -77,7 +77,7 @@ class GreeQuietModeSwitchEntity(GreeEntity, SwitchEntity): @property def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" - return DEVICE_CLASS_SWITCH + return SwitchDeviceClass.SWITCH @property def is_on(self) -> bool: @@ -107,7 +107,7 @@ class GreeFreshAirSwitchEntity(GreeEntity, SwitchEntity): @property def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" - return DEVICE_CLASS_SWITCH + return SwitchDeviceClass.SWITCH @property def is_on(self) -> bool: @@ -137,7 +137,7 @@ class GreeXFanSwitchEntity(GreeEntity, SwitchEntity): @property def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" - return DEVICE_CLASS_SWITCH + return SwitchDeviceClass.SWITCH @property def is_on(self) -> bool: From 2462d4cdf698c670516965cd923c35b1a06aa546 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 13 Dec 2021 15:05:20 +0100 Subject: [PATCH 0362/2644] Use SensorDeviceClass in gtfs (#61657) Co-authored-by: epenet --- homeassistant/components/gtfs/sensor.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/gtfs/sensor.py b/homeassistant/components/gtfs/sensor.py index 367a45aa073..99bc0ca51ed 100644 --- a/homeassistant/components/gtfs/sensor.py +++ b/homeassistant/components/gtfs/sensor.py @@ -14,15 +14,10 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorDeviceClass, SensorEntity, ) -from homeassistant.const import ( - ATTR_ATTRIBUTION, - CONF_NAME, - CONF_OFFSET, - DEVICE_CLASS_TIMESTAMP, - STATE_UNKNOWN, -) +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, CONF_OFFSET, STATE_UNKNOWN from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -522,7 +517,7 @@ def setup_platform( class GTFSDepartureSensor(SensorEntity): """Implementation of a GTFS departure sensor.""" - _attr_device_class = DEVICE_CLASS_TIMESTAMP + _attr_device_class = SensorDeviceClass.TIMESTAMP def __init__( self, From 3118bfdfab685be4c57d69401c8c8a647a5b54c7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 13 Dec 2021 15:06:19 +0100 Subject: [PATCH 0363/2644] Use new enums in guardian (#61660) Co-authored-by: epenet --- .../components/guardian/binary_sensor.py | 16 ++++++------- homeassistant/components/guardian/sensor.py | 23 ++++++++----------- 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/guardian/binary_sensor.py b/homeassistant/components/guardian/binary_sensor.py index 6ac07274501..1d7195a8f17 100644 --- a/homeassistant/components/guardian/binary_sensor.py +++ b/homeassistant/components/guardian/binary_sensor.py @@ -2,16 +2,14 @@ from __future__ import annotations from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_CONNECTIVITY, - DEVICE_CLASS_MOISTURE, - DEVICE_CLASS_MOVING, + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC 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 homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -35,19 +33,19 @@ SENSOR_KIND_MOVED = "moved" SENSOR_DESCRIPTION_AP_ENABLED = BinarySensorEntityDescription( key=SENSOR_KIND_AP_INFO, name="Onboard AP Enabled", - device_class=DEVICE_CLASS_CONNECTIVITY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=EntityCategory.DIAGNOSTIC, ) SENSOR_DESCRIPTION_LEAK_DETECTED = BinarySensorEntityDescription( key=SENSOR_KIND_LEAK_DETECTED, name="Leak Detected", - device_class=DEVICE_CLASS_MOISTURE, + device_class=BinarySensorDeviceClass.MOISTURE, ) SENSOR_DESCRIPTION_MOVED = BinarySensorEntityDescription( key=SENSOR_KIND_MOVED, name="Recently Moved", - device_class=DEVICE_CLASS_MOVING, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.MOVING, + entity_category=EntityCategory.DIAGNOSTIC, ) PAIRED_SENSOR_DESCRIPTIONS = ( diff --git a/homeassistant/components/guardian/sensor.py b/homeassistant/components/guardian/sensor.py index 9a88805baaf..895452e0fda 100644 --- a/homeassistant/components/guardian/sensor.py +++ b/homeassistant/components/guardian/sensor.py @@ -2,21 +2,16 @@ from __future__ import annotations from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_TEMPERATURE, - ENTITY_CATEGORY_DIAGNOSTIC, - PERCENTAGE, - TEMP_FAHRENHEIT, - TIME_MINUTES, -) +from homeassistant.const import PERCENTAGE, TEMP_FAHRENHEIT, TIME_MINUTES 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 . import PairedSensorEntity, ValveControllerEntity @@ -37,22 +32,22 @@ SENSOR_KIND_UPTIME = "uptime" SENSOR_DESCRIPTION_BATTERY = SensorEntityDescription( key=SENSOR_KIND_BATTERY, name="Battery", - device_class=DEVICE_CLASS_BATTERY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=SensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=PERCENTAGE, ) SENSOR_DESCRIPTION_TEMPERATURE = SensorEntityDescription( key=SENSOR_KIND_TEMPERATURE, name="Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_FAHRENHEIT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ) SENSOR_DESCRIPTION_UPTIME = SensorEntityDescription( key=SENSOR_KIND_UPTIME, name="Uptime", icon="mdi:timer", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=TIME_MINUTES, ) From 10f57cf1f5100f1e19103a2f5f3ba2f27b200d75 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 13 Dec 2021 17:04:21 +0100 Subject: [PATCH 0364/2644] Use new enums in homekit-controller (#61689) Co-authored-by: epenet --- .../homekit_controller/binary_sensor.py | 19 ++--- .../components/homekit_controller/button.py | 6 +- .../homekit_controller/humidifier.py | 8 +- .../homekit_controller/media_player.py | 7 +- .../components/homekit_controller/sensor.py | 76 ++++++++----------- 5 files changed, 50 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/homekit_controller/binary_sensor.py b/homeassistant/components/homekit_controller/binary_sensor.py index ad079f8322d..191aee6fca0 100644 --- a/homeassistant/components/homekit_controller/binary_sensor.py +++ b/homeassistant/components/homekit_controller/binary_sensor.py @@ -3,12 +3,7 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_GAS, - DEVICE_CLASS_MOISTURE, - DEVICE_CLASS_MOTION, - DEVICE_CLASS_OCCUPANCY, - DEVICE_CLASS_OPENING, - DEVICE_CLASS_SMOKE, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.core import callback @@ -19,7 +14,7 @@ from . import KNOWN_DEVICES, HomeKitEntity class HomeKitMotionSensor(HomeKitEntity, BinarySensorEntity): """Representation of a Homekit motion sensor.""" - _attr_device_class = DEVICE_CLASS_MOTION + _attr_device_class = BinarySensorDeviceClass.MOTION def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" @@ -34,7 +29,7 @@ class HomeKitMotionSensor(HomeKitEntity, BinarySensorEntity): class HomeKitContactSensor(HomeKitEntity, BinarySensorEntity): """Representation of a Homekit contact sensor.""" - _attr_device_class = DEVICE_CLASS_OPENING + _attr_device_class = BinarySensorDeviceClass.OPENING def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" @@ -49,7 +44,7 @@ class HomeKitContactSensor(HomeKitEntity, BinarySensorEntity): class HomeKitSmokeSensor(HomeKitEntity, BinarySensorEntity): """Representation of a Homekit smoke sensor.""" - _attr_device_class = DEVICE_CLASS_SMOKE + _attr_device_class = BinarySensorDeviceClass.SMOKE def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" @@ -64,7 +59,7 @@ class HomeKitSmokeSensor(HomeKitEntity, BinarySensorEntity): class HomeKitCarbonMonoxideSensor(HomeKitEntity, BinarySensorEntity): """Representation of a Homekit BO sensor.""" - _attr_device_class = DEVICE_CLASS_GAS + _attr_device_class = BinarySensorDeviceClass.GAS def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" @@ -79,7 +74,7 @@ class HomeKitCarbonMonoxideSensor(HomeKitEntity, BinarySensorEntity): class HomeKitOccupancySensor(HomeKitEntity, BinarySensorEntity): """Representation of a Homekit occupancy sensor.""" - _attr_device_class = DEVICE_CLASS_OCCUPANCY + _attr_device_class = BinarySensorDeviceClass.OCCUPANCY def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" @@ -94,7 +89,7 @@ class HomeKitOccupancySensor(HomeKitEntity, BinarySensorEntity): class HomeKitLeakSensor(HomeKitEntity, BinarySensorEntity): """Representation of a Homekit leak sensor.""" - _attr_device_class = DEVICE_CLASS_MOISTURE + _attr_device_class = BinarySensorDeviceClass.MOISTURE def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" diff --git a/homeassistant/components/homekit_controller/button.py b/homeassistant/components/homekit_controller/button.py index b83cc351fd5..07759475249 100644 --- a/homeassistant/components/homekit_controller/button.py +++ b/homeassistant/components/homekit_controller/button.py @@ -15,8 +15,8 @@ from homeassistant.components.button import ( ButtonEntity, ButtonEntityDescription, ) -from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import callback +from homeassistant.helpers.entity import EntityCategory from . import KNOWN_DEVICES, CharacteristicEntity @@ -33,14 +33,14 @@ BUTTON_ENTITIES: dict[str, HomeKitButtonEntityDescription] = { key=CharacteristicsTypes.Vendor.HAA_SETUP, name="Setup", icon="mdi:cog", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, write_value="#HAA@trcmd", ), CharacteristicsTypes.Vendor.HAA_UPDATE: HomeKitButtonEntityDescription( key=CharacteristicsTypes.Vendor.HAA_UPDATE, name="Update", device_class=ButtonDeviceClass.UPDATE, - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, write_value="#HAA@trcmd", ), } diff --git a/homeassistant/components/homekit_controller/humidifier.py b/homeassistant/components/homekit_controller/humidifier.py index 1505ead993b..2defa273175 100644 --- a/homeassistant/components/homekit_controller/humidifier.py +++ b/homeassistant/components/homekit_controller/humidifier.py @@ -4,10 +4,8 @@ from __future__ import annotations from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes -from homeassistant.components.humidifier import HumidifierEntity +from homeassistant.components.humidifier import HumidifierDeviceClass, HumidifierEntity from homeassistant.components.humidifier.const import ( - DEVICE_CLASS_DEHUMIDIFIER, - DEVICE_CLASS_HUMIDIFIER, MODE_AUTO, MODE_NORMAL, SUPPORT_MODES, @@ -35,7 +33,7 @@ HA_MODE_TO_HK = { class HomeKitHumidifier(HomeKitEntity, HumidifierEntity): """Representation of a HomeKit Controller Humidifier.""" - _attr_device_class = DEVICE_CLASS_HUMIDIFIER + _attr_device_class = HumidifierDeviceClass.HUMIDIFIER def get_characteristic_types(self): """Define the homekit characteristics the entity cares about.""" @@ -136,7 +134,7 @@ class HomeKitHumidifier(HomeKitEntity, HumidifierEntity): class HomeKitDehumidifier(HomeKitEntity, HumidifierEntity): """Representation of a HomeKit Controller Humidifier.""" - _attr_device_class = DEVICE_CLASS_DEHUMIDIFIER + _attr_device_class = HumidifierDeviceClass.DEHUMIDIFIER def get_characteristic_types(self): """Define the homekit characteristics the entity cares about.""" diff --git a/homeassistant/components/homekit_controller/media_player.py b/homeassistant/components/homekit_controller/media_player.py index 1134e4bb4da..e22e9db7dc7 100644 --- a/homeassistant/components/homekit_controller/media_player.py +++ b/homeassistant/components/homekit_controller/media_player.py @@ -10,7 +10,10 @@ from aiohomekit.model.characteristics import ( from aiohomekit.model.services import ServicesTypes from aiohomekit.utils import clamp_enum_to_char -from homeassistant.components.media_player import DEVICE_CLASS_TV, MediaPlayerEntity +from homeassistant.components.media_player import ( + MediaPlayerDeviceClass, + MediaPlayerEntity, +) from homeassistant.components.media_player.const import ( SUPPORT_PAUSE, SUPPORT_PLAY, @@ -57,7 +60,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class HomeKitTelevision(HomeKitEntity, MediaPlayerEntity): """Representation of a HomeKit Controller Television.""" - _attr_device_class = DEVICE_CLASS_TV + _attr_device_class = MediaPlayerDeviceClass.TV def get_characteristic_types(self): """Define the homekit characteristics the entity cares about.""" diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index 15324a2436e..a12858542b9 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -8,26 +8,14 @@ from aiohomekit.model.characteristics import Characteristic, CharacteristicsType from aiohomekit.model.services import ServicesTypes from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_MILLION, - DEVICE_CLASS_AQI, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_NITROGEN_DIOXIDE, - DEVICE_CLASS_OZONE, - DEVICE_CLASS_PM10, - DEVICE_CLASS_PM25, - DEVICE_CLASS_POWER, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_SULPHUR_DIOXIDE, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, LIGHT_LUX, PERCENTAGE, POWER_WATT, @@ -52,36 +40,36 @@ SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = { CharacteristicsTypes.Vendor.EVE_ENERGY_WATT: HomeKitSensorEntityDescription( key=CharacteristicsTypes.Vendor.EVE_ENERGY_WATT, name="Real Time Energy", - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_WATT, ), CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY: HomeKitSensorEntityDescription( key=CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY, name="Real Time Energy", - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_WATT, ), CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY_2: HomeKitSensorEntityDescription( key=CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY_2, name="Real Time Energy", - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_WATT, ), CharacteristicsTypes.Vendor.EVE_DEGREE_AIR_PRESSURE: HomeKitSensorEntityDescription( key=CharacteristicsTypes.Vendor.EVE_DEGREE_AIR_PRESSURE, name="Air Pressure", - device_class=DEVICE_CLASS_PRESSURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PRESSURE_HPA, ), CharacteristicsTypes.TEMPERATURE_CURRENT: HomeKitSensorEntityDescription( key=CharacteristicsTypes.TEMPERATURE_CURRENT, name="Current Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, # This sensor is only for temperature characteristics that are not part # of a temperature sensor service. @@ -93,8 +81,8 @@ SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = { CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT: HomeKitSensorEntityDescription( key=CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT, name="Current Humidity", - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, # This sensor is only for humidity characteristics that are not part # of a humidity sensor service. @@ -106,49 +94,49 @@ SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = { CharacteristicsTypes.AIR_QUALITY: HomeKitSensorEntityDescription( key=CharacteristicsTypes.AIR_QUALITY, name="Air Quality", - device_class=DEVICE_CLASS_AQI, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.AQI, + state_class=SensorStateClass.MEASUREMENT, ), CharacteristicsTypes.DENSITY_PM25: HomeKitSensorEntityDescription( key=CharacteristicsTypes.DENSITY_PM25, name="PM2.5 Density", - device_class=DEVICE_CLASS_PM25, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.PM25, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, ), CharacteristicsTypes.DENSITY_PM10: HomeKitSensorEntityDescription( key=CharacteristicsTypes.DENSITY_PM10, name="PM10 Density", - device_class=DEVICE_CLASS_PM10, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.PM10, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, ), CharacteristicsTypes.DENSITY_OZONE: HomeKitSensorEntityDescription( key=CharacteristicsTypes.DENSITY_OZONE, name="Ozone Density", - device_class=DEVICE_CLASS_OZONE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.OZONE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, ), CharacteristicsTypes.DENSITY_NO2: HomeKitSensorEntityDescription( key=CharacteristicsTypes.DENSITY_NO2, name="Nitrogen Dioxide Density", - device_class=DEVICE_CLASS_NITROGEN_DIOXIDE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.NITROGEN_DIOXIDE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, ), CharacteristicsTypes.DENSITY_SO2: HomeKitSensorEntityDescription( key=CharacteristicsTypes.DENSITY_SO2, name="Sulphur Dioxide Density", - device_class=DEVICE_CLASS_SULPHUR_DIOXIDE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.SULPHUR_DIOXIDE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, ), CharacteristicsTypes.DENSITY_VOC: HomeKitSensorEntityDescription( key=CharacteristicsTypes.DENSITY_VOC, name="Volatile Organic Compound Density", - device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, ), } @@ -166,7 +154,7 @@ for k, v in list(SIMPLE_SENSOR.items()): class HomeKitHumiditySensor(HomeKitEntity, SensorEntity): """Representation of a Homekit humidity sensor.""" - _attr_device_class = DEVICE_CLASS_HUMIDITY + _attr_device_class = SensorDeviceClass.HUMIDITY _attr_native_unit_of_measurement = PERCENTAGE def get_characteristic_types(self): @@ -187,7 +175,7 @@ class HomeKitHumiditySensor(HomeKitEntity, SensorEntity): class HomeKitTemperatureSensor(HomeKitEntity, SensorEntity): """Representation of a Homekit temperature sensor.""" - _attr_device_class = DEVICE_CLASS_TEMPERATURE + _attr_device_class = SensorDeviceClass.TEMPERATURE _attr_native_unit_of_measurement = TEMP_CELSIUS def get_characteristic_types(self): @@ -208,7 +196,7 @@ class HomeKitTemperatureSensor(HomeKitEntity, SensorEntity): class HomeKitLightSensor(HomeKitEntity, SensorEntity): """Representation of a Homekit light level sensor.""" - _attr_device_class = DEVICE_CLASS_ILLUMINANCE + _attr_device_class = SensorDeviceClass.ILLUMINANCE _attr_native_unit_of_measurement = LIGHT_LUX def get_characteristic_types(self): @@ -250,7 +238,7 @@ class HomeKitCarbonDioxideSensor(HomeKitEntity, SensorEntity): class HomeKitBatterySensor(HomeKitEntity, SensorEntity): """Representation of a Homekit battery sensor.""" - _attr_device_class = DEVICE_CLASS_BATTERY + _attr_device_class = SensorDeviceClass.BATTERY _attr_native_unit_of_measurement = PERCENTAGE def get_characteristic_types(self): From 0662ab019fa1acd830d89ed2b2e74353b9d01d0e Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 13 Dec 2021 17:11:21 +0100 Subject: [PATCH 0365/2644] Improve balboa tests (#61691) --- .coveragerc | 1 - homeassistant/components/balboa/climate.py | 6 +- tests/components/balboa/__init__.py | 146 +------------ tests/components/balboa/conftest.py | 94 ++++++++ tests/components/balboa/test_binary_sensor.py | 66 ++---- tests/components/balboa/test_climate.py | 205 ++++++++---------- tests/components/balboa/test_config_flow.py | 71 ++---- tests/components/balboa/test_init.py | 18 +- 8 files changed, 243 insertions(+), 364 deletions(-) create mode 100644 tests/components/balboa/conftest.py diff --git a/.coveragerc b/.coveragerc index 32a9d586c52..a1250c86bda 100644 --- a/.coveragerc +++ b/.coveragerc @@ -94,7 +94,6 @@ omit = homeassistant/components/azure_service_bus/* homeassistant/components/baidu/tts.py homeassistant/components/balboa/__init__.py - homeassistant/components/balboa/entity.py homeassistant/components/beewi_smartclim/sensor.py homeassistant/components/bbb_gpio/* homeassistant/components/bbox/device_tracker.py diff --git a/homeassistant/components/balboa/climate.py b/homeassistant/components/balboa/climate.py index c99448a77de..edd44b03f17 100644 --- a/homeassistant/components/balboa/climate.py +++ b/homeassistant/components/balboa/climate.py @@ -29,6 +29,8 @@ from homeassistant.const import ( from .const import CLIMATE, CLIMATE_SUPPORTED_FANSTATES, CLIMATE_SUPPORTED_MODES, DOMAIN from .entity import BalboaEntity +SET_TEMPERATURE_WAIT = 1 + async def async_setup_entry(hass, entry, async_add_entities): """Set up the spa climate device.""" @@ -124,10 +126,10 @@ class BalboaSpaClimate(BalboaEntity, ClimateEntity): newtemp = kwargs[ATTR_TEMPERATURE] if newtemp > self._client.tmax[self._client.TEMPRANGE_LOW][scale]: await self._client.change_temprange(self._client.TEMPRANGE_HIGH) - await asyncio.sleep(1) + await asyncio.sleep(SET_TEMPERATURE_WAIT) if newtemp < self._client.tmin[self._client.TEMPRANGE_HIGH][scale]: await self._client.change_temprange(self._client.TEMPRANGE_LOW) - await asyncio.sleep(1) + await asyncio.sleep(SET_TEMPERATURE_WAIT) await self._client.send_temp_change(newtemp) async def async_set_preset_mode(self, preset_mode) -> None: diff --git a/tests/components/balboa/__init__.py b/tests/components/balboa/__init__.py index 13c8b6240a7..7cae68f2203 100644 --- a/tests/components/balboa/__init__.py +++ b/tests/components/balboa/__init__.py @@ -1,7 +1,4 @@ """Test the Balboa Spa Client integration.""" -import asyncio -from unittest.mock import patch - from homeassistant.components.balboa.const import DOMAIN as BALBOA_DOMAIN from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant @@ -22,146 +19,7 @@ async def init_integration(hass: HomeAssistant) -> MockConfigEntry: ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.balboa.BalboaSpaWifi", - new=BalboaMock, - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() return config_entry - - -async def init_integration_mocked(hass: HomeAssistant) -> MockConfigEntry: - """Mock integration setup.""" - config_entry = MockConfigEntry( - domain=BALBOA_DOMAIN, - data={ - CONF_HOST: TEST_HOST, - }, - ) - config_entry.add_to_hass(hass) - - with patch( - "homeassistant.components.balboa.BalboaSpaWifi.connect", - new=BalboaMock.connect, - ), patch( - "homeassistant.components.balboa.BalboaSpaWifi.listen_until_configured", - new=BalboaMock.listen_until_configured, - ), patch( - "homeassistant.components.balboa.BalboaSpaWifi.listen", - new=BalboaMock.listen, - ), patch( - "homeassistant.components.balboa.BalboaSpaWifi.check_connection_status", - new=BalboaMock.check_connection_status, - ), patch( - "homeassistant.components.balboa.BalboaSpaWifi.send_panel_req", - new=BalboaMock.send_panel_req, - ), patch( - "homeassistant.components.balboa.BalboaSpaWifi.send_mod_ident_req", - new=BalboaMock.send_mod_ident_req, - ), patch( - "homeassistant.components.balboa.BalboaSpaWifi.spa_configured", - new=BalboaMock.spa_configured, - ), patch( - "homeassistant.components.balboa.BalboaSpaWifi.get_model_name", - new=BalboaMock.get_model_name, - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - return config_entry - - -class BalboaMock: - """Mock pybalboa library.""" - - def __init__(self, hostname, port=BALBOA_DEFAULT_PORT): - """Mock init.""" - self.host = hostname - self.port = port - self.connected = False - self.new_data_cb = None - self.lastupd = 0 - self.connected = False - self.fake_action = False - - async def connect(self): - """Connect to the spa.""" - self.connected = True - return True - - async def broken_connect(self): - """Connect to the spa.""" - self.connected = False - return False - - async def disconnect(self): - """Stop talking to the spa.""" - self.connected = False - - async def send_panel_req(self, arg_ba, arg_bb): - """Send a panel request, 2 bytes of data.""" - self.fake_action = False - return - - async def send_mod_ident_req(self): - """Ask for the module identification.""" - self.fake_action = False - return - - @staticmethod - def get_macaddr(): - """Return the macaddr of the spa wifi.""" - return "ef:ef:ef:c0:ff:ee" - - def get_model_name(self): - """Return the model name.""" - self.fake_action = False - return "FakeSpa" - - @staticmethod - def get_ssid(): - """Return the software version.""" - return "V0.0" - - @staticmethod - async def set_time(new_time, timescale=None): - """Set time on spa to new_time with optional timescale.""" - return - - async def listen(self): - """Listen to the spa babble forever.""" - while True: - if not self.connected: - # sleep and hope the checker fixes us - await asyncio.sleep(5) - continue - - # fake it - await asyncio.sleep(5) - - async def check_connection_status(self): - """Set this up to periodically check the spa connection and fix.""" - self.fake_action = False - while True: - # fake it - await asyncio.sleep(15) - - async def spa_configured(self): - """Check if the spa has been configured.""" - self.fake_action = False - return - - async def int_new_data_cb(self): - """Call false internal data callback.""" - - if self.new_data_cb is None: - return - await self.new_data_cb() # pylint: disable=not-callable - - async def listen_until_configured(self, maxiter=20): - """Listen to the spa babble until we are configured.""" - if not self.connected: - return False - return True diff --git a/tests/components/balboa/conftest.py b/tests/components/balboa/conftest.py new file mode 100644 index 00000000000..84a8a2c4c19 --- /dev/null +++ b/tests/components/balboa/conftest.py @@ -0,0 +1,94 @@ +"""Provide common fixtures.""" +from __future__ import annotations + +from collections.abc import Generator +import time +from unittest.mock import MagicMock, patch + +from pybalboa.balboa import text_heatmode +import pytest + + +@pytest.fixture(name="client") +def client_fixture() -> Generator[None, MagicMock, None]: + """Mock balboa.""" + with patch( + "homeassistant.components.balboa.BalboaSpaWifi", autospec=True + ) as mock_balboa: + # common attributes + client = mock_balboa.return_value + client.connected = True + client.lastupd = time.time() + client.new_data_cb = None + client.connect.return_value = True + client.get_macaddr.return_value = "ef:ef:ef:c0:ff:ee" + client.get_model_name.return_value = "FakeSpa" + client.get_ssid.return_value = "V0.0" + + # constants should preferebly be moved in the library + # to be class attributes or further refactored + client.TSCALE_C = 1 + client.TSCALE_F = 0 + client.HEATMODE_READY = 0 + client.HEATMODE_REST = 1 + client.HEATMODE_RNR = 2 + client.TIMESCALE_12H = 0 + client.TIMESCALE_24H = 1 + client.PUMP_OFF = 0 + client.PUMP_LOW = 1 + client.PUMP_HIGH = 2 + client.TEMPRANGE_LOW = 0 + client.TEMPRANGE_HIGH = 1 + client.tmin = [ + [50.0, 10.0], + [80.0, 26.0], + ] + client.tmax = [ + [80.0, 26.0], + [104.0, 40.0], + ] + client.BLOWER_OFF = 0 + client.BLOWER_LOW = 1 + client.BLOWER_MEDIUM = 2 + client.BLOWER_HIGH = 3 + client.FILTER_OFF = 0 + client.FILTER_1 = 1 + client.FILTER_2 = 2 + client.FILTER_1_2 = 3 + client.OFF = 0 + client.ON = 1 + client.HEATSTATE_IDLE = 0 + client.HEATSTATE_HEATING = 1 + client.HEATSTATE_HEAT_WAITING = 2 + client.VOLTAGE_240 = 240 + client.VOLTAGE_UNKNOWN = 0 + client.HEATERTYPE_STANDARD = "Standard" + client.HEATERTYPE_UNKNOWN = "Unknown" + + # Climate attributes + client.heatmode = 0 + client.get_heatmode_stringlist.return_value = text_heatmode + client.get_tempscale.return_value = client.TSCALE_F + client.have_blower.return_value = False + + # Climate methods + client.get_heatstate.return_value = 0 + client.get_blower.return_value = 0 + client.get_curtemp.return_value = 20.0 + client.get_settemp.return_value = 20.0 + + def get_heatmode(text=False): + """Ask for the current heatmode.""" + if text: + return text_heatmode[client.heatmode] + return client.heatmode + + client.get_heatmode.side_effect = get_heatmode + yield client + + +@pytest.fixture(autouse=True) +def set_temperature_wait(): + """Mock set temperature wait time.""" + with patch("homeassistant.components.balboa.climate.SET_TEMPERATURE_WAIT", new=0): + yield diff --git a/tests/components/balboa/test_binary_sensor.py b/tests/components/balboa/test_binary_sensor.py index 748801b7b8f..4f080f29ab3 100644 --- a/tests/components/balboa/test_binary_sensor.py +++ b/tests/components/balboa/test_binary_sensor.py @@ -1,14 +1,10 @@ """Tests of the climate entity of the balboa integration.""" +from unittest.mock import MagicMock -from unittest.mock import patch - -from homeassistant.components.balboa.const import DOMAIN as BALBOA_DOMAIN, SIGNAL_UPDATE from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant -from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.setup import async_setup_component -from . import init_integration_mocked +from . import init_integration ENTITY_BINARY_SENSOR = "binary_sensor.fakespa_" @@ -20,57 +16,41 @@ FILTER_MAP = [ ] -async def test_filters(hass: HomeAssistant): +async def test_filters(hass: HomeAssistant, client: MagicMock) -> None: """Test spa filters.""" - config_entry = await _setup_binary_sensor_test(hass) + config_entry = await init_integration(hass) for filter_mode in range(4): for spa_filter in range(1, 3): - state = await _patch_filter(hass, config_entry, filter_mode, spa_filter) + state = await _patch_filter( + hass, config_entry, filter_mode, spa_filter, client + ) assert state.state == FILTER_MAP[filter_mode][spa_filter - 1] -async def test_circ_pump(hass: HomeAssistant): +async def test_circ_pump(hass: HomeAssistant, client: MagicMock) -> None: """Test spa circ pump.""" - with patch( - "homeassistant.components.balboa.BalboaSpaWifi.have_circ_pump", - return_value=True, - ): - config_entry = await _setup_binary_sensor_test(hass) + client.have_circ_pump.return_value = (True,) + config_entry = await init_integration(hass) - state = await _patch_circ_pump(hass, config_entry, True) + state = await _patch_circ_pump(hass, config_entry, True, client) assert state.state == STATE_ON - state = await _patch_circ_pump(hass, config_entry, False) + state = await _patch_circ_pump(hass, config_entry, False, client) assert state.state == STATE_OFF -async def _patch_circ_pump(hass, config_entry, pump_state): +async def _patch_circ_pump(hass, config_entry, pump_state, client): """Patch the circ pump state.""" - with patch( - "homeassistant.components.balboa.BalboaSpaWifi.get_circ_pump", - return_value=pump_state, - ): - async_dispatcher_send(hass, SIGNAL_UPDATE.format(config_entry.entry_id)) - await hass.async_block_till_done() - return hass.states.get(f"{ENTITY_BINARY_SENSOR}circ_pump") - - -async def _patch_filter(hass, config_entry, filter_mode, num): - """Patch the filter state.""" - with patch( - "homeassistant.components.balboa.BalboaSpaWifi.get_filtermode", - return_value=filter_mode, - ): - async_dispatcher_send(hass, SIGNAL_UPDATE.format(config_entry.entry_id)) - await hass.async_block_till_done() - return hass.states.get(f"{ENTITY_BINARY_SENSOR}filter{num}") - - -async def _setup_binary_sensor_test(hass): - """Prepare the test.""" - config_entry = await init_integration_mocked(hass) - await async_setup_component(hass, BALBOA_DOMAIN, config_entry) + client.get_circ_pump.return_value = pump_state + await client.new_data_cb() await hass.async_block_till_done() + return hass.states.get(f"{ENTITY_BINARY_SENSOR}circ_pump") - return config_entry + +async def _patch_filter(hass, config_entry, filter_mode, num, client): + """Patch the filter state.""" + client.get_filtermode.return_value = filter_mode + await client.new_data_cb() + await hass.async_block_till_done() + return hass.states.get(f"{ENTITY_BINARY_SENSOR}filter{num}") diff --git a/tests/components/balboa/test_climate.py b/tests/components/balboa/test_climate.py index 2363c35efaa..94a5b612f2c 100644 --- a/tests/components/balboa/test_climate.py +++ b/tests/components/balboa/test_climate.py @@ -1,10 +1,10 @@ """Tests of the climate entity of the balboa integration.""" +from __future__ import annotations -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest -from homeassistant.components.balboa.const import DOMAIN as BALBOA_DOMAIN, SIGNAL_UPDATE from homeassistant.components.climate.const import ( ATTR_FAN_MODE, ATTR_HVAC_ACTION, @@ -28,10 +28,8 @@ from homeassistant.components.climate.const import ( ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_FAHRENHEIT from homeassistant.core import HomeAssistant -from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.setup import async_setup_component -from . import init_integration_mocked +from . import init_integration from tests.components.climate import common @@ -51,13 +49,13 @@ HVAC_SETTINGS = [ ENTITY_CLIMATE = "climate.fakespa_climate" -async def test_spa_defaults(hass: HomeAssistant): +async def test_spa_defaults(hass: HomeAssistant, client: MagicMock) -> None: """Test supported features flags.""" - - await _setup_climate_test(hass) + await init_integration(hass) state = hass.states.get(ENTITY_CLIMATE) + assert state assert ( state.attributes["supported_features"] == SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE @@ -69,16 +67,15 @@ async def test_spa_defaults(hass: HomeAssistant): assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE -async def test_spa_defaults_fake_tscale(hass: HomeAssistant): +async def test_spa_defaults_fake_tscale(hass: HomeAssistant, client: MagicMock) -> None: """Test supported features flags.""" + client.get_tempscale.return_value = 1 - with patch( - "homeassistant.components.balboa.BalboaSpaWifi.get_tempscale", return_value=1 - ): - await _setup_climate_test(hass) + await init_integration(hass) state = hass.states.get(ENTITY_CLIMATE) + assert state assert ( state.attributes["supported_features"] == SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE @@ -90,20 +87,19 @@ async def test_spa_defaults_fake_tscale(hass: HomeAssistant): assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE -async def test_spa_with_blower(hass: HomeAssistant): +async def test_spa_with_blower(hass: HomeAssistant, client: MagicMock) -> None: """Test supported features flags.""" + client.have_blower.return_value = True - with patch( - "homeassistant.components.balboa.BalboaSpaWifi.have_blower", return_value=True - ): - config_entry = await _setup_climate_test(hass) + config_entry = await init_integration(hass) # force a refresh - async_dispatcher_send(hass, SIGNAL_UPDATE.format(config_entry.entry_id)) + await client.new_data_cb() await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) + assert state assert ( state.attributes["supported_features"] == SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE | SUPPORT_FAN_MODE @@ -111,161 +107,144 @@ async def test_spa_with_blower(hass: HomeAssistant): for fan_state in range(4): # set blower - state = await _patch_blower(hass, config_entry, fan_state) + state = await _patch_blower(hass, config_entry, fan_state, client) + assert state assert state.attributes[ATTR_FAN_MODE] == FAN_SETTINGS[fan_state] # test the nonsense checks - for fan_state in (None, 70): - state = await _patch_blower(hass, config_entry, fan_state) + for fan_state in (None, 70): # type: ignore[assignment] + state = await _patch_blower(hass, config_entry, fan_state, client) + assert state assert state.attributes[ATTR_FAN_MODE] == FAN_OFF -async def test_spa_temperature(hass: HomeAssistant): +async def test_spa_temperature(hass: HomeAssistant, client: MagicMock) -> None: """Test spa temperature settings.""" - config_entry = await _setup_climate_test(hass) + config_entry = await init_integration(hass) # flip the spa into F # set temp to a valid number - state = await _patch_spa_settemp(hass, config_entry, 0, 100.0) + state = await _patch_spa_settemp(hass, config_entry, 0, 100.0, client) + assert state assert state.attributes.get(ATTR_TEMPERATURE) == 38.0 -async def test_spa_temperature_unit(hass: HomeAssistant): +async def test_spa_temperature_unit(hass: HomeAssistant, client: MagicMock) -> None: """Test temperature unit conversions.""" with patch.object(hass.config.units, "temperature_unit", TEMP_FAHRENHEIT): - config_entry = await _setup_climate_test(hass) + config_entry = await init_integration(hass) - state = await _patch_spa_settemp(hass, config_entry, 0, 15.4) + state = await _patch_spa_settemp(hass, config_entry, 0, 15.4, client) + assert state assert state.attributes.get(ATTR_TEMPERATURE) == 15.0 -async def test_spa_hvac_modes(hass: HomeAssistant): +async def test_spa_hvac_modes(hass: HomeAssistant, client: MagicMock) -> None: """Test hvac modes.""" - config_entry = await _setup_climate_test(hass) + config_entry = await init_integration(hass) # try out the different heat modes for heat_mode in range(2): - state = await _patch_spa_heatmode(hass, config_entry, heat_mode) + state = await _patch_spa_heatmode(hass, config_entry, heat_mode, client) + assert state modes = state.attributes.get(ATTR_HVAC_MODES) assert [HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF] == modes assert state.state == HVAC_SETTINGS[heat_mode] with pytest.raises(ValueError): - await _patch_spa_heatmode(hass, config_entry, 2) + await _patch_spa_heatmode(hass, config_entry, 2, client) -async def test_spa_hvac_action(hass: HomeAssistant): +async def test_spa_hvac_action(hass: HomeAssistant, client: MagicMock) -> None: """Test setting of the HVAC action.""" - config_entry = await _setup_climate_test(hass) + config_entry = await init_integration(hass) # try out the different heat states - state = await _patch_spa_heatstate(hass, config_entry, 1) + state = await _patch_spa_heatstate(hass, config_entry, 1, client) + assert state assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT - state = await _patch_spa_heatstate(hass, config_entry, 0) + state = await _patch_spa_heatstate(hass, config_entry, 0, client) assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE -async def test_spa_preset_modes(hass: HomeAssistant): +async def test_spa_preset_modes(hass: HomeAssistant, client: MagicMock) -> None: """Test the various preset modes.""" - config_entry = await _setup_climate_test(hass) + await init_integration(hass) state = hass.states.get(ENTITY_CLIMATE) + assert state modes = state.attributes.get(ATTR_PRESET_MODES) assert ["Ready", "Rest", "Ready in Rest"] == modes # Put it in Ready and Rest modelist = ["Ready", "Rest"] for mode in modelist: - with patch( - "homeassistant.components.balboa.BalboaSpaWifi.get_heatmode", - return_value=modelist.index(mode), - ): - await common.async_set_preset_mode(hass, mode, ENTITY_CLIMATE) - async_dispatcher_send(hass, SIGNAL_UPDATE.format(config_entry.entry_id)) - await hass.async_block_till_done() + client.heatmode = modelist.index(mode) + await common.async_set_preset_mode(hass, mode, ENTITY_CLIMATE) + await client.new_data_cb() + await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes[ATTR_PRESET_MODE] == modelist.index(mode) + assert state + assert state.attributes[ATTR_PRESET_MODE] == mode # put it in RNR and test assertion - with patch( - "homeassistant.components.balboa.BalboaSpaWifi.get_heatmode", - return_value=2, - ), pytest.raises(ValueError): + client.heatmode = 2 + + with pytest.raises(ValueError): await common.async_set_preset_mode(hass, 2, ENTITY_CLIMATE) # Helpers -async def _patch_blower(hass, config_entry, fan_state): +async def _patch_blower(hass, config_entry, fan_state, client): """Patch the blower state.""" - with patch( - "homeassistant.components.balboa.BalboaSpaWifi.get_blower", - return_value=fan_state, - ): - if fan_state is not None and fan_state <= len(FAN_SETTINGS): - await common.async_set_fan_mode(hass, FAN_SETTINGS[fan_state]) - async_dispatcher_send(hass, SIGNAL_UPDATE.format(config_entry.entry_id)) - await hass.async_block_till_done() + client.get_blower.return_value = fan_state - return hass.states.get(ENTITY_CLIMATE) - - -async def _patch_spa_settemp(hass, config_entry, tscale, settemp): - """Patch the settemp.""" - with patch( - "homeassistant.components.balboa.BalboaSpaWifi.get_tempscale", - return_value=tscale, - ), patch( - "homeassistant.components.balboa.BalboaSpaWifi.get_settemp", - return_value=settemp, - ): - await common.async_set_temperature( - hass, temperature=settemp, entity_id=ENTITY_CLIMATE - ) - async_dispatcher_send(hass, SIGNAL_UPDATE.format(config_entry.entry_id)) - await hass.async_block_till_done() - - return hass.states.get(ENTITY_CLIMATE) - - -async def _patch_spa_heatmode(hass, config_entry, heat_mode): - """Patch the heatmode.""" - with patch( - "homeassistant.components.balboa.BalboaSpaWifi.get_heatmode", - return_value=heat_mode, - ): - await common.async_set_hvac_mode(hass, HVAC_SETTINGS[heat_mode], ENTITY_CLIMATE) - async_dispatcher_send(hass, SIGNAL_UPDATE.format(config_entry.entry_id)) - await hass.async_block_till_done() - - return hass.states.get(ENTITY_CLIMATE) - - -async def _patch_spa_heatstate(hass, config_entry, heat_state): - """Patch the heatmode.""" - with patch( - "homeassistant.components.balboa.BalboaSpaWifi.get_heatstate", - return_value=heat_state, - ): - await common.async_set_hvac_mode( - hass, HVAC_SETTINGS[heat_state], ENTITY_CLIMATE - ) - async_dispatcher_send(hass, SIGNAL_UPDATE.format(config_entry.entry_id)) - await hass.async_block_till_done() - - return hass.states.get(ENTITY_CLIMATE) - - -async def _setup_climate_test(hass): - """Prepare the test.""" - config_entry = await init_integration_mocked(hass) - await async_setup_component(hass, BALBOA_DOMAIN, config_entry) + if fan_state is not None and fan_state <= len(FAN_SETTINGS): + await common.async_set_fan_mode(hass, FAN_SETTINGS[fan_state]) + await client.new_data_cb() await hass.async_block_till_done() - return config_entry + return hass.states.get(ENTITY_CLIMATE) + + +async def _patch_spa_settemp(hass, config_entry, tscale, settemp, client): + """Patch the settemp.""" + client.get_tempscale.return_value = tscale + client.get_settemp.return_value = settemp + + await common.async_set_temperature( + hass, temperature=settemp, entity_id=ENTITY_CLIMATE + ) + await client.new_data_cb() + await hass.async_block_till_done() + + return hass.states.get(ENTITY_CLIMATE) + + +async def _patch_spa_heatmode(hass, config_entry, heat_mode, client): + """Patch the heatmode.""" + client.heatmode = heat_mode + + await common.async_set_hvac_mode(hass, HVAC_SETTINGS[heat_mode], ENTITY_CLIMATE) + await client.new_data_cb() + await hass.async_block_till_done() + + return hass.states.get(ENTITY_CLIMATE) + + +async def _patch_spa_heatstate(hass, config_entry, heat_state, client): + """Patch the heatmode.""" + client.get_heatstate.return_value = heat_state + + await common.async_set_hvac_mode(hass, HVAC_SETTINGS[heat_state], ENTITY_CLIMATE) + await client.new_data_cb() + await hass.async_block_till_done() + + return hass.states.get(ENTITY_CLIMATE) diff --git a/tests/components/balboa/test_config_flow.py b/tests/components/balboa/test_config_flow.py index fc12289d90a..98c2a90abe2 100644 --- a/tests/components/balboa/test_config_flow.py +++ b/tests/components/balboa/test_config_flow.py @@ -1,5 +1,5 @@ """Test the Balboa Spa Client config flow.""" -from unittest.mock import patch +from unittest.mock import MagicMock, patch from homeassistant import config_entries, data_entry_flow from homeassistant.components.balboa.const import CONF_SYNC_TIME, DOMAIN @@ -12,8 +12,6 @@ from homeassistant.data_entry_flow import ( RESULT_TYPE_FORM, ) -from . import BalboaMock - from tests.common import MockConfigEntry TEST_DATA = { @@ -22,7 +20,7 @@ TEST_DATA = { TEST_ID = "FakeBalboa" -async def test_form(hass: HomeAssistant) -> None: +async def test_form(hass: HomeAssistant, client: MagicMock) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -31,23 +29,8 @@ async def test_form(hass: HomeAssistant) -> None: assert result["errors"] == {} with patch( - "homeassistant.components.balboa.config_flow.BalboaSpaWifi.connect", - new=BalboaMock.connect, - ), patch( - "homeassistant.components.balboa.config_flow.BalboaSpaWifi.disconnect", - new=BalboaMock.disconnect, - ), patch( - "homeassistant.components.balboa.config_flow.BalboaSpaWifi.listen", - new=BalboaMock.listen, - ), patch( - "homeassistant.components.balboa.config_flow.BalboaSpaWifi.send_mod_ident_req", - new=BalboaMock.send_mod_ident_req, - ), patch( - "homeassistant.components.balboa.config_flow.BalboaSpaWifi.send_panel_req", - new=BalboaMock.send_panel_req, - ), patch( - "homeassistant.components.balboa.config_flow.BalboaSpaWifi.spa_configured", - new=BalboaMock.spa_configured, + "homeassistant.components.balboa.config_flow.BalboaSpaWifi", + return_value=client, ), patch( "homeassistant.components.balboa.async_setup_entry", return_value=True, @@ -63,19 +46,17 @@ async def test_form(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_cannot_connect(hass: HomeAssistant) -> None: +async def test_form_cannot_connect(hass: HomeAssistant, client: MagicMock) -> None: """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) with patch( - "homeassistant.components.balboa.config_flow.BalboaSpaWifi.connect", - new=BalboaMock.broken_connect, - ), patch( - "homeassistant.components.balboa.config_flow.BalboaSpaWifi.disconnect", - new=BalboaMock.disconnect, + "homeassistant.components.balboa.config_flow.BalboaSpaWifi", + return_value=client, ): + client.connect.return_value = False result2 = await hass.config_entries.flow.async_configure( result["flow_id"], TEST_DATA, @@ -85,16 +66,17 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: assert result2["errors"] == {"base": "cannot_connect"} -async def test_unknown_error(hass: HomeAssistant) -> None: +async def test_unknown_error(hass: HomeAssistant, client: MagicMock) -> None: """Test we handle unknown error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) with patch( - "homeassistant.components.balboa.config_flow.BalboaSpaWifi.connect", - side_effect=Exception, + "homeassistant.components.balboa.config_flow.BalboaSpaWifi", + return_value=client, ): + client.connect.side_effect = Exception("Boom") result2 = await hass.config_entries.flow.async_configure( result["flow_id"], TEST_DATA, @@ -104,7 +86,7 @@ async def test_unknown_error(hass: HomeAssistant) -> None: assert result2["errors"] == {"base": "unknown"} -async def test_already_configured(hass: HomeAssistant) -> None: +async def test_already_configured(hass: HomeAssistant, client: MagicMock) -> None: """Test when provided credentials are already configured.""" MockConfigEntry(domain=DOMAIN, data=TEST_DATA, unique_id=TEST_ID).add_to_hass(hass) @@ -116,11 +98,8 @@ async def test_already_configured(hass: HomeAssistant) -> None: assert result["step_id"] == SOURCE_USER with patch( - "homeassistant.components.balboa.config_flow.BalboaSpaWifi.connect", - new=BalboaMock.connect, - ), patch( - "homeassistant.components.balboa.config_flow.BalboaSpaWifi.disconnect", - new=BalboaMock.disconnect, + "homeassistant.components.balboa.config_flow.BalboaSpaWifi", + return_value=client, ), patch( "homeassistant.components.balboa.async_setup_entry", return_value=True, @@ -135,25 +114,15 @@ async def test_already_configured(hass: HomeAssistant) -> None: assert result2["reason"] == "already_configured" -async def test_options_flow(hass): +async def test_options_flow(hass: HomeAssistant, client: MagicMock) -> None: """Test specifying non default settings using options flow.""" config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_DATA, unique_id=TEST_ID) config_entry.add_to_hass(hass) - # Rather than mocking out 15 or so functions, we just need to mock - # the entire library, otherwise it will get stuck in a listener and - # the various loops in pybalboa. - with patch( - "homeassistant.components.balboa.config_flow.BalboaSpaWifi", - new=BalboaMock, - ), patch( - "homeassistant.components.balboa.BalboaSpaWifi", - new=BalboaMock, - ): - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() - result = await hass.config_entries.options.async_init(config_entry.entry_id) + result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "init" @@ -164,4 +133,4 @@ async def test_options_flow(hass): ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert config_entry.options == {CONF_SYNC_TIME: True} + assert dict(config_entry.options) == {CONF_SYNC_TIME: True} diff --git a/tests/components/balboa/test_init.py b/tests/components/balboa/test_init.py index ac0dea3b007..a0b6e6a78ba 100644 --- a/tests/components/balboa/test_init.py +++ b/tests/components/balboa/test_init.py @@ -1,18 +1,18 @@ """Tests of the initialization of the balboa integration.""" -from unittest.mock import patch +from unittest.mock import MagicMock from homeassistant.components.balboa.const import DOMAIN as BALBOA_DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant -from . import TEST_HOST, BalboaMock, init_integration +from . import TEST_HOST, init_integration from tests.common import MockConfigEntry -async def test_setup_entry(hass: HomeAssistant): +async def test_setup_entry(hass: HomeAssistant, client: MagicMock) -> None: """Validate that setup entry also configure the client.""" config_entry = await init_integration(hass) @@ -23,7 +23,7 @@ async def test_setup_entry(hass: HomeAssistant): assert config_entry.state == ConfigEntryState.NOT_LOADED -async def test_setup_entry_fails(hass): +async def test_setup_entry_fails(hass: HomeAssistant, client: MagicMock) -> None: """Validate that setup entry also configure the client.""" config_entry = MockConfigEntry( domain=BALBOA_DOMAIN, @@ -33,11 +33,9 @@ async def test_setup_entry_fails(hass): ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.balboa.BalboaSpaWifi.connect", - new=BalboaMock.broken_connect, - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + client.connect.return_value = False + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() assert config_entry.state == ConfigEntryState.SETUP_RETRY From 3fb4a0a0a802f6acb724db68952df89bbcd13502 Mon Sep 17 00:00:00 2001 From: LJU Date: Mon, 13 Dec 2021 17:13:20 +0100 Subject: [PATCH 0366/2644] Fix typo transponder in LCN (#61658) --- homeassistant/components/lcn/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/lcn/strings.json b/homeassistant/components/lcn/strings.json index 078172ff34a..2ed8cb8d1c7 100644 --- a/homeassistant/components/lcn/strings.json +++ b/homeassistant/components/lcn/strings.json @@ -2,7 +2,7 @@ "device_automation": { "trigger_type": { "transmitter": "transmitter code received", - "transponder": "transpoder code received", + "transponder": "transponder code received", "fingerprint": "fingerprint code received", "send_keys": "send keys received" } From fadbab0e32ff59cab6524eaad0ba193f93142984 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 13 Dec 2021 17:14:37 +0100 Subject: [PATCH 0367/2644] Add additional-tag to machine builds (#61693) --- .github/workflows/builder.yml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 3f8dea3b657..e7dc7ebb270 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -131,7 +131,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2021.11.4 + uses: home-assistant/builder@2021.12.0 with: args: | $BUILD_ARGS \ @@ -170,6 +170,17 @@ jobs: - name: Checkout the repository uses: actions/checkout@v2.4.0 + - name: Set build additional args + run: | + # Create general tags + if [[ "${{ needs.init.outputs.version }}" =~ d ]]; then + echo "BUILD_ARGS=--additional-tag dev" >> $GITHUB_ENV + elif [[ "${{ needs.init.outputs.version }}" =~ b ]]; then + echo "BUILD_ARGS=--additional-tag beta" >> $GITHUB_ENV + else + echo "BUILD_ARGS=--additional-tag stable" >> $GITHUB_ENV + fi + - name: Login to DockerHub uses: docker/login-action@v1.10.0 with: @@ -184,7 +195,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2021.11.4 + uses: home-assistant/builder@2021.12.0 with: args: | $BUILD_ARGS \ From 88a93d5d53ce490484bed6fdb5e84958bdd487d6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 13 Dec 2021 17:35:06 +0100 Subject: [PATCH 0368/2644] Use new enums in growatt_server (#61655) Co-authored-by: epenet --- .../growatt_server/sensor_types/inverter.py | 45 +++++----- .../growatt_server/sensor_types/mix.py | 84 +++++++++---------- .../growatt_server/sensor_types/storage.py | 63 +++++++------- .../growatt_server/sensor_types/tlx.py | 60 ++++++------- .../growatt_server/sensor_types/total.py | 19 ++--- 5 files changed, 120 insertions(+), 151 deletions(-) diff --git a/homeassistant/components/growatt_server/sensor_types/inverter.py b/homeassistant/components/growatt_server/sensor_types/inverter.py index 709ea81b3c5..eb9315c0777 100644 --- a/homeassistant/components/growatt_server/sensor_types/inverter.py +++ b/homeassistant/components/growatt_server/sensor_types/inverter.py @@ -1,13 +1,8 @@ """Growatt Sensor definitions for the Inverter type.""" from __future__ import annotations -from homeassistant.components.sensor import STATE_CLASS_TOTAL +from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.const import ( - DEVICE_CLASS_CURRENT, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_VOLTAGE, ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, @@ -24,7 +19,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Energy today", api_key="powerToday", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, precision=1, ), GrowattSensorEntityDescription( @@ -32,16 +27,16 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Lifetime energy output", api_key="powerTotal", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, precision=1, - state_class=STATE_CLASS_TOTAL, + state_class=SensorStateClass.TOTAL, ), GrowattSensorEntityDescription( key="inverter_voltage_input_1", name="Input 1 voltage", api_key="vpv1", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, + device_class=SensorDeviceClass.VOLTAGE, precision=2, ), GrowattSensorEntityDescription( @@ -49,7 +44,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Input 1 Amperage", api_key="ipv1", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, + device_class=SensorDeviceClass.CURRENT, precision=1, ), GrowattSensorEntityDescription( @@ -57,7 +52,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Input 1 Wattage", api_key="ppv1", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, precision=1, ), GrowattSensorEntityDescription( @@ -65,7 +60,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Input 2 voltage", api_key="vpv2", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, + device_class=SensorDeviceClass.VOLTAGE, precision=1, ), GrowattSensorEntityDescription( @@ -73,7 +68,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Input 2 Amperage", api_key="ipv2", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, + device_class=SensorDeviceClass.CURRENT, precision=1, ), GrowattSensorEntityDescription( @@ -81,7 +76,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Input 2 Wattage", api_key="ppv2", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, precision=1, ), GrowattSensorEntityDescription( @@ -89,7 +84,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Input 3 voltage", api_key="vpv3", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, + device_class=SensorDeviceClass.VOLTAGE, precision=1, ), GrowattSensorEntityDescription( @@ -97,7 +92,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Input 3 Amperage", api_key="ipv3", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, + device_class=SensorDeviceClass.CURRENT, precision=1, ), GrowattSensorEntityDescription( @@ -105,7 +100,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Input 3 Wattage", api_key="ppv3", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, precision=1, ), GrowattSensorEntityDescription( @@ -113,7 +108,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Internal wattage", api_key="ppv", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, precision=1, ), GrowattSensorEntityDescription( @@ -121,7 +116,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Reactive voltage", api_key="vacr", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, + device_class=SensorDeviceClass.VOLTAGE, precision=1, ), GrowattSensorEntityDescription( @@ -129,7 +124,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Reactive amperage", api_key="iacr", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, + device_class=SensorDeviceClass.CURRENT, precision=1, ), GrowattSensorEntityDescription( @@ -144,7 +139,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Output power", api_key="pac", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, precision=1, ), GrowattSensorEntityDescription( @@ -152,7 +147,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Reactive wattage", api_key="pacr", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, precision=1, ), GrowattSensorEntityDescription( @@ -160,7 +155,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Intelligent Power Management temperature", api_key="ipmTemperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, precision=1, ), GrowattSensorEntityDescription( @@ -168,7 +163,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Temperature", api_key="temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, precision=1, ), ) diff --git a/homeassistant/components/growatt_server/sensor_types/mix.py b/homeassistant/components/growatt_server/sensor_types/mix.py index 939da82902a..6cb61ea2e08 100644 --- a/homeassistant/components/growatt_server/sensor_types/mix.py +++ b/homeassistant/components/growatt_server/sensor_types/mix.py @@ -1,16 +1,8 @@ """Growatt Sensor definitions for the Mix type.""" from __future__ import annotations -from homeassistant.components.sensor import ( - STATE_CLASS_TOTAL, - STATE_CLASS_TOTAL_INCREASING, -) +from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.const import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_TIMESTAMP, - DEVICE_CLASS_VOLTAGE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, PERCENTAGE, @@ -27,80 +19,80 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Statement of charge", api_key="capacity", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_BATTERY, + device_class=SensorDeviceClass.BATTERY, ), GrowattSensorEntityDescription( key="mix_battery_charge_today", name="Battery charged today", api_key="eBatChargeToday", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, ), GrowattSensorEntityDescription( key="mix_battery_charge_lifetime", name="Lifetime battery charged", api_key="eBatChargeTotal", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, ), GrowattSensorEntityDescription( key="mix_battery_discharge_today", name="Battery discharged today", api_key="eBatDisChargeToday", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, ), GrowattSensorEntityDescription( key="mix_battery_discharge_lifetime", name="Lifetime battery discharged", api_key="eBatDisChargeTotal", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, ), GrowattSensorEntityDescription( key="mix_solar_generation_today", name="Solar energy today", api_key="epvToday", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, ), GrowattSensorEntityDescription( key="mix_solar_generation_lifetime", name="Lifetime solar energy", api_key="epvTotal", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, ), GrowattSensorEntityDescription( key="mix_battery_discharge_w", name="Battery discharging W", api_key="pDischarge1", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, ), GrowattSensorEntityDescription( key="mix_battery_voltage", name="Battery voltage", api_key="vbat", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, + device_class=SensorDeviceClass.VOLTAGE, ), GrowattSensorEntityDescription( key="mix_pv1_voltage", name="PV1 voltage", api_key="vpv1", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, + device_class=SensorDeviceClass.VOLTAGE, ), GrowattSensorEntityDescription( key="mix_pv2_voltage", name="PV2 voltage", api_key="vpv2", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, + device_class=SensorDeviceClass.VOLTAGE, ), # Values from 'mix_totals' API call GrowattSensorEntityDescription( @@ -108,30 +100,30 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Load consumption today", api_key="elocalLoadToday", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, ), GrowattSensorEntityDescription( key="mix_load_consumption_lifetime", name="Lifetime load consumption", api_key="elocalLoadTotal", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, ), GrowattSensorEntityDescription( key="mix_export_to_grid_today", name="Export to grid today", api_key="etoGridToday", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, ), GrowattSensorEntityDescription( key="mix_export_to_grid_lifetime", name="Lifetime export to grid", api_key="etogridTotal", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, ), # Values from 'mix_system_status' API call GrowattSensorEntityDescription( @@ -139,63 +131,63 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Battery charging", api_key="chargePower", native_unit_of_measurement=POWER_KILO_WATT, - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, ), GrowattSensorEntityDescription( key="mix_load_consumption", name="Load consumption", api_key="pLocalLoad", native_unit_of_measurement=POWER_KILO_WATT, - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, ), GrowattSensorEntityDescription( key="mix_wattage_pv_1", name="PV1 Wattage", api_key="pPv1", native_unit_of_measurement=POWER_KILO_WATT, - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, ), GrowattSensorEntityDescription( key="mix_wattage_pv_2", name="PV2 Wattage", api_key="pPv2", native_unit_of_measurement=POWER_KILO_WATT, - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, ), GrowattSensorEntityDescription( key="mix_wattage_pv_all", name="All PV Wattage", api_key="ppv", native_unit_of_measurement=POWER_KILO_WATT, - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, ), GrowattSensorEntityDescription( key="mix_export_to_grid", name="Export to grid", api_key="pactogrid", native_unit_of_measurement=POWER_KILO_WATT, - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, ), GrowattSensorEntityDescription( key="mix_import_from_grid", name="Import from grid", api_key="pactouser", native_unit_of_measurement=POWER_KILO_WATT, - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, ), GrowattSensorEntityDescription( key="mix_battery_discharge_kw", name="Battery discharging kW", api_key="pdisCharge1", native_unit_of_measurement=POWER_KILO_WATT, - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, ), GrowattSensorEntityDescription( key="mix_grid_voltage", name="Grid voltage", api_key="vAc1", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, + device_class=SensorDeviceClass.VOLTAGE, ), # Values from 'mix_detail' API call GrowattSensorEntityDescription( @@ -203,35 +195,35 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="System production today (self-consumption + export)", api_key="eCharge", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, ), GrowattSensorEntityDescription( key="mix_load_consumption_solar_today", name="Load consumption today (solar)", api_key="eChargeToday", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, ), GrowattSensorEntityDescription( key="mix_self_consumption_today", name="Self consumption today (solar + battery)", api_key="eChargeToday1", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, ), GrowattSensorEntityDescription( key="mix_load_consumption_battery_today", name="Load consumption today (battery)", api_key="echarge1", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, ), GrowattSensorEntityDescription( key="mix_import_from_grid_today", name="Import from grid today (load)", api_key="etouser", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, ), # This sensor is manually created using the most recent X-Axis value from the chartData GrowattSensorEntityDescription( @@ -239,7 +231,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Last Data Update", api_key="lastdataupdate", native_unit_of_measurement=None, - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, ), # Values from 'dashboard_data' API call GrowattSensorEntityDescription( @@ -247,7 +239,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Import from grid today (load + charging)", api_key="etouser_combined", # This id is not present in the raw API data, it is added by the sensor native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), ) diff --git a/homeassistant/components/growatt_server/sensor_types/storage.py b/homeassistant/components/growatt_server/sensor_types/storage.py index 77d1b4b2c00..11e64274686 100644 --- a/homeassistant/components/growatt_server/sensor_types/storage.py +++ b/homeassistant/components/growatt_server/sensor_types/storage.py @@ -1,13 +1,8 @@ """Growatt Sensor definitions for the Storage type.""" from __future__ import annotations -from homeassistant.components.sensor import STATE_CLASS_TOTAL +from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.const import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_CURRENT, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_VOLTAGE, ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, @@ -24,73 +19,73 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Storage production today", api_key="eBatDisChargeToday", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, ), GrowattSensorEntityDescription( key="storage_storage_production_lifetime", name="Lifetime Storage production", api_key="eBatDisChargeTotal", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, ), GrowattSensorEntityDescription( key="storage_grid_discharge_today", name="Grid discharged today", api_key="eacDisChargeToday", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, ), GrowattSensorEntityDescription( key="storage_load_consumption_today", name="Load consumption today", api_key="eopDischrToday", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, ), GrowattSensorEntityDescription( key="storage_load_consumption_lifetime", name="Lifetime load consumption", api_key="eopDischrTotal", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, ), GrowattSensorEntityDescription( key="storage_grid_charged_today", name="Grid charged today", api_key="eacChargeToday", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, ), GrowattSensorEntityDescription( key="storage_charge_storage_lifetime", name="Lifetime storaged charged", api_key="eChargeTotal", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, ), GrowattSensorEntityDescription( key="storage_solar_production", name="Solar power production", api_key="ppv", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, ), GrowattSensorEntityDescription( key="storage_battery_percentage", name="Battery percentage", api_key="capacity", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_BATTERY, + device_class=SensorDeviceClass.BATTERY, ), GrowattSensorEntityDescription( key="storage_power_flow", name="Storage charging/ discharging(-ve)", api_key="pCharge", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, ), GrowattSensorEntityDescription( key="storage_load_consumption_solar_storage", @@ -103,43 +98,43 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Charge today", api_key="eChargeToday", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, ), GrowattSensorEntityDescription( key="storage_import_from_grid", name="Import from grid", api_key="pAcInPut", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, ), GrowattSensorEntityDescription( key="storage_import_from_grid_today", name="Import from grid today", api_key="eToUserToday", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, ), GrowattSensorEntityDescription( key="storage_import_from_grid_total", name="Import from grid total", api_key="eToUserTotal", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, ), GrowattSensorEntityDescription( key="storage_load_consumption", name="Load consumption", api_key="outPutPower", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, ), GrowattSensorEntityDescription( key="storage_grid_voltage", name="AC input voltage", api_key="vGrid", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, + device_class=SensorDeviceClass.VOLTAGE, precision=2, ), GrowattSensorEntityDescription( @@ -147,7 +142,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="PV charging voltage", api_key="vpv", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, + device_class=SensorDeviceClass.VOLTAGE, precision=2, ), GrowattSensorEntityDescription( @@ -162,7 +157,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Output voltage", api_key="outPutVolt", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, + device_class=SensorDeviceClass.VOLTAGE, precision=2, ), GrowattSensorEntityDescription( @@ -177,7 +172,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Solar charge current", api_key="iAcCharge", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, + device_class=SensorDeviceClass.CURRENT, precision=2, ), GrowattSensorEntityDescription( @@ -185,7 +180,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Solar current to storage", api_key="iChargePV1", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, + device_class=SensorDeviceClass.CURRENT, precision=2, ), GrowattSensorEntityDescription( @@ -193,7 +188,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Grid charge current", api_key="chgCurr", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, + device_class=SensorDeviceClass.CURRENT, precision=2, ), GrowattSensorEntityDescription( @@ -201,7 +196,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Grid out current", api_key="outPutCurrent", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, + device_class=SensorDeviceClass.CURRENT, precision=2, ), GrowattSensorEntityDescription( @@ -209,7 +204,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Battery voltage", api_key="vBat", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, + device_class=SensorDeviceClass.VOLTAGE, precision=2, ), GrowattSensorEntityDescription( @@ -217,7 +212,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Load percentage", api_key="loadPercent", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_BATTERY, + device_class=SensorDeviceClass.BATTERY, precision=2, ), ) diff --git a/homeassistant/components/growatt_server/sensor_types/tlx.py b/homeassistant/components/growatt_server/sensor_types/tlx.py index acffb0ac98a..597ddd789cf 100644 --- a/homeassistant/components/growatt_server/sensor_types/tlx.py +++ b/homeassistant/components/growatt_server/sensor_types/tlx.py @@ -1,16 +1,8 @@ """Growatt Sensor definitions for the TLX type.""" from __future__ import annotations -from homeassistant.components.sensor import ( - STATE_CLASS_TOTAL, - STATE_CLASS_TOTAL_INCREASING, -) +from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.const import ( - DEVICE_CLASS_CURRENT, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_VOLTAGE, ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, @@ -27,7 +19,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Energy today", api_key="eacToday", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, precision=1, ), GrowattSensorEntityDescription( @@ -35,8 +27,8 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Lifetime energy output", api_key="eacTotal", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, precision=1, ), GrowattSensorEntityDescription( @@ -44,8 +36,8 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Lifetime total energy input 1", api_key="epv1Total", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, precision=1, ), GrowattSensorEntityDescription( @@ -53,8 +45,8 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Energy Today Input 1", api_key="epv1Today", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, precision=1, ), GrowattSensorEntityDescription( @@ -62,7 +54,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Input 1 voltage", api_key="vpv1", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, + device_class=SensorDeviceClass.VOLTAGE, precision=1, ), GrowattSensorEntityDescription( @@ -70,7 +62,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Input 1 Amperage", api_key="ipv1", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, + device_class=SensorDeviceClass.CURRENT, precision=1, ), GrowattSensorEntityDescription( @@ -78,7 +70,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Input 1 Wattage", api_key="ppv1", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, precision=1, ), GrowattSensorEntityDescription( @@ -86,8 +78,8 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Lifetime total energy input 2", api_key="epv2Total", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, precision=1, ), GrowattSensorEntityDescription( @@ -95,8 +87,8 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Energy Today Input 2", api_key="epv2Today", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, precision=1, ), GrowattSensorEntityDescription( @@ -104,7 +96,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Input 2 voltage", api_key="vpv2", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, + device_class=SensorDeviceClass.VOLTAGE, precision=1, ), GrowattSensorEntityDescription( @@ -112,7 +104,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Input 2 Amperage", api_key="ipv2", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, + device_class=SensorDeviceClass.CURRENT, precision=1, ), GrowattSensorEntityDescription( @@ -120,7 +112,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Input 2 Wattage", api_key="ppv2", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, precision=1, ), GrowattSensorEntityDescription( @@ -128,7 +120,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Internal wattage", api_key="ppv", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, precision=1, ), GrowattSensorEntityDescription( @@ -136,7 +128,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Reactive voltage", api_key="vacrs", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, + device_class=SensorDeviceClass.VOLTAGE, precision=1, ), GrowattSensorEntityDescription( @@ -151,7 +143,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Output power", api_key="pac", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, precision=1, ), GrowattSensorEntityDescription( @@ -159,7 +151,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Temperature 1", api_key="temp1", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, precision=1, ), GrowattSensorEntityDescription( @@ -167,7 +159,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Temperature 2", api_key="temp2", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, precision=1, ), GrowattSensorEntityDescription( @@ -175,7 +167,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Temperature 3", api_key="temp3", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, precision=1, ), GrowattSensorEntityDescription( @@ -183,7 +175,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Temperature 4", api_key="temp4", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, precision=1, ), GrowattSensorEntityDescription( @@ -191,7 +183,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Temperature 5", api_key="temp5", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, precision=1, ), ) diff --git a/homeassistant/components/growatt_server/sensor_types/total.py b/homeassistant/components/growatt_server/sensor_types/total.py index 5f5282748d1..f3d48cf8027 100644 --- a/homeassistant/components/growatt_server/sensor_types/total.py +++ b/homeassistant/components/growatt_server/sensor_types/total.py @@ -1,13 +1,8 @@ """Growatt Sensor definitions for Totals.""" from __future__ import annotations -from homeassistant.components.sensor import STATE_CLASS_TOTAL -from homeassistant.const import ( - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - ENERGY_KILO_WATT_HOUR, - POWER_WATT, -) +from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass +from homeassistant.const import ENERGY_KILO_WATT_HOUR, POWER_WATT from .sensor_entity_description import GrowattSensorEntityDescription @@ -29,28 +24,28 @@ TOTAL_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( name="Energy Today", api_key="todayEnergy", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, ), GrowattSensorEntityDescription( key="total_output_power", name="Output Power", api_key="invTodayPpv", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, ), GrowattSensorEntityDescription( key="total_energy_output", name="Lifetime energy output", api_key="totalEnergy", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, ), GrowattSensorEntityDescription( key="total_maximum_output", name="Maximum power", api_key="nominalPower", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, ), ) From 00b90dff5546f8a4c114b0d540af352bfbef6b5c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 13 Dec 2021 17:36:51 +0100 Subject: [PATCH 0369/2644] Use SensorDeviceClass in greeneye-monitor (#61653) Co-authored-by: epenet --- homeassistant/components/greeneye_monitor/sensor.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/greeneye_monitor/sensor.py b/homeassistant/components/greeneye_monitor/sensor.py index 57c5c79891d..b0d1a246819 100644 --- a/homeassistant/components/greeneye_monitor/sensor.py +++ b/homeassistant/components/greeneye_monitor/sensor.py @@ -5,15 +5,12 @@ from typing import Any, Optional, Union, cast import greeneye -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.const import ( CONF_NAME, CONF_SENSOR_TYPE, CONF_SENSORS, CONF_TEMPERATURE_UNIT, - DEVICE_CLASS_POWER, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_VOLTAGE, ELECTRIC_POTENTIAL_VOLT, POWER_WATT, TIME_HOURS, @@ -162,7 +159,7 @@ class CurrentSensor(GEMSensor): """Entity showing power usage on one channel of the monitor.""" _attr_native_unit_of_measurement = UNIT_WATTS - _attr_device_class = DEVICE_CLASS_POWER + _attr_device_class = SensorDeviceClass.POWER def __init__( self, monitor_serial_number: int, number: int, name: str, net_metering: bool @@ -261,7 +258,7 @@ class PulseCounter(GEMSensor): class TemperatureSensor(GEMSensor): """Entity showing temperature from one temperature sensor.""" - _attr_device_class = DEVICE_CLASS_TEMPERATURE + _attr_device_class = SensorDeviceClass.TEMPERATURE def __init__( self, monitor_serial_number: int, number: int, name: str, unit: str @@ -291,7 +288,7 @@ class VoltageSensor(GEMSensor): """Entity showing voltage.""" _attr_native_unit_of_measurement = ELECTRIC_POTENTIAL_VOLT - _attr_device_class = DEVICE_CLASS_VOLTAGE + _attr_device_class = SensorDeviceClass.VOLTAGE def __init__(self, monitor_serial_number: int, number: int, name: str) -> None: """Construct the entity.""" From 3098778001843bf7f879e506c25745002ce7e009 Mon Sep 17 00:00:00 2001 From: Austin Mroczek Date: Mon, 13 Dec 2021 08:39:11 -0800 Subject: [PATCH 0370/2644] Bump total_connect_client to 2021.12 (#61634) --- homeassistant/components/totalconnect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/totalconnect/manifest.json b/homeassistant/components/totalconnect/manifest.json index 0eec41968cc..15854881ae3 100644 --- a/homeassistant/components/totalconnect/manifest.json +++ b/homeassistant/components/totalconnect/manifest.json @@ -2,7 +2,7 @@ "domain": "totalconnect", "name": "Total Connect", "documentation": "https://www.home-assistant.io/integrations/totalconnect", - "requirements": ["total_connect_client==2021.11.4"], + "requirements": ["total_connect_client==2021.12"], "dependencies": [], "codeowners": ["@austinmroczek"], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 578839a5b34..a4db8fdea79 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2341,7 +2341,7 @@ tololib==0.1.0b3 toonapi==0.2.1 # homeassistant.components.totalconnect -total_connect_client==2021.11.4 +total_connect_client==2021.12 # homeassistant.components.tplink_lte tp-connected==0.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 910005d493f..966fe4b854c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1382,7 +1382,7 @@ tololib==0.1.0b3 toonapi==0.2.1 # homeassistant.components.totalconnect -total_connect_client==2021.11.4 +total_connect_client==2021.12 # homeassistant.components.transmission transmissionrpc==0.11 From 3cfc349e998b0518d856f7de55140e2d5829208d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 13 Dec 2021 17:39:38 +0100 Subject: [PATCH 0371/2644] Use new DeviceClass enum in geniushub (#61608) Co-authored-by: epenet --- homeassistant/components/geniushub/sensor.py | 6 +++--- homeassistant/components/geniushub/switch.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/geniushub/sensor.py b/homeassistant/components/geniushub/sensor.py index 362e729f57a..c179fe7a588 100644 --- a/homeassistant/components/geniushub/sensor.py +++ b/homeassistant/components/geniushub/sensor.py @@ -4,8 +4,8 @@ from __future__ import annotations from datetime import timedelta from typing import Any -from homeassistant.components.sensor import SensorEntity -from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.const import PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util @@ -76,7 +76,7 @@ class GeniusBattery(GeniusDevice, SensorEntity): @property def device_class(self) -> str: """Return the device class of the sensor.""" - return DEVICE_CLASS_BATTERY + return SensorDeviceClass.BATTERY @property def native_unit_of_measurement(self) -> str: diff --git a/homeassistant/components/geniushub/switch.py b/homeassistant/components/geniushub/switch.py index 2666f3d365b..aebf64d21bd 100644 --- a/homeassistant/components/geniushub/switch.py +++ b/homeassistant/components/geniushub/switch.py @@ -3,7 +3,7 @@ from datetime import timedelta import voluptuous as vol -from homeassistant.components.switch import DEVICE_CLASS_OUTLET, SwitchEntity +from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.typing import ConfigType @@ -55,7 +55,7 @@ class GeniusSwitch(GeniusZone, SwitchEntity): @property def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" - return DEVICE_CLASS_OUTLET + return SwitchDeviceClass.OUTLET @property def is_on(self) -> bool: From efbec55818ed5777160dc3b654c4da061d306fe6 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 13 Dec 2021 08:41:45 -0800 Subject: [PATCH 0372/2644] Suppress errors for legacy nest api when using media source (#61629) --- homeassistant/components/nest/media_source.py | 3 ++ tests/components/nest/test_media_source.py | 32 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/homeassistant/components/nest/media_source.py b/homeassistant/components/nest/media_source.py index 328a9b000b3..0f26331d7e5 100644 --- a/homeassistant/components/nest/media_source.py +++ b/homeassistant/components/nest/media_source.py @@ -64,6 +64,9 @@ async def async_get_media_source(hass: HomeAssistant) -> MediaSource: async def get_media_source_devices(hass: HomeAssistant) -> Mapping[str, Device]: """Return a mapping of device id to eligible Nest event media devices.""" + if DATA_SUBSCRIBER not in hass.data[DOMAIN]: + # Integration unloaded, or is legacy nest integration + return {} subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER] device_manager = await subscriber.async_get_device_manager() device_registry = await hass.helpers.device_registry.async_get_registry() diff --git a/tests/components/nest/test_media_source.py b/tests/components/nest/test_media_source.py index f52b89c4f4d..95f2afa8a06 100644 --- a/tests/components/nest/test_media_source.py +++ b/tests/components/nest/test_media_source.py @@ -16,6 +16,7 @@ from homeassistant.components import media_source from homeassistant.components.media_player.errors import BrowseError from homeassistant.components.media_source import const from homeassistant.components.media_source.error import Unresolvable +from homeassistant.config_entries import ConfigEntryState from homeassistant.helpers import device_registry as dr from homeassistant.helpers.template import DATE_STR_FORMAT import homeassistant.util.dt as dt_util @@ -164,6 +165,37 @@ async def test_supported_device(hass, auth): assert len(browse.children) == 0 +async def test_integration_unloaded(hass, auth): + """Test the media player loads, but has no devices, when config unloaded.""" + await async_setup_devices( + hass, + auth, + CAMERA_DEVICE_TYPE, + CAMERA_TRAITS, + ) + + browse = await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}") + assert browse.domain == DOMAIN + assert browse.identifier == "" + assert browse.title == "Nest" + assert len(browse.children) == 1 + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + entry = entries[0] + assert entry.state is ConfigEntryState.LOADED + + assert await hass.config_entries.async_unload(entry.entry_id) + assert entry.state == ConfigEntryState.NOT_LOADED + + # No devices returned + browse = await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}") + assert browse.domain == DOMAIN + assert browse.identifier == "" + assert browse.title == "Nest" + assert len(browse.children) == 0 + + async def test_camera_event(hass, auth, hass_client): """Test a media source and image created for an event.""" event_timestamp = dt_util.now() From e0b29d1800dba5df09a7c562484550f63a5c9a64 Mon Sep 17 00:00:00 2001 From: Jonathan Keljo Date: Mon, 13 Dec 2021 10:23:48 -0800 Subject: [PATCH 0373/2644] Update sisyphus-control to version 3.1.2 (#58198) * Update sisyphus_control dependency to version 3.1.1 [3.1.1] - 2021-10-21 ==================== Changed ------- * Re-enabled support for Python 3.8 (Home Assistant won't remove that until next year) [3.1] - 2021-10-21 ================== Added ----- * Python types for the entire API * `Table.firmware_version` and `Table.mac_address` Fixed ------- * `find_table_ips` returns an empty list rather than `None` if none are found * Locked dependency versions of `python-socketio` and `python-engineio` to those that support the SocketIO protocol version used by Sisyphus * Remove spurious test requirements change * Update to sisyphus-control 3.1.2 [3.1.2] - 2021-12-13 ==================== Changed ------- * Relax version requirements for socketio/engineio --- homeassistant/components/sisyphus/manifest.json | 10 +++++++--- requirements_all.txt | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sisyphus/manifest.json b/homeassistant/components/sisyphus/manifest.json index d8a0392ab55..1e0f1dc5bad 100644 --- a/homeassistant/components/sisyphus/manifest.json +++ b/homeassistant/components/sisyphus/manifest.json @@ -2,7 +2,11 @@ "domain": "sisyphus", "name": "Sisyphus", "documentation": "https://www.home-assistant.io/integrations/sisyphus", - "requirements": ["sisyphus-control==3.0"], - "codeowners": ["@jkeljo"], + "requirements": [ + "sisyphus-control==3.1.2" + ], + "codeowners": [ + "@jkeljo" + ], "iot_class": "local_push" -} +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index a4db8fdea79..824f45eed31 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2161,7 +2161,7 @@ simplepush==1.1.4 simplisafe-python==2021.12.1 # homeassistant.components.sisyphus -sisyphus-control==3.0 +sisyphus-control==3.1.2 # homeassistant.components.skybell skybellpy==0.6.3 From 237232dad6ccdb76cfb3da4c0a45d4909293c2af Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 13 Dec 2021 10:46:57 -0800 Subject: [PATCH 0374/2644] Bump aiohue to 3.0.4 (#61709) --- homeassistant/components/hue/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index ee337cd3d71..7f424f14594 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==3.0.3"], + "requirements": ["aiohue==3.0.4"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 824f45eed31..9747dba5700 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -192,7 +192,7 @@ aiohomekit==0.6.4 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.3 +aiohue==3.0.4 # homeassistant.components.imap aioimaplib==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 966fe4b854c..ddd6dc0849a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -137,7 +137,7 @@ aiohomekit==0.6.4 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.3 +aiohue==3.0.4 # homeassistant.components.apache_kafka aiokafka==0.6.0 From b68a2747f31efa37f992e62e0a59a2856dc1a329 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 13 Dec 2021 20:03:01 +0100 Subject: [PATCH 0375/2644] Use relative imports [A-H] (#61574) --- .../alarm_control_panel/device_condition.py | 12 +++++------- .../alarm_control_panel/device_trigger.py | 12 ++++++------ .../components/august/binary_sensor.py | 2 +- homeassistant/components/august/sensor.py | 2 +- homeassistant/components/awair/sensor.py | 2 +- .../components/azure_devops/config_flow.py | 8 ++------ .../components/azure_devops/sensor.py | 8 +++----- homeassistant/components/blink/config_flow.py | 7 ++----- .../components/cover/reproduce_state.py | 14 +++++++------- homeassistant/components/deconz/config_flow.py | 3 +-- homeassistant/components/deconz/services.py | 2 +- .../device_automation/toggle_entity.py | 18 +++++++++--------- .../components/device_automation/trigger.py | 5 +---- .../components/dynalite/dynalitebase.py | 2 +- homeassistant/components/efergy/sensor.py | 2 +- homeassistant/components/elmax/common.py | 3 ++- homeassistant/components/elmax/config_flow.py | 7 ++++--- homeassistant/components/elmax/switch.py | 7 ++++--- .../components/geo_location/trigger.py | 3 ++- .../components/google_travel_time/helpers.py | 3 ++- .../components/honeywell/config_flow.py | 2 +- 21 files changed, 57 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/device_condition.py b/homeassistant/components/alarm_control_panel/device_condition.py index cc01f4a5954..a378cf262ed 100644 --- a/homeassistant/components/alarm_control_panel/device_condition.py +++ b/homeassistant/components/alarm_control_panel/device_condition.py @@ -5,13 +5,6 @@ from typing import Final import voluptuous as vol -from homeassistant.components.alarm_control_panel.const import ( - SUPPORT_ALARM_ARM_AWAY, - SUPPORT_ALARM_ARM_CUSTOM_BYPASS, - SUPPORT_ALARM_ARM_HOME, - SUPPORT_ALARM_ARM_NIGHT, - SUPPORT_ALARM_ARM_VACATION, -) from homeassistant.const import ( ATTR_ENTITY_ID, CONF_CONDITION, @@ -42,6 +35,11 @@ from .const import ( CONDITION_ARMED_VACATION, CONDITION_DISARMED, CONDITION_TRIGGERED, + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_CUSTOM_BYPASS, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, + SUPPORT_ALARM_ARM_VACATION, ) CONDITION_TYPES: Final[set[str]] = { diff --git a/homeassistant/components/alarm_control_panel/device_trigger.py b/homeassistant/components/alarm_control_panel/device_trigger.py index 9eea745862a..ce53596fc8d 100644 --- a/homeassistant/components/alarm_control_panel/device_trigger.py +++ b/homeassistant/components/alarm_control_panel/device_trigger.py @@ -5,12 +5,6 @@ from typing import Any, Final import voluptuous as vol -from homeassistant.components.alarm_control_panel.const import ( - SUPPORT_ALARM_ARM_AWAY, - SUPPORT_ALARM_ARM_HOME, - SUPPORT_ALARM_ARM_NIGHT, - SUPPORT_ALARM_ARM_VACATION, -) from homeassistant.components.automation import ( AutomationActionType, AutomationTriggerInfo, @@ -38,6 +32,12 @@ from homeassistant.helpers.entity import get_supported_features from homeassistant.helpers.typing import ConfigType from . import DOMAIN +from .const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, + SUPPORT_ALARM_ARM_VACATION, +) BASIC_TRIGGER_TYPES: Final[set[str]] = {"triggered", "disarmed", "arming"} TRIGGER_TYPES: Final[set[str]] = BASIC_TRIGGER_TYPES | { diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index d8c2117f4a3..2ec53b06bdb 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -17,7 +17,6 @@ from yalexs.doorbell import DoorbellDetail from yalexs.lock import LockDoorStatus from yalexs.util import update_lock_detail_from_activity -from homeassistant.components.august import AugustData from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, @@ -27,6 +26,7 @@ from homeassistant.core import callback from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.event import async_call_later +from . import AugustData from .const import ACTIVITY_UPDATE_INTERVAL, DATA_AUGUST, DOMAIN from .entity import AugustEntityMixin diff --git a/homeassistant/components/august/sensor.py b/homeassistant/components/august/sensor.py index 031e6fd9282..a7b28070f8e 100644 --- a/homeassistant/components/august/sensor.py +++ b/homeassistant/components/august/sensor.py @@ -10,7 +10,6 @@ from yalexs.activity import ActivityType from yalexs.keypad import KeypadDetail from yalexs.lock import LockDetail -from homeassistant.components.august import AugustData from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -22,6 +21,7 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.helpers.restore_state import RestoreEntity +from . import AugustData from .const import ( ATTR_OPERATION_AUTORELOCK, ATTR_OPERATION_KEYPAD, diff --git a/homeassistant/components/awair/sensor.py b/homeassistant/components/awair/sensor.py index 4e67e56cfe3..b74c19330ed 100644 --- a/homeassistant/components/awair/sensor.py +++ b/homeassistant/components/awair/sensor.py @@ -5,7 +5,6 @@ from python_awair.air_data import AirData from python_awair.devices import AwairDevice import voluptuous as vol -from homeassistant.components.awair import AwairDataUpdateCoordinator, AwairResult from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( @@ -21,6 +20,7 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import AwairDataUpdateCoordinator, AwairResult from .const import ( API_DUST, API_PM25, diff --git a/homeassistant/components/azure_devops/config_flow.py b/homeassistant/components/azure_devops/config_flow.py index 30073031195..350bad5852a 100644 --- a/homeassistant/components/azure_devops/config_flow.py +++ b/homeassistant/components/azure_devops/config_flow.py @@ -3,14 +3,10 @@ from aioazuredevops.client import DevOpsClient import aiohttp import voluptuous as vol -from homeassistant.components.azure_devops.const import ( - CONF_ORG, - CONF_PAT, - CONF_PROJECT, - DOMAIN, -) from homeassistant.config_entries import ConfigFlow +from .const import CONF_ORG, CONF_PAT, CONF_PROJECT, DOMAIN + class AzureDevOpsFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a Azure DevOps config flow.""" diff --git a/homeassistant/components/azure_devops/sensor.py b/homeassistant/components/azure_devops/sensor.py index d249e8a8088..ac884f73d68 100644 --- a/homeassistant/components/azure_devops/sensor.py +++ b/homeassistant/components/azure_devops/sensor.py @@ -7,17 +7,15 @@ from typing import Any from aioazuredevops.builds import DevOpsBuild -from homeassistant.components.azure_devops import ( - AzureDevOpsDeviceEntity, - AzureDevOpsEntityDescription, -) -from homeassistant.components.azure_devops.const import CONF_ORG, DOMAIN from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType +from . import AzureDevOpsDeviceEntity, AzureDevOpsEntityDescription +from .const import CONF_ORG, DOMAIN + @dataclass class AzureDevOpsSensorEntityDescriptionMixin: diff --git a/homeassistant/components/blink/config_flow.py b/homeassistant/components/blink/config_flow.py index b46243a12d9..a4bee490fb3 100644 --- a/homeassistant/components/blink/config_flow.py +++ b/homeassistant/components/blink/config_flow.py @@ -6,11 +6,6 @@ from blinkpy.blinkpy import Blink, BlinkSetupError import voluptuous as vol from homeassistant import config_entries, core, exceptions -from homeassistant.components.blink.const import ( - DEFAULT_SCAN_INTERVAL, - DEVICE_ID, - DOMAIN, -) from homeassistant.const import ( CONF_PASSWORD, CONF_PIN, @@ -19,6 +14,8 @@ from homeassistant.const import ( ) from homeassistant.core import callback +from .const import DEFAULT_SCAN_INTERVAL, DEVICE_ID, DOMAIN + _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/cover/reproduce_state.py b/homeassistant/components/cover/reproduce_state.py index 1be68bcfeba..59846627890 100644 --- a/homeassistant/components/cover/reproduce_state.py +++ b/homeassistant/components/cover/reproduce_state.py @@ -6,12 +6,6 @@ from collections.abc import Iterable import logging from typing import Any -from homeassistant.components.cover import ( - ATTR_CURRENT_POSITION, - ATTR_CURRENT_TILT_POSITION, - ATTR_POSITION, - ATTR_TILT_POSITION, -) from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_CLOSE_COVER, @@ -27,7 +21,13 @@ from homeassistant.const import ( ) from homeassistant.core import Context, HomeAssistant, State -from . import DOMAIN +from . import ( + ATTR_CURRENT_POSITION, + ATTR_CURRENT_TILT_POSITION, + ATTR_POSITION, + ATTR_TILT_POSITION, + DOMAIN, +) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 473cbd72971..e4d61394ce0 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -19,7 +19,6 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import ssdp -from homeassistant.components.deconz.gateway import DeconzGateway from homeassistant.components.hassio import HassioServiceInfo from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT @@ -36,7 +35,7 @@ from .const import ( DOMAIN, LOGGER, ) -from .gateway import get_gateway_from_config_entry +from .gateway import DeconzGateway, get_gateway_from_config_entry DECONZ_MANUFACTURERURL = "http://www.dresden-elektronik.de" CONF_SERIAL = "serial" diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index 529616138a2..aeb528c0ac9 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -5,7 +5,6 @@ from types import MappingProxyType from pydeconz.utils import normalize_bridge_id import voluptuous as vol -from homeassistant.components.deconz.gateway import DeconzGateway from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import ( config_validation as cv, @@ -20,6 +19,7 @@ from homeassistant.helpers.entity_registry import ( from .config_flow import get_master_gateway from .const import CONF_BRIDGE_ID, DOMAIN, LOGGER +from .gateway import DeconzGateway DECONZ_SERVICES = "deconz_services" diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index 2d0254b9a0a..8128eca9dbc 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -9,15 +9,6 @@ from homeassistant.components.automation import ( AutomationActionType, AutomationTriggerInfo, ) -from homeassistant.components.device_automation.const import ( - CONF_IS_OFF, - CONF_IS_ON, - CONF_TOGGLE, - CONF_TURN_OFF, - CONF_TURN_ON, - CONF_TURNED_OFF, - CONF_TURNED_ON, -) from homeassistant.components.homeassistant.triggers import state as state_trigger from homeassistant.const import ( ATTR_ENTITY_ID, @@ -33,6 +24,15 @@ from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.helpers.typing import ConfigType, TemplateVarsType from . import DEVICE_TRIGGER_BASE_SCHEMA +from .const import ( + CONF_IS_OFF, + CONF_IS_ON, + CONF_TOGGLE, + CONF_TURN_OFF, + CONF_TURN_ON, + CONF_TURNED_OFF, + CONF_TURNED_ON, +) # mypy: allow-untyped-calls, allow-untyped-defs diff --git a/homeassistant/components/device_automation/trigger.py b/homeassistant/components/device_automation/trigger.py index 1a63dcb9e9b..62bd8d1c808 100644 --- a/homeassistant/components/device_automation/trigger.py +++ b/homeassistant/components/device_automation/trigger.py @@ -1,12 +1,9 @@ """Offer device oriented automation.""" import voluptuous as vol -from homeassistant.components.device_automation import ( - DEVICE_TRIGGER_BASE_SCHEMA, - async_get_device_automation_platform, -) from homeassistant.const import CONF_DOMAIN +from . import DEVICE_TRIGGER_BASE_SCHEMA, async_get_device_automation_platform from .exceptions import InvalidDeviceAutomationConfig # mypy: allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/components/dynalite/dynalitebase.py b/homeassistant/components/dynalite/dynalitebase.py index 9bb4f3aeb27..c1814307d1c 100644 --- a/homeassistant/components/dynalite/dynalitebase.py +++ b/homeassistant/components/dynalite/dynalitebase.py @@ -4,13 +4,13 @@ from __future__ import annotations from collections.abc import Callable from typing import Any -from homeassistant.components.dynalite.bridge import DynaliteBridge 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 DeviceInfo, Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback +from .bridge import DynaliteBridge from .const import DOMAIN, LOGGER diff --git a/homeassistant/components/efergy/sensor.py b/homeassistant/components/efergy/sensor.py index e2d71716f24..b448c6db6ec 100644 --- a/homeassistant/components/efergy/sensor.py +++ b/homeassistant/components/efergy/sensor.py @@ -7,7 +7,6 @@ from re import sub from pyefergy import Efergy, exceptions import voluptuous as vol -from homeassistant.components.efergy import EfergyEntity from homeassistant.components.sensor import ( PLATFORM_SCHEMA, SensorDeviceClass, @@ -29,6 +28,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from . import EfergyEntity from .const import CONF_APPTOKEN, CONF_CURRENT_VALUES, DATA_KEY_API, DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/elmax/common.py b/homeassistant/components/elmax/common.py index b44ffa2152b..43d1cbff150 100644 --- a/homeassistant/components/elmax/common.py +++ b/homeassistant/components/elmax/common.py @@ -18,12 +18,13 @@ from elmax_api.http import Elmax from elmax_api.model.endpoint import DeviceEndpoint from elmax_api.model.panel import PanelEntry, PanelStatus -from homeassistant.components.elmax.const import DEFAULT_TIMEOUT, DOMAIN from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from .const import DEFAULT_TIMEOUT, DOMAIN + _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/elmax/config_flow.py b/homeassistant/components/elmax/config_flow.py index c19c49f4b0b..5cd2169c695 100644 --- a/homeassistant/components/elmax/config_flow.py +++ b/homeassistant/components/elmax/config_flow.py @@ -10,7 +10,10 @@ from elmax_api.model.panel import PanelEntry import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.elmax.const import ( +from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError + +from .const import ( CONF_ELMAX_PANEL_ID, CONF_ELMAX_PANEL_NAME, CONF_ELMAX_PANEL_PIN, @@ -18,8 +21,6 @@ from homeassistant.components.elmax.const import ( CONF_ELMAX_USERNAME, DOMAIN, ) -from homeassistant.data_entry_flow import FlowResult -from homeassistant.exceptions import HomeAssistantError _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/elmax/switch.py b/homeassistant/components/elmax/switch.py index 4a8cd5f4214..11c0c406576 100644 --- a/homeassistant/components/elmax/switch.py +++ b/homeassistant/components/elmax/switch.py @@ -4,14 +4,15 @@ from typing import Any from elmax_api.model.command import SwitchCommand from elmax_api.model.panel import PanelStatus -from homeassistant.components.elmax import ElmaxCoordinator -from homeassistant.components.elmax.common import ElmaxEntity -from homeassistant.components.elmax.const import DOMAIN from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import HomeAssistantType +from . import ElmaxCoordinator +from .common import ElmaxEntity +from .const import DOMAIN + class ElmaxSwitch(ElmaxEntity, SwitchEntity): """Implement the Elmax switch entity.""" diff --git a/homeassistant/components/geo_location/trigger.py b/homeassistant/components/geo_location/trigger.py index c030b3d3075..e57c7a9aec6 100644 --- a/homeassistant/components/geo_location/trigger.py +++ b/homeassistant/components/geo_location/trigger.py @@ -3,13 +3,14 @@ import logging import voluptuous as vol -from homeassistant.components.geo_location import DOMAIN from homeassistant.const import CONF_EVENT, CONF_PLATFORM, CONF_SOURCE, CONF_ZONE from homeassistant.core import HassJob, callback from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers.config_validation import entity_domain from homeassistant.helpers.event import TrackStates, async_track_state_change_filtered +from . import DOMAIN + # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/google_travel_time/helpers.py b/homeassistant/components/google_travel_time/helpers.py index 00d3119e868..f295bceb65d 100644 --- a/homeassistant/components/google_travel_time/helpers.py +++ b/homeassistant/components/google_travel_time/helpers.py @@ -3,10 +3,11 @@ from googlemaps import Client from googlemaps.distance_matrix import distance_matrix from googlemaps.exceptions import ApiError -from homeassistant.components.google_travel_time.const import TRACKABLE_DOMAINS from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE from homeassistant.helpers import location +from .const import TRACKABLE_DOMAINS + def is_valid_config_entry(hass, logger, api_key, origin, destination): """Return whether the config entry data is valid.""" diff --git a/homeassistant/components/honeywell/config_flow.py b/homeassistant/components/honeywell/config_flow.py index 318809aaa03..ecf42f4533f 100644 --- a/homeassistant/components/honeywell/config_flow.py +++ b/homeassistant/components/honeywell/config_flow.py @@ -2,9 +2,9 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.honeywell import get_somecomfort_client from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from . import get_somecomfort_client from .const import CONF_COOL_AWAY_TEMPERATURE, CONF_HEAT_AWAY_TEMPERATURE, DOMAIN From 482e4578142cdf67a5b59884ae165b1831290658 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 13 Dec 2021 20:44:35 +0100 Subject: [PATCH 0376/2644] Use BinarySensorDeviceClass in hikvision (#61722) Co-authored-by: epenet --- .../components/hikvision/binary_sensor.py | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/hikvision/binary_sensor.py b/homeassistant/components/hikvision/binary_sensor.py index 0d57278c826..90a8401a7c1 100644 --- a/homeassistant/components/hikvision/binary_sensor.py +++ b/homeassistant/components/hikvision/binary_sensor.py @@ -6,9 +6,8 @@ from pyhik.hikvision import HikCamera import voluptuous as vol from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_CONNECTIVITY, - DEVICE_CLASS_MOTION, PLATFORM_SCHEMA, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.const import ( @@ -39,28 +38,28 @@ DEFAULT_DELAY = 0 ATTR_DELAY = "delay" DEVICE_CLASS_MAP = { - "Motion": DEVICE_CLASS_MOTION, - "Line Crossing": DEVICE_CLASS_MOTION, - "Field Detection": DEVICE_CLASS_MOTION, + "Motion": BinarySensorDeviceClass.MOTION, + "Line Crossing": BinarySensorDeviceClass.MOTION, + "Field Detection": BinarySensorDeviceClass.MOTION, "Video Loss": None, - "Tamper Detection": DEVICE_CLASS_MOTION, + "Tamper Detection": BinarySensorDeviceClass.MOTION, "Shelter Alarm": None, "Disk Full": None, "Disk Error": None, - "Net Interface Broken": DEVICE_CLASS_CONNECTIVITY, - "IP Conflict": DEVICE_CLASS_CONNECTIVITY, + "Net Interface Broken": BinarySensorDeviceClass.CONNECTIVITY, + "IP Conflict": BinarySensorDeviceClass.CONNECTIVITY, "Illegal Access": None, "Video Mismatch": None, "Bad Video": None, - "PIR Alarm": DEVICE_CLASS_MOTION, - "Face Detection": DEVICE_CLASS_MOTION, - "Scene Change Detection": DEVICE_CLASS_MOTION, + "PIR Alarm": BinarySensorDeviceClass.MOTION, + "Face Detection": BinarySensorDeviceClass.MOTION, + "Scene Change Detection": BinarySensorDeviceClass.MOTION, "I/O": None, - "Unattended Baggage": DEVICE_CLASS_MOTION, - "Attended Baggage": DEVICE_CLASS_MOTION, + "Unattended Baggage": BinarySensorDeviceClass.MOTION, + "Attended Baggage": BinarySensorDeviceClass.MOTION, "Recording Failure": None, - "Exiting Region": DEVICE_CLASS_MOTION, - "Entering Region": DEVICE_CLASS_MOTION, + "Exiting Region": BinarySensorDeviceClass.MOTION, + "Entering Region": BinarySensorDeviceClass.MOTION, } CUSTOMIZE_SCHEMA = vol.Schema( From 782229ff44f371a66b7b210dd3cdbaf9070ca533 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 13 Dec 2021 20:48:44 +0100 Subject: [PATCH 0377/2644] Use _attr_* in hddtemp (#61721) Co-authored-by: epenet --- homeassistant/components/hddtemp/sensor.py | 41 +++++++--------------- 1 file changed, 12 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/hddtemp/sensor.py b/homeassistant/components/hddtemp/sensor.py index 49d1c2f28fa..da916d71e23 100644 --- a/homeassistant/components/hddtemp/sensor.py +++ b/homeassistant/components/hddtemp/sensor.py @@ -6,13 +6,16 @@ from telnetlib import Telnet import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorDeviceClass, + SensorEntity, +) from homeassistant.const import ( CONF_DISKS, CONF_HOST, CONF_NAME, CONF_PORT, - DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) @@ -63,34 +66,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class HddTempSensor(SensorEntity): """Representation of a HDDTemp sensor.""" + _attr_device_class = SensorDeviceClass.TEMPERATURE + def __init__(self, name, disk, hddtemp): """Initialize a HDDTemp sensor.""" self.hddtemp = hddtemp self.disk = disk - self._name = f"{name} {disk}" - self._state = None + self._attr_name = f"{name} {disk}" self._details = None - self._unit = None - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def native_value(self): - """Return the state of the device.""" - return self._state - - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return DEVICE_CLASS_TEMPERATURE - - @property - def native_unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return self._unit @property def extra_state_attributes(self): @@ -104,13 +87,13 @@ class HddTempSensor(SensorEntity): if self.hddtemp.data and self.disk in self.hddtemp.data: self._details = self.hddtemp.data[self.disk].split("|") - self._state = self._details[2] + self._attr_native_value = self._details[2] if self._details is not None and self._details[3] == "F": - self._unit = TEMP_FAHRENHEIT + self._attr_native_unit_of_measurement = TEMP_FAHRENHEIT else: - self._unit = TEMP_CELSIUS + self._attr_native_unit_of_measurement = TEMP_CELSIUS else: - self._state = None + self._attr_native_value = None class HddTempData: From 7adffe692799ecf8924b1b39726f387608385f56 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 13 Dec 2021 21:14:50 +0100 Subject: [PATCH 0378/2644] Use SensorStateClass in hassio (#61720) Co-authored-by: epenet --- homeassistant/components/hassio/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hassio/sensor.py b/homeassistant/components/hassio/sensor.py index 0608a9f817b..42be1ff4b0a 100644 --- a/homeassistant/components/hassio/sensor.py +++ b/homeassistant/components/hassio/sensor.py @@ -2,9 +2,9 @@ from __future__ import annotations from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE @@ -42,7 +42,7 @@ ADDON_ENTITY_DESCRIPTIONS = COMMON_ENTITY_DESCRIPTIONS + ( name="CPU Percent", icon="mdi:cpu-64-bit", native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( entity_registry_enabled_default=False, @@ -50,7 +50,7 @@ ADDON_ENTITY_DESCRIPTIONS = COMMON_ENTITY_DESCRIPTIONS + ( name="Memory Percent", icon="mdi:memory", native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), ) From 65ec251309de004d449b59f309fe0c73b3e75b27 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 Dec 2021 21:38:22 +0100 Subject: [PATCH 0379/2644] Fix updating apple_tv addresses (#61724) --- .../components/apple_tv/config_flow.py | 9 ++-- tests/components/apple_tv/test_config_flow.py | 47 ++++++++++++++----- 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/apple_tv/config_flow.py b/homeassistant/components/apple_tv/config_flow.py index 878483e0ce7..545d9c5fd90 100644 --- a/homeassistant/components/apple_tv/config_flow.py +++ b/homeassistant/components/apple_tv/config_flow.py @@ -235,7 +235,9 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(self.device_identifier) # but be sure to update the address if its changed so the scanner # will probe the new address - self._abort_if_unique_id_configured(updates={CONF_ADDRESS: self.atv.address}) + self._abort_if_unique_id_configured( + updates={CONF_ADDRESS: str(self.atv.address)} + ) self.context["identifier"] = self.unique_id return await self.async_step_confirm() @@ -280,15 +282,16 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ), } all_identifiers = set(self.atv.all_identifiers) + discovered_ip_address = str(self.atv.address) for entry in self._async_current_entries(): if not all_identifiers.intersection( entry.data.get(CONF_IDENTIFIERS, [entry.unique_id]) ): continue - if entry.data.get(CONF_ADDRESS) != self.atv.address: + if entry.data.get(CONF_ADDRESS) != discovered_ip_address: self.hass.config_entries.async_update_entry( entry, - data={**entry.data, CONF_ADDRESS: self.atv.address}, + data={**entry.data, CONF_ADDRESS: discovered_ip_address}, ) self.hass.async_create_task( self.hass.config_entries.async_reload(entry.entry_id) diff --git a/tests/components/apple_tv/test_config_flow.py b/tests/components/apple_tv/test_config_flow.py index 1d2a5a459d4..e9f352041cb 100644 --- a/tests/components/apple_tv/test_config_flow.py +++ b/tests/components/apple_tv/test_config_flow.py @@ -1,5 +1,6 @@ """Test config flow.""" +from ipaddress import IPv4Address from unittest.mock import ANY, patch from pyatv import exceptions @@ -629,7 +630,9 @@ async def test_zeroconf_ip_change(hass, mock_scan): unrelated_entry.add_to_hass(hass) entry.add_to_hass(hass) mock_scan.result = [ - create_conf("127.0.0.1", "Device", mrp_service(), airplay_service()) + create_conf( + IPv4Address("127.0.0.1"), "Device", mrp_service(), airplay_service() + ) ] with patch( @@ -695,7 +698,9 @@ async def test_zeroconf_unexpected_error(hass, mock_scan): async def test_zeroconf_abort_if_other_in_progress(hass, mock_scan): """Test discovering unsupported zeroconf service.""" - mock_scan.result = [create_conf("127.0.0.1", "Device", airplay_service())] + mock_scan.result = [ + create_conf(IPv4Address("127.0.0.1"), "Device", airplay_service()) + ] result = await hass.config_entries.flow.async_init( DOMAIN, @@ -714,7 +719,9 @@ async def test_zeroconf_abort_if_other_in_progress(hass, mock_scan): assert result["step_id"] == "confirm" mock_scan.result = [ - create_conf("127.0.0.1", "Device", mrp_service(), airplay_service()) + create_conf( + IPv4Address("127.0.0.1"), "Device", mrp_service(), airplay_service() + ) ] result2 = await hass.config_entries.flow.async_init( @@ -737,7 +744,9 @@ async def test_zeroconf_missing_device_during_protocol_resolve( hass, mock_scan, pairing, mock_zeroconf ): """Test discovery after service been added to existing flow with missing device.""" - mock_scan.result = [create_conf("127.0.0.1", "Device", airplay_service())] + mock_scan.result = [ + create_conf(IPv4Address("127.0.0.1"), "Device", airplay_service()) + ] # Find device with AirPlay service and set up flow for it result = await hass.config_entries.flow.async_init( @@ -754,7 +763,9 @@ async def test_zeroconf_missing_device_during_protocol_resolve( ) mock_scan.result = [ - create_conf("127.0.0.1", "Device", mrp_service(), airplay_service()) + create_conf( + IPv4Address("127.0.0.1"), "Device", mrp_service(), airplay_service() + ) ] # Find the same device again, but now also with MRP service. The first flow should @@ -789,7 +800,9 @@ async def test_zeroconf_additional_protocol_resolve_failure( hass, mock_scan, pairing, mock_zeroconf ): """Test discovery with missing service.""" - mock_scan.result = [create_conf("127.0.0.1", "Device", airplay_service())] + mock_scan.result = [ + create_conf(IPv4Address("127.0.0.1"), "Device", airplay_service()) + ] # Find device with AirPlay service and set up flow for it result = await hass.config_entries.flow.async_init( @@ -806,7 +819,9 @@ async def test_zeroconf_additional_protocol_resolve_failure( ) mock_scan.result = [ - create_conf("127.0.0.1", "Device", mrp_service(), airplay_service()) + create_conf( + IPv4Address("127.0.0.1"), "Device", mrp_service(), airplay_service() + ) ] # Find the same device again, but now also with MRP service. The first flow should @@ -824,7 +839,9 @@ async def test_zeroconf_additional_protocol_resolve_failure( ), ) - mock_scan.result = [create_conf("127.0.0.1", "Device", airplay_service())] + mock_scan.result = [ + create_conf(IPv4Address("127.0.0.1"), "Device", airplay_service()) + ] # Number of services found during initial scan (1) will not match the updated count # (2), so it will trigger a re-scan to find all services. This will however fail @@ -841,7 +858,9 @@ async def test_zeroconf_pair_additionally_found_protocols( hass, mock_scan, pairing, mock_zeroconf ): """Test discovered protocols are merged to original flow.""" - mock_scan.result = [create_conf("127.0.0.1", "Device", airplay_service())] + mock_scan.result = [ + create_conf(IPv4Address("127.0.0.1"), "Device", airplay_service()) + ] # Find device with AirPlay service and set up flow for it result = await hass.config_entries.flow.async_init( @@ -860,7 +879,9 @@ async def test_zeroconf_pair_additionally_found_protocols( await hass.async_block_till_done() mock_scan.result = [ - create_conf("127.0.0.1", "Device", raop_service(), airplay_service()) + create_conf( + IPv4Address("127.0.0.1"), "Device", raop_service(), airplay_service() + ) ] # Find the same device again, but now also with RAOP service. The first flow should @@ -874,7 +895,11 @@ async def test_zeroconf_pair_additionally_found_protocols( mock_scan.result = [ create_conf( - "127.0.0.1", "Device", raop_service(), mrp_service(), airplay_service() + IPv4Address("127.0.0.1"), + "Device", + raop_service(), + mrp_service(), + airplay_service(), ) ] From cd5fe11b4433294517e975217c6a5d6150761238 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Mon, 13 Dec 2021 21:42:25 +0100 Subject: [PATCH 0380/2644] Use async_on_unload for Nut update_listener (#61589) --- homeassistant/components/nut/__init__.py | 23 ++++++++--------------- homeassistant/components/nut/const.py | 2 -- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/nut/__init__.py b/homeassistant/components/nut/__init__.py index 1a040b99f57..cb68e8d3328 100644 --- a/homeassistant/components/nut/__init__.py +++ b/homeassistant/components/nut/__init__.py @@ -29,7 +29,6 @@ from .const import ( PLATFORMS, PYNUT_DATA, PYNUT_UNIQUE_ID, - UNDO_UPDATE_LISTENER, ) _LOGGER = logging.getLogger(__name__) @@ -80,7 +79,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.debug("NUT Sensors Available: %s", status) - undo_listener = entry.add_update_listener(_async_update_listener) + entry.async_on_unload(entry.add_update_listener(_async_update_listener)) unique_id = _unique_id_from_status(status) if unique_id is None: unique_id = entry.entry_id @@ -90,7 +89,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: COORDINATOR: coordinator, PYNUT_DATA: data, PYNUT_UNIQUE_ID: unique_id, - UNDO_UPDATE_LISTENER: undo_listener, } device_registry = dr.async_get(hass) @@ -108,6 +106,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok + + async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): """Handle options update.""" await hass.config_entries.async_reload(entry.entry_id) @@ -165,18 +170,6 @@ def _unique_id_from_status(status): return "_".join(unique_id_group) -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() - - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok - - class PyNUTData: """Stores the data retrieved from NUT. diff --git a/homeassistant/components/nut/const.py b/homeassistant/components/nut/const.py index ffb21a545bb..9f6b43974b7 100644 --- a/homeassistant/components/nut/const.py +++ b/homeassistant/components/nut/const.py @@ -26,8 +26,6 @@ DOMAIN = "nut" PLATFORMS = [Platform.SENSOR] -UNDO_UPDATE_LISTENER = "undo_update_listener" - DEFAULT_NAME = "NUT UPS" DEFAULT_HOST = "localhost" DEFAULT_PORT = 3493 From 8ba07a828892929ba302c4141135cb5fa93ef06d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 13 Dec 2021 22:46:51 +0200 Subject: [PATCH 0381/2644] Add configuration_url to syncthru devices (#61508) --- homeassistant/components/syncthru/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/syncthru/__init__.py b/homeassistant/components/syncthru/__init__.py index a891bc50831..792267791eb 100644 --- a/homeassistant/components/syncthru/__init__.py +++ b/homeassistant/components/syncthru/__init__.py @@ -67,6 +67,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, + configuration_url=printer.url, connections=device_connections(printer), identifiers=device_identifiers(printer), model=printer.model(), From 69043fe6de86d06701fd0aa24914f1130ce84825 Mon Sep 17 00:00:00 2001 From: Vilppu Vuorinen Date: Mon, 13 Dec 2021 23:52:35 +0200 Subject: [PATCH 0382/2644] Update pymelcloud to 2.5.6 (#61717) --- homeassistant/components/melcloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/melcloud/manifest.json b/homeassistant/components/melcloud/manifest.json index f875984453d..355f4c9058b 100644 --- a/homeassistant/components/melcloud/manifest.json +++ b/homeassistant/components/melcloud/manifest.json @@ -3,7 +3,7 @@ "name": "MELCloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/melcloud", - "requirements": ["pymelcloud==2.5.5"], + "requirements": ["pymelcloud==2.5.6"], "codeowners": ["@vilppuvuorinen"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 9747dba5700..88ebb0f1a2a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1640,7 +1640,7 @@ pymazda==0.2.2 pymediaroom==0.6.4.1 # homeassistant.components.melcloud -pymelcloud==2.5.5 +pymelcloud==2.5.6 # homeassistant.components.meteoclimatic pymeteoclimatic==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ddd6dc0849a..a1ece5222d4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1004,7 +1004,7 @@ pymata-express==1.19 pymazda==0.2.2 # homeassistant.components.melcloud -pymelcloud==2.5.5 +pymelcloud==2.5.6 # homeassistant.components.meteoclimatic pymeteoclimatic==0.0.6 From 82e280d2ac0a5b2529e630b7ee1263ee1cea7b27 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 13 Dec 2021 17:04:50 -0500 Subject: [PATCH 0383/2644] Remove deprecated yaml config from flume (#61517) --- homeassistant/components/flume/config_flow.py | 4 -- homeassistant/components/flume/sensor.py | 36 +----------------- tests/components/flume/test_config_flow.py | 38 ------------------- 3 files changed, 2 insertions(+), 76 deletions(-) diff --git a/homeassistant/components/flume/config_flow.py b/homeassistant/components/flume/config_flow.py index 1bab8817dbb..6d9554f42c0 100644 --- a/homeassistant/components/flume/config_flow.py +++ b/homeassistant/components/flume/config_flow.py @@ -103,10 +103,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - async def async_step_import(self, user_input): - """Handle import.""" - return await self.async_step_user(user_input) - async def async_step_reauth(self, user_input=None): """Handle reauth.""" self._reauth_unique_id = self.context["unique_id"] diff --git a/homeassistant/components/flume/sensor.py b/homeassistant/components/flume/sensor.py index 2ff7712cfd5..3f7b0f671fe 100644 --- a/homeassistant/components/flume/sensor.py +++ b/homeassistant/components/flume/sensor.py @@ -4,22 +4,9 @@ import logging from numbers import Number from pyflume import FlumeData -import voluptuous as vol -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, - SensorEntity, - SensorEntityDescription, -) -from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import ( - CONF_CLIENT_ID, - CONF_CLIENT_SECRET, - CONF_NAME, - CONF_PASSWORD, - CONF_USERNAME, -) -import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.const import CONF_NAME from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -47,25 +34,6 @@ _LOGGER = logging.getLogger(__name__) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=15) SCAN_INTERVAL = timedelta(minutes=1) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_CLIENT_SECRET): cv.string, - vol.Optional(CONF_NAME): cv.string, - } -) - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Import the platform into a config entry.""" - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config - ) - ) - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Flume sensor.""" diff --git a/tests/components/flume/test_config_flow.py b/tests/components/flume/test_config_flow.py index 70ee359b7b4..4ba7becc147 100644 --- a/tests/components/flume/test_config_flow.py +++ b/tests/components/flume/test_config_flow.py @@ -64,44 +64,6 @@ async def test_form(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_import(hass): - """Test we can import the sensor platform config.""" - - mock_flume_device_list = _get_mocked_flume_device_list() - - with patch( - "homeassistant.components.flume.config_flow.FlumeAuth", - return_value=True, - ), patch( - "homeassistant.components.flume.config_flow.FlumeDeviceList", - return_value=mock_flume_device_list, - ), patch( - "homeassistant.components.flume.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", - CONF_CLIENT_ID: "client_id", - CONF_CLIENT_SECRET: "client_secret", - }, - ) - await hass.async_block_till_done() - - assert result["type"] == "create_entry" - assert result["title"] == "test-username" - assert result["data"] == { - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", - CONF_CLIENT_ID: "client_id", - CONF_CLIENT_SECRET: "client_secret", - } - assert len(mock_setup_entry.mock_calls) == 1 - - async def test_form_invalid_auth(hass): """Test we handle invalid auth.""" result = await hass.config_entries.flow.async_init( From 905295707d7ef014eec630275429ab4b57ae5dfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Mon, 13 Dec 2021 23:13:04 +0100 Subject: [PATCH 0384/2644] Add support for app launching in Apple TV (#61732) --- .../components/apple_tv/browse_media.py | 44 +++++++++++++++++ .../components/apple_tv/media_player.py | 48 ++++++++++++++++++- 2 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/apple_tv/browse_media.py diff --git a/homeassistant/components/apple_tv/browse_media.py b/homeassistant/components/apple_tv/browse_media.py new file mode 100644 index 00000000000..3c0eee8b6ad --- /dev/null +++ b/homeassistant/components/apple_tv/browse_media.py @@ -0,0 +1,44 @@ +"""Support for media browsing.""" + +from homeassistant.components.media_player import BrowseMedia +from homeassistant.components.media_player.const import ( + MEDIA_CLASS_APP, + MEDIA_CLASS_DIRECTORY, + MEDIA_TYPE_APP, + MEDIA_TYPE_APPS, +) + + +def build_app_list(app_list): + """Create response payload for app list.""" + app_list = [ + {"app_id": app_id, "title": app_name, "type": MEDIA_TYPE_APP} + for app_name, app_id in app_list.items() + ] + + return BrowseMedia( + media_class=MEDIA_CLASS_DIRECTORY, + media_content_id=None, + media_content_type=MEDIA_TYPE_APPS, + title="Apps", + can_play=True, + can_expand=False, + children=[item_payload(item) for item in app_list], + children_media_class=MEDIA_CLASS_APP, + ) + + +def item_payload(item): + """ + Create response payload for a single media item. + + Used by async_browse_media. + """ + return BrowseMedia( + title=item["title"], + media_class=MEDIA_CLASS_APP, + media_content_type=MEDIA_TYPE_APP, + media_content_id=item["app_id"], + can_play=False, + can_expand=False, + ) diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py index bc4697e0d5e..77c97a1b54b 100644 --- a/homeassistant/components/apple_tv/media_player.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -1,6 +1,7 @@ """Support for Apple TV media player.""" import logging +from pyatv import exceptions from pyatv.const import ( DeviceState, FeatureName, @@ -12,14 +13,16 @@ from pyatv.const import ( ) from pyatv.helpers import is_streamable -from homeassistant.components.media_player import MediaPlayerEntity +from homeassistant.components.media_player import BrowseMedia, MediaPlayerEntity from homeassistant.components.media_player.const import ( + MEDIA_TYPE_APP, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, REPEAT_MODE_ALL, REPEAT_MODE_OFF, REPEAT_MODE_ONE, + SUPPORT_BROWSE_MEDIA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, @@ -27,6 +30,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_PREVIOUS_TRACK, SUPPORT_REPEAT_SET, SUPPORT_SEEK, + SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, SUPPORT_STOP, SUPPORT_TURN_OFF, @@ -46,6 +50,7 @@ from homeassistant.core import callback import homeassistant.util.dt as dt_util from . import AppleTVEntity +from .browse_media import build_app_list from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -60,6 +65,7 @@ SUPPORT_BASE = SUPPORT_TURN_ON | SUPPORT_TURN_OFF # of these). SUPPORT_APPLE_TV = ( SUPPORT_BASE + | SUPPORT_BROWSE_MEDIA | SUPPORT_PLAY_MEDIA | SUPPORT_PAUSE | SUPPORT_PLAY @@ -89,6 +95,8 @@ SUPPORT_FEATURE_MAPPING = { FeatureName.SetRepeat: SUPPORT_REPEAT_SET, FeatureName.SetShuffle: SUPPORT_SHUFFLE_SET, FeatureName.SetVolume: SUPPORT_VOLUME_SET, + FeatureName.AppList: SUPPORT_BROWSE_MEDIA | SUPPORT_SELECT_SOURCE, + FeatureName.LaunchApp: SUPPORT_BROWSE_MEDIA | SUPPORT_SELECT_SOURCE, } @@ -108,6 +116,7 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): """Initialize the Apple TV media player.""" super().__init__(name, identifier, manager, **kwargs) self._playing = None + self._app_list = {} @callback def async_device_connected(self, atv): @@ -135,6 +144,21 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): # Listen to power updates self.atv.power.listener = self + if self.atv.features.in_state(FeatureState.Available, FeatureName.AppList): + self.hass.create_task(self._update_app_list()) + + async def _update_app_list(self): + _LOGGER.debug("Updating app list") + try: + apps = await self.atv.apps.app_list() + except exceptions.NotSupportedError: + _LOGGER.error("Listing apps is not supported") + except exceptions.ProtocolError: + _LOGGER.exception("Failed to update app list") + else: + self._app_list = {app.name: app.identifier for app in apps} + self.async_write_ha_state() + @callback def async_device_disconnected(self): """Handle when connection was lost to device.""" @@ -198,6 +222,11 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): return self.atv.metadata.app.name return None + @property + def source_list(self): + """List of available input sources.""" + return list(self._app_list.keys()) + @property def media_content_type(self): """Content type of current playing media.""" @@ -248,7 +277,9 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): """Send the play_media command to the media player.""" # If input (file) has a file format supported by pyatv, then stream it with # RAOP. Otherwise try to play it with regular AirPlay. - if self._is_feature_available(FeatureName.StreamFile) and ( + if media_type == MEDIA_TYPE_APP: + await self.atv.apps.launch_app(media_id) + elif self._is_feature_available(FeatureName.StreamFile) and ( await is_streamable(media_id) or media_type == MEDIA_TYPE_MUSIC ): _LOGGER.debug("Streaming %s via RAOP", media_id) @@ -346,6 +377,14 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): return self.atv.features.in_state(FeatureState.Available, feature) return False + async def async_browse_media( + self, + media_content_type=None, + media_content_id=None, + ) -> BrowseMedia: + """Implement the websocket media browsing helper.""" + return build_app_list(self._app_list) + async def async_turn_on(self): """Turn the media player on.""" if self._is_feature_available(FeatureName.TurnOn): @@ -425,3 +464,8 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): await self.atv.remote_control.set_shuffle( ShuffleState.Songs if shuffle else ShuffleState.Off ) + + async def async_select_source(self, source: str) -> None: + """Select input source.""" + if app_id := self._app_list.get(source): + await self.atv.apps.launch_app(app_id) From 85607970cf6c7672396050cfa059c844ea33da74 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 13 Dec 2021 23:16:13 +0100 Subject: [PATCH 0385/2644] Use attr* in garages_amsterdam (#61605) Co-authored-by: epenet --- .../garages_amsterdam/binary_sensor.py | 32 ++++------------- .../components/garages_amsterdam/sensor.py | 36 ++++--------------- 2 files changed, 12 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/garages_amsterdam/binary_sensor.py b/homeassistant/components/garages_amsterdam/binary_sensor.py index cb2ba8906bc..5b444146624 100644 --- a/homeassistant/components/garages_amsterdam/binary_sensor.py +++ b/homeassistant/components/garages_amsterdam/binary_sensor.py @@ -1,14 +1,11 @@ """Binary Sensor platform for Garages Amsterdam.""" from __future__ import annotations -from typing import Any - from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_PROBLEM, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( @@ -43,25 +40,18 @@ async def async_setup_entry( class GaragesamsterdamBinarySensor(CoordinatorEntity, BinarySensorEntity): """Binary Sensor representing garages amsterdam data.""" + _attr_attribution = ATTRIBUTION + _attr_device_class = BinarySensorDeviceClass.PROBLEM + def __init__( self, coordinator: DataUpdateCoordinator, garage_name: str, info_type: str ) -> None: """Initialize garages amsterdam binary sensor.""" super().__init__(coordinator) - self._unique_id = f"{garage_name}-{info_type}" + self._attr_unique_id = f"{garage_name}-{info_type}" self._garage_name = garage_name self._info_type = info_type - self._name = garage_name - - @property - def name(self) -> str: - """Return the name of the sensor.""" - return self._name - - @property - def unique_id(self) -> str: - """Return the unique id of the device.""" - return self._unique_id + self._attr_name = garage_name @property def is_on(self) -> bool: @@ -69,13 +59,3 @@ class GaragesamsterdamBinarySensor(CoordinatorEntity, BinarySensorEntity): return ( getattr(self.coordinator.data[self._garage_name], self._info_type) != "ok" ) - - @property - def device_class(self) -> str: - """Return the class of the binary sensor.""" - return DEVICE_CLASS_PROBLEM - - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return device attributes.""" - return {ATTR_ATTRIBUTION: ATTRIBUTION} diff --git a/homeassistant/components/garages_amsterdam/sensor.py b/homeassistant/components/garages_amsterdam/sensor.py index da3a7a4dc24..252f010dfdb 100644 --- a/homeassistant/components/garages_amsterdam/sensor.py +++ b/homeassistant/components/garages_amsterdam/sensor.py @@ -1,11 +1,8 @@ """Sensor platform for Garages Amsterdam.""" from __future__ import annotations -from typing import Any - from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( @@ -48,25 +45,19 @@ async def async_setup_entry( class GaragesamsterdamSensor(CoordinatorEntity, SensorEntity): """Sensor representing garages amsterdam data.""" + _attr_attribution = ATTRIBUTION + _attr_native_unit_of_measurement = "cars" + def __init__( self, coordinator: DataUpdateCoordinator, garage_name: str, info_type: str ) -> None: """Initialize garages amsterdam sensor.""" super().__init__(coordinator) - self._unique_id = f"{garage_name}-{info_type}" + self._attr_unique_id = f"{garage_name}-{info_type}" self._garage_name = garage_name self._info_type = info_type - self._name = f"{garage_name} - {info_type}".replace("_", " ") - - @property - def name(self) -> str: - """Return the name of the sensor.""" - return self._name - - @property - def unique_id(self) -> str: - """Return the unique id of the device.""" - return self._unique_id + self._attr_name = f"{garage_name} - {info_type}".replace("_", " ") + self._attr_icon = SENSORS[info_type] @property def available(self) -> bool: @@ -79,18 +70,3 @@ class GaragesamsterdamSensor(CoordinatorEntity, SensorEntity): def native_value(self) -> str: """Return the state of the sensor.""" return getattr(self.coordinator.data[self._garage_name], self._info_type) - - @property - def icon(self) -> str: - """Return the icon.""" - return SENSORS[self._info_type] - - @property - def native_unit_of_measurement(self) -> str: - """Return unit of measurement.""" - return "cars" - - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return device attributes.""" - return {ATTR_ATTRIBUTION: ATTRIBUTION} From 6157dfe68b19a41a1ff39d657dd8cf08ff857b9d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 13 Dec 2021 23:16:54 +0100 Subject: [PATCH 0386/2644] Use new HumidifierDeviceClass enum in generic_hygrostat (#61607) Co-authored-by: epenet --- .../components/generic_hygrostat/__init__.py | 7 ++---- .../generic_hygrostat/humidifier.py | 24 ++++++++++++------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/generic_hygrostat/__init__.py b/homeassistant/components/generic_hygrostat/__init__.py index b58c98b1d0d..20877f63369 100644 --- a/homeassistant/components/generic_hygrostat/__init__.py +++ b/homeassistant/components/generic_hygrostat/__init__.py @@ -2,10 +2,7 @@ import voluptuous as vol -from homeassistant.components.humidifier.const import ( - DEVICE_CLASS_DEHUMIDIFIER, - DEVICE_CLASS_HUMIDIFIER, -) +from homeassistant.components.humidifier import HumidifierDeviceClass from homeassistant.const import CONF_NAME from homeassistant.helpers import config_validation as cv, discovery @@ -34,7 +31,7 @@ HYGROSTAT_SCHEMA = vol.Schema( vol.Required(CONF_HUMIDIFIER): cv.entity_id, vol.Required(CONF_SENSOR): cv.entity_id, vol.Optional(CONF_DEVICE_CLASS): vol.In( - [DEVICE_CLASS_HUMIDIFIER, DEVICE_CLASS_DEHUMIDIFIER] + [HumidifierDeviceClass.HUMIDIFIER, HumidifierDeviceClass.DEHUMIDIFIER] ), vol.Optional(CONF_MAX_HUMIDITY): vol.Coerce(int), vol.Optional(CONF_MIN_DUR): vol.All(cv.time_period, cv.positive_timedelta), diff --git a/homeassistant/components/generic_hygrostat/humidifier.py b/homeassistant/components/generic_hygrostat/humidifier.py index 383674a7f75..7a92eb79d51 100644 --- a/homeassistant/components/generic_hygrostat/humidifier.py +++ b/homeassistant/components/generic_hygrostat/humidifier.py @@ -2,11 +2,13 @@ import asyncio import logging -from homeassistant.components.humidifier import PLATFORM_SCHEMA, HumidifierEntity +from homeassistant.components.humidifier import ( + PLATFORM_SCHEMA, + HumidifierDeviceClass, + HumidifierEntity, +) from homeassistant.components.humidifier.const import ( ATTR_HUMIDITY, - DEVICE_CLASS_DEHUMIDIFIER, - DEVICE_CLASS_HUMIDIFIER, MODE_AWAY, MODE_NORMAL, SUPPORT_MODES, @@ -146,7 +148,7 @@ class GenericHygrostat(HumidifierEntity, RestoreEntity): self._remove_stale_tracking = None self._is_away = False if not self._device_class: - self._device_class = DEVICE_CLASS_HUMIDIFIER + self._device_class = HumidifierDeviceClass.HUMIDIFIER async def async_added_to_hass(self): """Run when entity about to be added.""" @@ -185,7 +187,7 @@ class GenericHygrostat(HumidifierEntity, RestoreEntity): if old_state.state: self._state = old_state.state == STATE_ON if self._target_humidity is None: - if self._device_class == DEVICE_CLASS_HUMIDIFIER: + if self._device_class == HumidifierDeviceClass.HUMIDIFIER: self._target_humidity = self.min_humidity else: self._target_humidity = self.max_humidity @@ -396,8 +398,10 @@ class GenericHygrostat(HumidifierEntity, RestoreEntity): too_dry = self._target_humidity - self._cur_humidity >= dry_tolerance too_wet = self._cur_humidity - self._target_humidity >= wet_tolerance if self._is_device_active: - if (self._device_class == DEVICE_CLASS_HUMIDIFIER and too_wet) or ( - self._device_class == DEVICE_CLASS_DEHUMIDIFIER and too_dry + if ( + self._device_class == HumidifierDeviceClass.HUMIDIFIER and too_wet + ) or ( + self._device_class == HumidifierDeviceClass.DEHUMIDIFIER and too_dry ): _LOGGER.info("Turning off humidifier %s", self._switch_entity_id) await self._async_device_turn_off() @@ -405,8 +409,10 @@ class GenericHygrostat(HumidifierEntity, RestoreEntity): # The time argument is passed only in keep-alive case await self._async_device_turn_on() else: - if (self._device_class == DEVICE_CLASS_HUMIDIFIER and too_dry) or ( - self._device_class == DEVICE_CLASS_DEHUMIDIFIER and too_wet + if ( + self._device_class == HumidifierDeviceClass.HUMIDIFIER and too_dry + ) or ( + self._device_class == HumidifierDeviceClass.DEHUMIDIFIER and too_wet ): _LOGGER.info("Turning on humidifier %s", self._switch_entity_id) await self._async_device_turn_on() From 3e0e2978e68c1f8ce63f55069c47ce0a19444f2b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 13 Dec 2021 23:19:32 +0100 Subject: [PATCH 0387/2644] Use new DeviceClass enums in google-assistant (#61611) Co-authored-by: epenet --- .../components/google_assistant/const.py | 44 +++++++++++-------- .../components/google_assistant/trait.py | 42 ++++++++++-------- 2 files changed, 48 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index 269c0aafea1..37154bf5e4d 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -142,25 +142,31 @@ DOMAIN_TO_GOOGLE_TYPES = { } DEVICE_CLASS_TO_GOOGLE_TYPES = { - (cover.DOMAIN, cover.DEVICE_CLASS_GARAGE): TYPE_GARAGE, - (cover.DOMAIN, cover.DEVICE_CLASS_GATE): TYPE_GARAGE, - (cover.DOMAIN, cover.DEVICE_CLASS_DOOR): TYPE_DOOR, - (cover.DOMAIN, cover.DEVICE_CLASS_AWNING): TYPE_AWNING, - (cover.DOMAIN, cover.DEVICE_CLASS_SHUTTER): TYPE_SHUTTER, - (switch.DOMAIN, switch.DEVICE_CLASS_SWITCH): TYPE_SWITCH, - (switch.DOMAIN, switch.DEVICE_CLASS_OUTLET): TYPE_OUTLET, - (binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_DOOR): TYPE_DOOR, - (binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_GARAGE_DOOR): TYPE_GARAGE, - (binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_LOCK): TYPE_SENSOR, - (binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_OPENING): TYPE_SENSOR, - (binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_WINDOW): TYPE_SENSOR, - (media_player.DOMAIN, media_player.DEVICE_CLASS_TV): TYPE_TV, - (media_player.DOMAIN, media_player.DEVICE_CLASS_SPEAKER): TYPE_SPEAKER, - (media_player.DOMAIN, media_player.DEVICE_CLASS_RECEIVER): TYPE_RECEIVER, - (sensor.DOMAIN, sensor.DEVICE_CLASS_TEMPERATURE): TYPE_SENSOR, - (sensor.DOMAIN, sensor.DEVICE_CLASS_HUMIDITY): TYPE_SENSOR, - (humidifier.DOMAIN, humidifier.DEVICE_CLASS_HUMIDIFIER): TYPE_HUMIDIFIER, - (humidifier.DOMAIN, humidifier.DEVICE_CLASS_DEHUMIDIFIER): TYPE_DEHUMIDIFIER, + (cover.DOMAIN, cover.CoverDeviceClass.GARAGE): TYPE_GARAGE, + (cover.DOMAIN, cover.CoverDeviceClass.GATE): TYPE_GARAGE, + (cover.DOMAIN, cover.CoverDeviceClass.DOOR): TYPE_DOOR, + (cover.DOMAIN, cover.CoverDeviceClass.AWNING): TYPE_AWNING, + (cover.DOMAIN, cover.CoverDeviceClass.SHUTTER): TYPE_SHUTTER, + (switch.DOMAIN, switch.SwitchDeviceClass.SWITCH): TYPE_SWITCH, + (switch.DOMAIN, switch.SwitchDeviceClass.OUTLET): TYPE_OUTLET, + (binary_sensor.DOMAIN, binary_sensor.BinarySensorDeviceClass.DOOR): TYPE_DOOR, + ( + binary_sensor.DOMAIN, + binary_sensor.BinarySensorDeviceClass.GARAGE_DOOR, + ): TYPE_GARAGE, + (binary_sensor.DOMAIN, binary_sensor.BinarySensorDeviceClass.LOCK): TYPE_SENSOR, + (binary_sensor.DOMAIN, binary_sensor.BinarySensorDeviceClass.OPENING): TYPE_SENSOR, + (binary_sensor.DOMAIN, binary_sensor.BinarySensorDeviceClass.WINDOW): TYPE_SENSOR, + (media_player.DOMAIN, media_player.MediaPlayerDeviceClass.TV): TYPE_TV, + (media_player.DOMAIN, media_player.MediaPlayerDeviceClass.SPEAKER): TYPE_SPEAKER, + (media_player.DOMAIN, media_player.MediaPlayerDeviceClass.RECEIVER): TYPE_RECEIVER, + (sensor.DOMAIN, sensor.SensorDeviceClass.TEMPERATURE): TYPE_SENSOR, + (sensor.DOMAIN, sensor.SensorDeviceClass.HUMIDITY): TYPE_SENSOR, + (humidifier.DOMAIN, humidifier.HumidifierDeviceClass.HUMIDIFIER): TYPE_HUMIDIFIER, + ( + humidifier.DOMAIN, + humidifier.HumidifierDeviceClass.DEHUMIDIFIER, + ): TYPE_DEHUMIDIFIER, } CHALLENGE_ACK_NEEDED = "ackNeeded" diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 30ea244bac9..4b6593abadb 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -807,7 +807,8 @@ class TemperatureControlTrait(_Trait): def supported(domain, features, device_class, _): """Test if state is supported.""" return ( - domain == sensor.DOMAIN and device_class == sensor.DEVICE_CLASS_TEMPERATURE + domain == sensor.DOMAIN + and device_class == sensor.SensorDeviceClass.TEMPERATURE ) def sync_attributes(self): @@ -1111,7 +1112,10 @@ class HumiditySettingTrait(_Trait): if domain == humidifier.DOMAIN: return True - return domain == sensor.DOMAIN and device_class == sensor.DEVICE_CLASS_HUMIDITY + return ( + domain == sensor.DOMAIN + and device_class == sensor.SensorDeviceClass.HUMIDITY + ) def sync_attributes(self): """Return humidity attributes for a sync request.""" @@ -1121,7 +1125,7 @@ class HumiditySettingTrait(_Trait): if domain == sensor.DOMAIN: device_class = attrs.get(ATTR_DEVICE_CLASS) - if device_class == sensor.DEVICE_CLASS_HUMIDITY: + if device_class == sensor.SensorDeviceClass.HUMIDITY: response["queryOnlyHumiditySetting"] = True elif domain == humidifier.DOMAIN: @@ -1144,7 +1148,7 @@ class HumiditySettingTrait(_Trait): if domain == sensor.DOMAIN: device_class = attrs.get(ATTR_DEVICE_CLASS) - if device_class == sensor.DEVICE_CLASS_HUMIDITY: + if device_class == sensor.SensorDeviceClass.HUMIDITY: current_humidity = self.state.state if current_humidity not in (STATE_UNKNOWN, STATE_UNAVAILABLE): response["humidityAmbientPercent"] = round(float(current_humidity)) @@ -1759,9 +1763,9 @@ class OpenCloseTrait(_Trait): # Cover device classes that require 2FA COVER_2FA = ( - cover.DEVICE_CLASS_DOOR, - cover.DEVICE_CLASS_GARAGE, - cover.DEVICE_CLASS_GATE, + cover.CoverDeviceClass.DOOR, + cover.CoverDeviceClass.GARAGE, + cover.CoverDeviceClass.GATE, ) name = TRAIT_OPENCLOSE @@ -1774,11 +1778,11 @@ class OpenCloseTrait(_Trait): return True return domain == binary_sensor.DOMAIN and device_class in ( - binary_sensor.DEVICE_CLASS_DOOR, - binary_sensor.DEVICE_CLASS_GARAGE_DOOR, - binary_sensor.DEVICE_CLASS_LOCK, - binary_sensor.DEVICE_CLASS_OPENING, - binary_sensor.DEVICE_CLASS_WINDOW, + binary_sensor.BinarySensorDeviceClass.DOOR, + binary_sensor.BinarySensorDeviceClass.GARAGE_DOOR, + binary_sensor.BinarySensorDeviceClass.LOCK, + binary_sensor.BinarySensorDeviceClass.OPENING, + binary_sensor.BinarySensorDeviceClass.WINDOW, ) @staticmethod @@ -2245,7 +2249,7 @@ class ChannelTrait(_Trait): if ( domain == media_player.DOMAIN and (features & media_player.SUPPORT_PLAY_MEDIA) - and device_class == media_player.DEVICE_CLASS_TV + and device_class == media_player.MediaPlayerDeviceClass.TV ): return True @@ -2293,12 +2297,12 @@ class SensorStateTrait(_Trait): """ sensor_types = { - sensor.DEVICE_CLASS_AQI: ("AirQuality", "AQI"), - sensor.DEVICE_CLASS_CO: ("CarbonDioxideLevel", "PARTS_PER_MILLION"), - sensor.DEVICE_CLASS_CO2: ("CarbonMonoxideLevel", "PARTS_PER_MILLION"), - sensor.DEVICE_CLASS_PM25: ("PM2.5", "MICROGRAMS_PER_CUBIC_METER"), - sensor.DEVICE_CLASS_PM10: ("PM10", "MICROGRAMS_PER_CUBIC_METER"), - sensor.DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS: ( + sensor.SensorDeviceClass.AQI: ("AirQuality", "AQI"), + sensor.SensorDeviceClass.CO: ("CarbonDioxideLevel", "PARTS_PER_MILLION"), + sensor.SensorDeviceClass.CO2: ("CarbonMonoxideLevel", "PARTS_PER_MILLION"), + sensor.SensorDeviceClass.PM25: ("PM2.5", "MICROGRAMS_PER_CUBIC_METER"), + sensor.SensorDeviceClass.PM10: ("PM10", "MICROGRAMS_PER_CUBIC_METER"), + sensor.SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: ( "VolatileOrganicCompounds", "PARTS_PER_MILLION", ), From 9aa38201cd4b1e89f8f14a822d609a4950731584 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 14 Dec 2021 00:12:00 +0100 Subject: [PATCH 0388/2644] Upgrade vehicle to 0.3.0 (#61738) --- homeassistant/components/rdw/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rdw/manifest.json b/homeassistant/components/rdw/manifest.json index c4f80f812ca..757e54e97c7 100644 --- a/homeassistant/components/rdw/manifest.json +++ b/homeassistant/components/rdw/manifest.json @@ -3,7 +3,7 @@ "name": "RDW", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rdw", - "requirements": ["vehicle==0.2.2"], + "requirements": ["vehicle==0.3.0"], "codeowners": ["@frenck"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 88ebb0f1a2a..1c9befe49a8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2387,7 +2387,7 @@ uvcclient==0.11.0 vallox-websocket-api==2.8.1 # homeassistant.components.rdw -vehicle==0.2.2 +vehicle==0.3.0 # homeassistant.components.velbus velbus-aio==2021.11.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a1ece5222d4..ebad4e5dc1b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1416,7 +1416,7 @@ url-normalize==1.4.1 uvcclient==0.11.0 # homeassistant.components.rdw -vehicle==0.2.2 +vehicle==0.3.0 # homeassistant.components.velbus velbus-aio==2021.11.7 From b1a3ba2025e97f289e37a9c35fb4ed09afb345ac Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 14 Dec 2021 00:13:13 +0100 Subject: [PATCH 0389/2644] Upgrade sentry-sdk to 1.5.1 (#61735) --- homeassistant/components/sentry/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json index 9d8575dbc4e..6fa0e4112a2 100644 --- a/homeassistant/components/sentry/manifest.json +++ b/homeassistant/components/sentry/manifest.json @@ -3,7 +3,7 @@ "name": "Sentry", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sentry", - "requirements": ["sentry-sdk==1.5.0"], + "requirements": ["sentry-sdk==1.5.1"], "codeowners": ["@dcramer", "@frenck"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 1c9befe49a8..b005127387c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2140,7 +2140,7 @@ sense-hat==2.2.0 sense_energy==0.9.3 # homeassistant.components.sentry -sentry-sdk==1.5.0 +sentry-sdk==1.5.1 # homeassistant.components.sharkiq sharkiqpy==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ebad4e5dc1b..2c635964c27 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1276,7 +1276,7 @@ screenlogicpy==0.5.3 sense_energy==0.9.3 # homeassistant.components.sentry -sentry-sdk==1.5.0 +sentry-sdk==1.5.1 # homeassistant.components.sharkiq sharkiqpy==0.1.8 From 3b80cbc495a96053f7590871bb41439175e4d853 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 14 Dec 2021 00:17:08 +0100 Subject: [PATCH 0390/2644] Use _attr_* in danfoss_air (#61341) Co-authored-by: epenet --- .../components/danfoss_air/binary_sensor.py | 30 +++------ .../components/danfoss_air/sensor.py | 66 +++++++------------ 2 files changed, 31 insertions(+), 65 deletions(-) diff --git a/homeassistant/components/danfoss_air/binary_sensor.py b/homeassistant/components/danfoss_air/binary_sensor.py index 9d3123185c4..379d76ec4c8 100644 --- a/homeassistant/components/danfoss_air/binary_sensor.py +++ b/homeassistant/components/danfoss_air/binary_sensor.py @@ -2,7 +2,7 @@ from pydanfossair.commands import ReadCommand from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_OPENING, + BinarySensorDeviceClass, BinarySensorEntity, ) @@ -14,7 +14,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): data = hass.data[DANFOSS_AIR_DOMAIN] sensors = [ - ["Danfoss Air Bypass Active", ReadCommand.bypass, DEVICE_CLASS_OPENING], + [ + "Danfoss Air Bypass Active", + ReadCommand.bypass, + BinarySensorDeviceClass.OPENING, + ], ["Danfoss Air Away Mode Active", ReadCommand.away_mode, None], ] @@ -32,28 +36,12 @@ class DanfossAirBinarySensor(BinarySensorEntity): def __init__(self, data, name, sensor_type, device_class): """Initialize the Danfoss Air binary sensor.""" self._data = data - self._name = name - self._state = None + self._attr_name = name self._type = sensor_type - self._device_class = device_class - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def is_on(self): - """Return the state of the sensor.""" - return self._state - - @property - def device_class(self): - """Type of device class.""" - return self._device_class + self._attr_device_class = device_class def update(self): """Fetch new state data for the sensor.""" self._data.update() - self._state = self._data.get_value(self._type) + self._attr_is_on = self._data.get_value(self._type) diff --git a/homeassistant/components/danfoss_air/sensor.py b/homeassistant/components/danfoss_air/sensor.py index 264e69739af..098032478aa 100644 --- a/homeassistant/components/danfoss_air/sensor.py +++ b/homeassistant/components/danfoss_air/sensor.py @@ -3,14 +3,12 @@ import logging from pydanfossair.commands import ReadCommand -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity -from homeassistant.const import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_TEMPERATURE, - PERCENTAGE, - TEMP_CELSIUS, +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorStateClass, ) +from homeassistant.const import PERCENTAGE, TEMP_CELSIUS from . import DOMAIN as DANFOSS_AIR_DOMAIN @@ -26,29 +24,29 @@ def setup_platform(hass, config, add_entities, discovery_info=None): "Danfoss Air Exhaust Temperature", TEMP_CELSIUS, ReadCommand.exhaustTemperature, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + SensorDeviceClass.TEMPERATURE, + SensorStateClass.MEASUREMENT, ], [ "Danfoss Air Outdoor Temperature", TEMP_CELSIUS, ReadCommand.outdoorTemperature, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + SensorDeviceClass.TEMPERATURE, + SensorStateClass.MEASUREMENT, ], [ "Danfoss Air Supply Temperature", TEMP_CELSIUS, ReadCommand.supplyTemperature, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + SensorDeviceClass.TEMPERATURE, + SensorStateClass.MEASUREMENT, ], [ "Danfoss Air Extract Temperature", TEMP_CELSIUS, ReadCommand.extractTemperature, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + SensorDeviceClass.TEMPERATURE, + SensorStateClass.MEASUREMENT, ], [ "Danfoss Air Remaining Filter", @@ -61,8 +59,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): "Danfoss Air Humidity", PERCENTAGE, ReadCommand.humidity, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + SensorDeviceClass.HUMIDITY, + SensorStateClass.MEASUREMENT, ], ["Danfoss Air Fan Step", PERCENTAGE, ReadCommand.fan_step, None, None], [ @@ -83,7 +81,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): "Danfoss Air Dial Battery", PERCENTAGE, ReadCommand.battery_percent, - DEVICE_CLASS_BATTERY, + SensorDeviceClass.BATTERY, None, ], ] @@ -104,33 +102,13 @@ class DanfossAir(SensorEntity): def __init__(self, data, name, sensor_unit, sensor_type, device_class, state_class): """Initialize the sensor.""" self._data = data - self._name = name - self._state = None + self._attr_name = name + self._attr_native_value = None self._type = sensor_type - self._unit = sensor_unit - self._device_class = device_class + self._attr_native_unit_of_measurement = sensor_unit + self._attr_device_class = device_class self._attr_state_class = state_class - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def device_class(self): - """Return the device class of the sensor.""" - return self._device_class - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state - - @property - def native_unit_of_measurement(self): - """Return the unit of measurement.""" - return self._unit - def update(self): """Update the new state of the sensor. @@ -139,6 +117,6 @@ class DanfossAir(SensorEntity): """ self._data.update() - self._state = self._data.get_value(self._type) - if self._state is None: + self._attr_native_value = self._data.get_value(self._type) + if self._attr_native_value is None: _LOGGER.debug("Could not get data for %s", self._type) From 933d624a4e5e5622e0d2bb42f06e6034db683671 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 14 Dec 2021 00:17:39 +0100 Subject: [PATCH 0391/2644] Use _attr_* in ecoal_boiler (#61363) Co-authored-by: epenet --- .../components/ecoal_boiler/sensor.py | 32 ++++--------------- .../components/ecoal_boiler/switch.py | 16 ++-------- 2 files changed, 9 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/ecoal_boiler/sensor.py b/homeassistant/components/ecoal_boiler/sensor.py index d9689631280..8ed5129b628 100644 --- a/homeassistant/components/ecoal_boiler/sensor.py +++ b/homeassistant/components/ecoal_boiler/sensor.py @@ -1,6 +1,6 @@ """Allows reading temperatures from ecoal/esterownik.pl controller.""" -from homeassistant.components.sensor import SensorEntity -from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.const import TEMP_CELSIUS from . import AVAILABLE_SENSORS, DATA_ECOAL_BOILER @@ -20,32 +20,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class EcoalTempSensor(SensorEntity): """Representation of a temperature sensor using ecoal status data.""" + _attr_device_class = SensorDeviceClass.TEMPERATURE + _attr_native_unit_of_measurement = TEMP_CELSIUS + def __init__(self, ecoal_contr, name, status_attr): """Initialize the sensor.""" self._ecoal_contr = ecoal_contr - self._name = name + self._attr_name = name self._status_attr = status_attr - self._state = None - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state - - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return DEVICE_CLASS_TEMPERATURE - - @property - def native_unit_of_measurement(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS def update(self): """Fetch new state data for the sensor. @@ -54,4 +36,4 @@ class EcoalTempSensor(SensorEntity): """ # Old values read 0.5 back can still be used status = self._ecoal_contr.get_cached_status() - self._state = getattr(status, self._status_attr) + self._attr_native_value = getattr(status, self._status_attr) diff --git a/homeassistant/components/ecoal_boiler/switch.py b/homeassistant/components/ecoal_boiler/switch.py index 995a49554e6..e12d4dbf0a2 100644 --- a/homeassistant/components/ecoal_boiler/switch.py +++ b/homeassistant/components/ecoal_boiler/switch.py @@ -28,7 +28,7 @@ class EcoalSwitch(SwitchEntity): Sets HA switch to state as read from controller. """ self._ecoal_contr = ecoal_contr - self._name = name + self._attr_name = name self._state_attr = state_attr # Ecoalcotroller holds convention that same postfix is used # to set attribute @@ -36,13 +36,6 @@ class EcoalSwitch(SwitchEntity): # as attribute name in status instance: # status. self._contr_set_fun = getattr(self._ecoal_contr, f"set_{state_attr}") - # No value set, will be read from controller instead - self._state = None - - @property - def name(self) -> str | None: - """Return the name of the switch.""" - return self._name def update(self): """Fetch new state data for the sensor. @@ -50,7 +43,7 @@ class EcoalSwitch(SwitchEntity): This is the only method that should fetch new data for Home Assistant. """ status = self._ecoal_contr.get_cached_status() - self._state = getattr(status, self._state_attr) + self._attr_is_on = getattr(status, self._state_attr) def invalidate_ecoal_cache(self): """Invalidate ecoal interface cache. @@ -59,11 +52,6 @@ class EcoalSwitch(SwitchEntity): """ self._ecoal_contr.status = None - @property - def is_on(self) -> bool: - """Return true if device is on.""" - return self._state - def turn_on(self, **kwargs) -> None: """Turn the device on.""" self._contr_set_fun(1) From f26b88998a2f789e1c7a8d0e04fb391f4a92df22 Mon Sep 17 00:00:00 2001 From: lunmay <28674102+lunmay@users.noreply.github.com> Date: Tue, 14 Dec 2021 00:19:58 +0100 Subject: [PATCH 0392/2644] Fix typo in wolflink strings.sensor.json (#61716) Stablization -> Stabilization --- homeassistant/components/wolflink/strings.sensor.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/wolflink/strings.sensor.json b/homeassistant/components/wolflink/strings.sensor.json index 75c8199a117..c197edb5ea8 100644 --- a/homeassistant/components/wolflink/strings.sensor.json +++ b/homeassistant/components/wolflink/strings.sensor.json @@ -19,7 +19,7 @@ "ruhekontakt": "Rest contact", "vorspulen": "Entry rinsing", "zunden": "Ignition", - "stabilisierung": "Stablization", + "stabilisierung": "Stabilization", "ventilprufung": "Valve test", "nachspulen": "Post-flush", "softstart": "Soft start", From 0d4051efcb697c0dc90d5919ebb89d93ede672eb Mon Sep 17 00:00:00 2001 From: lunmay <28674102+lunmay@users.noreply.github.com> Date: Tue, 14 Dec 2021 00:24:14 +0100 Subject: [PATCH 0393/2644] Typo fixes for Adax in strings.json (#61492) --- homeassistant/components/adax/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/adax/strings.json b/homeassistant/components/adax/strings.json index 0cb60dceac9..6157b7dfc91 100644 --- a/homeassistant/components/adax/strings.json +++ b/homeassistant/components/adax/strings.json @@ -9,8 +9,8 @@ }, "local": { "data": { - "wifi_ssid": "Wifi ssid", - "wifi_pswd": "Wifi password" + "wifi_ssid": "Wi-Fi SSID", + "wifi_pswd": "Wi-Fi Password" }, "description": "Reset the heater by pressing + and OK until display shows 'Reset'. Then press and hold OK button on the heater until the blue led starts blinking before pressing Submit. Configuring heater might take some minutes." }, From dea20cf81698502341f39fba212fbdde019b7c87 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Tue, 14 Dec 2021 00:31:22 +0100 Subject: [PATCH 0394/2644] Upgrades Garages Amsterdam to v3.0.0 (#61734) --- homeassistant/components/garages_amsterdam/__init__.py | 8 ++++---- homeassistant/components/garages_amsterdam/config_flow.py | 8 ++++---- homeassistant/components/garages_amsterdam/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/garages_amsterdam/conftest.py | 2 +- tests/components/garages_amsterdam/test_config_flow.py | 2 +- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/garages_amsterdam/__init__.py b/homeassistant/components/garages_amsterdam/__init__.py index 5e7fbade8de..01dc6b17545 100644 --- a/homeassistant/components/garages_amsterdam/__init__.py +++ b/homeassistant/components/garages_amsterdam/__init__.py @@ -3,7 +3,7 @@ from datetime import timedelta import logging import async_timeout -import garages_amsterdam +from garages_amsterdam import GaragesAmsterdam from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -43,9 +43,9 @@ async def get_coordinator( async with async_timeout.timeout(10): return { garage.garage_name: garage - for garage in await garages_amsterdam.get_garages( - aiohttp_client.async_get_clientsession(hass) - ) + for garage in await GaragesAmsterdam( + session=aiohttp_client.async_get_clientsession(hass) + ).all_garages() } coordinator = DataUpdateCoordinator( diff --git a/homeassistant/components/garages_amsterdam/config_flow.py b/homeassistant/components/garages_amsterdam/config_flow.py index a043f7c2b00..c8a61f9a160 100644 --- a/homeassistant/components/garages_amsterdam/config_flow.py +++ b/homeassistant/components/garages_amsterdam/config_flow.py @@ -5,7 +5,7 @@ import logging from typing import Any from aiohttp import ClientResponseError -import garages_amsterdam +from garages_amsterdam import GaragesAmsterdam import voluptuous as vol from homeassistant import config_entries @@ -30,9 +30,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if self._options is None: self._options = [] try: - api_data = await garages_amsterdam.get_garages( - aiohttp_client.async_get_clientsession(self.hass) - ) + api_data = await GaragesAmsterdam( + session=aiohttp_client.async_get_clientsession(self.hass) + ).all_garages() except ClientResponseError: _LOGGER.error("Unexpected response from server") return self.async_abort(reason="cannot_connect") diff --git a/homeassistant/components/garages_amsterdam/manifest.json b/homeassistant/components/garages_amsterdam/manifest.json index ef90655276b..aedfa3cca65 100644 --- a/homeassistant/components/garages_amsterdam/manifest.json +++ b/homeassistant/components/garages_amsterdam/manifest.json @@ -3,7 +3,7 @@ "name": "Garages Amsterdam", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/garages_amsterdam", - "requirements": ["garages-amsterdam==2.1.1"], + "requirements": ["garages-amsterdam==3.0.0"], "codeowners": ["@klaasnicolaas"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index b005127387c..825000f71b5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -695,7 +695,7 @@ fritzconnection==1.7.2 gTTS==2.2.3 # homeassistant.components.garages_amsterdam -garages-amsterdam==2.1.1 +garages-amsterdam==3.0.0 # homeassistant.components.geniushub geniushub-client==0.6.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2c635964c27..9cff5f22d03 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -430,7 +430,7 @@ fritzconnection==1.7.2 gTTS==2.2.3 # homeassistant.components.garages_amsterdam -garages-amsterdam==2.1.1 +garages-amsterdam==3.0.0 # homeassistant.components.geo_json_events # homeassistant.components.usgs_earthquakes_feed diff --git a/tests/components/garages_amsterdam/conftest.py b/tests/components/garages_amsterdam/conftest.py index 49d242dabd5..aced2894d67 100644 --- a/tests/components/garages_amsterdam/conftest.py +++ b/tests/components/garages_amsterdam/conftest.py @@ -9,7 +9,7 @@ import pytest def mock_cases(): """Mock garages_amsterdam garages.""" with patch( - "garages_amsterdam.get_garages", + "garages_amsterdam.GaragesAmsterdam.all_garages", return_value=[ Mock( garage_name="IJDok", diff --git a/tests/components/garages_amsterdam/test_config_flow.py b/tests/components/garages_amsterdam/test_config_flow.py index a9f5f2c58ad..3749cf039db 100644 --- a/tests/components/garages_amsterdam/test_config_flow.py +++ b/tests/components/garages_amsterdam/test_config_flow.py @@ -57,7 +57,7 @@ async def test_error_handling( """Test we get the form.""" with patch( - "homeassistant.components.garages_amsterdam.config_flow.garages_amsterdam.get_garages", + "homeassistant.components.garages_amsterdam.config_flow.GaragesAmsterdam.all_garages", side_effect=side_effect, ): result = await hass.config_entries.flow.async_init( From bc79d41266bef39b0071bd635284fd7584d03753 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 14 Dec 2021 01:02:35 +0100 Subject: [PATCH 0395/2644] Upgrade black to 21.12b0 (#61741) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 91216c9efa9..018c0063f9e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ repos: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black - rev: 21.11b1 + rev: 21.12b0 hooks: - id: black args: diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 66786035e98..244add03cc4 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,7 +1,7 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit bandit==1.7.0 -black==21.11b1 +black==21.12b0 codespell==2.0.0 flake8-comprehensions==3.7.0 flake8-docstrings==1.6.0 From 4204f5799f4d8d52216635ee4582073b0538c839 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 14 Dec 2021 01:04:55 +0100 Subject: [PATCH 0396/2644] Add check for incompatible device trigger in Hue integration (#61726) --- .../components/hue/v2/device_trigger.py | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hue/v2/device_trigger.py b/homeassistant/components/hue/v2/device_trigger.py index 74863a1897e..3f474cdf70b 100644 --- a/homeassistant/components/hue/v2/device_trigger.py +++ b/homeassistant/components/hue/v2/device_trigger.py @@ -7,6 +7,7 @@ from aiohue.v2.models.button import ButtonEvent from aiohue.v2.models.resource import ResourceTypes import voluptuous as vol +from homeassistant.components import persistent_notification from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.homeassistant.triggers import event as event_trigger from homeassistant.const import ( @@ -35,7 +36,7 @@ if TYPE_CHECKING: TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_TYPE): str, - vol.Required(CONF_SUBTYPE): int, + vol.Required(CONF_SUBTYPE): vol.Union(int, str), vol.Optional(CONF_UNIQUE_ID): str, } ) @@ -54,6 +55,33 @@ DEVICE_SPECIFIC_EVENT_TYPES = { } +def check_invalid_device_trigger( + bridge: HueBridge, + config: ConfigType, + device_entry: DeviceEntry, + automation_info: AutomationTriggerInfo | None = None, +): + """Check automation config for deprecated format.""" + # NOTE: Remove this check after 2022.6 + if isinstance(config["subtype"], int): + return + # found deprecated V1 style trigger, notify the user that it should be adjusted + msg = ( + f"Incompatible device trigger detected for " + f"[{device_entry.name}](/config/devices/device/{device_entry.id}) " + "Please manually fix the outdated automation(s) once to fix this issue." + ) + if automation_info: + automation_id = automation_info["variables"]["this"]["attributes"]["id"] # type: ignore + msg += f"\n\n[Check it out](/config/automation/edit/{automation_id})." + persistent_notification.async_create( + bridge.hass, + msg, + title="Outdated device trigger found", + notification_id=f"hue_trigger_{device_entry.id}", + ) + + async def async_validate_trigger_config( bridge: "HueBridge", device_entry: DeviceEntry, @@ -61,6 +89,7 @@ async def async_validate_trigger_config( ): """Validate config.""" config = TRIGGER_SCHEMA(config) + check_invalid_device_trigger(bridge, config, device_entry) return config @@ -84,6 +113,7 @@ async def async_attach_trigger( }, } ) + check_invalid_device_trigger(bridge, config, device_entry, automation_info) return await event_trigger.async_attach_trigger( hass, event_config, action, automation_info, platform_type="device" ) From eb345bfdf13f92cbad8eb785c92384853fb95ded Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 14 Dec 2021 00:13:33 +0000 Subject: [PATCH 0397/2644] [ci skip] Translation update --- .../components/adax/translations/en.json | 4 +- .../components/adax/translations/zh-Hans.json | 13 ++++- .../adguard/translations/zh-Hans.json | 2 +- .../components/apple_tv/translations/nl.json | 23 ++++++-- .../apple_tv/translations/zh-Hans.json | 12 +++++ .../binary_sensor/translations/nl.json | 2 + .../cloudflare/translations/zh-Hans.json | 9 +++- .../coinbase/translations/zh-Hans.json | 36 +++++++++++++ .../daikin/translations/zh-Hans.json | 13 +++-- .../dlna_dmr/translations/zh-Hans.json | 47 ++++++++++++++++- .../elmax/translations/zh-Hans.json | 18 +++++++ .../ezviz/translations/zh-Hans.json | 4 +- .../jellyfin/translations/zh-Hans.json | 21 ++++++++ .../components/lcn/translations/ca.json | 2 +- .../components/lcn/translations/en.json | 2 +- .../components/lcn/translations/et.json | 2 +- .../components/lcn/translations/nl.json | 10 ++++ .../components/lcn/translations/no.json | 10 ++++ .../components/lcn/translations/zh-Hans.json | 7 +++ .../modern_forms/translations/no.json | 2 +- .../components/mqtt/translations/no.json | 2 +- .../components/mqtt/translations/zh-Hans.json | 30 +++++++++-- .../mysensors/translations/zh-Hans.json | 18 +++++++ .../components/nina/translations/zh-Hans.json | 11 ++++ .../components/octoprint/translations/no.json | 2 +- .../onvif/translations/zh-Hans.json | 3 +- .../powerwall/translations/zh-Hans.json | 9 ++++ .../simplisafe/translations/nl.json | 3 +- .../simplisafe/translations/no.json | 2 +- .../simplisafe/translations/zh-Hans.json | 1 + .../tailscale/translations/zh-Hans.json | 26 ++++++++++ .../translations/zh-Hans.json | 2 +- .../tradfri/translations/zh-Hans.json | 1 + .../components/upnp/translations/zh-Hans.json | 14 +++++ .../wolflink/translations/sensor.en.json | 2 +- .../translations/select.nl.json | 52 +++++++++++++++++++ .../translations/select.zh-Hans.json | 12 +++++ .../components/zwave_js/translations/nl.json | 2 + 38 files changed, 397 insertions(+), 34 deletions(-) create mode 100644 homeassistant/components/coinbase/translations/zh-Hans.json create mode 100644 homeassistant/components/elmax/translations/zh-Hans.json create mode 100644 homeassistant/components/jellyfin/translations/zh-Hans.json create mode 100644 homeassistant/components/lcn/translations/nl.json create mode 100644 homeassistant/components/lcn/translations/no.json create mode 100644 homeassistant/components/lcn/translations/zh-Hans.json create mode 100644 homeassistant/components/mysensors/translations/zh-Hans.json create mode 100644 homeassistant/components/nina/translations/zh-Hans.json create mode 100644 homeassistant/components/powerwall/translations/zh-Hans.json create mode 100644 homeassistant/components/tailscale/translations/zh-Hans.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/select.nl.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/select.zh-Hans.json diff --git a/homeassistant/components/adax/translations/en.json b/homeassistant/components/adax/translations/en.json index 637bef63ece..ae31fdbc041 100644 --- a/homeassistant/components/adax/translations/en.json +++ b/homeassistant/components/adax/translations/en.json @@ -19,8 +19,8 @@ }, "local": { "data": { - "wifi_pswd": "Wifi password", - "wifi_ssid": "Wifi ssid" + "wifi_pswd": "Wi-Fi Password", + "wifi_ssid": "Wi-Fi SSID" }, "description": "Reset the heater by pressing + and OK until display shows 'Reset'. Then press and hold OK button on the heater until the blue led starts blinking before pressing Submit. Configuring heater might take some minutes." }, diff --git a/homeassistant/components/adax/translations/zh-Hans.json b/homeassistant/components/adax/translations/zh-Hans.json index 7cc89fcc775..2946f1ebefb 100644 --- a/homeassistant/components/adax/translations/zh-Hans.json +++ b/homeassistant/components/adax/translations/zh-Hans.json @@ -1,16 +1,27 @@ { "config": { + "abort": { + "invalid_auth": "\u65e0\u6548\u7684\u6388\u6743" + }, "error": { "cannot_connect": "\u8fde\u63a5\u5931\u8d25" }, "step": { "cloud": { "data": { - "account_id": "\u5e10\u6237ID" + "account_id": "\u5e10\u6237ID", + "password": "\u5bc6\u7801" + } + }, + "local": { + "data": { + "wifi_pswd": "WiFi \u5bc6\u7801", + "wifi_ssid": "WiFi \u540d\u79f0 (SSID)" } }, "user": { "data": { + "connection_type": "\u9009\u62e9\u8fde\u63a5\u7c7b\u578b", "password": "\u5bc6\u7801" } } diff --git a/homeassistant/components/adguard/translations/zh-Hans.json b/homeassistant/components/adguard/translations/zh-Hans.json index ee68ce83e91..e48fd2fd9cc 100644 --- a/homeassistant/components/adguard/translations/zh-Hans.json +++ b/homeassistant/components/adguard/translations/zh-Hans.json @@ -13,7 +13,7 @@ "host": "\u4e3b\u673a\u5730\u5740", "password": "\u5bc6\u7801", "port": "\u7aef\u53e3", - "ssl": "\u4f7f\u7528 SSL \u8bc1\u4e66\u51ed\u8bc1", + "ssl": "\u4f7f\u7528 SSL \u8fde\u63a5", "username": "\u7528\u6237\u540d", "verify_ssl": "\u9a8c\u8bc1 SSL \u8bc1\u4e66\u51ed\u8bc1" }, diff --git a/homeassistant/components/apple_tv/translations/nl.json b/homeassistant/components/apple_tv/translations/nl.json index cc04522334d..7fdc20c7291 100644 --- a/homeassistant/components/apple_tv/translations/nl.json +++ b/homeassistant/components/apple_tv/translations/nl.json @@ -1,12 +1,17 @@ { "config": { "abort": { + "already_configured": "Apparaat is al geconfigureerd", "already_configured_device": "Apparaat is al geconfigureerd", "already_in_progress": "De configuratiestroom is al aan de gang", "backoff": "Het apparaat accepteert op dit moment geen koppelingsverzoeken (u heeft mogelijk te vaak een ongeldige pincode ingevoerd), probeer het later opnieuw.", "device_did_not_pair": "Er is geen poging gedaan om het koppelingsproces te voltooien vanaf het apparaat.", + "device_not_found": "Apparaat werd niet gevonden tijdens het zoeken, probeer het opnieuw toe te voegen.", + "inconsistent_device": "De verwachte protocollen zijn niet gevonden tijdens het zoeken. Dit wijst gewoonlijk op een probleem met multicast DNS (Zeroconf). Probeer het apparaat opnieuw toe te voegen.", "invalid_config": "De configuratie voor dit apparaat is onvolledig. Probeer het opnieuw toe te voegen.", "no_devices_found": "Geen apparaten gevonden op het netwerk", + "reauth_successful": "Herauthenticatie was succesvol", + "setup_failed": "Kan het apparaat niet instellen.", "unknown": "Onverwachte fout" }, "error": { @@ -16,14 +21,14 @@ "no_usable_service": "Er is een apparaat gevonden, maar er kon geen manier worden gevonden om er verbinding mee te maken. Als u dit bericht blijft zien, probeert u het IP-adres in te voeren of uw Apple TV opnieuw op te starten.", "unknown": "Onverwachte fout" }, - "flow_title": "{name}", + "flow_title": "{name} ( {type} )", "step": { "confirm": { - "description": "U staat op het punt om de Apple TV met de naam `{name}` toe te voegen aan Home Assistant.\n\n**Om het proces te voltooien, moet u mogelijk meerdere PIN-codes invoeren.**\n\nLet op: u kunt uw Apple TV *niet* uitschakelen met deze integratie. Alleen de mediaspeler in Home Assistant wordt uitgeschakeld!", + "description": "U staat op het punt om `{name}` van het type `{type}` toe te voegen aan Home Assistant.\n\n**Om het proces te voltooien, kan het zijn dat u meerdere PIN-codes moet invoeren.**\n\nLet op dat u *niet* uw Apple TV kunt uitschakelen met deze integratie. Alleen de mediaspeler in Home Assistant gaat uit!", "title": "Bevestig het toevoegen van Apple TV" }, "pair_no_pin": { - "description": "Koppeling is vereist voor de `{protocol}` service. Voer de PIN {pin} in op uw Apple TV om verder te gaan.", + "description": "Koppeling is vereist voor de `{protocol}` service. Voer de PIN {pin} in op uw apparaat om verder te gaan.", "title": "Koppelen" }, "pair_with_pin": { @@ -33,8 +38,16 @@ "description": "Koppelen is vereist voor het `{protocol}` protocol. Voer de PIN-code in die op het scherm wordt getoond. Beginnende nullen moeten worden weggelaten, d.w.z. voer 123 in als de getoonde code 0123 is.", "title": "Koppelen" }, + "password": { + "description": "Een wachtwoord is vereist door `{protocol}`. Dit wordt nog niet ondersteund, schakel het wachtwoord uit om verder te gaan.", + "title": "Wachtwoord vereist" + }, + "protocol_disabled": { + "description": "Koppelen is vereist voor `{protocol}` maar het is uitgeschakeld op het apparaat. Controleer mogelijke toegangsbeperkingen (bijv. alle apparaten op het lokale netwerk toestaan verbinding te maken) op het apparaat.\n\nU kunt doorgaan zonder dit protocol te koppelen, maar sommige functies zullen beperkt zijn.", + "title": "Koppelen niet mogelijk" + }, "reconfigure": { - "description": "Deze Apple TV ondervindt verbindingsproblemen en moet opnieuw worden geconfigureerd.", + "description": "Configureer dit apparaat opnieuw om de functionaliteit te herstellen.", "title": "Apparaat herconfiguratie" }, "service_problem": { @@ -45,7 +58,7 @@ "data": { "device_input": "Apparaat" }, - "description": "Begin met het invoeren van de apparaatnaam (bijv. Keuken of Slaapkamer) of het IP-adres van de Apple TV die u wilt toevoegen. Als er automatisch apparaten in uw netwerk zijn gevonden, worden deze hieronder weergegeven.\n\nAls u het apparaat niet kunt zien of problemen ondervindt, probeer dan het IP-adres van het apparaat in te voeren.\n\n{devices}", + "description": "Begin met het invoeren van de apparaatnaam (bijv. Keuken of Slaapkamer) of het IP-adres van de Apple TV die u wilt toevoegen. \n\nAls u het apparaat niet kunt zien of problemen ondervindt, probeer dan het IP-adres van het apparaat in te voeren.\n\n", "title": "Stel een nieuwe Apple TV in" } } diff --git a/homeassistant/components/apple_tv/translations/zh-Hans.json b/homeassistant/components/apple_tv/translations/zh-Hans.json index 4b178c75fce..2232ea02b41 100644 --- a/homeassistant/components/apple_tv/translations/zh-Hans.json +++ b/homeassistant/components/apple_tv/translations/zh-Hans.json @@ -1,11 +1,15 @@ { "config": { "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u88ab\u914d\u7f6e", "already_configured_device": "\u8bbe\u5907\u5df2\u88ab\u914d\u7f6e", "already_in_progress": "\u914d\u7f6e\u6d41\u7a0b\u5df2\u5728\u8fdb\u884c\u4e2d", "backoff": "\u8bbe\u5907\u76ee\u524d\u6682\u4e0d\u63a5\u53d7\u914d\u5bf9\u8bf7\u6c42\uff08\u53ef\u80fd\u591a\u6b21\u8f93\u5165\u65e0\u6548 PIN \u7801\uff09\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5\u3002", + "device_not_found": "\u65e0\u6cd5\u4fa6\u6d4b\u5230\u8bbe\u5907\uff0c\u8bf7\u5c1d\u8bd5\u91cd\u65b0\u6dfb\u52a0", "invalid_config": "\u6b64\u8bbe\u5907\u7684\u914d\u7f6e\u4fe1\u606f\u4e0d\u5b8c\u6574\u3002\u8bf7\u5c1d\u8bd5\u91cd\u65b0\u6dfb\u52a0\u3002", "no_devices_found": "\u672a\u5728\u6b64\u7f51\u7edc\u53d1\u73b0\u76f8\u5173\u8bbe\u5907", + "reauth_successful": "\u91cd\u9a8c\u8bc1\u6210\u529f", + "setup_failed": "\u65e0\u6cd5\u8bbe\u7f6e\u8bbe\u5907\u3002", "unknown": "\u672a\u77e5\u9519\u8bef" }, "error": { @@ -15,6 +19,7 @@ "no_usable_service": "\u5df2\u76f8\u5173\u627e\u5230\u8bbe\u5907\uff0c\u4f46\u65e0\u6cd5\u8bc6\u522b\u5e76\u4e0e\u5176\u5efa\u7acb\u8fde\u63a5\u3002\u82e5\u60a8\u4e00\u76f4\u6536\u5230\u6b64\u8b66\u544a\u6d88\u606f\uff0c\u8bf7\u5c1d\u8bd5\u4e3a\u5176\u6307\u5b9a\u56fa\u5b9a IP \u5730\u5740\u6216\u91cd\u65b0\u542f\u52a8\u60a8\u7684 Apple TV\u3002", "unknown": "\u672a\u77e5\u9519\u8bef" }, + "flow_title": "{name} ({type})", "step": { "confirm": { "description": "\u60a8\u5373\u5c06\u6dfb\u52a0 Apple TV (\u540d\u79f0\u4e3a\u201c{name}\u201d)\u5230 Home Assistant\u3002 \n\n **\u8981\u5b8c\u6210\u6b64\u8fc7\u7a0b\uff0c\u53ef\u80fd\u9700\u8981\u8f93\u5165\u591a\u4e2a PIN \u7801\u3002** \n\n\u8bf7\u6ce8\u610f\uff0c\u6b64\u96c6\u6210*\u4e0d\u80fd*\u5173\u95ed Apple TV \u7684\u7535\u6e90\uff0c\u53ea\u4f1a\u5173\u95ed Home Assistant \u4e2d\u7684\u5a92\u4f53\u64ad\u653e\u5668\uff01", @@ -30,6 +35,13 @@ }, "title": "\u914d\u5bf9\u4e2d" }, + "password": { + "description": "`{protocol}` \u9700\u8981\u8f93\u5165\u5bc6\u7801\u3002\u76ee\u524d\u8be5\u8bbe\u5907\u6682\u4e0d\u652f\u6301\u6b64\u529f\u80fd\uff0c\u8bf7\u7981\u7528\u540e\u518d\u7ee7\u7eed\u64cd\u4f5c\u3002", + "title": "\u8f93\u5165\u5bc6\u7801" + }, + "protocol_disabled": { + "title": "\u65e0\u6cd5\u914d\u5bf9" + }, "reconfigure": { "description": "\u8be5 Apple TV \u9047\u5230\u4e00\u4e9b\u8fde\u63a5\u95ee\u9898\uff0c\u987b\u91cd\u65b0\u914d\u7f6e\u3002", "title": "\u8bbe\u5907\u91cd\u65b0\u914d\u7f6e" diff --git a/homeassistant/components/binary_sensor/translations/nl.json b/homeassistant/components/binary_sensor/translations/nl.json index f3d8a263187..b4fbaf43b45 100644 --- a/homeassistant/components/binary_sensor/translations/nl.json +++ b/homeassistant/components/binary_sensor/translations/nl.json @@ -84,6 +84,7 @@ "not_powered": "{entity_name} niet ingeschakeld", "not_present": "{entity_name} is niet aanwezig", "not_running": "{entity_name} is niet langer actief", + "not_tampered": "{entity_name} gestopt met het detecteren van sabotage", "not_unsafe": "{entity_name} werd veilig", "occupied": "{entity_name} werd bezet", "opened": "{entity_name} geopend", @@ -94,6 +95,7 @@ "running": "{entity_name} is actief geworden", "smoke": "{entity_name} begon rook te detecteren", "sound": "{entity_name} begon geluid te detecteren", + "tampered": "{entity_name} begonnen met het detecteren van sabotage", "turned_off": "{entity_name} uitgeschakeld", "turned_on": "{entity_name} ingeschakeld", "unsafe": "{entity_name} werd onveilig", diff --git a/homeassistant/components/cloudflare/translations/zh-Hans.json b/homeassistant/components/cloudflare/translations/zh-Hans.json index 78429184bad..54d1a3b55f2 100644 --- a/homeassistant/components/cloudflare/translations/zh-Hans.json +++ b/homeassistant/components/cloudflare/translations/zh-Hans.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "reauth_successful": "\u91cd\u9a8c\u8bc1\u6210\u529f" + }, "error": { "cannot_connect": "\u8fde\u63a5\u5931\u8d25", "invalid_auth": "\u9a8c\u8bc1\u7801\u65e0\u6548" @@ -7,13 +10,15 @@ "step": { "reauth_confirm": { "data": { + "api_token": "API Token", "description": "\u4f7f\u7528\u60a8\u7684 Cloudflare \u5e10\u6237\u91cd\u65b0\u8fdb\u884c\u8eab\u4efd\u9a8c\u8bc1\u3002" } }, "user": { "data": { - "api_token": "API \u5bc6\u7801" - } + "api_token": "API Token" + }, + "title": "\u8fde\u63a5\u81f3 Cloudflare" }, "zone": { "data": { diff --git a/homeassistant/components/coinbase/translations/zh-Hans.json b/homeassistant/components/coinbase/translations/zh-Hans.json new file mode 100644 index 00000000000..02d3f5e6773 --- /dev/null +++ b/homeassistant/components/coinbase/translations/zh-Hans.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u88ab\u914d\u7f6e" + }, + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "invalid_auth": "\u9a8c\u8bc1\u65e0\u6548", + "unknown": "\u672a\u77e5\u9519\u8bef" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "api_token": "API Token", + "currencies": "\u8d26\u6237\u4f59\u989d", + "exchange_rates": "\u6c47\u7387" + }, + "description": "\u8bf7\u8f93\u5165\u7531 Coinbase \u63d0\u4f9b\u7684 API \u5bc6\u94a5\u4fe1\u606f", + "title": "Coinbase API \u5bc6\u94a5\u8be6\u60c5" + } + } + }, + "options": { + "error": { + "currency_unavaliable": "Coinbase \u65e0\u6cd5\u63d0\u4f9b\u5176\u8bbe\u5b9a\u7684\u6c47\u7387\u4fe1\u606f", + "exchange_rate_unavaliable": "Coinbase \u65e0\u6cd5\u63d0\u4f9b\u5176\u8bbe\u5b9a\u7684\u6c47\u7387\u4fe1\u606f", + "unknown": "\u672a\u77e5\u9519\u8bef" + }, + "step": { + "init": { + "description": "\u8c03\u6574 Coinbase \u9009\u9879" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/daikin/translations/zh-Hans.json b/homeassistant/components/daikin/translations/zh-Hans.json index 0acb5110fec..844b7bd78f6 100644 --- a/homeassistant/components/daikin/translations/zh-Hans.json +++ b/homeassistant/components/daikin/translations/zh-Hans.json @@ -5,17 +5,20 @@ "cannot_connect": "\u8fde\u63a5\u5931\u8d25" }, "error": { + "api_password": "\u9a8c\u8bc1\u65e0\u6548\uff0c\u8bf7\u4f7f\u7528\u5176\u5b83\u7684 API \u5bc6\u94a5\u6216\u5bc6\u7801\u91cd\u8bd5", "cannot_connect": "\u8fde\u63a5\u5931\u8d25", - "invalid_auth": "\u9a8c\u8bc1\u7801\u9519\u8bef" + "invalid_auth": "\u9a8c\u8bc1\u9519\u8bef", + "unknown": "\u672a\u77e5\u9519\u8bef" }, "step": { "user": { "data": { - "api_key": "API\u5bc6\u7801", - "host": "\u4e3b\u673a" + "api_key": "API \u5bc6\u94a5", + "host": "\u4e3b\u673a\u5730\u5740", + "password": "\u5bc6\u7801" }, - "description": "\u8f93\u5165\u60a8\u7684 Daikin \u7a7a\u8c03\u7684 IP \u5730\u5740\u3002", - "title": "\u914d\u7f6e Daikin \u7a7a\u8c03" + "description": "\u8f93\u5165\u60a8\u7684\u5927\u91d1\u7a7a\u8c03\u7684 IP \u5730\u5740\u3002\n\n\u6ce8\u610f\uff1aBRP072Cxx \u6216 SKYFi \u578b\u53f7\u8bbe\u5907\u9700\u8981\u63d0\u4f9b API \u5bc6\u94a5\u548c\u5bc6\u7801", + "title": "\u914d\u7f6e\u5927\u91d1\u7a7a\u8c03" } } } diff --git a/homeassistant/components/dlna_dmr/translations/zh-Hans.json b/homeassistant/components/dlna_dmr/translations/zh-Hans.json index 909a38b4b74..8bcf49d86c2 100644 --- a/homeassistant/components/dlna_dmr/translations/zh-Hans.json +++ b/homeassistant/components/dlna_dmr/translations/zh-Hans.json @@ -1,12 +1,57 @@ { "config": { + "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u88ab\u914d\u7f6e", + "alternative_integration": "\u8be5\u8bbe\u5907\u5728\u53e6\u4e00\u96c6\u6210\u80fd\u63d0\u4f9b\u66f4\u597d\u7684\u652f\u6301", + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "could_not_connect": "\u65e0\u6cd5\u8fde\u63a5\u5230 DLNA \u8bbe\u5907", + "discovery_error": "\u672a\u53d1\u73b0\u53ef\u7528\u7684 DLNA \u8bbe\u5907", + "incomplete_config": "\u914d\u7f6e\u7f3a\u5c11\u5fc5\u8981\u7684\u53d8\u91cf\u4fe1\u606f", + "non_unique_id": "\u53d1\u73b0\u591a\u53f0\u8bbe\u5907\u5177\u6709\u76f8\u540c\u7684\u552f\u4e00 ID", + "not_dmr": "\u8be5\u8bbe\u5907\u4e0d\u662f\u4e00\u4e2a\u53d7\u652f\u6301\u7684\u6570\u5b57\u5a92\u4f53\u6e32\u67d3\u5668" + }, "error": { - "could_not_connect": "\u65e0\u6cd5\u8fde\u63a5\u5230 DLNA \u8bbe\u5907" + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "could_not_connect": "\u65e0\u6cd5\u8fde\u63a5\u5230 DLNA \u8bbe\u5907", + "not_dmr": "\u8be5\u8bbe\u5907\u4e0d\u662f\u4e00\u4e2a\u53d7\u652f\u6301\u7684\u6570\u5b57\u5a92\u4f53\u6e32\u67d3\u5668" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "\u4f60\u60f3\u8981\u5f00\u59cb\u8bbe\u7f6e\u5417\uff1f" + }, + "import_turn_on": { + "description": "\u8bf7\u6253\u5f00\u8bbe\u5907\u5e76\u5355\u51fb\u201c\u63d0\u4ea4\u201d\u6309\u94ae\u4ee5\u7ee7\u7eed\u8fc1\u79fb" + }, + "manual": { + "data": { + "url": "URL" + }, + "description": "\u8bbe\u5907\u63cf\u8ff0\u6587\u4ef6(.xml)\u7f51\u5740", + "title": "\u624b\u52a8\u914d\u7f6e DLNA DMR \u8bbe\u5907\u8fde\u63a5" + }, + "user": { + "data": { + "host": "\u4e3b\u673a\u5730\u5740", + "url": "URL" + }, + "title": "\u53d1\u73b0 DLNA DMR \u8bbe\u5907" + } } }, "options": { "error": { "invalid_url": "\u65e0\u6548\u7f51\u5740" + }, + "step": { + "init": { + "data": { + "callback_url_override": "\u4e8b\u4ef6\u4fa6\u542c\u5668\u56de\u8c03 URL", + "listen_port": "\u4e8b\u4ef6\u4fa6\u542c\u5668\u7aef\u53e3\uff08\u5982\u4e0d\u6307\u5b9a\u5219\u968f\u673a\u7aef\u53e3\u53f7\uff09", + "poll_availability": "\u8f6e\u8be2\u8bbe\u5907\u53ef\u7528\u6027" + }, + "title": "DLNA \u6570\u5b57\u5a92\u4f53\u6e32\u67d3\u5668\u914d\u7f6e" + } } } } \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/zh-Hans.json b/homeassistant/components/elmax/translations/zh-Hans.json new file mode 100644 index 00000000000..0d0c87c6a5b --- /dev/null +++ b/homeassistant/components/elmax/translations/zh-Hans.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "panels": { + "data": { + "panel_pin": "PIN \u7801" + } + }, + "user": { + "data": { + "password": "\u5bc6\u7801", + "username": "\u7528\u6237\u540d" + }, + "title": "\u8d26\u6237\u767b\u5f55" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/zh-Hans.json b/homeassistant/components/ezviz/translations/zh-Hans.json index 3d8daedec73..ee3cf07bdb3 100644 --- a/homeassistant/components/ezviz/translations/zh-Hans.json +++ b/homeassistant/components/ezviz/translations/zh-Hans.json @@ -23,10 +23,10 @@ "user": { "data": { "password": "\u5bc6\u7801", - "url": "URL", + "url": "\u9009\u62e9\u670d\u52a1\u5668\u5730\u5740\uff1a", "username": "\u7528\u6237\u540d" }, - "title": "\u8fde\u63a5\u5230\u8424\u77f3\u4e91" + "title": "\u8fde\u63a5\u81f3\u8424\u77f3\u4e91" }, "user_custom_url": { "data": { diff --git a/homeassistant/components/jellyfin/translations/zh-Hans.json b/homeassistant/components/jellyfin/translations/zh-Hans.json new file mode 100644 index 00000000000..df97498e25e --- /dev/null +++ b/homeassistant/components/jellyfin/translations/zh-Hans.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u5f53\u524d\u90e8\u4ef6\u5df2\u5b58\u5728\u914d\u7f6e\uff0c\u8bf7\u5220\u9664\u73b0\u6709\u914d\u7f6e\u540e\u91cd\u8bd5\u3002" + }, + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "invalid_auth": "\u9a8c\u8bc1\u65e0\u6548", + "unknown": "\u672a\u77e5\u9519\u8bef" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u7801", + "url": "\u4e3b\u673a\u5730\u5740", + "username": "\u7528\u6237\u540d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lcn/translations/ca.json b/homeassistant/components/lcn/translations/ca.json index e6d704f1668..9fe9fd952d3 100644 --- a/homeassistant/components/lcn/translations/ca.json +++ b/homeassistant/components/lcn/translations/ca.json @@ -4,7 +4,7 @@ "fingerprint": "codi d'empremta rebut", "send_keys": "claus d'enviament rebudes", "transmitter": "codi del transmissor rebut", - "transponder": "codi del transpoder rebut" + "transponder": "codi del transpondedor rebut" } } } \ No newline at end of file diff --git a/homeassistant/components/lcn/translations/en.json b/homeassistant/components/lcn/translations/en.json index 9a27b35a4d4..ad42b1ffc8f 100644 --- a/homeassistant/components/lcn/translations/en.json +++ b/homeassistant/components/lcn/translations/en.json @@ -4,7 +4,7 @@ "fingerprint": "fingerprint code received", "send_keys": "send keys received", "transmitter": "transmitter code received", - "transponder": "transpoder code received" + "transponder": "transponder code received" } } } \ No newline at end of file diff --git a/homeassistant/components/lcn/translations/et.json b/homeassistant/components/lcn/translations/et.json index eff0c5bd79e..058873e63c5 100644 --- a/homeassistant/components/lcn/translations/et.json +++ b/homeassistant/components/lcn/translations/et.json @@ -4,7 +4,7 @@ "fingerprint": "vastu v\u00f5etud s\u00f5rmej\u00e4ljekood", "send_keys": "vastuv\u00f5etud v\u00f5tmete saatmine", "transmitter": "saatja kood vastu v\u00f5etud", - "transponder": "saadud transpooderi kood" + "transponder": "saadud transponderi kood" } } } \ No newline at end of file diff --git a/homeassistant/components/lcn/translations/nl.json b/homeassistant/components/lcn/translations/nl.json new file mode 100644 index 00000000000..776dc63535d --- /dev/null +++ b/homeassistant/components/lcn/translations/nl.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "trigger_type": { + "fingerprint": "vingerafdruk code ontvangen", + "send_keys": "stuur sleutels ontvangen", + "transmitter": "zendercode ontvangen", + "transponder": "transpondercode ontvangen" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lcn/translations/no.json b/homeassistant/components/lcn/translations/no.json new file mode 100644 index 00000000000..5f4a8a79ba7 --- /dev/null +++ b/homeassistant/components/lcn/translations/no.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "trigger_type": { + "fingerprint": "fingeravtrykkkode mottatt", + "send_keys": "sende n\u00f8kler mottatt", + "transmitter": "senderkode mottatt", + "transponder": "transpoderkode mottatt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lcn/translations/zh-Hans.json b/homeassistant/components/lcn/translations/zh-Hans.json new file mode 100644 index 00000000000..959219da6df --- /dev/null +++ b/homeassistant/components/lcn/translations/zh-Hans.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "trigger_type": { + "fingerprint": "\u6536\u5230\u6307\u7eb9\u7801" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modern_forms/translations/no.json b/homeassistant/components/modern_forms/translations/no.json index 04718b9d039..e7da915d39b 100644 --- a/homeassistant/components/modern_forms/translations/no.json +++ b/homeassistant/components/modern_forms/translations/no.json @@ -19,7 +19,7 @@ "description": "Sett opp Modern Forms-fanen din for \u00e5 integrere med Home Assistant." }, "zeroconf_confirm": { - "description": "Vil du legge til Modern Forms-viften med navnet {name} i Hjemmeassistent?", + "description": "Vil du legge til Modern Forms-viften med navnet {name} i Home Assistant?", "title": "Oppdaget Modern Forms-vifteenhet" } } diff --git a/homeassistant/components/mqtt/translations/no.json b/homeassistant/components/mqtt/translations/no.json index 11f3610e033..b6f7753d3a9 100644 --- a/homeassistant/components/mqtt/translations/no.json +++ b/homeassistant/components/mqtt/translations/no.json @@ -80,7 +80,7 @@ "will_retain": "Testament melding behold", "will_topic": "Testament melding emne" }, - "description": "Discovery - Hvis oppdagelse er aktivert (anbefales), vil Home Assistant automatisk oppdage enheter og enheter som publiserer konfigurasjonen p\u00e5 MQTT-megleren. Hvis s\u00f8k er deaktivert, m\u00e5 all konfigurasjon utf\u00f8res manuelt.\nF\u00f8dselsmelding - F\u00f8dselsmeldingen vil bli sendt hver gang Home Assistant (re) kobles til MQTT megleren.\nWill message - Will-meldingen vil bli sendt hver gang Home Assistant mister forbindelsen til megleren, b\u00e5de i tilfelle en ren (f.eks. at Home Assistant avsluttes) og i tilfelle en uren (f.eks. hjemmeassistent krasjer eller mister nettverkstilkoblingen) koble fra.", + "description": "Oppdagelse - Hvis oppdagelse er aktivert (anbefales), vil Home Assistant automatisk oppdage enheter og entiteter som publiserer konfigurasjonen til MQTT-megleren. Hvis oppdaglse er deaktivert, m\u00e5 all konfigurasjon utf\u00f8res manuelt.\nF\u00f8dselsmelding - F\u00f8dselsmeldingen vil bli sendt hver gang Home Assistant kobler til MQTT megleren.\nWill message - Will-meldingen vil bli sendt hver gang Home Assistant mister forbindelsen til megleren, b\u00e5de i tilfelle en ren (f.eks. at Home Assistant avsluttes) og i tilfelle en uren (f.eks. Home Assistant krasjer eller mister nettverkstilkoblingen) frakobling.", "title": "MQTT-alternativer" } } diff --git a/homeassistant/components/mqtt/translations/zh-Hans.json b/homeassistant/components/mqtt/translations/zh-Hans.json index 31fc4e36825..fafb91a66f3 100644 --- a/homeassistant/components/mqtt/translations/zh-Hans.json +++ b/homeassistant/components/mqtt/translations/zh-Hans.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u670d\u52a1\u5df2\u88ab\u914d\u7f6e", - "single_instance_allowed": "\u53ea\u5141\u8bb8\u4e00\u4e2a MQTT \u914d\u7f6e\u3002" + "single_instance_allowed": "\u914d\u7f6e\u5df2\u5b58\u5728\uff0c\u8bf7\u5220\u9664\u73b0\u6709\u914d\u7f6e\u540e\u518d\u91cd\u8bd5\u3002" }, "error": { "cannot_connect": "\u65e0\u6cd5\u8fde\u63a5\u5230\u670d\u52a1\u5668\u3002" @@ -11,7 +11,7 @@ "broker": { "data": { "broker": "\u670d\u52a1\u5668", - "discovery": "\u542f\u7528\u53d1\u73b0", + "discovery": "\u542f\u7528\u53d1\u73b0\u529f\u80fd", "password": "\u5bc6\u7801", "port": "\u7aef\u53e3", "username": "\u7528\u6237\u540d" @@ -20,7 +20,7 @@ }, "hassio_confirm": { "data": { - "discovery": "\u542f\u7528\u53d1\u73b0" + "discovery": "\u542f\u7528\u53d1\u73b0\u529f\u80fd" }, "description": "\u662f\u5426\u8981\u914d\u7f6e Home Assistant \u8fde\u63a5\u5230 Supervisor \u52a0\u8f7d\u9879 {addon} \u63d0\u4f9b\u7684 MQTT \u670d\u52a1\u5668\uff1f", "title": "\u6765\u81ea Supervisor \u52a0\u8f7d\u9879\u7684 MQTT \u670d\u52a1\u5668" @@ -50,11 +50,33 @@ } }, "options": { + "error": { + "bad_birth": "\u65e0\u6548\u7684\u51fa\u751f\u6d88\u606f(bitrh)\u7c7b\u578b", + "bad_will": "\u65e0\u6548\u7684\u9057\u5631\u6d88\u606f(will)\u7c7b\u578b", + "cannot_connect": "\u8fde\u63a5\u5931\u8d25" + }, "step": { "broker": { "data": { + "broker": "\u670d\u52a1\u5668", + "password": "\u5bc6\u7801", + "port": "\u7aef\u53e3", "username": "\u7528\u6237\u540d" - } + }, + "description": "\u8bf7\u8f93\u5165\u60a8\u7684 MQTT \u670d\u52a1\u5668\u8fde\u63a5\u4fe1\u606f", + "title": "\u670d\u52a1\u5668\u9009\u9879" + }, + "options": { + "data": { + "birth_enable": "\u542f\u7528\u51fa\u751f\u6d88\u606f(birth)", + "birth_qos": "\u51fa\u751f\u6d88\u606f QoS", + "birth_topic": "\u51fa\u751f\u6d88\u606f(birth)\u7c7b\u578b", + "discovery": "\u542f\u7528\u53d1\u73b0\u529f\u80fd", + "will_enable": "\u542f\u7528\u9057\u5631\u6d88\u606f(will)", + "will_topic": "\u9057\u5631\u6d88\u606f\u7c7b\u578b(will)" + }, + "description": "\u201c\u53d1\u73b0\u201d\u529f\u80fd - \u82e5\u201c\u53d1\u73b0\u201d\u542f\u7528\u529f\u80fd(\u63a8\u8350)\uff0cHome Assistant \u5c06\u4f1a\u901a\u8fc7\u4e0e\u5176\u8fde\u63a5\u7684 MQTT \u670d\u52a1\u5668\u4e2d\u81ea\u52a8\u641c\u5bfb\u76f8\u5173\u8bbe\u5907\u548c\u5b9e\u4f53\u3002\u5982\u679c\u201c\u53d1\u73b0\u201d\u529f\u80fd\u5173\u95ed\uff0c\u5219\u6240\u6709\u4e0e\u5176\u76f8\u5173\u7684\u914d\u7f6e\u9700\u624b\u52a8\u914d\u7f6e\u3002\n\n\u51fa\u751f\u6d88\u606f(bitrh message) - Home Assistant \u5c06\u4f1a\u5728\u6bcf\u6b21(\u91cd)\u8fde\u63a5\u65f6\u90fd\u4f1a\u53d1\u9001\u51fa\u751f\u6d88\u606f\u5230 MQTT \u670d\u52a1\u5668\n\u9057\u5631\u6d88\u606f(will message) - Home Assistant \u5c06\u4f1a\u5728\u6bcf\u6b21\u4e0e MQTT \u670d\u52a1\u5668\u5931\u53bb\u8fde\u63a5\u65f6\uff0c\u5bf9\u5176\u53d1\u9001\u9057\u5631\u4fe1\u606f\uff0c\u65e0\u8bba\u662f\u6b63\u5e38\u79bb\u7ebf(\u4f8b\u5982 Home Assistant \u6b63\u5e38\u5173\u95ed)\u6216\u975e\u6b63\u5e38\u79bb\u7ebf(\u4f8b\u5982 Home Assistant \u5d29\u6e83\u6216\u7f51\u7edc\u8fde\u63a5\u65ad\u5f00)\u3002", + "title": "MQTT \u9009\u9879" } } } diff --git a/homeassistant/components/mysensors/translations/zh-Hans.json b/homeassistant/components/mysensors/translations/zh-Hans.json new file mode 100644 index 00000000000..fa797918a5a --- /dev/null +++ b/homeassistant/components/mysensors/translations/zh-Hans.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "mqtt_required": "\u8be5 MQTT \u96c6\u6210\u672a\u914d\u7f6e" + }, + "step": { + "gw_mqtt": { + "data": { + "persistence_file": "\u6301\u4e45\u6027\u6587\u4ef6\uff08\u7559\u7a7a\u5c06\u81ea\u52a8\u751f\u6210\uff09", + "topic_in_prefix": "\u8f93\u5165\u7c7b\u578b\u524d\u7f00 (topic_in_prefix)", + "topic_out_prefix": "\u8f93\u51fa\u7c7b\u578b\u524d\u7f00 (topic_out_prefix)", + "version": "MySensor \u7248\u672c" + }, + "description": "MQTT \u7f51\u5173\u8bbe\u7f6e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nina/translations/zh-Hans.json b/homeassistant/components/nina/translations/zh-Hans.json new file mode 100644 index 00000000000..4cdfc942589 --- /dev/null +++ b/homeassistant/components/nina/translations/zh-Hans.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "corona_filter": "\u79fb\u9664\u65b0\u578b\u51a0\u72b6\u75c5\u6bd2\u8b66\u544a" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/octoprint/translations/no.json b/homeassistant/components/octoprint/translations/no.json index b7761c5370f..a94360f5bd2 100644 --- a/homeassistant/components/octoprint/translations/no.json +++ b/homeassistant/components/octoprint/translations/no.json @@ -12,7 +12,7 @@ }, "flow_title": "OctoPrint-skriver: {host}", "progress": { - "get_api_key": "\u00c5pne OctoPrint UI og klikk \"Tillat\" p\u00e5 tilgangsforesp\u00f8rselen for \"Hjemmeassistent\"." + "get_api_key": "\u00c5pne OctoPrint UI og klikk \"Tillat\" p\u00e5 tilgangsforesp\u00f8rselen for \"Home Assistant\"." }, "step": { "user": { diff --git a/homeassistant/components/onvif/translations/zh-Hans.json b/homeassistant/components/onvif/translations/zh-Hans.json index 8ebde5a1bda..1f87cc0a8cb 100644 --- a/homeassistant/components/onvif/translations/zh-Hans.json +++ b/homeassistant/components/onvif/translations/zh-Hans.json @@ -21,6 +21,7 @@ "configure": { "data": { "host": "\u4e3b\u673a\u5730\u5740", + "name": "\u540d\u79f0", "password": "\u5bc6\u7801", "port": "\u7aef\u53e3", "username": "\u7528\u6237\u540d" @@ -52,7 +53,7 @@ "data": { "auto": "\u81ea\u52a8\u641c\u7d22" }, - "description": "\u901a\u8fc7\u70b9\u51fb\u63d0\u4ea4\u6309\u94ae\uff0cHome Assistant \u5c06\u4f1a\u5c1d\u8bd5\u641c\u7d22\u60a8\u7684\u7f51\u7edc\u4e2d\u652f\u6301 Profile S \u7684 ONVIF \u8bbe\u5907\u3002\n\n\u9700\u8981\u6ce8\u610f\u7684\u662f\uff0c\u6709\u4e9b\u751f\u4ea7\u5546\u51fa\u5382\u65f6\u9ed8\u8ba4\u4f1a\u5c06 ONVIF \u529f\u80fd\u5173\u95ed\u3002\u8bf7\u786e\u8ba4\u60a8\u7684\u6444\u50cf\u5934\u5df2\u6253\u5f00\u8be5\u529f\u80fd\u3002", + "description": "\u901a\u8fc7\u70b9\u51fb\u201c\u63d0\u4ea4\u201d\u6309\u94ae\uff0cHome Assistant \u5c06\u4f1a\u5c1d\u8bd5\u641c\u7d22\u60a8\u5f53\u524d\u7684\u7f51\u7edc\u4e2d\u652f\u6301 Profile S \u7684 ONVIF \u8bbe\u5907\u3002\n\n\u9700\u8981\u6ce8\u610f\u7684\u662f\uff0c\u90e8\u5206\u751f\u4ea7\u5546\u51fa\u5382\u65f6\u9ed8\u8ba4\u4f1a\u5c06 ONVIF \u529f\u80fd\u5173\u95ed\u3002\u8bf7\u786e\u8ba4\u60a8\u7684\u6444\u50cf\u5934\u6216\u8bbe\u5907\u5df2\u6253\u5f00\u8be5\u529f\u80fd\u3002", "title": "\u914d\u7f6e ONVIF \u8bbe\u5907" } } diff --git a/homeassistant/components/powerwall/translations/zh-Hans.json b/homeassistant/components/powerwall/translations/zh-Hans.json new file mode 100644 index 00000000000..2d9db194e68 --- /dev/null +++ b/homeassistant/components/powerwall/translations/zh-Hans.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "\u5bc6\u7801\u901a\u5e38\u662f Backup Gateway \u5e8f\u5217\u53f7\u6700\u540e\u4e94\u4f4d\u5b57\u7b26\uff0c\u60a8\u53ef\u4ee5\u5728 Tesla App \u4e2d\u627e\u5230\u76f8\u5173\u5b57\u7b26\u3002\nBackup Gateway 2 \u7684\u540e\u4e94\u4f4d\u5b57\u7b26\u5bc6\u7801\u53ef\u4ee5\u5728\u95e8\u5185\u4fa7\u627e\u5230" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/nl.json b/homeassistant/components/simplisafe/translations/nl.json index 4d6df03f890..3380746db25 100644 --- a/homeassistant/components/simplisafe/translations/nl.json +++ b/homeassistant/components/simplisafe/translations/nl.json @@ -32,11 +32,12 @@ }, "user": { "data": { + "auth_code": "Autorisatie Code", "code": "Code (gebruikt in Home Assistant)", "password": "Wachtwoord", "username": "E-mail" }, - "description": "Met ingang van 2021 is SimpliSafe overgestapt op een nieuw authenticatiemechanisme via de webapp. Vanwege technische beperkingen is er een handmatige stap aan het einde van dit proces; zorg ervoor dat u de [documentatie](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) leest voordat u begint.\n\nWanneer u er klaar voor bent, klikt u op [hier]({url}) om de SimpliSafe web app te openen en uw inloggegevens in te voeren. Wanneer het proces is voltooid, gaat u hier terug en klikt u op Verzenden.", + "description": "SimpliSafe verifieert met Home Assistant via de SimpliSafe web app. Vanwege technische beperkingen is er een handmatige stap aan het einde van dit proces; zorg ervoor dat u de [documentatie]({docs_url}) leest voordat u begint.\n\n1. Klik op [hier]({url}) om de SimpliSafe web app te openen en voer uw referenties in.\n\n2. Wanneer het aanmeldingsproces is voltooid, gaat u hier terug en voert u de onderstaande autorisatiecode in.", "title": "Vul uw gegevens in" } } diff --git a/homeassistant/components/simplisafe/translations/no.json b/homeassistant/components/simplisafe/translations/no.json index 8d9b8b5df50..2ac57586f0b 100644 --- a/homeassistant/components/simplisafe/translations/no.json +++ b/homeassistant/components/simplisafe/translations/no.json @@ -37,7 +37,7 @@ "password": "Passord", "username": "E-post" }, - "description": "Fra og med 2021 har SimpliSafe flyttet til en ny godkjenningsmekanisme via nettappen. P\u00e5 grunn av tekniske begrensninger er det et manuelt trinn p\u00e5 slutten av denne prosessen; S\u00f8rg for at du leser [dokumentasjonen] (http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) f\u00f8r du starter. \n\n N\u00e5r du er klar, klikker du [her] ( {url} ) for \u00e5 \u00e5pne SimpliSafe -webappen og legge inn legitimasjonen din. N\u00e5r prosessen er fullf\u00f8rt, g\u00e5r du tilbake hit og klikker Send.", + "description": "SimpliSafe autentiserer med Home Assistant via SimpliSafe-nettappen. P\u00e5 grunn av tekniske begrensninger er det et manuelt trinn p\u00e5 slutten av denne prosessen; s\u00f8rg for at du leser [dokumentasjonen]( {docs_url} ) f\u00f8r du starter. \n\n 1. Klikk [her]( {url} ) for \u00e5 \u00e5pne SimpliSafe-nettappen og angi legitimasjonen din. \n\n 2. N\u00e5r p\u00e5loggingsprosessen er fullf\u00f8rt, g\u00e5 tilbake hit og skriv inn autorisasjonskoden nedenfor.", "title": "Fyll ut informasjonen din." } } diff --git a/homeassistant/components/simplisafe/translations/zh-Hans.json b/homeassistant/components/simplisafe/translations/zh-Hans.json index 9b0e1467d02..134eee33bfe 100644 --- a/homeassistant/components/simplisafe/translations/zh-Hans.json +++ b/homeassistant/components/simplisafe/translations/zh-Hans.json @@ -7,6 +7,7 @@ "step": { "user": { "data": { + "auth_code": "\u6388\u6743\u7801", "password": "\u5bc6\u7801", "username": "\u7535\u5b50\u90ae\u4ef6\u5730\u5740" }, diff --git a/homeassistant/components/tailscale/translations/zh-Hans.json b/homeassistant/components/tailscale/translations/zh-Hans.json new file mode 100644 index 00000000000..c60891584b7 --- /dev/null +++ b/homeassistant/components/tailscale/translations/zh-Hans.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "reauth_successful": "\u91cd\u9a8c\u8bc1\u6210\u529f" + }, + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "invalid_auth": "\u9a8c\u8bc1\u65e0\u6548" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API \u5bc6\u94a5" + }, + "description": "Tailscale API \u5bc6\u94a5\u6709\u6548\u671f\u4e3a 90 \u5929\u3002\u60a8\u53ef\u4ee5\u5728\u4e0b\u65b9\u7f51\u9875\u5730\u5740\u4e2d\u91cd\u65b0\u5efa\u7acb\u4e00\u4e2a API \u5bc6\u94a5\uff1a\nhttps://login.tailscale.com/admin/settings/authkeys." + }, + "user": { + "data": { + "api_key": "API \u5bc6\u94a5", + "tailnet": "Tailnet" + }, + "description": "Home Assistant \u4e0e Tailsacle \u8fde\u63a5\u9700\u8981\u8fdb\u884c\u8eab\u4efd\u9a8c\u8bc1\uff0c\u60a8\u9700\u8981\u5728\u4e0b\u65b9\u7f51\u9875\u5730\u5740\u4e2d\u521b\u5efa\u4e00\u4e2a API \u5bc6\u94a5\uff1a\nhttps://login.tailscale.com/admin/settings/authkeys\n\nTailnet \u662f\u60a8\u7684 Tailscale \u7f51\u7edc\u7684\u540d\u79f0\u3002\u60a8\u53ef\u4ee5\u5728 Tailscale Admin Panel (\u4e8e Tailscale \u5546\u6807\u7684\u65c1\u8fb9)\u9875\u9762\u4e2d\u5de6\u4e0a\u89d2\u627e\u5230\u5b83" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla_wall_connector/translations/zh-Hans.json b/homeassistant/components/tesla_wall_connector/translations/zh-Hans.json index 21ee02edbf0..c50e493685b 100644 --- a/homeassistant/components/tesla_wall_connector/translations/zh-Hans.json +++ b/homeassistant/components/tesla_wall_connector/translations/zh-Hans.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "\u4e3b\u673a" + "host": "\u4e3b\u673a\u5730\u5740" }, "title": "\u914d\u7f6e Tesla \u58c1\u6302\u5f0f\u5145\u7535\u8fde\u63a5\u5668" } diff --git a/homeassistant/components/tradfri/translations/zh-Hans.json b/homeassistant/components/tradfri/translations/zh-Hans.json index 85c9d4251ed..50142115451 100644 --- a/homeassistant/components/tradfri/translations/zh-Hans.json +++ b/homeassistant/components/tradfri/translations/zh-Hans.json @@ -5,6 +5,7 @@ "already_in_progress": "\u6865\u914d\u7f6e\u5df2\u5728\u8fdb\u884c\u4e2d\u3002" }, "error": { + "cannot_authenticate": "\u8fde\u63a5\u5931\u8d25\uff0c\u8bf7\u68c0\u67e5\u7f51\u5173\u662f\u5426\u4e0e\u5176\u5b83\u670d\u52a1\u5668\u5df2\u914d\u5bf9", "cannot_connect": "\u65e0\u6cd5\u8fde\u63a5\u5230\u7f51\u5173\u3002", "invalid_key": "\u65e0\u6cd5\u7528\u63d0\u4f9b\u7684\u5bc6\u94a5\u6ce8\u518c\u3002\u5982\u679c\u9519\u8bef\u6301\u7eed\u53d1\u751f\uff0c\u8bf7\u5c1d\u8bd5\u91cd\u65b0\u542f\u52a8\u7f51\u5173\u3002", "timeout": "\u4ee3\u7801\u9a8c\u8bc1\u8d85\u65f6" diff --git a/homeassistant/components/upnp/translations/zh-Hans.json b/homeassistant/components/upnp/translations/zh-Hans.json index 035514237e9..172fdc51e0b 100644 --- a/homeassistant/components/upnp/translations/zh-Hans.json +++ b/homeassistant/components/upnp/translations/zh-Hans.json @@ -4,12 +4,26 @@ "already_configured": "UPnP/IGD \u5df2\u914d\u7f6e\u5b8c\u6210", "no_devices_found": "\u6ca1\u6709\u5728\u7f51\u7edc\u4e0a\u627e\u5230 UPnP/IGD \u8bbe\u5907\u3002" }, + "flow_title": "{name}", "step": { + "ssdp_confirm": { + "description": "\u662f\u5426\u9700\u8981\u914d\u7f6e\u8be5 UPnP/IGD \u8bbe\u5907\uff1f" + }, "user": { "data": { + "unique_id": "\u8bbe\u5907", "usn": "\u8bbe\u5907" } } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u66f4\u65b0\u95f4\u9694\uff08\u5355\u4f4d\uff1a\u79d2\uff0c\u6700\u77ed 30\u79d2\uff09" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/sensor.en.json b/homeassistant/components/wolflink/translations/sensor.en.json index bd505e845ae..eeb940d81fe 100644 --- a/homeassistant/components/wolflink/translations/sensor.en.json +++ b/homeassistant/components/wolflink/translations/sensor.en.json @@ -65,7 +65,7 @@ "sparen": "Economy", "spreizung_hoch": "dT too wide", "spreizung_kf": "Spread KF", - "stabilisierung": "Stablization", + "stabilisierung": "Stabilization", "standby": "Standby", "start": "Start", "storung": "Fault", diff --git a/homeassistant/components/yamaha_musiccast/translations/select.nl.json b/homeassistant/components/yamaha_musiccast/translations/select.nl.json new file mode 100644 index 00000000000..05dc828c0f4 --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/select.nl.json @@ -0,0 +1,52 @@ +{ + "state": { + "yamaha_musiccast__dimmer": { + "auto": "Auto" + }, + "yamaha_musiccast__zone_equalizer_mode": { + "auto": "Auto", + "bypass": "Omzeilen", + "manual": "Handmatig" + }, + "yamaha_musiccast__zone_link_audio_delay": { + "audio_sync": "Audiosynchronisatie", + "audio_sync_off": "Audiosynchronisatie Uit", + "audio_sync_on": "Audiosynchronisatie Aan", + "balanced": "Gebalanceerd", + "lip_sync": "Lipsynchronisatie" + }, + "yamaha_musiccast__zone_link_audio_quality": { + "compressed": "Gecomprimeerd", + "uncompressed": "Ongecomprimeerd" + }, + "yamaha_musiccast__zone_link_control": { + "speed": "Snelheid", + "stability": "Stabiliteit", + "standard": "Standaard" + }, + "yamaha_musiccast__zone_sleep": { + "120 min": "120 minuten", + "30 min": "30 minuten", + "60 min": "60 minuten", + "90 min": "90 minuten", + "off": "Uit" + }, + "yamaha_musiccast__zone_surr_decoder_type": { + "auto": "Auto", + "dolby_pl": "Dolby ProLogic", + "dolby_pl2x_game": "Dolby ProLogic 2x Game", + "dolby_pl2x_movie": "Dolby ProLogic 2x Movie", + "dolby_pl2x_music": "Dolby ProLogic 2x Music", + "dolby_surround": "Dolby Surround", + "dts_neo6_cinema": "DTS Neo:6 Cinema", + "dts_neo6_music": "DTS Neo:6 Music", + "dts_neural_x": "DTS Neural:X", + "toggle": "Toggle" + }, + "yamaha_musiccast__zone_tone_control_mode": { + "auto": "Auto", + "bypass": "Omzeilen", + "manual": "Handmatig" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/select.zh-Hans.json b/homeassistant/components/yamaha_musiccast/translations/select.zh-Hans.json new file mode 100644 index 00000000000..8d2b3bbd88c --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/select.zh-Hans.json @@ -0,0 +1,12 @@ +{ + "state": { + "yamaha_musiccast__zone_sleep": { + "90 min": "90 \u5206\u949f", + "off": "\u5173" + }, + "yamaha_musiccast__zone_surr_decoder_type": { + "auto": "\u81ea\u52a8", + "toggle": "\u5207\u6362" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/nl.json b/homeassistant/components/zwave_js/translations/nl.json index 76718aa5346..1f99373c18f 100644 --- a/homeassistant/components/zwave_js/translations/nl.json +++ b/homeassistant/components/zwave_js/translations/nl.json @@ -33,6 +33,7 @@ "s2_unauthenticated_key": "S2 niet-geverifieerde sleutel", "usb_path": "USB-apparaatpad" }, + "description": "De add-on genereert beveiligingssleutels als deze velden leeg worden gelaten.", "title": "Voer de Z-Wave JS add-on configuratie in" }, "hassio_confirm": { @@ -119,6 +120,7 @@ "s2_unauthenticated_key": "S2 niet-geverifieerde sleutel", "usb_path": "USB-apparaatpad" }, + "description": "De add-on genereert beveiligingssleutels als deze velden leeg worden gelaten.", "title": "Voer de configuratie van de Z-Wave JS-add-on in" }, "install_addon": { From 89a6640b82a39602142cc1cc9ebb1fa057a172c9 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 14 Dec 2021 01:23:32 +0100 Subject: [PATCH 0398/2644] Blacklist availability check for a light at startup in Hue integration (#61737) --- homeassistant/components/hue/v2/entity.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/v2/entity.py b/homeassistant/components/hue/v2/entity.py index 68c427fd3a5..ae345238c23 100644 --- a/homeassistant/components/hue/v2/entity.py +++ b/homeassistant/components/hue/v2/entity.py @@ -47,6 +47,20 @@ class HueBaseEntity(Entity): self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self.device.id)}, ) + # some (3th party) Hue lights report their connection status incorrectly + # causing the zigbee availability to report as disconnected while in fact + # it can be controlled. Although this is in fact something the device manufacturer + # should fix, we work around it here. If the light is reported unavailable at + # startup, we ignore the availability status of the zigbee connection + self._ignore_availability = False + if self.device is None: + return + if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id): + self._ignore_availability = ( + # Official Hue lights are reliable + self.device.product_data.manufacturer_name != "Signify Netherlands B.V." + and zigbee.status != ConnectivityServiceStatus.CONNECTED + ) @property def name(self) -> str: @@ -98,13 +112,12 @@ class HueBaseEntity(Entity): def available(self) -> bool: """Return entity availability.""" if self.device is None: - # devices without a device attached should be always available + # entities without a device attached should be always available return True if self.resource.type == ResourceTypes.ZIGBEE_CONNECTIVITY: # the zigbee connectivity sensor itself should be always available return True - if self.device.product_data.manufacturer_name != "Signify Netherlands B.V.": - # availability status for non-philips brand lights is unreliable + if self._ignore_availability: return True if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id): # all device-attached entities get availability from the zigbee connectivity From 438d19f72bc6057c38a315d546d35e8bb664513a Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 14 Dec 2021 01:27:58 +0100 Subject: [PATCH 0399/2644] Fix Flash effect for Hue lights (#61733) --- homeassistant/components/hue/v2/group.py | 8 ++++++++ homeassistant/components/hue/v2/light.py | 9 +++++++++ tests/components/hue/test_light_v2.py | 11 +++++++++++ 3 files changed, 28 insertions(+) diff --git a/homeassistant/components/hue/v2/group.py b/homeassistant/components/hue/v2/group.py index 08f1dc72325..4427d4cb415 100644 --- a/homeassistant/components/hue/v2/group.py +++ b/homeassistant/components/hue/v2/group.py @@ -6,16 +6,19 @@ from typing import Any from aiohue.v2 import HueBridgeV2 from aiohue.v2.controllers.events import EventType from aiohue.v2.controllers.groups import GroupedLight, Room, Zone +from aiohue.v2.models.feature import AlertEffectType from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, + ATTR_FLASH, ATTR_TRANSITION, ATTR_XY_COLOR, COLOR_MODE_BRIGHTNESS, COLOR_MODE_COLOR_TEMP, COLOR_MODE_ONOFF, COLOR_MODE_XY, + SUPPORT_FLASH, SUPPORT_TRANSITION, LightEntity, ) @@ -32,6 +35,7 @@ ALLOWED_ERRORS = [ 'device (groupedLight) is "soft off", command (on) may not have effect', "device (light) has communication issues, command (on) may not have effect", 'device (light) is "soft off", command (on) may not have effect', + "attribute (supportedAlertActions) cannot be written", ] @@ -88,6 +92,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity): self.group = group self.controller = controller self.api: HueBridgeV2 = bridge.api + self._attr_supported_features |= SUPPORT_FLASH self._attr_supported_features |= SUPPORT_TRANSITION # Entities for Hue groups are disabled by default @@ -146,6 +151,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity): xy_color = kwargs.get(ATTR_XY_COLOR) color_temp = kwargs.get(ATTR_COLOR_TEMP) brightness = kwargs.get(ATTR_BRIGHTNESS) + flash = kwargs.get(ATTR_FLASH) if brightness is not None: # Hue uses a range of [0, 100] to control brightness. brightness = float((brightness / 255) * 100) @@ -160,6 +166,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity): and xy_color is None and color_temp is None and transition is None + and flash is None ): await self.bridge.async_request_call( self.controller.set_state, @@ -180,6 +187,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity): color_xy=xy_color if light.supports_color else None, color_temp=color_temp if light.supports_color_temperature else None, transition_time=transition, + alert=AlertEffectType.BREATHE if flash is not None else None, allowed_errors=ALLOWED_ERRORS, ) diff --git a/homeassistant/components/hue/v2/light.py b/homeassistant/components/hue/v2/light.py index de5388e1220..afb4c3d88bd 100644 --- a/homeassistant/components/hue/v2/light.py +++ b/homeassistant/components/hue/v2/light.py @@ -6,17 +6,20 @@ from typing import Any from aiohue import HueBridgeV2 from aiohue.v2.controllers.events import EventType from aiohue.v2.controllers.lights import LightsController +from aiohue.v2.models.feature import AlertEffectType from aiohue.v2.models.light import Light from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, + ATTR_FLASH, ATTR_TRANSITION, ATTR_XY_COLOR, COLOR_MODE_BRIGHTNESS, COLOR_MODE_COLOR_TEMP, COLOR_MODE_ONOFF, COLOR_MODE_XY, + SUPPORT_FLASH, SUPPORT_TRANSITION, LightEntity, ) @@ -31,6 +34,7 @@ from .entity import HueBaseEntity ALLOWED_ERRORS = [ "device (light) has communication issues, command (on) may not have effect", 'device (light) is "soft off", command (on) may not have effect', + "attribute (supportedAlertActions) cannot be written", ] @@ -68,6 +72,7 @@ class HueLight(HueBaseEntity, LightEntity): ) -> None: """Initialize the light.""" super().__init__(bridge, controller, resource) + self._attr_supported_features |= SUPPORT_FLASH self.resource = resource self.controller = controller self._supported_color_modes = set() @@ -154,6 +159,7 @@ class HueLight(HueBaseEntity, LightEntity): xy_color = kwargs.get(ATTR_XY_COLOR) color_temp = kwargs.get(ATTR_COLOR_TEMP) brightness = kwargs.get(ATTR_BRIGHTNESS) + flash = kwargs.get(ATTR_FLASH) if brightness is not None: # Hue uses a range of [0, 100] to control brightness. brightness = float((brightness / 255) * 100) @@ -169,12 +175,14 @@ class HueLight(HueBaseEntity, LightEntity): color_xy=xy_color, color_temp=color_temp, transition_time=transition, + alert=AlertEffectType.BREATHE if flash is not None else None, allowed_errors=ALLOWED_ERRORS, ) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" transition = kwargs.get(ATTR_TRANSITION) + flash = kwargs.get(ATTR_FLASH) if transition is not None: # hue transition duration is in milliseconds transition = int(transition * 1000) @@ -183,5 +191,6 @@ class HueLight(HueBaseEntity, LightEntity): id=self.resource.id, on=False, transition_time=transition, + alert=AlertEffectType.BREATHE if flash is not None else None, allowed_errors=ALLOWED_ERRORS, ) diff --git a/tests/components/hue/test_light_v2.py b/tests/components/hue/test_light_v2.py index 362b7076a92..7a51f833207 100644 --- a/tests/components/hue/test_light_v2.py +++ b/tests/components/hue/test_light_v2.py @@ -121,6 +121,17 @@ async def test_light_turn_on_service(hass, mock_bridge_v2, v2_resources_test_dat assert mock_bridge_v2.mock_requests[1]["json"]["on"]["on"] is True assert mock_bridge_v2.mock_requests[1]["json"]["dynamics"]["duration"] == 6000 + # test again with sending flash/alert + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": test_light_id, "flash": "long"}, + blocking=True, + ) + assert len(mock_bridge_v2.mock_requests) == 3 + assert mock_bridge_v2.mock_requests[2]["json"]["on"]["on"] is True + assert mock_bridge_v2.mock_requests[2]["json"]["alert"]["action"] == "breathe" + async def test_light_turn_off_service(hass, mock_bridge_v2, v2_resources_test_data): """Test calling the turn off service on a light.""" From 228f141bfda0b9275bf55f0e0323c5c948e3436a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 14 Dec 2021 01:39:51 +0100 Subject: [PATCH 0400/2644] Upgrade tailscale to 0.1.5 (#61744) --- homeassistant/components/tailscale/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tailscale/manifest.json b/homeassistant/components/tailscale/manifest.json index 4d47e397b76..eaa51855d38 100644 --- a/homeassistant/components/tailscale/manifest.json +++ b/homeassistant/components/tailscale/manifest.json @@ -3,7 +3,7 @@ "name": "Tailscale", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tailscale", - "requirements": ["tailscale==0.1.4"], + "requirements": ["tailscale==0.1.5"], "codeowners": ["@frenck"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 825000f71b5..ca163f49aaf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2284,7 +2284,7 @@ systembridge==2.2.3 tahoma-api==0.0.16 # homeassistant.components.tailscale -tailscale==0.1.4 +tailscale==0.1.5 # homeassistant.components.tank_utility tank_utility==1.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9cff5f22d03..f011872118b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1364,7 +1364,7 @@ surepy==0.7.2 systembridge==2.2.3 # homeassistant.components.tailscale -tailscale==0.1.4 +tailscale==0.1.5 # homeassistant.components.tellduslive tellduslive==0.10.11 From 1ed6abe23d5c2ecf20df0772bb6be96329c59870 Mon Sep 17 00:00:00 2001 From: RDFurman Date: Mon, 13 Dec 2021 21:38:43 -0700 Subject: [PATCH 0401/2644] Honeywell unique id fix (#59393) * Move error logging and remove reload * Change device assignment and improve logging * Use dictionary for devices * Check if new device exists in API response * Add test and make loop better * Make test assert on error in log --- .../components/honeywell/__init__.py | 35 ++++++++++++------- homeassistant/components/honeywell/climate.py | 2 +- tests/components/honeywell/test_init.py | 21 ++++++++++- 3 files changed, 44 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/honeywell/__init__.py b/homeassistant/components/honeywell/__init__.py index 485562f8b5d..4f3f27360f8 100644 --- a/homeassistant/components/honeywell/__init__.py +++ b/homeassistant/components/honeywell/__init__.py @@ -30,14 +30,13 @@ async def async_setup_entry(hass, config): loc_id = config.data.get(CONF_LOC_ID) dev_id = config.data.get(CONF_DEV_ID) - devices = [] + devices = {} for location in client.locations_by_id.values(): - for device in location.devices_by_id.values(): - if (not loc_id or location.locationid == loc_id) and ( - not dev_id or device.deviceid == dev_id - ): - devices.append(device) + if not loc_id or location.locationid == loc_id: + for device in location.devices_by_id.values(): + if not dev_id or device.deviceid == dev_id: + devices[device.deviceid] = device if len(devices) == 0: _LOGGER.debug("No devices found") @@ -107,23 +106,30 @@ class HoneywellData: if self._client is None: return False - devices = [ + refreshed_devices = [ device for location in self._client.locations_by_id.values() for device in location.devices_by_id.values() ] - if len(devices) == 0: - _LOGGER.error("Failed to find any devices") + if len(refreshed_devices) == 0: + _LOGGER.error("Failed to find any devices after retry") return False - self.devices = devices + for updated_device in refreshed_devices: + if updated_device.deviceid in self.devices: + self.devices[updated_device.deviceid] = updated_device + else: + _LOGGER.info( + "New device with ID %s detected, reload the honeywell integration if you want to access it in Home Assistant" + ) + await self._hass.config_entries.async_reload(self._config.entry_id) return True async def _refresh_devices(self): """Refresh each enabled device.""" - for device in self.devices: + for device in self.devices.values(): await self._hass.async_add_executor_job(device.refresh) await asyncio.sleep(UPDATE_LOOP_SLEEP_TIME) @@ -143,11 +149,16 @@ class HoneywellData: ) as exp: retries -= 1 if retries == 0: + _LOGGER.error( + "Ran out of retry attempts (3 attempts allocated). Error: %s", + exp, + ) raise exp result = await self._retry() if not result: + _LOGGER.error("Retry result was empty. Error: %s", exp) raise exp - _LOGGER.error("SomeComfort update failed, Retrying - Error: %s", exp) + _LOGGER.info("SomeComfort update failed, retrying. Error: %s", exp) diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index d2766515595..6c686e92b8e 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -122,7 +122,7 @@ async def async_setup_entry(hass, config, async_add_entities, discovery_info=Non async_add_entities( [ HoneywellUSThermostat(data, device, cool_away_temp, heat_away_temp) - for device in data.devices + for device in data.devices.values() ] ) diff --git a/tests/components/honeywell/test_init.py b/tests/components/honeywell/test_init.py index 619d770c59e..49917aae151 100644 --- a/tests/components/honeywell/test_init.py +++ b/tests/components/honeywell/test_init.py @@ -1,6 +1,8 @@ """Test honeywell setup process.""" -from unittest.mock import patch +from unittest.mock import create_autospec, patch + +import somecomfort from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant @@ -29,3 +31,20 @@ async def test_setup_multiple_thermostats( await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED assert hass.states.async_entity_ids_count() == 2 + + +@patch("homeassistant.components.honeywell.UPDATE_LOOP_SLEEP_TIME", 0) +async def test_setup_multiple_thermostats_with_same_deviceid( + hass: HomeAssistant, caplog, config_entry: MockConfigEntry, device, client +) -> None: + """Test Honeywell TCC API returning duplicate device IDs.""" + mock_location2 = create_autospec(somecomfort.Location, instance=True) + mock_location2.locationid.return_value = "location2" + mock_location2.devices_by_id = {device.deviceid: device} + client.locations_by_id["location2"] = mock_location2 + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED + assert hass.states.async_entity_ids_count() == 1 + assert "Platform honeywell does not generate unique IDs" not in caplog.text From 4a1f49852aa11ab711f1f6cd6203f56cb6065529 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 14 Dec 2021 05:39:19 +0100 Subject: [PATCH 0402/2644] Use SensorDeviceClass enum in Luftdaten (#61746) --- homeassistant/components/luftdaten/__init__.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py index 63b9965243e..9aef41434ed 100644 --- a/homeassistant/components/luftdaten/__init__.py +++ b/homeassistant/components/luftdaten/__init__.py @@ -7,7 +7,7 @@ from luftdaten import Luftdaten from luftdaten.exceptions import LuftdatenError import voluptuous as vol -from homeassistant.components.sensor import SensorEntityDescription +from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, @@ -15,9 +15,6 @@ from homeassistant.const import ( CONF_SCAN_INTERVAL, CONF_SENSORS, CONF_SHOW_ON_MAP, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, PERCENTAGE, PRESSURE_PA, TEMP_CELSIUS, @@ -55,28 +52,28 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key=SENSOR_TEMPERATURE, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( key=SENSOR_HUMIDITY, name="Humidity", icon="mdi:water-percent", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=SENSOR_PRESSURE, name="Pressure", icon="mdi:arrow-down-bold", native_unit_of_measurement=PRESSURE_PA, - device_class=DEVICE_CLASS_PRESSURE, + device_class=SensorDeviceClass.PRESSURE, ), SensorEntityDescription( key=SENSOR_PRESSURE_AT_SEALEVEL, name="Pressure at sealevel", icon="mdi:download", native_unit_of_measurement=PRESSURE_PA, - device_class=DEVICE_CLASS_PRESSURE, + device_class=SensorDeviceClass.PRESSURE, ), SensorEntityDescription( key=SENSOR_PM10, From de4514475087ac7061e1ffdf7b6d8b89617a3eea Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 14 Dec 2021 09:21:12 +0100 Subject: [PATCH 0403/2644] Remove deprecated YAML configuration from Luftdaten (#61748) --- .../components/luftdaten/__init__.py | 68 +++---------------- .../components/luftdaten/config_flow.py | 4 -- .../components/luftdaten/test_config_flow.py | 21 ------ tests/components/luftdaten/test_init.py | 39 ----------- 4 files changed, 9 insertions(+), 123 deletions(-) delete mode 100644 tests/components/luftdaten/test_init.py diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py index 9aef41434ed..f525bcb69bc 100644 --- a/homeassistant/components/luftdaten/__init__.py +++ b/homeassistant/components/luftdaten/__init__.py @@ -5,7 +5,6 @@ import logging from luftdaten import Luftdaten from luftdaten.exceptions import LuftdatenError -import voluptuous as vol from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription from homeassistant.config_entries import SOURCE_IMPORT @@ -14,7 +13,6 @@ from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_SCAN_INTERVAL, CONF_SENSORS, - CONF_SHOW_ON_MAP, PERCENTAGE, PRESSURE_PA, TEMP_CELSIUS, @@ -26,7 +24,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval -from .config_flow import configured_sensors, duplicate_stations +from .config_flow import duplicate_stations from .const import CONF_SENSOR_ID, DEFAULT_SCAN_INTERVAL, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -90,32 +88,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ) SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] -SENSOR_SCHEMA = vol.Schema( - { - vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_KEYS): vol.All( - cv.ensure_list, [vol.In(SENSOR_KEYS)] - ) - } -) - -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_SENSOR_ID): cv.positive_int, - vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA, - vol.Optional(CONF_SHOW_ON_MAP, default=False): cv.boolean, - vol.Optional( - CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL - ): cv.time_period, - } - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) @callback @@ -125,38 +98,15 @@ def _async_fixup_sensor_id(hass, config_entry, sensor_id): ) -async def async_setup(hass, config): - """Set up the Luftdaten component.""" - hass.data[DOMAIN] = {} - hass.data[DOMAIN][DATA_LUFTDATEN_CLIENT] = {} - hass.data[DOMAIN][DATA_LUFTDATEN_LISTENER] = {} - - if DOMAIN not in config: - return True - - conf = config[DOMAIN] - station_id = conf[CONF_SENSOR_ID] - - if station_id not in configured_sensors(hass): - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_SENSORS: conf[CONF_SENSORS], - CONF_SENSOR_ID: conf[CONF_SENSOR_ID], - CONF_SHOW_ON_MAP: conf[CONF_SHOW_ON_MAP], - }, - ) - ) - - hass.data[DOMAIN][CONF_SCAN_INTERVAL] = conf[CONF_SCAN_INTERVAL] - - return True - - async def async_setup_entry(hass, config_entry): """Set up Luftdaten as config entry.""" + hass.data.setdefault( + DOMAIN, + { + DATA_LUFTDATEN_CLIENT: {}, + DATA_LUFTDATEN_LISTENER: {}, + }, + ) if not isinstance(config_entry.data[CONF_SENSOR_ID], int): _async_fixup_sensor_id(hass, config_entry, config_entry.data[CONF_SENSOR_ID]) diff --git a/homeassistant/components/luftdaten/config_flow.py b/homeassistant/components/luftdaten/config_flow.py index 56dee86e9fb..b7aae271815 100644 --- a/homeassistant/components/luftdaten/config_flow.py +++ b/homeassistant/components/luftdaten/config_flow.py @@ -53,10 +53,6 @@ class LuftDatenFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=vol.Schema(data_schema), errors=errors or {} ) - async def async_step_import(self, import_config): - """Import a config entry from configuration.yaml.""" - return await self.async_step_user(import_config) - async def async_step_user(self, user_input=None): """Handle the start of the config flow.""" diff --git a/tests/components/luftdaten/test_config_flow.py b/tests/components/luftdaten/test_config_flow.py index 4ea55e26aee..ac475a8ab38 100644 --- a/tests/components/luftdaten/test_config_flow.py +++ b/tests/components/luftdaten/test_config_flow.py @@ -59,27 +59,6 @@ async def test_show_form(hass): assert result["step_id"] == "user" -async def test_step_import(hass): - """Test that the import step works.""" - conf = {CONF_SENSOR_ID: "12345abcde", CONF_SHOW_ON_MAP: False} - - flow = config_flow.LuftDatenFlowHandler() - flow.hass = hass - - with patch("luftdaten.Luftdaten.get_data", return_value=True), patch( - "luftdaten.Luftdaten.validate_sensor", return_value=True - ): - result = await flow.async_step_import(import_config=conf) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "12345abcde" - assert result["data"] == { - CONF_SENSOR_ID: "12345abcde", - CONF_SHOW_ON_MAP: False, - CONF_SCAN_INTERVAL: 600, - } - - async def test_step_user(hass): """Test that the user step works.""" conf = { diff --git a/tests/components/luftdaten/test_init.py b/tests/components/luftdaten/test_init.py deleted file mode 100644 index ebe5f73669e..00000000000 --- a/tests/components/luftdaten/test_init.py +++ /dev/null @@ -1,39 +0,0 @@ -"""Test the Luftdaten component setup.""" -from unittest.mock import patch - -from homeassistant.components import luftdaten -from homeassistant.components.luftdaten.const import CONF_SENSOR_ID, DOMAIN -from homeassistant.const import CONF_SCAN_INTERVAL, CONF_SHOW_ON_MAP -from homeassistant.setup import async_setup_component - - -async def test_config_with_sensor_passed_to_config_entry(hass): - """Test that configured options for a sensor are loaded.""" - conf = { - CONF_SENSOR_ID: "12345abcde", - CONF_SHOW_ON_MAP: False, - CONF_SCAN_INTERVAL: 600, - } - - with patch.object( - hass.config_entries.flow, "async_init" - ) as mock_config_entries, patch.object( - luftdaten, "configured_sensors", return_value=[] - ): - assert await async_setup_component(hass, DOMAIN, conf) is True - - assert len(mock_config_entries.flow.mock_calls) == 0 - - -async def test_config_already_registered_not_passed_to_config_entry(hass): - """Test that an already registered sensor does not initiate an import.""" - conf = {CONF_SENSOR_ID: "12345abcde"} - - with patch.object( - hass.config_entries.flow, "async_init" - ) as mock_config_entries, patch.object( - luftdaten, "configured_sensors", return_value=["12345abcde"] - ): - assert await async_setup_component(hass, DOMAIN, conf) is True - - assert len(mock_config_entries.flow.mock_calls) == 0 From 8fc69b72428982406aefa987d77b3bbf86d7927e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 14 Dec 2021 09:56:52 +0100 Subject: [PATCH 0404/2644] Use new SwitchDeviceClass enum in home-plus-control (#61760) Co-authored-by: epenet --- homeassistant/components/home_plus_control/switch.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/home_plus_control/switch.py b/homeassistant/components/home_plus_control/switch.py index 809a246e631..73fd515e65b 100644 --- a/homeassistant/components/home_plus_control/switch.py +++ b/homeassistant/components/home_plus_control/switch.py @@ -1,11 +1,7 @@ """Legrand Home+ Control Switch Entity Module that uses the HomeAssistant DataUpdateCoordinator.""" from functools import partial -from homeassistant.components.switch import ( - DEVICE_CLASS_OUTLET, - DEVICE_CLASS_SWITCH, - SwitchEntity, -) +from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.core import callback from homeassistant.helpers import dispatcher from homeassistant.helpers.entity import DeviceInfo @@ -97,8 +93,8 @@ class HomeControlSwitchEntity(CoordinatorEntity, SwitchEntity): def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" if self.module.device == "plug": - return DEVICE_CLASS_OUTLET - return DEVICE_CLASS_SWITCH + return SwitchDeviceClass.OUTLET + return SwitchDeviceClass.SWITCH @property def available(self) -> bool: From b7c0b21c6c43903fb01a9375b868d50453df5b57 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 14 Dec 2021 10:53:47 +0100 Subject: [PATCH 0405/2644] Use new enums in homematic (#61765) Co-authored-by: Pascal Vizeli Co-authored-by: epenet --- .../components/homematic/binary_sensor.py | 38 ++++---- homeassistant/components/homematic/cover.py | 4 +- homeassistant/components/homematic/sensor.py | 90 +++++++++---------- 3 files changed, 59 insertions(+), 73 deletions(-) diff --git a/homeassistant/components/homematic/binary_sensor.py b/homeassistant/components/homematic/binary_sensor.py index b25fb2949aa..442e4c8e434 100644 --- a/homeassistant/components/homematic/binary_sensor.py +++ b/homeassistant/components/homematic/binary_sensor.py @@ -1,10 +1,6 @@ """Support for HomeMatic binary sensors.""" from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_MOTION, - DEVICE_CLASS_OPENING, - DEVICE_CLASS_PRESENCE, - DEVICE_CLASS_SMOKE, + BinarySensorDeviceClass, BinarySensorEntity, ) @@ -12,24 +8,24 @@ from .const import ATTR_DISCOVER_DEVICES, ATTR_DISCOVERY_TYPE, DISCOVER_BATTERY from .entity import HMDevice SENSOR_TYPES_CLASS = { - "IPShutterContact": DEVICE_CLASS_OPENING, - "IPShutterContactSabotage": DEVICE_CLASS_OPENING, - "MaxShutterContact": DEVICE_CLASS_OPENING, - "Motion": DEVICE_CLASS_MOTION, - "MotionV2": DEVICE_CLASS_MOTION, - "PresenceIP": DEVICE_CLASS_PRESENCE, + "IPShutterContact": BinarySensorDeviceClass.OPENING, + "IPShutterContactSabotage": BinarySensorDeviceClass.OPENING, + "MaxShutterContact": BinarySensorDeviceClass.OPENING, + "Motion": BinarySensorDeviceClass.MOTION, + "MotionV2": BinarySensorDeviceClass.MOTION, + "PresenceIP": BinarySensorDeviceClass.MOTION, "Remote": None, "RemoteMotion": None, - "ShutterContact": DEVICE_CLASS_OPENING, - "Smoke": DEVICE_CLASS_SMOKE, - "SmokeV2": DEVICE_CLASS_SMOKE, + "ShutterContact": BinarySensorDeviceClass.OPENING, + "Smoke": BinarySensorDeviceClass.SMOKE, + "SmokeV2": BinarySensorDeviceClass.SMOKE, "TiltSensor": None, "WeatherSensor": None, - "IPContact": DEVICE_CLASS_OPENING, - "MotionIP": DEVICE_CLASS_MOTION, - "MotionIPV2": DEVICE_CLASS_MOTION, - "MotionIPContactSabotage": DEVICE_CLASS_MOTION, - "IPRemoteMotionV2": DEVICE_CLASS_MOTION, + "IPContact": BinarySensorDeviceClass.OPENING, + "MotionIP": BinarySensorDeviceClass.MOTION, + "MotionIPV2": BinarySensorDeviceClass.MOTION, + "MotionIPContactSabotage": BinarySensorDeviceClass.MOTION, + "IPRemoteMotionV2": BinarySensorDeviceClass.MOTION, } @@ -63,7 +59,7 @@ class HMBinarySensor(HMDevice, BinarySensorEntity): """Return the class of this sensor from DEVICE_CLASSES.""" # If state is MOTION (Only RemoteMotion working) if self._state == "MOTION": - return DEVICE_CLASS_MOTION + return BinarySensorDeviceClass.MOTION return SENSOR_TYPES_CLASS.get(self._hmdevice.__class__.__name__) def _init_data_struct(self): @@ -76,7 +72,7 @@ class HMBinarySensor(HMDevice, BinarySensorEntity): class HMBatterySensor(HMDevice, BinarySensorEntity): """Representation of an HomeMatic low battery sensor.""" - _attr_device_class = DEVICE_CLASS_BATTERY + _attr_device_class = BinarySensorDeviceClass.BATTERY @property def is_on(self): diff --git a/homeassistant/components/homematic/cover.py b/homeassistant/components/homematic/cover.py index deed671931f..b92c3e5b4d7 100644 --- a/homeassistant/components/homematic/cover.py +++ b/homeassistant/components/homematic/cover.py @@ -2,7 +2,7 @@ from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, - DEVICE_CLASS_GARAGE, + CoverDeviceClass, CoverEntity, ) @@ -112,7 +112,7 @@ class HMCover(HMDevice, CoverEntity): class HMGarage(HMCover): """Represents a Homematic Garage cover. Homematic garage covers do not support position attributes.""" - _attr_device_class = DEVICE_CLASS_GARAGE + _attr_device_class = CoverDeviceClass.GARAGE @property def current_cover_position(self): diff --git a/homeassistant/components/homematic/sensor.py b/homeassistant/components/homematic/sensor.py index 19b24fbb3f8..3585510beb7 100644 --- a/homeassistant/components/homematic/sensor.py +++ b/homeassistant/components/homematic/sensor.py @@ -5,25 +5,15 @@ from copy import copy import logging from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.const import ( ATTR_NAME, CONCENTRATION_PARTS_PER_MILLION, DEGREE, - DEVICE_CLASS_CO2, - DEVICE_CLASS_CURRENT, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_GAS, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_POWER, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_VOLTAGE, ELECTRIC_CURRENT_MILLIAMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_WATT_HOUR, @@ -64,110 +54,110 @@ SENSOR_DESCRIPTIONS: dict[str, SensorEntityDescription] = { "HUMIDITY": SensorEntityDescription( key="HUMIDITY", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), "ACTUAL_TEMPERATURE": SensorEntityDescription( key="ACTUAL_TEMPERATURE", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), "TEMPERATURE": SensorEntityDescription( key="TEMPERATURE", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), "LUX": SensorEntityDescription( key="LUX", native_unit_of_measurement=LIGHT_LUX, - device_class=DEVICE_CLASS_ILLUMINANCE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, ), "CURRENT_ILLUMINATION": SensorEntityDescription( key="CURRENT_ILLUMINATION", native_unit_of_measurement=LIGHT_LUX, - device_class=DEVICE_CLASS_ILLUMINANCE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, ), "ILLUMINATION": SensorEntityDescription( key="ILLUMINATION", native_unit_of_measurement=LIGHT_LUX, - device_class=DEVICE_CLASS_ILLUMINANCE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, ), "AVERAGE_ILLUMINATION": SensorEntityDescription( key="AVERAGE_ILLUMINATION", native_unit_of_measurement=LIGHT_LUX, - device_class=DEVICE_CLASS_ILLUMINANCE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, ), "LOWEST_ILLUMINATION": SensorEntityDescription( key="LOWEST_ILLUMINATION", native_unit_of_measurement=LIGHT_LUX, - device_class=DEVICE_CLASS_ILLUMINANCE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, ), "HIGHEST_ILLUMINATION": SensorEntityDescription( key="HIGHEST_ILLUMINATION", native_unit_of_measurement=LIGHT_LUX, - device_class=DEVICE_CLASS_ILLUMINANCE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, ), "POWER": SensorEntityDescription( key="POWER", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), "IEC_POWER": SensorEntityDescription( key="IEC_POWER", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), "CURRENT": SensorEntityDescription( key="CURRENT", native_unit_of_measurement=ELECTRIC_CURRENT_MILLIAMPERE, - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, ), "CONCENTRATION": SensorEntityDescription( key="CONCENTRATION", native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, - device_class=DEVICE_CLASS_CO2, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CO2, + state_class=SensorStateClass.MEASUREMENT, ), "ENERGY_COUNTER": SensorEntityDescription( key="ENERGY_COUNTER", native_unit_of_measurement=ENERGY_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), "IEC_ENERGY_COUNTER": SensorEntityDescription( key="IEC_ENERGY_COUNTER", native_unit_of_measurement=ENERGY_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), "VOLTAGE": SensorEntityDescription( key="VOLTAGE", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, ), "GAS_POWER": SensorEntityDescription( key="GAS_POWER", native_unit_of_measurement=VOLUME_CUBIC_METERS, - device_class=DEVICE_CLASS_GAS, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.GAS, + state_class=SensorStateClass.MEASUREMENT, ), "GAS_ENERGY_COUNTER": SensorEntityDescription( key="GAS_ENERGY_COUNTER", native_unit_of_measurement=VOLUME_CUBIC_METERS, - device_class=DEVICE_CLASS_GAS, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.GAS, + state_class=SensorStateClass.TOTAL_INCREASING, ), "RAIN_COUNTER": SensorEntityDescription( key="RAIN_COUNTER", @@ -193,8 +183,8 @@ SENSOR_DESCRIPTIONS: dict[str, SensorEntityDescription] = { "AIR_PRESSURE": SensorEntityDescription( key="AIR_PRESSURE", native_unit_of_measurement=PRESSURE_HPA, - device_class=DEVICE_CLASS_PRESSURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, ), "FREQUENCY": SensorEntityDescription( key="FREQUENCY", From eddc1ae0ed3fa260e13623eebe42eb274f247a1c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 14 Dec 2021 10:54:47 +0100 Subject: [PATCH 0406/2644] Use new enums in huisbaasje (#61776) Co-authored-by: epenet --- homeassistant/components/huisbaasje/const.py | 61 ++++++++----------- homeassistant/components/huisbaasje/sensor.py | 4 +- tests/components/huisbaasje/test_sensor.py | 54 +++++++++------- 3 files changed, 60 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/huisbaasje/const.py b/homeassistant/components/huisbaasje/const.py index e989b7949e2..637ebd03a17 100644 --- a/homeassistant/components/huisbaasje/const.py +++ b/homeassistant/components/huisbaasje/const.py @@ -8,15 +8,8 @@ from huisbaasje.const import ( SOURCE_TYPE_GAS, ) -from homeassistant.components.sensor import STATE_CLASS_TOTAL_INCREASING -from homeassistant.const import ( - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_GAS, - DEVICE_CLASS_POWER, - ENERGY_KILO_WATT_HOUR, - TIME_HOURS, - VOLUME_CUBIC_METERS, -) +from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass +from homeassistant.const import ENERGY_KILO_WATT_HOUR, TIME_HOURS, VOLUME_CUBIC_METERS DATA_COORDINATOR = "coordinator" @@ -48,68 +41,68 @@ SOURCE_TYPES = [ SENSORS_INFO = [ { "name": "Huisbaasje Current Power", - "device_class": DEVICE_CLASS_POWER, + "device_class": SensorDeviceClass.POWER, "source_type": SOURCE_TYPE_ELECTRICITY, }, { "name": "Huisbaasje Current Power In Peak", - "device_class": DEVICE_CLASS_POWER, + "device_class": SensorDeviceClass.POWER, "source_type": SOURCE_TYPE_ELECTRICITY_IN, }, { "name": "Huisbaasje Current Power In Off Peak", - "device_class": DEVICE_CLASS_POWER, + "device_class": SensorDeviceClass.POWER, "source_type": SOURCE_TYPE_ELECTRICITY_IN_LOW, }, { "name": "Huisbaasje Current Power Out Peak", - "device_class": DEVICE_CLASS_POWER, + "device_class": SensorDeviceClass.POWER, "source_type": SOURCE_TYPE_ELECTRICITY_OUT, }, { "name": "Huisbaasje Current Power Out Off Peak", - "device_class": DEVICE_CLASS_POWER, + "device_class": SensorDeviceClass.POWER, "source_type": SOURCE_TYPE_ELECTRICITY_OUT_LOW, }, { "name": "Huisbaasje Energy Consumption Peak Today", - "device_class": DEVICE_CLASS_ENERGY, + "device_class": SensorDeviceClass.ENERGY, "unit_of_measurement": ENERGY_KILO_WATT_HOUR, "source_type": SOURCE_TYPE_ELECTRICITY_IN, "sensor_type": SENSOR_TYPE_THIS_DAY, - "state_class": STATE_CLASS_TOTAL_INCREASING, + "state_class": SensorStateClass.TOTAL_INCREASING, "precision": 3, }, { "name": "Huisbaasje Energy Consumption Off Peak Today", - "device_class": DEVICE_CLASS_ENERGY, + "device_class": SensorDeviceClass.ENERGY, "unit_of_measurement": ENERGY_KILO_WATT_HOUR, "source_type": SOURCE_TYPE_ELECTRICITY_IN_LOW, "sensor_type": SENSOR_TYPE_THIS_DAY, - "state_class": STATE_CLASS_TOTAL_INCREASING, + "state_class": SensorStateClass.TOTAL_INCREASING, "precision": 3, }, { "name": "Huisbaasje Energy Production Peak Today", - "device_class": DEVICE_CLASS_ENERGY, + "device_class": SensorDeviceClass.ENERGY, "unit_of_measurement": ENERGY_KILO_WATT_HOUR, "source_type": SOURCE_TYPE_ELECTRICITY_OUT, "sensor_type": SENSOR_TYPE_THIS_DAY, - "state_class": STATE_CLASS_TOTAL_INCREASING, + "state_class": SensorStateClass.TOTAL_INCREASING, "precision": 3, }, { "name": "Huisbaasje Energy Production Off Peak Today", - "device_class": DEVICE_CLASS_ENERGY, + "device_class": SensorDeviceClass.ENERGY, "unit_of_measurement": ENERGY_KILO_WATT_HOUR, "source_type": SOURCE_TYPE_ELECTRICITY_OUT_LOW, "sensor_type": SENSOR_TYPE_THIS_DAY, - "state_class": STATE_CLASS_TOTAL_INCREASING, + "state_class": SensorStateClass.TOTAL_INCREASING, "precision": 3, }, { "name": "Huisbaasje Energy Today", - "device_class": DEVICE_CLASS_ENERGY, + "device_class": SensorDeviceClass.ENERGY, "unit_of_measurement": ENERGY_KILO_WATT_HOUR, "source_type": SOURCE_TYPE_ELECTRICITY, "sensor_type": SENSOR_TYPE_THIS_DAY, @@ -117,7 +110,7 @@ SENSORS_INFO = [ }, { "name": "Huisbaasje Energy This Week", - "device_class": DEVICE_CLASS_ENERGY, + "device_class": SensorDeviceClass.ENERGY, "unit_of_measurement": ENERGY_KILO_WATT_HOUR, "source_type": SOURCE_TYPE_ELECTRICITY, "sensor_type": SENSOR_TYPE_THIS_WEEK, @@ -125,7 +118,7 @@ SENSORS_INFO = [ }, { "name": "Huisbaasje Energy This Month", - "device_class": DEVICE_CLASS_ENERGY, + "device_class": SensorDeviceClass.ENERGY, "unit_of_measurement": ENERGY_KILO_WATT_HOUR, "source_type": SOURCE_TYPE_ELECTRICITY, "sensor_type": SENSOR_TYPE_THIS_MONTH, @@ -133,7 +126,7 @@ SENSORS_INFO = [ }, { "name": "Huisbaasje Energy This Year", - "device_class": DEVICE_CLASS_ENERGY, + "device_class": SensorDeviceClass.ENERGY, "unit_of_measurement": ENERGY_KILO_WATT_HOUR, "source_type": SOURCE_TYPE_ELECTRICITY, "sensor_type": SENSOR_TYPE_THIS_YEAR, @@ -148,41 +141,41 @@ SENSORS_INFO = [ }, { "name": "Huisbaasje Gas Today", - "device_class": DEVICE_CLASS_GAS, + "device_class": SensorDeviceClass.GAS, "unit_of_measurement": VOLUME_CUBIC_METERS, "source_type": SOURCE_TYPE_GAS, "sensor_type": SENSOR_TYPE_THIS_DAY, - "state_class": STATE_CLASS_TOTAL_INCREASING, + "state_class": SensorStateClass.TOTAL_INCREASING, "icon": "mdi:counter", "precision": 1, }, { "name": "Huisbaasje Gas This Week", - "device_class": DEVICE_CLASS_GAS, + "device_class": SensorDeviceClass.GAS, "unit_of_measurement": VOLUME_CUBIC_METERS, "source_type": SOURCE_TYPE_GAS, "sensor_type": SENSOR_TYPE_THIS_WEEK, - "state_class": STATE_CLASS_TOTAL_INCREASING, + "state_class": SensorStateClass.TOTAL_INCREASING, "icon": "mdi:counter", "precision": 1, }, { "name": "Huisbaasje Gas This Month", - "device_class": DEVICE_CLASS_GAS, + "device_class": SensorDeviceClass.GAS, "unit_of_measurement": VOLUME_CUBIC_METERS, "source_type": SOURCE_TYPE_GAS, "sensor_type": SENSOR_TYPE_THIS_MONTH, - "state_class": STATE_CLASS_TOTAL_INCREASING, + "state_class": SensorStateClass.TOTAL_INCREASING, "icon": "mdi:counter", "precision": 1, }, { "name": "Huisbaasje Gas This Year", - "device_class": DEVICE_CLASS_GAS, + "device_class": SensorDeviceClass.GAS, "unit_of_measurement": VOLUME_CUBIC_METERS, "source_type": SOURCE_TYPE_GAS, "sensor_type": SENSOR_TYPE_THIS_YEAR, - "state_class": STATE_CLASS_TOTAL_INCREASING, + "state_class": SensorStateClass.TOTAL_INCREASING, "icon": "mdi:counter", "precision": 1, }, diff --git a/homeassistant/components/huisbaasje/sensor.py b/homeassistant/components/huisbaasje/sensor.py index 4ffc0048079..99d6cd0141c 100644 --- a/homeassistant/components/huisbaasje/sensor.py +++ b/homeassistant/components/huisbaasje/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity +from homeassistant.components.sensor import SensorEntity, SensorStateClass from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ID, POWER_WATT from homeassistant.core import HomeAssistant @@ -44,7 +44,7 @@ class HuisbaasjeSensor(CoordinatorEntity, SensorEntity): unit_of_measurement: str = POWER_WATT, icon: str = "mdi:lightning-bolt", precision: int = 0, - state_class: str | None = STATE_CLASS_MEASUREMENT, + state_class: str | None = SensorStateClass.MEASUREMENT, ) -> None: """Initialize the sensor.""" super().__init__(coordinator) diff --git a/tests/components/huisbaasje/test_sensor.py b/tests/components/huisbaasje/test_sensor.py index f6e7d1c4609..171c6bd25c0 100644 --- a/tests/components/huisbaasje/test_sensor.py +++ b/tests/components/huisbaasje/test_sensor.py @@ -3,11 +3,7 @@ from unittest.mock import patch from homeassistant.components import huisbaasje from homeassistant.components.huisbaasje.const import FLOW_CUBIC_METERS_PER_HOUR -from homeassistant.components.sensor import ( - ATTR_STATE_CLASS, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, -) +from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorStateClass from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ICON, @@ -64,7 +60,10 @@ async def test_setup_entry(hass: HomeAssistant): assert current_power.state == "1012.0" assert current_power.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER assert current_power.attributes.get(ATTR_ICON) == "mdi:lightning-bolt" - assert current_power.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + current_power.attributes.get(ATTR_STATE_CLASS) + is SensorStateClass.MEASUREMENT + ) assert current_power.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT current_power_in = hass.states.get("sensor.huisbaasje_current_power_in_peak") @@ -72,7 +71,8 @@ async def test_setup_entry(hass: HomeAssistant): assert current_power_in.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER assert current_power_in.attributes.get(ATTR_ICON) == "mdi:lightning-bolt" assert ( - current_power_in.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + current_power_in.attributes.get(ATTR_STATE_CLASS) + is SensorStateClass.MEASUREMENT ) assert current_power_in.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT @@ -86,7 +86,7 @@ async def test_setup_entry(hass: HomeAssistant): assert current_power_in_low.attributes.get(ATTR_ICON) == "mdi:lightning-bolt" assert ( current_power_in_low.attributes.get(ATTR_STATE_CLASS) - == STATE_CLASS_MEASUREMENT + is SensorStateClass.MEASUREMENT ) assert ( current_power_in_low.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT @@ -98,7 +98,7 @@ async def test_setup_entry(hass: HomeAssistant): assert current_power_out.attributes.get(ATTR_ICON) == "mdi:lightning-bolt" assert ( current_power_out.attributes.get(ATTR_STATE_CLASS) - == STATE_CLASS_MEASUREMENT + is SensorStateClass.MEASUREMENT ) assert current_power_out.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT @@ -113,7 +113,7 @@ async def test_setup_entry(hass: HomeAssistant): assert current_power_out_low.attributes.get(ATTR_ICON) == "mdi:lightning-bolt" assert ( current_power_out_low.attributes.get(ATTR_STATE_CLASS) - == STATE_CLASS_MEASUREMENT + is SensorStateClass.MEASUREMENT ) assert ( current_power_out_low.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT @@ -133,7 +133,7 @@ async def test_setup_entry(hass: HomeAssistant): ) assert ( energy_consumption_peak_today.attributes.get(ATTR_STATE_CLASS) - is STATE_CLASS_TOTAL_INCREASING + is SensorStateClass.TOTAL_INCREASING ) assert ( energy_consumption_peak_today.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -154,7 +154,7 @@ async def test_setup_entry(hass: HomeAssistant): ) assert ( energy_consumption_off_peak_today.attributes.get(ATTR_STATE_CLASS) - is STATE_CLASS_TOTAL_INCREASING + is SensorStateClass.TOTAL_INCREASING ) assert ( energy_consumption_off_peak_today.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -175,7 +175,7 @@ async def test_setup_entry(hass: HomeAssistant): ) assert ( energy_production_peak_today.attributes.get(ATTR_STATE_CLASS) - is STATE_CLASS_TOTAL_INCREASING + is SensorStateClass.TOTAL_INCREASING ) assert ( energy_production_peak_today.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -196,7 +196,7 @@ async def test_setup_entry(hass: HomeAssistant): ) assert ( energy_production_off_peak_today.attributes.get(ATTR_STATE_CLASS) - is STATE_CLASS_TOTAL_INCREASING + is SensorStateClass.TOTAL_INCREASING ) assert ( energy_production_off_peak_today.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -207,7 +207,10 @@ async def test_setup_entry(hass: HomeAssistant): assert energy_today.state == "3.3" assert energy_today.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY assert energy_today.attributes.get(ATTR_ICON) == "mdi:lightning-bolt" - assert energy_today.attributes.get(ATTR_STATE_CLASS) is STATE_CLASS_MEASUREMENT + assert ( + energy_today.attributes.get(ATTR_STATE_CLASS) + is SensorStateClass.MEASUREMENT + ) assert ( energy_today.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR @@ -218,7 +221,8 @@ async def test_setup_entry(hass: HomeAssistant): assert energy_this_week.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY assert energy_this_week.attributes.get(ATTR_ICON) == "mdi:lightning-bolt" assert ( - energy_this_week.attributes.get(ATTR_STATE_CLASS) is STATE_CLASS_MEASUREMENT + energy_this_week.attributes.get(ATTR_STATE_CLASS) + is SensorStateClass.MEASUREMENT ) assert ( energy_this_week.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -233,7 +237,7 @@ async def test_setup_entry(hass: HomeAssistant): assert energy_this_month.attributes.get(ATTR_ICON) == "mdi:lightning-bolt" assert ( energy_this_month.attributes.get(ATTR_STATE_CLASS) - is STATE_CLASS_MEASUREMENT + is SensorStateClass.MEASUREMENT ) assert ( energy_this_month.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -245,7 +249,8 @@ async def test_setup_entry(hass: HomeAssistant): assert energy_this_year.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY assert energy_this_year.attributes.get(ATTR_ICON) == "mdi:lightning-bolt" assert ( - energy_this_year.attributes.get(ATTR_STATE_CLASS) is STATE_CLASS_MEASUREMENT + energy_this_year.attributes.get(ATTR_STATE_CLASS) + is SensorStateClass.MEASUREMENT ) assert ( energy_this_year.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -256,7 +261,9 @@ async def test_setup_entry(hass: HomeAssistant): assert current_gas.state == "0.0" assert current_gas.attributes.get(ATTR_DEVICE_CLASS) is None assert current_gas.attributes.get(ATTR_ICON) == "mdi:fire" - assert current_gas.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + current_gas.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + ) assert ( current_gas.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == FLOW_CUBIC_METERS_PER_HOUR @@ -267,7 +274,8 @@ async def test_setup_entry(hass: HomeAssistant): assert gas_today.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_GAS assert gas_today.attributes.get(ATTR_ICON) == "mdi:counter" assert ( - gas_today.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + gas_today.attributes.get(ATTR_STATE_CLASS) + is SensorStateClass.TOTAL_INCREASING ) assert gas_today.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS @@ -277,7 +285,7 @@ async def test_setup_entry(hass: HomeAssistant): assert gas_this_week.attributes.get(ATTR_ICON) == "mdi:counter" assert ( gas_this_week.attributes.get(ATTR_STATE_CLASS) - is STATE_CLASS_TOTAL_INCREASING + is SensorStateClass.TOTAL_INCREASING ) assert ( gas_this_week.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -290,7 +298,7 @@ async def test_setup_entry(hass: HomeAssistant): assert gas_this_month.attributes.get(ATTR_ICON) == "mdi:counter" assert ( gas_this_month.attributes.get(ATTR_STATE_CLASS) - is STATE_CLASS_TOTAL_INCREASING + is SensorStateClass.TOTAL_INCREASING ) assert ( gas_this_month.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -303,7 +311,7 @@ async def test_setup_entry(hass: HomeAssistant): assert gas_this_year.attributes.get(ATTR_ICON) == "mdi:counter" assert ( gas_this_year.attributes.get(ATTR_STATE_CLASS) - is STATE_CLASS_TOTAL_INCREASING + is SensorStateClass.TOTAL_INCREASING ) assert ( gas_this_year.attributes.get(ATTR_UNIT_OF_MEASUREMENT) From adc0c6523f76e803163b48f786ef5adf7832d895 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 14 Dec 2021 11:24:32 +0100 Subject: [PATCH 0407/2644] Use new SensorDeviceClass enum in homeassistant-triggers (#61764) Co-authored-by: epenet --- homeassistant/components/homeassistant/triggers/time.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/homeassistant/triggers/time.py b/homeassistant/components/homeassistant/triggers/time.py index 1bfdc0e4e58..49a42d3843d 100644 --- a/homeassistant/components/homeassistant/triggers/time.py +++ b/homeassistant/components/homeassistant/triggers/time.py @@ -128,7 +128,7 @@ async def async_attach_trigger(hass, config, action, automation_info): elif ( new_state.domain == "sensor" and new_state.attributes.get(ATTR_DEVICE_CLASS) - == sensor.DEVICE_CLASS_TIMESTAMP + == sensor.SensorDeviceClass.TIMESTAMP and new_state.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN) ): trigger_dt = dt_util.parse_datetime(new_state.state) From f4edd0ea20054d5b4871ec7eb7448bd2c34d1ceb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 14 Dec 2021 11:43:14 +0100 Subject: [PATCH 0408/2644] Use new enums in homematicip_cloud (#61768) Co-authored-by: epenet --- .../homematicip_cloud/binary_sensor.py | 36 +++++++------------ .../components/homematicip_cloud/cover.py | 12 +++---- .../components/homematicip_cloud/sensor.py | 31 +++++++--------- 3 files changed, 31 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 0cc462ac1f4..0d00d5139b3 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -30,17 +30,7 @@ from homematicip.aio.group import AsyncSecurityGroup, AsyncSecurityZoneGroup from homematicip.base.enums import SmokeDetectorAlarmType, WindowState from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_DOOR, - DEVICE_CLASS_LIGHT, - DEVICE_CLASS_MOISTURE, - DEVICE_CLASS_MOTION, - DEVICE_CLASS_MOVING, - DEVICE_CLASS_OPENING, - DEVICE_CLASS_POWER, - DEVICE_CLASS_PRESENCE, - DEVICE_CLASS_SAFETY, - DEVICE_CLASS_SMOKE, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -205,7 +195,7 @@ class HomematicipBaseActionSensor(HomematicipGenericEntity, BinarySensorEntity): @property def device_class(self) -> str: """Return the class of this sensor.""" - return DEVICE_CLASS_MOVING + return BinarySensorDeviceClass.MOVING @property def is_on(self) -> bool: @@ -250,7 +240,7 @@ class HomematicipMultiContactInterface(HomematicipGenericEntity, BinarySensorEnt @property def device_class(self) -> str: """Return the class of this sensor.""" - return DEVICE_CLASS_OPENING + return BinarySensorDeviceClass.OPENING @property def is_on(self) -> bool | None: @@ -284,7 +274,7 @@ class HomematicipShutterContact(HomematicipMultiContactInterface, BinarySensorEn @property def device_class(self) -> str: """Return the class of this sensor.""" - return DEVICE_CLASS_DOOR + return BinarySensorDeviceClass.DOOR @property def extra_state_attributes(self) -> dict[str, Any]: @@ -305,7 +295,7 @@ class HomematicipMotionDetector(HomematicipGenericEntity, BinarySensorEntity): @property def device_class(self) -> str: """Return the class of this sensor.""" - return DEVICE_CLASS_MOTION + return BinarySensorDeviceClass.MOTION @property def is_on(self) -> bool: @@ -319,7 +309,7 @@ class HomematicipPresenceDetector(HomematicipGenericEntity, BinarySensorEntity): @property def device_class(self) -> str: """Return the class of this sensor.""" - return DEVICE_CLASS_PRESENCE + return BinarySensorDeviceClass.PRESENCE @property def is_on(self) -> bool: @@ -333,7 +323,7 @@ class HomematicipSmokeDetector(HomematicipGenericEntity, BinarySensorEntity): @property def device_class(self) -> str: """Return the class of this sensor.""" - return DEVICE_CLASS_SMOKE + return BinarySensorDeviceClass.SMOKE @property def is_on(self) -> bool: @@ -352,7 +342,7 @@ class HomematicipWaterDetector(HomematicipGenericEntity, BinarySensorEntity): @property def device_class(self) -> str: """Return the class of this sensor.""" - return DEVICE_CLASS_MOISTURE + return BinarySensorDeviceClass.MOISTURE @property def is_on(self) -> bool: @@ -388,7 +378,7 @@ class HomematicipRainSensor(HomematicipGenericEntity, BinarySensorEntity): @property def device_class(self) -> str: """Return the class of this sensor.""" - return DEVICE_CLASS_MOISTURE + return BinarySensorDeviceClass.MOISTURE @property def is_on(self) -> bool: @@ -406,7 +396,7 @@ class HomematicipSunshineSensor(HomematicipGenericEntity, BinarySensorEntity): @property def device_class(self) -> str: """Return the class of this sensor.""" - return DEVICE_CLASS_LIGHT + return BinarySensorDeviceClass.LIGHT @property def is_on(self) -> bool: @@ -435,7 +425,7 @@ class HomematicipBatterySensor(HomematicipGenericEntity, BinarySensorEntity): @property def device_class(self) -> str: """Return the class of this sensor.""" - return DEVICE_CLASS_BATTERY + return BinarySensorDeviceClass.BATTERY @property def is_on(self) -> bool: @@ -455,7 +445,7 @@ class HomematicipPluggableMainsFailureSurveillanceSensor( @property def device_class(self) -> str: """Return the class of this sensor.""" - return DEVICE_CLASS_POWER + return BinarySensorDeviceClass.POWER @property def is_on(self) -> bool: @@ -474,7 +464,7 @@ class HomematicipSecurityZoneSensorGroup(HomematicipGenericEntity, BinarySensorE @property def device_class(self) -> str: """Return the class of this sensor.""" - return DEVICE_CLASS_SAFETY + return BinarySensorDeviceClass.SAFETY @property def available(self) -> bool: diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py index 0d0278ac455..937e36a15fc 100644 --- a/homeassistant/components/homematicip_cloud/cover.py +++ b/homeassistant/components/homematicip_cloud/cover.py @@ -15,9 +15,7 @@ from homematicip.base.enums import DoorCommand, DoorState from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, - DEVICE_CLASS_BLIND, - DEVICE_CLASS_GARAGE, - DEVICE_CLASS_SHUTTER, + CoverDeviceClass, CoverEntity, ) from homeassistant.config_entries import ConfigEntry @@ -69,7 +67,7 @@ class HomematicipBlindModule(HomematicipGenericEntity, CoverEntity): @property def device_class(self) -> str: """Return the class of the cover.""" - return DEVICE_CLASS_BLIND + return CoverDeviceClass.BLIND @property def current_cover_position(self) -> int | None: @@ -162,7 +160,7 @@ class HomematicipMultiCoverShutter(HomematicipGenericEntity, CoverEntity): @property def device_class(self) -> str: """Return the class of the cover.""" - return DEVICE_CLASS_SHUTTER + return CoverDeviceClass.SHUTTER @property def current_cover_position(self) -> int | None: @@ -280,7 +278,7 @@ class HomematicipGarageDoorModule(HomematicipGenericEntity, CoverEntity): @property def device_class(self) -> str: """Return the class of the cover.""" - return DEVICE_CLASS_GARAGE + return CoverDeviceClass.GARAGE @property def is_closed(self) -> bool | None: @@ -311,7 +309,7 @@ class HomematicipCoverShutterGroup(HomematicipGenericEntity, CoverEntity): @property def device_class(self) -> str: """Return the class of the cover.""" - return DEVICE_CLASS_SHUTTER + return CoverDeviceClass.SHUTTER @property def current_cover_position(self) -> int | None: diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index d1c3f71a83f..323d462dbf8 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -27,17 +27,12 @@ from homematicip.aio.device import ( from homematicip.base.enums import ValveState from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_POWER, - DEVICE_CLASS_TEMPERATURE, ENERGY_KILO_WATT_HOUR, LENGTH_MILLIMETERS, LIGHT_LUX, @@ -134,7 +129,7 @@ async def async_setup_entry( class HomematicipAccesspointDutyCycle(HomematicipGenericEntity, SensorEntity): """Representation of then HomeMaticIP access point.""" - _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_state_class = SensorStateClass.MEASUREMENT def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize access point status entity.""" @@ -188,7 +183,7 @@ class HomematicipHeatingThermostat(HomematicipGenericEntity, SensorEntity): class HomematicipHumiditySensor(HomematicipGenericEntity, SensorEntity): """Representation of the HomematicIP humidity sensor.""" - _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_state_class = SensorStateClass.MEASUREMENT def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the thermometer device.""" @@ -197,7 +192,7 @@ class HomematicipHumiditySensor(HomematicipGenericEntity, SensorEntity): @property def device_class(self) -> str: """Return the device class of the sensor.""" - return DEVICE_CLASS_HUMIDITY + return SensorDeviceClass.HUMIDITY @property def native_value(self) -> int: @@ -213,7 +208,7 @@ class HomematicipHumiditySensor(HomematicipGenericEntity, SensorEntity): class HomematicipTemperatureSensor(HomematicipGenericEntity, SensorEntity): """Representation of the HomematicIP thermometer.""" - _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_state_class = SensorStateClass.MEASUREMENT def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the thermometer device.""" @@ -222,7 +217,7 @@ class HomematicipTemperatureSensor(HomematicipGenericEntity, SensorEntity): @property def device_class(self) -> str: """Return the device class of the sensor.""" - return DEVICE_CLASS_TEMPERATURE + return SensorDeviceClass.TEMPERATURE @property def native_value(self) -> float: @@ -252,7 +247,7 @@ class HomematicipTemperatureSensor(HomematicipGenericEntity, SensorEntity): class HomematicipIlluminanceSensor(HomematicipGenericEntity, SensorEntity): """Representation of the HomematicIP Illuminance sensor.""" - _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_state_class = SensorStateClass.MEASUREMENT def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the device.""" @@ -261,7 +256,7 @@ class HomematicipIlluminanceSensor(HomematicipGenericEntity, SensorEntity): @property def device_class(self) -> str: """Return the device class of the sensor.""" - return DEVICE_CLASS_ILLUMINANCE + return SensorDeviceClass.ILLUMINANCE @property def native_value(self) -> float: @@ -291,7 +286,7 @@ class HomematicipIlluminanceSensor(HomematicipGenericEntity, SensorEntity): class HomematicipPowerSensor(HomematicipGenericEntity, SensorEntity): """Representation of the HomematicIP power measuring sensor.""" - _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_state_class = SensorStateClass.MEASUREMENT def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the device.""" @@ -300,7 +295,7 @@ class HomematicipPowerSensor(HomematicipGenericEntity, SensorEntity): @property def device_class(self) -> str: """Return the device class of the sensor.""" - return DEVICE_CLASS_POWER + return SensorDeviceClass.POWER @property def native_value(self) -> float: @@ -316,7 +311,7 @@ class HomematicipPowerSensor(HomematicipGenericEntity, SensorEntity): class HomematicipEnergySensor(HomematicipGenericEntity, SensorEntity): """Representation of the HomematicIP energy measuring sensor.""" - _attr_state_class = STATE_CLASS_TOTAL_INCREASING + _attr_state_class = SensorStateClass.TOTAL_INCREASING def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the device.""" @@ -325,7 +320,7 @@ class HomematicipEnergySensor(HomematicipGenericEntity, SensorEntity): @property def device_class(self) -> str: """Return the device class of the sensor.""" - return DEVICE_CLASS_ENERGY + return SensorDeviceClass.ENERGY @property def native_value(self) -> float: From 96e54ca9391ac55e99c3370fa48060c2b09a5ee4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 14 Dec 2021 11:55:31 +0100 Subject: [PATCH 0409/2644] Use new SensorDeviceClass enum in hvv_departures (#61780) Co-authored-by: epenet --- homeassistant/components/hvv_departures/binary_sensor.py | 4 ++-- homeassistant/components/hvv_departures/sensor.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/hvv_departures/binary_sensor.py b/homeassistant/components/hvv_departures/binary_sensor.py index a3494a2b6d8..19792cb8e39 100644 --- a/homeassistant/components/hvv_departures/binary_sensor.py +++ b/homeassistant/components/hvv_departures/binary_sensor.py @@ -7,7 +7,7 @@ import async_timeout from pygti.exceptions import InvalidAuth from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_PROBLEM, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.const import ATTR_ATTRIBUTION @@ -172,7 +172,7 @@ class HvvDepartureBinarySensor(CoordinatorEntity, BinarySensorEntity): @property def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" - return DEVICE_CLASS_PROBLEM + return BinarySensorDeviceClass.PROBLEM @property def extra_state_attributes(self): diff --git a/homeassistant/components/hvv_departures/sensor.py b/homeassistant/components/hvv_departures/sensor.py index d82a15cebe9..507c0601803 100644 --- a/homeassistant/components/hvv_departures/sensor.py +++ b/homeassistant/components/hvv_departures/sensor.py @@ -5,8 +5,8 @@ import logging from aiohttp import ClientConnectorError from pygti.exceptions import InvalidAuth -from homeassistant.components.sensor import SensorEntity -from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ID, DEVICE_CLASS_TIMESTAMP +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ID from homeassistant.helpers import aiohttp_client from homeassistant.helpers.entity import DeviceInfo from homeassistant.util import Throttle @@ -195,7 +195,7 @@ class HVVDepartureSensor(SensorEntity): @property def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" - return DEVICE_CLASS_TIMESTAMP + return SensorDeviceClass.TIMESTAMP @property def extra_state_attributes(self): From cec43fe868340ad1ed75471a5d31b747a7cad381 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 14 Dec 2021 12:59:00 +0100 Subject: [PATCH 0410/2644] Use new enums in huawei-lte (#61771) Co-authored-by: epenet --- homeassistant/components/huawei_lte/sensor.py | 140 +++++++++--------- 1 file changed, 68 insertions(+), 72 deletions(-) diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index 57963046cbc..c894fc39659 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -10,26 +10,22 @@ from typing import NamedTuple import attr from homeassistant.components.sensor import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_SIGNAL_STRENGTH, DOMAIN as SENSOR_DOMAIN, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DATA_BYTES, DATA_RATE_BYTES_PER_SECOND, - ENTITY_CATEGORY_CONFIG, - ENTITY_CATEGORY_DIAGNOSTIC, FREQUENCY_MEGAHERTZ, PERCENTAGE, STATE_UNKNOWN, TIME_SECONDS, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import Entity, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -73,32 +69,32 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { (KEY_DEVICE_INFORMATION, "WanIPAddress"): SensorMeta( name="WAN IP address", icon="mdi:ip", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, enabled_default=True, ), (KEY_DEVICE_INFORMATION, "WanIPv6Address"): SensorMeta( name="WAN IPv6 address", icon="mdi:ip", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), (KEY_DEVICE_INFORMATION, "uptime"): SensorMeta( name="Uptime", icon="mdi:timer-outline", unit=TIME_SECONDS, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), (KEY_DEVICE_SIGNAL, "band"): SensorMeta( name="Band", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), (KEY_DEVICE_SIGNAL, "cell_id"): SensorMeta( name="Cell ID", icon="mdi:transmission-tower", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), (KEY_DEVICE_SIGNAL, "dl_mcs"): SensorMeta( name="Downlink MCS", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), (KEY_DEVICE_SIGNAL, "dlbandwidth"): SensorMeta( name="Downlink bandwidth", @@ -107,47 +103,47 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { "mdi:speedometer-medium", "mdi:speedometer", )[bisect((8, 15), x if x is not None else -1000)], - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), (KEY_DEVICE_SIGNAL, "earfcn"): SensorMeta( name="EARFCN", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), (KEY_DEVICE_SIGNAL, "lac"): SensorMeta( name="LAC", icon="mdi:map-marker", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), (KEY_DEVICE_SIGNAL, "plmn"): SensorMeta( name="PLMN", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), (KEY_DEVICE_SIGNAL, "rac"): SensorMeta( name="RAC", icon="mdi:map-marker", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), (KEY_DEVICE_SIGNAL, "rrc_status"): SensorMeta( name="RRC status", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), (KEY_DEVICE_SIGNAL, "tac"): SensorMeta( name="TAC", icon="mdi:map-marker", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), (KEY_DEVICE_SIGNAL, "tdd"): SensorMeta( name="TDD", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), (KEY_DEVICE_SIGNAL, "txpower"): SensorMeta( name="Transmit power", - device_class=DEVICE_CLASS_SIGNAL_STRENGTH, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=EntityCategory.DIAGNOSTIC, ), (KEY_DEVICE_SIGNAL, "ul_mcs"): SensorMeta( name="Uplink MCS", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), (KEY_DEVICE_SIGNAL, "ulbandwidth"): SensorMeta( name="Uplink bandwidth", @@ -156,7 +152,7 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { "mdi:speedometer-medium", "mdi:speedometer", )[bisect((8, 15), x if x is not None else -1000)], - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), (KEY_DEVICE_SIGNAL, "mode"): SensorMeta( name="Mode", @@ -166,16 +162,16 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { str(x), "mdi:signal" ) ), - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), (KEY_DEVICE_SIGNAL, "pci"): SensorMeta( name="PCI", icon="mdi:transmission-tower", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), (KEY_DEVICE_SIGNAL, "rsrq"): SensorMeta( name="RSRQ", - device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, # http://www.lte-anbieter.info/technik/rsrq.php icon=lambda x: ( "mdi:signal-cellular-outline", @@ -183,13 +179,13 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { "mdi:signal-cellular-2", "mdi:signal-cellular-3", )[bisect((-11, -8, -5), x if x is not None else -1000)], - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, enabled_default=True, ), (KEY_DEVICE_SIGNAL, "rsrp"): SensorMeta( name="RSRP", - device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, # http://www.lte-anbieter.info/technik/rsrp.php icon=lambda x: ( "mdi:signal-cellular-outline", @@ -197,13 +193,13 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { "mdi:signal-cellular-2", "mdi:signal-cellular-3", )[bisect((-110, -95, -80), x if x is not None else -1000)], - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, enabled_default=True, ), (KEY_DEVICE_SIGNAL, "rssi"): SensorMeta( name="RSSI", - device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, # https://eyesaas.com/wi-fi-signal-strength/ icon=lambda x: ( "mdi:signal-cellular-outline", @@ -211,13 +207,13 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { "mdi:signal-cellular-2", "mdi:signal-cellular-3", )[bisect((-80, -70, -60), x if x is not None else -1000)], - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, enabled_default=True, ), (KEY_DEVICE_SIGNAL, "sinr"): SensorMeta( name="SINR", - device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, # http://www.lte-anbieter.info/technik/sinr.php icon=lambda x: ( "mdi:signal-cellular-outline", @@ -225,13 +221,13 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { "mdi:signal-cellular-2", "mdi:signal-cellular-3", )[bisect((0, 5, 10), x if x is not None else -1000)], - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, enabled_default=True, ), (KEY_DEVICE_SIGNAL, "rscp"): SensorMeta( name="RSCP", - device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, # https://wiki.teltonika.lt/view/RSCP icon=lambda x: ( "mdi:signal-cellular-outline", @@ -239,12 +235,12 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { "mdi:signal-cellular-2", "mdi:signal-cellular-3", )[bisect((-95, -85, -75), x if x is not None else -1000)], - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), (KEY_DEVICE_SIGNAL, "ecio"): SensorMeta( name="EC/IO", - device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, # https://wiki.teltonika.lt/view/EC/IO icon=lambda x: ( "mdi:signal-cellular-outline", @@ -252,17 +248,17 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { "mdi:signal-cellular-2", "mdi:signal-cellular-3", )[bisect((-20, -10, -6), x if x is not None else -1000)], - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), (KEY_DEVICE_SIGNAL, "transmode"): SensorMeta( name="Transmission mode", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), (KEY_DEVICE_SIGNAL, "cqi0"): SensorMeta( name="CQI 0", icon="mdi:speedometer", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), (KEY_DEVICE_SIGNAL, "cqi1"): SensorMeta( name="CQI 1", @@ -270,7 +266,7 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { ), (KEY_DEVICE_SIGNAL, "enodeb_id"): SensorMeta( name="eNodeB ID", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), (KEY_DEVICE_SIGNAL, "ltedlfreq"): SensorMeta( name="Downlink frequency", @@ -278,7 +274,7 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { round(int(x) / 10) if x is not None else None, FREQUENCY_MEGAHERTZ, ), - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), (KEY_DEVICE_SIGNAL, "lteulfreq"): SensorMeta( name="Uplink frequency", @@ -286,7 +282,7 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { round(int(x) / 10) if x is not None else None, FREQUENCY_MEGAHERTZ, ), - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), KEY_MONITORING_CHECK_NOTIFICATIONS: SensorMeta( exclude=re.compile( @@ -304,13 +300,13 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { name="Current month download", unit=DATA_BYTES, icon="mdi:download", - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, ), (KEY_MONITORING_MONTH_STATISTICS, "CurrentMonthUpload"): SensorMeta( name="Current month upload", unit=DATA_BYTES, icon="mdi:upload", - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, ), KEY_MONITORING_STATUS: SensorMeta( include=re.compile( @@ -320,36 +316,36 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { ), (KEY_MONITORING_STATUS, "BatteryPercent"): SensorMeta( name="Battery", - device_class=DEVICE_CLASS_BATTERY, + device_class=SensorDeviceClass.BATTERY, unit=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), (KEY_MONITORING_STATUS, "CurrentWifiUser"): SensorMeta( name="WiFi clients connected", icon="mdi:wifi", - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), (KEY_MONITORING_STATUS, "PrimaryDns"): SensorMeta( name="Primary DNS server", icon="mdi:ip", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), (KEY_MONITORING_STATUS, "SecondaryDns"): SensorMeta( name="Secondary DNS server", icon="mdi:ip", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), (KEY_MONITORING_STATUS, "PrimaryIPv6Dns"): SensorMeta( name="Primary IPv6 DNS server", icon="mdi:ip", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), (KEY_MONITORING_STATUS, "SecondaryIPv6Dns"): SensorMeta( name="Secondary IPv6 DNS server", icon="mdi:ip", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), KEY_MONITORING_TRAFFIC_STATISTICS: SensorMeta( exclude=re.compile(r"^showtraffic$", re.IGNORECASE) @@ -361,43 +357,43 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { name="Current connection download", unit=DATA_BYTES, icon="mdi:download", - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, ), (KEY_MONITORING_TRAFFIC_STATISTICS, "CurrentDownloadRate"): SensorMeta( name="Current download rate", unit=DATA_RATE_BYTES_PER_SECOND, icon="mdi:download", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), (KEY_MONITORING_TRAFFIC_STATISTICS, "CurrentUpload"): SensorMeta( name="Current connection upload", unit=DATA_BYTES, icon="mdi:upload", - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, ), (KEY_MONITORING_TRAFFIC_STATISTICS, "CurrentUploadRate"): SensorMeta( name="Current upload rate", unit=DATA_RATE_BYTES_PER_SECOND, icon="mdi:upload", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), (KEY_MONITORING_TRAFFIC_STATISTICS, "TotalConnectTime"): SensorMeta( name="Total connected duration", unit=TIME_SECONDS, icon="mdi:timer-outline", - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, ), (KEY_MONITORING_TRAFFIC_STATISTICS, "TotalDownload"): SensorMeta( name="Total download", unit=DATA_BYTES, icon="mdi:download", - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, ), (KEY_MONITORING_TRAFFIC_STATISTICS, "TotalUpload"): SensorMeta( name="Total upload", unit=DATA_BYTES, icon="mdi:upload", - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, ), KEY_NET_CURRENT_PLMN: SensorMeta( exclude=re.compile(r"^(Rat|ShortName|Spn)$", re.IGNORECASE) @@ -405,15 +401,15 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { (KEY_NET_CURRENT_PLMN, "State"): SensorMeta( name="Operator search mode", formatter=lambda x: ({"0": "Auto", "1": "Manual"}.get(x, "Unknown"), None), - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), (KEY_NET_CURRENT_PLMN, "FullName"): SensorMeta( name="Operator name", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), (KEY_NET_CURRENT_PLMN, "Numeric"): SensorMeta( name="Operator code", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), KEY_NET_NET_MODE: SensorMeta(include=re.compile(r"^NetworkMode$", re.IGNORECASE)), (KEY_NET_NET_MODE, "NetworkMode"): SensorMeta( @@ -430,7 +426,7 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { }.get(x, "Unknown"), None, ), - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), (KEY_SMS_SMS_COUNT, "LocalDeleted"): SensorMeta( name="SMS deleted (device)", From a28ce75a922ca2694db5c307c5292f39d5cc503c Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Tue, 14 Dec 2021 13:01:30 +0100 Subject: [PATCH 0411/2644] Bump brunt package to 1.0.1 (#61784) --- homeassistant/components/brunt/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/brunt/manifest.json b/homeassistant/components/brunt/manifest.json index 976b017ca09..1ddbbb62f56 100644 --- a/homeassistant/components/brunt/manifest.json +++ b/homeassistant/components/brunt/manifest.json @@ -3,7 +3,7 @@ "name": "Brunt Blind Engine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/brunt", - "requirements": ["brunt==1.0.0"], + "requirements": ["brunt==1.0.1"], "codeowners": ["@eavanvalkenburg"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index ca163f49aaf..046c2270411 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -446,7 +446,7 @@ brother==1.1.0 brottsplatskartan==0.0.1 # homeassistant.components.brunt -brunt==1.0.0 +brunt==1.0.1 # homeassistant.components.bsblan bsblan==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f011872118b..f35c90313e7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -287,7 +287,7 @@ broadlink==0.18.0 brother==1.1.0 # homeassistant.components.brunt -brunt==1.0.0 +brunt==1.0.1 # homeassistant.components.bsblan bsblan==0.4.0 From 237a8a833148d1574d0f5fcbd45bd88db0250da4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 14 Dec 2021 13:06:55 +0100 Subject: [PATCH 0412/2644] Use new enums in hydrawise (#61781) Co-authored-by: epenet --- homeassistant/components/hydrawise/binary_sensor.py | 7 +++---- homeassistant/components/hydrawise/sensor.py | 9 +++------ homeassistant/components/hydrawise/switch.py | 6 +++--- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/hydrawise/binary_sensor.py b/homeassistant/components/hydrawise/binary_sensor.py index 7a673a1e7ae..1c201b532f5 100644 --- a/homeassistant/components/hydrawise/binary_sensor.py +++ b/homeassistant/components/hydrawise/binary_sensor.py @@ -6,9 +6,8 @@ import logging import voluptuous as vol from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_CONNECTIVITY, - DEVICE_CLASS_MOISTURE, PLATFORM_SCHEMA, + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -22,14 +21,14 @@ _LOGGER = logging.getLogger(__name__) BINARY_SENSOR_STATUS = BinarySensorEntityDescription( key="status", name="Status", - device_class=DEVICE_CLASS_CONNECTIVITY, + device_class=BinarySensorDeviceClass.CONNECTIVITY, ) BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( BinarySensorEntityDescription( key="is_watering", name="Watering", - device_class=DEVICE_CLASS_MOISTURE, + device_class=BinarySensorDeviceClass.MOISTURE, ), ) diff --git a/homeassistant/components/hydrawise/sensor.py b/homeassistant/components/hydrawise/sensor.py index ee9e931a351..a3ff0fef871 100644 --- a/homeassistant/components/hydrawise/sensor.py +++ b/homeassistant/components/hydrawise/sensor.py @@ -7,14 +7,11 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, + SensorDeviceClass, SensorEntity, SensorEntityDescription, ) -from homeassistant.const import ( - CONF_MONITORED_CONDITIONS, - DEVICE_CLASS_TIMESTAMP, - TIME_MINUTES, -) +from homeassistant.const import CONF_MONITORED_CONDITIONS, TIME_MINUTES import homeassistant.helpers.config_validation as cv from homeassistant.util import dt @@ -26,7 +23,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="next_cycle", name="Next Cycle", - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, ), SensorEntityDescription( key="watering_time", diff --git a/homeassistant/components/hydrawise/switch.py b/homeassistant/components/hydrawise/switch.py index 8b3707ad5a0..8dc91fd1c65 100644 --- a/homeassistant/components/hydrawise/switch.py +++ b/homeassistant/components/hydrawise/switch.py @@ -6,8 +6,8 @@ import logging import voluptuous as vol from homeassistant.components.switch import ( - DEVICE_CLASS_SWITCH, PLATFORM_SCHEMA, + SwitchDeviceClass, SwitchEntity, SwitchEntityDescription, ) @@ -28,12 +28,12 @@ SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = ( SwitchEntityDescription( key="auto_watering", name="Automatic Watering", - device_class=DEVICE_CLASS_SWITCH, + device_class=SwitchDeviceClass.SWITCH, ), SwitchEntityDescription( key="manual_watering", name="Manual Watering", - device_class=DEVICE_CLASS_SWITCH, + device_class=SwitchDeviceClass.SWITCH, ), ) From bcc925151789b85a6edf437b8af35faf93bbef9f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 14 Dec 2021 13:07:59 +0100 Subject: [PATCH 0413/2644] Use new enums in hunterdouglas_powerview (#61777) Co-authored-by: epenet --- .../components/hunterdouglas_powerview/cover.py | 4 ++-- .../components/hunterdouglas_powerview/sensor.py | 13 +++++-------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/hunterdouglas_powerview/cover.py b/homeassistant/components/hunterdouglas_powerview/cover.py index 22636b7e3c4..f8644b15a5d 100644 --- a/homeassistant/components/hunterdouglas_powerview/cover.py +++ b/homeassistant/components/hunterdouglas_powerview/cover.py @@ -14,11 +14,11 @@ import async_timeout from homeassistant.components.cover import ( ATTR_POSITION, - DEVICE_CLASS_SHADE, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION, SUPPORT_STOP, + CoverDeviceClass, CoverEntity, ) from homeassistant.core import callback @@ -145,7 +145,7 @@ class PowerViewShade(ShadeEntity, CoverEntity): @property def device_class(self): """Return device class.""" - return DEVICE_CLASS_SHADE + return CoverDeviceClass.SHADE @property def name(self): diff --git a/homeassistant/components/hunterdouglas_powerview/sensor.py b/homeassistant/components/hunterdouglas_powerview/sensor.py index c789dd150da..1251ee778dc 100644 --- a/homeassistant/components/hunterdouglas_powerview/sensor.py +++ b/homeassistant/components/hunterdouglas_powerview/sensor.py @@ -1,13 +1,10 @@ """Support for hunterdouglass_powerview sensors.""" from aiopvapi.resources.shade import factory as PvShade -from homeassistant.components.sensor import SensorEntity -from homeassistant.const import ( - DEVICE_CLASS_BATTERY, - ENTITY_CATEGORY_DIAGNOSTIC, - PERCENTAGE, -) +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.const import PERCENTAGE from homeassistant.core import callback +from homeassistant.helpers.entity import EntityCategory from .const import ( COORDINATOR, @@ -53,7 +50,7 @@ async def async_setup_entry(hass, entry, async_add_entities): class PowerViewShadeBatterySensor(ShadeEntity, SensorEntity): """Representation of an shade battery charge sensor.""" - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_entity_category = EntityCategory.DIAGNOSTIC @property def native_unit_of_measurement(self): @@ -68,7 +65,7 @@ class PowerViewShadeBatterySensor(ShadeEntity, SensorEntity): @property def device_class(self): """Shade battery Class.""" - return DEVICE_CLASS_BATTERY + return SensorDeviceClass.BATTERY @property def unique_id(self): From 36da11e924a4f8b3f947fcff65c5cb99a4eee3ff Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 14 Dec 2021 13:08:49 +0100 Subject: [PATCH 0414/2644] Use new enums in hue (#61772) Co-authored-by: epenet --- homeassistant/components/hue/switch.py | 4 ++-- homeassistant/components/hue/v1/sensor.py | 16 ++++++---------- homeassistant/components/hue/v1/sensor_base.py | 4 ++-- homeassistant/components/hue/v2/sensor.py | 16 ++++++---------- 4 files changed, 16 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/hue/switch.py b/homeassistant/components/hue/switch.py index d8d0d0fd05c..7f8e048d692 100644 --- a/homeassistant/components/hue/switch.py +++ b/homeassistant/components/hue/switch.py @@ -10,8 +10,8 @@ from aiohue.v2.models.resource import SensingService from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .bridge import HueBridge @@ -62,7 +62,7 @@ async def async_setup_entry( class HueSensingServiceEnabledEntity(HueBaseEntity, SwitchEntity): """Representation of a Switch entity from Hue SensingService.""" - _attr_entity_category = ENTITY_CATEGORY_CONFIG + _attr_entity_category = EntityCategory.CONFIG _attr_device_class = SwitchDeviceClass.SWITCH def __init__( diff --git a/homeassistant/components/hue/v1/sensor.py b/homeassistant/components/hue/v1/sensor.py index fc490ab1e4b..3a79f5f37f1 100644 --- a/homeassistant/components/hue/v1/sensor.py +++ b/homeassistant/components/hue/v1/sensor.py @@ -7,16 +7,12 @@ from aiohue.v1.sensors import ( ) from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, SensorDeviceClass, SensorEntity, + SensorStateClass, ) -from homeassistant.const import ( - ENTITY_CATEGORY_DIAGNOSTIC, - LIGHT_LUX, - PERCENTAGE, - TEMP_CELSIUS, -) +from homeassistant.const import LIGHT_LUX, PERCENTAGE, TEMP_CELSIUS +from homeassistant.helpers.entity import EntityCategory from ..const import DOMAIN as HUE_DOMAIN from .sensor_base import SENSOR_CONFIG_MAP, GenericHueSensor, GenericZLLSensor @@ -79,7 +75,7 @@ class HueTemperature(GenericHueGaugeSensorEntity): """The temperature sensor entity for a Hue motion sensor device.""" _attr_device_class = SensorDeviceClass.TEMPERATURE - _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_state_class = SensorStateClass.MEASUREMENT _attr_native_unit_of_measurement = TEMP_CELSIUS @property @@ -95,9 +91,9 @@ class HueBattery(GenericHueSensor, SensorEntity): """Battery class for when a batt-powered device is only represented as an event.""" _attr_device_class = SensorDeviceClass.BATTERY - _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_state_class = SensorStateClass.MEASUREMENT _attr_native_unit_of_measurement = PERCENTAGE - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_entity_category = EntityCategory.DIAGNOSTIC @property def unique_id(self): diff --git a/homeassistant/components/hue/v1/sensor_base.py b/homeassistant/components/hue/v1/sensor_base.py index 142941a1859..84921707f2a 100644 --- a/homeassistant/components/hue/v1/sensor_base.py +++ b/homeassistant/components/hue/v1/sensor_base.py @@ -9,7 +9,7 @@ from aiohue import AiohueException, Unauthorized from aiohue.v1.sensors import TYPE_ZLL_PRESENCE import async_timeout -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT +from homeassistant.components.sensor import SensorStateClass from homeassistant.core import callback from homeassistant.helpers import debounce, entity from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -181,7 +181,7 @@ class GenericHueSensor(GenericHueDevice, entity.Entity): @property def state_class(self): """Return the state class of this entity, from STATE_CLASSES, if any.""" - return STATE_CLASS_MEASUREMENT + return SensorStateClass.MEASUREMENT async def async_added_to_hass(self): """When entity is added to hass.""" diff --git a/homeassistant/components/hue/v2/sensor.py b/homeassistant/components/hue/v2/sensor.py index daff7c8d6de..ff2b7b78e7d 100644 --- a/homeassistant/components/hue/v2/sensor.py +++ b/homeassistant/components/hue/v2/sensor.py @@ -19,18 +19,14 @@ from aiohue.v2.models.temperature import Temperature from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, SensorDeviceClass, SensorEntity, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ENTITY_CATEGORY_DIAGNOSTIC, - LIGHT_LUX, - PERCENTAGE, - TEMP_CELSIUS, -) +from homeassistant.const import LIGHT_LUX, PERCENTAGE, TEMP_CELSIUS from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from ..bridge import HueBridge @@ -84,7 +80,7 @@ async def async_setup_entry( class HueSensorBase(HueBaseEntity, SensorEntity): """Representation of a Hue sensor.""" - _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_state_class = SensorStateClass.MEASUREMENT def __init__( self, @@ -144,7 +140,7 @@ class HueBatterySensor(HueSensorBase): _attr_native_unit_of_measurement = PERCENTAGE _attr_device_class = SensorDeviceClass.BATTERY - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_entity_category = EntityCategory.DIAGNOSTIC @property def native_value(self) -> int: @@ -161,7 +157,7 @@ class HueZigbeeConnectivitySensor(HueSensorBase): """Representation of a Hue ZigbeeConnectivity sensor.""" _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_entity_registry_enabled_default = False @property From 8cda315cd1c913072b90c86cf9166cbb348805e7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 14 Dec 2021 13:09:19 +0100 Subject: [PATCH 0415/2644] Use SensorDeviceClass enum in htu21d (#61770) Co-authored-by: epenet --- homeassistant/components/htu21d/sensor.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/htu21d/sensor.py b/homeassistant/components/htu21d/sensor.py index d43a0733daf..6c3844d6657 100644 --- a/homeassistant/components/htu21d/sensor.py +++ b/homeassistant/components/htu21d/sensor.py @@ -11,16 +11,11 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, + SensorDeviceClass, SensorEntity, SensorEntityDescription, ) -from homeassistant.const import ( - CONF_NAME, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_TEMPERATURE, - PERCENTAGE, - TEMP_CELSIUS, -) +from homeassistant.const import CONF_NAME, PERCENTAGE, TEMP_CELSIUS import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle @@ -40,12 +35,12 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key=SENSOR_TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( key=SENSOR_HUMIDITY, native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, ), ) From dfcadd600c1fb44de44a09b45c6b7312aeadaccf Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 14 Dec 2021 07:11:07 -0500 Subject: [PATCH 0416/2644] Remove deprecated yaml from foscam (#61761) --- homeassistant/components/foscam/__init__.py | 3 +- homeassistant/components/foscam/camera.py | 54 +----- .../components/foscam/config_flow.py | 28 --- tests/components/foscam/test_config_flow.py | 173 ------------------ 4 files changed, 4 insertions(+), 254 deletions(-) diff --git a/homeassistant/components/foscam/__init__.py b/homeassistant/components/foscam/__init__.py index 2ebea0615f7..380e1d18280 100644 --- a/homeassistant/components/foscam/__init__.py +++ b/homeassistant/components/foscam/__init__.py @@ -23,8 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up foscam from a config entry.""" hass.config_entries.async_setup_platforms(entry, PLATFORMS) - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = entry.data + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = entry.data return True diff --git a/homeassistant/components/foscam/camera.py b/homeassistant/components/foscam/camera.py index 7a1e1037ddb..4fd3d1d63be 100644 --- a/homeassistant/components/foscam/camera.py +++ b/homeassistant/components/foscam/camera.py @@ -6,36 +6,11 @@ import asyncio from libpyfoscam import FoscamCamera import voluptuous as vol -from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera -from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PASSWORD, - CONF_PORT, - CONF_USERNAME, -) +from homeassistant.components.camera import SUPPORT_STREAM, Camera +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.helpers import config_validation as cv, entity_platform -from .const import ( - CONF_RTSP_PORT, - CONF_STREAM, - DOMAIN, - LOGGER, - SERVICE_PTZ, - SERVICE_PTZ_PRESET, -) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required("ip"): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_NAME, default="Foscam Camera"): cv.string, - vol.Optional(CONF_PORT, default=88): cv.port, - vol.Optional(CONF_RTSP_PORT): cv.port, - } -) +from .const import CONF_RTSP_PORT, CONF_STREAM, LOGGER, SERVICE_PTZ, SERVICE_PTZ_PRESET DIR_UP = "up" DIR_DOWN = "down" @@ -67,29 +42,6 @@ ATTR_PRESET_NAME = "preset_name" PTZ_GOTO_PRESET_COMMAND = "ptz_goto_preset" -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up a Foscam IP Camera.""" - LOGGER.warning( - "Loading foscam via platform config is deprecated, it will be automatically imported; Please remove it afterwards" - ) - - config_new = { - CONF_NAME: config[CONF_NAME], - CONF_HOST: config["ip"], - CONF_PORT: config[CONF_PORT], - CONF_USERNAME: config[CONF_USERNAME], - CONF_PASSWORD: config[CONF_PASSWORD], - CONF_STREAM: "Main", - CONF_RTSP_PORT: config.get(CONF_RTSP_PORT, 554), - } - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config_new - ) - ) - - async def async_setup_entry(hass, config_entry, async_add_entities): """Add a Foscam IP camera from a config entry.""" platform = entity_platform.async_get_current_platform() diff --git a/homeassistant/components/foscam/config_flow.py b/homeassistant/components/foscam/config_flow.py index 0ab4e5d9866..8d19220130d 100644 --- a/homeassistant/components/foscam/config_flow.py +++ b/homeassistant/components/foscam/config_flow.py @@ -116,34 +116,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - async def async_step_import(self, import_config): - """Handle config import from yaml.""" - try: - return await self._validate_and_create(import_config) - - except CannotConnect: - LOGGER.error("Error importing foscam platform config: cannot connect") - return self.async_abort(reason="cannot_connect") - - except InvalidAuth: - LOGGER.error("Error importing foscam platform config: invalid auth") - return self.async_abort(reason="invalid_auth") - - except InvalidResponse: - LOGGER.exception( - "Error importing foscam platform config: invalid response from camera" - ) - return self.async_abort(reason="invalid_response") - - except AbortFlow: - raise - - except Exception: # pylint: disable=broad-except - LOGGER.exception( - "Error importing foscam platform config: unexpected exception" - ) - return self.async_abort(reason="unknown") - class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/tests/components/foscam/test_config_flow.py b/tests/components/foscam/test_config_flow.py index 3a108b539d8..63c30c16bab 100644 --- a/tests/components/foscam/test_config_flow.py +++ b/tests/components/foscam/test_config_flow.py @@ -245,176 +245,3 @@ async def test_user_unknown_exception(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {"base": "unknown"} - - -async def test_import_user_valid(hass): - """Test valid config from import.""" - - with patch( - "homeassistant.components.foscam.config_flow.FoscamCamera", - ) as mock_foscam_camera, patch( - "homeassistant.components.foscam.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - setup_mock_foscam_camera(mock_foscam_camera) - - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=VALID_CONFIG, - ) - - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == CAMERA_NAME - assert result["data"] == VALID_CONFIG - - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_import_user_valid_with_name(hass): - """Test valid config with extra name from import.""" - - with patch( - "homeassistant.components.foscam.config_flow.FoscamCamera", - ) as mock_foscam_camera, patch( - "homeassistant.components.foscam.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - setup_mock_foscam_camera(mock_foscam_camera) - - name = CAMERA_NAME + " 1234" - with_name = VALID_CONFIG.copy() - with_name[config_flow.CONF_NAME] = name - - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=with_name, - ) - - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == name - assert result["data"] == VALID_CONFIG - - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_import_invalid_auth(hass): - """Test we handle invalid auth from import.""" - - with patch( - "homeassistant.components.foscam.config_flow.FoscamCamera", - ) as mock_foscam_camera: - setup_mock_foscam_camera(mock_foscam_camera) - - invalid_user = VALID_CONFIG.copy() - invalid_user[config_flow.CONF_USERNAME] = "invalid" - - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=invalid_user, - ) - - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "invalid_auth" - - -async def test_import_cannot_connect(hass): - """Test we handle cannot connect error from import.""" - - with patch( - "homeassistant.components.foscam.config_flow.FoscamCamera", - ) as mock_foscam_camera: - setup_mock_foscam_camera(mock_foscam_camera) - - invalid_host = VALID_CONFIG.copy() - invalid_host[config_flow.CONF_HOST] = "127.0.0.1" - - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=invalid_host, - ) - - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "cannot_connect" - - -async def test_import_invalid_response(hass): - """Test we handle invalid response error from import.""" - - with patch( - "homeassistant.components.foscam.config_flow.FoscamCamera", - ) as mock_foscam_camera: - setup_mock_foscam_camera(mock_foscam_camera) - - invalid_response = VALID_CONFIG.copy() - invalid_response[config_flow.CONF_USERNAME] = INVALID_RESPONSE_CONFIG[ - config_flow.CONF_USERNAME - ] - - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=invalid_response, - ) - - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "invalid_response" - - -async def test_import_already_configured(hass): - """Test we handle already configured from import.""" - - entry = MockConfigEntry( - domain=config_flow.DOMAIN, - data=VALID_CONFIG, - ) - entry.add_to_hass(hass) - - with patch( - "homeassistant.components.foscam.config_flow.FoscamCamera", - ) as mock_foscam_camera: - setup_mock_foscam_camera(mock_foscam_camera) - - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=VALID_CONFIG, - ) - - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - -async def test_import_unknown_exception(hass): - """Test we handle unknown exceptions from import.""" - - with patch( - "homeassistant.components.foscam.config_flow.FoscamCamera", - ) as mock_foscam_camera: - mock_foscam_camera.side_effect = Exception("test") - - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=VALID_CONFIG, - ) - - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "unknown" From 36d3fb15f7a4a0b9ed9ac7e654d7c6e4d4edc390 Mon Sep 17 00:00:00 2001 From: MattWestb <49618193+MattWestb@users.noreply.github.com> Date: Tue, 14 Dec 2021 14:49:00 +0100 Subject: [PATCH 0417/2644] Fix ZHA unoccupied setpoints. (#61791) ATTR_UNOCCP_HEAT_SETPT and ATTR_UNOCCP_COOL_SETPT is mixed up. Fixing so heating is heating and cooling is colling. --- homeassistant/components/zha/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index c6ad3dcc89e..f895ca1a733 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -184,11 +184,11 @@ class Thermostat(ZhaEntity, ClimateEntity): unoccupied_cooling_setpoint = self._thrm.unoccupied_cooling_setpoint if unoccupied_cooling_setpoint is not None: - data[ATTR_UNOCCP_HEAT_SETPT] = unoccupied_cooling_setpoint + data[ATTR_UNOCCP_COOL_SETPT] = unoccupied_cooling_setpoint unoccupied_heating_setpoint = self._thrm.unoccupied_heating_setpoint if unoccupied_heating_setpoint is not None: - data[ATTR_UNOCCP_COOL_SETPT] = unoccupied_heating_setpoint + data[ATTR_UNOCCP_HEAT_SETPT] = unoccupied_heating_setpoint return data @property From c02aae58fb49963bce070e512e24a765434f7e5c Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 14 Dec 2021 15:12:19 +0100 Subject: [PATCH 0418/2644] Add twinkly DHCP support (#61434) * Add twinkly DHCP support * fix typing import * fix format * Fix imports v2 * Using IP * Fix tests * Apply suggestions from code review Thanks @bdraco Co-authored-by: J. Nick Koston * fix black * Add confirm step * Add more tests * Update homeassistant/components/twinkly/config_flow.py Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- .../components/twinkly/config_flow.py | 70 ++++++++++++--- .../components/twinkly/manifest.json | 1 + homeassistant/components/twinkly/strings.json | 3 + homeassistant/generated/dhcp.py | 4 + tests/components/twinkly/test_config_flow.py | 85 ++++++++++++++++++- 5 files changed, 152 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/twinkly/config_flow.py b/homeassistant/components/twinkly/config_flow.py index 0a9adf76e0e..a1bc8332caa 100644 --- a/homeassistant/components/twinkly/config_flow.py +++ b/homeassistant/components/twinkly/config_flow.py @@ -1,13 +1,16 @@ """Config flow to configure the Twinkly integration.""" +from __future__ import annotations import asyncio import logging +from typing import Any from aiohttp import ClientError import twinkly_client from voluptuous import Required, Schema -from homeassistant import config_entries +from homeassistant import config_entries, data_entry_flow +from homeassistant.components import dhcp from homeassistant.const import CONF_HOST from .const import ( @@ -29,6 +32,10 @@ class TwinklyConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovered_device: tuple[dict[str, Any], str] | None = None + async def async_step_user(self, user_input=None): """Handle config steps.""" host = user_input[CONF_HOST] if user_input else None @@ -43,15 +50,8 @@ class TwinklyConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(device_info[DEV_ID]) self._abort_if_unique_id_configured() - return self.async_create_entry( - title=device_info[DEV_NAME], - data={ - CONF_ENTRY_HOST: host, - CONF_ENTRY_ID: device_info[DEV_ID], - CONF_ENTRY_NAME: device_info[DEV_NAME], - CONF_ENTRY_MODEL: device_info[DEV_MODEL], - }, - ) + return self._create_entry_from_device(device_info, host) + except (asyncio.TimeoutError, ClientError) as err: _LOGGER.info("Cannot reach Twinkly '%s' (client)", host, exc_info=err) errors[CONF_HOST] = "cannot_connect" @@ -59,3 +59,53 @@ class TwinklyConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="user", data_schema=Schema(schema), errors=errors ) + + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> data_entry_flow.FlowResult: + """Handle dhcp discovery for twinkly.""" + self._async_abort_entries_match({CONF_ENTRY_HOST: discovery_info.ip}) + device_info = await twinkly_client.TwinklyClient( + discovery_info.ip + ).get_device_info() + await self.async_set_unique_id(device_info[DEV_ID]) + self._abort_if_unique_id_configured( + updates={CONF_ENTRY_HOST: discovery_info.ip} + ) + + self._discovered_device = (device_info, discovery_info.ip) + return await self.async_step_discovery_confirm() + + async def async_step_discovery_confirm( + self, user_input=None + ) -> data_entry_flow.FlowResult: + """Confirm discovery.""" + assert self._discovered_device is not None + device_info, host = self._discovered_device + + if user_input is not None: + return self._create_entry_from_device(device_info, host) + + self._set_confirm_only() + placeholders = { + "model": device_info[DEV_MODEL], + "name": device_info[DEV_NAME], + "host": host, + } + return self.async_show_form( + step_id="discovery_confirm", description_placeholders=placeholders + ) + + def _create_entry_from_device( + self, device_info: dict[str, Any], host: str + ) -> data_entry_flow.FlowResult: + """Create entry from device data.""" + return self.async_create_entry( + title=device_info[DEV_NAME], + data={ + CONF_ENTRY_HOST: host, + CONF_ENTRY_ID: device_info[DEV_ID], + CONF_ENTRY_NAME: device_info[DEV_NAME], + CONF_ENTRY_MODEL: device_info[DEV_MODEL], + }, + ) diff --git a/homeassistant/components/twinkly/manifest.json b/homeassistant/components/twinkly/manifest.json index 58c2d9b763b..9cc9ce08254 100644 --- a/homeassistant/components/twinkly/manifest.json +++ b/homeassistant/components/twinkly/manifest.json @@ -6,5 +6,6 @@ "dependencies": [], "codeowners": ["@dr1rrb"], "config_flow": true, + "dhcp": [{ "hostname": "twinkly_*" }], "iot_class": "local_polling" } diff --git a/homeassistant/components/twinkly/strings.json b/homeassistant/components/twinkly/strings.json index 70e7f970b58..bda6cdee519 100644 --- a/homeassistant/components/twinkly/strings.json +++ b/homeassistant/components/twinkly/strings.json @@ -7,6 +7,9 @@ "data": { "host": "Host (or IP address) of your twinkly device" } + }, + "discovery_confirm": { + "description": "Do you want to setup {name} - {model} ({host})?" } }, "error": { diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index fae3df053f1..3fef7f71d53 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -520,6 +520,10 @@ DHCP = [ "domain": "tuya", "macaddress": "D81F12*" }, + { + "domain": "twinkly", + "hostname": "twinkly_*" + }, { "domain": "verisure", "macaddress": "0023C1*" diff --git a/tests/components/twinkly/test_config_flow.py b/tests/components/twinkly/test_config_flow.py index 46566bdf54b..5c4d3bfb098 100644 --- a/tests/components/twinkly/test_config_flow.py +++ b/tests/components/twinkly/test_config_flow.py @@ -1,8 +1,8 @@ """Tests for the config_flow of the twinly component.""" - from unittest.mock import patch from homeassistant import config_entries +from homeassistant.components import dhcp from homeassistant.components.twinkly.const import ( CONF_ENTRY_HOST, CONF_ENTRY_ID, @@ -13,6 +13,8 @@ from homeassistant.components.twinkly.const import ( from . import TEST_MODEL, ClientMock +from tests.common import MockConfigEntry + async def test_invalid_host(hass): """Test the failure when invalid host provided.""" @@ -60,3 +62,84 @@ async def test_success_flow(hass): CONF_ENTRY_NAME: client.id, CONF_ENTRY_MODEL: TEST_MODEL, } + + +async def test_dhcp_can_confirm(hass): + """Test DHCP discovery flow can confirm right away.""" + client = ClientMock() + with patch("twinkly_client.TwinklyClient", return_value=client): + result = await hass.config_entries.flow.async_init( + TWINKLY_DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + hostname="Twinkly_XYZ", + ip="1.2.3.4", + macaddress="aa:bb:cc:dd:ee:ff", + ), + ) + await hass.async_block_till_done() + + assert result["type"] == "form" + assert result["step_id"] == "discovery_confirm" + + +async def test_dhcp_success(hass): + """Test DHCP discovery flow success.""" + client = ClientMock() + with patch("twinkly_client.TwinklyClient", return_value=client): + result = await hass.config_entries.flow.async_init( + TWINKLY_DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + hostname="Twinkly_XYZ", + ip="1.2.3.4", + macaddress="aa:bb:cc:dd:ee:ff", + ), + ) + await hass.async_block_till_done() + + assert result["type"] == "form" + assert result["step_id"] == "discovery_confirm" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] == "create_entry" + assert result["title"] == client.id + assert result["data"] == { + CONF_ENTRY_HOST: "1.2.3.4", + CONF_ENTRY_ID: client.id, + CONF_ENTRY_NAME: client.id, + CONF_ENTRY_MODEL: TEST_MODEL, + } + + +async def test_dhcp_already_exists(hass): + """Test DHCP discovery flow that fails to connect.""" + client = ClientMock() + + entry = MockConfigEntry( + domain=TWINKLY_DOMAIN, + data={ + CONF_ENTRY_HOST: "1.2.3.4", + CONF_ENTRY_ID: client.id, + CONF_ENTRY_NAME: client.id, + CONF_ENTRY_MODEL: TEST_MODEL, + }, + unique_id=client.id, + ) + entry.add_to_hass(hass) + + with patch("twinkly_client.TwinklyClient", return_value=client): + result = await hass.config_entries.flow.async_init( + TWINKLY_DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + hostname="Twinkly_XYZ", + ip="1.2.3.4", + macaddress="aa:bb:cc:dd:ee:ff", + ), + ) + await hass.async_block_till_done() + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" From 2c26eae9b2c98bffa3f983fc1fa48c5c0e910d01 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 14 Dec 2021 18:32:17 +0100 Subject: [PATCH 0419/2644] Use new DeviceClass enums in iaqualink (#61805) Co-authored-by: epenet --- homeassistant/components/iaqualink/binary_sensor.py | 4 ++-- homeassistant/components/iaqualink/sensor.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/iaqualink/binary_sensor.py b/homeassistant/components/iaqualink/binary_sensor.py index 26d446541e6..4694c5af86f 100644 --- a/homeassistant/components/iaqualink/binary_sensor.py +++ b/homeassistant/components/iaqualink/binary_sensor.py @@ -1,7 +1,7 @@ """Support for Aqualink temperature sensors.""" from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_COLD, DOMAIN, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -40,5 +40,5 @@ class HassAqualinkBinarySensor(AqualinkEntity, BinarySensorEntity): def device_class(self) -> str: """Return the class of the binary sensor.""" if self.name == "Freeze Protection": - return DEVICE_CLASS_COLD + return BinarySensorDeviceClass.COLD return None diff --git a/homeassistant/components/iaqualink/sensor.py b/homeassistant/components/iaqualink/sensor.py index 61e4560c3be..7724fbf28ee 100644 --- a/homeassistant/components/iaqualink/sensor.py +++ b/homeassistant/components/iaqualink/sensor.py @@ -1,9 +1,9 @@ """Support for Aqualink temperature sensors.""" from __future__ import annotations -from homeassistant.components.sensor import DOMAIN, SensorEntity +from homeassistant.components.sensor import DOMAIN, SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import HomeAssistant from . import AqualinkEntity @@ -55,5 +55,5 @@ class HassAqualinkSensor(AqualinkEntity, SensorEntity): def device_class(self) -> str | None: """Return the class of the sensor.""" if self.dev.name.endswith("_temp"): - return DEVICE_CLASS_TEMPERATURE + return SensorDeviceClass.TEMPERATURE return None From 1692fab66477e26aa93264e4a6b0e29f32d123fc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 14 Dec 2021 18:32:33 +0100 Subject: [PATCH 0420/2644] Use new enums in integration (#61803) Co-authored-by: epenet --- homeassistant/components/integration/sensor.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 463cb3b4e05..ee7b4b8e86e 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -5,11 +5,10 @@ import logging import voluptuous as vol from homeassistant.components.sensor import ( - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, PLATFORM_SCHEMA, - STATE_CLASS_TOTAL, + SensorDeviceClass, SensorEntity, + SensorStateClass, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -119,7 +118,7 @@ class IntegrationSensor(RestoreEntity, SensorEntity): self._unit_of_measurement = unit_of_measurement self._unit_prefix = UNIT_PREFIXES[unit_prefix] self._unit_time = UNIT_TIME[unit_time] - self._attr_state_class = STATE_CLASS_TOTAL + self._attr_state_class = SensorStateClass.TOTAL async def async_added_to_hass(self): """Handle entity which will be added.""" @@ -149,9 +148,10 @@ class IntegrationSensor(RestoreEntity, SensorEntity): ) if ( self.device_class is None - and new_state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER + and new_state.attributes.get(ATTR_DEVICE_CLASS) + == SensorDeviceClass.POWER ): - self._attr_device_class = DEVICE_CLASS_ENERGY + self._attr_device_class = SensorDeviceClass.ENERGY if ( old_state is None From e203b85303767e04c578181b3a3385a55bf6d5f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Tue, 14 Dec 2021 18:40:47 +0100 Subject: [PATCH 0421/2644] Tibber, update library, fixes #61525 (#61813) --- homeassistant/components/tibber/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index a653e91b991..c0b047a2856 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -2,7 +2,7 @@ "domain": "tibber", "name": "Tibber", "documentation": "https://www.home-assistant.io/integrations/tibber", - "requirements": ["pyTibber==0.21.0"], + "requirements": ["pyTibber==0.21.1"], "codeowners": ["@danielhiversen"], "quality_scale": "silver", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 046c2270411..7ee8e65c9ca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1336,7 +1336,7 @@ pyRFXtrx==0.27.0 # pySwitchmate==0.4.6 # homeassistant.components.tibber -pyTibber==0.21.0 +pyTibber==0.21.1 # homeassistant.components.dlink pyW215==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f35c90313e7..e1b470670a5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -817,7 +817,7 @@ pyMetno==0.9.0 pyRFXtrx==0.27.0 # homeassistant.components.tibber -pyTibber==0.21.0 +pyTibber==0.21.1 # homeassistant.components.nextbus py_nextbusnext==0.1.5 From 93c05b627fa4dc859eea08af6c83226211e975cc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 14 Dec 2021 18:46:02 +0100 Subject: [PATCH 0422/2644] Use new SensorDeviceClass in ihc (#61799) Co-authored-by: epenet --- homeassistant/components/ihc/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ihc/sensor.py b/homeassistant/components/ihc/sensor.py index 17c17980c95..d032043b932 100644 --- a/homeassistant/components/ihc/sensor.py +++ b/homeassistant/components/ihc/sensor.py @@ -1,6 +1,6 @@ """Support for IHC sensors.""" -from homeassistant.components.sensor import SensorEntity -from homeassistant.const import CONF_UNIT_OF_MEASUREMENT, DEVICE_CLASS_TEMPERATURE +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.const import CONF_UNIT_OF_MEASUREMENT from homeassistant.util.unit_system import TEMPERATURE_UNITS from . import IHC_CONTROLLER, IHC_INFO @@ -42,7 +42,7 @@ class IHCSensor(IHCDevice, SensorEntity): def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" return ( - DEVICE_CLASS_TEMPERATURE + SensorDeviceClass.TEMPERATURE if self._unit_of_measurement in TEMPERATURE_UNITS else None ) From d254d963b88cee0e05355eca047d0c6f5ccaac6c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 14 Dec 2021 18:46:38 +0100 Subject: [PATCH 0423/2644] Use SensorDeviceClass in incomfort (#61800) Co-authored-by: epenet --- homeassistant/components/incomfort/sensor.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/incomfort/sensor.py b/homeassistant/components/incomfort/sensor.py index 0ce2372867a..e0c781c3fc5 100644 --- a/homeassistant/components/incomfort/sensor.py +++ b/homeassistant/components/incomfort/sensor.py @@ -6,15 +6,11 @@ from typing import Any from homeassistant.components.sensor import ( DOMAIN as SENSOR_DOMAIN, + SensorDeviceClass, SensorEntity, SensorEntityDescription, ) -from homeassistant.const import ( - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, - PRESSURE_BAR, - TEMP_CELSIUS, -) +from homeassistant.const import PRESSURE_BAR, TEMP_CELSIUS from homeassistant.util import slugify from . import DOMAIN, IncomfortChild @@ -35,20 +31,20 @@ SENSOR_TYPES: tuple[IncomfortSensorEntityDescription, ...] = ( IncomfortSensorEntityDescription( key="pressure", name=INCOMFORT_PRESSURE, - device_class=DEVICE_CLASS_PRESSURE, + device_class=SensorDeviceClass.PRESSURE, native_unit_of_measurement=PRESSURE_BAR, ), IncomfortSensorEntityDescription( key="heater_temp", name=INCOMFORT_HEATER_TEMP, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, extra_key="is_pumping", ), IncomfortSensorEntityDescription( key="tap_temp", name=INCOMFORT_TAP_TEMP, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, extra_key="is_tapping", ), From ef2a28cce2fd5075e1877585115823c11754a1af Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 14 Dec 2021 18:48:06 +0100 Subject: [PATCH 0424/2644] Use new BinarySensorDeviceClass in insteon (#61801) Co-authored-by: epenet --- .../components/insteon/binary_sensor.py | 33 +++++++------------ 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/insteon/binary_sensor.py b/homeassistant/components/insteon/binary_sensor.py index 69a4a5f5280..29c2280b6fc 100644 --- a/homeassistant/components/insteon/binary_sensor.py +++ b/homeassistant/components/insteon/binary_sensor.py @@ -14,17 +14,8 @@ from pyinsteon.groups import ( ) from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_DOOR, - DEVICE_CLASS_GAS, - DEVICE_CLASS_LIGHT, - DEVICE_CLASS_MOISTURE, - DEVICE_CLASS_MOTION, - DEVICE_CLASS_OPENING, - DEVICE_CLASS_PROBLEM, - DEVICE_CLASS_SAFETY, - DEVICE_CLASS_SMOKE, DOMAIN as BINARY_SENSOR_DOMAIN, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.core import callback @@ -35,17 +26,17 @@ from .insteon_entity import InsteonEntity from .utils import async_add_insteon_entities SENSOR_TYPES = { - OPEN_CLOSE_SENSOR: DEVICE_CLASS_OPENING, - MOTION_SENSOR: DEVICE_CLASS_MOTION, - DOOR_SENSOR: DEVICE_CLASS_DOOR, - LEAK_SENSOR_WET: DEVICE_CLASS_MOISTURE, - LIGHT_SENSOR: DEVICE_CLASS_LIGHT, - LOW_BATTERY: DEVICE_CLASS_BATTERY, - CO_SENSOR: DEVICE_CLASS_GAS, - SMOKE_SENSOR: DEVICE_CLASS_SMOKE, - TEST_SENSOR: DEVICE_CLASS_SAFETY, - SENSOR_MALFUNCTION: DEVICE_CLASS_PROBLEM, - HEARTBEAT: DEVICE_CLASS_PROBLEM, + OPEN_CLOSE_SENSOR: BinarySensorDeviceClass.OPENING, + MOTION_SENSOR: BinarySensorDeviceClass.MOTION, + DOOR_SENSOR: BinarySensorDeviceClass.DOOR, + LEAK_SENSOR_WET: BinarySensorDeviceClass.MOISTURE, + LIGHT_SENSOR: BinarySensorDeviceClass.LIGHT, + LOW_BATTERY: BinarySensorDeviceClass.BATTERY, + CO_SENSOR: BinarySensorDeviceClass.GAS, + SMOKE_SENSOR: BinarySensorDeviceClass.SMOKE, + TEST_SENSOR: BinarySensorDeviceClass.SAFETY, + SENSOR_MALFUNCTION: BinarySensorDeviceClass.PROBLEM, + HEARTBEAT: BinarySensorDeviceClass.PROBLEM, } From ffccc5bfa50042d23adb41326e6a1c3745eef126 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 14 Dec 2021 19:39:18 +0100 Subject: [PATCH 0425/2644] Use new enums in iotawatt (#61802) Co-authored-by: epenet --- homeassistant/components/iotawatt/sensor.py | 37 +++++++++------------ 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/iotawatt/sensor.py b/homeassistant/components/iotawatt/sensor.py index 1da5100ea9f..2da40ae1b78 100644 --- a/homeassistant/components/iotawatt/sensor.py +++ b/homeassistant/components/iotawatt/sensor.py @@ -8,17 +8,12 @@ import logging from iotawattpy.sensor import Sensor from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.const import ( - DEVICE_CLASS_CURRENT, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_POWER_FACTOR, - DEVICE_CLASS_VOLTAGE, ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_WATT_HOUR, @@ -55,63 +50,63 @@ ENTITY_DESCRIPTION_KEY_MAP: dict[str, IotaWattSensorEntityDescription] = { "Amps": IotaWattSensorEntityDescription( "Amps", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - state_class=STATE_CLASS_MEASUREMENT, - device_class=DEVICE_CLASS_CURRENT, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.CURRENT, entity_registry_enabled_default=False, ), "Hz": IotaWattSensorEntityDescription( "Hz", native_unit_of_measurement=FREQUENCY_HERTZ, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, icon="mdi:flash", entity_registry_enabled_default=False, ), "PF": IotaWattSensorEntityDescription( "PF", native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, - device_class=DEVICE_CLASS_POWER_FACTOR, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.POWER_FACTOR, value=lambda value: value * 100, entity_registry_enabled_default=False, ), "Watts": IotaWattSensorEntityDescription( "Watts", native_unit_of_measurement=POWER_WATT, - state_class=STATE_CLASS_MEASUREMENT, - device_class=DEVICE_CLASS_POWER, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.POWER, ), "WattHours": IotaWattSensorEntityDescription( "WattHours", native_unit_of_measurement=ENERGY_WATT_HOUR, - state_class=STATE_CLASS_TOTAL, - device_class=DEVICE_CLASS_ENERGY, + state_class=SensorStateClass.TOTAL, + device_class=SensorDeviceClass.ENERGY, ), "VA": IotaWattSensorEntityDescription( "VA", native_unit_of_measurement=POWER_VOLT_AMPERE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, icon="mdi:flash", entity_registry_enabled_default=False, ), "VAR": IotaWattSensorEntityDescription( "VAR", native_unit_of_measurement=VOLT_AMPERE_REACTIVE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, icon="mdi:flash", entity_registry_enabled_default=False, ), "VARh": IotaWattSensorEntityDescription( "VARh", native_unit_of_measurement=VOLT_AMPERE_REACTIVE_HOURS, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, icon="mdi:flash", entity_registry_enabled_default=False, ), "Volts": IotaWattSensorEntityDescription( "Volts", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - state_class=STATE_CLASS_MEASUREMENT, - device_class=DEVICE_CLASS_VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, entity_registry_enabled_default=False, ), } From 3404be8bb0d2272c9d12f5dde5ce32f2c16c7c05 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 14 Dec 2021 19:42:12 +0100 Subject: [PATCH 0426/2644] Use SensorDeviceClass in icloud (#61804) Co-authored-by: epenet --- homeassistant/components/icloud/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/icloud/sensor.py b/homeassistant/components/icloud/sensor.py index 699f3e9baa3..bf5ff5860e3 100644 --- a/homeassistant/components/icloud/sensor.py +++ b/homeassistant/components/icloud/sensor.py @@ -3,9 +3,9 @@ from __future__ import annotations from typing import Any -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE +from homeassistant.const import PERCENTAGE from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo @@ -53,7 +53,7 @@ def add_entities(account, async_add_entities, tracked): class IcloudDeviceBatterySensor(SensorEntity): """Representation of a iCloud device battery sensor.""" - _attr_device_class = DEVICE_CLASS_BATTERY + _attr_device_class = SensorDeviceClass.BATTERY _attr_native_unit_of_measurement = PERCENTAGE def __init__(self, account: IcloudAccount, device: IcloudDevice) -> None: From 67ddfcb218c9cba8a3dad1fa54539af9aa6e5f43 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 14 Dec 2021 19:59:17 +0100 Subject: [PATCH 0427/2644] Use new DeviceClass enums in hive (#61758) Co-authored-by: epenet --- homeassistant/components/hive/binary_sensor.py | 18 +++++++----------- homeassistant/components/hive/sensor.py | 4 ++-- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/hive/binary_sensor.py b/homeassistant/components/hive/binary_sensor.py index cd1bef406d2..5a346c2cc01 100644 --- a/homeassistant/components/hive/binary_sensor.py +++ b/homeassistant/components/hive/binary_sensor.py @@ -2,11 +2,7 @@ from datetime import timedelta from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_CONNECTIVITY, - DEVICE_CLASS_MOTION, - DEVICE_CLASS_OPENING, - DEVICE_CLASS_SMOKE, - DEVICE_CLASS_SOUND, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.helpers.entity import DeviceInfo @@ -15,12 +11,12 @@ from . import HiveEntity from .const import ATTR_MODE, DOMAIN DEVICETYPE = { - "contactsensor": DEVICE_CLASS_OPENING, - "motionsensor": DEVICE_CLASS_MOTION, - "Connectivity": DEVICE_CLASS_CONNECTIVITY, - "SMOKE_CO": DEVICE_CLASS_SMOKE, - "DOG_BARK": DEVICE_CLASS_SOUND, - "GLASS_BREAK": DEVICE_CLASS_SOUND, + "contactsensor": BinarySensorDeviceClass.OPENING, + "motionsensor": BinarySensorDeviceClass.MOTION, + "Connectivity": BinarySensorDeviceClass.CONNECTIVITY, + "SMOKE_CO": BinarySensorDeviceClass.SMOKE, + "DOG_BARK": BinarySensorDeviceClass.SOUND, + "GLASS_BREAK": BinarySensorDeviceClass.SOUND, } PARALLEL_UPDATES = 0 SCAN_INTERVAL = timedelta(seconds=15) diff --git a/homeassistant/components/hive/sensor.py b/homeassistant/components/hive/sensor.py index bca144cd59c..764c83cfff0 100644 --- a/homeassistant/components/hive/sensor.py +++ b/homeassistant/components/hive/sensor.py @@ -2,7 +2,7 @@ from datetime import timedelta -from homeassistant.components.sensor import DEVICE_CLASS_BATTERY, SensorEntity +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.helpers.entity import DeviceInfo from . import HiveEntity @@ -11,7 +11,7 @@ from .const import DOMAIN PARALLEL_UPDATES = 0 SCAN_INTERVAL = timedelta(seconds=15) DEVICETYPE = { - "Battery": {"unit": " % ", "type": DEVICE_CLASS_BATTERY}, + "Battery": {"unit": " % ", "type": SensorDeviceClass.BATTERY}, } From 6bf41325ca97b39d3005835deef3f9c2e8b448df Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 14 Dec 2021 19:59:47 +0100 Subject: [PATCH 0428/2644] Use new DeviceClass enum in home_connect (#61759) Co-authored-by: epenet --- homeassistant/components/home_connect/api.py | 4 ++-- homeassistant/components/home_connect/sensor.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/home_connect/api.py b/homeassistant/components/home_connect/api.py index cf14de7cc42..380688ba6ff 100644 --- a/homeassistant/components/home_connect/api.py +++ b/homeassistant/components/home_connect/api.py @@ -7,12 +7,12 @@ import homeconnect from homeconnect.api import HomeConnectError from homeassistant import config_entries, core +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ICON, CONF_DEVICE, CONF_ENTITIES, - DEVICE_CLASS_TIMESTAMP, PERCENTAGE, TIME_SECONDS, ) @@ -159,7 +159,7 @@ class DeviceWithPrograms(HomeConnectDevice): device. """ sensors = { - "Remaining Program Time": (None, None, DEVICE_CLASS_TIMESTAMP, 1), + "Remaining Program Time": (None, None, SensorDeviceClass.TIMESTAMP, 1), "Duration": (TIME_SECONDS, "mdi:update", None, 1), "Program Progress": (PERCENTAGE, "mdi:progress-clock", None, 1), } diff --git a/homeassistant/components/home_connect/sensor.py b/homeassistant/components/home_connect/sensor.py index 910bec3e6ab..8c68113a055 100644 --- a/homeassistant/components/home_connect/sensor.py +++ b/homeassistant/components/home_connect/sensor.py @@ -3,8 +3,8 @@ from datetime import timedelta import logging -from homeassistant.components.sensor import SensorEntity -from homeassistant.const import CONF_ENTITIES, DEVICE_CLASS_TIMESTAMP +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.const import CONF_ENTITIES import homeassistant.util.dt as dt_util from .const import ATTR_VALUE, BSH_OPERATION_STATE, DOMAIN @@ -57,7 +57,7 @@ class HomeConnectSensor(HomeConnectEntity, SensorEntity): if self._key not in status: self._state = None else: - if self.device_class == DEVICE_CLASS_TIMESTAMP: + if self.device_class == SensorDeviceClass.TIMESTAMP: if ATTR_VALUE not in status[self._key]: self._state = None elif ( From d9105b071ad3344b8868078a70fd27a10be00081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Tue, 14 Dec 2021 20:06:50 +0100 Subject: [PATCH 0429/2644] Last reset is no longer deprecated (#61816) --- homeassistant/components/sensor/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 75db36b91b2..14adfe85d41 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -59,7 +59,7 @@ from .const import CONF_STATE_CLASS # noqa: F401 _LOGGER: Final = logging.getLogger(__name__) -ATTR_LAST_RESET: Final = "last_reset" # Deprecated, to be removed in 2021.11 +ATTR_LAST_RESET: Final = "last_reset" ATTR_STATE_CLASS: Final = "state_class" DOMAIN: Final = "sensor" @@ -215,7 +215,7 @@ class SensorEntityDescription(EntityDescription): """A class that describes sensor entities.""" device_class: SensorDeviceClass | str | None = None - last_reset: datetime | None = None # Deprecated, to be removed in 2021.11 + last_reset: datetime | None = None native_unit_of_measurement: str | None = None state_class: SensorStateClass | str | None = None unit_of_measurement: None = None # Type override, use native_unit_of_measurement @@ -247,7 +247,7 @@ class SensorEntity(Entity): entity_description: SensorEntityDescription _attr_device_class: SensorDeviceClass | str | None - _attr_last_reset: datetime | None # Deprecated, to be removed in 2021.11 + _attr_last_reset: datetime | None _attr_native_unit_of_measurement: str | None _attr_native_value: StateType | date | datetime = None _attr_state_class: SensorStateClass | str | None @@ -280,7 +280,7 @@ class SensorEntity(Entity): return None @property - def last_reset(self) -> datetime | None: # Deprecated, to be removed in 2021.11 + def last_reset(self) -> datetime | None: """Return the time when the sensor was last reset, if any.""" if hasattr(self, "_attr_last_reset"): return self._attr_last_reset From cf09d1b604717ed43e014cd6601b0cc5d192520d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 14 Dec 2021 20:14:50 +0100 Subject: [PATCH 0430/2644] Improve warnings when a zone trigger is referencing a none-existing zone (#61763) --- homeassistant/components/zone/trigger.py | 12 +++++++ tests/components/zone/test_trigger.py | 46 ++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/homeassistant/components/zone/trigger.py b/homeassistant/components/zone/trigger.py index 373727e3f4d..a008a30007a 100644 --- a/homeassistant/components/zone/trigger.py +++ b/homeassistant/components/zone/trigger.py @@ -1,4 +1,6 @@ """Offer zone automation rules.""" +import logging + import voluptuous as vol from homeassistant.const import ( @@ -25,6 +27,8 @@ EVENT_ENTER = "enter" EVENT_LEAVE = "leave" DEFAULT_EVENT = EVENT_ENTER +_LOGGER = logging.getLogger(__name__) + _EVENT_DESCRIPTION = {EVENT_ENTER: "entering", EVENT_LEAVE: "leaving"} _TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( @@ -76,6 +80,14 @@ async def async_attach_trigger( return zone_state = hass.states.get(zone_entity_id) + if not zone_state: + _LOGGER.warning( + "Automation '%s' is referencing non-existing zone '%s' in a zone trigger", + automation_info["name"], + zone_entity_id, + ) + return + from_match = condition.zone(hass, zone_state, from_s) if from_s else False to_match = condition.zone(hass, zone_state, to_s) if to_s else False diff --git a/tests/components/zone/test_trigger.py b/tests/components/zone/test_trigger.py index ee62dd5df3a..b742032f863 100644 --- a/tests/components/zone/test_trigger.py +++ b/tests/components/zone/test_trigger.py @@ -307,3 +307,49 @@ async def test_zone_condition(hass, calls): hass.bus.async_fire("test_event") await hass.async_block_till_done() assert len(calls) == 1 + + +async def test_unknown_zone(hass, calls, caplog): + """Test for firing on zone enter.""" + context = Context() + hass.states.async_set( + "test.entity", "hello", {"latitude": 32.881011, "longitude": -117.234758} + ) + await hass.async_block_till_done() + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "My Automation", + "trigger": { + "platform": "zone", + "entity_id": "test.entity", + "zone": "zone.no_such_zone", + "event": "enter", + }, + "action": { + "service": "test.automation", + }, + } + }, + ) + + assert ( + "Automation 'My Automation' is referencing non-existing zone 'zone.no_such_zone' in a zone trigger" + not in caplog.text + ) + + hass.states.async_set( + "test.entity", + "hello", + {"latitude": 32.880586, "longitude": -117.237564}, + context=context, + ) + await hass.async_block_till_done() + + assert ( + "Automation 'My Automation' is referencing non-existing zone 'zone.no_such_zone' in a zone trigger" + in caplog.text + ) From 01c8e5f49d8b6a1157dd1752d5604c8afa76adf2 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 14 Dec 2021 20:24:37 +0100 Subject: [PATCH 0431/2644] Fix turn_off with transition for grouped Hue lights (#61728) * fix turn_off with transition for grouped hue lights * add test --- homeassistant/components/hue/v2/group.py | 31 +++++++++++++++++++----- tests/components/hue/test_light_v2.py | 31 +++++++++++++++++++++++- 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/hue/v2/group.py b/homeassistant/components/hue/v2/group.py index 4427d4cb415..c5f7ae5d926 100644 --- a/homeassistant/components/hue/v2/group.py +++ b/homeassistant/components/hue/v2/group.py @@ -193,12 +193,31 @@ class GroupedHueLight(HueBaseEntity, LightEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" - await self.bridge.async_request_call( - self.controller.set_state, - id=self.resource.id, - on=False, - allowed_errors=ALLOWED_ERRORS, - ) + transition = kwargs.get(ATTR_TRANSITION) + if transition is not None: + # hue transition duration is in milliseconds + transition = int(transition * 1000) + + # NOTE: a grouped_light can only handle turn on/off + # To set other features, you'll have to control the attached lights + if transition is None: + await self.bridge.async_request_call( + self.controller.set_state, + id=self.resource.id, + on=False, + allowed_errors=ALLOWED_ERRORS, + ) + return + + # redirect all other feature commands to underlying lights + for light in self.controller.get_lights(self.resource.id): + await self.bridge.async_request_call( + self.api.lights.set_state, + light.id, + on=False, + transition_time=transition, + allowed_errors=ALLOWED_ERRORS, + ) @callback def on_update(self) -> None: diff --git a/tests/components/hue/test_light_v2.py b/tests/components/hue/test_light_v2.py index 7a51f833207..70a5af6d98e 100644 --- a/tests/components/hue/test_light_v2.py +++ b/tests/components/hue/test_light_v2.py @@ -306,7 +306,12 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data): await hass.services.async_call( "light", "turn_on", - {"entity_id": test_light_id, "brightness_pct": 100, "xy_color": (0.123, 0.123)}, + { + "entity_id": test_light_id, + "brightness_pct": 100, + "xy_color": (0.123, 0.123), + "transition": 6, + }, blocking=True, ) @@ -319,6 +324,9 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data): ) assert mock_bridge_v2.mock_requests[index]["json"]["color"]["xy"]["x"] == 0.123 assert mock_bridge_v2.mock_requests[index]["json"]["color"]["xy"]["y"] == 0.123 + assert ( + mock_bridge_v2.mock_requests[index]["json"]["dynamics"]["duration"] == 6000 + ) # Now generate update events by emitting the json we've sent as incoming events for index in range(0, 3): @@ -357,3 +365,24 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data): test_light = hass.states.get(test_light_id) assert test_light is not None assert test_light.state == "off" + + # Test calling the turn off service on a grouped light with transition + mock_bridge_v2.mock_requests.clear() + test_light_id = "light.test_zone" + await hass.services.async_call( + "light", + "turn_off", + { + "entity_id": test_light_id, + "transition": 6, + }, + blocking=True, + ) + + # PUT request should have been sent to ALL group lights with correct params + assert len(mock_bridge_v2.mock_requests) == 3 + for index in range(0, 3): + assert mock_bridge_v2.mock_requests[index]["json"]["on"]["on"] is False + assert ( + mock_bridge_v2.mock_requests[index]["json"]["dynamics"]["duration"] == 6000 + ) From b59f39b214310db6412cb3885483f3c043396539 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 14 Dec 2021 21:56:36 +0100 Subject: [PATCH 0432/2644] Use SensorStateClass in iqvia (#61823) --- homeassistant/components/iqvia/sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/iqvia/sensor.py b/homeassistant/components/iqvia/sensor.py index d4c01c6fb67..46da1aea0aa 100644 --- a/homeassistant/components/iqvia/sensor.py +++ b/homeassistant/components/iqvia/sensor.py @@ -7,9 +7,9 @@ from typing import NamedTuple import numpy as np from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_STATE @@ -99,7 +99,7 @@ INDEX_SENSOR_DESCRIPTIONS = ( key=TYPE_ALLERGY_TODAY, name="Allergy Index: Today", icon="mdi:flower", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_ALLERGY_TOMORROW, @@ -110,7 +110,7 @@ INDEX_SENSOR_DESCRIPTIONS = ( key=TYPE_ASTHMA_TODAY, name="Asthma Index: Today", icon="mdi:flower", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_ASTHMA_TOMORROW, @@ -121,7 +121,7 @@ INDEX_SENSOR_DESCRIPTIONS = ( key=TYPE_DISEASE_TODAY, name="Cold & Flu Index: Today", icon="mdi:pill", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), ) From 12349eb70efc519ec3b4d132dfd1164f62714a96 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 14 Dec 2021 22:11:26 +0100 Subject: [PATCH 0433/2644] Use SensorDeviceClass in ipp (#61822) --- homeassistant/components/ipp/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ipp/sensor.py b/homeassistant/components/ipp/sensor.py index 2cc7d00a633..6e6cee696bc 100644 --- a/homeassistant/components/ipp/sensor.py +++ b/homeassistant/components/ipp/sensor.py @@ -4,9 +4,9 @@ from __future__ import annotations from datetime import datetime, timedelta from typing import Any -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_LOCATION, DEVICE_CLASS_TIMESTAMP, PERCENTAGE +from homeassistant.const import ATTR_LOCATION, PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import utcnow @@ -170,7 +170,7 @@ class IPPPrinterSensor(IPPSensor): class IPPUptimeSensor(IPPSensor): """Defines a IPP uptime sensor.""" - _attr_device_class = DEVICE_CLASS_TIMESTAMP + _attr_device_class = SensorDeviceClass.TIMESTAMP def __init__( self, entry_id: str, unique_id: str, coordinator: IPPDataUpdateCoordinator From 158ff601b6511169f0a1d7d36627bc8c0f25660e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 14 Dec 2021 22:11:54 +0100 Subject: [PATCH 0434/2644] Use SensorDeviceClass in islamic_prayer_times (#61824) --- homeassistant/components/islamic_prayer_times/sensor.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/islamic_prayer_times/sensor.py b/homeassistant/components/islamic_prayer_times/sensor.py index 38a95fb803b..916d571fd70 100644 --- a/homeassistant/components/islamic_prayer_times/sensor.py +++ b/homeassistant/components/islamic_prayer_times/sensor.py @@ -1,7 +1,6 @@ """Platform to retrieve Islamic prayer times information for Home Assistant.""" -from homeassistant.components.sensor import SensorEntity -from homeassistant.const import DEVICE_CLASS_TIMESTAMP +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.dt as dt_util @@ -23,7 +22,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class IslamicPrayerTimeSensor(SensorEntity): """Representation of an Islamic prayer time sensor.""" - _attr_device_class = DEVICE_CLASS_TIMESTAMP + _attr_device_class = SensorDeviceClass.TIMESTAMP _attr_icon = PRAYER_TIMES_ICON _attr_should_poll = False From 002512f4ff1144a40391418aedda729d8f314ed1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 14 Dec 2021 22:12:34 +0100 Subject: [PATCH 0435/2644] Use SensorDeviceClass in jewish-calendar (#61827) --- homeassistant/components/jewish_calendar/sensor.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index cb3a85b78cd..d53f3702bcc 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -8,8 +8,12 @@ from typing import Any from hdate import HDate from hdate.zmanim import Zmanim -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription -from homeassistant.const import DEVICE_CLASS_TIMESTAMP, SUN_EVENT_SUNSET +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) +from homeassistant.const import SUN_EVENT_SUNSET from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.sun import get_astral_event_date @@ -255,7 +259,7 @@ class JewishCalendarSensor(SensorEntity): class JewishCalendarTimeSensor(JewishCalendarSensor): """Implement attrbutes for sensors returning times.""" - _attr_device_class = DEVICE_CLASS_TIMESTAMP + _attr_device_class = SensorDeviceClass.TIMESTAMP def get_state( self, daytime_date: HDate, after_shkia_date: HDate, after_tzais_date: HDate From 72556e5eaa21442f6e74a155d5a8729091cf2934 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Tue, 14 Dec 2021 22:36:55 +0100 Subject: [PATCH 0436/2644] Bump python-miio to 0.5.9.2 (#61831) --- homeassistant/components/xiaomi_miio/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json index 757fca8be1f..8de844cdd44 100644 --- a/homeassistant/components/xiaomi_miio/manifest.json +++ b/homeassistant/components/xiaomi_miio/manifest.json @@ -3,7 +3,7 @@ "name": "Xiaomi Miio", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/xiaomi_miio", - "requirements": ["construct==2.10.56", "micloud==0.4", "python-miio==0.5.9.1"], + "requirements": ["construct==2.10.56", "micloud==0.4", "python-miio==0.5.9.2"], "codeowners": ["@rytilahti", "@syssi", "@starkillerOG", "@bieniu"], "zeroconf": ["_miio._udp.local."], "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 7ee8e65c9ca..6b519383bbc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1916,7 +1916,7 @@ python-kasa==0.4.0 # python-lirc==1.2.3 # homeassistant.components.xiaomi_miio -python-miio==0.5.9.1 +python-miio==0.5.9.2 # homeassistant.components.mpd python-mpd2==3.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e1b470670a5..e936c06c121 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1157,7 +1157,7 @@ python-juicenet==1.0.2 python-kasa==0.4.0 # homeassistant.components.xiaomi_miio -python-miio==0.5.9.1 +python-miio==0.5.9.2 # homeassistant.components.nest python-nest==4.1.0 From 77843be01f54758d141003a6554ebe39171ac6a1 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 15 Dec 2021 00:13:42 +0000 Subject: [PATCH 0437/2644] [ci skip] Translation update --- .../components/adax/translations/ca.json | 4 +-- .../components/adax/translations/et.json | 4 +-- .../components/adax/translations/it.json | 4 +-- .../components/adax/translations/pl.json | 22 ++++++++++-- .../components/adax/translations/tr.json | 2 +- .../components/apple_tv/translations/pl.json | 18 ++++++---- .../aseko_pool_live/translations/pl.json | 20 +++++++++++ .../components/balboa/translations/pl.json | 2 +- .../binary_sensor/translations/pl.json | 2 ++ .../components/elmax/translations/pl.json | 34 +++++++++++++++++++ .../enphase_envoy/translations/pl.json | 3 +- .../components/fronius/translations/pl.json | 7 +++- .../components/knx/translations/pl.json | 5 +-- .../components/lcn/translations/ca.json | 2 +- .../components/lcn/translations/pl.json | 10 ++++++ .../components/nam/translations/pl.json | 2 +- .../components/nest/translations/pl.json | 11 ++++-- .../components/nina/translations/pl.json | 14 ++++++-- .../components/owntracks/translations/pl.json | 2 +- .../simplisafe/translations/pl.json | 3 +- .../components/tailscale/translations/pl.json | 12 +++++-- .../tesla_wall_connector/translations/pl.json | 30 ++++++++++++++++ .../tractive/translations/sensor.pl.json | 7 ++-- .../translations/pl.json | 17 ++++++++-- .../components/twinkly/translations/ca.json | 3 ++ .../components/twinkly/translations/de.json | 3 ++ .../components/twinkly/translations/en.json | 3 ++ .../components/twinkly/translations/et.json | 3 ++ .../components/twinkly/translations/fr.json | 3 ++ .../components/twinkly/translations/hu.json | 3 ++ .../components/twinkly/translations/it.json | 3 ++ .../components/twinkly/translations/pl.json | 3 ++ .../components/twinkly/translations/ru.json | 3 ++ .../twinkly/translations/zh-Hant.json | 3 ++ .../components/unifi/translations/pl.json | 12 +++---- .../components/vacuum/translations/pt-BR.json | 5 +++ .../wolflink/translations/sensor.et.json | 2 +- .../wolflink/translations/sensor.id.json | 1 + .../yale_smart_alarm/translations/pl.json | 3 +- .../translations/select.pl.json | 26 +++++++++++--- .../components/zwave_js/translations/pl.json | 2 ++ 41 files changed, 268 insertions(+), 50 deletions(-) create mode 100644 homeassistant/components/aseko_pool_live/translations/pl.json create mode 100644 homeassistant/components/elmax/translations/pl.json create mode 100644 homeassistant/components/lcn/translations/pl.json create mode 100644 homeassistant/components/tesla_wall_connector/translations/pl.json diff --git a/homeassistant/components/adax/translations/ca.json b/homeassistant/components/adax/translations/ca.json index f2313e56af5..4e9f5eeb317 100644 --- a/homeassistant/components/adax/translations/ca.json +++ b/homeassistant/components/adax/translations/ca.json @@ -19,8 +19,8 @@ }, "local": { "data": { - "wifi_pswd": "Contrasenya WiFi", - "wifi_ssid": "SSID WiFi" + "wifi_pswd": "Contrasenya Wi-Fi", + "wifi_ssid": "SSID Wi-Fi" }, "description": "Reinicia l'escalfador prement '+' i 'OK' fins que la pantalla mostri 'Reset'. A continuaci\u00f3 i abans de fer clic a Envia, mant\u00e9 premut el bot\u00f3 'OK' fins que el led blau comenci a parpellejar. La configuraci\u00f3 de l'escalfador pot trigar uns minuts." }, diff --git a/homeassistant/components/adax/translations/et.json b/homeassistant/components/adax/translations/et.json index 414e7b1022f..2d154614295 100644 --- a/homeassistant/components/adax/translations/et.json +++ b/homeassistant/components/adax/translations/et.json @@ -19,8 +19,8 @@ }, "local": { "data": { - "wifi_pswd": "Wifi salas\u00f5na", - "wifi_ssid": "Wifi ssid" + "wifi_pswd": "Wi-Fi salas\u00f5na", + "wifi_ssid": "Wi-Fi SSID" }, "description": "L\u00e4htesta k\u00fctteseade, vajutades + ja OK kuni ekraanil kuvatakse \"Reset\". Seej\u00e4rel vajuta ja hoia kerisel nuppu OK kuni sinine led hakkab vilkuma enne nupu Edasta vajutamist. K\u00fctteseadme konfigureerimine v\u00f5ib v\u00f5tta aega m\u00f5ni minut." }, diff --git a/homeassistant/components/adax/translations/it.json b/homeassistant/components/adax/translations/it.json index b331b8a5f65..6af9bf7743c 100644 --- a/homeassistant/components/adax/translations/it.json +++ b/homeassistant/components/adax/translations/it.json @@ -19,8 +19,8 @@ }, "local": { "data": { - "wifi_pswd": "Password WiFi", - "wifi_ssid": "SSID Wifi" + "wifi_pswd": "Password Wi-Fi", + "wifi_ssid": "SSID Wi-Fi" }, "description": "Ripristinare il riscaldatore premendo + e OK finch\u00e9 il display non mostra 'Reset'. Quindi premere e tenere premuto il pulsante OK sul riscaldatore fino a quando il led blu inizia a lampeggiare prima di premere Invia. La configurazione del riscaldatore potrebbe richiedere alcuni minuti." }, diff --git a/homeassistant/components/adax/translations/pl.json b/homeassistant/components/adax/translations/pl.json index 05d2f4a918c..2a8e1016805 100644 --- a/homeassistant/components/adax/translations/pl.json +++ b/homeassistant/components/adax/translations/pl.json @@ -1,19 +1,37 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "heater_not_available": "Grzejnik niedost\u0119pny. Spr\u00f3buj go zresetowa\u0107, naciskaj\u0105c + i OK przez kilka sekund.", + "heater_not_found": "Nie znaleziono grzejnika. Spr\u00f3buj przesun\u0105\u0107 grzejnik bli\u017cej komputera z Home Assistant.", + "invalid_auth": "Niepoprawne uwierzytelnienie" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie" }, "step": { + "cloud": { + "data": { + "account_id": "Identyfikator konta", + "password": "Has\u0142o" + } + }, + "local": { + "data": { + "wifi_pswd": "Has\u0142o WiFi", + "wifi_ssid": "SSID WiFi" + }, + "description": "Zresetuj grzejnik, naciskaj\u0105c + i OK, a\u017c na wy\u015bwietlaczu pojawi si\u0119 \u201eReset\u201d. Nast\u0119pnie naci\u015bnij i przytrzymaj przycisk OK na grzejniku, a\u017c niebieska dioda zacznie miga\u0107 przed naci\u015bni\u0119ciem przycisku \"Zatwierd\u017a\". Konfiguracja grzejnika mo\u017ce zaj\u0105\u0107 kilka minut." + }, "user": { "data": { "account_id": "Identyfikator konta", + "connection_type": "Wybierz typ po\u0142\u0105czenia", "host": "Nazwa hosta lub adres IP", "password": "Has\u0142o" - } + }, + "description": "Wybierz typ po\u0142\u0105czenia. \"Lokalny\" wymaga grzejnik\u00f3w z bluetooth." } } } diff --git a/homeassistant/components/adax/translations/tr.json b/homeassistant/components/adax/translations/tr.json index 56406fed6ec..bd7ef0fb6e9 100644 --- a/homeassistant/components/adax/translations/tr.json +++ b/homeassistant/components/adax/translations/tr.json @@ -20,7 +20,7 @@ "local": { "data": { "wifi_pswd": "Kablosuz a\u011f parolas\u0131", - "wifi_ssid": "Wifi ssid" + "wifi_ssid": "Wifi A\u011f Ad\u0131" }, "description": "Ekranda 'S\u0131f\u0131rla' g\u00f6r\u00fcnene kadar + ve OK tu\u015flar\u0131na basarak \u0131s\u0131t\u0131c\u0131y\u0131 s\u0131f\u0131rlay\u0131n. Ard\u0131ndan G\u00f6nder'e basmadan \u00f6nce mavi led yan\u0131p s\u00f6nmeye ba\u015flayana kadar \u0131s\u0131t\u0131c\u0131daki OK d\u00fc\u011fmesini bas\u0131l\u0131 tutun. Is\u0131t\u0131c\u0131y\u0131 yap\u0131land\u0131rmak birka\u00e7 dakika s\u00fcrebilir." }, diff --git a/homeassistant/components/apple_tv/translations/pl.json b/homeassistant/components/apple_tv/translations/pl.json index 7936f1ccea6..1ba9b46dc3b 100644 --- a/homeassistant/components/apple_tv/translations/pl.json +++ b/homeassistant/components/apple_tv/translations/pl.json @@ -1,12 +1,16 @@ { "config": { "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "already_configured_device": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "already_in_progress": "Konfiguracja jest ju\u017c w toku", "backoff": "Urz\u0105dzenie w tej chwili nie akceptuje \u017c\u0105da\u0144 parowania (by\u0107 mo\u017ce zbyt wiele razy wpisa\u0142e\u015b nieprawid\u0142owy kod PIN), spr\u00f3buj ponownie p\u00f3\u017aniej.", "device_did_not_pair": "Nie podj\u0119to pr\u00f3by zako\u0144czenia procesu parowania z urz\u0105dzenia.", + "device_not_found": "Urz\u0105dzenie nie zosta\u0142o znalezione podczas wykrywania, spr\u00f3buj doda\u0107 je ponownie.", + "inconsistent_device": "Oczekiwane protoko\u0142y nie zosta\u0142y znalezione podczas wykrywania. Zwykle wskazuje to na problem z multicastem DNS (Zeroconf). Spr\u00f3buj ponownie doda\u0107 urz\u0105dzenie.", "invalid_config": "Konfiguracja tego urz\u0105dzenia jest niekompletna. Spr\u00f3buj doda\u0107 go ponownie.", "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", "setup_failed": "Nie uda\u0142o si\u0119 skonfigurowa\u0107 urz\u0105dzenia.", "unknown": "Nieoczekiwany b\u0142\u0105d" }, @@ -17,14 +21,14 @@ "no_usable_service": "Znaleziono urz\u0105dzenie, ale nie uda\u0142o si\u0119 zidentyfikowa\u0107 \u017cadnego sposobu na nawi\u0105zanie z nim po\u0142\u0105czenia. Je\u015bli nadal widzisz t\u0119 wiadomo\u015b\u0107, spr\u00f3buj poda\u0107 jego adres IP lub uruchom ponownie Apple TV.", "unknown": "Nieoczekiwany b\u0142\u0105d" }, - "flow_title": "{name}", + "flow_title": "{name} ({type})", "step": { "confirm": { - "description": "Zamierzasz doda\u0107 Apple TV o nazwie \"{name}\" do Home Assistanta. \n\n **Aby uko\u0144czy\u0107 ca\u0142y proces, mo\u017ce by\u0107 konieczne wprowadzenie wielu kod\u00f3w PIN.** \n\nPami\u0119taj, \u017ce \"NIE\" b\u0119dziesz w stanie wy\u0142\u0105czy\u0107 Apple TV dzi\u0119ki tej integracji. Wy\u0142\u0105cza si\u0119 tylko sam odtwarzacz multimedialny w Home Assistant!", + "description": "Zamierzasz doda\u0107 \"{name}\" ({type}) do Home Assistanta. \n\n **Aby uko\u0144czy\u0107 ca\u0142y proces, mo\u017ce by\u0107 konieczne wprowadzenie wielu kod\u00f3w PIN.** \n\nPami\u0119taj, \u017ce \"NIE\" b\u0119dziesz w stanie wy\u0142\u0105czy\u0107 Apple TV dzi\u0119ki tej integracji. Wy\u0142\u0105cza si\u0119 tylko sam odtwarzacz multimedialny w Home Assistant!", "title": "Potwierdzenie dodania Apple TV" }, "pair_no_pin": { - "description": "Parowanie jest wymagane dla us\u0142ugi \"{protocol}\". Aby kontynuowa\u0107, wprowad\u017a kod {pin} na swoim Apple TV.", + "description": "Parowanie jest wymagane dla us\u0142ugi \"{protocol}\". Aby kontynuowa\u0107, wprowad\u017a kod {pin} na swoim urz\u0105dzeniu.", "title": "Parowanie" }, "pair_with_pin": { @@ -35,13 +39,15 @@ "title": "Parowanie" }, "password": { + "description": "Has\u0142o jest wymagane przez `{protocol}`. To nie jest jeszcze obs\u0142ugiwane. Wy\u0142\u0105cz has\u0142o, aby kontynuowa\u0107.", "title": "Wymagane has\u0142o" }, "protocol_disabled": { - "title": "Brak mo\u017cliwo\u015bci sparowania" + "description": "Parowanie jest wymagane dla `{protocol}`, ale jest wy\u0142\u0105czone na urz\u0105dzeniu. Sprawd\u017a potencjalne ograniczenia dost\u0119pu na urz\u0105dzeniu (np. zezw\u00f3l wszystkim urz\u0105dzeniom w sieci lokalnej na po\u0142\u0105czenie). \n\n Mo\u017cesz kontynuowa\u0107 bez parowania tego protoko\u0142u, ale niekt\u00f3re funkcje b\u0119d\u0105 ograniczone.", + "title": "Brak mo\u017cliwo\u015bci parowania" }, "reconfigure": { - "description": "Ten Apple TV ma pewne problemy z po\u0142\u0105czeniem i musi zosta\u0107 ponownie skonfigurowany.", + "description": "Ponownie skonfiguruj to urz\u0105dzenie, aby przywr\u00f3ci\u0107 jego funkcjonalno\u015b\u0107.", "title": "Ponowna konfiguracja urz\u0105dzenia" }, "service_problem": { @@ -52,7 +58,7 @@ "data": { "device_input": "Urz\u0105dzenie" }, - "description": "Zacznij od wprowadzenia nazwy urz\u0105dzenia (np. Kuchnia lub Sypialnia) lub adresu IP Apple TV, kt\u00f3re chcesz doda\u0107. Je\u015bli jakie\u015b urz\u0105dzenia zosta\u0142y automatycznie znalezione w Twojej sieci, s\u0105 one pokazane poni\u017cej. \n\nJe\u015bli nie widzisz swojego urz\u0105dzenia lub wyst\u0119puj\u0105 jakiekolwiek problemy, spr\u00f3buj okre\u015bli\u0107 adres IP urz\u0105dzenia. \n\n{devices}", + "description": "Zacznij od wprowadzenia nazwy urz\u0105dzenia (np. Kuchnia lub Sypialnia) lub adresu IP Apple TV, kt\u00f3re chcesz doda\u0107.\n\nJe\u015bli nie widzisz swojego urz\u0105dzenia lub wyst\u0119puj\u0105 jakiekolwiek problemy, spr\u00f3buj okre\u015bli\u0107 adres IP urz\u0105dzenia.", "title": "Konfiguracja nowego Apple TV" } } diff --git a/homeassistant/components/aseko_pool_live/translations/pl.json b/homeassistant/components/aseko_pool_live/translations/pl.json new file mode 100644 index 00000000000..04ced93480c --- /dev/null +++ b/homeassistant/components/aseko_pool_live/translations/pl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "email": "Adres e-mail", + "password": "Has\u0142o" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/balboa/translations/pl.json b/homeassistant/components/balboa/translations/pl.json index cc5429ad077..a0524761e57 100644 --- a/homeassistant/components/balboa/translations/pl.json +++ b/homeassistant/components/balboa/translations/pl.json @@ -12,7 +12,7 @@ "data": { "host": "Nazwa hosta lub adres IP" }, - "title": "Po\u0142\u0105cz si\u0119 z urz\u0105dzeniem Balboa Wi-Fi" + "title": "Po\u0142\u0105czenie z urz\u0105dzeniem Balboa Wi-Fi" } } }, diff --git a/homeassistant/components/binary_sensor/translations/pl.json b/homeassistant/components/binary_sensor/translations/pl.json index f0267fd248b..277598ccd3c 100644 --- a/homeassistant/components/binary_sensor/translations/pl.json +++ b/homeassistant/components/binary_sensor/translations/pl.json @@ -84,6 +84,7 @@ "not_powered": "nast\u0105pi od\u0142\u0105czenie zasilania {entity_name}", "not_present": "sensor {entity_name} przestanie wykrywa\u0107 obecno\u015b\u0107", "not_running": "zako\u0144czy si\u0119 dzia\u0142anie {entity_name}", + "not_tampered": "sensor {entity_name} przestanie wykrywa\u0107 naruszenie", "not_unsafe": "sensor {entity_name} przestanie wykrywa\u0107 zagro\u017cenie", "occupied": "sensor {entity_name} stanie si\u0119 zaj\u0119ty", "opened": "nast\u0105pi otwarcie {entity_name}", @@ -94,6 +95,7 @@ "running": "rozpocznie si\u0119 dzia\u0142anie {entity_name}", "smoke": "sensor {entity_name} wykryje dym", "sound": "sensor {entity_name} wykryje d\u017awi\u0119k", + "tampered": "sensor {entity_name} wykryje naruszenie", "turned_off": "nast\u0105pi wy\u0142\u0105czenie {entity_name}", "turned_on": "nast\u0105pi w\u0142\u0105czenie {entity_name}", "unsafe": "sensor {entity_name} wykryje zagro\u017cenie", diff --git a/homeassistant/components/elmax/translations/pl.json b/homeassistant/components/elmax/translations/pl.json new file mode 100644 index 00000000000..8396e1e51a6 --- /dev/null +++ b/homeassistant/components/elmax/translations/pl.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "bad_auth": "Niepoprawne uwierzytelnienie", + "invalid_pin": "Podany kod PIN jest nieprawid\u0142owy", + "network_error": "Wyst\u0105pi\u0142 b\u0142\u0105d sieci.", + "no_panel_online": "Nie znaleziono panelu sterowania online Elmax.", + "unknown_error": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "panels": { + "data": { + "panel_id": "Identyfikator panelu", + "panel_name": "Nazwa panelu", + "panel_pin": "Kod PIN" + }, + "description": "Wybierz panel, kt\u00f3rym chcesz sterowa\u0107 za pomoc\u0105 tej integracji. Nale\u017cy pami\u0119ta\u0107, \u017ce panel musi by\u0107 w\u0142\u0105czony, aby mo\u017cna by\u0142o go skonfigurowa\u0107.", + "title": "Wyb\u00f3r panelu" + }, + "user": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Prosz\u0119 zalogowa\u0107 si\u0119 do chmury Elmax za pomoc\u0105 swoich danych uwierzytelniaj\u0105cych", + "title": "Logowanie do konta" + } + } + }, + "title": "Konfiguracja chmury Elmax" +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/pl.json b/homeassistant/components/enphase_envoy/translations/pl.json index bf6c0ce430c..ed57bf9cf0a 100644 --- a/homeassistant/components/enphase_envoy/translations/pl.json +++ b/homeassistant/components/enphase_envoy/translations/pl.json @@ -16,7 +16,8 @@ "host": "Nazwa hosta lub adres IP", "password": "Has\u0142o", "username": "Nazwa u\u017cytkownika" - } + }, + "description": "W przypadku nowszych modeli, wpisz nazw\u0119 u\u017cytkownika \u201eenvoy\u201d bez has\u0142a. W przypadku starszych modeli, wprowad\u017a nazw\u0119 u\u017cytkownika \u201einstaller\u201d bez has\u0142a. W przypadku wszystkich innych modeli wprowad\u017a prawid\u0142ow\u0105 nazw\u0119 u\u017cytkownika i has\u0142o." } } } diff --git a/homeassistant/components/fronius/translations/pl.json b/homeassistant/components/fronius/translations/pl.json index b98f78aa82f..a0f7104d775 100644 --- a/homeassistant/components/fronius/translations/pl.json +++ b/homeassistant/components/fronius/translations/pl.json @@ -1,13 +1,18 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "invalid_host": "Nieprawid\u0142owa nazwa hosta lub adres IP" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "unknown": "Nieoczekiwany b\u0142\u0105d" }, + "flow_title": "{device}", "step": { + "confirm_discovery": { + "description": "Czy chcesz doda\u0107 {device} do Home Assistanta?" + }, "user": { "data": { "host": "Nazwa hosta lub adres IP" diff --git a/homeassistant/components/knx/translations/pl.json b/homeassistant/components/knx/translations/pl.json index 9f8c851b5d1..43599c9e4e3 100644 --- a/homeassistant/components/knx/translations/pl.json +++ b/homeassistant/components/knx/translations/pl.json @@ -12,6 +12,7 @@ "data": { "host": "Nazwa hosta lub adres IP", "individual_address": "Indywidualny adres dla po\u0142\u0105czenia", + "local_ip": "Lokalny adres IP (pozostaw pusty, je\u015bli nie masz pewno\u015bci)", "port": "Port", "route_back": "Tryb Route Back / NAT" }, @@ -29,13 +30,13 @@ "data": { "gateway": "Po\u0142\u0105czenie tunelowe KNX" }, - "description": "Prosz\u0119 wybra\u0107 bram\u0119 z listy." + "description": "Prosz\u0119 wybra\u0107 bramk\u0119 z listy." }, "type": { "data": { "connection_type": "Typ po\u0142\u0105czenia KNX" }, - "description": "Prosz\u0119 wprowadzi\u0107 typ po\u0142\u0105czenia, kt\u00f3rego powinni\u015bmy u\u017cy\u0107 dla po\u0142\u0105czenia KNX. \n AUTOMATIC - Integracja sama zadba o po\u0142\u0105czenie z magistral\u0105 KNX poprzez skanowanie bramy. \n TUNNELING - Integracja po\u0142\u0105czy si\u0119 z magistral\u0105 KNX poprzez tunelowanie. \n ROUTING - Integracja po\u0142\u0105czy si\u0119 z magistral\u0105 KNX poprzez routing." + "description": "Prosz\u0119 wprowadzi\u0107 typ po\u0142\u0105czenia, kt\u00f3rego powinni\u015bmy u\u017cy\u0107 dla po\u0142\u0105czenia KNX. \n AUTOMATIC - Integracja sama zadba o po\u0142\u0105czenie z magistral\u0105 KNX poprzez skanowanie bramki. \n TUNNELING - Integracja po\u0142\u0105czy si\u0119 z magistral\u0105 KNX poprzez tunelowanie. \n ROUTING - Integracja po\u0142\u0105czy si\u0119 z magistral\u0105 KNX poprzez routing." } } }, diff --git a/homeassistant/components/lcn/translations/ca.json b/homeassistant/components/lcn/translations/ca.json index 9fe9fd952d3..e1c08f18137 100644 --- a/homeassistant/components/lcn/translations/ca.json +++ b/homeassistant/components/lcn/translations/ca.json @@ -4,7 +4,7 @@ "fingerprint": "codi d'empremta rebut", "send_keys": "claus d'enviament rebudes", "transmitter": "codi del transmissor rebut", - "transponder": "codi del transpondedor rebut" + "transponder": "codi del transponedor rebut" } } } \ No newline at end of file diff --git a/homeassistant/components/lcn/translations/pl.json b/homeassistant/components/lcn/translations/pl.json new file mode 100644 index 00000000000..18446607167 --- /dev/null +++ b/homeassistant/components/lcn/translations/pl.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "trigger_type": { + "fingerprint": "otrzymano kod odcisku palca", + "send_keys": "otrzymano klucze wysy\u0142ania", + "transmitter": "otrzymano kod nadajnika", + "transponder": "otrzymano kod transpondera" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/pl.json b/homeassistant/components/nam/translations/pl.json index 870578b3d4d..c9a4623a471 100644 --- a/homeassistant/components/nam/translations/pl.json +++ b/homeassistant/components/nam/translations/pl.json @@ -4,7 +4,7 @@ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "device_unsupported": "Urz\u0105dzenie nie jest obs\u0142ugiwane", "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", - "reauth_unsuccessful": "B\u0142\u0105d ponownego uwierzytelnienia. Usu\u0144 intergracj\u0119 i skonfiguruj j\u0105 ponownie. " + "reauth_unsuccessful": "B\u0142\u0105d ponownego uwierzytelnienia. Usu\u0144 integracj\u0119 i skonfiguruj j\u0105 ponownie." }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", diff --git a/homeassistant/components/nest/translations/pl.json b/homeassistant/components/nest/translations/pl.json index 7658b9dfc64..c81e5f180b9 100644 --- a/homeassistant/components/nest/translations/pl.json +++ b/homeassistant/components/nest/translations/pl.json @@ -2,6 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji", + "invalid_access_token": "Niepoprawny token dost\u0119pu", "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", "no_url_available": "Brak dost\u0119pnego adresu URL. Aby uzyska\u0107 informacje na temat tego b\u0142\u0119du, [sprawd\u017a sekcj\u0119 pomocy] ({docs_url})", "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", @@ -12,10 +13,13 @@ "default": "Pomy\u015blnie uwierzytelniono" }, "error": { + "bad_project_id": "Podaj prawid\u0142owy Identyfikator projektu chmury (sprawd\u017a konsol\u0119 chmury)", "internal_error": "Wewn\u0119trzny b\u0142\u0105d sprawdzania poprawno\u015bci kodu", "invalid_pin": "Nieprawid\u0142owy kod PIN", + "subscriber_error": "Nieznany b\u0142\u0105d subskrybenta, zobacz logi", "timeout": "Przekroczono limit czasu sprawdzania poprawno\u015bci kodu", - "unknown": "Nieoczekiwany b\u0142\u0105d" + "unknown": "Nieoczekiwany b\u0142\u0105d", + "wrong_project_id": "Podaj prawid\u0142owy Identyfikator projektu chmury (znaleziono identyfikator projektu dost\u0119pu do urz\u0105dzenia)" }, "step": { "auth": { @@ -44,9 +48,10 @@ }, "pubsub": { "data": { - "cloud_project_id": "Identyfikator projektu Google Cloud" + "cloud_project_id": "Identyfikator projektu chmury Google" }, - "title": "Skonfiguruj Google Cloud" + "description": "Odwied\u017a [Konsol\u0119 chmury]({url}), aby znale\u017a\u0107 sw\u00f3j identyfikator projektu chmury Google.", + "title": "Konfiguracja chmury Google" }, "reauth_confirm": { "description": "Integracja Nest wymaga ponownego uwierzytelnienia Twojego konta", diff --git a/homeassistant/components/nina/translations/pl.json b/homeassistant/components/nina/translations/pl.json index 2a6b459e102..631127679c1 100644 --- a/homeassistant/components/nina/translations/pl.json +++ b/homeassistant/components/nina/translations/pl.json @@ -1,7 +1,12 @@ { "config": { + "abort": { + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + }, "error": { - "no_selection": "Prosz\u0119 przynajmniej wybra\u0107 miasto lub powiat" + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "no_selection": "Prosz\u0119 wybra\u0107 przynajmniej jedno miasto lub powiat", + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "user": { @@ -11,8 +16,11 @@ "_i_to_l": "Miasto/powiat (I-L)", "_m_to_q": "Miasto/powiat (M-Q)", "_r_to_u": "Miasto/powiat (R-U)", - "_v_to_z": "Miasto/powiat (V-Z)" - } + "_v_to_z": "Miasto/powiat (V-Z)", + "corona_filter": "Usu\u0144 ostrze\u017cenia o koronawirusie", + "slots": "Maksymalna liczba ostrze\u017ce\u0144 na miasto/powiat" + }, + "title": "Wybierz miasto/powiat" } } } diff --git a/homeassistant/components/owntracks/translations/pl.json b/homeassistant/components/owntracks/translations/pl.json index 09bd29b99f2..98c8779fe1f 100644 --- a/homeassistant/components/owntracks/translations/pl.json +++ b/homeassistant/components/owntracks/translations/pl.json @@ -4,7 +4,7 @@ "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." }, "create_entry": { - "default": "\n\nNa Androidzie, otw\u00f3rz [aplikacj\u0119 OwnTracks]({android_url}), id\u017a do: ustawienia -> po\u0142\u0105czenia. Zmie\u0144 nast\u0119puj\u0105ce ustawienia:\n - Tryb: Private HTTP\n - Host: {webhook_url}\n - Identyfikacja:\n - Nazwa u\u017cytkow'nika: `''`\n - ID urz\u0105dzenia: `''`\n\nNa iOS, otw\u00f3rz [aplikacj\u0119 OwnTracks]({ios_url}), naci\u015bnij ikon\u0119 (i) w lewym g\u00f3rnym rogu -> ustawienia. Zmie\u0144 nast\u0119puj\u0105ce ustawienia:\n - Tryb: HTTP\n - URL: {webhook_url}\n - W\u0142\u0105cz uwierzytelnianie\n - ID u\u017cytkownika: `''`\n\n{secret}\n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}), by pozna\u0107 szczeg\u00f3\u0142y." + "default": "\n\nNa Androidzie, otw\u00f3rz [aplikacj\u0119 OwnTracks]({android_url}), id\u017a do: ustawienia -> po\u0142\u0105czenia. Zmie\u0144 nast\u0119puj\u0105ce ustawienia:\n - Tryb: Private HTTP\n - Host: {webhook_url}\n - Identyfikacja:\n - Nazwa u\u017cytkownika: `''`\n - ID urz\u0105dzenia: `''`\n\nNa iOS, otw\u00f3rz [aplikacj\u0119 OwnTracks]({ios_url}), naci\u015bnij ikon\u0119 (i) w lewym g\u00f3rnym rogu -> ustawienia. Zmie\u0144 nast\u0119puj\u0105ce ustawienia:\n - Tryb: HTTP\n - URL: {webhook_url}\n - W\u0142\u0105cz uwierzytelnianie\n - ID u\u017cytkownika: `''`\n\n{secret}\n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}), by pozna\u0107 szczeg\u00f3\u0142y." }, "step": { "user": { diff --git a/homeassistant/components/simplisafe/translations/pl.json b/homeassistant/components/simplisafe/translations/pl.json index 4ec2afba5c3..e95f99c1d2b 100644 --- a/homeassistant/components/simplisafe/translations/pl.json +++ b/homeassistant/components/simplisafe/translations/pl.json @@ -32,11 +32,12 @@ }, "user": { "data": { + "auth_code": "Kod autoryzacji", "code": "Kod (u\u017cywany w interfejsie Home Assistant)", "password": "Has\u0142o", "username": "Adres e-mail" }, - "description": "Pocz\u0105wszy od 2021 r. SimpliSafe przesz\u0142o na nowy mechanizm uwierzytelniania za po\u015brednictwem swojej aplikacji internetowej. Ze wzgl\u0119du na ograniczenia techniczne, na ko\u0144cu tego procesu znajduje si\u0119 r\u0119czny krok; upewnij si\u0119, \u017ce przed rozpocz\u0119ciem przeczyta\u0142e\u015b [dokumentacj\u0119](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code).\n\nKiedy b\u0119dziesz gotowy, kliknij [tutaj]( {url} ), aby otworzy\u0107 aplikacj\u0119 internetow\u0105 SimpliSafe i wprowadzi\u0107 swoje dane uwierzytelniaj\u0105ce. Po zako\u0144czeniu procesu wr\u00f3\u0107 tutaj i kliknij \"Zatwierd\u017a\".", + "description": "SimpliSafe uwierzytelnia si\u0119 z Home Assistantem za po\u015brednictwem aplikacji internetowej SimpliSafe. Ze wzgl\u0119du na ograniczenia techniczne na ko\u0144cu tego procesu znajduje si\u0119 r\u0119czny krok; upewnij si\u0119, \u017ce przed rozpocz\u0119ciem przeczyta\u0142e\u015b [dokumentacj\u0119]( {docs_url} ).\n\n1. Kliknij [tutaj]( {url} ), aby otworzy\u0107 aplikacj\u0119 internetow\u0105 SimpliSafe i wprowadzi\u0107 swoje dane uwierzytelniaj\u0105ce. \n\n2. Po zako\u0144czeniu procesu logowania wr\u00f3\u0107 tutaj i wprowad\u017a poni\u017cszy kod autoryzacyjny.", "title": "Wprowad\u017a dane" } } diff --git a/homeassistant/components/tailscale/translations/pl.json b/homeassistant/components/tailscale/translations/pl.json index 78ffdd12821..579d8de83e1 100644 --- a/homeassistant/components/tailscale/translations/pl.json +++ b/homeassistant/components/tailscale/translations/pl.json @@ -1,17 +1,25 @@ { "config": { + "abort": { + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie" + }, "step": { "reauth_confirm": { "data": { "api_key": "Klucz API" - } + }, + "description": "Tokeny API dla Tailscale s\u0105 wa\u017cne przez 90 dni. Mo\u017cesz utworzy\u0107 nowy klucz API na https://login.tailscale.com/admin/settings/authkeys" }, "user": { "data": { "api_key": "Klucz API", "tailnet": "Tailnet" }, - "description": "Aby uwierzytelni\u0107 si\u0119 w Tailscale, musisz utworzy\u0107 klucz API na stronie https://login.tailscale.com/admin/settings/authkeys.\n\nTailnet to nazwa Twojej sieci Tailscale. Mo\u017cna j\u0105 znale\u017a\u0107 w lewym g\u00f3rnym rogu w panelu administracyjnym Tailscale (obok logo Tailscale)." + "description": "Aby uwierzytelni\u0107 si\u0119 w Tailscale, musisz utworzy\u0107 klucz API na stronie https://login.tailscale.com/admin/settings/authkeys.\n\nTailnet to nazwa Twojej sieci Tailscale. Mo\u017cna j\u0105 znale\u017a\u0107 w lewym g\u00f3rnym rogu w panelu administracyjnym Tailscale (obok loga Tailscale)." } } } diff --git a/homeassistant/components/tesla_wall_connector/translations/pl.json b/homeassistant/components/tesla_wall_connector/translations/pl.json new file mode 100644 index 00000000000..6ee749485ee --- /dev/null +++ b/homeassistant/components/tesla_wall_connector/translations/pl.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "flow_title": "{serial_number} ({host})", + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + }, + "title": "Konfiguracja z\u0142\u0105cza \u015bciennego Tesla" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji" + }, + "title": "Konfiguracja opcji dla z\u0142\u0105cza \u015bciennego Tesla" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/sensor.pl.json b/homeassistant/components/tractive/translations/sensor.pl.json index b49f022e279..c722e2628d4 100644 --- a/homeassistant/components/tractive/translations/sensor.pl.json +++ b/homeassistant/components/tractive/translations/sensor.pl.json @@ -1,9 +1,10 @@ { "state": { "tractive__tracker_state": { - "not_reporting": "Nie odpowiada", - "operational": "Operacyjny", - "system_startup": "Uruchamianie systemu" + "not_reporting": "nie raportuje", + "operational": "sprawny", + "system_shutdown_user": "wy\u0142\u0105czanie systemu", + "system_startup": "uruchamianie systemu" } } } \ No newline at end of file diff --git a/homeassistant/components/trafikverket_weatherstation/translations/pl.json b/homeassistant/components/trafikverket_weatherstation/translations/pl.json index 28f8e9e81dd..2dfd13bf268 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/pl.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/pl.json @@ -1,10 +1,23 @@ { "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane" + }, "error": { - "cannot_connect": "Nie uda\u0142o si\u0119 po\u0142\u0105czy\u0107", - "invalid_auth": "Niepoprawna autentykacja", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", "invalid_station": "Nie mo\u017cna znale\u017a\u0107 stacji pogodowej o podanej nazwie", "more_stations": "Znaleziono wiele stacji pogodowych o podanej nazwie" + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API", + "conditions": "Monitorowane warunki pogodowe", + "name": "Nazwa u\u017cytkownika", + "station": "Stacja" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/twinkly/translations/ca.json b/homeassistant/components/twinkly/translations/ca.json index 2801361990e..6a38c4936da 100644 --- a/homeassistant/components/twinkly/translations/ca.json +++ b/homeassistant/components/twinkly/translations/ca.json @@ -7,6 +7,9 @@ "cannot_connect": "Ha fallat la connexi\u00f3" }, "step": { + "discovery_confirm": { + "description": "Vols configurar {name} - {model} ({host})?" + }, "user": { "data": { "host": "Amfitri\u00f3 (o adre\u00e7a IP) del dispositiu Twinkly" diff --git a/homeassistant/components/twinkly/translations/de.json b/homeassistant/components/twinkly/translations/de.json index 0f5a7e0b886..9ec5e9683b1 100644 --- a/homeassistant/components/twinkly/translations/de.json +++ b/homeassistant/components/twinkly/translations/de.json @@ -7,6 +7,9 @@ "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { + "discovery_confirm": { + "description": "M\u00f6chtest du {name} - {model} ( {host} ) einrichten?" + }, "user": { "data": { "host": "Host (oder IP-Adresse) deines twinkly-Ger\u00e4ts" diff --git a/homeassistant/components/twinkly/translations/en.json b/homeassistant/components/twinkly/translations/en.json index 2126bac3c27..68c1d1cd69c 100644 --- a/homeassistant/components/twinkly/translations/en.json +++ b/homeassistant/components/twinkly/translations/en.json @@ -7,6 +7,9 @@ "cannot_connect": "Failed to connect" }, "step": { + "discovery_confirm": { + "description": "Do you want to setup {name} - {model} ({host})?" + }, "user": { "data": { "host": "Host (or IP address) of your twinkly device" diff --git a/homeassistant/components/twinkly/translations/et.json b/homeassistant/components/twinkly/translations/et.json index 99e417685ae..0d1b87a36fc 100644 --- a/homeassistant/components/twinkly/translations/et.json +++ b/homeassistant/components/twinkly/translations/et.json @@ -7,6 +7,9 @@ "cannot_connect": "\u00dchendamine nurjus" }, "step": { + "discovery_confirm": { + "description": "Kas seadistada {name} \u2013 {model} ( {host} )?" + }, "user": { "data": { "host": "Twinkly seadme host (v\u00f5i IP-aadress)" diff --git a/homeassistant/components/twinkly/translations/fr.json b/homeassistant/components/twinkly/translations/fr.json index 02ba8cb1b3e..92171723b55 100644 --- a/homeassistant/components/twinkly/translations/fr.json +++ b/homeassistant/components/twinkly/translations/fr.json @@ -7,6 +7,9 @@ "cannot_connect": "\u00c9chec de connexion" }, "step": { + "discovery_confirm": { + "description": "Voulez-vous configurer {name} - {model} ( {host} )\u00a0?" + }, "user": { "data": { "host": "Nom r\u00e9seau (ou adresse IP) de votre Twinkly" diff --git a/homeassistant/components/twinkly/translations/hu.json b/homeassistant/components/twinkly/translations/hu.json index 9c5137c30a4..94983ce4893 100644 --- a/homeassistant/components/twinkly/translations/hu.json +++ b/homeassistant/components/twinkly/translations/hu.json @@ -7,6 +7,9 @@ "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "step": { + "discovery_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name} - {model} ({host})?" + }, "user": { "data": { "host": "A Twinkly eszk\u00f6z c\u00edme" diff --git a/homeassistant/components/twinkly/translations/it.json b/homeassistant/components/twinkly/translations/it.json index ec62abeea01..e3279b2b5e9 100644 --- a/homeassistant/components/twinkly/translations/it.json +++ b/homeassistant/components/twinkly/translations/it.json @@ -7,6 +7,9 @@ "cannot_connect": "Impossibile connettersi" }, "step": { + "discovery_confirm": { + "description": "Vuoi configurare {name} - {model} ({host})?" + }, "user": { "data": { "host": "Host (o indirizzo IP) del tuo dispositivo twinkly" diff --git a/homeassistant/components/twinkly/translations/pl.json b/homeassistant/components/twinkly/translations/pl.json index 2c1434dc035..8036d6cc2f6 100644 --- a/homeassistant/components/twinkly/translations/pl.json +++ b/homeassistant/components/twinkly/translations/pl.json @@ -7,6 +7,9 @@ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "step": { + "discovery_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {name} - {model} ({host})?" + }, "user": { "data": { "host": "Nazwa hosta lub adres IP" diff --git a/homeassistant/components/twinkly/translations/ru.json b/homeassistant/components/twinkly/translations/ru.json index a4cadb51f4f..f1c9f09921c 100644 --- a/homeassistant/components/twinkly/translations/ru.json +++ b/homeassistant/components/twinkly/translations/ru.json @@ -7,6 +7,9 @@ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, "step": { + "discovery_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name} - {model} ({host})?" + }, "user": { "data": { "host": "\u0418\u043c\u044f \u0445\u043e\u0441\u0442\u0430 (\u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441) \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" diff --git a/homeassistant/components/twinkly/translations/zh-Hant.json b/homeassistant/components/twinkly/translations/zh-Hant.json index 15fde13f9a9..3ef76a545c7 100644 --- a/homeassistant/components/twinkly/translations/zh-Hant.json +++ b/homeassistant/components/twinkly/translations/zh-Hant.json @@ -7,6 +7,9 @@ "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "step": { + "discovery_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name} - {model} ({host})\uff1f" + }, "user": { "data": { "host": "Twinkly \u88dd\u7f6e\u4e3b\u6a5f\u540d\u7a31\uff08\u6216 IP \u4f4d\u5740\uff09" diff --git a/homeassistant/components/unifi/translations/pl.json b/homeassistant/components/unifi/translations/pl.json index 6e4d6364b27..a52660ac262 100644 --- a/homeassistant/components/unifi/translations/pl.json +++ b/homeassistant/components/unifi/translations/pl.json @@ -21,7 +21,7 @@ "username": "Nazwa u\u017cytkownika", "verify_ssl": "Weryfikacja certyfikatu SSL" }, - "title": "Konfiguracja kontrolera UniFi" + "title": "Konfiguracja sieci UniFi" } } }, @@ -34,19 +34,19 @@ "poe_clients": "Zezwalaj na kontrol\u0119 POE klient\u00f3w" }, "description": "Konfigurowanie kontroli klienta.\n\nUtw\u00f3rz prze\u0142\u0105czniki dla numer\u00f3w seryjnych, dla kt\u00f3rych chcesz kontrolowa\u0107 dost\u0119p do sieci.", - "title": "UniFi opcje 2/3" + "title": "Opcje sieciowe UniFi 2/3" }, "device_tracker": { "data": { "detection_time": "Czas w sekundach od momentu, kiedy ostatnio widziano, a\u017c do momentu, kiedy uznano go za nieobecny.", - "ignore_wired_bug": "Wy\u0142\u0105czanie logiki b\u0142\u0119d\u00f3w dla po\u0142\u0105cze\u0144 przewodowych UniFi", + "ignore_wired_bug": "Wy\u0142\u0105czanie logiki b\u0142\u0119d\u00f3w dla po\u0142\u0105cze\u0144 przewodowych sieci UniFi", "ssid_filter": "Wybierz SSIDy do \u015bledzenia klient\u00f3w bezprzewodowych", "track_clients": "\u015aled\u017a klient\u00f3w sieciowych", "track_devices": "\u015aled\u017a urz\u0105dzenia sieciowe (urz\u0105dzenia Ubiquiti)", "track_wired_clients": "Uwzgl\u0119dnij klient\u00f3w sieci przewodowej" }, "description": "Konfiguracja \u015bledzenia urz\u0105dze\u0144", - "title": "Opcje UniFi" + "title": "Opcje sieciowe UniFi 1/3" }, "init": { "data": { @@ -62,7 +62,7 @@ "track_clients": "\u015aled\u017a klient\u00f3w sieciowych", "track_devices": "\u015aled\u017a urz\u0105dzenia sieciowe (urz\u0105dzenia Ubiquiti)" }, - "description": "Konfigurowanie integracji UniFi" + "description": "Konfigurowanie integracji sieci UniFi" }, "statistics_sensors": { "data": { @@ -70,7 +70,7 @@ "allow_uptime_sensors": "Sensory czasu pracy dla klient\u00f3w sieciowych" }, "description": "Konfiguracja sensora statystyk", - "title": "Opcje UniFi 3/3" + "title": "Opcje sieciowe UniFi 3/3" } } } diff --git a/homeassistant/components/vacuum/translations/pt-BR.json b/homeassistant/components/vacuum/translations/pt-BR.json index b516880d5df..77a38f3b298 100644 --- a/homeassistant/components/vacuum/translations/pt-BR.json +++ b/homeassistant/components/vacuum/translations/pt-BR.json @@ -1,7 +1,12 @@ { "device_automation": { "condition_type": { + "is_cleaning": "{entity_name} est\u00e1 limpando", "is_docked": "{entity_name} est\u00e1 na base" + }, + "trigger_type": { + "cleaning": "{entity_name} iniciou a limpeza", + "docked": "{entity_name} est\u00e1 na base" } }, "state": { diff --git a/homeassistant/components/wolflink/translations/sensor.et.json b/homeassistant/components/wolflink/translations/sensor.et.json index db6e5db23e6..4ed9c342089 100644 --- a/homeassistant/components/wolflink/translations/sensor.et.json +++ b/homeassistant/components/wolflink/translations/sensor.et.json @@ -65,7 +65,7 @@ "sparen": "S\u00e4\u00e4sture\u017eiim", "spreizung_hoch": "temperatuurivahemik liiga suur", "spreizung_kf": "KF-i hajutamine", - "stabilisierung": "Rahunemine", + "stabilisierung": "Stabiliseerumine", "standby": "Ootel", "start": "K\u00e4ivitus", "storung": "Viga", diff --git a/homeassistant/components/wolflink/translations/sensor.id.json b/homeassistant/components/wolflink/translations/sensor.id.json index bcef8fe6d2c..b3d31b620a2 100644 --- a/homeassistant/components/wolflink/translations/sensor.id.json +++ b/homeassistant/components/wolflink/translations/sensor.id.json @@ -40,6 +40,7 @@ "solarbetrieb": "Mode surya", "sparbetrieb": "Mode ekonomi", "sparen": "Ekonomi", + "stabilisierung": "Stabilisasi", "standby": "Siaga", "start": "Mulai", "storung": "Kesalahan", diff --git a/homeassistant/components/yale_smart_alarm/translations/pl.json b/homeassistant/components/yale_smart_alarm/translations/pl.json index b409b7026c1..c6bd7ed558a 100644 --- a/homeassistant/components/yale_smart_alarm/translations/pl.json +++ b/homeassistant/components/yale_smart_alarm/translations/pl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Konto jest ju\u017c skonfigurowane" + "already_configured": "Konto jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { "invalid_auth": "Niepoprawne uwierzytelnienie" diff --git a/homeassistant/components/yamaha_musiccast/translations/select.pl.json b/homeassistant/components/yamaha_musiccast/translations/select.pl.json index e5c4bd8f3d0..a6e9bde7c4f 100644 --- a/homeassistant/components/yamaha_musiccast/translations/select.pl.json +++ b/homeassistant/components/yamaha_musiccast/translations/select.pl.json @@ -1,11 +1,19 @@ { "state": { + "yamaha_musiccast__dimmer": { + "auto": "Automatyczny" + }, "yamaha_musiccast__zone_equalizer_mode": { "auto": "Automatycznie", + "bypass": "Pomijanie", "manual": "R\u0119cznie" }, "yamaha_musiccast__zone_link_audio_delay": { - "balanced": "Zr\u00f3wnowa\u017cone" + "audio_sync": "Synchronizacja d\u017awi\u0119ku", + "audio_sync_off": "Synchronizacja d\u017awi\u0119ku wy\u0142\u0105czona", + "audio_sync_on": "Synchronizacja d\u017awi\u0119ku w\u0142\u0105czona", + "balanced": "Zr\u00f3wnowa\u017cone", + "lip_sync": "Synchronizacja ust" }, "yamaha_musiccast__zone_link_audio_quality": { "compressed": "Skompresowane", @@ -21,16 +29,24 @@ "30 min": "30 minut", "60 min": "60 minut", "90 min": "90 minut", - "off": "wy\u0142\u0105czone" + "off": "Wy\u0142\u0105czone" }, "yamaha_musiccast__zone_surr_decoder_type": { "auto": "Automatycznie", + "dolby_pl": "Dolby ProLogic", + "dolby_pl2x_game": "Dolby ProLogic 2x (Gra)", + "dolby_pl2x_movie": "Dolby ProLogic 2x (Film)", + "dolby_pl2x_music": "Dolby ProLogic 2x (Muzyka)", + "dolby_surround": "Dolby Surround", + "dts_neo6_cinema": "DTS Neo:6 (Kino)", + "dts_neo6_music": "DTS Neo:6 (Muzyka)", + "dts_neural_x": "DTS Neural:X", "toggle": "Prze\u0142\u0105cz" }, "yamaha_musiccast__zone_tone_control_mode": { - "auto": "Automatycznie", - "bypass": "Polski", - "manual": "R\u0119cznie" + "auto": "Automatyczna", + "bypass": "Pomijanie", + "manual": "R\u0119czna" } } } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/pl.json b/homeassistant/components/zwave_js/translations/pl.json index 812d00b8e20..68dd6555552 100644 --- a/homeassistant/components/zwave_js/translations/pl.json +++ b/homeassistant/components/zwave_js/translations/pl.json @@ -33,6 +33,7 @@ "s2_unauthenticated_key": "Klucz nieuwierzytelniony S2", "usb_path": "\u015acie\u017cka urz\u0105dzenia USB" }, + "description": "Dodatek wygeneruje klucze bezpiecze\u0144stwa, je\u015bli te pola pozostan\u0105 puste.", "title": "Wprowad\u017a konfiguracj\u0119 dodatku Z-Wave JS" }, "hassio_confirm": { @@ -119,6 +120,7 @@ "s2_unauthenticated_key": "Klucz nieuwierzytelniony S2", "usb_path": "\u015acie\u017cka urz\u0105dzenia USB" }, + "description": "Dodatek wygeneruje klucze bezpiecze\u0144stwa, je\u015bli te pola pozostan\u0105 puste.", "title": "Wprowad\u017a konfiguracj\u0119 dodatku Z-Wave JS" }, "install_addon": { From a818afdad110b4abba9754de28f0897734f2a988 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 15 Dec 2021 03:10:06 +0100 Subject: [PATCH 0438/2644] Revert pillow 8.3.2 (#61793) * Revert "Bump pillow from 8.2.0 to 8.3.2 (#61661)" This reverts commit 3635946211c54ec7a5b88a7b53269e0aba13c60b. * Add comment Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Fix comment Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/doods/manifest.json | 2 +- homeassistant/components/image/manifest.json | 2 +- homeassistant/components/proxy/manifest.json | 2 +- homeassistant/components/qrcode/manifest.json | 2 +- homeassistant/components/seven_segments/manifest.json | 2 +- homeassistant/components/sighthound/manifest.json | 2 +- homeassistant/components/tensorflow/manifest.json | 2 +- homeassistant/package_constraints.txt | 4 +++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/gen_requirements_all.py | 2 ++ 11 files changed, 14 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json index 44597ac8aeb..ae584af5916 100644 --- a/homeassistant/components/doods/manifest.json +++ b/homeassistant/components/doods/manifest.json @@ -2,7 +2,7 @@ "domain": "doods", "name": "DOODS - Dedicated Open Object Detection Service", "documentation": "https://www.home-assistant.io/integrations/doods", - "requirements": ["pydoods==1.0.2", "pillow==8.3.2"], + "requirements": ["pydoods==1.0.2", "pillow==8.2.0"], "codeowners": [], "iot_class": "local_polling" } diff --git a/homeassistant/components/image/manifest.json b/homeassistant/components/image/manifest.json index 9416ea7ef9e..82b7e58a653 100644 --- a/homeassistant/components/image/manifest.json +++ b/homeassistant/components/image/manifest.json @@ -3,7 +3,7 @@ "name": "Image", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/image", - "requirements": ["pillow==8.3.2"], + "requirements": ["pillow==8.2.0"], "dependencies": ["http"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal" diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index 47982ac120e..68c7717e16c 100644 --- a/homeassistant/components/proxy/manifest.json +++ b/homeassistant/components/proxy/manifest.json @@ -2,6 +2,6 @@ "domain": "proxy", "name": "Camera Proxy", "documentation": "https://www.home-assistant.io/integrations/proxy", - "requirements": ["pillow==8.3.2"], + "requirements": ["pillow==8.2.0"], "codeowners": [] } diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index adfad7569e8..a414e197fd6 100644 --- a/homeassistant/components/qrcode/manifest.json +++ b/homeassistant/components/qrcode/manifest.json @@ -2,7 +2,7 @@ "domain": "qrcode", "name": "QR Code", "documentation": "https://www.home-assistant.io/integrations/qrcode", - "requirements": ["pillow==8.3.2", "pyzbar==0.1.7"], + "requirements": ["pillow==8.2.0", "pyzbar==0.1.7"], "codeowners": [], "iot_class": "calculated" } diff --git a/homeassistant/components/seven_segments/manifest.json b/homeassistant/components/seven_segments/manifest.json index 14dc16814a6..9a0287b2132 100644 --- a/homeassistant/components/seven_segments/manifest.json +++ b/homeassistant/components/seven_segments/manifest.json @@ -2,7 +2,7 @@ "domain": "seven_segments", "name": "Seven Segments OCR", "documentation": "https://www.home-assistant.io/integrations/seven_segments", - "requirements": ["pillow==8.3.2"], + "requirements": ["pillow==8.2.0"], "codeowners": ["@fabaff"], "iot_class": "local_polling" } diff --git a/homeassistant/components/sighthound/manifest.json b/homeassistant/components/sighthound/manifest.json index b0febac8150..b22b645a7e8 100644 --- a/homeassistant/components/sighthound/manifest.json +++ b/homeassistant/components/sighthound/manifest.json @@ -2,7 +2,7 @@ "domain": "sighthound", "name": "Sighthound", "documentation": "https://www.home-assistant.io/integrations/sighthound", - "requirements": ["pillow==8.3.2", "simplehound==0.3"], + "requirements": ["pillow==8.2.0", "simplehound==0.3"], "codeowners": ["@robmarkcole"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index fbc1d848d33..8d5ea0acaa2 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -7,7 +7,7 @@ "tf-models-official==2.3.0", "pycocotools==2.0.1", "numpy==1.21.4", - "pillow==8.3.2" + "pillow==8.2.0" ], "codeowners": [], "iot_class": "local_polling" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3702db188e2..cc755fb965c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ httpx==0.21.0 ifaddr==0.1.7 jinja2==3.0.3 paho-mqtt==1.6.1 -pillow==8.3.2 +pillow==8.2.0 pip>=8.0.3,<20.3 pyserial==3.5 python-slugify==4.0.1 @@ -35,6 +35,8 @@ voluptuous==0.12.2 yarl==1.6.3 zeroconf==0.37.0 +# Constrain pillow to 8.2.0 because later versions are causing issues in nightly builds. + pycryptodome>=3.6.6 # Constrain urllib3 to ensure we deal with CVE-2020-26137 and CVE-2021-33503 diff --git a/requirements_all.txt b/requirements_all.txt index 6b519383bbc..d01a13fe961 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1229,7 +1229,7 @@ pilight==0.1.1 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -pillow==8.3.2 +pillow==8.2.0 # homeassistant.components.dominos pizzapi==0.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e936c06c121..a12830b05bc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -746,7 +746,7 @@ pilight==0.1.1 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -pillow==8.3.2 +pillow==8.2.0 # homeassistant.components.plex plexapi==4.7.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index dd8bc989874..6dfc910d805 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -61,6 +61,8 @@ CONSTRAINT_PATH = os.path.join( os.path.dirname(__file__), "../homeassistant/package_constraints.txt" ) CONSTRAINT_BASE = """ +# Constrain pillow to 8.2.0 because later versions are causing issues in nightly builds. + pycryptodome>=3.6.6 # Constrain urllib3 to ensure we deal with CVE-2020-26137 and CVE-2021-33503 From a371d568c074917282b73e636b66a4428bc51165 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 15 Dec 2021 10:40:06 +0100 Subject: [PATCH 0439/2644] Bump pychromecast to 10.2.1 (#61811) --- homeassistant/components/cast/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index 3f3c31b8d3d..bee18948a33 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", - "requirements": ["pychromecast==10.1.1"], + "requirements": ["pychromecast==10.2.1"], "after_dependencies": [ "cloud", "http", diff --git a/requirements_all.txt b/requirements_all.txt index d01a13fe961..18708ecc291 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1405,7 +1405,7 @@ pycfdns==1.2.2 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==10.1.1 +pychromecast==10.2.1 # homeassistant.components.pocketcasts pycketcasts==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a12830b05bc..cefee75ad0e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -859,7 +859,7 @@ pybotvac==0.0.22 pycfdns==1.2.2 # homeassistant.components.cast -pychromecast==10.1.1 +pychromecast==10.2.1 # homeassistant.components.climacell pyclimacell==0.18.2 From f17164fa47b2c6660d0206ed34bfa5a3a26a7373 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 15 Dec 2021 10:40:37 +0100 Subject: [PATCH 0440/2644] Don't override pychromecast MediaController's APP ID (#61796) --- homeassistant/components/cast/media_player.py | 8 +--- tests/components/cast/test_media_player.py | 43 +++++++++++++++---- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 46c25501f3a..61922a4cd8b 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -47,7 +47,6 @@ from homeassistant.components.plex.const import PLEX_URI_SCHEME from homeassistant.components.plex.services import lookup_plex_media from homeassistant.const import ( CAST_APP_ID_HOMEASSISTANT_LOVELACE, - CAST_APP_ID_HOMEASSISTANT_MEDIA, EVENT_HOMEASSISTANT_STOP, STATE_IDLE, STATE_OFF, @@ -230,7 +229,6 @@ class CastDevice(MediaPlayerEntity): self._cast_info.cast_info, ChromeCastZeroconf.get_zeroconf(), ) - chromecast.media_controller.app_id = CAST_APP_ID_HOMEASSISTANT_MEDIA self._chromecast = chromecast if CAST_MULTIZONE_MANAGER_KEY not in self.hass.data: @@ -527,9 +525,8 @@ class CastDevice(MediaPlayerEntity): self._chromecast.register_handler(controller) controller.play_media(media) else: - self._chromecast.media_controller.play_media( - media_id, media_type, **kwargs.get(ATTR_MEDIA_EXTRA, {}) - ) + app_data = {"media_id": media_id, "media_type": media_type, **extra} + quick_play(self._chromecast, "homeassistant_media", app_data) def _media_status(self): """ @@ -820,7 +817,6 @@ class DynamicCastGroup: self._cast_info.cast_info, ChromeCastZeroconf.get_zeroconf(), ) - chromecast.media_controller.app_id = CAST_APP_ID_HOMEASSISTANT_MEDIA self._chromecast = chromecast if CAST_MULTIZONE_MANAGER_KEY not in self.hass.data: diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index adab55c50df..85562f39761 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -754,7 +754,7 @@ async def test_supported_features( assert state.attributes.get("supported_features") == supported_features -async def test_entity_play_media(hass: HomeAssistant): +async def test_entity_play_media(hass: HomeAssistant, quick_play_mock): """Test playing media.""" entity_id = "media_player.speaker" reg = er.async_get(hass) @@ -776,8 +776,28 @@ async def test_entity_play_media(hass: HomeAssistant): assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid)) # Play_media - await common.async_play_media(hass, "audio", "best.mp3", entity_id) - chromecast.media_controller.play_media.assert_called_once_with("best.mp3", "audio") + await hass.services.async_call( + media_player.DOMAIN, + media_player.SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: entity_id, + media_player.ATTR_MEDIA_CONTENT_TYPE: "audio", + media_player.ATTR_MEDIA_CONTENT_ID: "best.mp3", + media_player.ATTR_MEDIA_EXTRA: {"metadata": {"metadatatype": 3}}, + }, + blocking=True, + ) + + chromecast.media_controller.play_media.assert_not_called() + quick_play_mock.assert_called_once_with( + chromecast, + "homeassistant_media", + { + "media_id": "best.mp3", + "media_type": "audio", + "metadata": {"metadatatype": 3}, + }, + ) async def test_entity_play_media_cast(hass: HomeAssistant, quick_play_mock): @@ -865,7 +885,7 @@ async def test_entity_play_media_cast_invalid(hass, caplog, quick_play_mock): assert "App unknown not supported" in caplog.text -async def test_entity_play_media_sign_URL(hass: HomeAssistant): +async def test_entity_play_media_sign_URL(hass: HomeAssistant, quick_play_mock): """Test playing media.""" entity_id = "media_player.speaker" @@ -886,8 +906,10 @@ async def test_entity_play_media_sign_URL(hass: HomeAssistant): # Play_media await common.async_play_media(hass, "audio", "/best.mp3", entity_id) - chromecast.media_controller.play_media.assert_called_once_with(ANY, "audio") - assert chromecast.media_controller.play_media.call_args[0][0].startswith( + quick_play_mock.assert_called_once_with( + chromecast, "homeassistant_media", {"media_id": ANY, "media_type": "audio"} + ) + assert quick_play_mock.call_args[0][2]["media_id"].startswith( "http://example.com:8123/best.mp3?authSig=" ) @@ -1231,7 +1253,7 @@ async def test_group_media_states(hass, mz_mock): assert state.state == "playing" -async def test_group_media_control(hass, mz_mock): +async def test_group_media_control(hass, mz_mock, quick_play_mock): """Test media controls are handled by group if entity has no state.""" entity_id = "media_player.speaker" reg = er.async_get(hass) @@ -1286,7 +1308,12 @@ async def test_group_media_control(hass, mz_mock): # Verify play_media is not forwarded await common.async_play_media(hass, "music", "best.mp3", entity_id) assert not grp_media.play_media.called - assert chromecast.media_controller.play_media.called + assert not chromecast.media_controller.play_media.called + quick_play_mock.assert_called_once_with( + chromecast, + "homeassistant_media", + {"media_id": "best.mp3", "media_type": "music"}, + ) async def test_failed_cast_on_idle(hass, caplog): From a1abcad0ca974387c9e89ddf5a5baf06c867b4e3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 15 Dec 2021 10:54:14 +0100 Subject: [PATCH 0441/2644] Use new CoverDeviceClass in knx (#61868) Co-authored-by: epenet --- homeassistant/components/knx/cover.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index 96996e0ef27..8bc24c209f1 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -12,7 +12,6 @@ from homeassistant import config_entries from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, - DEVICE_CLASS_BLIND, SUPPORT_CLOSE, SUPPORT_CLOSE_TILT, SUPPORT_OPEN, @@ -21,6 +20,7 @@ from homeassistant.components.cover import ( SUPPORT_SET_TILT_POSITION, SUPPORT_STOP, SUPPORT_STOP_TILT, + CoverDeviceClass, CoverEntity, ) from homeassistant.const import ( @@ -83,7 +83,7 @@ class KNXCover(KnxEntity, CoverEntity): self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_device_class = config.get(CONF_DEVICE_CLASS) or ( - DEVICE_CLASS_BLIND if self._device.supports_angle else None + CoverDeviceClass.BLIND if self._device.supports_angle else None ) self._attr_supported_features = ( SUPPORT_CLOSE | SUPPORT_OPEN | SUPPORT_SET_POSITION From d5defa899572ff63cca893b3b4b66b374b3c36fc Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 15 Dec 2021 11:28:43 +0100 Subject: [PATCH 0442/2644] Support publishing MQTT messages with raw bytes payloads (#61090) * correctly publish mqtt ouput * Additional tests * Add template test with binary output * render_outgoing_payload with command templates * use MqttCommandTemplate helper class * add tests command_template * Additional tests * support pass-through for MqttComandTemplate * fix bugs * unify workform always initiate with hass * clean up * remove not needed lines * comment not adding value --- homeassistant/components/mqtt/__init__.py | 60 ++++++++++++++-- .../components/mqtt/alarm_control_panel.py | 12 ++-- homeassistant/components/mqtt/climate.py | 11 ++- homeassistant/components/mqtt/cover.py | 54 +++++++-------- homeassistant/components/mqtt/fan.py | 20 +++--- homeassistant/components/mqtt/humidifier.py | 20 +++--- homeassistant/components/mqtt/number.py | 21 +++--- homeassistant/components/mqtt/select.py | 21 +++--- tests/components/mqtt/test_init.py | 69 ++++++++++++++++++- 9 files changed, 209 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 7e93c26a887..fb9b4707ac8 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -1,6 +1,7 @@ """Support for MQTT message handling.""" from __future__ import annotations +from ast import literal_eval import asyncio from dataclasses import dataclass import datetime as dt @@ -250,6 +251,55 @@ MQTT_PUBLISH_SCHEMA = vol.All( SubscribePayloadType = Union[str, bytes] # Only bytes if encoding is None +class MqttCommandTemplate: + """Class for rendering MQTT payload with command templates.""" + + def __init__( + self, + command_template: template.Template | None, + hass: HomeAssistant, + ) -> None: + """Instantiate a command template.""" + self._attr_command_template = command_template + if command_template is None: + return + + command_template.hass = hass + + @callback + def async_render( + self, + value: PublishPayloadType = None, + variables: template.TemplateVarsType = None, + ) -> PublishPayloadType: + """Render or convert the command template with given value or variables.""" + + def _convert_outgoing_payload( + payload: PublishPayloadType, + ) -> PublishPayloadType: + """Ensure correct raw MQTT payload is passed as bytes for publishing.""" + if isinstance(payload, str): + try: + native_object = literal_eval(payload) + if isinstance(native_object, bytes): + return native_object + + except (ValueError, TypeError, SyntaxError, MemoryError): + pass + + return payload + + if self._attr_command_template is None: + return value + + values = {"value": value} + if variables is not None: + values.update(variables) + return _convert_outgoing_payload( + self._attr_command_template.async_render(values, parse_result=False) + ) + + @dataclass class MqttServiceInfo(BaseServiceInfo): """Prepared info from mqtt entries.""" @@ -295,7 +345,9 @@ async def async_publish( hass: HomeAssistant, topic: Any, payload, qos=0, retain=False ) -> None: """Publish message to an MQTT topic.""" - await hass.data[DATA_MQTT].async_publish(topic, str(payload), qos, retain) + await hass.data[DATA_MQTT].async_publish( + topic, str(payload) if not isinstance(payload, bytes) else payload, qos, retain + ) AsyncDeprecatedMessageCallbackType = Callable[ @@ -523,9 +575,9 @@ async def async_setup_entry(hass, entry): if payload_template is not None: try: - payload = template.Template(payload_template, hass).async_render( - parse_result=False - ) + payload = MqttCommandTemplate( + template.Template(payload_template), hass + ).async_render() except (template.jinja2.TemplateError, TemplateError) as exc: _LOGGER.error( "Unable to publish to %s: rendering payload template of " diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 3c324c0789b..5076ceade65 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -34,7 +34,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType -from . import PLATFORMS, subscription +from . import PLATFORMS, MqttCommandTemplate, subscription from .. import mqtt from .const import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, DOMAIN from .debug_info import log_messages @@ -150,8 +150,9 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): value_template = self._config.get(CONF_VALUE_TEMPLATE) if value_template is not None: value_template.hass = self.hass - command_template = self._config[CONF_COMMAND_TEMPLATE] - command_template.hass = self.hass + self._command_template = MqttCommandTemplate( + self._config[CONF_COMMAND_TEMPLATE], self.hass + ).async_render async def _subscribe_topics(self): """(Re)Subscribe to topics.""" @@ -306,9 +307,8 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): async def _publish(self, code, action): """Publish via mqtt.""" - command_template = self._config[CONF_COMMAND_TEMPLATE] - values = {"action": action, "code": code} - payload = command_template.async_render(**values, parse_result=False) + variables = {"action": action, "code": code} + payload = self._command_template(None, variables=variables) await mqtt.async_publish( self.hass, self._config[CONF_COMMAND_TOPIC], diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index e1f63252495..c1b104440cb 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -52,7 +52,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType -from . import MQTT_BASE_PLATFORM_SCHEMA, PLATFORMS, subscription +from . import MQTT_BASE_PLATFORM_SCHEMA, PLATFORMS, MqttCommandTemplate, subscription from .. import mqtt from .const import CONF_QOS, CONF_RETAIN, DOMAIN from .debug_info import log_messages @@ -377,11 +377,10 @@ class MqttClimate(MqttEntity, ClimateEntity): command_templates = {} for key in COMMAND_TEMPLATE_KEYS: - command_templates[key] = lambda value: value - for key in COMMAND_TEMPLATE_KEYS & config.keys(): - tpl = config[key] - command_templates[key] = tpl.async_render_with_possible_json_value - tpl.hass = self.hass + command_templates[key] = MqttCommandTemplate( + config.get(key), self.hass + ).async_render + self._command_templates = command_templates async def _subscribe_topics(self): # noqa: C901 diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index c2800cc8239..8b8a0764aca 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -36,7 +36,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType -from . import PLATFORMS, subscription +from . import PLATFORMS, MqttCommandTemplate, subscription from .. import mqtt from .const import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, DOMAIN from .debug_info import log_messages @@ -288,17 +288,17 @@ class MqttCover(MqttEntity, CoverEntity): if value_template is not None: value_template.hass = self.hass - set_position_template = self._config.get(CONF_SET_POSITION_TEMPLATE) - if set_position_template is not None: - set_position_template.hass = self.hass + self._set_position_template = MqttCommandTemplate( + self._config.get(CONF_SET_POSITION_TEMPLATE), self.hass + ).async_render get_position_template = self._config.get(CONF_GET_POSITION_TEMPLATE) if get_position_template is not None: get_position_template.hass = self.hass - set_tilt_template = self._config.get(CONF_TILT_COMMAND_TEMPLATE) - if set_tilt_template is not None: - set_tilt_template.hass = self.hass + self._set_tilt_template = MqttCommandTemplate( + self._config.get(CONF_TILT_COMMAND_TEMPLATE), self.hass + ).async_render tilt_status_template = self._config.get(CONF_TILT_STATUS_TEMPLATE) if tilt_status_template is not None: @@ -611,21 +611,19 @@ class MqttCover(MqttEntity, CoverEntity): async def async_set_cover_tilt_position(self, **kwargs): """Move the cover tilt to a specific position.""" - template = self._config.get(CONF_TILT_COMMAND_TEMPLATE) tilt = kwargs[ATTR_TILT_POSITION] percentage_tilt = tilt tilt = self.find_in_range_from_percent(tilt) # Handover the tilt after calculated from percent would make it more consistent with receiving templates - if template is not None: - variables = { - "tilt_position": percentage_tilt, - "entity_id": self.entity_id, - "position_open": self._config[CONF_POSITION_OPEN], - "position_closed": self._config[CONF_POSITION_CLOSED], - "tilt_min": self._config[CONF_TILT_MIN], - "tilt_max": self._config[CONF_TILT_MAX], - } - tilt = template.async_render(parse_result=False, variables=variables) + variables = { + "tilt_position": percentage_tilt, + "entity_id": self.entity_id, + "position_open": self._config.get(CONF_POSITION_OPEN), + "position_closed": self._config.get(CONF_POSITION_CLOSED), + "tilt_min": self._config.get(CONF_TILT_MIN), + "tilt_max": self._config.get(CONF_TILT_MAX), + } + tilt = self._set_tilt_template(tilt, variables=variables) await mqtt.async_publish( self.hass, @@ -641,20 +639,18 @@ class MqttCover(MqttEntity, CoverEntity): async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" - template = self._config.get(CONF_SET_POSITION_TEMPLATE) position = kwargs[ATTR_POSITION] percentage_position = position position = self.find_in_range_from_percent(position, COVER_PAYLOAD) - if template is not None: - variables = { - "position": percentage_position, - "entity_id": self.entity_id, - "position_open": self._config[CONF_POSITION_OPEN], - "position_closed": self._config[CONF_POSITION_CLOSED], - "tilt_min": self._config[CONF_TILT_MIN], - "tilt_max": self._config[CONF_TILT_MAX], - } - position = template.async_render(parse_result=False, variables=variables) + variables = { + "position": percentage_position, + "entity_id": self.entity_id, + "position_open": self._config[CONF_POSITION_OPEN], + "position_closed": self._config[CONF_POSITION_CLOSED], + "tilt_min": self._config[CONF_TILT_MIN], + "tilt_max": self._config[CONF_TILT_MAX], + } + position = self._set_position_template(position, variables=variables) await mqtt.async_publish( self.hass, diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 9d0c954f3ab..aecf94bdd42 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -36,7 +36,7 @@ from homeassistant.util.percentage import ( ranged_value_to_percentage, ) -from . import PLATFORMS, subscription +from . import PLATFORMS, MqttCommandTemplate, subscription from .. import mqtt from .const import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, DOMAIN from .debug_info import log_messages @@ -332,13 +332,17 @@ class MqttFan(MqttEntity, FanEntity): if self._feature_preset_mode: self._supported_features |= SUPPORT_PRESET_MODE - for tpl_dict in (self._command_templates, self._value_templates): - for key, tpl in tpl_dict.items(): - if tpl is None: - tpl_dict[key] = lambda value: value - else: - tpl.hass = self.hass - tpl_dict[key] = tpl.async_render_with_possible_json_value + for key, tpl in self._command_templates.items(): + self._command_templates[key] = MqttCommandTemplate( + tpl, self.hass + ).async_render + + for key, tpl in self._value_templates.items(): + if tpl is None: + self._value_templates[key] = lambda value: value + else: + tpl.hass = self.hass + self._value_templates[key] = tpl.async_render_with_possible_json_value async def _subscribe_topics(self): """(Re)Subscribe to topics.""" diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index a5346c0bf58..fcafa185509 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -27,7 +27,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType -from . import PLATFORMS, subscription +from . import PLATFORMS, MqttCommandTemplate, subscription from .. import mqtt from .const import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, DOMAIN from .debug_info import log_messages @@ -237,13 +237,17 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): ) self._optimistic_mode = optimistic or self._topic[CONF_MODE_STATE_TOPIC] is None - for tpl_dict in (self._command_templates, self._value_templates): - for key, tpl in tpl_dict.items(): - if tpl is None: - tpl_dict[key] = lambda value: value - else: - tpl.hass = self.hass - tpl_dict[key] = tpl.async_render_with_possible_json_value + for key, tpl in self._command_templates.items(): + self._command_templates[key] = MqttCommandTemplate( + tpl, self.hass + ).async_render + + for key, tpl in self._value_templates.items(): + if tpl is None: + self._value_templates[key] = lambda value: value + else: + tpl.hass = self.hass + self._value_templates[key] = tpl.async_render_with_possible_json_value async def _subscribe_topics(self): """(Re)Subscribe to topics.""" diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 29f6b4bad21..2c2c23606d4 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -25,7 +25,7 @@ from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType -from . import PLATFORMS, subscription +from . import PLATFORMS, MqttCommandTemplate, subscription from .. import mqtt from .const import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, DOMAIN from .debug_info import log_messages @@ -138,15 +138,20 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity): self._optimistic = config[CONF_OPTIMISTIC] self._templates = { - CONF_COMMAND_TEMPLATE: config.get(CONF_COMMAND_TEMPLATE), + CONF_COMMAND_TEMPLATE: MqttCommandTemplate( + config.get(CONF_COMMAND_TEMPLATE), self.hass + ).async_render, CONF_VALUE_TEMPLATE: config.get(CONF_VALUE_TEMPLATE), } - for key, tpl in self._templates.items(): - if tpl is None: - self._templates[key] = lambda value: value - else: - tpl.hass = self.hass - self._templates[key] = tpl.async_render_with_possible_json_value + + value_template = self._templates[CONF_VALUE_TEMPLATE] + if value_template is None: + self._templates[CONF_VALUE_TEMPLATE] = lambda value: value + else: + value_template.hass = self.hass + self._templates[ + CONF_VALUE_TEMPLATE + ] = value_template.async_render_with_possible_json_value async def _subscribe_topics(self): """(Re)Subscribe to topics.""" diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index 7b374ba8955..e51800953c0 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -13,7 +13,7 @@ from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType -from . import PLATFORMS, subscription +from . import PLATFORMS, MqttCommandTemplate, subscription from .. import mqtt from .const import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, DOMAIN from .debug_info import log_messages @@ -102,15 +102,20 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity): self._attr_options = config[CONF_OPTIONS] self._templates = { - CONF_COMMAND_TEMPLATE: config.get(CONF_COMMAND_TEMPLATE), + CONF_COMMAND_TEMPLATE: MqttCommandTemplate( + config.get(CONF_COMMAND_TEMPLATE), self.hass + ).async_render, CONF_VALUE_TEMPLATE: config.get(CONF_VALUE_TEMPLATE), } - for key, tpl in self._templates.items(): - if tpl is None: - self._templates[key] = lambda value: value - else: - tpl.hass = self.hass - self._templates[key] = tpl.async_render_with_possible_json_value + + value_template = self._templates[CONF_VALUE_TEMPLATE] + if value_template is None: + self._templates[CONF_VALUE_TEMPLATE] = lambda value: value + else: + value_template.hass = self.hass + self._templates[ + CONF_VALUE_TEMPLATE + ] = value_template.async_render_with_possible_json_value async def _subscribe_topics(self): """(Re)Subscribe to topics.""" diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index a4a3c1d6909..32528881d64 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -18,7 +18,7 @@ from homeassistant.const import ( ) from homeassistant.core import CoreState, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, template from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow @@ -91,7 +91,7 @@ async def test_mqtt_disconnects_on_home_assistant_stop(hass, mqtt_mock): assert mqtt_mock.async_disconnect.called -async def test_publish_(hass, mqtt_mock): +async def test_publish(hass, mqtt_mock): """Test the publish function.""" await mqtt.async_publish(hass, "test-topic", "test-payload") await hass.async_block_till_done() @@ -137,6 +137,57 @@ async def test_publish_(hass, mqtt_mock): ) mqtt_mock.reset_mock() + # test binary pass-through + mqtt.publish( + hass, + "test-topic3", + b"\xde\xad\xbe\xef", + 0, + False, + ) + await hass.async_block_till_done() + assert mqtt_mock.async_publish.called + assert mqtt_mock.async_publish.call_args[0] == ( + "test-topic3", + b"\xde\xad\xbe\xef", + 0, + False, + ) + mqtt_mock.reset_mock() + + +async def test_convert_outgoing_payload(hass): + """Test the converting of outgoing MQTT payloads without template.""" + command_template = mqtt.MqttCommandTemplate(None, hass) + assert command_template.async_render(b"\xde\xad\xbe\xef") == b"\xde\xad\xbe\xef" + + assert ( + command_template.async_render("b'\\xde\\xad\\xbe\\xef'") + == "b'\\xde\\xad\\xbe\\xef'" + ) + + assert command_template.async_render(1234) == 1234 + + assert command_template.async_render(1234.56) == 1234.56 + + assert command_template.async_render(None) is None + + +async def test_command_template_value(hass): + """Test the rendering of MQTT command template.""" + + variables = {"id": 1234, "some_var": "beer"} + + # test rendering value + tpl = template.Template("{{ value + 1 }}", hass) + cmd_tpl = mqtt.MqttCommandTemplate(tpl, hass) + assert cmd_tpl.async_render(4321) == "4322" + + # test variables at rendering + tpl = template.Template("{{ some_var }}", hass) + cmd_tpl = mqtt.MqttCommandTemplate(tpl, hass) + assert cmd_tpl.async_render(None, variables=variables) == "beer" + async def test_service_call_without_topic_does_not_publish(hass, mqtt_mock): """Test the service call if topic is missing.""" @@ -260,6 +311,20 @@ async def test_service_call_with_template_payload_renders_template(hass, mqtt_mo ) assert mqtt_mock.async_publish.called assert mqtt_mock.async_publish.call_args[0][1] == "8" + mqtt_mock.reset_mock() + + await hass.services.async_call( + mqtt.DOMAIN, + mqtt.SERVICE_PUBLISH, + { + mqtt.ATTR_TOPIC: "test/topic", + mqtt.ATTR_PAYLOAD_TEMPLATE: "{{ (4+4) | pack('B') }}", + }, + blocking=True, + ) + assert mqtt_mock.async_publish.called + assert mqtt_mock.async_publish.call_args[0][1] == b"\x08" + mqtt_mock.reset_mock() async def test_service_call_with_bad_template(hass, mqtt_mock): From bc61c5f49e3ea160c41c96249c71e9f3d570ea37 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 15 Dec 2021 12:06:16 +0100 Subject: [PATCH 0443/2644] Use new BinarySensorDeviceClass in keenetic_ndms2 (#61867) Co-authored-by: epenet --- homeassistant/components/keenetic_ndms2/binary_sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/keenetic_ndms2/binary_sensor.py b/homeassistant/components/keenetic_ndms2/binary_sensor.py index aa46c0fab02..351f3f66875 100644 --- a/homeassistant/components/keenetic_ndms2/binary_sensor.py +++ b/homeassistant/components/keenetic_ndms2/binary_sensor.py @@ -1,6 +1,6 @@ """The Keenetic Client class.""" from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_CONNECTIVITY, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -23,7 +23,7 @@ async def async_setup_entry( class RouterOnlineBinarySensor(BinarySensorEntity): """Representation router connection status.""" - _attr_device_class = DEVICE_CLASS_CONNECTIVITY + _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY _attr_should_poll = False def __init__(self, router: KeeneticRouter) -> None: From 19f398259da741f6fadd88a7d0f4c71e6411f944 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 15 Dec 2021 12:06:47 +0100 Subject: [PATCH 0444/2644] Use new enums in keba (#61869) Co-authored-by: epenet --- .../components/keba/binary_sensor.py | 35 +++++++++++++------ homeassistant/components/keba/sensor.py | 21 +++++------ 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/keba/binary_sensor.py b/homeassistant/components/keba/binary_sensor.py index 29292470155..05659bd8336 100644 --- a/homeassistant/components/keba/binary_sensor.py +++ b/homeassistant/components/keba/binary_sensor.py @@ -1,9 +1,6 @@ """Support for KEBA charging station binary sensors.""" from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_CONNECTIVITY, - DEVICE_CLASS_PLUG, - DEVICE_CLASS_POWER, - DEVICE_CLASS_SAFETY, + BinarySensorDeviceClass, BinarySensorEntity, ) @@ -19,14 +16,32 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= sensors = [ KebaBinarySensor( - keba, "Online", "Status", "device_state", DEVICE_CLASS_CONNECTIVITY - ), - KebaBinarySensor(keba, "Plug", "Plug", "plug_state", DEVICE_CLASS_PLUG), - KebaBinarySensor( - keba, "State", "Charging State", "charging_state", DEVICE_CLASS_POWER + keba, + "Online", + "Status", + "device_state", + BinarySensorDeviceClass.CONNECTIVITY, ), KebaBinarySensor( - keba, "Tmo FS", "Failsafe Mode", "failsafe_mode_state", DEVICE_CLASS_SAFETY + keba, + "Plug", + "Plug", + "plug_state", + BinarySensorDeviceClass.PLUG, + ), + KebaBinarySensor( + keba, + "State", + "Charging State", + "charging_state", + BinarySensorDeviceClass.POWER, + ), + KebaBinarySensor( + keba, + "Tmo FS", + "Failsafe Mode", + "failsafe_mode_state", + BinarySensorDeviceClass.SAFETY, ), ] async_add_entities(sensors) diff --git a/homeassistant/components/keba/sensor.py b/homeassistant/components/keba/sensor.py index fc761074bc7..248e6544646 100644 --- a/homeassistant/components/keba/sensor.py +++ b/homeassistant/components/keba/sensor.py @@ -2,13 +2,10 @@ from __future__ import annotations from homeassistant.components.sensor import ( - DEVICE_CLASS_CURRENT, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.const import ( ELECTRIC_CURRENT_AMPERE, @@ -34,7 +31,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= key="Curr user", name="Max Current", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, + device_class=SensorDeviceClass.CURRENT, ), ), KebaSensor( @@ -44,7 +41,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= key="Setenergy", name="Energy Target", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, ), ), KebaSensor( @@ -54,8 +51,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= key="P", name="Charging Power", native_unit_of_measurement=POWER_KILO_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), ), KebaSensor( @@ -65,7 +62,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= key="E pres", name="Session Energy", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, ), ), KebaSensor( @@ -75,8 +72,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= key="E total", name="Total Energy", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), ), ] From 9d3661647767666fdbbadb393ddda336e8f510aa Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 15 Dec 2021 12:12:54 +0100 Subject: [PATCH 0445/2644] Use new enums in kostal_plenticore (#61871) Co-authored-by: epenet --- .../components/kostal_plenticore/const.py | 182 +++++++++--------- 1 file changed, 93 insertions(+), 89 deletions(-) diff --git a/homeassistant/components/kostal_plenticore/const.py b/homeassistant/components/kostal_plenticore/const.py index ebed1ddcb74..d4e1fa47a8b 100644 --- a/homeassistant/components/kostal_plenticore/const.py +++ b/homeassistant/components/kostal_plenticore/const.py @@ -3,18 +3,13 @@ from typing import NamedTuple from homeassistant.components.sensor import ( ATTR_STATE_CLASS, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, + SensorStateClass, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_CURRENT, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_VOLTAGE, ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, @@ -48,9 +43,9 @@ SENSOR_PROCESS_DATA = [ "Solar Power", { ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, + ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, ATTR_ENABLED_DEFAULT: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, "format_round", ), @@ -60,9 +55,9 @@ SENSOR_PROCESS_DATA = [ "Grid Power", { ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, + ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, ATTR_ENABLED_DEFAULT: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, "format_round", ), @@ -70,7 +65,10 @@ SENSOR_PROCESS_DATA = [ "devices:local", "HomeBat_P", "Home Power from Battery", - {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, + { + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, + }, "format_round", ), ( @@ -79,8 +77,8 @@ SENSOR_PROCESS_DATA = [ "Home Power from Grid", { ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, "format_round", ), @@ -90,8 +88,8 @@ SENSOR_PROCESS_DATA = [ "Home Power from Own", { ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, "format_round", ), @@ -101,8 +99,8 @@ SENSOR_PROCESS_DATA = [ "Home Power from PV", { ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, "format_round", ), @@ -112,8 +110,8 @@ SENSOR_PROCESS_DATA = [ "Home Power", { ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, "format_round", ), @@ -123,9 +121,9 @@ SENSOR_PROCESS_DATA = [ "AC Power", { ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, + ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, ATTR_ENABLED_DEFAULT: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, "format_round", ), @@ -135,8 +133,8 @@ SENSOR_PROCESS_DATA = [ "DC1 Power", { ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, "format_round", ), @@ -146,8 +144,8 @@ SENSOR_PROCESS_DATA = [ "DC1 Voltage", { ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_POTENTIAL_VOLT, - ATTR_DEVICE_CLASS: DEVICE_CLASS_VOLTAGE, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_DEVICE_CLASS: SensorDeviceClass.VOLTAGE, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, "format_round", ), @@ -157,8 +155,8 @@ SENSOR_PROCESS_DATA = [ "DC1 Current", { ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, - ATTR_DEVICE_CLASS: DEVICE_CLASS_CURRENT, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_DEVICE_CLASS: SensorDeviceClass.CURRENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, "format_float", ), @@ -168,8 +166,8 @@ SENSOR_PROCESS_DATA = [ "DC2 Power", { ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, "format_round", ), @@ -179,8 +177,8 @@ SENSOR_PROCESS_DATA = [ "DC2 Voltage", { ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_POTENTIAL_VOLT, - ATTR_DEVICE_CLASS: DEVICE_CLASS_VOLTAGE, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_DEVICE_CLASS: SensorDeviceClass.VOLTAGE, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, "format_round", ), @@ -190,8 +188,8 @@ SENSOR_PROCESS_DATA = [ "DC2 Current", { ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, - ATTR_DEVICE_CLASS: DEVICE_CLASS_CURRENT, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_DEVICE_CLASS: SensorDeviceClass.CURRENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, "format_float", ), @@ -201,8 +199,8 @@ SENSOR_PROCESS_DATA = [ "DC3 Power", { ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, "format_round", ), @@ -212,8 +210,8 @@ SENSOR_PROCESS_DATA = [ "DC3 Voltage", { ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_POTENTIAL_VOLT, - ATTR_DEVICE_CLASS: DEVICE_CLASS_VOLTAGE, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_DEVICE_CLASS: SensorDeviceClass.VOLTAGE, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, "format_round", ), @@ -223,8 +221,8 @@ SENSOR_PROCESS_DATA = [ "DC3 Current", { ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, - ATTR_DEVICE_CLASS: DEVICE_CLASS_CURRENT, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_DEVICE_CLASS: SensorDeviceClass.CURRENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, "format_float", ), @@ -234,8 +232,8 @@ SENSOR_PROCESS_DATA = [ "PV to Battery Power", { ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, "format_round", ), @@ -250,7 +248,7 @@ SENSOR_PROCESS_DATA = [ "devices:local:battery", "Cycles", "Battery Cycles", - {ATTR_ICON: "mdi:recycle", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT}, + {ATTR_ICON: "mdi:recycle", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT}, "format_round", ), ( @@ -259,8 +257,8 @@ SENSOR_PROCESS_DATA = [ "Battery Power", { ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, "format_round", ), @@ -268,7 +266,10 @@ SENSOR_PROCESS_DATA = [ "devices:local:battery", "SoC", "Battery SoC", - {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY}, + { + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, + ATTR_DEVICE_CLASS: SensorDeviceClass.BATTERY, + }, "format_round", ), ( @@ -292,7 +293,7 @@ SENSOR_PROCESS_DATA = [ { ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, "format_round", ), @@ -324,7 +325,7 @@ SENSOR_PROCESS_DATA = [ { ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, "format_round", ), @@ -341,7 +342,7 @@ SENSOR_PROCESS_DATA = [ "Home Consumption Day", { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, "format_energy", ), @@ -351,7 +352,7 @@ SENSOR_PROCESS_DATA = [ "Home Consumption Month", { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, "format_energy", ), @@ -361,7 +362,7 @@ SENSOR_PROCESS_DATA = [ "Home Consumption Year", { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, "format_energy", ), @@ -371,8 +372,8 @@ SENSOR_PROCESS_DATA = [ "Home Consumption Total", { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, }, "format_energy", ), @@ -382,7 +383,7 @@ SENSOR_PROCESS_DATA = [ "Home Consumption from Battery Day", { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, "format_energy", ), @@ -392,7 +393,7 @@ SENSOR_PROCESS_DATA = [ "Home Consumption from Battery Month", { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, "format_energy", ), @@ -402,7 +403,7 @@ SENSOR_PROCESS_DATA = [ "Home Consumption from Battery Year", { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, "format_energy", ), @@ -412,8 +413,8 @@ SENSOR_PROCESS_DATA = [ "Home Consumption from Battery Total", { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, }, "format_energy", ), @@ -423,7 +424,7 @@ SENSOR_PROCESS_DATA = [ "Home Consumption from Grid Day", { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, "format_energy", ), @@ -433,7 +434,7 @@ SENSOR_PROCESS_DATA = [ "Home Consumption from Grid Month", { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, "format_energy", ), @@ -443,7 +444,7 @@ SENSOR_PROCESS_DATA = [ "Home Consumption from Grid Year", { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, "format_energy", ), @@ -453,8 +454,8 @@ SENSOR_PROCESS_DATA = [ "Home Consumption from Grid Total", { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, }, "format_energy", ), @@ -464,7 +465,7 @@ SENSOR_PROCESS_DATA = [ "Home Consumption from PV Day", { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, "format_energy", ), @@ -474,7 +475,7 @@ SENSOR_PROCESS_DATA = [ "Home Consumption from PV Month", { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, "format_energy", ), @@ -484,7 +485,7 @@ SENSOR_PROCESS_DATA = [ "Home Consumption from PV Year", { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, "format_energy", ), @@ -494,8 +495,8 @@ SENSOR_PROCESS_DATA = [ "Home Consumption from PV Total", { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, }, "format_energy", ), @@ -505,7 +506,7 @@ SENSOR_PROCESS_DATA = [ "Energy PV1 Day", { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, "format_energy", ), @@ -515,7 +516,7 @@ SENSOR_PROCESS_DATA = [ "Energy PV1 Month", { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, "format_energy", ), @@ -525,7 +526,7 @@ SENSOR_PROCESS_DATA = [ "Energy PV1 Year", { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, "format_energy", ), @@ -535,8 +536,8 @@ SENSOR_PROCESS_DATA = [ "Energy PV1 Total", { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, }, "format_energy", ), @@ -546,7 +547,7 @@ SENSOR_PROCESS_DATA = [ "Energy PV2 Day", { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, "format_energy", ), @@ -556,7 +557,7 @@ SENSOR_PROCESS_DATA = [ "Energy PV2 Month", { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, "format_energy", ), @@ -566,7 +567,7 @@ SENSOR_PROCESS_DATA = [ "Energy PV2 Year", { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, "format_energy", ), @@ -576,8 +577,8 @@ SENSOR_PROCESS_DATA = [ "Energy PV2 Total", { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, }, "format_energy", ), @@ -587,7 +588,7 @@ SENSOR_PROCESS_DATA = [ "Energy PV3 Day", { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, "format_energy", ), @@ -597,7 +598,7 @@ SENSOR_PROCESS_DATA = [ "Energy PV3 Month", { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, "format_energy", ), @@ -607,7 +608,7 @@ SENSOR_PROCESS_DATA = [ "Energy PV3 Year", { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, "format_energy", ), @@ -617,8 +618,8 @@ SENSOR_PROCESS_DATA = [ "Energy PV3 Total", { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, }, "format_energy", ), @@ -628,7 +629,7 @@ SENSOR_PROCESS_DATA = [ "Energy Yield Day", { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, ATTR_ENABLED_DEFAULT: True, }, "format_energy", @@ -639,7 +640,7 @@ SENSOR_PROCESS_DATA = [ "Energy Yield Month", { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, "format_energy", ), @@ -649,7 +650,7 @@ SENSOR_PROCESS_DATA = [ "Energy Yield Year", { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, }, "format_energy", ), @@ -659,8 +660,8 @@ SENSOR_PROCESS_DATA = [ "Energy Yield Total", { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, }, "format_energy", ), @@ -679,7 +680,10 @@ SENSOR_SETTINGS_DATA = [ "devices:local", "Battery:MinHomeComsumption", "Battery min Home Consumption", - {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, + { + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, + }, "format_round", ), ( From a6f0492623e8db0089888b27bc5eb6cd4c6d8cdd Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 15 Dec 2021 12:15:05 +0100 Subject: [PATCH 0446/2644] Bump aiohue to 3.0.5 (#61875) --- homeassistant/components/hue/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index 7f424f14594..f32d8edc284 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==3.0.4"], + "requirements": ["aiohue==3.0.5"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 18708ecc291..746a4197cf2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -192,7 +192,7 @@ aiohomekit==0.6.4 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.4 +aiohue==3.0.5 # homeassistant.components.imap aioimaplib==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cefee75ad0e..da4d9d5bb0c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -137,7 +137,7 @@ aiohomekit==0.6.4 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.4 +aiohue==3.0.5 # homeassistant.components.apache_kafka aiokafka==0.6.0 From 25cf690b20c911acd17f665f081e7f8bf3ab597d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 15 Dec 2021 12:28:04 +0100 Subject: [PATCH 0447/2644] Tidy up package constraint messages (#61866) Co-authored-by: epenet --- homeassistant/package_constraints.txt | 5 ++++- script/gen_requirements_all.py | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index cc755fb965c..6f39d79cdd8 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,6 @@ httpx==0.21.0 ifaddr==0.1.7 jinja2==3.0.3 paho-mqtt==1.6.1 -pillow==8.2.0 pip>=8.0.3,<20.3 pyserial==3.5 python-slugify==4.0.1 @@ -36,7 +35,11 @@ yarl==1.6.3 zeroconf==0.37.0 # Constrain pillow to 8.2.0 because later versions are causing issues in nightly builds. +# https://github.com/home-assistant/core/issues/61756 +pillow==8.2.0 +# Constrain pycryptodome to avoid vulnerability +# see https://github.com/home-assistant/core/pull/16238 pycryptodome>=3.6.6 # Constrain urllib3 to ensure we deal with CVE-2020-26137 and CVE-2021-33503 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 6dfc910d805..02ef8d929da 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -62,7 +62,11 @@ CONSTRAINT_PATH = os.path.join( ) CONSTRAINT_BASE = """ # Constrain pillow to 8.2.0 because later versions are causing issues in nightly builds. +# https://github.com/home-assistant/core/issues/61756 +pillow==8.2.0 +# Constrain pycryptodome to avoid vulnerability +# see https://github.com/home-assistant/core/pull/16238 pycryptodome>=3.6.6 # Constrain urllib3 to ensure we deal with CVE-2020-26137 and CVE-2021-33503 @@ -184,7 +188,7 @@ def gather_recursive_requirements(domain, seen=None): seen.add(domain) integration = Integration(Path(f"homeassistant/components/{domain}")) integration.load_manifest() - reqs = set(integration.requirements) + reqs = {x for x in integration.requirements if x not in CONSTRAINT_BASE} for dep_domain in integration.dependencies: reqs.update(gather_recursive_requirements(dep_domain, seen)) return reqs From 764ff65a383c4aa57c32955a1bc640a842faf945 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 15 Dec 2021 13:10:53 +0100 Subject: [PATCH 0448/2644] Use new DeviceClass enums in konnected (#61870) Co-authored-by: epenet --- .../components/konnected/config_flow.py | 14 ++++++++++---- homeassistant/components/konnected/handlers.py | 12 ++++-------- homeassistant/components/konnected/sensor.py | 18 ++++++++++-------- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/konnected/config_flow.py b/homeassistant/components/konnected/config_flow.py index 230a40f35b9..467f3518831 100644 --- a/homeassistant/components/konnected/config_flow.py +++ b/homeassistant/components/konnected/config_flow.py @@ -11,8 +11,8 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_DOOR, DEVICE_CLASSES_SCHEMA, + BinarySensorDeviceClass, ) from homeassistant.const import ( CONF_ACCESS_TOKEN, @@ -101,7 +101,9 @@ IO_SCHEMA = vol.Schema( BINARY_SENSOR_SCHEMA = vol.Schema( { vol.Required(CONF_ZONE): vol.In(ZONES), - vol.Required(CONF_TYPE, default=DEVICE_CLASS_DOOR): DEVICE_CLASSES_SCHEMA, + vol.Required( + CONF_TYPE, default=BinarySensorDeviceClass.DOOR + ): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_INVERSE, default=False): cv.boolean, } @@ -571,7 +573,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow): { vol.Required( CONF_TYPE, - default=current_cfg.get(CONF_TYPE, DEVICE_CLASS_DOOR), + default=current_cfg.get( + CONF_TYPE, BinarySensorDeviceClass.DOOR + ), ): DEVICE_CLASSES_SCHEMA, vol.Optional( CONF_NAME, default=current_cfg.get(CONF_NAME, vol.UNDEFINED) @@ -600,7 +604,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow): { vol.Required( CONF_TYPE, - default=current_cfg.get(CONF_TYPE, DEVICE_CLASS_DOOR), + default=current_cfg.get( + CONF_TYPE, BinarySensorDeviceClass.DOOR + ), ): DEVICE_CLASSES_SCHEMA, vol.Optional( CONF_NAME, diff --git a/homeassistant/components/konnected/handlers.py b/homeassistant/components/konnected/handlers.py index b3c3f440e6f..ef878fc6f2b 100644 --- a/homeassistant/components/konnected/handlers.py +++ b/homeassistant/components/konnected/handlers.py @@ -1,12 +1,8 @@ """Handle Konnected messages.""" import logging -from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_STATE, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_TEMPERATURE, -) +from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.const import ATTR_ENTITY_ID, ATTR_STATE from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.util import decorator @@ -32,7 +28,7 @@ async def async_handle_state_update(hass, context, msg): async def async_handle_temp_update(hass, context, msg): """Handle a temperature sensor state update.""" _LOGGER.debug("[temp handler] context: %s msg: %s", context, msg) - entity_id, temp = context.get(DEVICE_CLASS_TEMPERATURE), msg.get("temp") + entity_id, temp = context.get(SensorDeviceClass.TEMPERATURE), msg.get("temp") if entity_id: async_dispatcher_send(hass, f"konnected.{entity_id}.update", temp) @@ -41,7 +37,7 @@ async def async_handle_temp_update(hass, context, msg): async def async_handle_humi_update(hass, context, msg): """Handle a humidity sensor state update.""" _LOGGER.debug("[humi handler] context: %s msg: %s", context, msg) - entity_id, humi = context.get(DEVICE_CLASS_HUMIDITY), msg.get("humi") + entity_id, humi = context.get(SensorDeviceClass.HUMIDITY), msg.get("humi") if entity_id: async_dispatcher_send(hass, f"konnected.{entity_id}.update", humi) diff --git a/homeassistant/components/konnected/sensor.py b/homeassistant/components/konnected/sensor.py index 0b835e3fdce..8098ad2fb41 100644 --- a/homeassistant/components/konnected/sensor.py +++ b/homeassistant/components/konnected/sensor.py @@ -1,15 +1,17 @@ """Support for DHT and DS18B20 sensors attached to a Konnected device.""" from __future__ import annotations -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) from homeassistant.const import ( CONF_DEVICES, CONF_NAME, CONF_SENSORS, CONF_TYPE, CONF_ZONE, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_TEMPERATURE, PERCENTAGE, TEMP_CELSIUS, ) @@ -21,16 +23,16 @@ from .const import DOMAIN as KONNECTED_DOMAIN, SIGNAL_DS18B20_NEW SENSOR_TYPES: dict[str, SensorEntityDescription] = { "temperature": SensorEntityDescription( - key=DEVICE_CLASS_TEMPERATURE, + key="temperature", name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), "humidity": SensorEntityDescription( - key=DEVICE_CLASS_HUMIDITY, + key="humidity", name="Humidity", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, ), } @@ -130,7 +132,7 @@ class KonnectedSensor(SensorEntity): @callback def async_set_state(self, state): """Update the sensor's state.""" - if self.entity_description.key == DEVICE_CLASS_HUMIDITY: + if self.entity_description.key == "humidity": self._state = int(float(state)) else: self._state = round(float(state), 1) From bff677e34e1f9336d3ba2502e960ce472b0841f0 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 15 Dec 2021 04:12:38 -0800 Subject: [PATCH 0449/2644] Bump google-nest-sdm to 0.4.8 (#61851) --- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nest/test_events.py | 115 ++++++++++++++++---- 4 files changed, 98 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 507711c73ff..0c14e59babc 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["ffmpeg", "http", "media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.6"], + "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.8"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 746a4197cf2..5236fb6dc8e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -747,7 +747,7 @@ google-cloud-pubsub==2.1.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==0.4.6 +google-nest-sdm==0.4.8 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index da4d9d5bb0c..aa0360b08fb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -470,7 +470,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.1.0 # homeassistant.components.nest -google-nest-sdm==0.4.6 +google-nest-sdm==0.4.8 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/tests/components/nest/test_events.py b/tests/components/nest/test_events.py index 4767fd815d2..4a625999155 100644 --- a/tests/components/nest/test_events.py +++ b/tests/components/nest/test_events.py @@ -39,13 +39,12 @@ async def async_setup_devices(hass, device_type, traits={}): return await async_setup_sdm_platform(hass, PLATFORM, devices=devices) -def create_device_traits(event_trait): +def create_device_traits(event_traits=[]): """Create fake traits for a device.""" - return { + result = { "sdm.devices.traits.Info": { "customName": "Front", }, - event_trait: {}, "sdm.devices.traits.CameraLiveStream": { "maxVideoResolution": { "width": 640, @@ -55,6 +54,8 @@ def create_device_traits(event_trait): "audioCodecs": ["AAC"], }, } + result.update({t: {} for t in event_traits}) + return result def create_event(event_type, device_id=DEVICE_ID, timestamp=None): @@ -91,7 +92,7 @@ async def test_doorbell_chime_event(hass): subscriber = await async_setup_devices( hass, "sdm.devices.types.DOORBELL", - create_device_traits("sdm.devices.traits.DoorbellChime"), + create_device_traits(["sdm.devices.traits.DoorbellChime"]), ) registry = er.async_get(hass) @@ -129,7 +130,7 @@ async def test_camera_motion_event(hass): subscriber = await async_setup_devices( hass, "sdm.devices.types.CAMERA", - create_device_traits("sdm.devices.traits.CameraMotion"), + create_device_traits(["sdm.devices.traits.CameraMotion"]), ) registry = er.async_get(hass) entry = registry.async_get("camera.front") @@ -157,7 +158,7 @@ async def test_camera_sound_event(hass): subscriber = await async_setup_devices( hass, "sdm.devices.types.CAMERA", - create_device_traits("sdm.devices.traits.CameraSound"), + create_device_traits(["sdm.devices.traits.CameraSound"]), ) registry = er.async_get(hass) entry = registry.async_get("camera.front") @@ -185,7 +186,7 @@ async def test_camera_person_event(hass): subscriber = await async_setup_devices( hass, "sdm.devices.types.DOORBELL", - create_device_traits("sdm.devices.traits.CameraEventImage"), + create_device_traits(["sdm.devices.traits.CameraPerson"]), ) registry = er.async_get(hass) entry = registry.async_get("camera.front") @@ -213,7 +214,9 @@ async def test_camera_multiple_event(hass): subscriber = await async_setup_devices( hass, "sdm.devices.types.DOORBELL", - create_device_traits("sdm.devices.traits.CameraEventImage"), + create_device_traits( + ["sdm.devices.traits.CameraMotion", "sdm.devices.traits.CameraPerson"] + ), ) registry = er.async_get(hass) entry = registry.async_get("camera.front") @@ -256,7 +259,7 @@ async def test_unknown_event(hass): subscriber = await async_setup_devices( hass, "sdm.devices.types.DOORBELL", - create_device_traits("sdm.devices.traits.DoorbellChime"), + create_device_traits(["sdm.devices.traits.DoorbellChime"]), ) await subscriber.async_receive_event(create_event("some-event-id")) await hass.async_block_till_done() @@ -270,7 +273,7 @@ async def test_unknown_device_id(hass): subscriber = await async_setup_devices( hass, "sdm.devices.types.DOORBELL", - create_device_traits("sdm.devices.traits.DoorbellChime"), + create_device_traits(["sdm.devices.traits.DoorbellChime"]), ) await subscriber.async_receive_event( create_event("sdm.devices.events.DoorbellChime.Chime", "invalid-device-id") @@ -286,7 +289,7 @@ async def test_event_message_without_device_event(hass): subscriber = await async_setup_devices( hass, "sdm.devices.types.DOORBELL", - create_device_traits("sdm.devices.traits.DoorbellChime"), + create_device_traits(["sdm.devices.traits.DoorbellChime"]), ) timestamp = utcnow() event = EventMessage( @@ -308,14 +311,12 @@ async def test_doorbell_event_thread(hass): subscriber = await async_setup_devices( hass, "sdm.devices.types.DOORBELL", - traits={ - "sdm.devices.traits.Info": { - "customName": "Front", - }, - "sdm.devices.traits.CameraLiveStream": {}, - "sdm.devices.traits.CameraClipPreview": {}, - "sdm.devices.traits.CameraPerson": {}, - }, + create_device_traits( + [ + "sdm.devices.traits.CameraClipPreview", + "sdm.devices.traits.CameraPerson", + ] + ), ) registry = er.async_get(hass) entry = registry.async_get("camera.front") @@ -351,7 +352,7 @@ async def test_doorbell_event_thread(hass): ) await subscriber.async_receive_event(EventMessage(message_data_1, auth=None)) - # Publish message #1 that sends a no-op update to end the event thread + # Publish message #2 that sends a no-op update to end the event thread timestamp2 = timestamp1 + datetime.timedelta(seconds=1) message_data_2 = event_message_data.copy() message_data_2.update( @@ -371,3 +372,77 @@ async def test_doorbell_event_thread(hass): "timestamp": timestamp1.replace(microsecond=0), "nest_event_id": EVENT_SESSION_ID, } + + +async def test_doorbell_event_session_update(hass): + """Test a pubsub message with updates to an existing session.""" + events = async_capture_events(hass, NEST_EVENT) + subscriber = await async_setup_devices( + hass, + "sdm.devices.types.DOORBELL", + create_device_traits( + [ + "sdm.devices.traits.CameraClipPreview", + "sdm.devices.traits.CameraPerson", + "sdm.devices.traits.CameraMotion", + ] + ), + ) + registry = er.async_get(hass) + entry = registry.async_get("camera.front") + assert entry is not None + + # Message #1 has a motion event + timestamp1 = utcnow() + await subscriber.async_receive_event( + create_events( + { + "sdm.devices.events.CameraMotion.Motion": { + "eventSessionId": EVENT_SESSION_ID, + "eventId": "n:1", + }, + "sdm.devices.events.CameraClipPreview.ClipPreview": { + "eventSessionId": EVENT_SESSION_ID, + "previewUrl": "image-url-1", + }, + }, + timestamp=timestamp1, + ) + ) + + # Message #2 has an extra person event + timestamp2 = utcnow() + await subscriber.async_receive_event( + create_events( + { + "sdm.devices.events.CameraMotion.Motion": { + "eventSessionId": EVENT_SESSION_ID, + "eventId": "n:1", + }, + "sdm.devices.events.CameraPerson.Person": { + "eventSessionId": EVENT_SESSION_ID, + "eventId": "n:2", + }, + "sdm.devices.events.CameraClipPreview.ClipPreview": { + "eventSessionId": EVENT_SESSION_ID, + "previewUrl": "image-url-1", + }, + }, + timestamp=timestamp2, + ) + ) + await hass.async_block_till_done() + + assert len(events) == 2 + assert events[0].data == { + "device_id": entry.device_id, + "type": "camera_motion", + "timestamp": timestamp1.replace(microsecond=0), + "nest_event_id": EVENT_SESSION_ID, + } + assert events[1].data == { + "device_id": entry.device_id, + "type": "camera_person", + "timestamp": timestamp2.replace(microsecond=0), + "nest_event_id": EVENT_SESSION_ID, + } From 9dea96399f36f1eef2e3aafc5cf86549e437bab9 Mon Sep 17 00:00:00 2001 From: Michael Davie Date: Wed, 15 Dec 2021 07:13:59 -0500 Subject: [PATCH 0450/2644] Fix broken Environment Canada (#61848) --- homeassistant/components/environment_canada/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index b340674b480..868e62f07c3 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -2,7 +2,7 @@ "domain": "environment_canada", "name": "Environment Canada", "documentation": "https://www.home-assistant.io/integrations/environment_canada", - "requirements": ["env_canada==0.5.18"], + "requirements": ["env_canada==0.5.20"], "codeowners": ["@gwww", "@michaeldavie"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 5236fb6dc8e..b3350a72275 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -609,7 +609,7 @@ enocean==0.50 enturclient==0.2.2 # homeassistant.components.environment_canada -env_canada==0.5.18 +env_canada==0.5.20 # homeassistant.components.envirophat # envirophat==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aa0360b08fb..f165bf743ac 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -384,7 +384,7 @@ emulated_roku==0.2.1 enocean==0.50 # homeassistant.components.environment_canada -env_canada==0.5.18 +env_canada==0.5.20 # homeassistant.components.enphase_envoy envoy_reader==0.20.1 From cefbc2c4281c7153fbe35d5fc8ff7b4b8c23fed3 Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Wed, 15 Dec 2021 13:15:56 +0100 Subject: [PATCH 0451/2644] Allow setting local_ip for knx routing connections (#61836) --- homeassistant/components/knx/__init__.py | 1 + homeassistant/components/knx/config_flow.py | 19 ++++- homeassistant/components/knx/strings.json | 7 +- .../components/knx/translations/en.json | 5 +- tests/components/knx/test_config_flow.py | 64 ++++++++++++++- tests/components/knx/test_init.py | 78 +++++++++++++++++++ 6 files changed, 164 insertions(+), 10 deletions(-) create mode 100644 tests/components/knx/test_init.py diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index ba6689a023d..5a66824fbcb 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -383,6 +383,7 @@ class KNXModule: if _conn_type == CONF_KNX_ROUTING: return ConnectionConfig( connection_type=ConnectionType.ROUTING, + local_ip=self.config.get(ConnectionSchema.CONF_KNX_LOCAL_IP), auto_reconnect=True, ) if _conn_type == CONF_KNX_TUNNELING: diff --git a/homeassistant/components/knx/config_flow.py b/homeassistant/components/knx/config_flow.py index 30071752731..96aa8f67e3b 100644 --- a/homeassistant/components/knx/config_flow.py +++ b/homeassistant/components/knx/config_flow.py @@ -137,9 +137,11 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): vol.Required( ConnectionSchema.CONF_KNX_ROUTE_BACK, default=False ): vol.Coerce(bool), - vol.Optional(ConnectionSchema.CONF_KNX_LOCAL_IP): str, } + if self.show_advanced_options: + fields[vol.Optional(ConnectionSchema.CONF_KNX_LOCAL_IP)] = str + return self.async_show_form( step_id="manual_tunnel", data_schema=vol.Schema(fields), errors=errors ) @@ -195,6 +197,9 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): CONF_KNX_INDIVIDUAL_ADDRESS: user_input[ CONF_KNX_INDIVIDUAL_ADDRESS ], + ConnectionSchema.CONF_KNX_LOCAL_IP: user_input.get( + ConnectionSchema.CONF_KNX_LOCAL_IP + ), CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, }, ) @@ -211,6 +216,9 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ): cv.port, } + if self.show_advanced_options: + fields[vol.Optional(ConnectionSchema.CONF_KNX_LOCAL_IP)] = str + return self.async_show_form( step_id="routing", data_schema=vol.Schema(fields), errors=errors ) @@ -306,7 +314,6 @@ class KNXOptionsFlowHandler(OptionsFlow): vol.Required( CONF_PORT, default=self.current_config.get(CONF_PORT, 3671) ): cv.port, - vol.Optional(ConnectionSchema.CONF_KNX_LOCAL_IP): str, vol.Required( ConnectionSchema.CONF_KNX_ROUTE_BACK, default=self.current_config.get( @@ -381,6 +388,14 @@ class KNXOptionsFlowHandler(OptionsFlow): } if self.show_advanced_options: + data_schema[ + vol.Optional( + ConnectionSchema.CONF_KNX_LOCAL_IP, + default=self.current_config.get( + ConnectionSchema.CONF_KNX_LOCAL_IP, + ), + ) + ] = str data_schema[ vol.Required( ConnectionSchema.CONF_KNX_STATE_UPDATER, diff --git a/homeassistant/components/knx/strings.json b/homeassistant/components/knx/strings.json index 7f770c25427..4db92888aab 100644 --- a/homeassistant/components/knx/strings.json +++ b/homeassistant/components/knx/strings.json @@ -28,7 +28,8 @@ "data": { "individual_address": "Individual address for the routing connection", "multicast_group": "The multicast group used for routing", - "multicast_port": "The multicast port used for routing" + "multicast_port": "The multicast port used for routing", + "local_ip": "Local IP (leave empty if unsure)" } } }, @@ -48,6 +49,7 @@ "individual_address": "Default individual address", "multicast_group": "Multicast group used for routing and discovery", "multicast_port": "Multicast port used for routing and discovery", + "local_ip": "Local IP (leave empty if unsure)", "state_updater": "Globally enable reading states from the KNX Bus", "rate_limit": "Maximum outgoing telegrams per second" } @@ -56,8 +58,7 @@ "data": { "port": "[%key:common::config_flow::data::port%]", "host": "[%key:common::config_flow::data::host%]", - "route_back": "Route Back / NAT Mode", - "local_ip": "Local IP (leave empty if unsure)" + "route_back": "Route Back / NAT Mode" } } } diff --git a/homeassistant/components/knx/translations/en.json b/homeassistant/components/knx/translations/en.json index 5320f0cfb03..91b9dfce5f3 100644 --- a/homeassistant/components/knx/translations/en.json +++ b/homeassistant/components/knx/translations/en.json @@ -22,7 +22,8 @@ "data": { "individual_address": "Individual address for the routing connection", "multicast_group": "The multicast group used for routing", - "multicast_port": "The multicast port used for routing" + "multicast_port": "The multicast port used for routing", + "local_ip": "Local IP (leave empty if unsure)" }, "description": "Please configure the routing options." }, @@ -48,6 +49,7 @@ "individual_address": "Default individual address", "multicast_group": "Multicast group used for routing and discovery", "multicast_port": "Multicast port used for routing and discovery", + "local_ip": "Local IP (leave empty if unsure)", "rate_limit": "Maximum outgoing telegrams per second", "state_updater": "Globally enable reading states from the KNX Bus" } @@ -55,7 +57,6 @@ "tunnel": { "data": { "host": "Host", - "local_ip": "Local IP (leave empty if unsure)", "port": "Port", "route_back": "Route Back / NAT Mode" } diff --git a/tests/components/knx/test_config_flow.py b/tests/components/knx/test_config_flow.py index ff1fc362aa5..65289c2b173 100644 --- a/tests/components/knx/test_config_flow.py +++ b/tests/components/knx/test_config_flow.py @@ -83,6 +83,60 @@ async def test_routing_setup(hass: HomeAssistant) -> None: CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, + ConnectionSchema.CONF_KNX_LOCAL_IP: None, + CONF_KNX_INDIVIDUAL_ADDRESS: "1.1.110", + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_routing_setup_advanced(hass: HomeAssistant) -> None: + """Test routing setup with advanced options.""" + with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways: + gateways.return_value = [] + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_USER, + "show_advanced_options": True, + }, + ) + assert result["type"] == RESULT_TYPE_FORM + assert not result["errors"] + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, + }, + ) + await hass.async_block_till_done() + assert result2["type"] == RESULT_TYPE_FORM + assert result2["step_id"] == "routing" + assert not result2["errors"] + + with patch( + "homeassistant.components.knx.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, + CONF_KNX_INDIVIDUAL_ADDRESS: "1.1.110", + ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", + }, + ) + await hass.async_block_till_done() + assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["title"] == CONF_KNX_ROUTING.capitalize() + assert result3["data"] == { + **DEFAULT_ENTRY_DATA, + CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, + ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + ConnectionSchema.CONF_KNX_MCAST_PORT: 3675, + ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", CONF_KNX_INDIVIDUAL_ADDRESS: "1.1.110", } @@ -144,7 +198,11 @@ async def test_tunneling_setup_for_local_ip(hass: HomeAssistant) -> None: with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways: gateways.return_value = [gateway] result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, + context={ + "source": config_entries.SOURCE_USER, + "show_advanced_options": True, + }, ) assert result["type"] == RESULT_TYPE_FORM assert not result["errors"] @@ -563,7 +621,6 @@ async def test_tunneling_options_flow( CONF_HOST: "192.168.1.1", CONF_PORT: 3675, ConnectionSchema.CONF_KNX_ROUTE_BACK: True, - ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", }, ) @@ -581,7 +638,6 @@ async def test_tunneling_options_flow( CONF_HOST: "192.168.1.1", CONF_PORT: 3675, ConnectionSchema.CONF_KNX_ROUTE_BACK: True, - ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", } @@ -611,6 +667,7 @@ async def test_advanced_options( ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, ConnectionSchema.CONF_KNX_RATE_LIMIT: 25, ConnectionSchema.CONF_KNX_STATE_UPDATER: False, + ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", }, ) @@ -626,4 +683,5 @@ async def test_advanced_options( ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, ConnectionSchema.CONF_KNX_RATE_LIMIT: 25, ConnectionSchema.CONF_KNX_STATE_UPDATER: False, + ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", } diff --git a/tests/components/knx/test_init.py b/tests/components/knx/test_init.py new file mode 100644 index 00000000000..4380b132cbd --- /dev/null +++ b/tests/components/knx/test_init.py @@ -0,0 +1,78 @@ +"""Test KNX init.""" +import pytest +from xknx import XKNX +from xknx.io import ConnectionConfig, ConnectionType + +from homeassistant.components.knx.const import ( + CONF_KNX_AUTOMATIC, + CONF_KNX_CONNECTION_TYPE, + CONF_KNX_INDIVIDUAL_ADDRESS, + CONF_KNX_ROUTING, + CONF_KNX_TUNNELING, + DOMAIN as KNX_DOMAIN, +) +from homeassistant.components.knx.schema import ConnectionSchema +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant + +from .conftest import KNXTestKit + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize( + "config_entry_data,connection_config", + [ + ( + { + CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS, + CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, + }, + ConnectionConfig(), + ), + ( + { + CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, + ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.1", + }, + ConnectionConfig( + connection_type=ConnectionType.ROUTING, local_ip="192.168.1.1" + ), + ), + ( + { + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, + CONF_HOST: "192.168.0.2", + CONF_PORT: 3675, + ConnectionSchema.CONF_KNX_ROUTE_BACK: False, + ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112", + }, + ConnectionConfig( + connection_type=ConnectionType.TUNNELING, + route_back=False, + gateway_ip="192.168.0.2", + gateway_port=3675, + local_ip="192.168.1.112", + auto_reconnect=True, + ), + ), + ], +) +async def test_init_connection_handling( + hass: HomeAssistant, knx: KNXTestKit, config_entry_data, connection_config +): + """Test correctly generating connection config.""" + + config_entry = MockConfigEntry( + title="KNX", + domain=KNX_DOMAIN, + data=config_entry_data, + ) + knx.mock_config_entry = config_entry + await knx.setup_integration({}) + + assert hass.data.get(KNX_DOMAIN) is not None + + assert ( + hass.data[KNX_DOMAIN].connection_config().__dict__ == connection_config.__dict__ + ) From 537287172724c1af3f7cfcbd4208a7b9768b7e87 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 15 Dec 2021 05:17:09 -0700 Subject: [PATCH 0452/2644] Ensure SimpliSafe websocket reconnects upon new token (#61835) --- homeassistant/components/simplisafe/__init__.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index a3a80c0d107..e80716de9f8 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -613,10 +613,24 @@ class SimpliSafe: data={**self.entry.data, CONF_TOKEN: token}, ) + @callback + def async_handle_refresh_token(token: str) -> None: + """Handle a new refresh token.""" + async_save_refresh_token(token) + + if TYPE_CHECKING: + assert self._api.websocket + + if self._api.websocket.connected: + # If a websocket connection is open, reconnect it to use the + # new access token: + asyncio.create_task(self._api.websocket.async_reconnect()) + self.entry.async_on_unload( - self._api.add_refresh_token_callback(async_save_refresh_token) + self._api.add_refresh_token_callback(async_handle_refresh_token) ) + # Save the refresh token we got on entry setup: async_save_refresh_token(self._api.refresh_token) async def async_update(self) -> None: From cc3a4fef53297eb4a15d5e6cddf32bf8c3cf0928 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 15 Dec 2021 07:55:31 -0500 Subject: [PATCH 0453/2644] Remove deprecated yaml config from enphase envoy (#61840) --- .../components/enphase_envoy/config_flow.py | 24 +--------- .../components/enphase_envoy/sensor.py | 44 +------------------ .../enphase_envoy/test_config_flow.py | 36 --------------- 3 files changed, 2 insertions(+), 102 deletions(-) diff --git a/homeassistant/components/enphase_envoy/config_flow.py b/homeassistant/components/enphase_envoy/config_flow.py index d1e0febe2e6..fa43cb61ffe 100644 --- a/homeassistant/components/enphase_envoy/config_flow.py +++ b/homeassistant/components/enphase_envoy/config_flow.py @@ -11,13 +11,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import zeroconf -from homeassistant.const import ( - CONF_HOST, - CONF_IP_ADDRESS, - CONF_NAME, - CONF_PASSWORD, - CONF_USERNAME, -) +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError @@ -60,7 +54,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize an envoy flow.""" self.ip_address = None - self.name = None self.username = None self._reauth_entry = None @@ -80,19 +73,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): schema[vol.Optional(CONF_PASSWORD, default="")] = str return vol.Schema(schema) - async def async_step_import(self, import_config): - """Handle a flow import.""" - self.ip_address = import_config[CONF_IP_ADDRESS] - self.username = import_config[CONF_USERNAME] - self.name = import_config[CONF_NAME] - return await self.async_step_user( - { - CONF_HOST: import_config[CONF_IP_ADDRESS], - CONF_USERNAME: import_config[CONF_USERNAME], - CONF_PASSWORD: import_config[CONF_PASSWORD], - } - ) - @callback def _async_current_hosts(self): """Return a set of hosts.""" @@ -136,8 +116,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def _async_envoy_name(self) -> str: """Return the name of the envoy.""" - if self.name: - return self.name if self.unique_id: return f"{ENVOY} {self.unique_id}" return ENVOY diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index eda3c229255..1ca99748c51 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -1,55 +1,13 @@ """Support for Enphase Envoy solar energy monitor.""" from __future__ import annotations -import logging - -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import ( - CONF_IP_ADDRESS, - CONF_MONITORED_CONDITIONS, - CONF_NAME, - CONF_PASSWORD, - CONF_USERNAME, -) -import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import SensorEntity from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import COORDINATOR, DOMAIN, NAME, SENSORS ICON = "mdi:flash" -CONST_DEFAULT_HOST = "envoy" -_LOGGER = logging.getLogger(__name__) - -SENSOR_KEYS: list[str] = [desc.key for desc in SENSORS] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_IP_ADDRESS, default=CONST_DEFAULT_HOST): cv.string, - vol.Optional(CONF_USERNAME, default="envoy"): cv.string, - vol.Optional(CONF_PASSWORD, default=""): cv.string, - vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_KEYS): vol.All( - cv.ensure_list, [vol.In(SENSOR_KEYS)] - ), - vol.Optional(CONF_NAME, default=""): cv.string, - } -) - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Enphase Envoy sensor.""" - _LOGGER.warning( - "Loading enphase_envoy via platform config is deprecated; The configuration" - " has been migrated to a config entry and can be safely removed" - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config - ) - ) async def async_setup_entry(hass, config_entry, async_add_entities): diff --git a/tests/components/enphase_envoy/test_config_flow.py b/tests/components/enphase_envoy/test_config_flow.py index 41a49a7b245..d8b23ac9864 100644 --- a/tests/components/enphase_envoy/test_config_flow.py +++ b/tests/components/enphase_envoy/test_config_flow.py @@ -204,42 +204,6 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: assert result2["errors"] == {"base": "unknown"} -async def test_import(hass: HomeAssistant) -> None: - """Test we can import from yaml.""" - - with patch( - "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.getData", - return_value=True, - ), patch( - "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.get_full_serial_number", - return_value="1234", - ), patch( - "homeassistant.components.enphase_envoy.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - "ip_address": "1.1.1.1", - "name": "Pool Envoy", - "username": "test-username", - "password": "test-password", - }, - ) - await hass.async_block_till_done() - - assert result2["type"] == "create_entry" - assert result2["title"] == "Pool Envoy" - assert result2["data"] == { - "host": "1.1.1.1", - "name": "Pool Envoy", - "username": "test-username", - "password": "test-password", - } - assert len(mock_setup_entry.mock_calls) == 1 - - async def test_zeroconf(hass: HomeAssistant) -> None: """Test we can setup from zeroconf.""" From ef15b159c11597cf162f2c45cedaf9fae7c02215 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 15 Dec 2021 14:48:19 +0100 Subject: [PATCH 0454/2644] Use SensorDeviceClass in lacrosse (#61879) Co-authored-by: epenet --- homeassistant/components/lacrosse/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lacrosse/sensor.py b/homeassistant/components/lacrosse/sensor.py index 99aa39ce7cd..b3a944630b8 100644 --- a/homeassistant/components/lacrosse/sensor.py +++ b/homeassistant/components/lacrosse/sensor.py @@ -9,6 +9,7 @@ import voluptuous as vol from homeassistant.components.sensor import ( ENTITY_ID_FORMAT, PLATFORM_SCHEMA, + SensorDeviceClass, SensorEntity, ) from homeassistant.const import ( @@ -17,7 +18,6 @@ from homeassistant.const import ( CONF_NAME, CONF_SENSORS, CONF_TYPE, - DEVICE_CLASS_TEMPERATURE, EVENT_HOMEASSISTANT_STOP, PERCENTAGE, TEMP_CELSIUS, @@ -175,7 +175,7 @@ class LaCrosseSensor(SensorEntity): class LaCrosseTemperature(LaCrosseSensor): """Implementation of a Lacrosse temperature sensor.""" - _attr_device_class = DEVICE_CLASS_TEMPERATURE + _attr_device_class = SensorDeviceClass.TEMPERATURE _attr_native_unit_of_measurement = TEMP_CELSIUS @property From 6c4f3356507d1effd3ed452eb340616bf6624f11 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 15 Dec 2021 15:02:42 +0100 Subject: [PATCH 0455/2644] Use new enums in litterrobot (#61884) Co-authored-by: epenet --- homeassistant/components/litterrobot/button.py | 4 ++-- homeassistant/components/litterrobot/entity.py | 5 ++--- homeassistant/components/litterrobot/sensor.py | 6 +++--- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/litterrobot/button.py b/homeassistant/components/litterrobot/button.py index 3b8be295731..90fbecc6753 100644 --- a/homeassistant/components/litterrobot/button.py +++ b/homeassistant/components/litterrobot/button.py @@ -3,8 +3,8 @@ from __future__ import annotations from homeassistant.components.button import ButtonEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN @@ -35,7 +35,7 @@ class LitterRobotResetWasteDrawerButton(LitterRobotEntity, ButtonEntity): """Litter-Robot reset waste drawer button.""" _attr_icon = "mdi:delete-variant" - _attr_entity_category = ENTITY_CATEGORY_CONFIG + _attr_entity_category = EntityCategory.CONFIG async def async_press(self) -> None: """Press the button.""" diff --git a/homeassistant/components/litterrobot/entity.py b/homeassistant/components/litterrobot/entity.py index 206358201c3..2c4ed37f955 100644 --- a/homeassistant/components/litterrobot/entity.py +++ b/homeassistant/components/litterrobot/entity.py @@ -9,9 +9,8 @@ from typing import Any from pylitterbot import Robot from pylitterbot.exceptions import InvalidCommandException -from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import callback -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.event import async_call_later from homeassistant.helpers.update_coordinator import CoordinatorEntity import homeassistant.util.dt as dt_util @@ -119,7 +118,7 @@ class LitterRobotControlEntity(LitterRobotEntity): class LitterRobotConfigEntity(LitterRobotControlEntity): """A Litter-Robot entity that can control configuration of the unit.""" - _attr_entity_category = ENTITY_CATEGORY_CONFIG + _attr_entity_category = EntityCategory.CONFIG def __init__(self, robot: Robot, entity_type: str, hub: LitterRobotHub) -> None: """Init a Litter-Robot control entity.""" diff --git a/homeassistant/components/litterrobot/sensor.py b/homeassistant/components/litterrobot/sensor.py index 6b4dc1b3300..de3126986d1 100644 --- a/homeassistant/components/litterrobot/sensor.py +++ b/homeassistant/components/litterrobot/sensor.py @@ -5,9 +5,9 @@ from datetime import datetime from pylitterbot.robot import Robot -from homeassistant.components.sensor import SensorEntity, StateType +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity, StateType from homeassistant.config_entries import ConfigEntry -from homeassistant.const import DEVICE_CLASS_TIMESTAMP, PERCENTAGE +from homeassistant.const import PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -70,7 +70,7 @@ class LitterRobotSleepTimeSensor(LitterRobotPropertySensor): @property def device_class(self) -> str: """Return the device class, if any.""" - return DEVICE_CLASS_TIMESTAMP + return SensorDeviceClass.TIMESTAMP ROBOT_SENSORS: list[tuple[type[LitterRobotPropertySensor], str, str]] = [ From b5d54d9bb1bee14f042c0d559fbf36306a3d8138 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 15 Dec 2021 15:15:47 +0100 Subject: [PATCH 0456/2644] Update frontend to 20211215.0 (#61877) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 4380440408f..4ac95c38afc 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20211212.0" + "home-assistant-frontend==20211215.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6f39d79cdd8..529f970cfc5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ ciso8601==2.2.0 cryptography==35.0.0 emoji==1.5.0 hass-nabucasa==0.50.0 -home-assistant-frontend==20211212.0 +home-assistant-frontend==20211215.0 httpx==0.21.0 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index b3350a72275..954d31cad56 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -828,7 +828,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211212.0 +home-assistant-frontend==20211215.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f165bf743ac..00565651956 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -524,7 +524,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211212.0 +home-assistant-frontend==20211215.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 232d79366199a6dcf6e0573ad81e418c59e2a599 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 15 Dec 2021 15:17:18 +0100 Subject: [PATCH 0457/2644] Use new DeviceClass enums in lutron_caseta (#61887) Co-authored-by: epenet --- homeassistant/components/lutron_caseta/binary_sensor.py | 4 ++-- homeassistant/components/lutron_caseta/cover.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/lutron_caseta/binary_sensor.py b/homeassistant/components/lutron_caseta/binary_sensor.py index c2fc311de43..e0966cd0f42 100644 --- a/homeassistant/components/lutron_caseta/binary_sensor.py +++ b/homeassistant/components/lutron_caseta/binary_sensor.py @@ -2,7 +2,7 @@ from pylutron_caseta import OCCUPANCY_GROUP_OCCUPIED from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_OCCUPANCY, + BinarySensorDeviceClass, BinarySensorEntity, ) @@ -35,7 +35,7 @@ class LutronOccupancySensor(LutronCasetaDevice, BinarySensorEntity): @property def device_class(self): """Flag supported features.""" - return DEVICE_CLASS_OCCUPANCY + return BinarySensorDeviceClass.OCCUPANCY @property def is_on(self): diff --git a/homeassistant/components/lutron_caseta/cover.py b/homeassistant/components/lutron_caseta/cover.py index 31f7e9b55bd..111da12a6f5 100644 --- a/homeassistant/components/lutron_caseta/cover.py +++ b/homeassistant/components/lutron_caseta/cover.py @@ -3,12 +3,12 @@ import logging from homeassistant.components.cover import ( ATTR_POSITION, - DEVICE_CLASS_SHADE, DOMAIN, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION, SUPPORT_STOP, + CoverDeviceClass, CoverEntity, ) @@ -58,7 +58,7 @@ class LutronCasetaCover(LutronCasetaDevice, CoverEntity): @property def device_class(self): """Return the device class.""" - return DEVICE_CLASS_SHADE + return CoverDeviceClass.SHADE async def async_stop_cover(self, **kwargs): """Top the cover.""" From 61a6d278b880e14fb59d05eff6128f64780bb927 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 15 Dec 2021 15:17:37 +0100 Subject: [PATCH 0458/2644] Use new enums in lookin (#61885) Co-authored-by: epenet --- homeassistant/components/lookin/media_player.py | 8 +++++--- homeassistant/components/lookin/sensor.py | 13 ++++++------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/lookin/media_player.py b/homeassistant/components/lookin/media_player.py index 9aa105e8f87..a0eceb62f15 100644 --- a/homeassistant/components/lookin/media_player.py +++ b/homeassistant/components/lookin/media_player.py @@ -10,8 +10,7 @@ from aiolookin import Remote from aiolookin.models import UDPCommandType, UDPEvent from homeassistant.components.media_player import ( - DEVICE_CLASS_RECEIVER, - DEVICE_CLASS_TV, + MediaPlayerDeviceClass, MediaPlayerEntity, ) from homeassistant.components.media_player.const import ( @@ -34,7 +33,10 @@ from .models import LookinData LOGGER = logging.getLogger(__name__) -_TYPE_TO_DEVICE_CLASS = {"01": DEVICE_CLASS_TV, "02": DEVICE_CLASS_RECEIVER} +_TYPE_TO_DEVICE_CLASS = { + "01": MediaPlayerDeviceClass.TV, + "02": MediaPlayerDeviceClass.RECEIVER, +} _FUNCTION_NAME_TO_FEATURE = { "power": SUPPORT_TURN_OFF, diff --git a/homeassistant/components/lookin/sensor.py b/homeassistant/components/lookin/sensor.py index 7b3972b7ce3..6dee2f2c0ac 100644 --- a/homeassistant/components/lookin/sensor.py +++ b/homeassistant/components/lookin/sensor.py @@ -4,11 +4,10 @@ from __future__ import annotations import logging from homeassistant.components.sensor import ( - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, TEMP_CELSIUS @@ -27,15 +26,15 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="temperature", name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="humidity", name="Humidity", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), ) From 5b77fb9a0fce52a1437a9542c3a2dfacbdd2ce16 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 15 Dec 2021 15:57:33 +0100 Subject: [PATCH 0459/2644] Use BinarySensorDeviceClass in lutron (#61886) Co-authored-by: epenet --- homeassistant/components/lutron/binary_sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lutron/binary_sensor.py b/homeassistant/components/lutron/binary_sensor.py index db5aa5dcccc..27831825b4d 100644 --- a/homeassistant/components/lutron/binary_sensor.py +++ b/homeassistant/components/lutron/binary_sensor.py @@ -2,7 +2,7 @@ from pylutron import OccupancyGroup from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_OCCUPANCY, + BinarySensorDeviceClass, BinarySensorEntity, ) @@ -29,7 +29,7 @@ class LutronOccupancySensor(LutronDevice, BinarySensorEntity): reported as a single occupancy group. """ - _attr_device_class = DEVICE_CLASS_OCCUPANCY + _attr_device_class = BinarySensorDeviceClass.OCCUPANCY @property def is_on(self): From b91124c030241e1a9233b6edcff7c5937ea6c31b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 15 Dec 2021 16:32:48 +0100 Subject: [PATCH 0460/2644] Use _attr_attribution in meteoclimatic (#61898) Co-authored-by: epenet --- homeassistant/components/meteoclimatic/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/meteoclimatic/sensor.py b/homeassistant/components/meteoclimatic/sensor.py index 603940b4cc8..07992ba328b 100644 --- a/homeassistant/components/meteoclimatic/sensor.py +++ b/homeassistant/components/meteoclimatic/sensor.py @@ -1,7 +1,6 @@ """Support for Meteoclimatic sensor.""" from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo @@ -28,7 +27,7 @@ async def async_setup_entry( class MeteoclimaticSensor(CoordinatorEntity, SensorEntity): """Representation of a Meteoclimatic sensor.""" - _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + _attr_attribution = ATTRIBUTION def __init__( self, coordinator: DataUpdateCoordinator, description: SensorEntityDescription From ce2063fb07a81f7965654aeb0e4788fb36ed5006 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 15 Dec 2021 16:33:15 +0100 Subject: [PATCH 0461/2644] Use new SensorDeviceClass in metoffice (#61899) Co-authored-by: epenet --- homeassistant/components/metoffice/sensor.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/metoffice/sensor.py b/homeassistant/components/metoffice/sensor.py index 4f2ed842086..9940f0bd5bb 100644 --- a/homeassistant/components/metoffice/sensor.py +++ b/homeassistant/components/metoffice/sensor.py @@ -1,11 +1,13 @@ """Support for UK Met Office weather service.""" from __future__ import annotations -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) from homeassistant.const import ( ATTR_ATTRIBUTION, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_TEMPERATURE, LENGTH_KILOMETERS, PERCENTAGE, SPEED_MILES_PER_HOUR, @@ -58,7 +60,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="temperature", name="Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, icon=None, entity_registry_enabled_default=True, @@ -66,7 +68,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="feels_like_temperature", name="Feels Like Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, icon=None, entity_registry_enabled_default=False, @@ -130,7 +132,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="humidity", name="Humidity", - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=PERCENTAGE, icon=None, entity_registry_enabled_default=False, From e095cd95bf054897b7f9630331a891bb5a2e74e3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 15 Dec 2021 16:35:35 +0100 Subject: [PATCH 0462/2644] Use new SensorDeviceClass in meteoclimatic (#61897) Co-authored-by: epenet --- .../components/meteoclimatic/const.py | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/meteoclimatic/const.py b/homeassistant/components/meteoclimatic/const.py index f5883cc3856..424d89e9382 100644 --- a/homeassistant/components/meteoclimatic/const.py +++ b/homeassistant/components/meteoclimatic/const.py @@ -5,7 +5,7 @@ from datetime import timedelta from meteoclimatic import Condition -from homeassistant.components.sensor import SensorEntityDescription +from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLOUDY, @@ -25,9 +25,6 @@ from homeassistant.components.weather import ( ) from homeassistant.const import ( DEGREE, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, LENGTH_MILLIMETERS, PERCENTAGE, PRESSURE_HPA, @@ -53,55 +50,55 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="temp_current", name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( key="temp_max", name="Daily Max Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( key="temp_min", name="Daily Min Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( key="humidity_current", name="Humidity", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key="humidity_max", name="Daily Max Humidity", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key="humidity_min", name="Daily Min Humidity", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key="pressure_current", name="Pressure", native_unit_of_measurement=PRESSURE_HPA, - device_class=DEVICE_CLASS_PRESSURE, + device_class=SensorDeviceClass.PRESSURE, ), SensorEntityDescription( key="pressure_max", name="Daily Max Pressure", native_unit_of_measurement=PRESSURE_HPA, - device_class=DEVICE_CLASS_PRESSURE, + device_class=SensorDeviceClass.PRESSURE, ), SensorEntityDescription( key="pressure_min", name="Daily Min Pressure", native_unit_of_measurement=PRESSURE_HPA, - device_class=DEVICE_CLASS_PRESSURE, + device_class=SensorDeviceClass.PRESSURE, ), SensorEntityDescription( key="wind_current", From 3ac585ea2d53c187359016dfeb6b1638578d0cd3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 15 Dec 2021 16:36:22 +0100 Subject: [PATCH 0463/2644] Use new enums in meteo-france (#61896) Co-authored-by: epenet --- .../components/meteo_france/const.py | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/meteo_france/const.py b/homeassistant/components/meteo_france/const.py index 2967e5e5bec..df667b1964b 100644 --- a/homeassistant/components/meteo_france/const.py +++ b/homeassistant/components/meteo_france/const.py @@ -4,8 +4,9 @@ from __future__ import annotations from dataclasses import dataclass from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, + SensorDeviceClass, SensorEntityDescription, + SensorStateClass, ) from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, @@ -25,9 +26,6 @@ from homeassistant.components.weather import ( ATTR_CONDITION_WINDY_VARIANT, ) from homeassistant.const import ( - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_TIMESTAMP, LENGTH_MILLIMETERS, PERCENTAGE, PRESSURE_HPA, @@ -75,8 +73,8 @@ SENSOR_TYPES: tuple[MeteoFranceSensorEntityDescription, ...] = ( key="pressure", name="Pressure", native_unit_of_measurement=PRESSURE_HPA, - device_class=DEVICE_CLASS_PRESSURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, data_path="current_forecast:sea_level", ), @@ -84,7 +82,7 @@ SENSOR_TYPES: tuple[MeteoFranceSensorEntityDescription, ...] = ( key="wind_gust", name="Wind gust", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, icon="mdi:weather-windy-variant", entity_registry_enabled_default=False, data_path="current_forecast:wind:gust", @@ -93,7 +91,7 @@ SENSOR_TYPES: tuple[MeteoFranceSensorEntityDescription, ...] = ( key="wind_speed", name="Wind speed", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, icon="mdi:weather-windy", entity_registry_enabled_default=False, data_path="current_forecast:wind:speed", @@ -102,8 +100,8 @@ SENSOR_TYPES: tuple[MeteoFranceSensorEntityDescription, ...] = ( key="temperature", name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, data_path="current_forecast:T:value", ), @@ -146,7 +144,7 @@ SENSOR_TYPES_RAIN: tuple[MeteoFranceSensorEntityDescription, ...] = ( MeteoFranceSensorEntityDescription( key="next_rain", name="Next rain", - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, data_path="", ), ) From c9132b229d60fd00171baaca7a705b374c10491b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 15 Dec 2021 16:37:17 +0100 Subject: [PATCH 0464/2644] Use new enums in melcloud (#61894) Co-authored-by: epenet --- homeassistant/components/melcloud/sensor.py | 26 ++++++++++----------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/melcloud/sensor.py b/homeassistant/components/melcloud/sensor.py index 19be1ea172d..ab4cded5fa7 100644 --- a/homeassistant/components/melcloud/sensor.py +++ b/homeassistant/components/melcloud/sensor.py @@ -9,12 +9,10 @@ from pymelcloud import DEVICE_TYPE_ATA, DEVICE_TYPE_ATW from pymelcloud.atw_device import Zone from homeassistant.components.sensor import ( - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.const import ENERGY_KILO_WATT_HOUR, TEMP_CELSIUS @@ -43,7 +41,7 @@ ATA_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( name="Room Temperature", icon="mdi:thermometer", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, value_fn=lambda x: x.device.room_temperature, enabled=lambda x: True, ), @@ -52,7 +50,7 @@ ATA_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( name="Energy", icon="mdi:factory", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, value_fn=lambda x: x.device.total_energy_consumed, enabled=lambda x: x.device.has_energy_consumed_meter, ), @@ -63,7 +61,7 @@ ATW_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( name="Outside Temperature", icon="mdi:thermometer", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, value_fn=lambda x: x.device.outside_temperature, enabled=lambda x: True, ), @@ -72,7 +70,7 @@ ATW_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( name="Tank Temperature", icon="mdi:thermometer", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, value_fn=lambda x: x.device.tank_temperature, enabled=lambda x: True, ), @@ -83,7 +81,7 @@ ATW_ZONE_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( name="Room Temperature", icon="mdi:thermometer", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, value_fn=lambda zone: zone.room_temperature, enabled=lambda x: True, ), @@ -92,7 +90,7 @@ ATW_ZONE_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( name="Flow Temperature", icon="mdi:thermometer", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, value_fn=lambda zone: zone.flow_temperature, enabled=lambda x: True, ), @@ -101,7 +99,7 @@ ATW_ZONE_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( name="Flow Return Temperature", icon="mdi:thermometer", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, value_fn=lambda zone: zone.return_temperature, enabled=lambda x: True, ), @@ -152,10 +150,10 @@ class MelDeviceSensor(SensorEntity): self._attr_name = f"{api.name} {description.name}" self._attr_unique_id = f"{api.device.serial}-{api.device.mac}-{description.key}" - if description.device_class == DEVICE_CLASS_ENERGY: - self._attr_state_class = STATE_CLASS_TOTAL_INCREASING + if description.device_class == SensorDeviceClass.ENERGY: + self._attr_state_class = SensorStateClass.TOTAL_INCREASING else: - self._attr_state_class = STATE_CLASS_MEASUREMENT + self._attr_state_class = SensorStateClass.MEASUREMENT @property def native_value(self): From 59e4b52065abf72941525467b5e8e399cb771ffa Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 15 Dec 2021 16:37:35 +0100 Subject: [PATCH 0465/2644] Use new enums in lyric (#61888) Co-authored-by: epenet --- homeassistant/components/lyric/sensor.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/lyric/sensor.py b/homeassistant/components/lyric/sensor.py index be156594524..bded1f9c677 100644 --- a/homeassistant/components/lyric/sensor.py +++ b/homeassistant/components/lyric/sensor.py @@ -10,16 +10,12 @@ from aiolyric.objects.device import LyricDevice from aiolyric.objects.location import LyricLocation from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_TIMESTAMP, -) from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -81,8 +77,8 @@ async def async_setup_entry( LyricSensorEntityDescription( key=f"{device.macID}_indoor_temperature", name="Indoor Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=hass.config.units.temperature_unit, value=lambda device: device.indoorTemperature, ), @@ -97,8 +93,8 @@ async def async_setup_entry( LyricSensorEntityDescription( key=f"{device.macID}_outdoor_temperature", name="Outdoor Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=hass.config.units.temperature_unit, value=lambda device: device.outdoorTemperature, ), @@ -113,8 +109,8 @@ async def async_setup_entry( LyricSensorEntityDescription( key=f"{device.macID}_outdoor_humidity", name="Outdoor Humidity", - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement="%", value=lambda device: device.displayedOutdoorHumidity, ), @@ -130,7 +126,7 @@ async def async_setup_entry( LyricSensorEntityDescription( key=f"{device.macID}_next_period_time", name="Next Period Time", - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, value=lambda device: get_datetime_from_future_time( device.changeableValues.nextPeriodTime ), From 66b8f87e43fbc08f2d8a2ee7bfd0dccbbb2c59bb Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Wed, 15 Dec 2021 19:30:38 +0100 Subject: [PATCH 0466/2644] Fix notify platform setup for KNX (#61842) * Fix notify platform setup for KNX * Apply review suggestions * Store hass config in DATA_HASS_CONFIG * Readd guard clause --- homeassistant/components/knx/__init__.py | 18 +++++++++---- homeassistant/components/knx/const.py | 3 +++ homeassistant/components/knx/notify.py | 33 ++++++++++++++---------- 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 5a66824fbcb..61d49243430 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -29,6 +29,7 @@ from homeassistant.const import ( CONF_PORT, CONF_TYPE, EVENT_HOMEASSISTANT_STOP, + Platform, ) from homeassistant.core import Event, HomeAssistant, ServiceCall from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError @@ -44,6 +45,7 @@ from .const import ( CONF_KNX_INDIVIDUAL_ADDRESS, CONF_KNX_ROUTING, CONF_KNX_TUNNELING, + DATA_HASS_CONFIG, DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS, @@ -195,6 +197,7 @@ SERVICE_KNX_EXPOSURE_REGISTER_SCHEMA = vol.Any( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Start the KNX integration.""" + hass.data[DATA_HASS_CONFIG] = config conf: ConfigType | None = config.get(DOMAIN) if conf is None: @@ -251,15 +254,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) hass.config_entries.async_setup_platforms( - entry, [platform for platform in SUPPORTED_PLATFORMS if platform in config] + entry, + [ + platform + for platform in SUPPORTED_PLATFORMS + if platform in config and platform is not Platform.NOTIFY + ], ) - # set up notify platform, no entry support for notify component yet, - # have to use discovery to load platform. - if NotifySchema.PLATFORM in conf: + # set up notify platform, no entry support for notify component yet + if NotifySchema.PLATFORM in config: hass.async_create_task( discovery.async_load_platform( - hass, "notify", DOMAIN, conf[NotifySchema.PLATFORM], config + hass, "notify", DOMAIN, {}, hass.data[DATA_HASS_CONFIG] ) ) @@ -312,6 +319,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: platform for platform in SUPPORTED_PLATFORMS if platform in hass.data[DATA_KNX_CONFIG] + and platform is not Platform.NOTIFY ], ) if unload_ok: diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index 950deff95c1..f50460de173 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -42,7 +42,10 @@ CONF_STATE_ADDRESS: Final = "state_address" CONF_SYNC_STATE: Final = "sync_state" CONF_KNX_INITIAL_CONNECTION_TYPES: Final = [CONF_KNX_TUNNELING, CONF_KNX_ROUTING] +# yaml config merged with config entry data DATA_KNX_CONFIG: Final = "knx_config" +# original hass yaml config +DATA_HASS_CONFIG: Final = "knx_hass_config" ATTR_COUNTER: Final = "counter" ATTR_SOURCE: Final = "source" diff --git a/homeassistant/components/knx/notify.py b/homeassistant/components/knx/notify.py index 61bee14e5e2..ee170f55802 100644 --- a/homeassistant/components/knx/notify.py +++ b/homeassistant/components/knx/notify.py @@ -11,7 +11,8 @@ from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import DOMAIN, KNX_ADDRESS +from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS +from .schema import NotifySchema async def async_get_service( @@ -20,24 +21,28 @@ async def async_get_service( discovery_info: DiscoveryInfoType | None = None, ) -> KNXNotificationService | None: """Get the KNX notification service.""" - if not discovery_info: + if discovery_info is None: return None - platform_config: dict = discovery_info - xknx: XKNX = hass.data[DOMAIN].xknx + if platform_config := hass.data[DATA_KNX_CONFIG].get(NotifySchema.PLATFORM): + xknx: XKNX = hass.data[DOMAIN].xknx - notification_devices = [] - for device_config in platform_config: - notification_devices.append( - XknxNotification( - xknx, - name=device_config[CONF_NAME], - group_address=device_config[KNX_ADDRESS], + notification_devices = [] + for device_config in platform_config: + notification_devices.append( + XknxNotification( + xknx, + name=device_config[CONF_NAME], + group_address=device_config[KNX_ADDRESS], + ) ) + return ( + KNXNotificationService(notification_devices) + if notification_devices + else None ) - return ( - KNXNotificationService(notification_devices) if notification_devices else None - ) + + return None class KNXNotificationService(BaseNotificationService): From 67061aeb7d6369ef249fe15945cf0da4540796f2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 15 Dec 2021 20:09:10 +0100 Subject: [PATCH 0467/2644] Use new enums in miflora (#61922) --- homeassistant/components/miflora/sensor.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/miflora/sensor.py b/homeassistant/components/miflora/sensor.py index 1b13b4d0537..aed52014228 100644 --- a/homeassistant/components/miflora/sensor.py +++ b/homeassistant/components/miflora/sensor.py @@ -12,9 +12,10 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, - STATE_CLASS_MEASUREMENT, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.const import ( CONDUCTIVITY, @@ -23,9 +24,6 @@ from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_SCAN_INTERVAL, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_TEMPERATURE, EVENT_HOMEASSISTANT_START, LIGHT_LUX, PERCENTAGE, @@ -63,13 +61,13 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="temperature", name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( key="light", name="Light intensity", native_unit_of_measurement=LIGHT_LUX, - device_class=DEVICE_CLASS_ILLUMINANCE, + device_class=SensorDeviceClass.ILLUMINANCE, ), SensorEntityDescription( key="moisture", @@ -87,7 +85,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="battery", name="Battery", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_BATTERY, + device_class=SensorDeviceClass.BATTERY, ), ) @@ -148,7 +146,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class MiFloraSensor(SensorEntity): """Implementing the MiFlora sensor.""" - _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_state_class = SensorStateClass.MEASUREMENT def __init__( self, From b42e2e6ef0d82e46bba8f6509fb6b8d9d5ee4f55 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 15 Dec 2021 20:10:42 +0100 Subject: [PATCH 0468/2644] Fix typo in template select (#61919) --- homeassistant/components/template/select.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/template/select.py b/homeassistant/components/template/select.py index ee43ba906c0..c3312ac2375 100644 --- a/homeassistant/components/template/select.py +++ b/homeassistant/components/template/select.py @@ -86,7 +86,7 @@ async def async_setup_platform( """Set up the template select.""" if discovery_info is None: _LOGGER.warning( - "Template number entities can only be configured under template:" + "Template select entities can only be configured under template:" ) return From 2da2de2ac83a1ceab9262d042cb5cf803df665fa Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 15 Dec 2021 20:11:06 +0100 Subject: [PATCH 0469/2644] Use SensorDeviceClass in mhz19 (#61923) --- homeassistant/components/mhz19/sensor.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mhz19/sensor.py b/homeassistant/components/mhz19/sensor.py index a599a9f7dfb..60608e37cec 100644 --- a/homeassistant/components/mhz19/sensor.py +++ b/homeassistant/components/mhz19/sensor.py @@ -9,6 +9,7 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, + SensorDeviceClass, SensorEntity, SensorEntityDescription, ) @@ -17,8 +18,6 @@ from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, CONF_MONITORED_CONDITIONS, CONF_NAME, - DEVICE_CLASS_CO2, - DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, ) import homeassistant.helpers.config_validation as cv @@ -40,13 +39,13 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key=SENSOR_TEMPERATURE, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( key=SENSOR_CO2, name="CO2", native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, - device_class=DEVICE_CLASS_CO2, + device_class=SensorDeviceClass.CO2, ), ) SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] From d95ce8a026f914a4aacdc8383bbcf3377f8e3b58 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 15 Dec 2021 20:14:18 +0100 Subject: [PATCH 0470/2644] Use new BinarySensorDeviceClass in minecraft_server (#61924) --- homeassistant/components/minecraft_server/binary_sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/minecraft_server/binary_sensor.py b/homeassistant/components/minecraft_server/binary_sensor.py index 79325f9c90c..6e97619a06d 100644 --- a/homeassistant/components/minecraft_server/binary_sensor.py +++ b/homeassistant/components/minecraft_server/binary_sensor.py @@ -1,7 +1,7 @@ """The Minecraft Server binary sensor platform.""" from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_CONNECTIVITY, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -33,7 +33,7 @@ class MinecraftServerStatusBinarySensor(MinecraftServerEntity, BinarySensorEntit server=server, type_name=NAME_STATUS, icon=ICON_STATUS, - device_class=DEVICE_CLASS_CONNECTIVITY, + device_class=BinarySensorDeviceClass.CONNECTIVITY, ) self._is_on = False From 6102e0aae8ead38351368a1402657447ca8d3ef7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 15 Dec 2021 20:14:50 +0100 Subject: [PATCH 0471/2644] Use new SensorDeviceClass in mitemp-bt (#61925) --- homeassistant/components/mitemp_bt/sensor.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mitemp_bt/sensor.py b/homeassistant/components/mitemp_bt/sensor.py index ed6c7f27b94..281bdcd7bcc 100644 --- a/homeassistant/components/mitemp_bt/sensor.py +++ b/homeassistant/components/mitemp_bt/sensor.py @@ -11,6 +11,7 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, + SensorDeviceClass, SensorEntity, SensorEntityDescription, ) @@ -20,9 +21,6 @@ from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_TIMEOUT, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_TEMPERATURE, PERCENTAGE, TEMP_CELSIUS, ) @@ -55,19 +53,19 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="temperature", name="Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, ), SensorEntityDescription( key="humidity", name="Humidity", - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( key="battery", name="Battery", - device_class=DEVICE_CLASS_BATTERY, + device_class=SensorDeviceClass.BATTERY, native_unit_of_measurement=PERCENTAGE, ), ) From 85199b4cdec41e94912ae65f6e3767c7d9f75aea Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 15 Dec 2021 20:40:38 +0100 Subject: [PATCH 0472/2644] Use new enums in modern_forms (#61930) --- homeassistant/components/modern_forms/sensor.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/modern_forms/sensor.py b/homeassistant/components/modern_forms/sensor.py index 5c9e0a18575..6d2ef5b6dab 100644 --- a/homeassistant/components/modern_forms/sensor.py +++ b/homeassistant/components/modern_forms/sensor.py @@ -3,9 +3,8 @@ from __future__ import annotations from datetime import datetime -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import DEVICE_CLASS_TIMESTAMP from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -70,7 +69,7 @@ class ModernFormsLightTimerRemainingTimeSensor(ModernFormsSensor): key="light_timer_remaining_time", name=f"{coordinator.data.info.device_name} Light Sleep Time", ) - self._attr_device_class = DEVICE_CLASS_TIMESTAMP + self._attr_device_class = SensorDeviceClass.TIMESTAMP @property def native_value(self) -> StateType | datetime: @@ -100,7 +99,7 @@ class ModernFormsFanTimerRemainingTimeSensor(ModernFormsSensor): key="fan_timer_remaining_time", name=f"{coordinator.data.info.device_name} Fan Sleep Time", ) - self._attr_device_class = DEVICE_CLASS_TIMESTAMP + self._attr_device_class = SensorDeviceClass.TIMESTAMP @property def native_value(self) -> StateType | datetime: From f9a310ea49febaea5e1eedf0c3c7447153157415 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 15 Dec 2021 20:43:39 +0100 Subject: [PATCH 0473/2644] Use new enums in motioneye (#61932) --- homeassistant/components/motioneye/switch.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/motioneye/switch.py b/homeassistant/components/motioneye/switch.py index 695cde842a7..215c361e0cd 100644 --- a/homeassistant/components/motioneye/switch.py +++ b/homeassistant/components/motioneye/switch.py @@ -17,8 +17,8 @@ from motioneye_client.const import ( from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -30,37 +30,37 @@ MOTIONEYE_SWITCHES = [ key=KEY_MOTION_DETECTION, name="Motion Detection", entity_registry_enabled_default=True, - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=KEY_TEXT_OVERLAY, name="Text Overlay", entity_registry_enabled_default=False, - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=KEY_VIDEO_STREAMING, name="Video Streaming", entity_registry_enabled_default=False, - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=KEY_STILL_IMAGES, name="Still Images", entity_registry_enabled_default=True, - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=KEY_MOVIES, name="Movies", entity_registry_enabled_default=True, - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=KEY_UPLOAD_ENABLED, name="Upload Enabled", entity_registry_enabled_default=False, - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), ] From 6b13dc72852b4fae85fe48796d9dcf9060166ac6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 15 Dec 2021 20:44:27 +0100 Subject: [PATCH 0474/2644] Use new enums in myq (#61933) --- homeassistant/components/myq/binary_sensor.py | 8 ++++---- homeassistant/components/myq/cover.py | 7 +++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/myq/binary_sensor.py b/homeassistant/components/myq/binary_sensor.py index 2cb35d351c4..0fe1c546890 100644 --- a/homeassistant/components/myq/binary_sensor.py +++ b/homeassistant/components/myq/binary_sensor.py @@ -1,9 +1,9 @@ """Support for MyQ gateways.""" from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_CONNECTIVITY, + BinarySensorDeviceClass, BinarySensorEntity, ) -from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC +from homeassistant.helpers.entity import EntityCategory from . import MyQEntity from .const import DOMAIN, MYQ_COORDINATOR, MYQ_GATEWAY @@ -26,8 +26,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class MyQBinarySensorEntity(MyQEntity, BinarySensorEntity): """Representation of a MyQ gateway.""" - _attr_device_class = DEVICE_CLASS_CONNECTIVITY - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY + _attr_entity_category = EntityCategory.DIAGNOSTIC @property def name(self): diff --git a/homeassistant/components/myq/cover.py b/homeassistant/components/myq/cover.py index 4379137b3f1..02876524c50 100644 --- a/homeassistant/components/myq/cover.py +++ b/homeassistant/components/myq/cover.py @@ -3,10 +3,9 @@ from pymyq.const import DEVICE_TYPE_GATE as MYQ_DEVICE_TYPE_GATE from pymyq.errors import MyQError from homeassistant.components.cover import ( - DEVICE_CLASS_GARAGE, - DEVICE_CLASS_GATE, SUPPORT_CLOSE, SUPPORT_OPEN, + CoverDeviceClass, CoverEntity, ) from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING @@ -37,9 +36,9 @@ class MyQCover(MyQEntity, CoverEntity): super().__init__(coordinator, device) self._device = device if device.device_type == MYQ_DEVICE_TYPE_GATE: - self._attr_device_class = DEVICE_CLASS_GATE + self._attr_device_class = CoverDeviceClass.GATE else: - self._attr_device_class = DEVICE_CLASS_GARAGE + self._attr_device_class = CoverDeviceClass.GARAGE self._attr_unique_id = device.device_id @property From aa83b0388a2c52f037cd3cc37c496baae9814543 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 15 Dec 2021 20:44:41 +0100 Subject: [PATCH 0475/2644] Use new enums in mullvad (#61934) --- homeassistant/components/mullvad/binary_sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mullvad/binary_sensor.py b/homeassistant/components/mullvad/binary_sensor.py index 1a91bba2038..46d62985807 100644 --- a/homeassistant/components/mullvad/binary_sensor.py +++ b/homeassistant/components/mullvad/binary_sensor.py @@ -1,6 +1,6 @@ """Setup Mullvad VPN Binary Sensors.""" from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_CONNECTIVITY, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.const import CONF_DEVICE_CLASS, CONF_ID, CONF_NAME @@ -12,7 +12,7 @@ BINARY_SENSORS = ( { CONF_ID: "mullvad_exit_ip", CONF_NAME: "Mullvad Exit IP", - CONF_DEVICE_CLASS: DEVICE_CLASS_CONNECTIVITY, + CONF_DEVICE_CLASS: BinarySensorDeviceClass.CONNECTIVITY, }, ) From 7db3246de49e5ee66b91b4eb5562f584435b0e2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 15 Dec 2021 21:53:21 +0200 Subject: [PATCH 0476/2644] Make config entry disabled_by an enum (#60445) * Make config entry disabled_by an enum * Update homeassistant/config_entries.py Co-authored-by: Erik Montnemery --- .../components/config/config_entries.py | 3 +- homeassistant/config_entries.py | 43 ++++++++++++++++--- .../components/config/test_config_entries.py | 10 ++--- tests/components/zwave_js/test_init.py | 6 ++- tests/helpers/test_device_registry.py | 6 +-- tests/helpers/test_entity_registry.py | 2 +- tests/test_config_entries.py | 30 ++++++++++--- 7 files changed, 78 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 61df9dc190d..a30d65f5982 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -304,7 +304,8 @@ async def config_entry_update(hass, connection, msg): "type": "config_entries/disable", "entry_id": str, # We only allow setting disabled_by user via API. - "disabled_by": vol.Any(config_entries.DISABLED_USER, None), + # No Enum support like this in voluptuous, use .value + "disabled_by": vol.Any(config_entries.ConfigEntryDisabler.USER.value, None), } ) async def config_entry_disable(hass, connection, msg): diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index cdea9da2540..0dd9cbee52b 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -13,6 +13,7 @@ from typing import TYPE_CHECKING, Any, Callable, Optional, cast import weakref from homeassistant import data_entry_flow, loader +from homeassistant.backports.enum import StrEnum from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback from homeassistant.exceptions import ( @@ -22,6 +23,7 @@ from homeassistant.exceptions import ( ) from homeassistant.helpers import device_registry, entity_registry from homeassistant.helpers.event import Event +from homeassistant.helpers.frame import report from homeassistant.helpers.typing import ( UNDEFINED, ConfigType, @@ -128,7 +130,15 @@ RECONFIGURE_NOTIFICATION_ID = "config_entry_reconfigure" EVENT_FLOW_DISCOVERED = "config_entry_discovered" -DISABLED_USER = "user" + +class ConfigEntryDisabler(StrEnum): + """What disabled a config entry.""" + + USER = "user" + + +# DISABLED_* is deprecated, to be removed in 2022.3 +DISABLED_USER = ConfigEntryDisabler.USER.value RELOAD_AFTER_UPDATE_DELAY = 30 @@ -195,7 +205,7 @@ class ConfigEntry: unique_id: str | None = None, entry_id: str | None = None, state: ConfigEntryState = ConfigEntryState.NOT_LOADED, - disabled_by: str | None = None, + disabled_by: ConfigEntryDisabler | None = None, ) -> None: """Initialize a config entry.""" # Unique id of the config entry @@ -237,6 +247,16 @@ class ConfigEntry: self.unique_id = unique_id # Config entry is disabled + if isinstance(disabled_by, str) and not isinstance( + disabled_by, ConfigEntryDisabler + ): + report( # type: ignore[unreachable] + "uses str for config entry disabled_by. This is deprecated and will " + "stop working in Home Assistant 2022.3, it should be updated to use " + "ConfigEntryDisabler instead", + error_if_core=False, + ) + disabled_by = ConfigEntryDisabler(disabled_by) self.disabled_by = disabled_by # Supports unload @@ -924,7 +944,9 @@ class ConfigEntries: # New in 0.104 unique_id=entry.get("unique_id"), # New in 2021.3 - disabled_by=entry.get("disabled_by"), + disabled_by=ConfigEntryDisabler(entry["disabled_by"]) + if entry.get("disabled_by") + else None, # New in 2021.6 pref_disable_new_entities=pref_disable_new_entities, pref_disable_polling=entry.get("pref_disable_polling"), @@ -985,7 +1007,7 @@ class ConfigEntries: return await self.async_setup(entry_id) async def async_set_disabled_by( - self, entry_id: str, disabled_by: str | None + self, entry_id: str, disabled_by: ConfigEntryDisabler | None ) -> bool: """Disable an entry. @@ -994,7 +1016,18 @@ class ConfigEntries: if (entry := self.async_get_entry(entry_id)) is None: raise UnknownEntry - if entry.disabled_by == disabled_by: + if isinstance(disabled_by, str) and not isinstance( + disabled_by, ConfigEntryDisabler + ): + report( # type: ignore[unreachable] + "uses str for config entry disabled_by. This is deprecated and will " + "stop working in Home Assistant 2022.3, it should be updated to use " + "ConfigEntryDisabler instead", + error_if_core=False, + ) + disabled_by = ConfigEntryDisabler(disabled_by) + + if entry.disabled_by is disabled_by: return True entry.disabled_by = disabled_by diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 20a19495597..7f88d9b482b 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -79,7 +79,7 @@ async def test_get_entries(hass, client): domain="comp3", title="Test 3", source="bla3", - disabled_by=core_ce.DISABLED_USER, + disabled_by=core_ce.ConfigEntryDisabler.USER, ).add_to_hass(hass) resp = await client.get("/api/config/config_entries/entry") @@ -121,7 +121,7 @@ async def test_get_entries(hass, client): "supports_unload": False, "pref_disable_new_entities": False, "pref_disable_polling": False, - "disabled_by": core_ce.DISABLED_USER, + "disabled_by": core_ce.ConfigEntryDisabler.USER, "reason": None, }, ] @@ -877,14 +877,14 @@ async def test_disable_entry(hass, hass_ws_client): "id": 5, "type": "config_entries/disable", "entry_id": entry.entry_id, - "disabled_by": core_ce.DISABLED_USER, + "disabled_by": core_ce.ConfigEntryDisabler.USER, } ) response = await ws_client.receive_json() assert response["success"] assert response["result"] == {"require_restart": True} - assert entry.disabled_by == core_ce.DISABLED_USER + assert entry.disabled_by is core_ce.ConfigEntryDisabler.USER assert entry.state is core_ce.ConfigEntryState.FAILED_UNLOAD # Enable @@ -930,7 +930,7 @@ async def test_disable_entry_nonexisting(hass, hass_ws_client): "id": 5, "type": "config_entries/disable", "entry_id": "non_existing", - "disabled_by": core_ce.DISABLED_USER, + "disabled_by": core_ce.ConfigEntryDisabler.USER, } ) response = await ws_client.receive_json() diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index b2cb7bc808e..d9a1695f60f 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -10,7 +10,7 @@ from zwave_js_server.model.node import Node from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.zwave_js.const import DOMAIN from homeassistant.components.zwave_js.helpers import get_device_id -from homeassistant.config_entries import DISABLED_USER, ConfigEntryState +from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -554,7 +554,9 @@ async def test_stop_addon( assert entry.state is ConfigEntryState.LOADED - await hass.config_entries.async_set_disabled_by(entry.entry_id, DISABLED_USER) + await hass.config_entries.async_set_disabled_by( + entry.entry_id, ConfigEntryDisabler.USER + ) await hass.async_block_till_done() assert entry.state == entry_state diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 455c90b8f65..571fa0c097e 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -1342,7 +1342,7 @@ async def test_disable_config_entry_disables_devices(hass, registry): assert entry2.disabled await hass.config_entries.async_set_disabled_by( - config_entry.entry_id, config_entries.DISABLED_USER + config_entry.entry_id, config_entries.ConfigEntryDisabler.USER ) await hass.async_block_till_done() @@ -1382,7 +1382,7 @@ async def test_only_disable_device_if_all_config_entries_are_disabled(hass, regi assert not entry1.disabled await hass.config_entries.async_set_disabled_by( - config_entry1.entry_id, config_entries.DISABLED_USER + config_entry1.entry_id, config_entries.ConfigEntryDisabler.USER ) await hass.async_block_till_done() @@ -1390,7 +1390,7 @@ async def test_only_disable_device_if_all_config_entries_are_disabled(hass, regi assert not entry1.disabled await hass.config_entries.async_set_disabled_by( - config_entry2.entry_id, config_entries.DISABLED_USER + config_entry2.entry_id, config_entries.ConfigEntryDisabler.USER ) await hass.async_block_till_done() diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index e34e33db005..50c2ea364c5 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -919,7 +919,7 @@ async def test_disable_config_entry_disables_entities(hass, registry): assert entry3.disabled await hass.config_entries.async_set_disabled_by( - config_entry.entry_id, config_entries.DISABLED_USER + config_entry.entry_id, config_entries.ConfigEntryDisabler.USER ) await hass.async_block_till_done() diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 0e743fda91e..80f2f086377 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -529,7 +529,7 @@ async def test_domains_gets_domains_excludes_ignore_and_disabled(manager): ).add_to_manager(manager) MockConfigEntry(domain="test3").add_to_manager(manager) MockConfigEntry( - domain="disabled", disabled_by=config_entries.DISABLED_USER + domain="disabled", disabled_by=config_entries.ConfigEntryDisabler.USER ).add_to_manager(manager) assert manager.async_domains() == ["test", "test2", "test3"] assert manager.async_domains(include_ignore=False) == ["test", "test2", "test3"] @@ -1323,7 +1323,7 @@ async def test_entry_disable_succeed(hass, manager): # Disable assert await manager.async_set_disabled_by( - entry.entry_id, config_entries.DISABLED_USER + entry.entry_id, config_entries.ConfigEntryDisabler.USER ) assert len(async_unload_entry.mock_calls) == 1 assert len(async_setup.mock_calls) == 0 @@ -1358,7 +1358,7 @@ async def test_entry_disable_without_reload_support(hass, manager): # Disable assert not await manager.async_set_disabled_by( - entry.entry_id, config_entries.DISABLED_USER + entry.entry_id, config_entries.ConfigEntryDisabler.USER ) assert len(async_setup.mock_calls) == 0 assert len(async_setup_entry.mock_calls) == 0 @@ -1374,7 +1374,9 @@ async def test_entry_disable_without_reload_support(hass, manager): async def test_entry_enable_without_reload_support(hass, manager): """Test that we can disable an entry without reload support.""" - entry = MockConfigEntry(domain="comp", disabled_by=config_entries.DISABLED_USER) + entry = MockConfigEntry( + domain="comp", disabled_by=config_entries.ConfigEntryDisabler.USER + ) entry.add_to_hass(hass) async_setup = AsyncMock(return_value=True) @@ -1398,7 +1400,7 @@ async def test_entry_enable_without_reload_support(hass, manager): # Disable assert not await manager.async_set_disabled_by( - entry.entry_id, config_entries.DISABLED_USER + entry.entry_id, config_entries.ConfigEntryDisabler.USER ) assert len(async_setup.mock_calls) == 1 assert len(async_setup_entry.mock_calls) == 1 @@ -2966,3 +2968,21 @@ async def test_loading_old_data(hass, hass_storage): assert entry.title == "Mock title" assert entry.data == {"my": "data"} assert entry.pref_disable_new_entities is True + + +async def test_deprecated_disabled_by_str_ctor(hass, caplog): + """Test deprecated str disabled_by constructor enumizes and logs a warning.""" + entry = MockConfigEntry(disabled_by=config_entries.ConfigEntryDisabler.USER.value) + assert entry.disabled_by is config_entries.ConfigEntryDisabler.USER + assert " str for config entry disabled_by. This is deprecated " in caplog.text + + +async def test_deprecated_disabled_by_str_set(hass, manager, caplog): + """Test deprecated str set disabled_by enumizes and logs a warning.""" + entry = MockConfigEntry() + entry.add_to_manager(manager) + assert await manager.async_set_disabled_by( + entry.entry_id, config_entries.ConfigEntryDisabler.USER.value + ) + assert entry.disabled_by is config_entries.ConfigEntryDisabler.USER + assert " str for config entry disabled_by. This is deprecated " in caplog.text From 77829e397b6cdf0b73957eba419bdec0edd86fcf Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 15 Dec 2021 11:54:57 -0800 Subject: [PATCH 0477/2644] Don't log DB connection string on error (#61927) --- homeassistant/components/recorder/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 333d955ab63..f9dde1f17c9 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -824,8 +824,7 @@ class Recorder(threading.Thread): return migration.get_schema_version(self) except Exception as err: # pylint: disable=broad-except _LOGGER.exception( - "Error during connection setup to %s: %s (retrying in %s seconds)", - self.db_url, + "Error during connection setup: %s (retrying in %s seconds)", err, self.db_retry_wait, ) From 1e9e05667158a592c5d46b8cf6aad37a4bad16d6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 15 Dec 2021 21:46:48 +0100 Subject: [PATCH 0478/2644] Use new enums in notion (#61950) --- .../components/notion/binary_sensor.py | 34 ++++++++----------- homeassistant/components/notion/sensor.py | 9 ++--- 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/notion/binary_sensor.py b/homeassistant/components/notion/binary_sensor.py index 42e32b1e19c..57c70849a9a 100644 --- a/homeassistant/components/notion/binary_sensor.py +++ b/homeassistant/components/notion/binary_sensor.py @@ -5,19 +5,13 @@ from dataclasses import dataclass from typing import Literal from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_CONNECTIVITY, - DEVICE_CLASS_DOOR, - DEVICE_CLASS_GARAGE_DOOR, - DEVICE_CLASS_MOISTURE, - DEVICE_CLASS_SMOKE, - DEVICE_CLASS_WINDOW, + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import NotionEntity @@ -55,63 +49,63 @@ BINARY_SENSOR_DESCRIPTIONS = ( NotionBinarySensorDescription( key=SENSOR_BATTERY, name="Low Battery", - device_class=DEVICE_CLASS_BATTERY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, on_state="critical", ), NotionBinarySensorDescription( key=SENSOR_DOOR, name="Door", - device_class=DEVICE_CLASS_DOOR, + device_class=BinarySensorDeviceClass.DOOR, on_state="open", ), NotionBinarySensorDescription( key=SENSOR_GARAGE_DOOR, name="Garage Door", - device_class=DEVICE_CLASS_GARAGE_DOOR, + device_class=BinarySensorDeviceClass.GARAGE_DOOR, on_state="open", ), NotionBinarySensorDescription( key=SENSOR_LEAK, name="Leak Detector", - device_class=DEVICE_CLASS_MOISTURE, + device_class=BinarySensorDeviceClass.MOISTURE, on_state="leak", ), NotionBinarySensorDescription( key=SENSOR_MISSING, name="Missing", - device_class=DEVICE_CLASS_CONNECTIVITY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=EntityCategory.DIAGNOSTIC, on_state="not_missing", ), NotionBinarySensorDescription( key=SENSOR_SAFE, name="Safe", - device_class=DEVICE_CLASS_DOOR, + device_class=BinarySensorDeviceClass.DOOR, on_state="open", ), NotionBinarySensorDescription( key=SENSOR_SLIDING, name="Sliding Door/Window", - device_class=DEVICE_CLASS_DOOR, + device_class=BinarySensorDeviceClass.DOOR, on_state="open", ), NotionBinarySensorDescription( key=SENSOR_SMOKE_CO, name="Smoke/Carbon Monoxide Detector", - device_class=DEVICE_CLASS_SMOKE, + device_class=BinarySensorDeviceClass.SMOKE, on_state="alarm", ), NotionBinarySensorDescription( key=SENSOR_WINDOW_HINGED_HORIZONTAL, name="Hinged Window", - device_class=DEVICE_CLASS_WINDOW, + device_class=BinarySensorDeviceClass.WINDOW, on_state="open", ), NotionBinarySensorDescription( key=SENSOR_WINDOW_HINGED_VERTICAL, name="Hinged Window", - device_class=DEVICE_CLASS_WINDOW, + device_class=BinarySensorDeviceClass.WINDOW, on_state="open", ), ) diff --git a/homeassistant/components/notion/sensor.py b/homeassistant/components/notion/sensor.py index 2e7260080bb..ff7ac7d23a6 100644 --- a/homeassistant/components/notion/sensor.py +++ b/homeassistant/components/notion/sensor.py @@ -1,11 +1,12 @@ """Support for Notion sensors.""" from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS +from homeassistant.const import TEMP_CELSIUS from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -16,9 +17,9 @@ SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=SENSOR_TEMPERATURE, name="Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), ) From 25d33a21269a311c2aa3fda2fd97680cfce5bd25 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 15 Dec 2021 22:00:10 +0100 Subject: [PATCH 0479/2644] Use new enums in nest (#61942) --- .../components/nest/legacy/binary_sensor.py | 15 ++++++--------- .../components/nest/legacy/sensor.py | 8 +++----- homeassistant/components/nest/sensor_sdm.py | 19 +++++++++---------- 3 files changed, 18 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/nest/legacy/binary_sensor.py b/homeassistant/components/nest/legacy/binary_sensor.py index c257ddd9456..79e1bf65c86 100644 --- a/homeassistant/components/nest/legacy/binary_sensor.py +++ b/homeassistant/components/nest/legacy/binary_sensor.py @@ -3,10 +3,7 @@ from itertools import chain import logging from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_CONNECTIVITY, - DEVICE_CLASS_MOTION, - DEVICE_CLASS_OCCUPANCY, - DEVICE_CLASS_SOUND, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.const import CONF_BINARY_SENSORS, CONF_MONITORED_CONDITIONS @@ -16,7 +13,7 @@ from .const import DATA_NEST, DATA_NEST_CONFIG _LOGGER = logging.getLogger(__name__) -BINARY_TYPES = {"online": DEVICE_CLASS_CONNECTIVITY} +BINARY_TYPES = {"online": BinarySensorDeviceClass.CONNECTIVITY} CLIMATE_BINARY_TYPES = { "fan": None, @@ -26,9 +23,9 @@ CLIMATE_BINARY_TYPES = { } CAMERA_BINARY_TYPES = { - "motion_detected": DEVICE_CLASS_MOTION, - "sound_detected": DEVICE_CLASS_SOUND, - "person_detected": DEVICE_CLASS_OCCUPANCY, + "motion_detected": BinarySensorDeviceClass.MOTION, + "sound_detected": BinarySensorDeviceClass.SOUND, + "person_detected": BinarySensorDeviceClass.OCCUPANCY, } STRUCTURE_BINARY_TYPES = {"away": None} @@ -160,7 +157,7 @@ class NestActivityZoneSensor(NestBinarySensor): @property def device_class(self): """Return the device class of the binary sensor.""" - return DEVICE_CLASS_MOTION + return BinarySensorDeviceClass.MOTION def update(self): """Retrieve latest state.""" diff --git a/homeassistant/components/nest/legacy/sensor.py b/homeassistant/components/nest/legacy/sensor.py index 7b3ba5ec2fe..c5229177b60 100644 --- a/homeassistant/components/nest/legacy/sensor.py +++ b/homeassistant/components/nest/legacy/sensor.py @@ -1,12 +1,10 @@ """Support for Nest Thermostat sensors for the legacy API.""" import logging -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_SENSORS, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_TEMPERATURE, PERCENTAGE, STATE_OFF, TEMP_CELSIUS, @@ -47,7 +45,7 @@ _VALID_SENSOR_TYPES = ( SENSOR_UNITS = {"humidity": PERCENTAGE} -SENSOR_DEVICE_CLASSES = {"humidity": DEVICE_CLASS_HUMIDITY} +SENSOR_DEVICE_CLASSES = {"humidity": SensorDeviceClass.HUMIDITY} VARIABLE_NAME_MAPPING = {"eta": "eta_begin", "operation_mode": "mode"} @@ -201,7 +199,7 @@ class NestTempSensor(NestSensorDevice, SensorEntity): @property def device_class(self): """Return the device class of the sensor.""" - return DEVICE_CLASS_TEMPERATURE + return SensorDeviceClass.TEMPERATURE def update(self): """Retrieve latest state.""" diff --git a/homeassistant/components/nest/sensor_sdm.py b/homeassistant/components/nest/sensor_sdm.py index 786efde43f7..30587606b19 100644 --- a/homeassistant/components/nest/sensor_sdm.py +++ b/homeassistant/components/nest/sensor_sdm.py @@ -7,14 +7,13 @@ from google_nest_sdm.device import Device from google_nest_sdm.device_traits import HumidityTrait, TemperatureTrait from google_nest_sdm.exceptions import GoogleNestException -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_TEMPERATURE, - PERCENTAGE, - TEMP_CELSIUS, +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorStateClass, ) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -58,7 +57,7 @@ class SensorBase(SensorEntity): """Representation of a dynamically updated Sensor.""" _attr_shoud_poll = False - _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_state_class = SensorStateClass.MEASUREMENT def __init__(self, device: Device) -> None: """Initialize the sensor.""" @@ -77,7 +76,7 @@ class SensorBase(SensorEntity): class TemperatureSensor(SensorBase): """Representation of a Temperature Sensor.""" - _attr_device_class = DEVICE_CLASS_TEMPERATURE + _attr_device_class = SensorDeviceClass.TEMPERATURE _attr_native_unit_of_measurement = TEMP_CELSIUS @property @@ -98,7 +97,7 @@ class TemperatureSensor(SensorBase): class HumiditySensor(SensorBase): """Representation of a Humidity Sensor.""" - _attr_device_class = DEVICE_CLASS_HUMIDITY + _attr_device_class = SensorDeviceClass.HUMIDITY _attr_native_unit_of_measurement = PERCENTAGE @property From 75e7104339f3e73a0a6b30f384a02832cb7bc12b Mon Sep 17 00:00:00 2001 From: javicalle <31999997+javicalle@users.noreply.github.com> Date: Wed, 15 Dec 2021 22:09:54 +0100 Subject: [PATCH 0480/2644] A few RFLink tests (#58544) * Full coverage for RFLink tests * use global constants * extend RFLink keepalive tests --- tests/components/rflink/test_init.py | 98 +++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 1 deletion(-) diff --git a/tests/components/rflink/test_init.py b/tests/components/rflink/test_init.py index f93c9703d30..73def144aba 100644 --- a/tests/components/rflink/test_init.py +++ b/tests/components/rflink/test_init.py @@ -7,15 +7,24 @@ from voluptuous.error import MultipleInvalid from homeassistant.bootstrap import async_setup_component from homeassistant.components.rflink import ( + CONF_KEEPALIVE_IDLE, CONF_RECONNECT_INTERVAL, DATA_ENTITY_LOOKUP, + DEFAULT_TCP_KEEPALIVE_IDLE_TIMER, + DOMAIN as RFLINK_DOMAIN, EVENT_KEY_COMMAND, EVENT_KEY_SENSOR, SERVICE_SEND_COMMAND, TMP_ENTITY, RflinkCommand, ) -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_STOP_COVER, SERVICE_TURN_OFF +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_HOST, + CONF_PORT, + SERVICE_STOP_COVER, + SERVICE_TURN_OFF, +) async def mock_rflink( @@ -380,3 +389,90 @@ async def test_not_connected(hass, monkeypatch): RflinkCommand.set_rflink_protocol(None) with pytest.raises(HomeAssistantError): await test_device._async_handle_command("turn_on") + + +async def test_keepalive(hass, monkeypatch, caplog): + """Validate negative keepalive values.""" + keepalive_value = -3 + domain = RFLINK_DOMAIN + config = { + RFLINK_DOMAIN: { + CONF_HOST: "10.10.0.1", + CONF_PORT: 1234, + CONF_KEEPALIVE_IDLE: keepalive_value, + } + } + + # setup mocking rflink module + _, mock_create, _, _ = await mock_rflink(hass, config, domain, monkeypatch) + + assert mock_create.call_args_list[0][1]["host"] == "10.10.0.1" + assert mock_create.call_args_list[0][1]["port"] == 1234 + assert ( + mock_create.call_args_list[0][1]["keepalive"] is None + ) # negative keepalive is not allowed + assert ( + f"A bogus TCP Keepalive IDLE timer was provided ({keepalive_value} secs)" + in caplog.text + ) + + +async def test2_keepalive(hass, monkeypatch, caplog): + """Validate very short keepalive values.""" + keepalive_value = 30 + domain = RFLINK_DOMAIN + config = { + RFLINK_DOMAIN: { + CONF_HOST: "10.10.0.1", + CONF_PORT: 1234, + CONF_KEEPALIVE_IDLE: keepalive_value, + } + } + + # setup mocking rflink module + _, mock_create, _, _ = await mock_rflink(hass, config, domain, monkeypatch) + + assert mock_create.call_args_list[0][1]["host"] == "10.10.0.1" + assert mock_create.call_args_list[0][1]["port"] == 1234 + assert ( + mock_create.call_args_list[0][1]["keepalive"] == keepalive_value + ) # very short keepalive is allowed but warned + assert ( + f"A very short TCP Keepalive IDLE timer was provided ({keepalive_value} secs)" + in caplog.text + ) + + +async def test3_keepalive(hass, monkeypatch, caplog): + """Validate keepalive=0 value.""" + domain = RFLINK_DOMAIN + config = { + RFLINK_DOMAIN: {CONF_HOST: "10.10.0.1", CONF_PORT: 1234, CONF_KEEPALIVE_IDLE: 0} + } + + # setup mocking rflink module + _, mock_create, _, _ = await mock_rflink(hass, config, domain, monkeypatch) + + assert mock_create.call_args_list[0][1]["host"] == "10.10.0.1" + assert mock_create.call_args_list[0][1]["port"] == 1234 + assert ( + mock_create.call_args_list[0][1]["keepalive"] is None + ) # keepalive=0 will disable it + assert "TCP Keepalive IDLE timer was provided" not in caplog.text + + +async def test_default_keepalive(hass, monkeypatch, caplog): + """Validate keepalive=0 value.""" + domain = RFLINK_DOMAIN + config = {RFLINK_DOMAIN: {CONF_HOST: "10.10.0.1", CONF_PORT: 1234}} + + # setup mocking rflink module + _, mock_create, _, _ = await mock_rflink(hass, config, domain, monkeypatch) + + assert mock_create.call_args_list[0][1]["host"] == "10.10.0.1" + assert mock_create.call_args_list[0][1]["port"] == 1234 + assert ( + mock_create.call_args_list[0][1]["keepalive"] + == DEFAULT_TCP_KEEPALIVE_IDLE_TIMER + ) # no keepalive config will default it + assert "TCP Keepalive IDLE timer was provided" not in caplog.text From 5bf67cac6681d0b477bbc22da430db9c81546808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 15 Dec 2021 23:25:40 +0200 Subject: [PATCH 0481/2644] Use RegistryEntryDisabler (#60436) --- homeassistant/config_entries.py | 5 +- tests/components/accuweather/test_sensor.py | 2 +- tests/components/ambee/test_sensor.py | 2 +- tests/components/brother/test_sensor.py | 2 +- .../components/config/test_entity_registry.py | 8 ++- tests/components/efergy/test_sensor.py | 2 +- .../components/forecast_solar/test_sensor.py | 2 +- tests/components/fronius/__init__.py | 4 +- .../greeneye_monitor/test_sensor.py | 9 ++- tests/components/hue/test_light_v2.py | 2 +- tests/components/hue/test_sensor_v2.py | 4 +- tests/components/hyperion/test_light.py | 2 +- tests/components/hyperion/test_switch.py | 2 +- tests/components/ipp/test_sensor.py | 2 +- tests/components/litejet/test_scene.py | 2 +- tests/components/met/test_weather.py | 2 +- .../components/monoprice/test_media_player.py | 2 +- tests/components/motioneye/test_sensor.py | 2 +- tests/components/motioneye/test_switch.py | 2 +- tests/components/nam/test_sensor.py | 2 +- tests/components/onewire/__init__.py | 4 +- tests/components/ozw/test_binary_sensor.py | 2 +- tests/components/ozw/test_sensor.py | 2 +- tests/components/p1_monitor/test_sensor.py | 2 +- tests/components/plex/test_sensor.py | 6 +- tests/components/renault/test_sensor.py | 4 +- tests/components/sonarr/test_sensor.py | 2 +- tests/components/switcher_kis/test_sensor.py | 2 +- tests/components/tasmota/test_sensor.py | 12 ++-- tests/components/wled/test_sensor.py | 2 +- .../components/zwave_js/test_binary_sensor.py | 2 +- tests/components/zwave_js/test_number.py | 2 +- tests/components/zwave_js/test_sensor.py | 4 +- tests/helpers/test_entity.py | 4 +- tests/helpers/test_entity_platform.py | 6 +- tests/helpers/test_entity_registry.py | 72 +++++++++---------- tests/test_config_entries.py | 8 ++- 37 files changed, 106 insertions(+), 90 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 0dd9cbee52b..d1910364f4c 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1587,12 +1587,13 @@ def _handle_entry_updated_filter(event: Event) -> bool: """Handle entity registry entry update filter. Only handle changes to "disabled_by". - If "disabled_by" was DISABLED_CONFIG_ENTRY, reload is not needed. + If "disabled_by" was CONFIG_ENTRY, reload is not needed. """ if ( event.data["action"] != "update" or "disabled_by" not in event.data["changes"] - or event.data["changes"]["disabled_by"] == entity_registry.DISABLED_CONFIG_ENTRY + or event.data["changes"]["disabled_by"] + is entity_registry.RegistryEntryDisabler.CONFIG_ENTRY ): return False return True diff --git a/tests/components/accuweather/test_sensor.py b/tests/components/accuweather/test_sensor.py index 62f282f2cf3..f7f7e30adfc 100644 --- a/tests/components/accuweather/test_sensor.py +++ b/tests/components/accuweather/test_sensor.py @@ -190,7 +190,7 @@ async def test_sensor_disabled(hass): assert entry assert entry.unique_id == "0123456-apparenttemperature" assert entry.disabled - assert entry.disabled_by == er.DISABLED_INTEGRATION + assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION # Test enabling entity updated_entry = registry.async_update_entity( diff --git a/tests/components/ambee/test_sensor.py b/tests/components/ambee/test_sensor.py index a198d420378..32bf5938456 100644 --- a/tests/components/ambee/test_sensor.py +++ b/tests/components/ambee/test_sensor.py @@ -280,7 +280,7 @@ async def test_pollen_disabled_by_default( entry = entity_registry.async_get(entity_id) assert entry assert entry.disabled - assert entry.disabled_by == er.DISABLED_INTEGRATION + assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION @pytest.mark.parametrize( diff --git a/tests/components/brother/test_sensor.py b/tests/components/brother/test_sensor.py index a2763c3ed91..d0eb0819303 100644 --- a/tests/components/brother/test_sensor.py +++ b/tests/components/brother/test_sensor.py @@ -273,7 +273,7 @@ async def test_disabled_by_default_sensors(hass): assert entry assert entry.unique_id == "0123456789_uptime" assert entry.disabled - assert entry.disabled_by == er.DISABLED_INTEGRATION + assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION async def test_availability(hass): diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index 17762f20df3..b4065d855ff 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -4,7 +4,7 @@ import pytest from homeassistant.components.config import entity_registry from homeassistant.const import ATTR_ICON from homeassistant.helpers.device_registry import DeviceEntryDisabler -from homeassistant.helpers.entity_registry import DISABLED_USER, RegistryEntry +from homeassistant.helpers.entity_registry import RegistryEntry, RegistryEntryDisabler from tests.common import ( MockConfigEntry, @@ -215,14 +215,16 @@ async def test_update_entity(hass, client): "id": 7, "type": "config/entity_registry/update", "entity_id": "test_domain.world", - "disabled_by": DISABLED_USER, + "disabled_by": RegistryEntryDisabler.USER, } ) msg = await client.receive_json() assert hass.states.get("test_domain.world") is None - assert registry.entities["test_domain.world"].disabled_by == DISABLED_USER + assert ( + registry.entities["test_domain.world"].disabled_by is RegistryEntryDisabler.USER + ) # UPDATE DISABLED_BY TO NONE await client.send_json( diff --git a/tests/components/efergy/test_sensor.py b/tests/components/efergy/test_sensor.py index 4f6c7f53209..a4b0d4ab238 100644 --- a/tests/components/efergy/test_sensor.py +++ b/tests/components/efergy/test_sensor.py @@ -89,7 +89,7 @@ async def test_sensor_readings( assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "EUR" assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING entity = ent_reg.async_get("sensor.power_usage_728386") - assert entity.disabled_by == er.DISABLED_INTEGRATION + assert entity.disabled_by is er.RegistryEntryDisabler.INTEGRATION ent_reg.async_update_entity(entity.entity_id, **{"disabled_by": None}) await hass.config_entries.async_reload(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/forecast_solar/test_sensor.py b/tests/components/forecast_solar/test_sensor.py index a6fb45158f8..7698a230751 100644 --- a/tests/components/forecast_solar/test_sensor.py +++ b/tests/components/forecast_solar/test_sensor.py @@ -166,7 +166,7 @@ async def test_disabled_by_default( entry = entity_registry.async_get(entity_id) assert entry assert entry.disabled - assert entry.disabled_by == er.DISABLED_INTEGRATION + assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION @pytest.mark.parametrize( diff --git a/tests/components/fronius/__init__.py b/tests/components/fronius/__init__.py index 7930f6c01f4..4222a2037aa 100644 --- a/tests/components/fronius/__init__.py +++ b/tests/components/fronius/__init__.py @@ -90,7 +90,9 @@ async def enable_all_entities(hass, config_entry_id, time_till_next_update): registry = er.async_get(hass) entities = er.async_entries_for_config_entry(registry, config_entry_id) for entry in [ - entry for entry in entities if entry.disabled_by == er.DISABLED_INTEGRATION + entry + for entry in entities + if entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION ]: registry.async_update_entity(entry.entity_id, **{"disabled_by": None}) await hass.async_block_till_done() diff --git a/tests/components/greeneye_monitor/test_sensor.py b/tests/components/greeneye_monitor/test_sensor.py index 63ab8b64423..401e9e93048 100644 --- a/tests/components/greeneye_monitor/test_sensor.py +++ b/tests/components/greeneye_monitor/test_sensor.py @@ -7,7 +7,10 @@ from homeassistant.components.greeneye_monitor.sensor import ( ) from homeassistant.const import STATE_UNKNOWN from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_registry import async_get as get_entity_registry +from homeassistant.helpers.entity_registry import ( + RegistryEntryDisabler, + async_get as get_entity_registry, +) from .common import ( SINGLE_MONITOR_CONFIG_POWER_SENSORS, @@ -161,5 +164,7 @@ def connect_monitor(monitors: AsyncMock, serial_number: int) -> MagicMock: async def disable_entity(hass: HomeAssistant, entity_id: str) -> None: """Disable the given entity.""" entity_registry = get_entity_registry(hass) - entity_registry.async_update_entity(entity_id, disabled_by="user") + entity_registry.async_update_entity( + entity_id, disabled_by=RegistryEntryDisabler.USER + ) await hass.async_block_till_done() diff --git a/tests/components/hue/test_light_v2.py b/tests/components/hue/test_light_v2.py index 70a5af6d98e..95e3324e81a 100644 --- a/tests/components/hue/test_light_v2.py +++ b/tests/components/hue/test_light_v2.py @@ -243,7 +243,7 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data): assert entity_entry assert entity_entry.disabled - assert entity_entry.disabled_by == er.DISABLED_INTEGRATION + assert entity_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION # entity should not have a device assigned assert entity_entry.device_id is None diff --git a/tests/components/hue/test_sensor_v2.py b/tests/components/hue/test_sensor_v2.py index 256c323ccce..2b060308b71 100644 --- a/tests/components/hue/test_sensor_v2.py +++ b/tests/components/hue/test_sensor_v2.py @@ -56,7 +56,7 @@ async def test_sensors(hass, mock_bridge_v2, v2_resources_test_data): assert entity_entry assert entity_entry.disabled - assert entity_entry.disabled_by == er.DISABLED_INTEGRATION + assert entity_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION async def test_enable_sensor( @@ -76,7 +76,7 @@ async def test_enable_sensor( assert entity_entry assert entity_entry.disabled - assert entity_entry.disabled_by == er.DISABLED_INTEGRATION + assert entity_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION # enable the entity updated_entry = ent_reg.async_update_entity( diff --git a/tests/components/hyperion/test_light.py b/tests/components/hyperion/test_light.py index 5d57d3be70d..f91cc312ca6 100644 --- a/tests/components/hyperion/test_light.py +++ b/tests/components/hyperion/test_light.py @@ -1345,7 +1345,7 @@ async def test_lights_can_be_enabled(hass: HomeAssistant) -> None: entry = entity_registry.async_get(TEST_PRIORITY_LIGHT_ENTITY_ID_1) assert entry assert entry.disabled - assert entry.disabled_by == er.DISABLED_INTEGRATION + assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1) assert not entity_state diff --git a/tests/components/hyperion/test_switch.py b/tests/components/hyperion/test_switch.py index e88db516eff..7ec441716ce 100644 --- a/tests/components/hyperion/test_switch.py +++ b/tests/components/hyperion/test_switch.py @@ -199,7 +199,7 @@ async def test_switches_can_be_enabled(hass: HomeAssistant) -> None: entry = entity_registry.async_get(entity_id) assert entry assert entry.disabled - assert entry.disabled_by == er.DISABLED_INTEGRATION + assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION entity_state = hass.states.get(entity_id) assert not entity_state diff --git a/tests/components/ipp/test_sensor.py b/tests/components/ipp/test_sensor.py index 405d7309b23..5531259c597 100644 --- a/tests/components/ipp/test_sensor.py +++ b/tests/components/ipp/test_sensor.py @@ -95,7 +95,7 @@ async def test_disabled_by_default_sensors( entry = registry.async_get("sensor.epson_xp_6000_series_uptime") assert entry assert entry.disabled - assert entry.disabled_by == er.DISABLED_INTEGRATION + assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION async def test_missing_entry_unique_id( diff --git a/tests/components/litejet/test_scene.py b/tests/components/litejet/test_scene.py index 077793279d8..9784d96d2ef 100644 --- a/tests/components/litejet/test_scene.py +++ b/tests/components/litejet/test_scene.py @@ -23,7 +23,7 @@ async def test_disabled_by_default(hass, mock_litejet): entry = registry.async_get(ENTITY_SCENE) assert entry assert entry.disabled - assert entry.disabled_by == er.DISABLED_INTEGRATION + assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION async def test_activate(hass, mock_litejet): diff --git a/tests/components/met/test_weather.py b/tests/components/met/test_weather.py index 8ffa4076b8a..3025356d8fb 100644 --- a/tests/components/met/test_weather.py +++ b/tests/components/met/test_weather.py @@ -22,7 +22,7 @@ async def test_tracking_home(hass, mock_weather): entry = registry.async_get("weather.test_home_hourly") assert entry assert entry.disabled - assert entry.disabled_by == er.DISABLED_INTEGRATION + assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION # Test we track config await hass.config.async_update(latitude=10, longitude=20) diff --git a/tests/components/monoprice/test_media_player.py b/tests/components/monoprice/test_media_player.py index 977d57cb07c..0a11d665395 100644 --- a/tests/components/monoprice/test_media_player.py +++ b/tests/components/monoprice/test_media_player.py @@ -518,7 +518,7 @@ async def test_first_run_with_failing_zones(hass): entry = registry.async_get(ZONE_7_ID) assert entry.disabled - assert entry.disabled_by == er.DISABLED_INTEGRATION + assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION async def test_not_first_run_with_failing_zone(hass): diff --git a/tests/components/motioneye/test_sensor.py b/tests/components/motioneye/test_sensor.py index 474b8690308..5ab6fc46f49 100644 --- a/tests/components/motioneye/test_sensor.py +++ b/tests/components/motioneye/test_sensor.py @@ -108,7 +108,7 @@ async def test_sensor_actions_can_be_enabled(hass: HomeAssistant) -> None: entry = entity_registry.async_get(TEST_SENSOR_ACTION_ENTITY_ID) assert entry assert entry.disabled - assert entry.disabled_by == er.DISABLED_INTEGRATION + assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION entity_state = hass.states.get(TEST_SENSOR_ACTION_ENTITY_ID) assert not entity_state diff --git a/tests/components/motioneye/test_switch.py b/tests/components/motioneye/test_switch.py index 05de2f0bbcf..09db967e5e3 100644 --- a/tests/components/motioneye/test_switch.py +++ b/tests/components/motioneye/test_switch.py @@ -160,7 +160,7 @@ async def test_disabled_switches_can_be_enabled(hass: HomeAssistant) -> None: entry = entity_registry.async_get(entity_id) assert entry assert entry.disabled - assert entry.disabled_by == er.DISABLED_INTEGRATION + assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION entity_state = hass.states.get(entity_id) assert not entity_state diff --git a/tests/components/nam/test_sensor.py b/tests/components/nam/test_sensor.py index aa05930f727..995d825454c 100644 --- a/tests/components/nam/test_sensor.py +++ b/tests/components/nam/test_sensor.py @@ -351,7 +351,7 @@ async def test_sensor_disabled(hass): assert entry assert entry.unique_id == "aa:bb:cc:dd:ee:ff-signal" assert entry.disabled - assert entry.disabled_by == er.DISABLED_INTEGRATION + assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION # Test enabling entity updated_entry = registry.async_update_entity( diff --git a/tests/components/onewire/__init__.py b/tests/components/onewire/__init__.py index 3ae6dbab050..0ab8b8f6917 100644 --- a/tests/components/onewire/__init__.py +++ b/tests/components/onewire/__init__.py @@ -19,7 +19,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceRegistry -from homeassistant.helpers.entity_registry import EntityRegistry +from homeassistant.helpers.entity_registry import EntityRegistry, RegistryEntryDisabler from .const import ( ATTR_DEFAULT_DISABLED, @@ -42,7 +42,7 @@ def check_and_enable_disabled_entities( entity_id = expected_entity[ATTR_ENTITY_ID] registry_entry = entity_registry.entities.get(entity_id) assert registry_entry.disabled - assert registry_entry.disabled_by == "integration" + assert registry_entry.disabled_by is RegistryEntryDisabler.INTEGRATION entity_registry.async_update_entity(entity_id, **{"disabled_by": None}) diff --git a/tests/components/ozw/test_binary_sensor.py b/tests/components/ozw/test_binary_sensor.py index e6af71d41b4..6053cc43457 100644 --- a/tests/components/ozw/test_binary_sensor.py +++ b/tests/components/ozw/test_binary_sensor.py @@ -22,7 +22,7 @@ async def test_binary_sensor(hass, generic_data, binary_sensor_msg): entry = registry.async_get(entity_id) assert entry assert entry.disabled - assert entry.disabled_by == er.DISABLED_INTEGRATION + assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION # Test enabling legacy entity updated_entry = registry.async_update_entity( diff --git a/tests/components/ozw/test_sensor.py b/tests/components/ozw/test_sensor.py index e043d5eb58d..01bdb7b51a2 100644 --- a/tests/components/ozw/test_sensor.py +++ b/tests/components/ozw/test_sensor.py @@ -43,7 +43,7 @@ async def test_sensor(hass, generic_data): entry = registry.async_get(entity_id) assert entry assert entry.disabled - assert entry.disabled_by == er.DISABLED_INTEGRATION + assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION # Test enabling entity updated_entry = registry.async_update_entity( diff --git a/tests/components/p1_monitor/test_sensor.py b/tests/components/p1_monitor/test_sensor.py index 61f3f027b6b..9cb01109c66 100644 --- a/tests/components/p1_monitor/test_sensor.py +++ b/tests/components/p1_monitor/test_sensor.py @@ -203,4 +203,4 @@ async def test_smartmeter_disabled_by_default( entry = entity_registry.async_get(entity_id) assert entry assert entry.disabled - assert entry.disabled_by == er.DISABLED_INTEGRATION + assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION diff --git a/tests/components/plex/test_sensor.py b/tests/components/plex/test_sensor.py index c07693cf073..d99ce483b04 100644 --- a/tests/components/plex/test_sensor.py +++ b/tests/components/plex/test_sensor.py @@ -179,7 +179,8 @@ async def test_library_sensor_values( # Test movie library sensor entity_registry.async_update_entity( - entity_id="sensor.plex_server_1_library_tv_shows", disabled_by="user" + entity_id="sensor.plex_server_1_library_tv_shows", + disabled_by=er.RegistryEntryDisabler.USER, ) entity_registry.async_update_entity( entity_id="sensor.plex_server_1_library_movies", disabled_by=None @@ -214,7 +215,8 @@ async def test_library_sensor_values( # Test music library sensor entity_registry.async_update_entity( - entity_id="sensor.plex_server_1_library_movies", disabled_by="user" + entity_id="sensor.plex_server_1_library_movies", + disabled_by=er.RegistryEntryDisabler.USER, ) entity_registry.async_update_entity( entity_id="sensor.plex_server_1_library_music", disabled_by=None diff --git a/tests/components/renault/test_sensor.py b/tests/components/renault/test_sensor.py index 2e584326e89..c04bf8c0280 100644 --- a/tests/components/renault/test_sensor.py +++ b/tests/components/renault/test_sensor.py @@ -7,7 +7,7 @@ import pytest from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_registry import EntityRegistry +from homeassistant.helpers.entity_registry import EntityRegistry, RegistryEntryDisabler from . import ( check_device_registry, @@ -38,7 +38,7 @@ def _check_and_enable_disabled_entities( entity_id = expected_entity[ATTR_ENTITY_ID] registry_entry = entity_registry.entities.get(entity_id) assert registry_entry.disabled - assert registry_entry.disabled_by == "integration" + assert registry_entry.disabled_by is RegistryEntryDisabler.INTEGRATION entity_registry.async_update_entity(entity_id, **{"disabled_by": None}) diff --git a/tests/components/sonarr/test_sensor.py b/tests/components/sonarr/test_sensor.py index 96d350ac6df..f68920e4e4f 100644 --- a/tests/components/sonarr/test_sensor.py +++ b/tests/components/sonarr/test_sensor.py @@ -116,7 +116,7 @@ async def test_disabled_by_default_sensors( entry = registry.async_get(entity_id) assert entry assert entry.disabled - assert entry.disabled_by == er.DISABLED_INTEGRATION + assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION async def test_availability( diff --git a/tests/components/switcher_kis/test_sensor.py b/tests/components/switcher_kis/test_sensor.py index b6fc1f2a49e..6fe3088625d 100644 --- a/tests/components/switcher_kis/test_sensor.py +++ b/tests/components/switcher_kis/test_sensor.py @@ -61,7 +61,7 @@ async def test_sensor_disabled(hass, mock_bridge): assert entry assert entry.unique_id == unique_id assert entry.disabled is True - assert entry.disabled_by == er.DISABLED_INTEGRATION + assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION # Test enabling entity updated_entry = registry.async_update_entity( diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py index 0cd18c89435..f3c4622c03d 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -788,12 +788,12 @@ async def test_indexed_sensor_attributes(hass, mqtt_mock, setup_tasmota): @pytest.mark.parametrize( "sensor_name, disabled, disabled_by", [ - ("tasmota_firmware_version", True, er.DISABLED_INTEGRATION), - ("tasmota_ip", True, er.DISABLED_INTEGRATION), + ("tasmota_firmware_version", True, er.RegistryEntryDisabler.INTEGRATION), + ("tasmota_ip", True, er.RegistryEntryDisabler.INTEGRATION), ("tasmota_last_restart_time", False, None), ("tasmota_mqtt_connect_count", False, None), - ("tasmota_rssi", True, er.DISABLED_INTEGRATION), - ("tasmota_signal", True, er.DISABLED_INTEGRATION), + ("tasmota_rssi", True, er.RegistryEntryDisabler.INTEGRATION), + ("tasmota_signal", True, er.RegistryEntryDisabler.INTEGRATION), ("tasmota_ssid", False, None), ("tasmota_wifi_connect_count", False, None), ], @@ -819,7 +819,7 @@ async def test_diagnostic_sensors( assert bool(state) != disabled entry = entity_reg.async_get(f"sensor.{sensor_name}") assert entry.disabled == disabled - assert entry.disabled_by == disabled_by + assert entry.disabled_by is disabled_by assert entry.entity_category == "diagnostic" @@ -843,7 +843,7 @@ async def test_enable_status_sensor(hass, mqtt_mock, setup_tasmota): assert state is None entry = entity_reg.async_get("sensor.tasmota_signal") assert entry.disabled - assert entry.disabled_by == er.DISABLED_INTEGRATION + assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION # Enable the signal level status sensor updated_entry = entity_reg.async_update_entity( diff --git a/tests/components/wled/test_sensor.py b/tests/components/wled/test_sensor.py index bc401f574a6..9e43f573f7e 100644 --- a/tests/components/wled/test_sensor.py +++ b/tests/components/wled/test_sensor.py @@ -196,7 +196,7 @@ async def test_disabled_by_default_sensors( entry = registry.async_get(entity_id) assert entry assert entry.disabled - assert entry.disabled_by == er.DISABLED_INTEGRATION + assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION @pytest.mark.parametrize( diff --git a/tests/components/zwave_js/test_binary_sensor.py b/tests/components/zwave_js/test_binary_sensor.py index d5aab6cd0f9..64c27f276e7 100644 --- a/tests/components/zwave_js/test_binary_sensor.py +++ b/tests/components/zwave_js/test_binary_sensor.py @@ -89,7 +89,7 @@ async def test_disabled_legacy_sensor(hass, multisensor_6, integration): entry = registry.async_get(entity_id) assert entry assert entry.disabled - assert entry.disabled_by == er.DISABLED_INTEGRATION + assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION # Test enabling legacy entity updated_entry = registry.async_update_entity( diff --git a/tests/components/zwave_js/test_number.py b/tests/components/zwave_js/test_number.py index 6d9458d096c..e987bfbebc6 100644 --- a/tests/components/zwave_js/test_number.py +++ b/tests/components/zwave_js/test_number.py @@ -174,4 +174,4 @@ async def test_disabled_basic_number(hass, ge_in_wall_dimmer_switch, integration assert entity_entry assert entity_entry.disabled - assert entity_entry.disabled_by == er.DISABLED_INTEGRATION + assert entity_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index de290e14760..74b7d331834 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -122,7 +122,7 @@ async def test_disabled_notification_sensor(hass, multisensor_6, integration): assert entity_entry assert entity_entry.disabled - assert entity_entry.disabled_by == er.DISABLED_INTEGRATION + assert entity_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION # Test enabling entity updated_entry = ent_reg.async_update_entity( @@ -149,7 +149,7 @@ async def test_disabled_indcator_sensor( assert entity_entry assert entity_entry.disabled - assert entity_entry.disabled_by == er.DISABLED_INTEGRATION + assert entity_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION async def test_config_parameter_sensor(hass, lock_id_lock_as_id150, integration): diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index d4051613c03..1d28c50b9a1 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -580,7 +580,7 @@ async def test_warn_disabled(hass, caplog): entity_id="hello.world", unique_id="test-unique-id", platform="test-platform", - disabled_by=entity_registry.DISABLED_USER, + disabled_by=entity_registry.RegistryEntryDisabler.USER, ) mock_registry(hass, {"hello.world": entry}) @@ -622,7 +622,7 @@ async def test_disabled_in_entity_registry(hass): assert hass.states.get("hello.world") is not None entry2 = registry.async_update_entity( - "hello.world", disabled_by=entity_registry.DISABLED_USER + "hello.world", disabled_by=entity_registry.RegistryEntryDisabler.USER ) await hass.async_block_till_done() assert entry2 != entry diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index baaea35b62c..7626bedc13d 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -519,7 +519,7 @@ async def test_registry_respect_entity_disabled(hass): unique_id="1234", # Using component.async_add_entities is equal to platform "domain" platform="test_platform", - disabled_by=er.DISABLED_USER, + disabled_by=er.RegistryEntryDisabler.USER, ) }, ) @@ -1077,7 +1077,7 @@ async def test_entity_disabled_by_integration(hass): entry_default = registry.async_get_or_create(DOMAIN, DOMAIN, "default") assert entry_default.disabled_by is None entry_disabled = registry.async_get_or_create(DOMAIN, DOMAIN, "disabled") - assert entry_disabled.disabled_by == er.DISABLED_INTEGRATION + assert entry_disabled.disabled_by is er.RegistryEntryDisabler.INTEGRATION async def test_entity_disabled_by_device(hass: HomeAssistant): @@ -1115,7 +1115,7 @@ async def test_entity_disabled_by_device(hass: HomeAssistant): registry = er.async_get(hass) entry_disabled = registry.async_get_or_create(DOMAIN, DOMAIN, "disabled") - assert entry_disabled.disabled_by == er.DISABLED_DEVICE + assert entry_disabled.disabled_by is er.RegistryEntryDisabler.DEVICE async def test_entity_info_added_to_entity_registry(hass): diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 50c2ea364c5..568d82ebd4b 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -76,7 +76,7 @@ def test_get_or_create_updates_data(registry): capabilities={"max": 100}, config_entry=orig_config_entry, device_id="mock-dev-id", - disabled_by=er.DISABLED_HASS, + disabled_by=er.RegistryEntryDisabler.HASS, entity_category="config", original_device_class="mock-device-class", original_icon="initial-original_icon", @@ -94,7 +94,7 @@ def test_get_or_create_updates_data(registry): config_entry_id=orig_config_entry.entry_id, device_class=None, device_id="mock-dev-id", - disabled_by=er.DISABLED_HASS, + disabled_by=er.RegistryEntryDisabler.HASS, entity_category="config", icon=None, id=orig_entry.id, @@ -116,7 +116,7 @@ def test_get_or_create_updates_data(registry): capabilities={"new-max": 100}, config_entry=new_config_entry, device_id="new-mock-dev-id", - disabled_by=er.DISABLED_USER, + disabled_by=er.RegistryEntryDisabler.USER, entity_category=None, original_device_class="new-mock-device-class", original_icon="updated-original_icon", @@ -134,7 +134,7 @@ def test_get_or_create_updates_data(registry): config_entry_id=new_config_entry.entry_id, device_class=None, device_id="new-mock-dev-id", - disabled_by=er.DISABLED_HASS, # Should not be updated + disabled_by=er.RegistryEntryDisabler.HASS, # Should not be updated entity_category="config", icon=None, id=orig_entry.id, @@ -188,7 +188,7 @@ async def test_loading_saving_data(hass, registry): capabilities={"max": 100}, config_entry=mock_config, device_id="mock-dev-id", - disabled_by=er.DISABLED_HASS, + disabled_by=er.RegistryEntryDisabler.HASS, entity_category="config", original_device_class="mock-device-class", original_icon="hass:original-icon", @@ -223,7 +223,7 @@ async def test_loading_saving_data(hass, registry): assert new_entry2.config_entry_id == mock_config.entry_id assert new_entry2.device_class == "user-class" assert new_entry2.device_id == "mock-dev-id" - assert new_entry2.disabled_by == er.DISABLED_HASS + assert new_entry2.disabled_by is er.RegistryEntryDisabler.HASS assert new_entry2.entity_category == "config" assert new_entry2.icon == "hass:user-icon" assert new_entry2.name == "User Name" @@ -277,19 +277,19 @@ async def test_loading_extra_values(hass, hass_storage): "entity_id": "test.disabled_user", "platform": "super_platform", "unique_id": "disabled-user", - "disabled_by": er.DISABLED_USER, + "disabled_by": er.RegistryEntryDisabler.USER, }, { "entity_id": "test.disabled_hass", "platform": "super_platform", "unique_id": "disabled-hass", - "disabled_by": er.DISABLED_HASS, + "disabled_by": er.RegistryEntryDisabler.HASS, }, { "entity_id": "test.invalid__entity", "platform": "super_platform", "unique_id": "invalid-hass", - "disabled_by": er.DISABLED_HASS, + "disabled_by": er.RegistryEntryDisabler.HASS, }, ] }, @@ -317,9 +317,9 @@ async def test_loading_extra_values(hass, hass_storage): "test", "super_platform", "disabled-user" ) assert entry_disabled_hass.disabled - assert entry_disabled_hass.disabled_by == er.DISABLED_HASS + assert entry_disabled_hass.disabled_by is er.RegistryEntryDisabler.HASS assert entry_disabled_user.disabled - assert entry_disabled_user.disabled_by == er.DISABLED_USER + assert entry_disabled_user.disabled_by is er.RegistryEntryDisabler.USER def test_async_get_entity_id(registry): @@ -399,7 +399,7 @@ async def test_migration_yaml_to_json(hass): "unique_id": "test-unique", "platform": "test-platform", "name": "Test Name", - "disabled_by": er.DISABLED_HASS, + "disabled_by": er.RegistryEntryDisabler.HASS, } } with patch("os.path.isfile", return_value=True), patch("os.remove"), patch( @@ -416,7 +416,7 @@ async def test_migration_yaml_to_json(hass): config_entry=mock_config, ) assert entry.name == "Test Name" - assert entry.disabled_by == er.DISABLED_HASS + assert entry.disabled_by is er.RegistryEntryDisabler.HASS assert entry.config_entry_id == "test-config-id" @@ -554,7 +554,7 @@ async def test_update_entity(registry): for attr_name, new_value in ( ("name", "new name"), ("icon", "new icon"), - ("disabled_by", er.DISABLED_USER), + ("disabled_by", er.RegistryEntryDisabler.USER), ): changes = {attr_name: new_value} updated_entry = registry.async_update_entity(entry.entity_id, **changes) @@ -573,14 +573,14 @@ async def test_update_entity(registry): async def test_disabled_by(registry): """Test that we can disable an entry when we create it.""" entry = registry.async_get_or_create( - "light", "hue", "5678", disabled_by=er.DISABLED_HASS + "light", "hue", "5678", disabled_by=er.RegistryEntryDisabler.HASS ) - assert entry.disabled_by == er.DISABLED_HASS + assert entry.disabled_by is er.RegistryEntryDisabler.HASS entry = registry.async_get_or_create( - "light", "hue", "5678", disabled_by=er.DISABLED_INTEGRATION + "light", "hue", "5678", disabled_by=er.RegistryEntryDisabler.INTEGRATION ) - assert entry.disabled_by == er.DISABLED_HASS + assert entry.disabled_by is er.RegistryEntryDisabler.HASS entry2 = registry.async_get_or_create("light", "hue", "1234") assert entry2.disabled_by is None @@ -596,16 +596,16 @@ async def test_disabled_by_config_entry_pref(registry): entry = registry.async_get_or_create( "light", "hue", "AAAA", config_entry=mock_config ) - assert entry.disabled_by == er.DISABLED_INTEGRATION + assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION entry2 = registry.async_get_or_create( "light", "hue", "BBBB", config_entry=mock_config, - disabled_by=er.DISABLED_USER, + disabled_by=er.RegistryEntryDisabler.USER, ) - assert entry2.disabled_by == er.DISABLED_USER + assert entry2.disabled_by is er.RegistryEntryDisabler.USER async def test_restore_states(hass): @@ -626,7 +626,7 @@ async def test_restore_states(hass): "hue", "5678", suggested_object_id="disabled", - disabled_by=er.DISABLED_HASS, + disabled_by=er.RegistryEntryDisabler.HASS, ) registry.async_get_or_create( "light", @@ -836,7 +836,7 @@ async def test_disable_device_disables_entities(hass, registry): "ABCD", config_entry=config_entry, device_id=device_entry.id, - disabled_by=er.DISABLED_USER, + disabled_by=er.RegistryEntryDisabler.USER, ) entry3 = registry.async_get_or_create( "light", @@ -844,7 +844,7 @@ async def test_disable_device_disables_entities(hass, registry): "EFGH", config_entry=config_entry, device_id=device_entry.id, - disabled_by=er.DISABLED_CONFIG_ENTRY, + disabled_by=er.RegistryEntryDisabler.CONFIG_ENTRY, ) assert not entry1.disabled @@ -858,13 +858,13 @@ async def test_disable_device_disables_entities(hass, registry): entry1 = registry.async_get(entry1.entity_id) assert entry1.disabled - assert entry1.disabled_by == er.DISABLED_DEVICE + assert entry1.disabled_by is er.RegistryEntryDisabler.DEVICE entry2 = registry.async_get(entry2.entity_id) assert entry2.disabled - assert entry2.disabled_by == er.DISABLED_USER + assert entry2.disabled_by is er.RegistryEntryDisabler.USER entry3 = registry.async_get(entry3.entity_id) assert entry3.disabled - assert entry3.disabled_by == er.DISABLED_CONFIG_ENTRY + assert entry3.disabled_by is er.RegistryEntryDisabler.CONFIG_ENTRY device_registry.async_update_device(device_entry.id, disabled_by=None) await hass.async_block_till_done() @@ -873,10 +873,10 @@ async def test_disable_device_disables_entities(hass, registry): assert not entry1.disabled entry2 = registry.async_get(entry2.entity_id) assert entry2.disabled - assert entry2.disabled_by == er.DISABLED_USER + assert entry2.disabled_by is er.RegistryEntryDisabler.USER entry3 = registry.async_get(entry3.entity_id) assert entry3.disabled - assert entry3.disabled_by == er.DISABLED_CONFIG_ENTRY + assert entry3.disabled_by is er.RegistryEntryDisabler.CONFIG_ENTRY async def test_disable_config_entry_disables_entities(hass, registry): @@ -903,7 +903,7 @@ async def test_disable_config_entry_disables_entities(hass, registry): "ABCD", config_entry=config_entry, device_id=device_entry.id, - disabled_by=er.DISABLED_USER, + disabled_by=er.RegistryEntryDisabler.USER, ) entry3 = registry.async_get_or_create( "light", @@ -911,7 +911,7 @@ async def test_disable_config_entry_disables_entities(hass, registry): "EFGH", config_entry=config_entry, device_id=device_entry.id, - disabled_by=er.DISABLED_DEVICE, + disabled_by=er.RegistryEntryDisabler.DEVICE, ) assert not entry1.disabled @@ -925,13 +925,13 @@ async def test_disable_config_entry_disables_entities(hass, registry): entry1 = registry.async_get(entry1.entity_id) assert entry1.disabled - assert entry1.disabled_by == er.DISABLED_CONFIG_ENTRY + assert entry1.disabled_by is er.RegistryEntryDisabler.CONFIG_ENTRY entry2 = registry.async_get(entry2.entity_id) assert entry2.disabled - assert entry2.disabled_by == er.DISABLED_USER + assert entry2.disabled_by is er.RegistryEntryDisabler.USER entry3 = registry.async_get(entry3.entity_id) assert entry3.disabled - assert entry3.disabled_by == er.DISABLED_DEVICE + assert entry3.disabled_by is er.RegistryEntryDisabler.DEVICE await hass.config_entries.async_set_disabled_by(config_entry.entry_id, None) await hass.async_block_till_done() @@ -940,7 +940,7 @@ async def test_disable_config_entry_disables_entities(hass, registry): assert not entry1.disabled entry2 = registry.async_get(entry2.entity_id) assert entry2.disabled - assert entry2.disabled_by == er.DISABLED_USER + assert entry2.disabled_by is er.RegistryEntryDisabler.USER # The device was re-enabled, so entity disabled by the device will be re-enabled too entry3 = registry.async_get(entry3.entity_id) assert not entry3.disabled_by @@ -970,7 +970,7 @@ async def test_disabled_entities_excluded_from_entity_list(hass, registry): "ABCD", config_entry=config_entry, device_id=device_entry.id, - disabled_by=er.DISABLED_USER, + disabled_by=er.RegistryEntryDisabler.USER, ) entries = er.async_entries_for_device(registry, device_entry.id) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 80f2f086377..abc5472200d 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1435,7 +1435,9 @@ async def test_reload_entry_entity_registry_ignores_no_entry(hass): # Test we ignore entities without config entry entry = registry.async_get_or_create("light", "hue", "123") - registry.async_update_entity(entry.entity_id, disabled_by=er.DISABLED_USER) + registry.async_update_entity( + entry.entity_id, disabled_by=er.RegistryEntryDisabler.USER + ) await hass.async_block_till_done() assert not handler.changed assert handler._remove_call_later is None @@ -1474,7 +1476,9 @@ async def test_reload_entry_entity_registry_works(hass): assert handler._remove_call_later is None # Disable entity, we should not do anything, only act when enabled. - registry.async_update_entity(entity_entry.entity_id, disabled_by=er.DISABLED_USER) + registry.async_update_entity( + entity_entry.entity_id, disabled_by=er.RegistryEntryDisabler.USER + ) await hass.async_block_till_done() assert not handler.changed assert handler._remove_call_later is None From 7a5177b7e20585ec23925a3fd91e3e655980ff72 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 15 Dec 2021 18:43:26 -0500 Subject: [PATCH 0482/2644] Clean up dirt from recollect_waste yaml (#61964) --- homeassistant/components/recollect_waste/sensor.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py index b7291df86ba..0eb2f584fcc 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -7,7 +7,6 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_FRIENDLY_NAME, DEVICE_CLASS_DATE from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -23,8 +22,6 @@ ATTR_NEXT_PICKUP_DATE = "next_pickup_date" DEFAULT_NAME = "Waste Pickup" -PLATFORM_SCHEMA = cv.deprecated(DOMAIN) - @callback def async_get_pickup_type_names( From 62f411fcccc9b5d84bd27a400eaf20f9779046b4 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 16 Dec 2021 00:53:20 +0100 Subject: [PATCH 0483/2644] Bump aiohue to 3.0.6 (#61974) --- homeassistant/components/hue/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index f32d8edc284..7003a3a8ccf 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==3.0.5"], + "requirements": ["aiohue==3.0.6"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 954d31cad56..b658b627b08 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -192,7 +192,7 @@ aiohomekit==0.6.4 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.5 +aiohue==3.0.6 # homeassistant.components.imap aioimaplib==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 00565651956..728ceb1a185 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -137,7 +137,7 @@ aiohomekit==0.6.4 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.5 +aiohue==3.0.6 # homeassistant.components.apache_kafka aiokafka==0.6.0 From b0f5e7dabfb2a3db8087c8102b15b4e3eeea6448 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 15 Dec 2021 19:07:12 -0500 Subject: [PATCH 0484/2644] Use Enums in zoneminder (#61975) --- homeassistant/components/zoneminder/binary_sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zoneminder/binary_sensor.py b/homeassistant/components/zoneminder/binary_sensor.py index 73d6877ef2d..0bcd9031188 100644 --- a/homeassistant/components/zoneminder/binary_sensor.py +++ b/homeassistant/components/zoneminder/binary_sensor.py @@ -1,6 +1,6 @@ """Support for ZoneMinder binary sensors.""" from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_CONNECTIVITY, + BinarySensorDeviceClass, BinarySensorEntity, ) @@ -38,7 +38,7 @@ class ZMAvailabilitySensor(BinarySensorEntity): @property def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" - return DEVICE_CLASS_CONNECTIVITY + return BinarySensorDeviceClass.CONNECTIVITY def update(self): """Update the state of this sensor (availability of ZoneMinder).""" From b22a9e4d0aa5370aaa432091c0e80d7c3653091a Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 16 Dec 2021 00:13:36 +0000 Subject: [PATCH 0485/2644] [ci skip] Translation update --- .../components/adax/translations/cs.json | 14 ++++++++++- .../components/adax/translations/nl.json | 4 ++-- .../components/elmax/translations/cs.json | 23 +++++++++++++++++-- .../components/knx/translations/ca.json | 2 ++ .../components/knx/translations/de.json | 2 ++ .../components/knx/translations/en.json | 7 +++--- .../components/knx/translations/et.json | 2 ++ .../components/knx/translations/hu.json | 2 ++ .../components/knx/translations/ja.json | 2 ++ .../components/knx/translations/pl.json | 2 ++ .../components/knx/translations/zh-Hant.json | 2 ++ .../components/lcn/translations/cs.json | 10 ++++++++ .../simplisafe/translations/cs.json | 1 + .../srp_energy/translations/nl.json | 2 +- .../components/twinkly/translations/cs.json | 3 +++ .../components/twinkly/translations/id.json | 3 +++ .../components/twinkly/translations/ja.json | 3 +++ .../components/twinkly/translations/nl.json | 3 +++ .../components/twinkly/translations/no.json | 3 +++ .../wolflink/translations/sensor.nl.json | 2 +- .../translations/select.cs.json | 12 ++++++++++ 21 files changed, 94 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/lcn/translations/cs.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/select.cs.json diff --git a/homeassistant/components/adax/translations/cs.json b/homeassistant/components/adax/translations/cs.json index 4dc53056df0..07eeaa34f63 100644 --- a/homeassistant/components/adax/translations/cs.json +++ b/homeassistant/components/adax/translations/cs.json @@ -2,6 +2,8 @@ "config": { "abort": { "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "heater_not_available": "Oh\u0159\u00edva\u010d nen\u00ed k dispozici. Zkuste resetovat oh\u0159\u00edva\u010d stisknut\u00edm tla\u010d\u00edtek + a OK na n\u011bkolik sekund.", + "heater_not_found": "Oh\u0159\u00edva\u010d nenalezen. Zkuste p\u0159em\u00edstit oh\u0159\u00edva\u010d bl\u00ed\u017ee k po\u010d\u00edta\u010di Home Assistant.", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" }, "error": { @@ -11,15 +13,25 @@ "step": { "cloud": { "data": { + "account_id": "ID \u00fa\u010dtu", "password": "Heslo" } }, + "local": { + "data": { + "wifi_pswd": "Heslo Wi-Fi", + "wifi_ssid": "Wi-Fi SSID" + }, + "description": "Resetujte oh\u0159\u00edva\u010d stisknut\u00edm + a OK, dokud se nezobraz\u00ed \"Reset\". Pot\u00e9 stiskn\u011bte a podr\u017ete tla\u010d\u00edtko OK na oh\u0159\u00edva\u010di, dokud modr\u00e1 led dioda neza\u010dne blikat, ne\u017e stisknete tla\u010d\u00edtko Odeslat. Konfigurace oh\u0159\u00edva\u010de m\u016f\u017ee trvat n\u011bkolik minut." + }, "user": { "data": { "account_id": "ID \u00fa\u010dtu", + "connection_type": "Vyberte typ p\u0159ipojen\u00ed", "host": "Hostitel", "password": "Heslo" - } + }, + "description": "Vyberte typ p\u0159ipojen\u00ed. Lok\u00e1ln\u00ed vy\u017eaduje oh\u0159\u00edva\u010de s bluetooth" } } } diff --git a/homeassistant/components/adax/translations/nl.json b/homeassistant/components/adax/translations/nl.json index 9622d41bf65..026c3c06dae 100644 --- a/homeassistant/components/adax/translations/nl.json +++ b/homeassistant/components/adax/translations/nl.json @@ -19,8 +19,8 @@ }, "local": { "data": { - "wifi_pswd": "Wifi wachtwoord", - "wifi_ssid": "Wifi ssid" + "wifi_pswd": "Wi-Fi Wachtwoord", + "wifi_ssid": "Wi-Fi SSID" }, "description": "Reset de kachel door op + en OK te drukken totdat het display 'Reset' toont. Houd vervolgens de OK-knop op de verwarming ingedrukt totdat de blauwe led begint te knipperen voordat u op Verzenden drukt. Het configureren van de verwarming kan enkele minuten duren." }, diff --git a/homeassistant/components/elmax/translations/cs.json b/homeassistant/components/elmax/translations/cs.json index 338ff5d9a44..1fa9b6a9196 100644 --- a/homeassistant/components/elmax/translations/cs.json +++ b/homeassistant/components/elmax/translations/cs.json @@ -1,12 +1,31 @@ { "config": { + "error": { + "bad_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "invalid_pin": "Poskytnut\u00fd k\u00f3d PIN je neplatn\u00fd", + "network_error": "Do\u0161lo k chyb\u011b s\u00edt\u011b", + "no_panel_online": "Nebyl nalezen \u017e\u00e1dn\u00fd online ovl\u00e1dac\u00ed panel Elmax.", + "unknown_error": "Vyskytla se neo\u010dek\u00e1van\u00e1 chyba" + }, "step": { + "panels": { + "data": { + "panel_id": "ID panelu", + "panel_name": "N\u00e1zev panelu", + "panel_pin": "PIN k\u00f3d" + }, + "description": "Vyberte, kter\u00fd panel chcete touto integrac\u00ed ovl\u00e1dat. Vezm\u011bte pros\u00edm na v\u011bdom\u00ed, \u017ee panel mus\u00ed b\u00fdt zapnut\u00fd, aby mohl b\u00fdt nakonfigurov\u00e1n.", + "title": "V\u00fdb\u011br panelu" + }, "user": { "data": { "password": "Heslo", "username": "U\u017eivatelsk\u00e9 jm\u00e9no" - } + }, + "description": "P\u0159ihlaste se do cloudu Elmax pomoc\u00ed sv\u00fdch p\u0159ihla\u0161ovac\u00edch \u00fadaj\u016f", + "title": "P\u0159ihl\u00e1\u0161en\u00ed k \u00fa\u010dtu" } } - } + }, + "title": "Nastaven\u00ed Elmax Cloud" } \ No newline at end of file diff --git a/homeassistant/components/knx/translations/ca.json b/homeassistant/components/knx/translations/ca.json index 031d4652373..fcfb7ed553e 100644 --- a/homeassistant/components/knx/translations/ca.json +++ b/homeassistant/components/knx/translations/ca.json @@ -21,6 +21,7 @@ "routing": { "data": { "individual_address": "Adre\u00e7a individual de la connexi\u00f3 d'encaminament", + "local_ip": "IP local (deixa-ho en blanc si no n'est\u00e0s segur/a)", "multicast_group": "Grup de multidifusi\u00f3 utilitzat per a l'encaminament", "multicast_port": "Port de multidifusi\u00f3 utilitzat per a l'encaminament" }, @@ -46,6 +47,7 @@ "data": { "connection_type": "Tipus de connexi\u00f3 KNX", "individual_address": "Adre\u00e7a individual predeterminada", + "local_ip": "IP local (deixa-ho en blanc si no n'est\u00e0s segur/a)", "multicast_group": "Grup de multidifusi\u00f3 utilitzat per a encaminament i descobriment", "multicast_port": "Port de multidifusi\u00f3 utilitzat per a encaminament i descobriment", "rate_limit": "Telegrames de sortida m\u00e0xims per segon", diff --git a/homeassistant/components/knx/translations/de.json b/homeassistant/components/knx/translations/de.json index 41afdfb14ac..d512c4f9455 100644 --- a/homeassistant/components/knx/translations/de.json +++ b/homeassistant/components/knx/translations/de.json @@ -21,6 +21,7 @@ "routing": { "data": { "individual_address": "Individuelle Adresse f\u00fcr die Routingverbindung", + "local_ip": "Lokale IP (leer lassen, wenn unsicher)", "multicast_group": "Die f\u00fcr das Routing verwendete Multicast-Gruppe", "multicast_port": "Der f\u00fcr das Routing verwendete Multicast-Port" }, @@ -46,6 +47,7 @@ "data": { "connection_type": "KNX-Verbindungstyp", "individual_address": "Individuelle Standardadresse", + "local_ip": "Lokale IP (leer lassen, wenn unsicher)", "multicast_group": "Multicast-Gruppe f\u00fcr Routing und Discovery verwenden", "multicast_port": "Multicast-Port f\u00fcr Routing und Discovery verwenden", "rate_limit": "Maximal ausgehende Telegrams pro Sekunde", diff --git a/homeassistant/components/knx/translations/en.json b/homeassistant/components/knx/translations/en.json index 91b9dfce5f3..8f812a7baf1 100644 --- a/homeassistant/components/knx/translations/en.json +++ b/homeassistant/components/knx/translations/en.json @@ -21,9 +21,9 @@ "routing": { "data": { "individual_address": "Individual address for the routing connection", + "local_ip": "Local IP (leave empty if unsure)", "multicast_group": "The multicast group used for routing", - "multicast_port": "The multicast port used for routing", - "local_ip": "Local IP (leave empty if unsure)" + "multicast_port": "The multicast port used for routing" }, "description": "Please configure the routing options." }, @@ -47,9 +47,9 @@ "data": { "connection_type": "KNX Connection Type", "individual_address": "Default individual address", + "local_ip": "Local IP (leave empty if unsure)", "multicast_group": "Multicast group used for routing and discovery", "multicast_port": "Multicast port used for routing and discovery", - "local_ip": "Local IP (leave empty if unsure)", "rate_limit": "Maximum outgoing telegrams per second", "state_updater": "Globally enable reading states from the KNX Bus" } @@ -57,6 +57,7 @@ "tunnel": { "data": { "host": "Host", + "local_ip": "Local IP (leave empty if unsure)", "port": "Port", "route_back": "Route Back / NAT Mode" } diff --git a/homeassistant/components/knx/translations/et.json b/homeassistant/components/knx/translations/et.json index a8c65b53209..d725a4414c6 100644 --- a/homeassistant/components/knx/translations/et.json +++ b/homeassistant/components/knx/translations/et.json @@ -21,6 +21,7 @@ "routing": { "data": { "individual_address": "Marsruutimis\u00fchenduse individuaalne aadress", + "local_ip": "Kohalik IP (j\u00e4ta t\u00fchjaks kui ei ole kindel)", "multicast_group": "Marsruutimiseks kasutatav multisaater\u00fchm", "multicast_port": "Marsruutimiseks kasutatav multisaateport" }, @@ -46,6 +47,7 @@ "data": { "connection_type": "KNX \u00fchenduse t\u00fc\u00fcp", "individual_address": "Vaikimisi individuaalne aadress", + "local_ip": "Kohalik IP (j\u00e4ta t\u00fchjaks kui ei ole kindel)", "multicast_group": "Marsruutimiseks ja avastamiseks kasutatav multisaategrupp", "multicast_port": "Marsruutimiseks ja avastamiseks kasutatav multisaateport", "rate_limit": "Maksimaalne v\u00e4ljaminevate teavituste arv sekundis", diff --git a/homeassistant/components/knx/translations/hu.json b/homeassistant/components/knx/translations/hu.json index 592c9a50a1c..acc102240a3 100644 --- a/homeassistant/components/knx/translations/hu.json +++ b/homeassistant/components/knx/translations/hu.json @@ -21,6 +21,7 @@ "routing": { "data": { "individual_address": "Az \u00fatv\u00e1laszt\u00e1si (routing) kapcsolat egy\u00e9ni c\u00edme", + "local_ip": "Helyi IP c\u00edm (hagyja \u00fcresen, ha nem biztos benne)", "multicast_group": "Az \u00fatv\u00e1laszt\u00e1shoz haszn\u00e1lt multicast csoport", "multicast_port": "Az \u00fatv\u00e1laszt\u00e1shoz haszn\u00e1lt multicast portsz\u00e1m" }, @@ -46,6 +47,7 @@ "data": { "connection_type": "KNX csatlakoz\u00e1s t\u00edpusa", "individual_address": "Alap\u00e9rtelmezett egy\u00e9ni c\u00edm", + "local_ip": "Helyi IP c\u00edm (hagyja \u00fcresen, ha nem biztos benne)", "multicast_group": "\u00datv\u00e1laszt\u00e1shoz \u00e9s felder\u00edt\u00e9shez haszn\u00e1lt multicast csoport", "multicast_port": "\u00datv\u00e1laszt\u00e1shoz \u00e9s felder\u00edt\u00e9shez haszn\u00e1lt multicast portsz\u00e1m\n", "rate_limit": "Maxim\u00e1lis kimen\u0151 \u00fczenet darabsz\u00e1m m\u00e1sodpercenk\u00e9nt", diff --git a/homeassistant/components/knx/translations/ja.json b/homeassistant/components/knx/translations/ja.json index 515d338cde7..83a11ef82fe 100644 --- a/homeassistant/components/knx/translations/ja.json +++ b/homeassistant/components/knx/translations/ja.json @@ -21,6 +21,7 @@ "routing": { "data": { "individual_address": "\u30eb\u30fc\u30c6\u30a3\u30f3\u30b0\u63a5\u7d9a\u306e\u500b\u5225\u306e\u30a2\u30c9\u30ec\u30b9", + "local_ip": "\u30ed\u30fc\u30ab\u30ebIP(\u4e0d\u660e\u306a\u5834\u5408\u306f\u7a7a\u306e\u307e\u307e\u306b\u3057\u3066\u304f\u3060\u3055\u3044)", "multicast_group": "\u30eb\u30fc\u30c6\u30a3\u30f3\u30b0\u306b\u4f7f\u7528\u3059\u308b\u30de\u30eb\u30c1\u30ad\u30e3\u30b9\u30c8\u30b0\u30eb\u30fc\u30d7", "multicast_port": "\u30eb\u30fc\u30c6\u30a3\u30f3\u30b0\u306b\u4f7f\u7528\u3059\u308b\u30de\u30eb\u30c1\u30ad\u30e3\u30b9\u30c8\u30dd\u30fc\u30c8" }, @@ -46,6 +47,7 @@ "data": { "connection_type": "KNX\u63a5\u7d9a\u30bf\u30a4\u30d7", "individual_address": "\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u500b\u5225\u30a2\u30c9\u30ec\u30b9", + "local_ip": "\u30ed\u30fc\u30ab\u30ebIP(\u4e0d\u660e\u306a\u5834\u5408\u306f\u7a7a\u306e\u307e\u307e\u306b\u3057\u3066\u304f\u3060\u3055\u3044)", "multicast_group": "\u30eb\u30fc\u30c6\u30a3\u30f3\u30b0\u3068\u691c\u51fa(discovery)\u306b\u4f7f\u7528\u3055\u308c\u308b\u30de\u30eb\u30c1\u30ad\u30e3\u30b9\u30c8\u30b0\u30eb\u30fc\u30d7", "multicast_port": "\u30eb\u30fc\u30c6\u30a3\u30f3\u30b0\u3068\u691c\u51fa(discovery)\u306b\u4f7f\u7528\u3055\u308c\u308b\u30de\u30eb\u30c1\u30ad\u30e3\u30b9\u30c8\u30dd\u30fc\u30c8", "rate_limit": "1 \u79d2\u3042\u305f\u308a\u306e\u6700\u5927\u9001\u4fe1\u96fb\u5831(telegrams )\u6570", diff --git a/homeassistant/components/knx/translations/pl.json b/homeassistant/components/knx/translations/pl.json index 43599c9e4e3..17be078037e 100644 --- a/homeassistant/components/knx/translations/pl.json +++ b/homeassistant/components/knx/translations/pl.json @@ -21,6 +21,7 @@ "routing": { "data": { "individual_address": "Indywidualny adres dla po\u0142\u0105czenia routingowego", + "local_ip": "Lokalny adres IP (pozostaw pusty, je\u015bli nie masz pewno\u015bci)", "multicast_group": "Grupa multicast u\u017cyta do routingu", "multicast_port": "Port multicast u\u017cyty do routingu" }, @@ -46,6 +47,7 @@ "data": { "connection_type": "Typ po\u0142\u0105czenia KNX", "individual_address": "Domy\u015blny adres indywidualny", + "local_ip": "Lokalny adres IP (pozostaw pusty, je\u015bli nie masz pewno\u015bci)", "multicast_group": "Grupa multicast u\u017cywana do routingu i wykrywania", "multicast_port": "Port multicast u\u017cywany do routingu i wykrywania", "rate_limit": "Maksymalna liczba wychodz\u0105cych wiadomo\u015bci na sekund\u0119", diff --git a/homeassistant/components/knx/translations/zh-Hant.json b/homeassistant/components/knx/translations/zh-Hant.json index 4cf9a43a4c9..d0d2d63320d 100644 --- a/homeassistant/components/knx/translations/zh-Hant.json +++ b/homeassistant/components/knx/translations/zh-Hant.json @@ -21,6 +21,7 @@ "routing": { "data": { "individual_address": "\u8def\u7531\u9023\u7dda\u500b\u5225\u4f4d\u5740", + "local_ip": "\u672c\u5730\u7aef IP\uff08\u5047\u5982\u4e0d\u78ba\u5b9a\uff0c\u4fdd\u7559\u7a7a\u767d\uff09", "multicast_group": "\u4f7f\u7528\u65bc\u8def\u7531\u7684 Multicast \u7fa4\u7d44", "multicast_port": "\u4f7f\u7528\u65bc\u8def\u7531\u7684 Multicast \u901a\u8a0a\u57e0" }, @@ -46,6 +47,7 @@ "data": { "connection_type": "KNX \u9023\u7dda\u985e\u578b", "individual_address": "\u9810\u8a2d\u500b\u5225\u4f4d\u5740", + "local_ip": "\u672c\u5730\u7aef IP\uff08\u5047\u5982\u4e0d\u78ba\u5b9a\uff0c\u4fdd\u7559\u7a7a\u767d\uff09", "multicast_group": "\u4f7f\u7528\u65bc\u8def\u7531\u8207\u63a2\u7d22\u7684 Multicast \u7fa4\u7d44", "multicast_port": "\u4f7f\u7528\u65bc\u8def\u7531\u8207\u63a2\u7d22\u7684 Multicast \u901a\u8a0a\u57e0", "rate_limit": "\u6700\u5927\u6bcf\u79d2\u767c\u51fa Telegram", diff --git a/homeassistant/components/lcn/translations/cs.json b/homeassistant/components/lcn/translations/cs.json new file mode 100644 index 00000000000..5cd9d995fc4 --- /dev/null +++ b/homeassistant/components/lcn/translations/cs.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "trigger_type": { + "fingerprint": "p\u0159ijat k\u00f3d otisku", + "send_keys": "odeslan\u00e9 kl\u00ed\u010de p\u0159ijaty", + "transmitter": "p\u0159ijat\u00fd k\u00f3d vys\u00edla\u010de", + "transponder": "p\u0159ijat\u00fd k\u00f3d transpond\u00e9ru" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/cs.json b/homeassistant/components/simplisafe/translations/cs.json index 7fd81a94391..bbb47121fbc 100644 --- a/homeassistant/components/simplisafe/translations/cs.json +++ b/homeassistant/components/simplisafe/translations/cs.json @@ -23,6 +23,7 @@ }, "user": { "data": { + "auth_code": "Autoriza\u010dn\u00ed k\u00f3d", "code": "K\u00f3d (pou\u017eit\u00fd v u\u017eivatelsk\u00e9m rozhran\u00ed Home Assistant)", "password": "Heslo", "username": "E-mail" diff --git a/homeassistant/components/srp_energy/translations/nl.json b/homeassistant/components/srp_energy/translations/nl.json index 91bdc3592b6..3cf2298b348 100644 --- a/homeassistant/components/srp_energy/translations/nl.json +++ b/homeassistant/components/srp_energy/translations/nl.json @@ -13,7 +13,7 @@ "user": { "data": { "id": "Account ID", - "is_tou": "Is tijd van gebruik plan", + "is_tou": "Is 'tijd van gebruik' plan", "password": "Wachtwoord", "username": "Gebruikersnaam" } diff --git a/homeassistant/components/twinkly/translations/cs.json b/homeassistant/components/twinkly/translations/cs.json index edb776e1483..da8bacbd4a6 100644 --- a/homeassistant/components/twinkly/translations/cs.json +++ b/homeassistant/components/twinkly/translations/cs.json @@ -7,6 +7,9 @@ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" }, "step": { + "discovery_confirm": { + "description": "Chcete nastavit {name} - {model} ( {host} )?" + }, "user": { "data": { "host": "Hostitel (nebo IP adresa) va\u0161eho za\u0159\u00edzen\u00ed Twinkly" diff --git a/homeassistant/components/twinkly/translations/id.json b/homeassistant/components/twinkly/translations/id.json index b4a5ba6cbfa..7cebe87febb 100644 --- a/homeassistant/components/twinkly/translations/id.json +++ b/homeassistant/components/twinkly/translations/id.json @@ -7,6 +7,9 @@ "cannot_connect": "Gagal terhubung" }, "step": { + "discovery_confirm": { + "description": "Ingin menyiapkan {name} - {model} ({host})?" + }, "user": { "data": { "host": "Host (atau alamat IP) perangkat twinkly Anda" diff --git a/homeassistant/components/twinkly/translations/ja.json b/homeassistant/components/twinkly/translations/ja.json index fcc91e956d1..e3955e4286f 100644 --- a/homeassistant/components/twinkly/translations/ja.json +++ b/homeassistant/components/twinkly/translations/ja.json @@ -7,6 +7,9 @@ "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "step": { + "discovery_confirm": { + "description": "{name} - {model} ({host}) \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, "user": { "data": { "host": "Twinkly device\u306e\u30db\u30b9\u30c8(\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9)" diff --git a/homeassistant/components/twinkly/translations/nl.json b/homeassistant/components/twinkly/translations/nl.json index 97a55150447..4efce28f37f 100644 --- a/homeassistant/components/twinkly/translations/nl.json +++ b/homeassistant/components/twinkly/translations/nl.json @@ -7,6 +7,9 @@ "cannot_connect": "Kan geen verbinding maken" }, "step": { + "discovery_confirm": { + "description": "Wilt u {name} - {model} ( {host} ) instellen?" + }, "user": { "data": { "host": "Hostnaam (of IP-adres van uw Twinkly apparaat" diff --git a/homeassistant/components/twinkly/translations/no.json b/homeassistant/components/twinkly/translations/no.json index 2bfe2d3606a..490f7f76724 100644 --- a/homeassistant/components/twinkly/translations/no.json +++ b/homeassistant/components/twinkly/translations/no.json @@ -7,6 +7,9 @@ "cannot_connect": "Tilkobling mislyktes" }, "step": { + "discovery_confirm": { + "description": "Vil du konfigurere {name} - {model} ( {host} )?" + }, "user": { "data": { "host": "Vert (eller IP-adresse) for din twinkly-enhet" diff --git a/homeassistant/components/wolflink/translations/sensor.nl.json b/homeassistant/components/wolflink/translations/sensor.nl.json index 304a4fd6f27..a528dcf11c2 100644 --- a/homeassistant/components/wolflink/translations/sensor.nl.json +++ b/homeassistant/components/wolflink/translations/sensor.nl.json @@ -65,7 +65,7 @@ "sparen": "Spaarstand", "spreizung_hoch": "dT te breed", "spreizung_kf": "Spreid KF", - "stabilisierung": "Stablisatie", + "stabilisierung": "Stabilisatie", "standby": "Stand-by", "start": "Start", "storung": "Fout", diff --git a/homeassistant/components/yamaha_musiccast/translations/select.cs.json b/homeassistant/components/yamaha_musiccast/translations/select.cs.json new file mode 100644 index 00000000000..c416256dd99 --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/select.cs.json @@ -0,0 +1,12 @@ +{ + "state": { + "yamaha_musiccast__zone_surr_decoder_type": { + "toggle": "P\u0159epnout" + }, + "yamaha_musiccast__zone_tone_control_mode": { + "auto": "Automaticky", + "bypass": "Bypass", + "manual": "Manu\u00e1ln\u00ed" + } + } +} \ No newline at end of file From d68946f568d9769556ddc9fb63177ca62b378d3e Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 15 Dec 2021 19:17:20 -0500 Subject: [PATCH 0486/2644] Use Enums in zamg (#61976) --- homeassistant/components/zamg/sensor.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zamg/sensor.py b/homeassistant/components/zamg/sensor.py index 054646800a9..51963530e51 100644 --- a/homeassistant/components/zamg/sensor.py +++ b/homeassistant/components/zamg/sensor.py @@ -14,7 +14,11 @@ from aiohttp.hdrs import USER_AGENT import requests import voluptuous as vol -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) from homeassistant.const import ( AREA_SQUARE_METERS, ATTR_ATTRIBUTION, @@ -23,7 +27,6 @@ from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_NAME, DEGREE, - DEVICE_CLASS_TEMPERATURE, LENGTH_METERS, PERCENTAGE, PRESSURE_HPA, @@ -124,7 +127,7 @@ SENSOR_TYPES: tuple[ZamgSensorEntityDescription, ...] = ( key="temperature", name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, col_heading=f"T {TEMP_CELSIUS}", dtype=float, ), @@ -139,7 +142,7 @@ SENSOR_TYPES: tuple[ZamgSensorEntityDescription, ...] = ( key="dewpoint", name="Dew Point", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, col_heading=f"TP {TEMP_CELSIUS}", dtype=float, ), From f72b2e71eebec543394f59051beb2062229cad94 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 16 Dec 2021 02:11:46 +0100 Subject: [PATCH 0487/2644] Use new BinarySensorDeviceClass in isy994 (#61825) --- .../components/isy994/binary_sensor.py | 42 +++++++++-------- homeassistant/components/isy994/const.py | 46 +++++++------------ 2 files changed, 38 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/isy994/binary_sensor.py b/homeassistant/components/isy994/binary_sensor.py index 58997eaa579..f0645110b86 100644 --- a/homeassistant/components/isy994/binary_sensor.py +++ b/homeassistant/components/isy994/binary_sensor.py @@ -13,15 +13,8 @@ from pyisy.constants import ( from pyisy.nodes import Group, Node from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_COLD, - DEVICE_CLASS_HEAT, - DEVICE_CLASS_LIGHT, - DEVICE_CLASS_MOISTURE, - DEVICE_CLASS_MOTION, - DEVICE_CLASS_OPENING, - DEVICE_CLASS_PROBLEM, DOMAIN as BINARY_SENSOR, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -52,9 +45,9 @@ from .entity import ISYNodeEntity, ISYProgramEntity from .helpers import migrate_old_unique_ids DEVICE_PARENT_REQUIRED = [ - DEVICE_CLASS_OPENING, - DEVICE_CLASS_MOISTURE, - DEVICE_CLASS_MOTION, + BinarySensorDeviceClass.OPENING, + BinarySensorDeviceClass.MOISTURE, + BinarySensorDeviceClass.MOTION, ] @@ -94,11 +87,15 @@ async def async_setup_entry( # detected after an ISY Restart, so we assume it's off. # As soon as the ISY Event Stream connects if it has a # valid state, it will be set. - device = ISYInsteonBinarySensorEntity(node, DEVICE_CLASS_COLD, False) + device = ISYInsteonBinarySensorEntity( + node, BinarySensorDeviceClass.COLD, False + ) devices.append(device) elif subnode_id == SUBNODE_CLIMATE_HEAT: # Subnode 3 is the "Heat Control" sensor - device = ISYInsteonBinarySensorEntity(node, DEVICE_CLASS_HEAT, False) + device = ISYInsteonBinarySensorEntity( + node, BinarySensorDeviceClass.HEAT, False + ) devices.append(device) continue @@ -113,7 +110,10 @@ async def async_setup_entry( ) continue - if device_class in (DEVICE_CLASS_OPENING, DEVICE_CLASS_MOISTURE): + if device_class in ( + BinarySensorDeviceClass.OPENING, + BinarySensorDeviceClass.MOISTURE, + ): # These sensors use an optional "negative" subnode 2 to # snag all state changes if subnode_id == SUBNODE_NEGATIVE: @@ -126,7 +126,7 @@ async def async_setup_entry( devices.append(device) continue if ( - device_class == DEVICE_CLASS_MOTION + device_class == BinarySensorDeviceClass.MOTION and device_type is not None and any(device_type.startswith(t) for t in TYPE_INSTEON_MOTION) ): @@ -138,13 +138,15 @@ async def async_setup_entry( initial_state = None if parent_device.state is None else False if subnode_id == SUBNODE_DUSK_DAWN: # Subnode 2 is the Dusk/Dawn sensor - device = ISYInsteonBinarySensorEntity(node, DEVICE_CLASS_LIGHT) + device = ISYInsteonBinarySensorEntity( + node, BinarySensorDeviceClass.LIGHT + ) devices.append(device) continue if subnode_id == SUBNODE_LOW_BATTERY: # Subnode 3 is the low battery node device = ISYInsteonBinarySensorEntity( - node, DEVICE_CLASS_BATTERY, initial_state + node, BinarySensorDeviceClass.BATTERY, initial_state ) devices.append(device) continue @@ -152,7 +154,7 @@ async def async_setup_entry( # Tamper Sub-node for MS II. Sometimes reported as "A" sometimes # reported as "10", which translate from Hex to 10 and 16 resp. device = ISYInsteonBinarySensorEntity( - node, DEVICE_CLASS_PROBLEM, initial_state + node, BinarySensorDeviceClass.PROBLEM, initial_state ) devices.append(device) continue @@ -352,7 +354,7 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity): # Do this first so we don't invert None on moisture sensors return None - if self.device_class == DEVICE_CLASS_MOISTURE: + if self.device_class == BinarySensorDeviceClass.MOISTURE: return not self._computed_state return self._computed_state @@ -454,7 +456,7 @@ class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity): @property def device_class(self) -> str: """Get the class of this device.""" - return DEVICE_CLASS_BATTERY + return BinarySensorDeviceClass.BATTERY @property def extra_state_attributes(self): diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index 0a7b02c1dac..470aaa64c64 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -1,21 +1,7 @@ """Constants for the ISY994 Platform.""" import logging -from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_COLD, - DEVICE_CLASS_DOOR, - DEVICE_CLASS_GAS, - DEVICE_CLASS_HEAT, - DEVICE_CLASS_MOISTURE, - DEVICE_CLASS_MOTION, - DEVICE_CLASS_OPENING, - DEVICE_CLASS_PROBLEM, - DEVICE_CLASS_SAFETY, - DEVICE_CLASS_SMOKE, - DEVICE_CLASS_SOUND, - DEVICE_CLASS_VIBRATION, -) +from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.climate.const import ( CURRENT_HVAC_COOL, CURRENT_HVAC_FAN, @@ -651,8 +637,8 @@ HA_HVAC_TO_ISY = { HA_FAN_TO_ISY = {FAN_ON: "on", FAN_AUTO: "auto"} BINARY_SENSOR_DEVICE_TYPES_ISY = { - DEVICE_CLASS_MOISTURE: ["16.8.", "16.13.", "16.14."], - DEVICE_CLASS_OPENING: [ + BinarySensorDeviceClass.MOISTURE: ["16.8.", "16.13.", "16.14."], + BinarySensorDeviceClass.OPENING: [ "16.9.", "16.6.", "16.7.", @@ -661,22 +647,22 @@ BINARY_SENSOR_DEVICE_TYPES_ISY = { "16.20.", "16.21.", ], - DEVICE_CLASS_MOTION: ["16.1.", "16.4.", "16.5.", "16.3.", "16.22."], + BinarySensorDeviceClass.MOTION: ["16.1.", "16.4.", "16.5.", "16.3.", "16.22."], } BINARY_SENSOR_DEVICE_TYPES_ZWAVE = { - DEVICE_CLASS_SAFETY: ["137", "172", "176", "177", "178"], - DEVICE_CLASS_SMOKE: ["138", "156"], - DEVICE_CLASS_PROBLEM: ["148", "149", "157", "158", "164", "174", "175"], - DEVICE_CLASS_GAS: ["150", "151"], - DEVICE_CLASS_SOUND: ["153"], - DEVICE_CLASS_COLD: ["152", "168"], - DEVICE_CLASS_HEAT: ["154", "166", "167"], - DEVICE_CLASS_MOISTURE: ["159", "169"], - DEVICE_CLASS_DOOR: ["160"], - DEVICE_CLASS_BATTERY: ["162"], - DEVICE_CLASS_MOTION: ["155"], - DEVICE_CLASS_VIBRATION: ["173"], + BinarySensorDeviceClass.SAFETY: ["137", "172", "176", "177", "178"], + BinarySensorDeviceClass.SMOKE: ["138", "156"], + BinarySensorDeviceClass.PROBLEM: ["148", "149", "157", "158", "164", "174", "175"], + BinarySensorDeviceClass.GAS: ["150", "151"], + BinarySensorDeviceClass.SOUND: ["153"], + BinarySensorDeviceClass.COLD: ["152", "168"], + BinarySensorDeviceClass.HEAT: ["154", "166", "167"], + BinarySensorDeviceClass.MOISTURE: ["159", "169"], + BinarySensorDeviceClass.DOOR: ["160"], + BinarySensorDeviceClass.BATTERY: ["162"], + BinarySensorDeviceClass.MOTION: ["155"], + BinarySensorDeviceClass.VIBRATION: ["173"], } From c49d59bf54a2d606918aa5fc535f071f13b17cfc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 16 Dec 2021 02:15:54 +0100 Subject: [PATCH 0488/2644] Use new enums in nexia (#61952) --- homeassistant/components/nexia/sensor.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/nexia/sensor.py b/homeassistant/components/nexia/sensor.py index 15479df310b..5ad5025fea3 100644 --- a/homeassistant/components/nexia/sensor.py +++ b/homeassistant/components/nexia/sensor.py @@ -2,14 +2,8 @@ from nexia.const import UNIT_CELSIUS -from homeassistant.components.sensor import SensorEntity -from homeassistant.const import ( - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_TEMPERATURE, - PERCENTAGE, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, -) +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.const import PERCENTAGE, TEMP_CELSIUS, TEMP_FAHRENHEIT from .const import DOMAIN from .coordinator import NexiaDataUpdateCoordinator @@ -86,7 +80,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): thermostat, "get_outdoor_temperature", "Outdoor Temperature", - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, unit, ) ) @@ -98,7 +92,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): thermostat, "get_relative_humidity", "Relative Humidity", - DEVICE_CLASS_HUMIDITY, + SensorDeviceClass.HUMIDITY, PERCENTAGE, percent_conv, ) @@ -119,7 +113,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): zone, "get_temperature", "Temperature", - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, unit, None, ) From 574f0fced3a27ec771c71eb936d77c30d9a0b5d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Thu, 16 Dec 2021 02:24:38 +0100 Subject: [PATCH 0489/2644] Bump pyatv to 0.9.8 (#61959) --- homeassistant/components/apple_tv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/apple_tv/manifest.json b/homeassistant/components/apple_tv/manifest.json index c54e2254fb3..a9a97b1660d 100644 --- a/homeassistant/components/apple_tv/manifest.json +++ b/homeassistant/components/apple_tv/manifest.json @@ -3,7 +3,7 @@ "name": "Apple TV", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/apple_tv", - "requirements": ["pyatv==0.9.7"], + "requirements": ["pyatv==0.9.8"], "zeroconf": [ "_mediaremotetv._tcp.local.", "_touch-able._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index b658b627b08..487a07c1732 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1378,7 +1378,7 @@ pyatmo==6.2.0 pyatome==0.1.1 # homeassistant.components.apple_tv -pyatv==0.9.7 +pyatv==0.9.8 # homeassistant.components.balboa pybalboa==0.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 728ceb1a185..a6bf1e345a9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -844,7 +844,7 @@ pyatag==0.3.5.3 pyatmo==6.2.0 # homeassistant.components.apple_tv -pyatv==0.9.7 +pyatv==0.9.8 # homeassistant.components.balboa pybalboa==0.13 From e1f5b63f1ea6d852ce971cb588055ea8f082972c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Dec 2021 08:24:04 +0100 Subject: [PATCH 0490/2644] Bump actions/upload-artifact from 2.3.0 to 2.3.1 (#62004) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 2 +- .github/workflows/wheels.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3a283a856f6..6e3ed202559 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -712,7 +712,7 @@ jobs: -p no:sugar \ tests/components/${{ matrix.group }} - name: Upload coverage artifact - uses: actions/upload-artifact@v2.3.0 + uses: actions/upload-artifact@v2.3.1 with: name: coverage-${{ matrix.python-version }}-${{ matrix.group }} path: coverage.xml diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index d118d00d9de..bcb2595bf00 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -45,13 +45,13 @@ jobs: ) > .env_file - name: Upload env_file - uses: actions/upload-artifact@v2.3.0 + uses: actions/upload-artifact@v2.3.1 with: name: env_file path: ./.env_file - name: Upload requirements_diff - uses: actions/upload-artifact@v2.3.0 + uses: actions/upload-artifact@v2.3.1 with: name: requirements_diff path: ./requirements_diff.txt From 9084a227fdacc164df5ccd9fbf2c022576c3d6da Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 03:01:20 -0500 Subject: [PATCH 0491/2644] Use enums in wiffi (#61984) --- homeassistant/components/wiffi/sensor.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/wiffi/sensor.py b/homeassistant/components/wiffi/sensor.py index c16ae3c6aca..e65debedc36 100644 --- a/homeassistant/components/wiffi/sensor.py +++ b/homeassistant/components/wiffi/sensor.py @@ -1,13 +1,9 @@ """Sensor platform support for wiffi devices.""" from homeassistant.components.sensor import ( - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, + SensorStateClass, ) from homeassistant.const import DEGREE, PRESSURE_MBAR, TEMP_CELSIUS from homeassistant.core import callback @@ -25,10 +21,10 @@ from .wiffi_strings import ( # map to determine HA device class from wiffi's unit of measurement UOM_TO_DEVICE_CLASS_MAP = { - WIFFI_UOM_TEMP_CELSIUS: DEVICE_CLASS_TEMPERATURE, - WIFFI_UOM_PERCENT: DEVICE_CLASS_HUMIDITY, - WIFFI_UOM_MILLI_BAR: DEVICE_CLASS_PRESSURE, - WIFFI_UOM_LUX: DEVICE_CLASS_ILLUMINANCE, + WIFFI_UOM_TEMP_CELSIUS: SensorDeviceClass.TEMPERATURE, + WIFFI_UOM_PERCENT: SensorDeviceClass.HUMIDITY, + WIFFI_UOM_MILLI_BAR: SensorDeviceClass.PRESSURE, + WIFFI_UOM_LUX: SensorDeviceClass.ILLUMINANCE, } # map to convert wiffi unit of measurements to common HA uom's @@ -74,9 +70,9 @@ class NumberEntity(WiffiEntity, SensorEntity): self._value = metric.value if self._is_measurement_entity(): - self._attr_state_class = STATE_CLASS_MEASUREMENT + self._attr_state_class = SensorStateClass.MEASUREMENT elif self._is_metered_entity(): - self._attr_state_class = STATE_CLASS_TOTAL_INCREASING + self._attr_state_class = SensorStateClass.TOTAL_INCREASING self.reset_expiration_date() From 2e0fc65bf3e3f7491474b2e1de72197049f31b22 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 03:01:46 -0500 Subject: [PATCH 0492/2644] Use enums in wirelesstag (#61985) --- .../components/wirelesstag/sensor.py | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/wirelesstag/sensor.py b/homeassistant/components/wirelesstag/sensor.py index 8038b42bffe..5c7de69cc2a 100644 --- a/homeassistant/components/wirelesstag/sensor.py +++ b/homeassistant/components/wirelesstag/sensor.py @@ -7,16 +7,12 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, - STATE_CLASS_MEASUREMENT, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) -from homeassistant.const import ( - CONF_MONITORED_CONDITIONS, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_TEMPERATURE, -) +from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -34,28 +30,28 @@ SENSOR_LIGHT = "light" SENSOR_TYPES: dict[str, SensorEntityDescription] = { SENSOR_TEMPERATURE: SensorEntityDescription( key=SENSOR_TEMPERATURE, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SENSOR_AMBIENT_TEMPERATURE: SensorEntityDescription( key=SENSOR_AMBIENT_TEMPERATURE, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SENSOR_HUMIDITY: SensorEntityDescription( key=SENSOR_HUMIDITY, - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SENSOR_MOISTURE: SensorEntityDescription( key=SENSOR_MOISTURE, device_class=SENSOR_MOISTURE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SENSOR_LIGHT: SensorEntityDescription( key=SENSOR_LIGHT, - device_class=DEVICE_CLASS_ILLUMINANCE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, ), } From b8dabfe6599389d7034dda1e8bbf8df1ca865726 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 03:02:23 -0500 Subject: [PATCH 0493/2644] Use enums in wemo (#61983) * Use enums in wemo * uno mas --- homeassistant/components/wemo/sensor.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/wemo/sensor.py b/homeassistant/components/wemo/sensor.py index 654ac92df56..49e6b187515 100644 --- a/homeassistant/components/wemo/sensor.py +++ b/homeassistant/components/wemo/sensor.py @@ -2,17 +2,12 @@ import asyncio from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) -from homeassistant.const import ( - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - ENERGY_KILO_WATT_HOUR, - POWER_WATT, -) +from homeassistant.const import ENERGY_KILO_WATT_HOUR, POWER_WATT from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import StateType from homeassistant.util import convert @@ -69,8 +64,8 @@ class InsightCurrentPower(InsightSensor): entity_description = SensorEntityDescription( key="currentpower", name="Current Power", - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_WATT, ) @@ -91,8 +86,8 @@ class InsightTodayEnergy(InsightSensor): entity_description = SensorEntityDescription( key="todaymw", name="Today Energy", - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ) From a41810efcf999c2d0af4bd9aeca5aa749b148b6f Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 03:04:11 -0500 Subject: [PATCH 0494/2644] Use enums in xbee (#61982) --- homeassistant/components/xbee/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/xbee/sensor.py b/homeassistant/components/xbee/sensor.py index 8dae25ad5e1..6993bdbc0b7 100644 --- a/homeassistant/components/xbee/sensor.py +++ b/homeassistant/components/xbee/sensor.py @@ -5,8 +5,8 @@ import logging import voluptuous as vol from xbee_helper.exceptions import ZigBeeException, ZigBeeTxFailure -from homeassistant.components.sensor import SensorEntity -from homeassistant.const import CONF_TYPE, DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.const import CONF_TYPE, TEMP_CELSIUS from . import DOMAIN, PLATFORM_SCHEMA, XBeeAnalogIn, XBeeAnalogInConfig, XBeeConfig @@ -46,7 +46,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class XBeeTemperatureSensor(SensorEntity): """Representation of XBee Pro temperature sensor.""" - _attr_device_class = DEVICE_CLASS_TEMPERATURE + _attr_device_class = SensorDeviceClass.TEMPERATURE _attr_native_unit_of_measurement = TEMP_CELSIUS def __init__(self, config, device): From 98c2c8c2d1b170919d770c024c6eb645553ba4fe Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 03:05:13 -0500 Subject: [PATCH 0495/2644] Use enums in xiaomi_aqara (#61981) --- .../components/xiaomi_aqara/binary_sensor.py | 7 +++-- .../components/xiaomi_aqara/sensor.py | 26 +++++++++---------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/xiaomi_aqara/binary_sensor.py b/homeassistant/components/xiaomi_aqara/binary_sensor.py index 41a99426c67..afbeeece496 100644 --- a/homeassistant/components/xiaomi_aqara/binary_sensor.py +++ b/homeassistant/components/xiaomi_aqara/binary_sensor.py @@ -2,8 +2,7 @@ import logging from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_MOISTURE, - DEVICE_CLASS_OPENING, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.core import callback @@ -303,7 +302,7 @@ class XiaomiDoorSensor(XiaomiBinarySensor): "Door Window Sensor", xiaomi_hub, data_key, - DEVICE_CLASS_OPENING, + BinarySensorDeviceClass.OPENING, config_entry, ) @@ -353,7 +352,7 @@ class XiaomiWaterLeakSensor(XiaomiBinarySensor): "Water Leak Sensor", xiaomi_hub, data_key, - DEVICE_CLASS_MOISTURE, + BinarySensorDeviceClass.MOISTURE, config_entry, ) diff --git a/homeassistant/components/xiaomi_aqara/sensor.py b/homeassistant/components/xiaomi_aqara/sensor.py index c3f5cf4dacc..c21dd3bcf86 100644 --- a/homeassistant/components/xiaomi_aqara/sensor.py +++ b/homeassistant/components/xiaomi_aqara/sensor.py @@ -3,15 +3,13 @@ from __future__ import annotations import logging -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) from homeassistant.const import ( ATTR_BATTERY_LEVEL, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_POWER, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, LIGHT_LUX, PERCENTAGE, POWER_WATT, @@ -28,27 +26,27 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { "temperature": SensorEntityDescription( key="temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), "humidity": SensorEntityDescription( key="humidity", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, ), "illumination": SensorEntityDescription( key="illumination", native_unit_of_measurement="lm", - device_class=DEVICE_CLASS_ILLUMINANCE, + device_class=SensorDeviceClass.ILLUMINANCE, ), "lux": SensorEntityDescription( key="lux", native_unit_of_measurement=LIGHT_LUX, - device_class=DEVICE_CLASS_ILLUMINANCE, + device_class=SensorDeviceClass.ILLUMINANCE, ), "pressure": SensorEntityDescription( key="pressure", native_unit_of_measurement=PRESSURE_HPA, - device_class=DEVICE_CLASS_PRESSURE, + device_class=SensorDeviceClass.PRESSURE, ), "bed_activity": SensorEntityDescription( key="bed_activity", @@ -58,7 +56,7 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { "load_power": SensorEntityDescription( key="load_power", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, ), "final_tilt_angle": SensorEntityDescription( key="final_tilt_angle", @@ -185,7 +183,7 @@ class XiaomiBatterySensor(XiaomiDevice, SensorEntity): """Representation of a XiaomiSensor.""" _attr_native_unit_of_measurement = PERCENTAGE - _attr_device_class = DEVICE_CLASS_BATTERY + _attr_device_class = SensorDeviceClass.BATTERY def parse_data(self, data, raw_data): """Parse data sent by gateway.""" From f691b0a1da1915ccfc02dab4d5dbdec9f9f5146f Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 03:09:28 -0500 Subject: [PATCH 0496/2644] Use enums in withings (#61987) --- homeassistant/components/withings/binary_sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/withings/binary_sensor.py b/homeassistant/components/withings/binary_sensor.py index 25ed695e8ff..b0af5051124 100644 --- a/homeassistant/components/withings/binary_sensor.py +++ b/homeassistant/components/withings/binary_sensor.py @@ -2,8 +2,8 @@ from __future__ import annotations from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_OCCUPANCY, DOMAIN as BINARY_SENSOR_DOMAIN, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -29,7 +29,7 @@ async def async_setup_entry( class WithingsHealthBinarySensor(BaseWithingsSensor, BinarySensorEntity): """Implementation of a Withings sensor.""" - _attr_device_class = DEVICE_CLASS_OCCUPANCY + _attr_device_class = BinarySensorDeviceClass.OCCUPANCY @property def is_on(self) -> bool: From 806366a0c1ced2061d10aa945e4c0dca08796f28 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 03:10:14 -0500 Subject: [PATCH 0497/2644] Use enums in wolflink (#61988) --- homeassistant/components/wolflink/sensor.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/wolflink/sensor.py b/homeassistant/components/wolflink/sensor.py index 975ddbdd068..e9ecf4c3e55 100644 --- a/homeassistant/components/wolflink/sensor.py +++ b/homeassistant/components/wolflink/sensor.py @@ -9,14 +9,8 @@ from wolf_smartset.models import ( Temperature, ) -from homeassistant.components.sensor import SensorEntity -from homeassistant.const import ( - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, - PRESSURE_BAR, - TEMP_CELSIUS, - TIME_HOURS, -) +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.const import PRESSURE_BAR, TEMP_CELSIUS, TIME_HOURS from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import COORDINATOR, DEVICE_ID, DOMAIN, PARAMETERS, STATES @@ -106,7 +100,7 @@ class WolfLinkTemperature(WolfLinkSensor): @property def device_class(self): """Return the device_class.""" - return DEVICE_CLASS_TEMPERATURE + return SensorDeviceClass.TEMPERATURE @property def native_unit_of_measurement(self): @@ -120,7 +114,7 @@ class WolfLinkPressure(WolfLinkSensor): @property def device_class(self): """Return the device_class.""" - return DEVICE_CLASS_PRESSURE + return SensorDeviceClass.PRESSURE @property def native_unit_of_measurement(self): From 173582d4fc899f049dd534d548c7976c40daa664 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 03:10:38 -0500 Subject: [PATCH 0498/2644] Use enums in yandex_transport (#61978) --- homeassistant/components/yandex_transport/sensor.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yandex_transport/sensor.py b/homeassistant/components/yandex_transport/sensor.py index 724fca14725..9140791c2c8 100644 --- a/homeassistant/components/yandex_transport/sensor.py +++ b/homeassistant/components/yandex_transport/sensor.py @@ -6,8 +6,12 @@ import logging from aioymaps import CaptchaError, YandexMapsRequester import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, DEVICE_CLASS_TIMESTAMP +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorDeviceClass, + SensorEntity, +) +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME from homeassistant.helpers.aiohttp_client import async_create_clientsession import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util @@ -140,7 +144,7 @@ class DiscoverYandexTransport(SensorEntity): @property def device_class(self): """Return the device class.""" - return DEVICE_CLASS_TIMESTAMP + return SensorDeviceClass.TIMESTAMP @property def name(self): From 1568ee67c23ad79055b9cec3f5ea7e8e979b8658 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 03:11:22 -0500 Subject: [PATCH 0499/2644] Use enums in velux (#61991) --- homeassistant/components/velux/cover.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/velux/cover.py b/homeassistant/components/velux/cover.py index 3bb228dd425..f4ea7c90708 100644 --- a/homeassistant/components/velux/cover.py +++ b/homeassistant/components/velux/cover.py @@ -5,12 +5,6 @@ from pyvlx.opening_device import Awning, Blind, GarageDoor, Gate, RollerShutter, from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, - DEVICE_CLASS_AWNING, - DEVICE_CLASS_BLIND, - DEVICE_CLASS_GARAGE, - DEVICE_CLASS_GATE, - DEVICE_CLASS_SHUTTER, - DEVICE_CLASS_WINDOW, SUPPORT_CLOSE, SUPPORT_CLOSE_TILT, SUPPORT_OPEN, @@ -19,6 +13,7 @@ from homeassistant.components.cover import ( SUPPORT_SET_TILT_POSITION, SUPPORT_STOP, SUPPORT_STOP_TILT, + CoverDeviceClass, CoverEntity, ) @@ -68,18 +63,18 @@ class VeluxCover(VeluxEntity, CoverEntity): def device_class(self): """Define this cover as either awning, blind, garage, gate, shutter or window.""" if isinstance(self.node, Awning): - return DEVICE_CLASS_AWNING + return CoverDeviceClass.AWNING if isinstance(self.node, Blind): - return DEVICE_CLASS_BLIND + return CoverDeviceClass.BLIND if isinstance(self.node, GarageDoor): - return DEVICE_CLASS_GARAGE + return CoverDeviceClass.GARAGE if isinstance(self.node, Gate): - return DEVICE_CLASS_GATE + return CoverDeviceClass.GATE if isinstance(self.node, RollerShutter): - return DEVICE_CLASS_SHUTTER + return CoverDeviceClass.SHUTTER if isinstance(self.node, Window): - return DEVICE_CLASS_WINDOW - return DEVICE_CLASS_WINDOW + return CoverDeviceClass.WINDOW + return CoverDeviceClass.WINDOW @property def is_closed(self): From 701699350dbd3971ff19af93af0d052d3d8aed75 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 03:11:51 -0500 Subject: [PATCH 0500/2644] Use enums in youless (#61977) --- homeassistant/components/youless/sensor.py | 44 +++++++++++----------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/youless/sensor.py b/homeassistant/components/youless/sensor.py index a1e094b8c70..19e9c635dce 100644 --- a/homeassistant/components/youless/sensor.py +++ b/homeassistant/components/youless/sensor.py @@ -4,17 +4,13 @@ from __future__ import annotations from youless_api.youless_sensor import YoulessSensor from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_DEVICE, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_GAS, - DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR, POWER_WATT, VOLUME_CUBIC_METERS, @@ -43,9 +39,13 @@ async def async_setup_entry( async_add_entities( [ GasSensor(coordinator, device), - PowerMeterSensor(coordinator, device, "low", STATE_CLASS_TOTAL_INCREASING), - PowerMeterSensor(coordinator, device, "high", STATE_CLASS_TOTAL_INCREASING), - PowerMeterSensor(coordinator, device, "total", STATE_CLASS_TOTAL), + PowerMeterSensor( + coordinator, device, "low", SensorStateClass.TOTAL_INCREASING + ), + PowerMeterSensor( + coordinator, device, "high", SensorStateClass.TOTAL_INCREASING + ), + PowerMeterSensor(coordinator, device, "total", SensorStateClass.TOTAL), CurrentPowerSensor(coordinator, device), DeliveryMeterSensor(coordinator, device, "low"), DeliveryMeterSensor(coordinator, device, "high"), @@ -103,8 +103,8 @@ class GasSensor(YoulessBaseSensor): """The Youless gas sensor.""" _attr_native_unit_of_measurement = VOLUME_CUBIC_METERS - _attr_device_class = DEVICE_CLASS_GAS - _attr_state_class = STATE_CLASS_TOTAL_INCREASING + _attr_device_class = SensorDeviceClass.GAS + _attr_state_class = SensorStateClass.TOTAL_INCREASING def __init__(self, coordinator: DataUpdateCoordinator, device: str) -> None: """Instantiate a gas sensor.""" @@ -122,8 +122,8 @@ class CurrentPowerSensor(YoulessBaseSensor): """The current power usage sensor.""" _attr_native_unit_of_measurement = POWER_WATT - _attr_device_class = DEVICE_CLASS_POWER - _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_device_class = SensorDeviceClass.POWER + _attr_state_class = SensorStateClass.MEASUREMENT def __init__(self, coordinator: DataUpdateCoordinator, device: str) -> None: """Instantiate the usage meter.""" @@ -141,8 +141,8 @@ class DeliveryMeterSensor(YoulessBaseSensor): """The Youless delivery meter value sensor.""" _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR - _attr_device_class = DEVICE_CLASS_ENERGY - _attr_state_class = STATE_CLASS_TOTAL_INCREASING + _attr_device_class = SensorDeviceClass.ENERGY + _attr_state_class = SensorStateClass.TOTAL_INCREASING def __init__( self, coordinator: DataUpdateCoordinator, device: str, dev_type: str @@ -167,15 +167,15 @@ class PowerMeterSensor(YoulessBaseSensor): """The Youless low meter value sensor.""" _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR - _attr_device_class = DEVICE_CLASS_ENERGY - _attr_state_class = STATE_CLASS_TOTAL_INCREASING + _attr_device_class = SensorDeviceClass.ENERGY + _attr_state_class = SensorStateClass.TOTAL_INCREASING def __init__( self, coordinator: DataUpdateCoordinator, device: str, dev_type: str, - state_class: str, + state_class: SensorStateClass, ) -> None: """Instantiate a power meter sensor.""" super().__init__( @@ -199,8 +199,8 @@ class ExtraMeterSensor(YoulessBaseSensor): """The Youless extra meter value sensor (s0).""" _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR - _attr_device_class = DEVICE_CLASS_ENERGY - _attr_state_class = STATE_CLASS_TOTAL_INCREASING + _attr_device_class = SensorDeviceClass.ENERGY + _attr_state_class = SensorStateClass.TOTAL_INCREASING def __init__( self, coordinator: DataUpdateCoordinator, device: str, dev_type: str @@ -225,8 +225,8 @@ class ExtraMeterPowerSensor(YoulessBaseSensor): """The Youless extra meter power value sensor (s0).""" _attr_native_unit_of_measurement = POWER_WATT - _attr_device_class = DEVICE_CLASS_POWER - _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_device_class = SensorDeviceClass.POWER + _attr_state_class = SensorStateClass.MEASUREMENT def __init__( self, coordinator: DataUpdateCoordinator, device: str, dev_type: str From 868e5db47a8ded89e3657fccc1726f453b763748 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 03:12:57 -0500 Subject: [PATCH 0501/2644] Use enums in vallox (#61992) --- homeassistant/components/vallox/sensor.py | 37 +++++++++++------------ 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/vallox/sensor.py b/homeassistant/components/vallox/sensor.py index 6eee46be737..69d6f7bf003 100644 --- a/homeassistant/components/vallox/sensor.py +++ b/homeassistant/components/vallox/sensor.py @@ -5,16 +5,13 @@ from dataclasses import dataclass from datetime import datetime, timedelta from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, - DEVICE_CLASS_CO2, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_TIMESTAMP, PERCENTAGE, TEMP_CELSIUS, ) @@ -144,7 +141,7 @@ SENSORS: tuple[ValloxSensorEntityDescription, ...] = ( name="Fan Speed", metric_key="A_CYC_FAN_SPEED", icon="mdi:fan", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, sensor_type=ValloxFanSpeedSensor, ), @@ -152,7 +149,7 @@ SENSORS: tuple[ValloxSensorEntityDescription, ...] = ( key="remaining_time_for_filter", name="Remaining Time For Filter", metric_key="A_CYC_REMAINING_TIME_FOR_FILTER", - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, sensor_type=ValloxFilterRemainingSensor, ), ValloxSensorEntityDescription( @@ -166,40 +163,40 @@ SENSORS: tuple[ValloxSensorEntityDescription, ...] = ( key="extract_air", name="Extract Air", metric_key="A_CYC_TEMP_EXTRACT_AIR", - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, ), ValloxSensorEntityDescription( key="exhaust_air", name="Exhaust Air", metric_key="A_CYC_TEMP_EXHAUST_AIR", - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, ), ValloxSensorEntityDescription( key="outdoor_air", name="Outdoor Air", metric_key="A_CYC_TEMP_OUTDOOR_AIR", - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, ), ValloxSensorEntityDescription( key="supply_air", name="Supply Air", metric_key="A_CYC_TEMP_SUPPLY_AIR", - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, ), ValloxSensorEntityDescription( key="humidity", name="Humidity", metric_key="A_CYC_RH_VALUE", - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, ), ValloxSensorEntityDescription( @@ -207,15 +204,15 @@ SENSORS: tuple[ValloxSensorEntityDescription, ...] = ( name="Efficiency", metric_key="A_CYC_EXTRACT_EFFICIENCY", icon="mdi:gauge", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, ), ValloxSensorEntityDescription( key="co2", name="CO2", metric_key="A_CYC_CO2_VALUE", - device_class=DEVICE_CLASS_CO2, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CO2, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, ), ) From b4c9d1844e546a571b8878c9748cd7d1adaa0d53 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 03:20:52 -0500 Subject: [PATCH 0502/2644] Use enums in webostv (#62000) --- homeassistant/components/webostv/media_player.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index 60768b6857f..5c79942f279 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -9,7 +9,10 @@ from aiopylgtv import PyLGTVCmdException, PyLGTVPairException, WebOsClient from websockets.exceptions import ConnectionClosed from homeassistant import util -from homeassistant.components.media_player import DEVICE_CLASS_TV, MediaPlayerEntity +from homeassistant.components.media_player import ( + MediaPlayerDeviceClass, + MediaPlayerEntity, +) from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, @@ -239,7 +242,7 @@ class LgWebOSMediaPlayerEntity(MediaPlayerEntity): @property def device_class(self): """Return the device class of the device.""" - return DEVICE_CLASS_TV + return MediaPlayerDeviceClass.TV @property def state(self): From 77647722026835f1a2719d1a66b7e430d2cdc4b5 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 03:21:28 -0500 Subject: [PATCH 0503/2644] Use enums in waterfurnace (#61999) --- .../components/waterfurnace/sensor.py | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/waterfurnace/sensor.py b/homeassistant/components/waterfurnace/sensor.py index 5d7832ca58d..1e543d15c60 100644 --- a/homeassistant/components/waterfurnace/sensor.py +++ b/homeassistant/components/waterfurnace/sensor.py @@ -1,12 +1,11 @@ """Support for Waterfurnace.""" -from homeassistant.components.sensor import ENTITY_ID_FORMAT, SensorEntity -from homeassistant.const import ( - DEVICE_CLASS_TEMPERATURE, - PERCENTAGE, - POWER_WATT, - TEMP_FAHRENHEIT, +from homeassistant.components.sensor import ( + ENTITY_ID_FORMAT, + SensorDeviceClass, + SensorEntity, ) +from homeassistant.const import PERCENTAGE, POWER_WATT, TEMP_FAHRENHEIT from homeassistant.core import callback from homeassistant.util import slugify @@ -40,13 +39,21 @@ SENSORS = [ "tstatactivesetpoint", None, TEMP_FAHRENHEIT, - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, ), WFSensorConfig( - "Leaving Air", "leavingairtemp", None, TEMP_FAHRENHEIT, DEVICE_CLASS_TEMPERATURE + "Leaving Air", + "leavingairtemp", + None, + TEMP_FAHRENHEIT, + SensorDeviceClass.TEMPERATURE, ), WFSensorConfig( - "Room Temp", "tstatroomtemp", None, TEMP_FAHRENHEIT, DEVICE_CLASS_TEMPERATURE + "Room Temp", + "tstatroomtemp", + None, + TEMP_FAHRENHEIT, + SensorDeviceClass.TEMPERATURE, ), WFSensorConfig("Loop Temp", "enteringwatertemp", None, TEMP_FAHRENHEIT), WFSensorConfig( From 9d66dd35ccf2459e5d0b840c1d0a8f1d304144b9 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 03:21:58 -0500 Subject: [PATCH 0504/2644] Use enums in waqi (#61998) --- homeassistant/components/waqi/sensor.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/waqi/sensor.py b/homeassistant/components/waqi/sensor.py index 98ee8db4e89..331e689f14a 100644 --- a/homeassistant/components/waqi/sensor.py +++ b/homeassistant/components/waqi/sensor.py @@ -7,13 +7,16 @@ import aiohttp import voluptuous as vol from waqiasync import WaqiClient -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorStateClass, +) from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_TEMPERATURE, ATTR_TIME, CONF_TOKEN, - DEVICE_CLASS_AQI, ) from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -102,8 +105,8 @@ class WaqiSensor(SensorEntity): _attr_icon = ATTR_ICON _attr_native_unit_of_measurement = ATTR_UNIT - _attr_device_class = DEVICE_CLASS_AQI - _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_device_class = SensorDeviceClass.AQI + _attr_state_class = SensorStateClass.MEASUREMENT def __init__(self, client, station): """Initialize the sensor.""" From 06c1949d2f73f1528e4d0e2077e6a13abcf22a04 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 03:23:54 -0500 Subject: [PATCH 0505/2644] Use enums in vilfo (#61995) --- homeassistant/components/vilfo/const.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vilfo/const.py b/homeassistant/components/vilfo/const.py index 36ecab0ca48..5ed9bc3efdd 100644 --- a/homeassistant/components/vilfo/const.py +++ b/homeassistant/components/vilfo/const.py @@ -3,8 +3,8 @@ from __future__ import annotations from dataclasses import dataclass -from homeassistant.components.sensor import SensorEntityDescription -from homeassistant.const import DEVICE_CLASS_TIMESTAMP, PERCENTAGE +from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription +from homeassistant.const import PERCENTAGE DOMAIN = "vilfo" @@ -44,6 +44,6 @@ SENSOR_TYPES: tuple[VilfoSensorEntityDescription, ...] = ( name="Boot time", icon="mdi:timer-outline", api_key=ATTR_API_DATA_FIELD_BOOT_TIME, - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, ), ) From 048102e0531b16424d5c9abb780046bc99c9d07c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 16 Dec 2021 02:25:18 -0600 Subject: [PATCH 0506/2644] Prevent apple_tv rediscovery from secondary identifiers (#61973) --- .../components/apple_tv/config_flow.py | 49 +++++++++++++------ tests/components/apple_tv/test_config_flow.py | 45 ++++++++++++++++- 2 files changed, 77 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/apple_tv/config_flow.py b/homeassistant/components/apple_tv/config_flow.py index 545d9c5fd90..4b630fc777b 100644 --- a/homeassistant/components/apple_tv/config_flow.py +++ b/homeassistant/components/apple_tv/config_flow.py @@ -1,4 +1,6 @@ """Config flow for Apple TV integration.""" +from __future__ import annotations + import asyncio from collections import deque from ipaddress import ip_address @@ -98,12 +100,19 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): re-used, otherwise the newly discovered identifier is used instead. """ all_identifiers = set(self.atv.all_identifiers) + if unique_id := self._entry_unique_id_from_identifers(all_identifiers): + return unique_id + return self.atv.identifier + + @callback + def _entry_unique_id_from_identifers(self, all_identifiers: set[str]) -> str | None: + """Search existing entries for an identifier and return the unique id.""" for entry in self._async_current_entries(): if all_identifiers.intersection( entry.data.get(CONF_IDENTIFIERS, [entry.unique_id]) ): return entry.unique_id - return self.atv.identifier + return None async def async_step_reauth(self, user_input=None): """Handle initial step when updating invalid credentials.""" @@ -166,6 +175,20 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if unique_id is None: return self.async_abort(reason="unknown") + if existing_unique_id := self._entry_unique_id_from_identifers({unique_id}): + await self.async_set_unique_id(existing_unique_id) + self._abort_if_unique_id_configured(updates={CONF_ADDRESS: host}) + + self._async_abort_entries_match({CONF_ADDRESS: host}) + await self._async_aggregate_discoveries(host, unique_id) + # Scan for the device in order to extract _all_ unique identifiers assigned to + # it. Not doing it like this will yield multiple config flows for the same + # device, one per protocol, which is undesired. + self.scan_filter = host + return await self.async_find_device_wrapper(self.async_found_zeroconf_device) + + async def _async_aggregate_discoveries(self, host: str, unique_id: str) -> None: + """Wait for multiple zeroconf services to be discovered an aggregate them.""" # # Suppose we have a device with three services: A, B and C. Let's assume # service A is discovered by Zeroconf, triggering a device scan that also finds @@ -195,23 +218,18 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # apple_tv device has multiple services that are discovered by # zeroconf. # + self._async_check_and_update_in_progress(host, unique_id) await asyncio.sleep(DISCOVERY_AGGREGATION_TIME) - - self._async_check_in_progress_and_set_address(host, unique_id) - - # Scan for the device in order to extract _all_ unique identifiers assigned to - # it. Not doing it like this will yield multiple config flows for the same - # device, one per protocol, which is undesired. - self.scan_filter = host - return await self.async_find_device_wrapper(self.async_found_zeroconf_device) + # Check again after sleeping in case another flow + # has made progress while we yielded to the event loop + self._async_check_and_update_in_progress(host, unique_id) + # Host must only be set AFTER checking and updating in progress + # flows or we will have a race condition where no flows move forward. + self.context[CONF_ADDRESS] = host @callback - def _async_check_in_progress_and_set_address(self, host: str, unique_id: str): - """Check for in-progress flows and update them with identifiers if needed. - - This code must not await between checking in progress and setting the host - or it will have a race condition where no flows move forward. - """ + def _async_check_and_update_in_progress(self, host: str, unique_id: str) -> None: + """Check for in-progress flows and update them with identifiers if needed.""" for flow in self._async_in_progress(include_uninitialized=True): context = flow["context"] if ( @@ -226,7 +244,6 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # Add potentially new identifiers from this device to the existing flow context["all_identifiers"].append(unique_id) raise data_entry_flow.AbortFlow("already_in_progress") - self.context[CONF_ADDRESS] = host async def async_found_zeroconf_device(self, user_input=None): """Handle device found after Zeroconf discovery.""" diff --git a/tests/components/apple_tv/test_config_flow.py b/tests/components/apple_tv/test_config_flow.py index e9f352041cb..b4811e57739 100644 --- a/tests/components/apple_tv/test_config_flow.py +++ b/tests/components/apple_tv/test_config_flow.py @@ -10,7 +10,11 @@ import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components import zeroconf from homeassistant.components.apple_tv import CONF_ADDRESS, config_flow -from homeassistant.components.apple_tv.const import CONF_START_OFF, DOMAIN +from homeassistant.components.apple_tv.const import ( + CONF_IDENTIFIERS, + CONF_START_OFF, + DOMAIN, +) from .common import airplay_service, create_conf, mrp_service, raop_service @@ -652,6 +656,45 @@ async def test_zeroconf_ip_change(hass, mock_scan): assert unrelated_entry.data[CONF_ADDRESS] == "127.0.0.2" +async def test_zeroconf_ip_change_via_secondary_identifier(hass, mock_scan): + """Test that the config entry gets updated when the ip changes and reloads. + + Instead of checking only the unique id, all the identifiers + in the config entry are checked + """ + entry = MockConfigEntry( + domain="apple_tv", + unique_id="aa:bb:cc:dd:ee:ff", + data={CONF_IDENTIFIERS: ["mrpid"], CONF_ADDRESS: "127.0.0.2"}, + ) + unrelated_entry = MockConfigEntry( + domain="apple_tv", unique_id="unrelated", data={CONF_ADDRESS: "127.0.0.2"} + ) + unrelated_entry.add_to_hass(hass) + entry.add_to_hass(hass) + mock_scan.result = [ + create_conf( + IPv4Address("127.0.0.1"), "Device", mrp_service(), airplay_service() + ) + ] + + with patch( + "homeassistant.components.apple_tv.async_setup_entry", return_value=True + ) as mock_async_setup: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=DMAP_SERVICE, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert len(mock_async_setup.mock_calls) == 2 + assert entry.data[CONF_ADDRESS] == "127.0.0.1" + assert unrelated_entry.data[CONF_ADDRESS] == "127.0.0.2" + + async def test_zeroconf_add_existing_aborts(hass, dmap_device): """Test start new zeroconf flow while existing flow is active aborts.""" await hass.config_entries.flow.async_init( From b1117c17f1b077998243016b89772b63646bf626 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 16 Dec 2021 09:26:44 +0100 Subject: [PATCH 0507/2644] Tweak comment for ENERGY sensor device class (#62006) --- homeassistant/components/sensor/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 14adfe85d41..b115da28800 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -90,7 +90,7 @@ class SensorDeviceClass(StrEnum): # date (ISO8601) DATE = "date" - # energy (kWh, Wh) + # energy (Wh, kWh, MWh) ENERGY = "energy" # frequency (Hz, kHz, MHz, GHz) From b03ead1c9b6355b1431e047c4005c2622a602b5e Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 03:27:53 -0500 Subject: [PATCH 0508/2644] Use enums in vicare (#61994) --- .../components/vicare/binary_sensor.py | 8 +-- homeassistant/components/vicare/const.py | 13 ++-- homeassistant/components/vicare/sensor.py | 65 +++++++++---------- 3 files changed, 39 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/vicare/binary_sensor.py b/homeassistant/components/vicare/binary_sensor.py index 510c332f89f..e4aa31793e5 100644 --- a/homeassistant/components/vicare/binary_sensor.py +++ b/homeassistant/components/vicare/binary_sensor.py @@ -13,7 +13,7 @@ from PyViCare.PyViCareUtils import ( import requests from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_POWER, + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -40,7 +40,7 @@ CIRCUIT_SENSORS: tuple[ViCareBinarySensorEntityDescription, ...] = ( ViCareBinarySensorEntityDescription( key=SENSOR_CIRCULATION_PUMP_ACTIVE, name="Circulation pump active", - device_class=DEVICE_CLASS_POWER, + device_class=BinarySensorDeviceClass.POWER, value_getter=lambda api: api.getCirculationPumpActive(), ), ) @@ -49,7 +49,7 @@ BURNER_SENSORS: tuple[ViCareBinarySensorEntityDescription, ...] = ( ViCareBinarySensorEntityDescription( key=SENSOR_BURNER_ACTIVE, name="Burner active", - device_class=DEVICE_CLASS_POWER, + device_class=BinarySensorDeviceClass.POWER, value_getter=lambda api: api.getActive(), ), ) @@ -58,7 +58,7 @@ COMPRESSOR_SENSORS: tuple[ViCareBinarySensorEntityDescription, ...] = ( ViCareBinarySensorEntityDescription( key=SENSOR_COMPRESSOR_ACTIVE, name="Compressor active", - device_class=DEVICE_CLASS_POWER, + device_class=BinarySensorDeviceClass.POWER, value_getter=lambda api: api.getActive(), ), ) diff --git a/homeassistant/components/vicare/const.py b/homeassistant/components/vicare/const.py index c5f78f31fc0..f38b3eea26a 100644 --- a/homeassistant/components/vicare/const.py +++ b/homeassistant/components/vicare/const.py @@ -1,13 +1,8 @@ """Constants for the ViCare integration.""" import enum -from homeassistant.const import ( - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_GAS, - ENERGY_KILO_WATT_HOUR, - VOLUME_CUBIC_METERS, - Platform, -) +from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.const import ENERGY_KILO_WATT_HOUR, VOLUME_CUBIC_METERS, Platform DOMAIN = "vicare" @@ -31,8 +26,8 @@ VICARE_CUBIC_METER = "cubicMeter" VICARE_KWH = "kilowattHour" VICARE_UNIT_TO_DEVICE_CLASS = { - VICARE_KWH: DEVICE_CLASS_ENERGY, - VICARE_CUBIC_METER: DEVICE_CLASS_GAS, + VICARE_KWH: SensorDeviceClass.ENERGY, + VICARE_CUBIC_METER: SensorDeviceClass.GAS, } VICARE_UNIT_TO_UNIT_OF_MEASUREMENT = { diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index 6397236f299..f409dc67668 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -15,16 +15,13 @@ from PyViCare.PyViCareUtils import ( import requests from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.const import ( CONF_NAME, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_TEMPERATURE, ENERGY_KILO_WATT_HOUR, PERCENTAGE, POWER_WATT, @@ -94,21 +91,21 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( name="Outside Temperature", native_unit_of_measurement=TEMP_CELSIUS, value_getter=lambda api: api.getOutsideTemperature(), - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), ViCareSensorEntityDescription( key=SENSOR_RETURN_TEMPERATURE, name="Return Temperature", native_unit_of_measurement=TEMP_CELSIUS, value_getter=lambda api: api.getReturnTemperature(), - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), ViCareSensorEntityDescription( key=SENSOR_BOILER_TEMPERATURE, name="Boiler Temperature", native_unit_of_measurement=TEMP_CELSIUS, value_getter=lambda api: api.getBoilerTemperature(), - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), ViCareSensorEntityDescription( key=SENSOR_DHW_GAS_CONSUMPTION_TODAY, @@ -116,8 +113,8 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value_getter=lambda api: api.getGasConsumptionDomesticHotWaterToday(), unit_getter=lambda api: api.getGasConsumptionDomesticHotWaterUnit(), - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), ViCareSensorEntityDescription( key=SENSOR_DHW_GAS_CONSUMPTION_THIS_WEEK, @@ -125,8 +122,8 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value_getter=lambda api: api.getGasConsumptionDomesticHotWaterThisWeek(), unit_getter=lambda api: api.getGasConsumptionDomesticHotWaterUnit(), - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), ViCareSensorEntityDescription( key=SENSOR_DHW_GAS_CONSUMPTION_THIS_MONTH, @@ -134,8 +131,8 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value_getter=lambda api: api.getGasConsumptionDomesticHotWaterThisMonth(), unit_getter=lambda api: api.getGasConsumptionDomesticHotWaterUnit(), - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), ViCareSensorEntityDescription( key=SENSOR_DHW_GAS_CONSUMPTION_THIS_YEAR, @@ -143,8 +140,8 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value_getter=lambda api: api.getGasConsumptionDomesticHotWaterThisYear(), unit_getter=lambda api: api.getGasConsumptionDomesticHotWaterUnit(), - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), ViCareSensorEntityDescription( key=SENSOR_GAS_CONSUMPTION_TODAY, @@ -152,8 +149,8 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value_getter=lambda api: api.getGasConsumptionHeatingToday(), unit_getter=lambda api: api.getGasConsumptionHeatingUnit(), - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), ViCareSensorEntityDescription( key=SENSOR_GAS_CONSUMPTION_THIS_WEEK, @@ -161,8 +158,8 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value_getter=lambda api: api.getGasConsumptionHeatingThisWeek(), unit_getter=lambda api: api.getGasConsumptionHeatingUnit(), - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), ViCareSensorEntityDescription( key=SENSOR_GAS_CONSUMPTION_THIS_MONTH, @@ -170,8 +167,8 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value_getter=lambda api: api.getGasConsumptionHeatingThisMonth(), unit_getter=lambda api: api.getGasConsumptionHeatingUnit(), - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), ViCareSensorEntityDescription( key=SENSOR_GAS_CONSUMPTION_THIS_YEAR, @@ -179,48 +176,48 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value_getter=lambda api: api.getGasConsumptionHeatingThisYear(), unit_getter=lambda api: api.getGasConsumptionHeatingUnit(), - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), ViCareSensorEntityDescription( key=SENSOR_POWER_PRODUCTION_CURRENT, name="Power production current", native_unit_of_measurement=POWER_WATT, value_getter=lambda api: api.getPowerProductionCurrent(), - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), ViCareSensorEntityDescription( key=SENSOR_POWER_PRODUCTION_TODAY, name="Power production today", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value_getter=lambda api: api.getPowerProductionToday(), - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), ViCareSensorEntityDescription( key=SENSOR_POWER_PRODUCTION_THIS_WEEK, name="Power production this week", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value_getter=lambda api: api.getPowerProductionThisWeek(), - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), ViCareSensorEntityDescription( key=SENSOR_POWER_PRODUCTION_THIS_MONTH, name="Power production this month", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value_getter=lambda api: api.getPowerProductionThisMonth(), - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), ViCareSensorEntityDescription( key=SENSOR_POWER_PRODUCTION_THIS_YEAR, name="Power production this year", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value_getter=lambda api: api.getPowerProductionThisYear(), - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), ) From c9dbcc49e3db38c4f335ccbb87513f7b3c998c9f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 16 Dec 2021 09:49:15 +0100 Subject: [PATCH 0509/2644] Use new enums in mysensors (#61935) --- .../components/mysensors/binary_sensor.py | 18 ++++----- homeassistant/components/mysensors/sensor.py | 39 ++++++++----------- 2 files changed, 23 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/mysensors/binary_sensor.py b/homeassistant/components/mysensors/binary_sensor.py index 91dc454dae3..a2118de3a76 100644 --- a/homeassistant/components/mysensors/binary_sensor.py +++ b/homeassistant/components/mysensors/binary_sensor.py @@ -3,13 +3,9 @@ from __future__ import annotations from homeassistant.components import mysensors from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_MOISTURE, - DEVICE_CLASS_MOTION, - DEVICE_CLASS_SAFETY, - DEVICE_CLASS_SOUND, - DEVICE_CLASS_VIBRATION, DEVICE_CLASSES, DOMAIN, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -23,13 +19,13 @@ from .helpers import on_unload SENSORS = { "S_DOOR": "door", - "S_MOTION": DEVICE_CLASS_MOTION, + "S_MOTION": BinarySensorDeviceClass.MOTION, "S_SMOKE": "smoke", - "S_SPRINKLER": DEVICE_CLASS_SAFETY, - "S_WATER_LEAK": DEVICE_CLASS_SAFETY, - "S_SOUND": DEVICE_CLASS_SOUND, - "S_VIBRATION": DEVICE_CLASS_VIBRATION, - "S_MOISTURE": DEVICE_CLASS_MOISTURE, + "S_SPRINKLER": BinarySensorDeviceClass.SAFETY, + "S_WATER_LEAK": BinarySensorDeviceClass.SAFETY, + "S_SOUND": BinarySensorDeviceClass.SOUND, + "S_VIBRATION": BinarySensorDeviceClass.VIBRATION, + "S_MOISTURE": BinarySensorDeviceClass.MOISTURE, } diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index 94a9cde1df2..9d02571cf49 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -8,22 +8,15 @@ from awesomeversion import AwesomeVersion from homeassistant.components import mysensors from homeassistant.components.sensor import ( DOMAIN, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONDUCTIVITY, DEGREE, - DEVICE_CLASS_CURRENT, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_POWER, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_VOLTAGE, ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_MILLIVOLT, ELECTRIC_POTENTIAL_VOLT, @@ -50,14 +43,14 @@ from .helpers import on_unload SENSORS: dict[str, SensorEntityDescription] = { "V_TEMP": SensorEntityDescription( key="V_TEMP", - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), "V_HUM": SensorEntityDescription( key="V_HUM", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), "V_DIMMER": SensorEntityDescription( key="V_DIMMER", @@ -115,14 +108,14 @@ SENSORS: dict[str, SensorEntityDescription] = { "V_WATT": SensorEntityDescription( key="V_WATT", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), "V_KWH": SensorEntityDescription( key="V_KWH", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), "V_LIGHT_LEVEL": SensorEntityDescription( key="V_LIGHT_LEVEL", @@ -150,8 +143,8 @@ SENSORS: dict[str, SensorEntityDescription] = { "V_LEVEL_S_LIGHT_LEVEL": SensorEntityDescription( key="V_LEVEL_S_LIGHT_LEVEL", native_unit_of_measurement=LIGHT_LUX, - device_class=DEVICE_CLASS_ILLUMINANCE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, ), "V_LEVEL_S_MOISTURE": SensorEntityDescription( key="V_LEVEL_S_MOISTURE", @@ -161,14 +154,14 @@ SENSORS: dict[str, SensorEntityDescription] = { "V_VOLTAGE": SensorEntityDescription( key="V_VOLTAGE", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, ), "V_CURRENT": SensorEntityDescription( key="V_CURRENT", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, ), "V_PH": SensorEntityDescription( key="V_PH", From a9879487cc5d7474ad6ccfd74fb03ae77c1b596d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 16 Dec 2021 09:54:45 +0100 Subject: [PATCH 0510/2644] Bump version to 2022.2.0dev0 (#62011) --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index cb33c20b14b..5afbd1daee1 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -6,7 +6,7 @@ from typing import Final from homeassistant.backports.enum import StrEnum MAJOR_VERSION: Final = 2022 -MINOR_VERSION: Final = 1 +MINOR_VERSION: Final = 2 PATCH_VERSION: Final = "0.dev0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" From b1b3079d0767a15c9f679b90222f2f7003a8d6f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Thu, 16 Dec 2021 12:15:40 +0100 Subject: [PATCH 0511/2644] Add Tibber estimated hour consumption sensor (#62003) --- homeassistant/components/tibber/manifest.json | 2 +- homeassistant/components/tibber/sensor.py | 6 ++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index c0b047a2856..8c7a49256df 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -2,7 +2,7 @@ "domain": "tibber", "name": "Tibber", "documentation": "https://www.home-assistant.io/integrations/tibber", - "requirements": ["pyTibber==0.21.1"], + "requirements": ["pyTibber==0.21.4"], "codeowners": ["@danielhiversen"], "quality_scale": "silver", "config_flow": true, diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 0958996bfee..9bc7502e9c2 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -88,6 +88,12 @@ RT_SENSORS: tuple[SensorEntityDescription, ...] = ( native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, ), + SensorEntityDescription( + key="estimatedHourConsumption", + name="Estimated consumption current hour", + device_class=SensorDeviceClass.ENERGY, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ), SensorEntityDescription( key="accumulatedProduction", name="accumulated production", diff --git a/requirements_all.txt b/requirements_all.txt index 487a07c1732..e4bfa5a9610 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1336,7 +1336,7 @@ pyRFXtrx==0.27.0 # pySwitchmate==0.4.6 # homeassistant.components.tibber -pyTibber==0.21.1 +pyTibber==0.21.4 # homeassistant.components.dlink pyW215==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a6bf1e345a9..0774c842fd6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -817,7 +817,7 @@ pyMetno==0.9.0 pyRFXtrx==0.27.0 # homeassistant.components.tibber -pyTibber==0.21.1 +pyTibber==0.21.4 # homeassistant.components.nextbus py_nextbusnext==0.1.5 From 04153c00752d1041a416bb4cd9eb754a7cd5d0cf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 16 Dec 2021 05:16:19 -0600 Subject: [PATCH 0512/2644] Add hardware version to the device registry (#61650) --- .../components/config/device_registry.py | 1 + homeassistant/helpers/device_registry.py | 66 ++++--- homeassistant/helpers/entity.py | 1 + .../components/config/test_device_registry.py | 2 + tests/helpers/test_device_registry.py | 164 +++++++++++++++++- 5 files changed, 206 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py index 4d4b8333d70..f553e5d5401 100644 --- a/homeassistant/components/config/device_registry.py +++ b/homeassistant/components/config/device_registry.py @@ -84,5 +84,6 @@ def _entry_dict(entry): "name_by_user": entry.name_by_user, "name": entry.name, "sw_version": entry.sw_version, + "hw_version": entry.hw_version, "via_device_id": entry.via_device_id, } diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index e31b77d3ae2..68af26160d8 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -33,7 +33,7 @@ DATA_REGISTRY = "device_registry" EVENT_DEVICE_REGISTRY_UPDATED = "device_registry_updated" STORAGE_KEY = "core.device_registry" STORAGE_VERSION_MAJOR = 1 -STORAGE_VERSION_MINOR = 2 +STORAGE_VERSION_MINOR = 3 SAVE_DELAY = 10 CLEANUP_DELAY = 10 @@ -87,6 +87,7 @@ class DeviceEntry: name: str | None = attr.ib(default=None) suggested_area: str | None = attr.ib(default=None) sw_version: str | None = attr.ib(default=None) + hw_version: str | None = attr.ib(default=None) via_device_id: str | None = attr.ib(default=None) # This value is not stored, just used to keep track of events to fire. is_new: bool = attr.ib(default=False) @@ -168,32 +169,37 @@ class DeviceRegistryStore(storage.Store): self, old_major_version: int, old_minor_version: int, old_data: dict[str, Any] ) -> dict[str, Any]: """Migrate to the new version.""" - if old_major_version < 2 and old_minor_version < 2: - # From version 1.1 - for device in old_data["devices"]: - # Introduced in 0.110 - try: - device["entry_type"] = DeviceEntryType(device.get("entry_type")) - except ValueError: - device["entry_type"] = None + if old_major_version < 2: + if old_minor_version < 2: + # From version 1.1 + for device in old_data["devices"]: + # Introduced in 0.110 + try: + device["entry_type"] = DeviceEntryType(device.get("entry_type")) + except ValueError: + device["entry_type"] = None - # Introduced in 0.79 - # renamed in 0.95 - device["via_device_id"] = device.get("via_device_id") or device.get( - "hub_device_id" - ) - # Introduced in 0.87 - device["area_id"] = device.get("area_id") - device["name_by_user"] = device.get("name_by_user") - # Introduced in 0.119 - device["disabled_by"] = device.get("disabled_by") - # Introduced in 2021.11 - device["configuration_url"] = device.get("configuration_url") - # Introduced in 0.111 - old_data["deleted_devices"] = old_data.get("deleted_devices", []) - for device in old_data["deleted_devices"]: - # Introduced in 2021.2 - device["orphaned_timestamp"] = device.get("orphaned_timestamp") + # Introduced in 0.79 + # renamed in 0.95 + device["via_device_id"] = device.get("via_device_id") or device.get( + "hub_device_id" + ) + # Introduced in 0.87 + device["area_id"] = device.get("area_id") + device["name_by_user"] = device.get("name_by_user") + # Introduced in 0.119 + device["disabled_by"] = device.get("disabled_by") + # Introduced in 2021.11 + device["configuration_url"] = device.get("configuration_url") + # Introduced in 0.111 + old_data["deleted_devices"] = old_data.get("deleted_devices", []) + for device in old_data["deleted_devices"]: + # Introduced in 2021.2 + device["orphaned_timestamp"] = device.get("orphaned_timestamp") + if old_minor_version < 3: + # Introduced in 2022.2 + for device in old_data["devices"]: + device["hw_version"] = device.get("hw_version") if old_major_version > 1: raise NotImplementedError @@ -314,6 +320,7 @@ class DeviceRegistry: name: str | None | UndefinedType = UNDEFINED, suggested_area: str | None | UndefinedType = UNDEFINED, sw_version: str | None | UndefinedType = UNDEFINED, + hw_version: str | None | UndefinedType = UNDEFINED, via_device: tuple[str, str] | None = None, ) -> DeviceEntry: """Get device. Create if it doesn't exist.""" @@ -378,6 +385,7 @@ class DeviceRegistry: name=name, suggested_area=suggested_area, sw_version=sw_version, + hw_version=hw_version, via_device_id=via_device_id, ) @@ -403,6 +411,7 @@ class DeviceRegistry: remove_config_entry_id: str | UndefinedType = UNDEFINED, suggested_area: str | None | UndefinedType = UNDEFINED, sw_version: str | None | UndefinedType = UNDEFINED, + hw_version: str | None | UndefinedType = UNDEFINED, via_device_id: str | None | UndefinedType = UNDEFINED, ) -> DeviceEntry | None: """Update properties of a device.""" @@ -420,6 +429,7 @@ class DeviceRegistry: remove_config_entry_id=remove_config_entry_id, suggested_area=suggested_area, sw_version=sw_version, + hw_version=hw_version, via_device_id=via_device_id, ) @@ -443,6 +453,7 @@ class DeviceRegistry: remove_config_entry_id: str | UndefinedType = UNDEFINED, suggested_area: str | None | UndefinedType = UNDEFINED, sw_version: str | None | UndefinedType = UNDEFINED, + hw_version: str | None | UndefinedType = UNDEFINED, via_device_id: str | None | UndefinedType = UNDEFINED, ) -> DeviceEntry | None: """Update device attributes.""" @@ -513,6 +524,7 @@ class DeviceRegistry: ("name", name), ("suggested_area", suggested_area), ("sw_version", sw_version), + ("hw_version", hw_version), ("via_device_id", via_device_id), ): if value is not UNDEFINED and value != getattr(old, attr_name): @@ -595,6 +607,7 @@ class DeviceRegistry: name_by_user=device["name_by_user"], name=device["name"], sw_version=device["sw_version"], + hw_version=device["hw_version"], via_device_id=device["via_device_id"], ) # Introduced in 0.111 @@ -631,6 +644,7 @@ class DeviceRegistry: "model": entry.model, "name": entry.name, "sw_version": entry.sw_version, + "hw_version": entry.hw_version, "entry_type": entry.entry_type, "id": entry.id, "via_device_id": entry.via_device_id, diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index cd04f9db184..87891c9f2a8 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -177,6 +177,7 @@ class DeviceInfo(TypedDict, total=False): name: str | None suggested_area: str | None sw_version: str | None + hw_version: str | None via_device: tuple[str, str] diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py index ee8c933f761..11b2f663f19 100644 --- a/tests/components/config/test_device_registry.py +++ b/tests/components/config/test_device_registry.py @@ -53,6 +53,7 @@ async def test_list_devices(hass, client, registry): "model": "model", "name": None, "sw_version": None, + "hw_version": None, "entry_type": None, "via_device_id": None, "area_id": None, @@ -68,6 +69,7 @@ async def test_list_devices(hass, client, registry): "model": "model", "name": None, "sw_version": None, + "hw_version": None, "entry_type": helpers_dr.DeviceEntryType.SERVICE, "via_device_id": dev1, "area_id": None, diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 571fa0c097e..e9a1506e71d 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -185,6 +185,7 @@ async def test_loading_from_storage(hass, hass_storage): "name_by_user": "Test Friendly Name", "name": "name", "sw_version": "version", + "hw_version": "hw_version", "via_device_id": None, } ], @@ -215,6 +216,7 @@ async def test_loading_from_storage(hass, hass_storage): assert entry.id == "abcdefghijklm" assert entry.area_id == "12345A" assert entry.name_by_user == "Test Friendly Name" + assert entry.hw_version == "hw_version" assert entry.entry_type is device_registry.DeviceEntryType.SERVICE assert entry.disabled_by is device_registry.DeviceEntryDisabler.USER assert isinstance(entry.config_entries, set) @@ -235,8 +237,8 @@ async def test_loading_from_storage(hass, hass_storage): @pytest.mark.parametrize("load_registries", [False]) -async def test_migration_1_1_to_1_2(hass, hass_storage): - """Test migration from version 1.1 to 1.2.""" +async def test_migration_1_1_to_1_3(hass, hass_storage): + """Test migration from version 1.1 to 1.3.""" hass_storage[device_registry.STORAGE_KEY] = { "version": 1, "minor_version": 1, @@ -266,6 +268,19 @@ async def test_migration_1_1_to_1_2(hass, hass_storage): "sw_version": None, }, ], + "deleted_devices": [ + { + "config_entries": ["123456"], + "connections": [], + "entry_type": "service", + "id": "deletedid", + "identifiers": [["serial", "12:34:56:AB:CD:FF"]], + "manufacturer": "manufacturer", + "model": "model", + "name": "name", + "sw_version": "version", + } + ], }, } @@ -311,6 +326,7 @@ async def test_migration_1_1_to_1_2(hass, hass_storage): "name": "name", "name_by_user": None, "sw_version": "new_version", + "hw_version": None, "via_device_id": None, }, { @@ -327,6 +343,132 @@ async def test_migration_1_1_to_1_2(hass, hass_storage): "name_by_user": None, "name": None, "sw_version": None, + "hw_version": None, + "via_device_id": None, + }, + ], + "deleted_devices": [ + { + "config_entries": ["123456"], + "connections": [], + "id": "deletedid", + "identifiers": [["serial", "12:34:56:AB:CD:FF"]], + "orphaned_timestamp": None, + } + ], + }, + } + + +@pytest.mark.parametrize("load_registries", [False]) +async def test_migration_1_2_to_1_3(hass, hass_storage): + """Test migration from version 1.2 to 1.3.""" + hass_storage[device_registry.STORAGE_KEY] = { + "version": 1, + "minor_version": 2, + "key": device_registry.STORAGE_KEY, + "data": { + "devices": [ + { + "area_id": None, + "config_entries": ["1234"], + "configuration_url": None, + "connections": [["Zigbee", "01.23.45.67.89"]], + "disabled_by": None, + "entry_type": "service", + "id": "abcdefghijklm", + "identifiers": [["serial", "12:34:56:AB:CD:EF"]], + "manufacturer": "manufacturer", + "model": "model", + "name": "name", + "name_by_user": None, + "sw_version": "new_version", + "hw_version": None, + "via_device_id": None, + }, + { + "area_id": None, + "config_entries": [None], + "configuration_url": None, + "connections": [], + "disabled_by": None, + "entry_type": None, + "id": "invalid-entry-type", + "identifiers": [["serial", "mock-id-invalid-entry"]], + "manufacturer": None, + "model": None, + "name_by_user": None, + "name": None, + "sw_version": None, + "hw_version": None, + "via_device_id": None, + }, + ], + "deleted_devices": [], + }, + } + + await device_registry.async_load(hass) + registry = device_registry.async_get(hass) + + # Test data was loaded + entry = registry.async_get_or_create( + config_entry_id="1234", + connections={("Zigbee", "01.23.45.67.89")}, + identifiers={("serial", "12:34:56:AB:CD:EF")}, + ) + assert entry.id == "abcdefghijklm" + + # Update to trigger a store + entry = registry.async_get_or_create( + config_entry_id="1234", + connections={("Zigbee", "01.23.45.67.89")}, + identifiers={("serial", "12:34:56:AB:CD:EF")}, + hw_version="new_version", + ) + assert entry.id == "abcdefghijklm" + + # Check we store migrated data + await flush_store(registry._store) + + assert hass_storage[device_registry.STORAGE_KEY] == { + "version": device_registry.STORAGE_VERSION_MAJOR, + "minor_version": device_registry.STORAGE_VERSION_MINOR, + "key": device_registry.STORAGE_KEY, + "data": { + "devices": [ + { + "area_id": None, + "config_entries": ["1234"], + "configuration_url": None, + "connections": [["Zigbee", "01.23.45.67.89"]], + "disabled_by": None, + "entry_type": "service", + "id": "abcdefghijklm", + "identifiers": [["serial", "12:34:56:AB:CD:EF"]], + "manufacturer": "manufacturer", + "model": "model", + "name": "name", + "name_by_user": None, + "sw_version": "new_version", + "hw_version": "new_version", + "via_device_id": None, + }, + { + "area_id": None, + "config_entries": [None], + "configuration_url": None, + "connections": [], + "disabled_by": None, + "entry_type": None, + "id": "invalid-entry-type", + "identifiers": [["serial", "mock-id-invalid-entry"]], + "manufacturer": None, + "model": None, + "name_by_user": None, + "name": None, + "sw_version": None, + "hw_version": None, "via_device_id": None, }, ], @@ -865,6 +1007,24 @@ async def test_update_sw_version(registry): assert updated_entry.sw_version == sw_version +async def test_update_hw_version(registry): + """Verify that we can update hardware version of a device.""" + entry = registry.async_get_or_create( + config_entry_id="1234", + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bla", "123")}, + ) + assert not entry.hw_version + hw_version = "0x20020263" + + with patch.object(registry, "async_schedule_save") as mock_save: + updated_entry = registry.async_update_device(entry.id, hw_version=hw_version) + + assert mock_save.call_count == 1 + assert updated_entry != entry + assert updated_entry.hw_version == hw_version + + async def test_update_suggested_area(registry, area_registry): """Verify that we can update the suggested area version of a device.""" entry = registry.async_get_or_create( From 389d9c2c35768a4f750b4421061830cec07c22e6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 16 Dec 2021 12:16:56 +0100 Subject: [PATCH 0513/2644] Use new enums in netgear (#61943) --- homeassistant/components/netgear/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netgear/sensor.py b/homeassistant/components/netgear/sensor.py index 9762a777194..1af78110ed4 100644 --- a/homeassistant/components/netgear/sensor.py +++ b/homeassistant/components/netgear/sensor.py @@ -1,6 +1,6 @@ """Support for Netgear routers.""" from homeassistant.components.sensor import ( - DEVICE_CLASS_SIGNAL_STRENGTH, + SensorDeviceClass, SensorEntity, SensorEntityDescription, ) @@ -25,7 +25,7 @@ SENSOR_TYPES = { key="signal", name="signal strength", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, ), "ssid": SensorEntityDescription( key="ssid", From 11fde22d45f7f932034a97228a275bb370ed2829 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 16 Dec 2021 05:24:18 -0600 Subject: [PATCH 0514/2644] Add hardware version support to homekit controller (#61743) --- homeassistant/components/homekit_controller/__init__.py | 1 + homeassistant/components/homekit_controller/connection.py | 1 + .../homekit_controller/specific_devices/test_anker_eufycam.py | 1 + .../homekit_controller/specific_devices/test_lg_tv.py | 1 + .../specific_devices/test_rainmachine_pro_8.py | 1 + .../specific_devices/test_ryse_smart_bridge.py | 2 ++ .../specific_devices/test_vocolinc_flowerbud.py | 1 + 7 files changed, 8 insertions(+) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index f91355906dc..7c07e921818 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -177,6 +177,7 @@ class HomeKitEntity(Entity): model=info.value(CharacteristicsTypes.MODEL, ""), name=info.value(CharacteristicsTypes.NAME), sw_version=info.value(CharacteristicsTypes.FIRMWARE_REVISION, ""), + hw_version=info.value(CharacteristicsTypes.HARDWARE_REVISION, ""), ) # Some devices only have a single accessory - we don't add a diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 8523fec7b8f..112c05b17f5 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -244,6 +244,7 @@ class HKDevice: manufacturer=info.value(CharacteristicsTypes.MANUFACTURER, ""), model=info.value(CharacteristicsTypes.MODEL, ""), sw_version=info.value(CharacteristicsTypes.FIRMWARE_REVISION, ""), + hw_version=info.value(CharacteristicsTypes.HARDWARE_REVISION, ""), ) if accessory.aid != 1: diff --git a/tests/components/homekit_controller/specific_devices/test_anker_eufycam.py b/tests/components/homekit_controller/specific_devices/test_anker_eufycam.py index 15ac6d2d3ab..652ac5b0559 100644 --- a/tests/components/homekit_controller/specific_devices/test_anker_eufycam.py +++ b/tests/components/homekit_controller/specific_devices/test_anker_eufycam.py @@ -40,6 +40,7 @@ async def test_eufycam_setup(hass): assert device.name == "eufyCam2-0000" assert device.model == "T8113" assert device.sw_version == "1.6.7" + assert device.hw_version == "1.0.0" # These cameras are via a bridge, so via should be set assert device.via_device_id is not None diff --git a/tests/components/homekit_controller/specific_devices/test_lg_tv.py b/tests/components/homekit_controller/specific_devices/test_lg_tv.py index 7f7ada4ac1f..bce8ed7418c 100644 --- a/tests/components/homekit_controller/specific_devices/test_lg_tv.py +++ b/tests/components/homekit_controller/specific_devices/test_lg_tv.py @@ -63,6 +63,7 @@ async def test_lg_tv(hass): assert device.model == "OLED55B9PUA" assert device.sw_version == "04.71.04" assert device.via_device_id is None + assert device.hw_version == "1" # A TV has media player device triggers triggers = await async_get_device_automations(hass, "trigger", device.id) diff --git a/tests/components/homekit_controller/specific_devices/test_rainmachine_pro_8.py b/tests/components/homekit_controller/specific_devices/test_rainmachine_pro_8.py index 81e31918c91..ee008fbfa62 100644 --- a/tests/components/homekit_controller/specific_devices/test_rainmachine_pro_8.py +++ b/tests/components/homekit_controller/specific_devices/test_rainmachine_pro_8.py @@ -40,6 +40,7 @@ async def test_rainmachine_pro_8_setup(hass): assert device.model == "SPK5 Pro" assert device.sw_version == "1.0.4" assert device.via_device_id is None + assert device.hw_version == "1" # The device is made up of multiple valves - make sure we have enumerated them all entry = entity_registry.async_get("switch.rainmachine_00ce4a_2") diff --git a/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py b/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py index e10e0ccd62a..302ebe70ac2 100644 --- a/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py +++ b/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py @@ -40,12 +40,14 @@ async def test_ryse_smart_bridge_setup(hass): assert device.name == "Master Bath South" assert device.model == "RYSE Shade" assert device.sw_version == "3.0.8" + assert device.hw_version == "1.0.0" bridge = device_registry.async_get(device.via_device_id) assert bridge.manufacturer == "RYSE Inc." assert bridge.name == "RYSE SmartBridge" assert bridge.model == "RYSE SmartBridge" assert bridge.sw_version == "1.3.0" + assert bridge.hw_version == "0101.3521.0436" # Check that the cover.ryse_smartshade is correctly found and set up cover_id = "cover.ryse_smartshade" diff --git a/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py b/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py index 6968c62257f..391ac1c8f39 100644 --- a/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py +++ b/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py @@ -34,6 +34,7 @@ async def test_vocolinc_flowerbud_setup(hass): assert device.model == "Flowerbud" assert device.sw_version == "3.121.2" assert device.via_device_id is None + assert device.hw_version == "0.1" # Assert the humidifier is detected entry = entity_registry.async_get("humidifier.vocolinc_flowerbud_0d324b") From 087724d2f283d9fe45716cab09dbebdb8b3fd580 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 06:26:39 -0500 Subject: [PATCH 0515/2644] Use enums in toon (#62021) --- .../components/toon/binary_sensor.py | 9 +-- homeassistant/components/toon/sensor.py | 79 +++++++++---------- 2 files changed, 41 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/toon/binary_sensor.py b/homeassistant/components/toon/binary_sensor.py index 30f5459c175..84e47ce6cb5 100644 --- a/homeassistant/components/toon/binary_sensor.py +++ b/homeassistant/components/toon/binary_sensor.py @@ -4,8 +4,7 @@ from __future__ import annotations from dataclasses import dataclass from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_CONNECTIVITY, - DEVICE_CLASS_PROBLEM, + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -113,7 +112,7 @@ BINARY_SENSOR_ENTITIES = ( name="Boiler Module Connection", section="thermostat", measurement="boiler_module_connected", - device_class=DEVICE_CLASS_CONNECTIVITY, + device_class=BinarySensorDeviceClass.CONNECTIVITY, entity_registry_enabled_default=False, cls=ToonBoilerModuleBinarySensor, ), @@ -167,7 +166,7 @@ BINARY_SENSOR_ENTITIES_BOILER: tuple[ToonBinarySensorEntityDescription, ...] = ( name="Boiler Status", section="thermostat", measurement="error_found", - device_class=DEVICE_CLASS_PROBLEM, + device_class=BinarySensorDeviceClass.PROBLEM, icon="mdi:alert", cls=ToonBoilerBinarySensor, ), @@ -176,7 +175,7 @@ BINARY_SENSOR_ENTITIES_BOILER: tuple[ToonBinarySensorEntityDescription, ...] = ( name="OpenTherm Connection", section="thermostat", measurement="opentherm_communication_error", - device_class=DEVICE_CLASS_PROBLEM, + device_class=BinarySensorDeviceClass.PROBLEM, icon="mdi:check-network-outline", entity_registry_enabled_default=False, cls=ToonBoilerBinarySensor, diff --git a/homeassistant/components/toon/sensor.py b/homeassistant/components/toon/sensor.py index 30cde4632a9..3a4fc1a71af 100644 --- a/homeassistant/components/toon/sensor.py +++ b/homeassistant/components/toon/sensor.py @@ -4,18 +4,13 @@ from __future__ import annotations from dataclasses import dataclass from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_GAS, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_TEMPERATURE, ENERGY_KILO_WATT_HOUR, PERCENTAGE, POWER_WATT, @@ -137,9 +132,9 @@ SENSOR_ENTITIES: tuple[ToonSensorEntityDescription, ...] = ( section="thermostat", measurement="current_display_temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, cls=ToonDisplayDeviceSensor, ), ToonSensorEntityDescription( @@ -148,9 +143,9 @@ SENSOR_ENTITIES: tuple[ToonSensorEntityDescription, ...] = ( section="thermostat", measurement="current_humidity", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, cls=ToonDisplayDeviceSensor, ), ToonSensorEntityDescription( @@ -167,7 +162,7 @@ SENSOR_ENTITIES: tuple[ToonSensorEntityDescription, ...] = ( name="Average Daily Gas Usage", section="gas_usage", measurement="day_average", - device_class=DEVICE_CLASS_GAS, + device_class=SensorDeviceClass.GAS, native_unit_of_measurement=VOLUME_CUBIC_METERS, entity_registry_enabled_default=False, cls=ToonGasMeterDeviceSensor, @@ -177,7 +172,7 @@ SENSOR_ENTITIES: tuple[ToonSensorEntityDescription, ...] = ( name="Gas Usage Today", section="gas_usage", measurement="day_usage", - device_class=DEVICE_CLASS_GAS, + device_class=SensorDeviceClass.GAS, native_unit_of_measurement=VOLUME_CUBIC_METERS, cls=ToonGasMeterDeviceSensor, ), @@ -196,8 +191,8 @@ SENSOR_ENTITIES: tuple[ToonSensorEntityDescription, ...] = ( section="gas_usage", measurement="meter", native_unit_of_measurement=VOLUME_CUBIC_METERS, - state_class=STATE_CLASS_TOTAL_INCREASING, - device_class=DEVICE_CLASS_GAS, + state_class=SensorStateClass.TOTAL_INCREASING, + device_class=SensorDeviceClass.GAS, cls=ToonGasMeterDeviceSensor, ), ToonSensorEntityDescription( @@ -215,7 +210,7 @@ SENSOR_ENTITIES: tuple[ToonSensorEntityDescription, ...] = ( section="power_usage", measurement="average", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, cls=ToonElectricityMeterDeviceSensor, ), @@ -225,7 +220,7 @@ SENSOR_ENTITIES: tuple[ToonSensorEntityDescription, ...] = ( section="power_usage", measurement="day_average", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, entity_registry_enabled_default=False, cls=ToonElectricityMeterDeviceSensor, ), @@ -244,7 +239,7 @@ SENSOR_ENTITIES: tuple[ToonSensorEntityDescription, ...] = ( section="power_usage", measurement="day_usage", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, cls=ToonElectricityMeterDeviceSensor, ), ToonSensorEntityDescription( @@ -253,8 +248,8 @@ SENSOR_ENTITIES: tuple[ToonSensorEntityDescription, ...] = ( section="power_usage", measurement="meter_high", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, cls=ToonElectricityMeterDeviceSensor, ), ToonSensorEntityDescription( @@ -263,8 +258,8 @@ SENSOR_ENTITIES: tuple[ToonSensorEntityDescription, ...] = ( section="power_usage", measurement="meter_low", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, cls=ToonElectricityMeterDeviceSensor, ), ToonSensorEntityDescription( @@ -273,8 +268,8 @@ SENSOR_ENTITIES: tuple[ToonSensorEntityDescription, ...] = ( section="power_usage", measurement="current", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, cls=ToonElectricityMeterDeviceSensor, ), ToonSensorEntityDescription( @@ -283,8 +278,8 @@ SENSOR_ENTITIES: tuple[ToonSensorEntityDescription, ...] = ( section="power_usage", measurement="meter_produced_high", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, cls=ToonElectricityMeterDeviceSensor, ), ToonSensorEntityDescription( @@ -293,8 +288,8 @@ SENSOR_ENTITIES: tuple[ToonSensorEntityDescription, ...] = ( section="power_usage", measurement="meter_produced_low", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, cls=ToonElectricityMeterDeviceSensor, ), ToonSensorEntityDescription( @@ -335,7 +330,7 @@ SENSOR_ENTITIES: tuple[ToonSensorEntityDescription, ...] = ( native_unit_of_measurement=VOLUME_CUBIC_METERS, icon="mdi:water", entity_registry_enabled_default=False, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, cls=ToonWaterMeterDeviceSensor, ), ToonSensorEntityDescription( @@ -346,7 +341,7 @@ SENSOR_ENTITIES: tuple[ToonSensorEntityDescription, ...] = ( native_unit_of_measurement=VOLUME_LMIN, icon="mdi:water-pump", entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, cls=ToonWaterMeterDeviceSensor, ), ToonSensorEntityDescription( @@ -368,8 +363,8 @@ SENSOR_ENTITIES_SOLAR: tuple[ToonSensorEntityDescription, ...] = ( section="power_usage", measurement="current_solar", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, cls=ToonSolarDeviceSensor, ), ToonSensorEntityDescription( @@ -378,7 +373,7 @@ SENSOR_ENTITIES_SOLAR: tuple[ToonSensorEntityDescription, ...] = ( section="power_usage", measurement="day_max_solar", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, cls=ToonSolarDeviceSensor, ), ToonSensorEntityDescription( @@ -387,8 +382,8 @@ SENSOR_ENTITIES_SOLAR: tuple[ToonSensorEntityDescription, ...] = ( section="power_usage", measurement="current_produced", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, cls=ToonSolarDeviceSensor, ), ToonSensorEntityDescription( @@ -397,8 +392,8 @@ SENSOR_ENTITIES_SOLAR: tuple[ToonSensorEntityDescription, ...] = ( section="power_usage", measurement="day_produced_solar", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, cls=ToonSolarDeviceSensor, ), ToonSensorEntityDescription( @@ -407,7 +402,7 @@ SENSOR_ENTITIES_SOLAR: tuple[ToonSensorEntityDescription, ...] = ( section="power_usage", measurement="day_to_grid_usage", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, entity_registry_enabled_default=False, cls=ToonSolarDeviceSensor, ), @@ -417,7 +412,7 @@ SENSOR_ENTITIES_SOLAR: tuple[ToonSensorEntityDescription, ...] = ( section="power_usage", measurement="day_from_grid_usage", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, entity_registry_enabled_default=False, cls=ToonSolarDeviceSensor, ), @@ -427,7 +422,7 @@ SENSOR_ENTITIES_SOLAR: tuple[ToonSensorEntityDescription, ...] = ( section="power_usage", measurement="average_produced", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, cls=ToonSolarDeviceSensor, ), @@ -438,7 +433,7 @@ SENSOR_ENTITIES_SOLAR: tuple[ToonSensorEntityDescription, ...] = ( measurement="current_covered_by_solar", native_unit_of_measurement=PERCENTAGE, icon="mdi:solar-power", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, cls=ToonSolarDeviceSensor, ), ) @@ -452,7 +447,7 @@ SENSOR_ENTITIES_BOILER: tuple[ToonSensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, icon="mdi:percent", entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, cls=ToonBoilerDeviceSensor, ), ) From ad778f53f732cae72e1081e73289acbcf570a20d Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 06:34:01 -0500 Subject: [PATCH 0516/2644] Use enums in tractive (#62024) --- homeassistant/components/tractive/switch.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tractive/switch.py b/homeassistant/components/tractive/switch.py index e606b68779e..2a425a8f2ac 100644 --- a/homeassistant/components/tractive/switch.py +++ b/homeassistant/components/tractive/switch.py @@ -9,9 +9,9 @@ from aiotractive.exceptions import TractiveError from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_CONFIG 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 . import Trackables @@ -50,21 +50,21 @@ SWITCH_TYPES: tuple[TractiveSwitchEntityDescription, ...] = ( name="Tracker Buzzer", icon="mdi:volume-high", method="async_set_buzzer", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), TractiveSwitchEntityDescription( key=ATTR_LED, name="Tracker LED", icon="mdi:led-on", method="async_set_led", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), TractiveSwitchEntityDescription( key=ATTR_LIVE_TRACKING, name="Live Tracking", icon="mdi:map-marker-path", method="async_set_live_tracking", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), ) From 12671b370eaa8e3ca3a12570f1ed85c213301db2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 16 Dec 2021 05:37:48 -0600 Subject: [PATCH 0517/2644] Bump flux_led to 0.26.15 (#62017) --- homeassistant/components/flux_led/config_flow.py | 6 ++++++ homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/config_flow.py b/homeassistant/components/flux_led/config_flow.py index d0190730ed7..f4ef6a9b290 100644 --- a/homeassistant/components/flux_led/config_flow.py +++ b/homeassistant/components/flux_led/config_flow.py @@ -94,6 +94,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): firmware_date=None, model_info=None, model_description=None, + remote_access_enabled=None, + remote_access_host=None, + remote_access_port=None, ) return await self._async_handle_discovery() @@ -261,6 +264,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): firmware_date=None, model_info=None, model_description=bulb.model_data.description, + remote_access_enabled=None, + remote_access_host=None, + remote_access_port=None, ) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 9e26a128ef3..34e642b4e1f 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Magic Home", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.26.7"], + "requirements": ["flux_led==0.26.15"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index e4bfa5a9610..95a0e08e830 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -667,7 +667,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.26.7 +flux_led==0.26.15 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0774c842fd6..d52f464f1eb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -408,7 +408,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.26.7 +flux_led==0.26.15 # homeassistant.components.homekit fnvhash==0.1.0 From 94ae6ac2b27ebe4c809e0889d14dad1847d64bbd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 16 Dec 2021 05:39:33 -0600 Subject: [PATCH 0518/2644] Handle color temp to RGBWW conversion (#61473) Co-authored-by: Erik Montnemery --- homeassistant/components/light/__init__.py | 24 +++++--- homeassistant/util/color.py | 10 ++++ tests/components/light/test_init.py | 68 ++++++++++++++++++++++ tests/util/test_color.py | 46 +++++++++++++++ 4 files changed, 140 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 390c675886d..3156306554c 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -382,14 +382,22 @@ async def async_setup(hass, config): # noqa: C901 params[ATTR_WHITE_VALUE] = rgbw_color[3] # If a color temperature is specified, emulate it if not supported by the light - if ( - ATTR_COLOR_TEMP in params - and COLOR_MODE_COLOR_TEMP not in legacy_supported_color_modes - ): - color_temp = params.pop(ATTR_COLOR_TEMP) - if color_supported(legacy_supported_color_modes): - temp_k = color_util.color_temperature_mired_to_kelvin(color_temp) - params[ATTR_HS_COLOR] = color_util.color_temperature_to_hs(temp_k) + if ATTR_COLOR_TEMP in params: + if ( + supported_color_modes + and COLOR_MODE_COLOR_TEMP not in supported_color_modes + and COLOR_MODE_RGBWW in supported_color_modes + ): + color_temp = params.pop(ATTR_COLOR_TEMP) + brightness = params.get(ATTR_BRIGHTNESS, light.brightness) + params[ATTR_RGBWW_COLOR] = color_util.color_temperature_to_rgbww( + color_temp, brightness, light.min_mireds, light.max_mireds + ) + elif COLOR_MODE_COLOR_TEMP not in legacy_supported_color_modes: + color_temp = params.pop(ATTR_COLOR_TEMP) + if color_supported(legacy_supported_color_modes): + temp_k = color_util.color_temperature_mired_to_kelvin(color_temp) + params[ATTR_HS_COLOR] = color_util.color_temperature_to_hs(temp_k) # If a color is specified, convert to the color space supported by the light # Backwards compatibility: Fall back to hs color if light.supported_color_modes diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 3d4f7122ad0..e8f802e3aa5 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -528,6 +528,16 @@ def color_temperature_to_rgb( return red, green, blue +def color_temperature_to_rgbww( + temperature: int, brightness: int, min_mireds: int, max_mireds: int +) -> tuple[int, int, int, int, int]: + """Convert color temperature to rgbcw.""" + mired_range = max_mireds - min_mireds + warm = ((max_mireds - temperature) / mired_range) * brightness + cold = brightness - warm + return (0, 0, 0, round(cold), round(warm)) + + def _clamp(color_component: float, minimum: float = 0, maximum: float = 255) -> float: """ Clamp the given color component value between the given min and max values. diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 1f7fe431f0e..a8a6ebc901e 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -1852,6 +1852,74 @@ async def test_light_service_call_color_temp_emulation( assert data == {"brightness": 255, "hs_color": (27.001, 19.243)} +async def test_light_service_call_color_temp_conversion( + hass, enable_custom_integrations +): + """Test color temp conversion in service calls.""" + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("Test_rgbww_ct", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("Test_rgbww", STATE_ON)) + + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = { + light.COLOR_MODE_COLOR_TEMP, + light.COLOR_MODE_RGBWW, + } + + entity1 = platform.ENTITIES[1] + entity1.supported_color_modes = {light.COLOR_MODE_RGBWW} + + assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) + await hass.async_block_till_done() + + state = hass.states.get(entity0.entity_id) + assert state.attributes["supported_color_modes"] == [ + light.COLOR_MODE_COLOR_TEMP, + light.COLOR_MODE_RGBWW, + ] + + state = hass.states.get(entity1.entity_id) + assert state.attributes["supported_color_modes"] == [light.COLOR_MODE_RGBWW] + + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": [ + entity0.entity_id, + entity1.entity_id, + ], + "brightness_pct": 100, + "color_temp": 153, + }, + blocking=True, + ) + _, data = entity0.last_call("turn_on") + assert data == {"brightness": 255, "color_temp": 153} + _, data = entity1.last_call("turn_on") + assert data == {"brightness": 255, "rgbww_color": (0, 0, 0, 0, 255)} + + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": [ + entity0.entity_id, + entity1.entity_id, + ], + "brightness_pct": 50, + "color_temp": 500, + }, + blocking=True, + ) + _, data = entity0.last_call("turn_on") + assert data == {"brightness": 128, "color_temp": 500} + _, data = entity1.last_call("turn_on") + assert data == {"brightness": 128, "rgbww_color": (0, 0, 0, 128, 0)} + + async def test_light_service_call_white_mode(hass, enable_custom_integrations): """Test color_mode white in service calls.""" platform = getattr(hass.components, "test.light") diff --git a/tests/util/test_color.py b/tests/util/test_color.py index d806a941965..9c4b781dc65 100644 --- a/tests/util/test_color.py +++ b/tests/util/test_color.py @@ -401,3 +401,49 @@ def test_color_rgb_to_rgbww(): assert color_util.color_rgb_to_rgbww(64, 64, 64, 154, 370) == (0, 14, 25, 64, 64) assert color_util.color_rgb_to_rgbww(32, 64, 16, 154, 370) == (9, 64, 0, 38, 38) assert color_util.color_rgb_to_rgbww(0, 0, 0, 154, 370) == (0, 0, 0, 0, 0) + + +def test_color_temperature_to_rgbww(): + """Test color temp to warm, cold conversion.""" + assert color_util.color_temperature_to_rgbww(153, 255, 153, 500) == ( + 0, + 0, + 0, + 0, + 255, + ) + assert color_util.color_temperature_to_rgbww(153, 128, 153, 500) == ( + 0, + 0, + 0, + 0, + 128, + ) + assert color_util.color_temperature_to_rgbww(500, 255, 153, 500) == ( + 0, + 0, + 0, + 255, + 0, + ) + assert color_util.color_temperature_to_rgbww(500, 128, 153, 500) == ( + 0, + 0, + 0, + 128, + 0, + ) + assert color_util.color_temperature_to_rgbww(347, 255, 153, 500) == ( + 0, + 0, + 0, + 143, + 112, + ) + assert color_util.color_temperature_to_rgbww(347, 128, 153, 500) == ( + 0, + 0, + 0, + 72, + 56, + ) From 44f4656fe68eb1e464da02d36b580aa4d5572acd Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 06:50:41 -0500 Subject: [PATCH 0519/2644] Use enums in utility_meter (#62033) --- homeassistant/components/utility_meter/sensor.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 69770ec2445..fd2d5b90a15 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -8,14 +8,13 @@ import voluptuous as vol from homeassistant.components.sensor import ( ATTR_LAST_RESET, - STATE_CLASS_TOTAL, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, + SensorStateClass, ) from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, - DEVICE_CLASS_ENERGY, ENERGY_KILO_WATT_HOUR, ENERGY_WATT_HOUR, EVENT_HOMEASSISTANT_START, @@ -78,8 +77,8 @@ ATTR_LAST_PERIOD = "last_period" ATTR_TARIFF = "tariff" DEVICE_CLASS_MAP = { - ENERGY_WATT_HOUR: DEVICE_CLASS_ENERGY, - ENERGY_KILO_WATT_HOUR: DEVICE_CLASS_ENERGY, + ENERGY_WATT_HOUR: SensorDeviceClass.ENERGY, + ENERGY_KILO_WATT_HOUR: SensorDeviceClass.ENERGY, } ICON = "mdi:counter" @@ -370,9 +369,9 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): def state_class(self): """Return the device class of the sensor.""" return ( - STATE_CLASS_TOTAL + SensorStateClass.TOTAL if self._sensor_net_consumption - else STATE_CLASS_TOTAL_INCREASING + else SensorStateClass.TOTAL_INCREASING ) @property From 633706d04a86cfd34e6d7d7ceccacf43a45588d6 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 06:52:06 -0500 Subject: [PATCH 0520/2644] Use enums in upnp (#62031) --- homeassistant/components/upnp/binary_sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/upnp/binary_sensor.py b/homeassistant/components/upnp/binary_sensor.py index c4e7264c34b..1ff1164cc58 100644 --- a/homeassistant/components/upnp/binary_sensor.py +++ b/homeassistant/components/upnp/binary_sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_CONNECTIVITY, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -45,7 +45,7 @@ async def async_setup_entry( class UpnpStatusBinarySensor(UpnpEntity, BinarySensorEntity): """Class for UPnP/IGD binary sensors.""" - _attr_device_class = DEVICE_CLASS_CONNECTIVITY + _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY def __init__( self, From a3765b2977790c96a39c573dd67de2187c38243c Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 06:52:29 -0500 Subject: [PATCH 0521/2644] Use enums in updated (#62030) --- homeassistant/components/updater/binary_sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/updater/binary_sensor.py b/homeassistant/components/updater/binary_sensor.py index 10090946f6e..5ed7a7969ee 100644 --- a/homeassistant/components/updater/binary_sensor.py +++ b/homeassistant/components/updater/binary_sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_UPDATE, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -21,7 +21,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class UpdaterBinary(CoordinatorEntity, BinarySensorEntity): """Representation of an updater binary sensor.""" - _attr_device_class = DEVICE_CLASS_UPDATE + _attr_device_class = BinarySensorDeviceClass.UPDATE _attr_name = "Updater" _attr_unique_id = "updater" From a49683d09a1f57f2590b3eb5fe5e74efacd0607f Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 06:53:01 -0500 Subject: [PATCH 0522/2644] Use enums in unifi (#62029) --- homeassistant/components/unifi/sensor.py | 11 ++++++----- homeassistant/components/unifi/switch.py | 9 ++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index be5b2d03e1c..deae3be3485 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -6,10 +6,11 @@ Support for uptime sensors of network clients. from datetime import datetime, timedelta -from homeassistant.components.sensor import DEVICE_CLASS_TIMESTAMP, DOMAIN, SensorEntity -from homeassistant.const import DATA_MEGABYTES, ENTITY_CATEGORY_DIAGNOSTIC +from homeassistant.components.sensor import DOMAIN, SensorDeviceClass, SensorEntity +from homeassistant.const import DATA_MEGABYTES from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import EntityCategory import homeassistant.util.dt as dt_util from .const import DOMAIN as UNIFI_DOMAIN @@ -86,7 +87,7 @@ class UniFiBandwidthSensor(UniFiClient, SensorEntity): DOMAIN = DOMAIN - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_native_unit_of_measurement = DATA_MEGABYTES @property @@ -132,8 +133,8 @@ class UniFiUpTimeSensor(UniFiClient, SensorEntity): DOMAIN = DOMAIN TYPE = UPTIME_SENSOR - _attr_device_class = DEVICE_CLASS_TIMESTAMP - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_device_class = SensorDeviceClass.TIMESTAMP + _attr_entity_category = EntityCategory.DIAGNOSTIC def __init__(self, client, controller): """Set up tracked client.""" diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 075718b4da5..638ff0aa397 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -15,11 +15,10 @@ from aiounifi.events import ( ) from homeassistant.components.switch import DOMAIN, SwitchEntity -from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import callback from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_registry import async_entries_for_config_entry from homeassistant.helpers.restore_state import RestoreEntity @@ -185,7 +184,7 @@ class UniFiPOEClientSwitch(UniFiClient, SwitchEntity, RestoreEntity): DOMAIN = DOMAIN TYPE = POE_SWITCH - _attr_entity_category = ENTITY_CATEGORY_CONFIG + _attr_entity_category = EntityCategory.CONFIG def __init__(self, client, controller): """Set up POE switch.""" @@ -274,7 +273,7 @@ class UniFiBlockClientSwitch(UniFiClient, SwitchEntity): DOMAIN = DOMAIN TYPE = BLOCK_SWITCH - _attr_entity_category = ENTITY_CATEGORY_CONFIG + _attr_entity_category = EntityCategory.CONFIG def __init__(self, client, controller): """Set up block switch.""" @@ -325,7 +324,7 @@ class UniFiDPIRestrictionSwitch(UniFiBase, SwitchEntity): DOMAIN = DOMAIN TYPE = DPI_SWITCH - _attr_entity_category = ENTITY_CATEGORY_CONFIG + _attr_entity_category = EntityCategory.CONFIG @property def key(self) -> Any: From e713a597eca11a3538608c5c7d1704aadce3222d Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 06:53:50 -0500 Subject: [PATCH 0523/2644] Use entity category enums in tuya (#62028) --- homeassistant/components/tuya/light.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 16eda1cc324..d73b6d236db 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -19,9 +19,9 @@ from homeassistant.components.light import ( LightEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_CONFIG 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 . import HomeAssistantTuyaData @@ -146,7 +146,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { TuyaLightEntityDescription( key=DPCode.BASIC_INDICATOR, name="Indicator Light", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), ), # Dimmer Switch From b4daa88d9e9aa712db238971e8c5398a5268761b Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 07:05:46 -0500 Subject: [PATCH 0524/2644] Use enums in trafikverket_weatherstation (#62027) --- .../trafikverket_weatherstation/sensor.py | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/trafikverket_weatherstation/sensor.py b/homeassistant/components/trafikverket_weatherstation/sensor.py index 01b70d5d3c7..e564002c61c 100644 --- a/homeassistant/components/trafikverket_weatherstation/sensor.py +++ b/homeassistant/components/trafikverket_weatherstation/sensor.py @@ -13,9 +13,10 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, - STATE_CLASS_MEASUREMENT, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( @@ -24,8 +25,6 @@ from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_NAME, DEGREE, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_TEMPERATURE, LENGTH_MILLIMETERS, PERCENTAGE, SPEED_METERS_PER_SECOND, @@ -71,8 +70,8 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( name="Air temperature", native_unit_of_measurement=TEMP_CELSIUS, icon="mdi:thermometer", - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), TrafikverketSensorEntityDescription( key="road_temp", @@ -80,8 +79,8 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( name="Road temperature", native_unit_of_measurement=TEMP_CELSIUS, icon="mdi:thermometer", - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), TrafikverketSensorEntityDescription( key="precipitation", @@ -96,7 +95,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( name="Wind direction", native_unit_of_measurement=DEGREE, icon="mdi:flag-triangle", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), TrafikverketSensorEntityDescription( key="wind_direction_text", @@ -110,7 +109,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( name="Wind speed", native_unit_of_measurement=SPEED_METERS_PER_SECOND, icon="mdi:weather-windy", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), TrafikverketSensorEntityDescription( key="wind_speed_max", @@ -119,7 +118,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( native_unit_of_measurement=SPEED_METERS_PER_SECOND, icon="mdi:weather-windy-variant", entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), TrafikverketSensorEntityDescription( key="humidity", @@ -127,9 +126,9 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( name="Humidity", native_unit_of_measurement=PERCENTAGE, icon="mdi:water-percent", - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), TrafikverketSensorEntityDescription( key="precipitation_amount", @@ -137,7 +136,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( name="Precipitation amount", native_unit_of_measurement=LENGTH_MILLIMETERS, icon="mdi:cup-water", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), TrafikverketSensorEntityDescription( key="precipitation_amountname", From db4721bfbae2de49455cd282658740278b512dd1 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 07:07:10 -0500 Subject: [PATCH 0525/2644] Use enums in trafikverket_train (#62026) --- .../components/trafikverket_train/sensor.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/trafikverket_train/sensor.py b/homeassistant/components/trafikverket_train/sensor.py index d67fef5a0df..08ebb32abcf 100644 --- a/homeassistant/components/trafikverket_train/sensor.py +++ b/homeassistant/components/trafikverket_train/sensor.py @@ -6,14 +6,12 @@ import logging from pytrafikverket import TrafikverketTrain import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import ( - CONF_API_KEY, - CONF_NAME, - CONF_WEEKDAY, - DEVICE_CLASS_TIMESTAMP, - WEEKDAYS, +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorDeviceClass, + SensorEntity, ) +from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_WEEKDAY, WEEKDAYS from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -118,7 +116,7 @@ def next_departuredate(departure): class TrainSensor(SensorEntity): """Contains data about a train depature.""" - _attr_device_class = DEVICE_CLASS_TIMESTAMP + _attr_device_class = SensorDeviceClass.TIMESTAMP def __init__(self, train_api, name, from_station, to_station, weekday, time): """Initialize the sensor.""" From 357d91fb0e84fb9cc949874f8bf400c007303d31 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 07:07:33 -0500 Subject: [PATCH 0526/2644] Use enums in tradfri (#62025) --- homeassistant/components/tradfri/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tradfri/sensor.py b/homeassistant/components/tradfri/sensor.py index 27d2690ce34..5da3179c507 100644 --- a/homeassistant/components/tradfri/sensor.py +++ b/homeassistant/components/tradfri/sensor.py @@ -6,9 +6,9 @@ from typing import Any, cast from pytradfri.command import Command -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE +from homeassistant.const import PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -43,7 +43,7 @@ async def async_setup_entry( class TradfriSensor(TradfriBaseDevice, SensorEntity): """The platform class required by Home Assistant.""" - _attr_device_class = DEVICE_CLASS_BATTERY + _attr_device_class = SensorDeviceClass.BATTERY _attr_native_unit_of_measurement = PERCENTAGE def __init__( From fcda72a33725ce4bce0a0fdb4cffe236e442e1a1 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 07:08:04 -0500 Subject: [PATCH 0527/2644] Use enums in tplink (#62023) --- homeassistant/components/tplink/sensor.py | 28 ++++++++++------------- homeassistant/components/tplink/switch.py | 4 ++-- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/tplink/sensor.py b/homeassistant/components/tplink/sensor.py index 0ffe375e6ff..fe9cb699114 100644 --- a/homeassistant/components/tplink/sensor.py +++ b/homeassistant/components/tplink/sensor.py @@ -7,18 +7,14 @@ from typing import cast from kasa import SmartDevice from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_VOLTAGE, - DEVICE_CLASS_CURRENT, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_VOLTAGE, ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, @@ -51,8 +47,8 @@ ENERGY_SENSORS: tuple[TPLinkSensorEntityDescription, ...] = ( TPLinkSensorEntityDescription( key=ATTR_CURRENT_POWER_W, native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, name="Current Consumption", emeter_attr="power", precision=1, @@ -60,8 +56,8 @@ ENERGY_SENSORS: tuple[TPLinkSensorEntityDescription, ...] = ( TPLinkSensorEntityDescription( key=ATTR_TOTAL_ENERGY_KWH, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, name="Total Consumption", emeter_attr="total", precision=3, @@ -69,16 +65,16 @@ ENERGY_SENSORS: tuple[TPLinkSensorEntityDescription, ...] = ( TPLinkSensorEntityDescription( key=ATTR_TODAY_ENERGY_KWH, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, name="Today's Consumption", precision=3, ), TPLinkSensorEntityDescription( key=ATTR_VOLTAGE, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, name="Voltage", emeter_attr="voltage", precision=1, @@ -86,8 +82,8 @@ ENERGY_SENSORS: tuple[TPLinkSensorEntityDescription, ...] = ( TPLinkSensorEntityDescription( key=ATTR_CURRENT_A, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, name="Current", emeter_attr="current", precision=2, diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index 927765d15f7..eb1094b7675 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -8,8 +8,8 @@ from kasa import SmartDevice from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import legacy_device_id @@ -49,7 +49,7 @@ class SmartPlugLedSwitch(CoordinatedTPLinkEntity, SwitchEntity): coordinator: TPLinkDataUpdateCoordinator - _attr_entity_category = ENTITY_CATEGORY_CONFIG + _attr_entity_category = EntityCategory.CONFIG def __init__( self, device: SmartDevice, coordinator: TPLinkDataUpdateCoordinator From 09892a5c55385d1e026310b427579cb3f3e6380f Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 07:08:37 -0500 Subject: [PATCH 0528/2644] Use enums in totalconnect (#62022) --- .../components/totalconnect/binary_sensor.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/totalconnect/binary_sensor.py b/homeassistant/components/totalconnect/binary_sensor.py index e37482cf1e1..d7b7301fd69 100644 --- a/homeassistant/components/totalconnect/binary_sensor.py +++ b/homeassistant/components/totalconnect/binary_sensor.py @@ -1,10 +1,6 @@ """Interfaces with TotalConnect sensors.""" from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_DOOR, - DEVICE_CLASS_GAS, - DEVICE_CLASS_MOTION, - DEVICE_CLASS_SAFETY, - DEVICE_CLASS_SMOKE, + BinarySensorDeviceClass, BinarySensorEntity, ) @@ -65,17 +61,17 @@ class TotalConnectBinarySensor(BinarySensorEntity): @property def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" + """Return the class of this device, from BinarySensorDeviceClass.""" if self._zone.is_type_security(): - return DEVICE_CLASS_DOOR + return BinarySensorDeviceClass.DOOR if self._zone.is_type_fire(): - return DEVICE_CLASS_SMOKE + return BinarySensorDeviceClass.SMOKE if self._zone.is_type_carbon_monoxide(): - return DEVICE_CLASS_GAS + return BinarySensorDeviceClass.GAS if self._zone.is_type_motion(): - return DEVICE_CLASS_MOTION + return BinarySensorDeviceClass.MOTION if self._zone.is_type_medical(): - return DEVICE_CLASS_SAFETY + return BinarySensorDeviceClass.SAFETY return None @property From 58942601b4f012abc361357f71f0efa63d2ab998 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 07:09:35 -0500 Subject: [PATCH 0529/2644] Use enums in tolo (#62020) --- .../components/tolo/binary_sensor.py | 12 +++++----- homeassistant/components/tolo/button.py | 4 ++-- homeassistant/components/tolo/select.py | 4 ++-- homeassistant/components/tolo/sensor.py | 24 +++++++++---------- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/tolo/binary_sensor.py b/homeassistant/components/tolo/binary_sensor.py index 777e2f16942..7b9bda34674 100644 --- a/homeassistant/components/tolo/binary_sensor.py +++ b/homeassistant/components/tolo/binary_sensor.py @@ -1,12 +1,12 @@ """TOLO Sauna binary sensors.""" from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_OPENING, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ToloSaunaCoordinatorEntity, ToloSaunaUpdateCoordinator @@ -31,9 +31,9 @@ async def async_setup_entry( class ToloFlowInBinarySensor(ToloSaunaCoordinatorEntity, BinarySensorEntity): """Water In Valve Sensor.""" - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_name = "Water In Valve" - _attr_device_class = DEVICE_CLASS_OPENING + _attr_device_class = BinarySensorDeviceClass.OPENING _attr_icon = "mdi:water-plus-outline" def __init__( @@ -53,9 +53,9 @@ class ToloFlowInBinarySensor(ToloSaunaCoordinatorEntity, BinarySensorEntity): class ToloFlowOutBinarySensor(ToloSaunaCoordinatorEntity, BinarySensorEntity): """Water Out Valve Sensor.""" - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_name = "Water Out Valve" - _attr_device_class = DEVICE_CLASS_OPENING + _attr_device_class = BinarySensorDeviceClass.OPENING _attr_icon = "mdi:water-minus-outline" def __init__( diff --git a/homeassistant/components/tolo/button.py b/homeassistant/components/tolo/button.py index 9dd7752b0c9..de3522d96b9 100644 --- a/homeassistant/components/tolo/button.py +++ b/homeassistant/components/tolo/button.py @@ -4,8 +4,8 @@ from tololib.const import LampMode from homeassistant.components.button import ButtonEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ToloSaunaCoordinatorEntity, ToloSaunaUpdateCoordinator @@ -29,7 +29,7 @@ async def async_setup_entry( class ToloLampNextColorButton(ToloSaunaCoordinatorEntity, ButtonEntity): """Button for switching to the next lamp color.""" - _attr_entity_category = ENTITY_CATEGORY_CONFIG + _attr_entity_category = EntityCategory.CONFIG _attr_icon = "mdi:palette" _attr_name = "Next Color" diff --git a/homeassistant/components/tolo/select.py b/homeassistant/components/tolo/select.py index 1dc1f4f6163..a0cdb0ed128 100644 --- a/homeassistant/components/tolo/select.py +++ b/homeassistant/components/tolo/select.py @@ -6,8 +6,8 @@ from tololib.const import LampMode from homeassistant.components.select import SelectEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ToloSaunaCoordinatorEntity, ToloSaunaUpdateCoordinator @@ -28,7 +28,7 @@ class ToloLampModeSelect(ToloSaunaCoordinatorEntity, SelectEntity): """TOLO Sauna lamp mode select.""" _attr_device_class = "tolo__lamp_mode" - _attr_entity_category = ENTITY_CATEGORY_CONFIG + _attr_entity_category = EntityCategory.CONFIG _attr_icon = "mdi:lightbulb-multiple-outline" _attr_name = "Lamp Mode" _attr_options = [lamp_mode.name.lower() for lamp_mode in LampMode] diff --git a/homeassistant/components/tolo/sensor.py b/homeassistant/components/tolo/sensor.py index 23533f68784..bcdb7db0165 100644 --- a/homeassistant/components/tolo/sensor.py +++ b/homeassistant/components/tolo/sensor.py @@ -1,14 +1,14 @@ """TOLO Sauna (non-binary, general) sensors.""" -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - DEVICE_CLASS_TEMPERATURE, - ENTITY_CATEGORY_DIAGNOSTIC, - PERCENTAGE, - TEMP_CELSIUS, +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorStateClass, ) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE, TEMP_CELSIUS from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ToloSaunaCoordinatorEntity, ToloSaunaUpdateCoordinator @@ -33,10 +33,10 @@ async def async_setup_entry( class ToloWaterLevelSensor(ToloSaunaCoordinatorEntity, SensorEntity): """Sensor for tank water level.""" - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_name = "Water Level" _attr_icon = "mdi:waves-arrow-up" - _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_state_class = SensorStateClass.MEASUREMENT _attr_native_unit_of_measurement = PERCENTAGE def __init__( @@ -56,10 +56,10 @@ class ToloWaterLevelSensor(ToloSaunaCoordinatorEntity, SensorEntity): class ToloTankTemperatureSensor(ToloSaunaCoordinatorEntity, SensorEntity): """Sensor for tank temperature.""" - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_name = "Tank Temperature" - _attr_device_class = DEVICE_CLASS_TEMPERATURE - _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_device_class = SensorDeviceClass.TEMPERATURE + _attr_state_class = SensorStateClass.MEASUREMENT _attr_native_unit_of_measurement = TEMP_CELSIUS def __init__( From 116759f2a12e3c506f100ad0b7a4bd5ae59a8ab2 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Thu, 16 Dec 2021 13:24:32 +0100 Subject: [PATCH 0530/2644] Implement DataUpdateCoordinator for Fritz (#60909) * Implement DataUpdateCoordinator for Fritz * mypy * Wrap sync method to async * Apply review comments + final cleanup * CoordinatorEntity --- homeassistant/components/fritz/__init__.py | 23 +---- homeassistant/components/fritz/common.py | 91 ++++++++++--------- homeassistant/components/fritz/const.py | 2 - .../components/fritz/device_tracker.py | 13 +-- homeassistant/components/fritz/switch.py | 22 +---- 5 files changed, 61 insertions(+), 90 deletions(-) diff --git a/homeassistant/components/fritz/__init__.py b/homeassistant/components/fritz/__init__.py index 193e11f49f3..2f53bc540a0 100644 --- a/homeassistant/components/fritz/__init__.py +++ b/homeassistant/components/fritz/__init__.py @@ -5,14 +5,8 @@ from fritzconnection.core.exceptions import FritzConnectionException, FritzSecur from fritzconnection.core.logger import fritzlogger from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_HOST, - CONF_PASSWORD, - CONF_PORT, - CONF_USERNAME, - EVENT_HOMEASSISTANT_STOP, -) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from .common import FritzBoxTools, FritzData @@ -41,8 +35,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) try: - await fritz_tools.async_setup() - await fritz_tools.async_start(entry.options) + await fritz_tools.async_setup(entry.options) except FritzSecurityError as ex: raise ConfigEntryAuthFailed from ex except FritzConnectionException as ex: @@ -54,15 +47,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if DATA_FRITZ not in hass.data: hass.data[DATA_FRITZ] = FritzData() - @callback - def _async_unload(event: Event) -> None: - fritz_tools.async_unload() - - entry.async_on_unload( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_unload) - ) entry.async_on_unload(entry.add_update_listener(update_listener)) + await fritz_tools.async_config_entry_first_refresh() + # Load the other platforms like switch hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -74,7 +62,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload FRITZ!Box Tools config entry.""" fritzbox: FritzBoxTools = hass.data[DOMAIN][entry.entry_id] - fritzbox.async_unload() fritz_data = hass.data[DATA_FRITZ] fritz_data.tracked.pop(fritzbox.unique_id) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 77d21c269bd..4dca2dba4fc 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -12,6 +12,7 @@ from fritzconnection import FritzConnection from fritzconnection.core.exceptions import ( FritzActionError, FritzConnectionException, + FritzSecurityError, FritzServiceError, ) from fritzconnection.lib.fritzhosts import FritzHosts @@ -24,21 +25,21 @@ from homeassistant.components.device_tracker.const import ( ) from homeassistant.components.switch import DOMAIN as DEVICE_SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, ServiceCall, callback +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import update_coordinator from homeassistant.helpers.device_registry import ( CONNECTION_NETWORK_MAC, async_entries_for_config_entry, async_get, ) -from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_registry import ( EntityRegistry, RegistryEntry, async_entries_for_device, ) -from homeassistant.helpers.event import async_track_time_interval from homeassistant.util import dt as dt_util from .const import ( @@ -50,7 +51,6 @@ from .const import ( SERVICE_CLEANUP, SERVICE_REBOOT, SERVICE_RECONNECT, - TRACKER_SCAN_INTERVAL, ) _LOGGER = logging.getLogger(__name__) @@ -105,6 +105,7 @@ class Device: mac: str ip_address: str name: str + wan_access: bool class HostInfo(TypedDict): @@ -116,7 +117,7 @@ class HostInfo(TypedDict): status: bool -class FritzBoxTools: +class FritzBoxTools(update_coordinator.DataUpdateCoordinator): """FrtizBoxTools class.""" def __init__( @@ -128,7 +129,13 @@ class FritzBoxTools: port: int = DEFAULT_PORT, ) -> None: """Initialize FritzboxTools class.""" - self._cancel_scan: CALLBACK_TYPE | None = None + super().__init__( + hass=hass, + logger=_LOGGER, + name=f"{DOMAIN}-{host}-coordinator", + update_interval=timedelta(seconds=30), + ) + self._devices: dict[str, FritzDevice] = {} self._options: MappingProxyType[str, Any] | None = None self._unique_id: str | None = None @@ -146,8 +153,11 @@ class FritzBoxTools: self._latest_firmware: str | None = None self._update_available: bool = False - async def async_setup(self) -> None: + async def async_setup( + self, options: MappingProxyType[str, Any] | None = None + ) -> None: """Wrap up FritzboxTools class setup.""" + self._options = options await self.hass.async_add_executor_job(self.setup) def setup(self) -> None: @@ -175,23 +185,14 @@ class FritzBoxTools: self._update_available, self._latest_firmware = self._update_device_info() - async def async_start(self, options: MappingProxyType[str, Any]) -> None: - """Start FritzHosts connection.""" - self.fritz_hosts = FritzHosts(fc=self.connection) - self._options = options - await self.hass.async_add_executor_job(self.scan_devices) - - self._cancel_scan = async_track_time_interval( - self.hass, self.scan_devices, timedelta(seconds=TRACKER_SCAN_INTERVAL) - ) - @callback - def async_unload(self) -> None: - """Unload FritzboxTools class.""" - _LOGGER.debug("Unloading FRITZ!Box router integration") - if self._cancel_scan is not None: - self._cancel_scan() - self._cancel_scan = None + async def _async_update_data(self) -> None: + """Update FritzboxTools data.""" + try: + self.fritz_hosts = FritzHosts(fc=self.connection) + await self.async_scan_devices() + except (FritzSecurityError, FritzConnectionException) as ex: + raise update_coordinator.UpdateFailed from ex @property def unique_id(self) -> str: @@ -262,6 +263,10 @@ class FritzBoxTools: ) return bool(version), version + async def async_scan_devices(self, now: datetime | None = None) -> None: + """Wrap up FritzboxTools class scan.""" + await self.hass.async_add_executor_job(self.scan_devices, now) + def scan_devices(self, now: datetime | None = None) -> None: """Scan for new devices and return a list of found device ids.""" _LOGGER.debug("Checking devices for FRITZ!Box router %s", self.host) @@ -283,8 +288,17 @@ class FritzBoxTools: dev_name = known_host["name"] dev_ip = known_host["ip"] dev_home = known_host["status"] + dev_wan_access = True + if dev_ip: + wan_access = self.connection.call_action( + "X_AVM-DE_HostFilter:1", + "GetWANAccessByIP", + NewIPv4Address=dev_ip, + ) + if wan_access: + dev_wan_access = not wan_access.get("NewDisallow") - dev_info = Device(dev_mac, dev_ip, dev_name) + dev_info = Device(dev_mac, dev_ip, dev_name, dev_wan_access) if dev_mac in self._devices: self._devices[dev_mac].update(dev_info, dev_home, consider_home) @@ -387,11 +401,12 @@ class FritzData: profile_switches: dict = field(default_factory=dict) -class FritzDeviceBase(Entity): +class FritzDeviceBase(update_coordinator.CoordinatorEntity): """Entity base class for a device connected to a FRITZ!Box router.""" def __init__(self, router: FritzBoxTools, device: FritzDevice) -> None: """Initialize a FRITZ!Box device.""" + super().__init__(router) self._router = router self._mac: str = device.mac_address self._name: str = device.hostname or DEFAULT_DEVICE_NAME @@ -405,8 +420,7 @@ class FritzDeviceBase(Entity): def ip_address(self) -> str | None: """Return the primary ip address of the device.""" if self._mac: - device: FritzDevice = self._router.devices[self._mac] - return device.ip_address + return self._router.devices[self._mac].ip_address return None @property @@ -418,8 +432,7 @@ class FritzDeviceBase(Entity): def hostname(self) -> str | None: """Return hostname of the device.""" if self._mac: - device: FritzDevice = self._router.devices[self._mac] - return device.hostname + return self._router.devices[self._mac].hostname return None @property @@ -451,17 +464,6 @@ class FritzDeviceBase(Entity): await self.async_process_update() self.async_write_ha_state() - async def async_added_to_hass(self) -> None: - """Register state update callback.""" - await self.async_process_update() - self.async_on_remove( - async_dispatcher_connect( - self.hass, - self._router.signal_device_update, - self.async_on_demand_update, - ) - ) - class FritzDevice: """Representation of a device connected to the FRITZ!Box.""" @@ -473,6 +475,7 @@ class FritzDevice: self._ip_address: str | None = None self._last_activity: datetime | None = None self._connected = False + self._wan_access = False def update(self, dev_info: Device, dev_home: bool, consider_home: float) -> None: """Update device info.""" @@ -494,6 +497,7 @@ class FritzDevice: self._last_activity = utc_point_in_time self._ip_address = dev_info.ip_address + self._wan_access = dev_info.wan_access @property def is_connected(self) -> bool: @@ -520,6 +524,11 @@ class FritzDevice: """Return device last activity.""" return self._last_activity + @property + def wan_access(self) -> bool: + """Return device wan access.""" + return self._wan_access + class SwitchInfo(TypedDict): """FRITZ!Box switch info class.""" diff --git a/homeassistant/components/fritz/const.py b/homeassistant/components/fritz/const.py index 4786ac9097c..0196a43ec4e 100644 --- a/homeassistant/components/fritz/const.py +++ b/homeassistant/components/fritz/const.py @@ -35,6 +35,4 @@ SWITCH_TYPE_DEFLECTION = "CallDeflection" SWITCH_TYPE_PORTFORWARD = "PortForward" SWITCH_TYPE_WIFINETWORK = "WiFiNetwork" -TRACKER_SCAN_INTERVAL = 30 - UPTIME_DEVIATION = 5 diff --git a/homeassistant/components/fritz/device_tracker.py b/homeassistant/components/fritz/device_tracker.py index 9483d8163e0..4b8169b4db8 100644 --- a/homeassistant/components/fritz/device_tracker.py +++ b/homeassistant/components/fritz/device_tracker.py @@ -119,12 +119,11 @@ class FritzBoxTracker(FritzDeviceBase, ScannerEntity): """Initialize a FRITZ!Box device.""" super().__init__(router, device) self._last_activity: datetime.datetime | None = device.last_activity - self._active = False @property def is_connected(self) -> bool: """Return device status.""" - return self._active + return self._router.devices[self._mac].is_connected @property def unique_id(self) -> str: @@ -142,6 +141,7 @@ class FritzBoxTracker(FritzDeviceBase, ScannerEntity): def extra_state_attributes(self) -> dict[str, str]: """Return the attributes.""" attrs: dict[str, str] = {} + self._last_activity = self._router.devices[self._mac].last_activity if self._last_activity is not None: attrs["last_time_reachable"] = self._last_activity.isoformat( timespec="seconds" @@ -152,12 +152,3 @@ class FritzBoxTracker(FritzDeviceBase, ScannerEntity): def source_type(self) -> str: """Return tracker source type.""" return SOURCE_TYPE_ROUTER - - async def async_process_update(self) -> None: - """Update device.""" - if not self._mac: - return - - device = self._router.devices[self._mac] - self._active = device.is_connected - self._last_activity = device.last_activity diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index c8341710d47..e9cbd80b133 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -595,23 +595,10 @@ class FritzBoxProfileSwitch(FritzDeviceBase, SwitchEntity): self._attr_unique_id = f"{self._mac}_internet_access" self._attr_entity_category = EntityCategory.CONFIG - async def async_process_update(self) -> None: - """Update device.""" - if not self._mac or not self.ip_address: - return - - wan_disable_info = await async_service_call_action( - self._router, - "X_AVM-DE_HostFilter", - "1", - "GetWANAccessByIP", - NewIPv4Address=self.ip_address, - ) - - if wan_disable_info is None: - return - - self._attr_is_on = not wan_disable_info["NewDisallow"] + @property + def is_on(self) -> bool: + """Switch status.""" + return self._router.devices[self._mac].wan_access async def async_turn_on(self, **kwargs: Any) -> None: """Turn on switch.""" @@ -624,7 +611,6 @@ class FritzBoxProfileSwitch(FritzDeviceBase, SwitchEntity): async def _async_handle_turn_on_off(self, turn_on: bool) -> bool: """Handle switch state change request.""" await self._async_switch_on_off(turn_on) - self._attr_is_on = turn_on self.async_write_ha_state() return True From 105ad861bd231b5a76b8db665d857699c92f4ec6 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Thu, 16 Dec 2021 13:25:06 +0100 Subject: [PATCH 0531/2644] Add buttons and deprecate services for Fritz (#61483) * Add buttons and deprecate services * Exclude tests * Log full service name --- .coveragerc | 1 + homeassistant/components/fritz/button.py | 100 +++++++++++++++++++++++ homeassistant/components/fritz/common.py | 27 +++++- homeassistant/components/fritz/const.py | 1 + 4 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/fritz/button.py diff --git a/.coveragerc b/.coveragerc index a1250c86bda..7a13c433b83 100644 --- a/.coveragerc +++ b/.coveragerc @@ -371,6 +371,7 @@ omit = homeassistant/components/freebox/switch.py homeassistant/components/fritz/__init__.py homeassistant/components/fritz/binary_sensor.py + homeassistant/components/fritz/button.py homeassistant/components/fritz/common.py homeassistant/components/fritz/const.py homeassistant/components/fritz/device_tracker.py diff --git a/homeassistant/components/fritz/button.py b/homeassistant/components/fritz/button.py new file mode 100644 index 00000000000..ea807545323 --- /dev/null +++ b/homeassistant/components/fritz/button.py @@ -0,0 +1,100 @@ +"""Switches for AVM Fritz!Box buttons.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +import logging +from typing import Final + +from homeassistant.components.button import ( + ButtonDeviceClass, + ButtonEntity, + ButtonEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_CONFIG +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import slugify + +from .common import FritzBoxTools +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +@dataclass +class FritzButtonDescriptionMixin: + """Mixin to describe a Button entity.""" + + press_action: Callable + + +@dataclass +class FritzButtonDescription(ButtonEntityDescription, FritzButtonDescriptionMixin): + """Class to describe a Button entity.""" + + +BUTTONS: Final = [ + FritzButtonDescription( + key="firmware_update", + name="Firmware Update", + device_class=ButtonDeviceClass.UPDATE, + entity_category=ENTITY_CATEGORY_CONFIG, + press_action=lambda router: router.async_trigger_firmware_update(), + ), + FritzButtonDescription( + key="reboot", + name="Reboot", + device_class=ButtonDeviceClass.RESTART, + entity_category=ENTITY_CATEGORY_CONFIG, + press_action=lambda router: router.async_trigger_reboot(), + ), + FritzButtonDescription( + key="reconnect", + name="Reconnect", + device_class=ButtonDeviceClass.RESTART, + entity_category=ENTITY_CATEGORY_CONFIG, + press_action=lambda router: router.async_trigger_reconnect(), + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set buttons for device.""" + _LOGGER.debug("Setting up buttons") + router: FritzBoxTools = hass.data[DOMAIN][entry.entry_id] + + async_add_entities([FritzButton(router, entry.title, button) for button in BUTTONS]) + + +class FritzButton(ButtonEntity): + """Defines a Fritz!Box base button.""" + + entity_description: FritzButtonDescription + + def __init__( + self, + router: FritzBoxTools, + device_friendly_name: str, + description: FritzButtonDescription, + ) -> None: + """Initialize Fritz!Box button.""" + self.entity_description = description + self.router = router + + self._attr_name = f"{device_friendly_name} {description.name}" + self._attr_unique_id = slugify(self._attr_name) + self._attr_device_info = DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, router.mac)} + ) + + async def async_press(self) -> None: + """Triggers Fritz!Box service.""" + await self.entity_description.press_action(self.router) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 4dca2dba4fc..6e5d89e58bd 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -6,7 +6,7 @@ from dataclasses import dataclass, field from datetime import datetime, timedelta import logging from types import MappingProxyType -from typing import Any, TypedDict +from typing import Any, TypedDict, cast from fritzconnection import FritzConnection from fritzconnection.core.exceptions import ( @@ -315,6 +315,25 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator): _LOGGER.debug("Checking host info for FRITZ!Box router %s", self.host) self._update_available, self._latest_firmware = self._update_device_info() + async def async_trigger_firmware_update(self) -> bool: + """Trigger firmware update.""" + results = await self.hass.async_add_executor_job( + self.connection.call_action, "UserInterface:1", "X_AVM-DE_DoUpdate" + ) + return cast(bool, results["NewX_AVM-DE_UpdateState"]) + + async def async_trigger_reboot(self) -> None: + """Trigger device reboot.""" + await self.hass.async_add_executor_job( + self.connection.call_action, "DeviceConfig1", "Reboot" + ) + + async def async_trigger_reconnect(self) -> None: + """Trigger device reconnect.""" + await self.hass.async_add_executor_job( + self.connection.call_action, "WANIPConn1", "ForceTermination" + ) + async def service_fritzbox( self, service_call: ServiceCall, config_entry: ConfigEntry ) -> None: @@ -326,12 +345,18 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator): try: if service_call.service == SERVICE_REBOOT: + _LOGGER.warning( + 'Service "fritz.reboot" is deprecated, please use the corresponding button entity instead' + ) await self.hass.async_add_executor_job( self.connection.call_action, "DeviceConfig1", "Reboot" ) return if service_call.service == SERVICE_RECONNECT: + _LOGGER.warning( + 'Service "fritz.reconnect" is deprecated, please use the corresponding button entity instead' + ) await self.hass.async_add_executor_job( self.connection.call_action, "WANIPConn1", diff --git a/homeassistant/components/fritz/const.py b/homeassistant/components/fritz/const.py index 0196a43ec4e..7bf65a8566d 100644 --- a/homeassistant/components/fritz/const.py +++ b/homeassistant/components/fritz/const.py @@ -7,6 +7,7 @@ from homeassistant.const import Platform DOMAIN = "fritz" PLATFORMS = [ + Platform.BUTTON, Platform.BINARY_SENSOR, Platform.DEVICE_TRACKER, Platform.SENSOR, From 550004f109b9a2838e5e00dfafb05168d5e819fd Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 16 Dec 2021 13:59:58 +0100 Subject: [PATCH 0532/2644] Fix mfi tests (#61904) Co-authored-by: epenet --- tests/components/mfi/test_sensor.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/components/mfi/test_sensor.py b/tests/components/mfi/test_sensor.py index 4032e29b743..17d8df958ef 100644 --- a/tests/components/mfi/test_sensor.py +++ b/tests/components/mfi/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the mFi sensor platform.""" +from copy import deepcopy import unittest.mock as mock from mficlient.client import FailedToLogin @@ -38,20 +39,20 @@ async def test_setup_failed_login(hass): """Test setup with login failure.""" with mock.patch("homeassistant.components.mfi.sensor.MFiClient") as mock_client: mock_client.side_effect = FailedToLogin - assert not PLATFORM.setup_platform(hass, dict(GOOD_CONFIG), None) + assert not PLATFORM.setup_platform(hass, GOOD_CONFIG, None) async def test_setup_failed_connect(hass): """Test setup with connection failure.""" with mock.patch("homeassistant.components.mfi.sensor.MFiClient") as mock_client: mock_client.side_effect = requests.exceptions.ConnectionError - assert not PLATFORM.setup_platform(hass, dict(GOOD_CONFIG), None) + assert not PLATFORM.setup_platform(hass, GOOD_CONFIG, None) async def test_setup_minimum(hass): """Test setup with minimum configuration.""" with mock.patch("homeassistant.components.mfi.sensor.MFiClient") as mock_client: - config = dict(GOOD_CONFIG) + config = deepcopy(GOOD_CONFIG) del config[THING]["port"] assert await async_setup_component(hass, COMPONENT.DOMAIN, config) await hass.async_block_till_done() @@ -64,9 +65,7 @@ async def test_setup_minimum(hass): async def test_setup_with_port(hass): """Test setup with port.""" with mock.patch("homeassistant.components.mfi.sensor.MFiClient") as mock_client: - config = dict(GOOD_CONFIG) - config[THING]["port"] = 6123 - assert await async_setup_component(hass, COMPONENT.DOMAIN, config) + assert await async_setup_component(hass, COMPONENT.DOMAIN, GOOD_CONFIG) await hass.async_block_till_done() assert mock_client.call_count == 1 assert mock_client.call_args == mock.call( @@ -77,7 +76,7 @@ async def test_setup_with_port(hass): async def test_setup_with_tls_disabled(hass): """Test setup without TLS.""" with mock.patch("homeassistant.components.mfi.sensor.MFiClient") as mock_client: - config = dict(GOOD_CONFIG) + config = deepcopy(GOOD_CONFIG) del config[THING]["port"] config[THING]["ssl"] = False config[THING]["verify_ssl"] = False From 7f823f72116596d94d8af17b8189eacf1d80006d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 16 Dec 2021 14:06:38 +0100 Subject: [PATCH 0533/2644] Fix OwnetError preventing onewire initialisation (#61696) Co-authored-by: epenet --- homeassistant/components/onewire/__init__.py | 7 ++++++- tests/components/onewire/test_init.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/onewire/__init__.py b/homeassistant/components/onewire/__init__.py index 753d30e5958..70a0a5fc856 100644 --- a/homeassistant/components/onewire/__init__.py +++ b/homeassistant/components/onewire/__init__.py @@ -1,6 +1,8 @@ """The 1-Wire component.""" import logging +from pyownet import protocol + from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -18,7 +20,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: onewirehub = OneWireHub(hass) try: await onewirehub.initialize(entry) - except CannotConnect as exc: + except ( + CannotConnect, # Failed to connect to the server + protocol.OwnetError, # Connected to server, but failed to list the devices + ) as exc: raise ConfigEntryNotReady() from exc hass.data[DOMAIN][entry.entry_id] = onewirehub diff --git a/tests/components/onewire/test_init.py b/tests/components/onewire/test_init.py index e3a3fdcc564..763cdc9c071 100644 --- a/tests/components/onewire/test_init.py +++ b/tests/components/onewire/test_init.py @@ -1,6 +1,8 @@ """Tests for 1-Wire config flow.""" import logging +from unittest.mock import MagicMock +from pyownet import protocol import pytest from homeassistant.components.onewire.const import DOMAIN @@ -19,6 +21,20 @@ async def test_owserver_connect_failure(hass: HomeAssistant, config_entry: Confi assert not hass.data.get(DOMAIN) +async def test_owserver_listing_failure( + hass: HomeAssistant, config_entry: ConfigEntry, owproxy: MagicMock +): + """Test listing failure raises ConfigEntryNotReady.""" + owproxy.return_value.dir.side_effect = protocol.OwnetError() + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert config_entry.state is ConfigEntryState.SETUP_RETRY + assert not hass.data.get(DOMAIN) + + @pytest.mark.usefixtures("owproxy") async def test_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): """Test being able to unload an entry.""" From db6b472e7a15f10467966cb1267babd86e89adfc Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Thu, 16 Dec 2021 14:08:40 +0100 Subject: [PATCH 0534/2644] Add restore logic to Shelly climate platform (#61632) * Add restore logic to Shelly climate platform * Handle missing channel on restore --- homeassistant/components/shelly/__init__.py | 2 +- homeassistant/components/shelly/climate.py | 198 +++++++++++++++++--- 2 files changed, 172 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index cead5baf006..8d78c148b51 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -69,7 +69,6 @@ from .utils import ( BLOCK_PLATFORMS: Final = [ Platform.BINARY_SENSOR, Platform.BUTTON, - Platform.CLIMATE, Platform.COVER, Platform.LIGHT, Platform.SENSOR, @@ -77,6 +76,7 @@ BLOCK_PLATFORMS: Final = [ ] BLOCK_SLEEPING_PLATFORMS: Final = [ Platform.BINARY_SENSOR, + Platform.CLIMATE, Platform.SENSOR, ] RPC_PLATFORMS: Final = [ diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index 79d233336e3..cdbff59fa02 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -3,12 +3,13 @@ from __future__ import annotations import asyncio import logging +from types import MappingProxyType from typing import Any, Final, cast from aioshelly.block_device import Block import async_timeout -from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN, ClimateEntity from homeassistant.components.climate.const import ( CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, @@ -21,7 +22,9 @@ from homeassistant.components.climate.const import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, State, callback +from homeassistant.helpers import device_registry, entity, entity_registry +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity @@ -33,7 +36,6 @@ from .const import ( DOMAIN, SHTRV_01_TEMPERATURE_SETTINGS, ) -from .entity import ShellyBlockEntity from .utils import get_device_entry_gen _LOGGER: Final = logging.getLogger(__name__) @@ -49,10 +51,30 @@ async def async_setup_entry( if get_device_entry_gen(config_entry) == 2: return + wrapper: BlockDeviceWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][ + config_entry.entry_id + ][BLOCK] + + if wrapper.device.initialized: + await async_setup_climate_entities(async_add_entities, wrapper) + else: + await async_restore_climate_entities( + hass, config_entry, async_add_entities, wrapper + ) + + +async def async_setup_climate_entities( + async_add_entities: AddEntitiesCallback, + wrapper: BlockDeviceWrapper, +) -> None: + """Set up online climate devices.""" + + _LOGGER.info("Setup online climate device %s", wrapper.name) device_block: Block | None = None sensor_block: Block | None = None - wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][BLOCK] + assert wrapper.device.blocks + for block in wrapper.device.blocks: if block.type == "device": device_block = block @@ -60,10 +82,37 @@ async def async_setup_entry( sensor_block = block if sensor_block and device_block: - async_add_entities([ShellyClimate(wrapper, sensor_block, device_block)]) + async_add_entities([BlockSleepingClimate(wrapper, sensor_block, device_block)]) -class ShellyClimate(ShellyBlockEntity, RestoreEntity, ClimateEntity): +async def async_restore_climate_entities( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + wrapper: BlockDeviceWrapper, +) -> None: + """Restore sleeping climate devices.""" + _LOGGER.info("Setup sleeping climate device %s", wrapper.name) + + ent_reg = await entity_registry.async_get_registry(hass) + entries = entity_registry.async_entries_for_config_entry( + ent_reg, config_entry.entry_id + ) + + for entry in entries: + + if entry.domain != CLIMATE_DOMAIN: + continue + + _LOGGER.debug("Found entry %s [%s]", entry.original_name, entry.domain) + async_add_entities([BlockSleepingClimate(wrapper, None, None, entry)]) + + +class BlockSleepingClimate( + RestoreEntity, + ClimateEntity, + entity.Entity, +): """Representation of a Shelly climate device.""" _attr_hvac_modes = [HVAC_MODE_OFF, HVAC_MODE_HEAT] @@ -74,45 +123,77 @@ class ShellyClimate(ShellyBlockEntity, RestoreEntity, ClimateEntity): _attr_target_temperature_step = SHTRV_01_TEMPERATURE_SETTINGS["step"] _attr_temperature_unit = TEMP_CELSIUS + # pylint: disable=super-init-not-called def __init__( - self, wrapper: BlockDeviceWrapper, sensor_block: Block, device_block: Block + self, + wrapper: BlockDeviceWrapper, + sensor_block: Block | None, + device_block: Block | None, + entry: entity_registry.RegistryEntry | None = None, ) -> None: """Initialize climate.""" - super().__init__(wrapper, sensor_block) - - self.device_block = device_block - - assert self.block.channel + self.wrapper = wrapper + self.block: Block | None = sensor_block self.control_result: dict[str, Any] | None = None + self.device_block: Block | None = device_block + self.last_state: State | None = None + self.last_state_attributes: MappingProxyType[str, Any] + self._preset_modes: list[str] = [] - self._attr_name = self.wrapper.name - self._attr_unique_id = self.wrapper.mac - self._attr_preset_modes: list[str] = [ - PRESET_NONE, - *wrapper.device.settings["thermostats"][int(self.block.channel)][ - "schedule_profile_names" - ], - ] + if self.block is not None and self.device_block is not None: + self._unique_id = f"{self.wrapper.mac}-{self.block.description}" + assert self.block.channel + self._preset_modes = [ + PRESET_NONE, + *wrapper.device.settings["thermostats"][int(self.block.channel)][ + "schedule_profile_names" + ], + ] + elif entry is not None: + self._unique_id = entry.unique_id + + @property + def unique_id(self) -> str: + """Set unique id of entity.""" + return self._unique_id + + @property + def name(self) -> str: + """Name of entity.""" + return self.wrapper.name + + @property + def should_poll(self) -> bool: + """If device should be polled.""" + return False @property def target_temperature(self) -> float | None: """Set target temperature.""" - return cast(float, self.block.targetTemp) + if self.block is not None: + return cast(float, self.block.targetTemp) + return self.last_state_attributes.get("temperature") @property def current_temperature(self) -> float | None: """Return current temperature.""" - return cast(float, self.block.temp) + if self.block is not None: + return cast(float, self.block.temp) + return self.last_state_attributes.get("current_temperature") @property def available(self) -> bool: """Device availability.""" - return not cast(bool, self.device_block.valveError) + if self.device_block is not None: + return not cast(bool, self.device_block.valveError) + return True @property def hvac_mode(self) -> str: """HVAC current mode.""" + if self.device_block is None: + return self.last_state.state if self.last_state else HVAC_MODE_OFF if self.device_block.mode is None or self._check_is_off(): return HVAC_MODE_OFF @@ -121,20 +202,45 @@ class ShellyClimate(ShellyBlockEntity, RestoreEntity, ClimateEntity): @property def preset_mode(self) -> str | None: """Preset current mode.""" + if self.device_block is None: + return self.last_state_attributes.get("preset_mode") if self.device_block.mode is None: - return None - return self._attr_preset_modes[cast(int, self.device_block.mode)] + return PRESET_NONE + return self._preset_modes[cast(int, self.device_block.mode)] @property def hvac_action(self) -> str | None: """HVAC current action.""" - if self.device_block.status is None or self._check_is_off(): + if ( + self.device_block is None + or self.device_block.status is None + or self._check_is_off() + ): return CURRENT_HVAC_OFF return ( CURRENT_HVAC_IDLE if self.device_block.status == "0" else CURRENT_HVAC_HEAT ) + @property + def preset_modes(self) -> list[str]: + """Preset available modes.""" + return self._preset_modes + + @property + def device_info(self) -> DeviceInfo: + """Device info.""" + return { + "connections": {(device_registry.CONNECTION_NETWORK_MAC, self.wrapper.mac)} + } + + @property + def channel(self) -> str | None: + """Device channel.""" + if self.block is not None: + return self.block.channel + return self.last_state_attributes.get("channel") + def _check_is_off(self) -> bool: """Return if valve is off or on.""" return bool( @@ -148,7 +254,7 @@ class ShellyClimate(ShellyBlockEntity, RestoreEntity, ClimateEntity): try: async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): return await self.wrapper.device.http_request( - "get", f"thermostat/{self.block.channel}", kwargs + "get", f"thermostat/{self.channel}", kwargs ) except (asyncio.TimeoutError, OSError) as err: _LOGGER.error( @@ -186,3 +292,41 @@ class ShellyClimate(ShellyBlockEntity, RestoreEntity, ClimateEntity): await self.set_state_full_path( schedule=1, schedule_profile=f"{preset_index}" ) + + async def async_added_to_hass(self) -> None: + """Handle entity which will be added.""" + _LOGGER.info("Restoring entity %s", self.name) + + last_state = await self.async_get_last_state() + + if last_state is not None: + self.last_state = last_state + self.last_state_attributes = self.last_state.attributes + self._preset_modes = cast( + list, self.last_state.attributes.get("preset_modes") + ) + + self.async_on_remove(self.wrapper.async_add_listener(self._update_callback)) + + async def async_update(self) -> None: + """Update entity with latest info.""" + await self.wrapper.async_request_refresh() + + @callback + def _update_callback(self) -> None: + """Handle device update.""" + if not self.wrapper.device.initialized: + self.async_write_ha_state() + return + + assert self.wrapper.device.blocks + + for block in self.wrapper.device.blocks: + if block.type == "device": + self.device_block = block + if hasattr(block, "targetTemp"): + self.block = block + + _LOGGER.debug("Entity %s attached to block", self.name) + self.async_write_ha_state() + return From 0cf0104662dfc90abf85c3e64736f192bad34bc2 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 08:12:23 -0500 Subject: [PATCH 0535/2644] Use enums in shelly (#62035) --- .../components/shelly/binary_sensor.py | 67 ++++----- homeassistant/components/shelly/button.py | 9 +- homeassistant/components/shelly/cover.py | 4 +- homeassistant/components/shelly/sensor.py | 139 +++++++++--------- 4 files changed, 106 insertions(+), 113 deletions(-) diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index 737efad923c..069e6a9839c 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -4,22 +4,13 @@ from __future__ import annotations from typing import Final, cast from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_CONNECTIVITY, - DEVICE_CLASS_GAS, - DEVICE_CLASS_MOISTURE, - DEVICE_CLASS_MOTION, - DEVICE_CLASS_OPENING, - DEVICE_CLASS_POWER, - DEVICE_CLASS_PROBLEM, - DEVICE_CLASS_SMOKE, - DEVICE_CLASS_UPDATE, - DEVICE_CLASS_VIBRATION, STATE_ON, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import CONF_SLEEP_PERIOD @@ -44,69 +35,69 @@ from .utils import ( SENSORS: Final = { ("device", "overtemp"): BlockAttributeDescription( name="Overheating", - device_class=DEVICE_CLASS_PROBLEM, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, ), ("device", "overpower"): BlockAttributeDescription( name="Overpowering", - device_class=DEVICE_CLASS_PROBLEM, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, ), ("light", "overpower"): BlockAttributeDescription( name="Overpowering", - device_class=DEVICE_CLASS_PROBLEM, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, ), ("relay", "overpower"): BlockAttributeDescription( name="Overpowering", - device_class=DEVICE_CLASS_PROBLEM, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, ), ("sensor", "dwIsOpened"): BlockAttributeDescription( name="Door", - device_class=DEVICE_CLASS_OPENING, + device_class=BinarySensorDeviceClass.OPENING, available=lambda block: cast(int, block.dwIsOpened) != -1, ), ("sensor", "flood"): BlockAttributeDescription( - name="Flood", device_class=DEVICE_CLASS_MOISTURE + name="Flood", device_class=BinarySensorDeviceClass.MOISTURE ), ("sensor", "gas"): BlockAttributeDescription( name="Gas", - device_class=DEVICE_CLASS_GAS, + device_class=BinarySensorDeviceClass.GAS, value=lambda value: value in ["mild", "heavy"], extra_state_attributes=lambda block: {"detected": block.gas}, ), ("sensor", "smoke"): BlockAttributeDescription( - name="Smoke", device_class=DEVICE_CLASS_SMOKE + name="Smoke", device_class=BinarySensorDeviceClass.SMOKE ), ("sensor", "vibration"): BlockAttributeDescription( - name="Vibration", device_class=DEVICE_CLASS_VIBRATION + name="Vibration", device_class=BinarySensorDeviceClass.VIBRATION ), ("input", "input"): BlockAttributeDescription( name="Input", - device_class=DEVICE_CLASS_POWER, + device_class=BinarySensorDeviceClass.POWER, default_enabled=False, removal_condition=is_block_momentary_input, ), ("relay", "input"): BlockAttributeDescription( name="Input", - device_class=DEVICE_CLASS_POWER, + device_class=BinarySensorDeviceClass.POWER, default_enabled=False, removal_condition=is_block_momentary_input, ), ("device", "input"): BlockAttributeDescription( name="Input", - device_class=DEVICE_CLASS_POWER, + device_class=BinarySensorDeviceClass.POWER, default_enabled=False, removal_condition=is_block_momentary_input, ), ("sensor", "extInput"): BlockAttributeDescription( name="External Input", - device_class=DEVICE_CLASS_POWER, + device_class=BinarySensorDeviceClass.POWER, default_enabled=False, ), ("sensor", "motion"): BlockAttributeDescription( - name="Motion", device_class=DEVICE_CLASS_MOTION + name="Motion", device_class=BinarySensorDeviceClass.MOTION ), } @@ -114,13 +105,13 @@ REST_SENSORS: Final = { "cloud": RestAttributeDescription( name="Cloud", value=lambda status, _: status["cloud"]["connected"], - device_class=DEVICE_CLASS_CONNECTIVITY, + device_class=BinarySensorDeviceClass.CONNECTIVITY, default_enabled=False, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), "fwupdate": RestAttributeDescription( name="Firmware Update", - device_class=DEVICE_CLASS_UPDATE, + device_class=BinarySensorDeviceClass.UPDATE, value=lambda status, _: status["update"]["has_update"], default_enabled=False, extra_state_attributes=lambda status: { @@ -128,7 +119,7 @@ REST_SENSORS: Final = { "installed_version": status["update"]["old_version"], "beta_version": status["update"].get("beta_version", ""), }, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), } @@ -137,7 +128,7 @@ RPC_SENSORS: Final = { key="input", sub_key="state", name="Input", - device_class=DEVICE_CLASS_POWER, + device_class=BinarySensorDeviceClass.POWER, default_enabled=False, removal_condition=is_rpc_momentary_input, ), @@ -145,22 +136,22 @@ RPC_SENSORS: Final = { key="cloud", sub_key="connected", name="Cloud", - device_class=DEVICE_CLASS_CONNECTIVITY, + device_class=BinarySensorDeviceClass.CONNECTIVITY, default_enabled=False, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), "fwupdate": RpcAttributeDescription( key="sys", sub_key="available_updates", name="Firmware Update", - device_class=DEVICE_CLASS_UPDATE, + device_class=BinarySensorDeviceClass.UPDATE, default_enabled=False, extra_state_attributes=lambda status, shelly: { "latest_stable_version": status.get("stable", {"version": ""})["version"], "installed_version": shelly["ver"], "beta_version": status.get("beta", {"version": ""})["version"], }, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), } diff --git a/homeassistant/components/shelly/button.py b/homeassistant/components/shelly/button.py index ec308814fd8..84c86f3fe91 100644 --- a/homeassistant/components/shelly/button.py +++ b/homeassistant/components/shelly/button.py @@ -11,10 +11,9 @@ from homeassistant.components.button import ( ButtonEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify @@ -40,7 +39,7 @@ BUTTONS: Final = [ key="ota_update", name="OTA Update", device_class=ButtonDeviceClass.UPDATE, - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, press_action=lambda wrapper: wrapper.async_trigger_ota_update(), ), ShellyButtonDescription( @@ -48,14 +47,14 @@ BUTTONS: Final = [ name="OTA Update Beta", device_class=ButtonDeviceClass.UPDATE, entity_registry_enabled_default=False, - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, press_action=lambda wrapper: wrapper.async_trigger_ota_update(beta=True), ), ShellyButtonDescription( key="reboot", name="Reboot", device_class=ButtonDeviceClass.RESTART, - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, press_action=lambda wrapper: wrapper.device.trigger_reboot(), ), ] diff --git a/homeassistant/components/shelly/cover.py b/homeassistant/components/shelly/cover.py index 47166ff2dbd..78aa7606aac 100644 --- a/homeassistant/components/shelly/cover.py +++ b/homeassistant/components/shelly/cover.py @@ -7,11 +7,11 @@ from aioshelly.block_device import Block from homeassistant.components.cover import ( ATTR_POSITION, - DEVICE_CLASS_SHUTTER, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION, SUPPORT_STOP, + CoverDeviceClass, CoverEntity, ) from homeassistant.config_entries import ConfigEntry @@ -41,7 +41,7 @@ async def async_setup_entry( class ShellyCover(ShellyBlockEntity, CoverEntity): """Switch that controls a cover block on Shelly devices.""" - _attr_device_class = DEVICE_CLASS_SHUTTER + _attr_device_class = CoverDeviceClass.SHUTTER def __init__(self, wrapper: BlockDeviceWrapper, block: Block) -> None: """Initialize light.""" diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 7fcf456b658..bccb538bf9d 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -3,8 +3,11 @@ from __future__ import annotations from typing import Final, cast -from homeassistant.components import sensor -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, @@ -12,7 +15,6 @@ from homeassistant.const import ( ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, - ENTITY_CATEGORY_DIAGNOSTIC, LIGHT_LUX, PERCENTAGE, POWER_WATT, @@ -20,6 +22,7 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -42,163 +45,163 @@ SENSORS: Final = { ("device", "battery"): BlockAttributeDescription( name="Battery", unit=PERCENTAGE, - device_class=sensor.DEVICE_CLASS_BATTERY, - state_class=sensor.STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, removal_condition=lambda settings, _: settings.get("external_power") == 1, available=lambda block: cast(int, block.battery) != -1, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), ("device", "deviceTemp"): BlockAttributeDescription( name="Device Temperature", unit=temperature_unit, value=lambda value: round(value, 1), - device_class=sensor.DEVICE_CLASS_TEMPERATURE, - state_class=sensor.STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, default_enabled=False, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), ("emeter", "current"): BlockAttributeDescription( name="Current", unit=ELECTRIC_CURRENT_AMPERE, value=lambda value: value, - device_class=sensor.DEVICE_CLASS_CURRENT, - state_class=sensor.STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, ), ("light", "power"): BlockAttributeDescription( name="Power", unit=POWER_WATT, value=lambda value: round(value, 1), - device_class=sensor.DEVICE_CLASS_POWER, - state_class=sensor.STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, default_enabled=False, ), ("device", "power"): BlockAttributeDescription( name="Power", unit=POWER_WATT, value=lambda value: round(value, 1), - device_class=sensor.DEVICE_CLASS_POWER, - state_class=sensor.STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), ("emeter", "power"): BlockAttributeDescription( name="Power", unit=POWER_WATT, value=lambda value: round(value, 1), - device_class=sensor.DEVICE_CLASS_POWER, - state_class=sensor.STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), ("device", "voltage"): BlockAttributeDescription( name="Voltage", unit=ELECTRIC_POTENTIAL_VOLT, value=lambda value: round(value, 1), - device_class=sensor.DEVICE_CLASS_VOLTAGE, - state_class=sensor.STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, default_enabled=False, ), ("emeter", "voltage"): BlockAttributeDescription( name="Voltage", unit=ELECTRIC_POTENTIAL_VOLT, value=lambda value: round(value, 1), - device_class=sensor.DEVICE_CLASS_VOLTAGE, - state_class=sensor.STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, ), ("emeter", "powerFactor"): BlockAttributeDescription( name="Power Factor", unit=PERCENTAGE, value=lambda value: round(value * 100, 1), - device_class=sensor.DEVICE_CLASS_POWER_FACTOR, - state_class=sensor.STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER_FACTOR, + state_class=SensorStateClass.MEASUREMENT, ), ("relay", "power"): BlockAttributeDescription( name="Power", unit=POWER_WATT, value=lambda value: round(value, 1), - device_class=sensor.DEVICE_CLASS_POWER, - state_class=sensor.STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), ("roller", "rollerPower"): BlockAttributeDescription( name="Power", unit=POWER_WATT, value=lambda value: round(value, 1), - device_class=sensor.DEVICE_CLASS_POWER, - state_class=sensor.STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), ("device", "energy"): BlockAttributeDescription( name="Energy", unit=ENERGY_KILO_WATT_HOUR, value=lambda value: round(value / 60 / 1000, 2), - device_class=sensor.DEVICE_CLASS_ENERGY, - state_class=sensor.STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), ("emeter", "energy"): BlockAttributeDescription( name="Energy", unit=ENERGY_KILO_WATT_HOUR, value=lambda value: round(value / 1000, 2), - device_class=sensor.DEVICE_CLASS_ENERGY, - state_class=sensor.STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), ("emeter", "energyReturned"): BlockAttributeDescription( name="Energy Returned", unit=ENERGY_KILO_WATT_HOUR, value=lambda value: round(value / 1000, 2), - device_class=sensor.DEVICE_CLASS_ENERGY, - state_class=sensor.STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), ("light", "energy"): BlockAttributeDescription( name="Energy", unit=ENERGY_KILO_WATT_HOUR, value=lambda value: round(value / 60 / 1000, 2), - device_class=sensor.DEVICE_CLASS_ENERGY, - state_class=sensor.STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, default_enabled=False, ), ("relay", "energy"): BlockAttributeDescription( name="Energy", unit=ENERGY_KILO_WATT_HOUR, value=lambda value: round(value / 60 / 1000, 2), - device_class=sensor.DEVICE_CLASS_ENERGY, - state_class=sensor.STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), ("roller", "rollerEnergy"): BlockAttributeDescription( name="Energy", unit=ENERGY_KILO_WATT_HOUR, value=lambda value: round(value / 60 / 1000, 2), - device_class=sensor.DEVICE_CLASS_ENERGY, - state_class=sensor.STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), ("sensor", "concentration"): BlockAttributeDescription( name="Gas Concentration", unit=CONCENTRATION_PARTS_PER_MILLION, icon="mdi:gauge", - state_class=sensor.STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), ("sensor", "extTemp"): BlockAttributeDescription( name="Temperature", unit=temperature_unit, value=lambda value: round(value, 1), - device_class=sensor.DEVICE_CLASS_TEMPERATURE, - state_class=sensor.STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, available=lambda block: cast(int, block.extTemp) != 999, ), ("sensor", "humidity"): BlockAttributeDescription( name="Humidity", unit=PERCENTAGE, value=lambda value: round(value, 1), - device_class=sensor.DEVICE_CLASS_HUMIDITY, - state_class=sensor.STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, available=lambda block: cast(int, block.humidity) != 999, ), ("sensor", "luminosity"): BlockAttributeDescription( name="Luminosity", unit=LIGHT_LUX, - device_class=sensor.DEVICE_CLASS_ILLUMINANCE, - state_class=sensor.STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, available=lambda block: cast(int, block.luminosity) != -1, ), ("sensor", "tilt"): BlockAttributeDescription( name="Tilt", unit=DEGREE, icon="mdi:angle-acute", - state_class=sensor.STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), ("relay", "totalWorkTime"): BlockAttributeDescription( name="Lamp Life", @@ -208,14 +211,14 @@ SENSORS: Final = { extra_state_attributes=lambda block: { "Operational hours": round(cast(int, block.totalWorkTime) / 3600, 1) }, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), ("adc", "adc"): BlockAttributeDescription( name="ADC", unit=ELECTRIC_POTENTIAL_VOLT, value=lambda value: round(value, 1), - device_class=sensor.DEVICE_CLASS_VOLTAGE, - state_class=sensor.STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, ), ("sensor", "sensorOp"): BlockAttributeDescription( name="Operation", @@ -230,17 +233,17 @@ REST_SENSORS: Final = { name="RSSI", unit=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, value=lambda status, _: status["wifi_sta"]["rssi"], - device_class=sensor.DEVICE_CLASS_SIGNAL_STRENGTH, - state_class=sensor.STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + state_class=SensorStateClass.MEASUREMENT, default_enabled=False, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), "uptime": RestAttributeDescription( name="Uptime", value=lambda status, last: get_device_uptime(status["uptime"], last), - device_class=sensor.DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, default_enabled=False, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), } @@ -252,8 +255,8 @@ RPC_SENSORS: Final = { name="Power", unit=POWER_WATT, value=lambda status, _: round(float(status), 1), - device_class=sensor.DEVICE_CLASS_POWER, - state_class=sensor.STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), "voltage": RpcAttributeDescription( key="switch", @@ -261,8 +264,8 @@ RPC_SENSORS: Final = { name="Voltage", unit=ELECTRIC_POTENTIAL_VOLT, value=lambda status, _: round(float(status), 1), - device_class=sensor.DEVICE_CLASS_VOLTAGE, - state_class=sensor.STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, default_enabled=False, ), "energy": RpcAttributeDescription( @@ -271,8 +274,8 @@ RPC_SENSORS: Final = { name="Energy", unit=ENERGY_KILO_WATT_HOUR, value=lambda status, _: round(status["total"] / 1000, 2), - device_class=sensor.DEVICE_CLASS_ENERGY, - state_class=sensor.STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), "temperature": RpcAttributeDescription( key="switch", @@ -280,8 +283,8 @@ RPC_SENSORS: Final = { name="Temperature", unit=TEMP_CELSIUS, value=lambda status, _: round(status["tC"], 1), - device_class=sensor.DEVICE_CLASS_TEMPERATURE, - state_class=sensor.STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, default_enabled=False, ), "rssi": RpcAttributeDescription( @@ -289,19 +292,19 @@ RPC_SENSORS: Final = { sub_key="rssi", name="RSSI", unit=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - device_class=sensor.DEVICE_CLASS_SIGNAL_STRENGTH, - state_class=sensor.STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + state_class=SensorStateClass.MEASUREMENT, default_enabled=False, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), "uptime": RpcAttributeDescription( key="sys", sub_key="uptime", name="Uptime", value=get_device_uptime, - device_class=sensor.DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, default_enabled=False, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), } From 9e30e0c9bd4c7cbeafa812da3e1a2a2ff3b828ba Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 08:12:57 -0500 Subject: [PATCH 0536/2644] Use enums in ring (#62041) --- homeassistant/components/ring/binary_sensor.py | 7 +++---- homeassistant/components/ring/sensor.py | 16 ++++++++-------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/ring/binary_sensor.py b/homeassistant/components/ring/binary_sensor.py index de854022301..12534dfd01f 100644 --- a/homeassistant/components/ring/binary_sensor.py +++ b/homeassistant/components/ring/binary_sensor.py @@ -5,8 +5,7 @@ from dataclasses import dataclass from datetime import datetime from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_MOTION, - DEVICE_CLASS_OCCUPANCY, + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -35,13 +34,13 @@ BINARY_SENSOR_TYPES: tuple[RingBinarySensorEntityDescription, ...] = ( key="ding", name="Ding", category=["doorbots", "authorized_doorbots"], - device_class=DEVICE_CLASS_OCCUPANCY, + device_class=BinarySensorDeviceClass.OCCUPANCY, ), RingBinarySensorEntityDescription( key="motion", name="Motion", category=["doorbots", "authorized_doorbots", "stickup_cams"], - device_class=DEVICE_CLASS_MOTION, + device_class=BinarySensorDeviceClass.MOTION, ), ) diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index 2745c69d50a..d4ac5bafc2d 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -3,12 +3,12 @@ from __future__ import annotations from dataclasses import dataclass -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription -from homeassistant.const import ( - DEVICE_CLASS_TIMESTAMP, - PERCENTAGE, - SIGNAL_STRENGTH_DECIBELS_MILLIWATT, +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, ) +from homeassistant.const import PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT from homeassistant.core import callback from homeassistant.helpers.icon import icon_for_battery_level @@ -209,7 +209,7 @@ SENSOR_TYPES: tuple[RingSensorEntityDescription, ...] = ( name="Last Activity", category=["doorbots", "authorized_doorbots", "stickup_cams"], icon="mdi:history", - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, cls=HistoryRingSensor, ), RingSensorEntityDescription( @@ -218,7 +218,7 @@ SENSOR_TYPES: tuple[RingSensorEntityDescription, ...] = ( category=["doorbots", "authorized_doorbots"], icon="mdi:history", kind="ding", - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, cls=HistoryRingSensor, ), RingSensorEntityDescription( @@ -227,7 +227,7 @@ SENSOR_TYPES: tuple[RingSensorEntityDescription, ...] = ( category=["doorbots", "authorized_doorbots", "stickup_cams"], icon="mdi:history", kind="motion", - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, cls=HistoryRingSensor, ), RingSensorEntityDescription( From 1dab28a95700f8f122bce1904045cacb4cd8a640 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 08:13:43 -0500 Subject: [PATCH 0537/2644] Use enums in uptime (#62032) --- homeassistant/components/uptime/sensor.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/uptime/sensor.py b/homeassistant/components/uptime/sensor.py index 4afddc0f834..90d2a9e34ec 100644 --- a/homeassistant/components/uptime/sensor.py +++ b/homeassistant/components/uptime/sensor.py @@ -3,12 +3,12 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import ( - CONF_NAME, - CONF_UNIT_OF_MEASUREMENT, - DEVICE_CLASS_TIMESTAMP, +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorDeviceClass, + SensorEntity, ) +from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -48,6 +48,6 @@ class UptimeSensor(SensorEntity): def __init__(self, name: str) -> None: """Initialize the uptime sensor.""" self._attr_name: str = name - self._attr_device_class: str = DEVICE_CLASS_TIMESTAMP + self._attr_device_class = SensorDeviceClass.TIMESTAMP self._attr_should_poll: bool = False self._attr_native_value = dt_util.utcnow() From 25f72e45d7cd54c923d514edbaab08e9a633c5de Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 08:19:23 -0500 Subject: [PATCH 0538/2644] Use enums in skybell (#62053) --- homeassistant/components/skybell/binary_sensor.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/skybell/binary_sensor.py b/homeassistant/components/skybell/binary_sensor.py index 1e7eae145e3..bede414f8ee 100644 --- a/homeassistant/components/skybell/binary_sensor.py +++ b/homeassistant/components/skybell/binary_sensor.py @@ -7,9 +7,8 @@ from typing import Any import voluptuous as vol from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_MOTION, - DEVICE_CLASS_OCCUPANCY, PLATFORM_SCHEMA, + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -25,12 +24,12 @@ BINARY_SENSOR_TYPES: dict[str, BinarySensorEntityDescription] = { "button": BinarySensorEntityDescription( key="device:sensor:button", name="Button", - device_class=DEVICE_CLASS_OCCUPANCY, + device_class=BinarySensorDeviceClass.OCCUPANCY, ), "motion": BinarySensorEntityDescription( key="device:sensor:motion", name="Motion", - device_class=DEVICE_CLASS_MOTION, + device_class=BinarySensorDeviceClass.MOTION, ), } From ed8c7afc525147ea75800de7cb9d15c81eaba5be Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 08:20:00 -0500 Subject: [PATCH 0539/2644] use enums in skybeacon (#62052) --- homeassistant/components/skybeacon/sensor.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/skybeacon/sensor.py b/homeassistant/components/skybeacon/sensor.py index a72e1372ca0..4153c9332bd 100644 --- a/homeassistant/components/skybeacon/sensor.py +++ b/homeassistant/components/skybeacon/sensor.py @@ -8,11 +8,14 @@ from pygatt.backends import Characteristic, GATTToolBackend from pygatt.exceptions import BLEError, NotConnectedError, NotificationTimeout import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorDeviceClass, + SensorEntity, +) from homeassistant.const import ( CONF_MAC, CONF_NAME, - DEVICE_CLASS_TEMPERATURE, EVENT_HOMEASSISTANT_STOP, PERCENTAGE, STATE_UNKNOWN, @@ -91,7 +94,7 @@ class SkybeaconHumid(SensorEntity): class SkybeaconTemp(SensorEntity): """Representation of a Skybeacon temperature sensor.""" - _attr_device_class = DEVICE_CLASS_TEMPERATURE + _attr_device_class = SensorDeviceClass.TEMPERATURE _attr_native_unit_of_measurement = TEMP_CELSIUS def __init__(self, name, mon): From 65b67d9d911799ce03c08ca439b83922131262db Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 08:20:36 -0500 Subject: [PATCH 0540/2644] Use enums in sensehat (#62051) --- homeassistant/components/sensehat/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensehat/sensor.py b/homeassistant/components/sensehat/sensor.py index 10f86609ae2..ba509b760c8 100644 --- a/homeassistant/components/sensehat/sensor.py +++ b/homeassistant/components/sensehat/sensor.py @@ -10,13 +10,13 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, + SensorDeviceClass, SensorEntity, SensorEntityDescription, ) from homeassistant.const import ( CONF_DISPLAY_OPTIONS, CONF_NAME, - DEVICE_CLASS_TEMPERATURE, PERCENTAGE, TEMP_CELSIUS, ) @@ -35,7 +35,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="temperature", name="temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( key="humidity", From b6ed3e87e0e8c5a53981c6612d00deacfff6eaac Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 08:25:12 -0500 Subject: [PATCH 0541/2644] Use enums in senses (#62050) --- homeassistant/components/sense/binary_sensor.py | 9 ++++++--- homeassistant/components/sense/sensor.py | 16 +++++++--------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/sense/binary_sensor.py b/homeassistant/components/sense/binary_sensor.py index ae5e4fc95bc..99591438e32 100644 --- a/homeassistant/components/sense/binary_sensor.py +++ b/homeassistant/components/sense/binary_sensor.py @@ -1,8 +1,11 @@ """Support for monitoring a Sense energy sensor device.""" import logging -from homeassistant.components.binary_sensor import BinarySensorEntity -from homeassistant.const import ATTR_ATTRIBUTION, DEVICE_CLASS_POWER +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, +) +from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_registry import async_get_registry @@ -113,7 +116,7 @@ class SenseDevice(BinarySensorEntity): @property def device_class(self): """Return the device class of the binary sensor.""" - return DEVICE_CLASS_POWER + return BinarySensorDeviceClass.POWER @property def should_poll(self): diff --git a/homeassistant/components/sense/sensor.py b/homeassistant/components/sense/sensor.py index 08677cda8d0..b5fdb4398c0 100644 --- a/homeassistant/components/sense/sensor.py +++ b/homeassistant/components/sense/sensor.py @@ -1,13 +1,11 @@ """Support for monitoring a Sense energy sensor.""" from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL, + SensorDeviceClass, SensorEntity, + SensorStateClass, ) from homeassistant.const import ( ATTR_ATTRIBUTION, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, PERCENTAGE, @@ -157,7 +155,7 @@ class SenseActiveSensor(SensorEntity): _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} _attr_should_poll = False _attr_available = False - _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_state_class = SensorStateClass.MEASUREMENT def __init__( self, @@ -250,8 +248,8 @@ class SenseVoltageSensor(SensorEntity): class SenseTrendsSensor(CoordinatorEntity, SensorEntity): """Implementation of a Sense energy sensor.""" - _attr_device_class = DEVICE_CLASS_ENERGY - _attr_state_class = STATE_CLASS_TOTAL + _attr_device_class = SensorDeviceClass.ENERGY + _attr_state_class = SensorStateClass.TOTAL _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} _attr_icon = ICON @@ -299,10 +297,10 @@ class SenseEnergyDevice(SensorEntity): """Implementation of a Sense energy device.""" _attr_available = False - _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_state_class = SensorStateClass.MEASUREMENT _attr_native_unit_of_measurement = POWER_WATT _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} - _attr_device_class = DEVICE_CLASS_POWER + _attr_device_class = SensorDeviceClass.POWER _attr_should_poll = False def __init__(self, sense_devices_data, device, sense_monitor_id): From 9ddf2035d08ee1a2247f302cbf012be4a3ef0f93 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 08:28:21 -0500 Subject: [PATCH 0542/2644] Use enums in sht31 (#62036) --- homeassistant/components/sht31/sensor.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sht31/sensor.py b/homeassistant/components/sht31/sensor.py index 2d7c81072f6..ff8c5251ca7 100644 --- a/homeassistant/components/sht31/sensor.py +++ b/homeassistant/components/sht31/sensor.py @@ -12,14 +12,13 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, + SensorDeviceClass, SensorEntity, SensorEntityDescription, ) from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_NAME, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_TEMPERATURE, PERCENTAGE, TEMP_CELSIUS, ) @@ -50,14 +49,14 @@ SENSOR_TYPES = ( SHT31SensorEntityDescription( key="temperature", name="Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, value_fn=lambda sensor: sensor.temperature, ), SHT31SensorEntityDescription( key="humidity", name="Humidity", - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=PERCENTAGE, value_fn=lambda sensor: ( round(val) # pylint: disable=undefined-variable From 4983a8f2187188ef5bd3735ce4c0a4a5736f6aa3 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 08:32:03 -0500 Subject: [PATCH 0543/2644] Use enums in simplisafe (#62037) --- .../components/simplisafe/binary_sensor.py | 28 ++++++++----------- homeassistant/components/simplisafe/sensor.py | 12 +++++--- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/simplisafe/binary_sensor.py b/homeassistant/components/simplisafe/binary_sensor.py index 240ff24c6c8..3e4c64e8658 100644 --- a/homeassistant/components/simplisafe/binary_sensor.py +++ b/homeassistant/components/simplisafe/binary_sensor.py @@ -6,18 +6,12 @@ from simplipy.device.sensor.v3 import SensorV3 from simplipy.system.v3 import SystemV3 from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_DOOR, - DEVICE_CLASS_GAS, - DEVICE_CLASS_MOISTURE, - DEVICE_CLASS_MOTION, - DEVICE_CLASS_SAFETY, - DEVICE_CLASS_SMOKE, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import SimpliSafe, SimpliSafeEntity @@ -36,13 +30,13 @@ SUPPORTED_BATTERY_SENSOR_TYPES = [ ] TRIGGERED_SENSOR_TYPES = { - DeviceTypes.CARBON_MONOXIDE: DEVICE_CLASS_GAS, - DeviceTypes.ENTRY: DEVICE_CLASS_DOOR, - DeviceTypes.GLASS_BREAK: DEVICE_CLASS_SAFETY, - DeviceTypes.LEAK: DEVICE_CLASS_MOISTURE, - DeviceTypes.MOTION: DEVICE_CLASS_MOTION, - DeviceTypes.SIREN: DEVICE_CLASS_SAFETY, - DeviceTypes.SMOKE: DEVICE_CLASS_SMOKE, + DeviceTypes.CARBON_MONOXIDE: BinarySensorDeviceClass.GAS, + DeviceTypes.ENTRY: BinarySensorDeviceClass.DOOR, + DeviceTypes.GLASS_BREAK: BinarySensorDeviceClass.SAFETY, + DeviceTypes.LEAK: BinarySensorDeviceClass.MOISTURE, + DeviceTypes.MOTION: BinarySensorDeviceClass.MOTION, + DeviceTypes.SIREN: BinarySensorDeviceClass.SAFETY, + DeviceTypes.SMOKE: BinarySensorDeviceClass.SMOKE, } @@ -100,8 +94,8 @@ class TriggeredBinarySensor(SimpliSafeEntity, BinarySensorEntity): class BatteryBinarySensor(SimpliSafeEntity, BinarySensorEntity): """Define a SimpliSafe battery binary sensor entity.""" - _attr_device_class = DEVICE_CLASS_BATTERY - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_device_class = BinarySensorDeviceClass.BATTERY + _attr_entity_category = EntityCategory.DIAGNOSTIC def __init__( self, simplisafe: SimpliSafe, system: SystemV3, sensor: SensorV3 diff --git a/homeassistant/components/simplisafe/sensor.py b/homeassistant/components/simplisafe/sensor.py index b2b0a432bd6..39566434f27 100644 --- a/homeassistant/components/simplisafe/sensor.py +++ b/homeassistant/components/simplisafe/sensor.py @@ -5,9 +5,13 @@ from simplipy.device import DeviceTypes from simplipy.device.sensor.v3 import SensorV3 from simplipy.system.v3 import SystemV3 -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_FAHRENHEIT +from homeassistant.const import TEMP_FAHRENHEIT from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -37,9 +41,9 @@ async def async_setup_entry( class SimplisafeFreezeSensor(SimpliSafeEntity, SensorEntity): """Define a SimpliSafe freeze sensor entity.""" - _attr_device_class = DEVICE_CLASS_TEMPERATURE + _attr_device_class = SensorDeviceClass.TEMPERATURE _attr_native_unit_of_measurement = TEMP_FAHRENHEIT - _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_state_class = SensorStateClass.MEASUREMENT def __init__( self, simplisafe: SimpliSafe, system: SystemV3, sensor: SensorV3 From 18ae4a9420eecc580f0d458820aac87f3846976a Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 08:33:23 -0500 Subject: [PATCH 0544/2644] Use enums in repetier (#62038) --- homeassistant/components/repetier/__init__.py | 9 ++++----- homeassistant/components/repetier/sensor.py | 7 +++---- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/repetier/__init__.py b/homeassistant/components/repetier/__init__.py index 4c5534d1a28..54acbc07449 100644 --- a/homeassistant/components/repetier/__init__.py +++ b/homeassistant/components/repetier/__init__.py @@ -8,7 +8,7 @@ import logging import pyrepetier import voluptuous as vol -from homeassistant.components.sensor import SensorEntityDescription +from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription from homeassistant.const import ( CONF_API_KEY, CONF_HOST, @@ -16,7 +16,6 @@ from homeassistant.const import ( CONF_NAME, CONF_PORT, CONF_SENSORS, - DEVICE_CLASS_TEMPERATURE, PERCENTAGE, TEMP_CELSIUS, ) @@ -143,21 +142,21 @@ SENSOR_TYPES: dict[str, RepetierSensorEntityDescription] = { type="temperature", native_unit_of_measurement=TEMP_CELSIUS, name="_bed_", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), "extruder_temperature": RepetierSensorEntityDescription( key="extruder_temperature", type="temperature", native_unit_of_measurement=TEMP_CELSIUS, name="_extruder_", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), "chamber_temperature": RepetierSensorEntityDescription( key="chamber_temperature", type="temperature", native_unit_of_measurement=TEMP_CELSIUS, name="_chamber_", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), "current_state": RepetierSensorEntityDescription( key="current_state", diff --git a/homeassistant/components/repetier/sensor.py b/homeassistant/components/repetier/sensor.py index 25c70cc2960..8ab05ee058c 100644 --- a/homeassistant/components/repetier/sensor.py +++ b/homeassistant/components/repetier/sensor.py @@ -3,8 +3,7 @@ from datetime import datetime import logging import time -from homeassistant.components.sensor import SensorEntity -from homeassistant.const import DEVICE_CLASS_TIMESTAMP +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -149,7 +148,7 @@ class RepetierJobSensor(RepetierSensor): class RepetierJobEndSensor(RepetierSensor): """Class to create and populate a Repetier Job End timestamp Sensor.""" - _attr_device_class = DEVICE_CLASS_TIMESTAMP + _attr_device_class = SensorDeviceClass.TIMESTAMP def update(self): """Update the sensor.""" @@ -173,7 +172,7 @@ class RepetierJobEndSensor(RepetierSensor): class RepetierJobStartSensor(RepetierSensor): """Class to create and populate a Repetier Job Start timestamp Sensor.""" - _attr_device_class = DEVICE_CLASS_TIMESTAMP + _attr_device_class = SensorDeviceClass.TIMESTAMP def update(self): """Update the sensor.""" From d5fe0fcee005e131485bdbe419c60e5492c4a47f Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 08:34:10 -0500 Subject: [PATCH 0545/2644] Use enums in rfxtrx (#62039) --- homeassistant/components/rfxtrx/binary_sensor.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/rfxtrx/binary_sensor.py b/homeassistant/components/rfxtrx/binary_sensor.py index d7c0ea306d8..10342fd826a 100644 --- a/homeassistant/components/rfxtrx/binary_sensor.py +++ b/homeassistant/components/rfxtrx/binary_sensor.py @@ -6,8 +6,7 @@ import logging import RFXtrx as rfxtrxmod from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_MOTION, - DEVICE_CLASS_SMOKE, + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -61,23 +60,23 @@ SENSOR_STATUS_OFF = [ SENSOR_TYPES = ( BinarySensorEntityDescription( key="X10 Security Motion Detector", - device_class=DEVICE_CLASS_MOTION, + device_class=BinarySensorDeviceClass.MOTION, ), BinarySensorEntityDescription( key="KD101 Smoke Detector", - device_class=DEVICE_CLASS_SMOKE, + device_class=BinarySensorDeviceClass.SMOKE, ), BinarySensorEntityDescription( key="Visonic Powercode Motion Detector", - device_class=DEVICE_CLASS_MOTION, + device_class=BinarySensorDeviceClass.MOTION, ), BinarySensorEntityDescription( key="Alecto SA30 Smoke Detector", - device_class=DEVICE_CLASS_SMOKE, + device_class=BinarySensorDeviceClass.SMOKE, ), BinarySensorEntityDescription( key="RM174RF Smoke Detector", - device_class=DEVICE_CLASS_SMOKE, + device_class=BinarySensorDeviceClass.SMOKE, ), ) From 0dc5ae6dca30a0d5352add02b7af56567e8b589a Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 08:34:51 -0500 Subject: [PATCH 0546/2644] Use enums in ridwell (#62040) --- homeassistant/components/ridwell/sensor.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ridwell/sensor.py b/homeassistant/components/ridwell/sensor.py index f80ddc45176..269a08587c7 100644 --- a/homeassistant/components/ridwell/sensor.py +++ b/homeassistant/components/ridwell/sensor.py @@ -7,9 +7,8 @@ from typing import Any from aioridwell.client import RidwellAccount, RidwellPickupEvent -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import DEVICE_CLASS_DATE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -40,7 +39,7 @@ async def async_setup_entry( class RidwellSensor(CoordinatorEntity, SensorEntity): """Define a Ridwell pickup sensor.""" - _attr_device_class = DEVICE_CLASS_DATE + _attr_device_class = SensorDeviceClass.DATE def __init__( self, coordinator: DataUpdateCoordinator, account: RidwellAccount From 7506b1227740e79b9a0d76a4333c7d86cc3ec85e Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 08:41:20 -0500 Subject: [PATCH 0547/2644] Use enums in risco (#62042) --- homeassistant/components/risco/binary_sensor.py | 6 +++--- homeassistant/components/risco/sensor.py | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/risco/binary_sensor.py b/homeassistant/components/risco/binary_sensor.py index cc93d7c11d4..3cfa8ba8a9d 100644 --- a/homeassistant/components/risco/binary_sensor.py +++ b/homeassistant/components/risco/binary_sensor.py @@ -1,6 +1,6 @@ """Support for Risco alarm zones.""" from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_MOTION, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.helpers import entity_platform @@ -72,8 +72,8 @@ class RiscoBinarySensor(BinarySensorEntity, RiscoEntity): @property def device_class(self): - """Return the class of this sensor, from DEVICE_CLASSES.""" - return DEVICE_CLASS_MOTION + """Return the class of this sensor, from BinarySensorDeviceClass.""" + return BinarySensorDeviceClass.MOTION async def _bypass(self, bypass): alarm = await self._risco.bypass_zone(self._zone_id, bypass) diff --git a/homeassistant/components/risco/sensor.py b/homeassistant/components/risco/sensor.py index 0068e8c0f04..fdb7bbc32b4 100644 --- a/homeassistant/components/risco/sensor.py +++ b/homeassistant/components/risco/sensor.py @@ -1,7 +1,6 @@ """Sensor for Risco Events.""" from homeassistant.components.binary_sensor import DOMAIN as BS_DOMAIN -from homeassistant.components.sensor import SensorEntity -from homeassistant.const import DEVICE_CLASS_TIMESTAMP +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, EVENTS_COORDINATOR @@ -116,4 +115,4 @@ class RiscoSensor(CoordinatorEntity, SensorEntity): @property def device_class(self): """Device class of sensor.""" - return DEVICE_CLASS_TIMESTAMP + return SensorDeviceClass.TIMESTAMP From 093202f138aa002f8330be63c1bbe8810329c9a4 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 08:41:59 -0500 Subject: [PATCH 0548/2644] Use enum in roku (#62043) --- homeassistant/components/roku/media_player.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index bb9b4bfa37f..72b93994a10 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -7,9 +7,8 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( - DEVICE_CLASS_RECEIVER, - DEVICE_CLASS_TV, BrowseMedia, + MediaPlayerDeviceClass, MediaPlayerEntity, ) from homeassistant.components.media_player.const import ( @@ -104,9 +103,9 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): def device_class(self) -> str | None: """Return the class of this device.""" if self.coordinator.data.info.device_type == "tv": - return DEVICE_CLASS_TV + return MediaPlayerDeviceClass.TV - return DEVICE_CLASS_RECEIVER + return MediaPlayerDeviceClass.RECEIVER @property def state(self) -> str | None: From fa522fc50408078995a2f67a64b6492ec31a6870 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 08:42:43 -0500 Subject: [PATCH 0549/2644] Use enums in rova (#62044) --- homeassistant/components/rova/sensor.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/rova/sensor.py b/homeassistant/components/rova/sensor.py index 60d0fbe6df0..654323a79f4 100644 --- a/homeassistant/components/rova/sensor.py +++ b/homeassistant/components/rova/sensor.py @@ -10,14 +10,11 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, + SensorDeviceClass, SensorEntity, SensorEntityDescription, ) -from homeassistant.const import ( - CONF_MONITORED_CONDITIONS, - CONF_NAME, - DEVICE_CLASS_TIMESTAMP, -) +from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle from homeassistant.util.dt import get_time_zone, now @@ -110,7 +107,7 @@ class RovaSensor(SensorEntity): self.data_service = data_service self._attr_name = f"{platform_name}_{description.name}" - self._attr_device_class = DEVICE_CLASS_TIMESTAMP + self._attr_device_class = SensorDeviceClass.TIMESTAMP def update(self): """Get the latest data from the sensor and update the state.""" From 395fa6d15f1319d9e2900a2a3b2146817a9386ea Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 08:57:06 -0500 Subject: [PATCH 0550/2644] Use enums in samsung_tv (#62047) --- homeassistant/components/samsungtv/media_player.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 1c4ff6c3f83..7fcc2268d9b 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -8,7 +8,10 @@ from typing import Any import voluptuous as vol from wakeonlan import send_magic_packet -from homeassistant.components.media_player import DEVICE_CLASS_TV, MediaPlayerEntity +from homeassistant.components.media_player import ( + MediaPlayerDeviceClass, + MediaPlayerEntity, +) from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, @@ -104,7 +107,7 @@ class SamsungTVDevice(MediaPlayerEntity): self._attr_state: str | None = None self._attr_unique_id = config_entry.unique_id self._attr_is_volume_muted: bool = False - self._attr_device_class = DEVICE_CLASS_TV + self._attr_device_class = MediaPlayerDeviceClass.TV self._attr_source_list = list(SOURCES) if self._on_script or self._mac: From 3b9547addc5ceedbf61f0d9faa8f296186a2e8c8 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 09:01:45 -0500 Subject: [PATCH 0551/2644] Use enums in qnap (#62055) --- homeassistant/components/qnap/sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/qnap/sensor.py b/homeassistant/components/qnap/sensor.py index 91df03947a0..5580bb14673 100644 --- a/homeassistant/components/qnap/sensor.py +++ b/homeassistant/components/qnap/sensor.py @@ -9,6 +9,7 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, + SensorDeviceClass, SensorEntity, SensorEntityDescription, ) @@ -24,7 +25,6 @@ from homeassistant.const import ( CONF_VERIFY_SSL, DATA_GIBIBYTES, DATA_RATE_MEBIBYTES_PER_SECOND, - DEVICE_CLASS_TEMPERATURE, PERCENTAGE, TEMP_CELSIUS, ) @@ -72,7 +72,7 @@ _SYSTEM_MON_COND: tuple[SensorEntityDescription, ...] = ( key="system_temp", name="System Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), ) _CPU_MON_COND: tuple[SensorEntityDescription, ...] = ( @@ -80,7 +80,7 @@ _CPU_MON_COND: tuple[SensorEntityDescription, ...] = ( key="cpu_temp", name="CPU Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( key="cpu_usage", @@ -138,7 +138,7 @@ _DRIVE_MON_COND: tuple[SensorEntityDescription, ...] = ( key="drive_temp", name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), ) _VOLUME_MON_COND: tuple[SensorEntityDescription, ...] = ( From ff654a9753101965bf9a398ee277a7f81a478a21 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 09:02:38 -0500 Subject: [PATCH 0552/2644] Use enums philips_js (#62063) --- homeassistant/components/philips_js/media_player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 9b6ddfeb76e..8e9ddfc353f 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -8,9 +8,9 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components.media_player import ( - DEVICE_CLASS_TV, PLATFORM_SCHEMA, BrowseMedia, + MediaPlayerDeviceClass, MediaPlayerEntity, ) from homeassistant.components.media_player.const import ( @@ -124,7 +124,7 @@ async def async_setup_entry( class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): """Representation of a Philips TV exposing the JointSpace API.""" - _attr_device_class = DEVICE_CLASS_TV + _attr_device_class = MediaPlayerDeviceClass.TV def __init__( self, From 079b7f217ffddb1e78b93726dc75d2a0a3bb788d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 16 Dec 2021 15:09:31 +0100 Subject: [PATCH 0553/2644] Use new enums in mqtt (#61936) * Use new enums in mqtt * Fix typo --- homeassistant/components/mqtt/humidifier.py | 9 +++++---- homeassistant/components/mqtt/sensor.py | 9 ++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index fcafa185509..1a0a9988762 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -10,9 +10,8 @@ from homeassistant.components.humidifier import ( ATTR_MODE, DEFAULT_MAX_HUMIDITY, DEFAULT_MIN_HUMIDITY, - DEVICE_CLASS_DEHUMIDIFIER, - DEVICE_CLASS_HUMIDIFIER, SUPPORT_MODES, + HumidifierDeviceClass, HumidifierEntity, ) from homeassistant.const import ( @@ -96,8 +95,10 @@ _PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( CONF_MODE_COMMAND_TOPIC, "available_modes" ): mqtt.valid_publish_topic, vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_DEVICE_CLASS, default=DEVICE_CLASS_HUMIDIFIER): vol.In( - [DEVICE_CLASS_HUMIDIFIER, DEVICE_CLASS_DEHUMIDIFIER] + vol.Optional( + CONF_DEVICE_CLASS, default=HumidifierDeviceClass.HUMIDIFIER + ): vol.In( + [HumidifierDeviceClass.HUMIDIFIER, HumidifierDeviceClass.DEHUMIDIFIER] ), vol.Optional(CONF_MODE_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 72f0b339fe2..4e0c38a096c 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -13,6 +13,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASSES_SCHEMA, ENTITY_ID_FORMAT, STATE_CLASSES_SCHEMA, + SensorDeviceClass, SensorEntity, ) from homeassistant.const import ( @@ -21,8 +22,6 @@ from homeassistant.const import ( CONF_NAME, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, - DEVICE_CLASS_DATE, - DEVICE_CLASS_TIMESTAMP, ) from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv @@ -200,14 +199,14 @@ class MqttSensor(MqttEntity, SensorEntity): ) if payload is not None and self.device_class in ( - DEVICE_CLASS_DATE, - DEVICE_CLASS_TIMESTAMP, + SensorDeviceClass.DATE, + SensorDeviceClass.TIMESTAMP, ): if (payload := dt_util.parse_datetime(payload)) is None: _LOGGER.warning( "Invalid state message '%s' from '%s'", msg.payload, msg.topic ) - elif self.device_class == DEVICE_CLASS_DATE: + elif self.device_class == SensorDeviceClass.DATE: payload = payload.date() self._state = payload From 5227019d3edf6e75c0a52191aba774985a3b3f4c Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 09:10:54 -0500 Subject: [PATCH 0554/2644] Use enums in rdw (#62059) --- homeassistant/components/rdw/binary_sensor.py | 4 ++-- homeassistant/components/rdw/sensor.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/rdw/binary_sensor.py b/homeassistant/components/rdw/binary_sensor.py index 80f4a425212..81d3e448b78 100644 --- a/homeassistant/components/rdw/binary_sensor.py +++ b/homeassistant/components/rdw/binary_sensor.py @@ -7,7 +7,7 @@ from dataclasses import dataclass from vehicle import Vehicle from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_PROBLEM, + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -48,7 +48,7 @@ BINARY_SENSORS: tuple[RDWBinarySensorEntityDescription, ...] = ( RDWBinarySensorEntityDescription( key="pending_recall", name="Pending Recall", - device_class=DEVICE_CLASS_PROBLEM, + device_class=BinarySensorDeviceClass.PROBLEM, is_on_fn=lambda vehicle: vehicle.pending_recall, ), ) diff --git a/homeassistant/components/rdw/sensor.py b/homeassistant/components/rdw/sensor.py index f2c8d93a8a2..04f525c61b8 100644 --- a/homeassistant/components/rdw/sensor.py +++ b/homeassistant/components/rdw/sensor.py @@ -8,7 +8,7 @@ from datetime import date from vehicle import Vehicle from homeassistant.components.sensor import ( - DEVICE_CLASS_DATE, + SensorDeviceClass, SensorEntity, SensorEntityDescription, ) @@ -43,13 +43,13 @@ SENSORS: tuple[RDWSensorEntityDescription, ...] = ( RDWSensorEntityDescription( key="apk_expiration", name="APK Expiration", - device_class=DEVICE_CLASS_DATE, + device_class=SensorDeviceClass.DATE, value_fn=lambda vehicle: vehicle.apk_expiration, ), RDWSensorEntityDescription( key="ascription_date", name="Ascription Date", - device_class=DEVICE_CLASS_DATE, + device_class=SensorDeviceClass.DATE, value_fn=lambda vehicle: vehicle.ascription_date, ), ) From 029af94d288394dd993ee813a6a6f69ae67f36e4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 16 Dec 2021 15:13:13 +0100 Subject: [PATCH 0555/2644] Use new enums in mobile_app (#61929) --- homeassistant/components/mobile_app/sensor.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py index 0631f8f72aa..c7896da2576 100644 --- a/homeassistant/components/mobile_app/sensor.py +++ b/homeassistant/components/mobile_app/sensor.py @@ -1,13 +1,11 @@ """Sensor platform for mobile_app.""" from __future__ import annotations -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.const import ( CONF_NAME, CONF_UNIQUE_ID, CONF_WEBHOOK_ID, - DEVICE_CLASS_DATE, - DEVICE_CLASS_TIMESTAMP, STATE_UNKNOWN, ) from homeassistant.core import callback @@ -95,12 +93,12 @@ class MobileAppSensor(MobileAppEntity, SensorEntity): if ( self.device_class in ( - DEVICE_CLASS_DATE, - DEVICE_CLASS_TIMESTAMP, + SensorDeviceClass.DATE, + SensorDeviceClass.TIMESTAMP, ) and (timestamp := dt_util.parse_datetime(state)) is not None ): - if self.device_class == DEVICE_CLASS_DATE: + if self.device_class == SensorDeviceClass.DATE: return timestamp.date() return timestamp From b315877ad00a03f1e2efece83498911dd187acc4 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 09:14:46 -0500 Subject: [PATCH 0556/2644] Clean up upcloud (#61971) --- homeassistant/components/upcloud/binary_sensor.py | 10 +--------- homeassistant/components/upcloud/switch.py | 11 ++--------- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/upcloud/binary_sensor.py b/homeassistant/components/upcloud/binary_sensor.py index ebdc30b69f1..691edde8473 100644 --- a/homeassistant/components/upcloud/binary_sensor.py +++ b/homeassistant/components/upcloud/binary_sensor.py @@ -1,23 +1,15 @@ """Support for monitoring the state of UpCloud servers.""" -import voluptuous as vol - from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_USERNAME from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import CONF_SERVERS, DATA_UPCLOUD, UpCloudServerEntity - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Required(CONF_SERVERS): vol.All(cv.ensure_list, [cv.string])} -) +from . import DATA_UPCLOUD, UpCloudServerEntity async def async_setup_entry( diff --git a/homeassistant/components/upcloud/switch.py b/homeassistant/components/upcloud/switch.py index 91676ab1d5a..484b6875d8f 100644 --- a/homeassistant/components/upcloud/switch.py +++ b/homeassistant/components/upcloud/switch.py @@ -2,21 +2,14 @@ from typing import Any -import voluptuous as vol - -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_USERNAME, STATE_OFF from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import CONF_SERVERS, DATA_UPCLOUD, SIGNAL_UPDATE_UPCLOUD, UpCloudServerEntity - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Required(CONF_SERVERS): vol.All(cv.ensure_list, [cv.string])} -) +from . import DATA_UPCLOUD, SIGNAL_UPDATE_UPCLOUD, UpCloudServerEntity async def async_setup_entry( From 5454c5467d096a0440e30a1fe8391655949601c5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 16 Dec 2021 15:15:54 +0100 Subject: [PATCH 0557/2644] Use new enums in nx584 (#61948) --- homeassistant/components/nx584/binary_sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/nx584/binary_sensor.py b/homeassistant/components/nx584/binary_sensor.py index 38787afb080..236021ab40f 100644 --- a/homeassistant/components/nx584/binary_sensor.py +++ b/homeassistant/components/nx584/binary_sensor.py @@ -8,9 +8,9 @@ import requests import voluptuous as vol from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_OPENING, - DEVICE_CLASSES, + DEVICE_CLASSES_SCHEMA as BINARY_SENSOR_DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.const import CONF_HOST, CONF_PORT @@ -25,7 +25,7 @@ DEFAULT_HOST = "localhost" DEFAULT_PORT = "5007" DEFAULT_SSL = False -ZONE_TYPES_SCHEMA = vol.Schema({cv.positive_int: vol.In(DEVICE_CLASSES)}) +ZONE_TYPES_SCHEMA = vol.Schema({cv.positive_int: BINARY_SENSOR_DEVICE_CLASSES_SCHEMA}) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -61,7 +61,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): zone_sensors = { zone["number"]: NX584ZoneSensor( - zone, zone_types.get(zone["number"], DEVICE_CLASS_OPENING) + zone, zone_types.get(zone["number"], BinarySensorDeviceClass.OPENING) ) for zone in zones if zone["number"] not in exclude From b16d779280614eea556fa8e4bea9114d2f066d36 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 16 Dec 2021 15:18:52 +0100 Subject: [PATCH 0558/2644] Use DeviceClass Enum in concord232 schema (#61968) --- homeassistant/components/concord232/binary_sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/concord232/binary_sensor.py b/homeassistant/components/concord232/binary_sensor.py index 3f21489de20..fac16c834d9 100644 --- a/homeassistant/components/concord232/binary_sensor.py +++ b/homeassistant/components/concord232/binary_sensor.py @@ -7,7 +7,7 @@ import requests import voluptuous as vol from homeassistant.components.binary_sensor import ( - DEVICE_CLASSES, + DEVICE_CLASSES_SCHEMA as BINARY_SENSOR_DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, @@ -28,7 +28,7 @@ DEFAULT_SSL = False SCAN_INTERVAL = datetime.timedelta(seconds=10) -ZONE_TYPES_SCHEMA = vol.Schema({cv.positive_int: vol.In(DEVICE_CLASSES)}) +ZONE_TYPES_SCHEMA = vol.Schema({cv.positive_int: BINARY_SENSOR_DEVICE_CLASSES_SCHEMA}) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { From c8607b1a4c3ffd8e842f016813ff23cf28733ad2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 16 Dec 2021 15:20:40 +0100 Subject: [PATCH 0559/2644] Use DeviceClass Enum in ness-alarm schema (#61969) --- homeassistant/components/ness_alarm/__init__.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ness_alarm/__init__.py b/homeassistant/components/ness_alarm/__init__.py index e17e63ee46c..e15e0fb2e20 100644 --- a/homeassistant/components/ness_alarm/__init__.py +++ b/homeassistant/components/ness_alarm/__init__.py @@ -5,7 +5,10 @@ import datetime from nessclient import ArmingState, Client import voluptuous as vol -from homeassistant.components.binary_sensor import DEVICE_CLASSES +from homeassistant.components.binary_sensor import ( + DEVICE_CLASSES_SCHEMA as BINARY_SENSOR_DEVICE_CLASSES_SCHEMA, + BinarySensorDeviceClass, +) from homeassistant.const import ( ATTR_CODE, ATTR_STATE, @@ -36,12 +39,14 @@ SIGNAL_ARMING_STATE_CHANGED = "ness_alarm.arming_state_changed" ZoneChangedData = namedtuple("ZoneChangedData", ["zone_id", "state"]) -DEFAULT_ZONE_TYPE = "motion" +DEFAULT_ZONE_TYPE = BinarySensorDeviceClass.MOTION ZONE_SCHEMA = vol.Schema( { vol.Required(CONF_ZONE_NAME): cv.string, vol.Required(CONF_ZONE_ID): cv.positive_int, - vol.Optional(CONF_ZONE_TYPE, default=DEFAULT_ZONE_TYPE): vol.In(DEVICE_CLASSES), + vol.Optional( + CONF_ZONE_TYPE, default=DEFAULT_ZONE_TYPE + ): BINARY_SENSOR_DEVICE_CLASSES_SCHEMA, } ) From 6083b56139eb264019eadec406fa58e0a97934d9 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 09:29:45 -0500 Subject: [PATCH 0560/2644] Use enums for Panasonic Viera (#62062) --- homeassistant/components/panasonic_viera/media_player.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/panasonic_viera/media_player.py b/homeassistant/components/panasonic_viera/media_player.py index 9058574354b..a4013f9b53c 100644 --- a/homeassistant/components/panasonic_viera/media_player.py +++ b/homeassistant/components/panasonic_viera/media_player.py @@ -5,7 +5,10 @@ import logging from panasonic_viera import Keys -from homeassistant.components.media_player import DEVICE_CLASS_TV, MediaPlayerEntity +from homeassistant.components.media_player import ( + MediaPlayerDeviceClass, + MediaPlayerEntity, +) from homeassistant.components.media_player.const import ( MEDIA_TYPE_URL, SUPPORT_NEXT_TRACK, @@ -95,7 +98,7 @@ class PanasonicVieraTVEntity(MediaPlayerEntity): @property def device_class(self): """Return the device class of the device.""" - return DEVICE_CLASS_TV + return MediaPlayerDeviceClass.TV @property def name(self): From 0ce985ee7bbf7e0c575696b530f8bf4f6d29bf5b Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 09:30:59 -0500 Subject: [PATCH 0561/2644] Use enums in ovo_energy (#62087) --- homeassistant/components/ovo_energy/sensor.py | 34 ++++++++----------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/ovo_energy/sensor.py b/homeassistant/components/ovo_energy/sensor.py index 17f92a3b2e2..276705248b3 100644 --- a/homeassistant/components/ovo_energy/sensor.py +++ b/homeassistant/components/ovo_energy/sensor.py @@ -10,17 +10,13 @@ from ovoenergy import OVODailyUsage from ovoenergy.ovoenergy import OVOEnergy from homeassistant.components.sensor import ( - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_MONETARY, - DEVICE_CLASS_TIMESTAMP, - ENERGY_KILO_WATT_HOUR, -) +from homeassistant.const import ENERGY_KILO_WATT_HOUR from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -48,30 +44,30 @@ SENSOR_TYPES_ELECTRICITY: tuple[OVOEnergySensorEntityDescription, ...] = ( OVOEnergySensorEntityDescription( key="last_electricity_reading", name="OVO Last Electricity Reading", - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value=lambda usage: usage.electricity[-1].consumption, ), OVOEnergySensorEntityDescription( key=KEY_LAST_ELECTRICITY_COST, name="OVO Last Electricity Cost", - device_class=DEVICE_CLASS_MONETARY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.MONETARY, + state_class=SensorStateClass.TOTAL_INCREASING, value=lambda usage: usage.electricity[-1].cost.amount, ), OVOEnergySensorEntityDescription( key="last_electricity_start_time", name="OVO Last Electricity Start Time", entity_registry_enabled_default=False, - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, value=lambda usage: dt_util.as_utc(usage.electricity[-1].interval.start), ), OVOEnergySensorEntityDescription( key="last_electricity_end_time", name="OVO Last Electricity End Time", entity_registry_enabled_default=False, - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, value=lambda usage: dt_util.as_utc(usage.electricity[-1].interval.end), ), ) @@ -80,8 +76,8 @@ SENSOR_TYPES_GAS: tuple[OVOEnergySensorEntityDescription, ...] = ( OVOEnergySensorEntityDescription( key="last_gas_reading", name="OVO Last Gas Reading", - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, icon="mdi:gas-cylinder", value=lambda usage: usage.gas[-1].consumption, @@ -89,8 +85,8 @@ SENSOR_TYPES_GAS: tuple[OVOEnergySensorEntityDescription, ...] = ( OVOEnergySensorEntityDescription( key=KEY_LAST_GAS_COST, name="OVO Last Gas Cost", - device_class=DEVICE_CLASS_MONETARY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.MONETARY, + state_class=SensorStateClass.TOTAL_INCREASING, icon="mdi:cash-multiple", value=lambda usage: usage.gas[-1].cost.amount, ), @@ -98,14 +94,14 @@ SENSOR_TYPES_GAS: tuple[OVOEnergySensorEntityDescription, ...] = ( key="last_gas_start_time", name="OVO Last Gas Start Time", entity_registry_enabled_default=False, - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, value=lambda usage: dt_util.as_utc(usage.gas[-1].interval.start), ), OVOEnergySensorEntityDescription( key="last_gas_end_time", name="OVO Last Gas End Time", entity_registry_enabled_default=False, - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, value=lambda usage: dt_util.as_utc(usage.gas[-1].interval.end), ), ) From 6d9787526b20858b1af22d8f0f6e1aadc7c30627 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 09:34:42 -0500 Subject: [PATCH 0562/2644] Use enums in oasa_telematics (#62077) --- homeassistant/components/oasa_telematics/sensor.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/oasa_telematics/sensor.py b/homeassistant/components/oasa_telematics/sensor.py index a5a4a98c3d4..87562db2324 100644 --- a/homeassistant/components/oasa_telematics/sensor.py +++ b/homeassistant/components/oasa_telematics/sensor.py @@ -6,8 +6,12 @@ from operator import itemgetter import oasatelematics import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, DEVICE_CLASS_TIMESTAMP +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorDeviceClass, + SensorEntity, +) +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.util import dt as dt_util @@ -70,7 +74,7 @@ class OASATelematicsSensor(SensorEntity): @property def device_class(self): """Return the class of this sensor.""" - return DEVICE_CLASS_TIMESTAMP + return SensorDeviceClass.TIMESTAMP @property def native_value(self): From 42fab1bb510efe71cbdc796afa5e05911cfc0365 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 09:36:38 -0500 Subject: [PATCH 0563/2644] Use enums in picnic (#62065) --- homeassistant/components/picnic/const.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/picnic/const.py b/homeassistant/components/picnic/const.py index 228983d8189..59b969236c4 100644 --- a/homeassistant/components/picnic/const.py +++ b/homeassistant/components/picnic/const.py @@ -5,8 +5,8 @@ from collections.abc import Callable from dataclasses import dataclass from typing import Any, Literal -from homeassistant.components.sensor import SensorEntityDescription -from homeassistant.const import CURRENCY_EURO, DEVICE_CLASS_TIMESTAMP +from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription +from homeassistant.const import CURRENCY_EURO from homeassistant.helpers.typing import StateType DOMAIN = "picnic" @@ -69,7 +69,7 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( ), PicnicSensorEntityDescription( key=SENSOR_SELECTED_SLOT_START, - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, icon="mdi:calendar-start", entity_registry_enabled_default=True, data_type="slot_data", @@ -77,7 +77,7 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( ), PicnicSensorEntityDescription( key=SENSOR_SELECTED_SLOT_END, - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, icon="mdi:calendar-end", entity_registry_enabled_default=True, data_type="slot_data", @@ -85,7 +85,7 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( ), PicnicSensorEntityDescription( key=SENSOR_SELECTED_SLOT_MAX_ORDER_TIME, - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, icon="mdi:clock-alert-outline", entity_registry_enabled_default=True, data_type="slot_data", @@ -105,14 +105,14 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( ), PicnicSensorEntityDescription( key=SENSOR_LAST_ORDER_SLOT_START, - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, icon="mdi:calendar-start", data_type="last_order_data", value_fn=lambda last_order: last_order.get("slot", {}).get("window_start"), ), PicnicSensorEntityDescription( key=SENSOR_LAST_ORDER_SLOT_END, - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, icon="mdi:calendar-end", data_type="last_order_data", value_fn=lambda last_order: last_order.get("slot", {}).get("window_end"), @@ -125,7 +125,7 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( ), PicnicSensorEntityDescription( key=SENSOR_LAST_ORDER_ETA_START, - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, icon="mdi:clock-start", entity_registry_enabled_default=True, data_type="last_order_data", @@ -133,7 +133,7 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( ), PicnicSensorEntityDescription( key=SENSOR_LAST_ORDER_ETA_END, - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, icon="mdi:clock-end", entity_registry_enabled_default=True, data_type="last_order_data", @@ -141,7 +141,7 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( ), PicnicSensorEntityDescription( key=SENSOR_LAST_ORDER_DELIVERY_TIME, - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, icon="mdi:timeline-clock", entity_registry_enabled_default=True, data_type="last_order_data", From a5cf783e6ae7ff809c15242fbf8350e1f90a975d Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 09:38:02 -0500 Subject: [PATCH 0564/2644] Use enums in ping (#62066) --- homeassistant/components/ping/binary_sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ping/binary_sensor.py b/homeassistant/components/ping/binary_sensor.py index ea07c3123e9..0ab79a65086 100644 --- a/homeassistant/components/ping/binary_sensor.py +++ b/homeassistant/components/ping/binary_sensor.py @@ -13,8 +13,8 @@ from icmplib import NameLookupError, async_ping import voluptuous as vol from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_CONNECTIVITY, PLATFORM_SCHEMA, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.const import CONF_HOST, CONF_NAME, STATE_ON @@ -99,9 +99,9 @@ class PingBinarySensor(RestoreEntity, BinarySensorEntity): return self._available @property - def device_class(self) -> str: + def device_class(self) -> BinarySensorDeviceClass: """Return the class of this sensor.""" - return DEVICE_CLASS_CONNECTIVITY + return BinarySensorDeviceClass.CONNECTIVITY @property def is_on(self) -> bool: From e1a7f6d1b28a3d9194a1179ca4c992620532ff4c Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 09:40:32 -0500 Subject: [PATCH 0565/2644] Use enums in pluugwise (#62067) --- homeassistant/components/plugwise/sensor.py | 117 ++++++++++---------- 1 file changed, 57 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/plugwise/sensor.py b/homeassistant/components/plugwise/sensor.py index 59e7858f947..73a3c386716 100644 --- a/homeassistant/components/plugwise/sensor.py +++ b/homeassistant/components/plugwise/sensor.py @@ -3,17 +3,9 @@ import logging from homeassistant.components.sensor import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_GAS, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_POWER, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, + SensorStateClass, ) from homeassistant.const import ( ENERGY_KILO_WATT_HOUR, @@ -46,26 +38,26 @@ _LOGGER = logging.getLogger(__name__) ATTR_TEMPERATURE = [ "Temperature", TEMP_CELSIUS, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + SensorDeviceClass.TEMPERATURE, + SensorStateClass.MEASUREMENT, ] ATTR_BATTERY_LEVEL = [ "Charge", PERCENTAGE, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + SensorDeviceClass.BATTERY, + SensorStateClass.MEASUREMENT, ] ATTR_ILLUMINANCE = [ "Illuminance", UNIT_LUMEN, - DEVICE_CLASS_ILLUMINANCE, - STATE_CLASS_MEASUREMENT, + SensorDeviceClass.ILLUMINANCE, + SensorStateClass.MEASUREMENT, ] ATTR_PRESSURE = [ "Pressure", PRESSURE_BAR, - DEVICE_CLASS_PRESSURE, - STATE_CLASS_MEASUREMENT, + SensorDeviceClass.PRESSURE, + SensorStateClass.MEASUREMENT, ] TEMP_SENSOR_MAP = { @@ -82,122 +74,122 @@ ENERGY_SENSOR_MAP = { "electricity_consumed": [ "Current Consumed Power", POWER_WATT, - DEVICE_CLASS_POWER, - STATE_CLASS_MEASUREMENT, + SensorDeviceClass.POWER, + SensorStateClass.MEASUREMENT, ], "electricity_produced": [ "Current Produced Power", POWER_WATT, - DEVICE_CLASS_POWER, - STATE_CLASS_MEASUREMENT, + SensorDeviceClass.POWER, + SensorStateClass.MEASUREMENT, ], "electricity_consumed_interval": [ "Consumed Power Interval", ENERGY_WATT_HOUR, - DEVICE_CLASS_ENERGY, - STATE_CLASS_TOTAL, + SensorDeviceClass.ENERGY, + SensorStateClass.TOTAL, ], "electricity_consumed_peak_interval": [ "Consumed Power Interval", ENERGY_WATT_HOUR, - DEVICE_CLASS_ENERGY, - STATE_CLASS_TOTAL, + SensorDeviceClass.ENERGY, + SensorStateClass.TOTAL, ], "electricity_consumed_off_peak_interval": [ "Consumed Power Interval (off peak)", ENERGY_WATT_HOUR, - DEVICE_CLASS_ENERGY, - STATE_CLASS_TOTAL, + SensorDeviceClass.ENERGY, + SensorStateClass.TOTAL, ], "electricity_produced_interval": [ "Produced Power Interval", ENERGY_WATT_HOUR, - DEVICE_CLASS_ENERGY, - STATE_CLASS_TOTAL, + SensorDeviceClass.ENERGY, + SensorStateClass.TOTAL, ], "electricity_produced_peak_interval": [ "Produced Power Interval", ENERGY_WATT_HOUR, - DEVICE_CLASS_ENERGY, - STATE_CLASS_TOTAL, + SensorDeviceClass.ENERGY, + SensorStateClass.TOTAL, ], "electricity_produced_off_peak_interval": [ "Produced Power Interval (off peak)", ENERGY_WATT_HOUR, - DEVICE_CLASS_ENERGY, - STATE_CLASS_TOTAL, + SensorDeviceClass.ENERGY, + SensorStateClass.TOTAL, ], "electricity_consumed_off_peak_point": [ "Current Consumed Power (off peak)", POWER_WATT, - DEVICE_CLASS_POWER, - STATE_CLASS_MEASUREMENT, + SensorDeviceClass.POWER, + SensorStateClass.MEASUREMENT, ], "electricity_consumed_peak_point": [ "Current Consumed Power", POWER_WATT, - DEVICE_CLASS_POWER, - STATE_CLASS_MEASUREMENT, + SensorDeviceClass.POWER, + SensorStateClass.MEASUREMENT, ], "electricity_consumed_off_peak_cumulative": [ "Cumulative Consumed Power (off peak)", ENERGY_KILO_WATT_HOUR, - DEVICE_CLASS_ENERGY, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass.ENERGY, + SensorStateClass.TOTAL_INCREASING, ], "electricity_consumed_peak_cumulative": [ "Cumulative Consumed Power", ENERGY_KILO_WATT_HOUR, - DEVICE_CLASS_ENERGY, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass.ENERGY, + SensorStateClass.TOTAL_INCREASING, ], "electricity_produced_off_peak_point": [ "Current Produced Power (off peak)", POWER_WATT, - DEVICE_CLASS_POWER, - STATE_CLASS_MEASUREMENT, + SensorDeviceClass.POWER, + SensorStateClass.MEASUREMENT, ], "electricity_produced_peak_point": [ "Current Produced Power", POWER_WATT, - DEVICE_CLASS_POWER, - STATE_CLASS_MEASUREMENT, + SensorDeviceClass.POWER, + SensorStateClass.MEASUREMENT, ], "electricity_produced_off_peak_cumulative": [ "Cumulative Produced Power (off peak)", ENERGY_KILO_WATT_HOUR, - DEVICE_CLASS_ENERGY, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass.ENERGY, + SensorStateClass.TOTAL_INCREASING, ], "electricity_produced_peak_cumulative": [ "Cumulative Produced Power", ENERGY_KILO_WATT_HOUR, - DEVICE_CLASS_ENERGY, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass.ENERGY, + SensorStateClass.TOTAL_INCREASING, ], "gas_consumed_interval": [ "Current Consumed Gas Interval", VOLUME_CUBIC_METERS, - DEVICE_CLASS_GAS, - STATE_CLASS_TOTAL, + SensorDeviceClass.GAS, + SensorStateClass.TOTAL, ], "gas_consumed_cumulative": [ "Consumed Gas", VOLUME_CUBIC_METERS, - DEVICE_CLASS_GAS, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass.GAS, + SensorStateClass.TOTAL_INCREASING, ], "net_electricity_point": [ "Current net Power", POWER_WATT, - DEVICE_CLASS_POWER, - STATE_CLASS_MEASUREMENT, + SensorDeviceClass.POWER, + SensorStateClass.MEASUREMENT, ], "net_electricity_cumulative": [ "Cumulative net Power", ENERGY_KILO_WATT_HOUR, - DEVICE_CLASS_ENERGY, - STATE_CLASS_TOTAL, + SensorDeviceClass.ENERGY, + SensorStateClass.TOTAL, ], } @@ -208,9 +200,14 @@ MISC_SENSOR_MAP = { "Heater Modulation Level", PERCENTAGE, None, - STATE_CLASS_MEASUREMENT, + SensorStateClass.MEASUREMENT, + ], + "valve_position": [ + "Valve Position", + PERCENTAGE, + None, + SensorStateClass.MEASUREMENT, ], - "valve_position": ["Valve Position", PERCENTAGE, None, STATE_CLASS_MEASUREMENT], "water_pressure": ATTR_PRESSURE, } From 2cc343bb7f5c1de5594a37ff4ac41b189a0587d6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 16 Dec 2021 15:43:15 +0100 Subject: [PATCH 0566/2644] Use new enums in nws (#61947) --- homeassistant/components/nws/const.py | 40 ++++++++++++-------------- homeassistant/components/nws/sensor.py | 3 +- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/nws/const.py b/homeassistant/components/nws/const.py index 1bef625eaf5..707186abebf 100644 --- a/homeassistant/components/nws/const.py +++ b/homeassistant/components/nws/const.py @@ -5,8 +5,9 @@ from dataclasses import dataclass from datetime import timedelta from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, + SensorDeviceClass, SensorEntityDescription, + SensorStateClass, ) from homeassistant.components.weather import ( ATTR_CONDITION_CLOUDY, @@ -25,9 +26,6 @@ from homeassistant.components.weather import ( ) from homeassistant.const import ( DEGREE, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, LENGTH_METERS, LENGTH_MILES, PERCENTAGE, @@ -115,8 +113,8 @@ SENSOR_TYPES: tuple[NWSSensorEntityDescription, ...] = ( key="dewpoint", name="Dew Point", icon=None, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, unit_convert=TEMP_CELSIUS, ), @@ -124,8 +122,8 @@ SENSOR_TYPES: tuple[NWSSensorEntityDescription, ...] = ( key="temperature", name="Temperature", icon=None, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, unit_convert=TEMP_CELSIUS, ), @@ -133,8 +131,8 @@ SENSOR_TYPES: tuple[NWSSensorEntityDescription, ...] = ( key="windChill", name="Wind Chill", icon=None, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, unit_convert=TEMP_CELSIUS, ), @@ -142,8 +140,8 @@ SENSOR_TYPES: tuple[NWSSensorEntityDescription, ...] = ( key="heatIndex", name="Heat Index", icon=None, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, unit_convert=TEMP_CELSIUS, ), @@ -151,8 +149,8 @@ SENSOR_TYPES: tuple[NWSSensorEntityDescription, ...] = ( key="relativeHumidity", name="Relative Humidity", icon=None, - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, unit_convert=PERCENTAGE, ), @@ -161,7 +159,7 @@ SENSOR_TYPES: tuple[NWSSensorEntityDescription, ...] = ( name="Wind Speed", icon="mdi:weather-windy", device_class=None, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, unit_convert=SPEED_MILES_PER_HOUR, ), @@ -170,7 +168,7 @@ SENSOR_TYPES: tuple[NWSSensorEntityDescription, ...] = ( name="Wind Gust", icon="mdi:weather-windy", device_class=None, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, unit_convert=SPEED_MILES_PER_HOUR, ), @@ -187,8 +185,8 @@ SENSOR_TYPES: tuple[NWSSensorEntityDescription, ...] = ( key="barometricPressure", name="Barometric Pressure", icon=None, - device_class=DEVICE_CLASS_PRESSURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PRESSURE_PA, unit_convert=PRESSURE_INHG, ), @@ -196,8 +194,8 @@ SENSOR_TYPES: tuple[NWSSensorEntityDescription, ...] = ( key="seaLevelPressure", name="Sea Level Pressure", icon=None, - device_class=DEVICE_CLASS_PRESSURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PRESSURE_PA, unit_convert=PRESSURE_INHG, ), @@ -206,7 +204,7 @@ SENSOR_TYPES: tuple[NWSSensorEntityDescription, ...] = ( name="Visibility", icon="mdi:eye", device_class=None, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=LENGTH_METERS, unit_convert=LENGTH_MILES, ), diff --git a/homeassistant/components/nws/sensor.py b/homeassistant/components/nws/sensor.py index 35bbcef838d..d3fe8833846 100644 --- a/homeassistant/components/nws/sensor.py +++ b/homeassistant/components/nws/sensor.py @@ -1,7 +1,6 @@ """Sensors for National Weather Service (NWS).""" from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_LATITUDE, CONF_LONGITUDE, LENGTH_METERS, @@ -57,7 +56,7 @@ class NWSSensor(CoordinatorEntity, SensorEntity): """An NWS Sensor Entity.""" entity_description: NWSSensorEntityDescription - _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + _attr_attribution = ATTRIBUTION def __init__( self, From 599c8f4757c5e91bcf9a8383d07715eb069b719b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 16 Dec 2021 15:49:11 +0100 Subject: [PATCH 0567/2644] Use new enums in neato (#61939) --- homeassistant/components/neato/sensor.py | 10 +++++----- homeassistant/components/neato/switch.py | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/neato/sensor.py b/homeassistant/components/neato/sensor.py index 3f7b925ef7f..7cc2d0f171a 100644 --- a/homeassistant/components/neato/sensor.py +++ b/homeassistant/components/neato/sensor.py @@ -8,11 +8,11 @@ from typing import Any from pybotvac.exceptions import NeatoRobotException from pybotvac.robot import Robot -from homeassistant.components.sensor import DEVICE_CLASS_BATTERY, SensorEntity +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE +from homeassistant.const import PERCENTAGE from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import NeatoHub @@ -81,12 +81,12 @@ class NeatoSensor(SensorEntity): @property def device_class(self) -> str: """Return the device class.""" - return DEVICE_CLASS_BATTERY + return SensorDeviceClass.BATTERY @property def entity_category(self) -> str: """Device entity category.""" - return ENTITY_CATEGORY_DIAGNOSTIC + return EntityCategory.DIAGNOSTIC @property def available(self) -> bool: diff --git a/homeassistant/components/neato/switch.py b/homeassistant/components/neato/switch.py index c34eea492e9..f66765ecf0d 100644 --- a/homeassistant/components/neato/switch.py +++ b/homeassistant/components/neato/switch.py @@ -9,9 +9,9 @@ from pybotvac.exceptions import NeatoRobotException from pybotvac.robot import Robot from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_CONFIG, STATE_OFF, STATE_ON +from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo, ToggleEntity +from homeassistant.helpers.entity import DeviceInfo, EntityCategory, ToggleEntity from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import NeatoHub @@ -109,7 +109,7 @@ class NeatoConnectedSwitch(ToggleEntity): @property def entity_category(self) -> str: """Device entity category.""" - return ENTITY_CATEGORY_CONFIG + return EntityCategory.CONFIG @property def device_info(self) -> DeviceInfo: From 7c8d2353567ccc3529c8b839d05668f4acb96746 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 16 Dec 2021 15:51:36 +0100 Subject: [PATCH 0568/2644] Use SensorDeviceClass in mfi (#61900) Co-authored-by: epenet --- homeassistant/components/mfi/sensor.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mfi/sensor.py b/homeassistant/components/mfi/sensor.py index b27f719d974..a951f10822f 100644 --- a/homeassistant/components/mfi/sensor.py +++ b/homeassistant/components/mfi/sensor.py @@ -5,7 +5,11 @@ from mficlient.client import FailedToLogin, MFiClient import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorDeviceClass, + SensorEntity, +) from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -13,7 +17,6 @@ from homeassistant.const import ( CONF_SSL, CONF_USERNAME, CONF_VERIFY_SSL, - DEVICE_CLASS_TEMPERATURE, STATE_OFF, STATE_ON, TEMP_CELSIUS, @@ -110,7 +113,7 @@ class MfiSensor(SensorEntity): return None if tag == "temperature": - return DEVICE_CLASS_TEMPERATURE + return SensorDeviceClass.TEMPERATURE return None From 16d16585ae26a603dc65455813f37c3b952f9f7d Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 10:00:22 -0500 Subject: [PATCH 0569/2644] Use enums in plaato (#62069) * Use enums in plaato * uno mas * uno mas --- homeassistant/components/plaato/binary_sensor.py | 12 ++++++------ homeassistant/components/plaato/sensor.py | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/plaato/binary_sensor.py b/homeassistant/components/plaato/binary_sensor.py index 52213d46791..14f2d2a069d 100644 --- a/homeassistant/components/plaato/binary_sensor.py +++ b/homeassistant/components/plaato/binary_sensor.py @@ -1,10 +1,10 @@ """Support for Plaato Airlock sensors.""" +from __future__ import annotations from pyplaato.plaato import PlaatoKeg from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_OPENING, - DEVICE_CLASS_PROBLEM, + BinarySensorDeviceClass, BinarySensorEntity, ) @@ -40,11 +40,11 @@ class PlaatoBinarySensor(PlaatoEntity, BinarySensorEntity): return False @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" + def device_class(self) -> BinarySensorDeviceClass | None: + """Return the class of this device, from BinarySensorDeviceClass.""" if self._coordinator is None: return None if self._sensor_type is PlaatoKeg.Pins.LEAK_DETECTION: - return DEVICE_CLASS_PROBLEM + return BinarySensorDeviceClass.PROBLEM if self._sensor_type is PlaatoKeg.Pins.POURING: - return DEVICE_CLASS_OPENING + return BinarySensorDeviceClass.OPENING diff --git a/homeassistant/components/plaato/sensor.py b/homeassistant/components/plaato/sensor.py index e3e37d4291e..398f44b72ff 100644 --- a/homeassistant/components/plaato/sensor.py +++ b/homeassistant/components/plaato/sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from pyplaato.models.device import PlaatoDevice from pyplaato.plaato import PlaatoKeg -from homeassistant.components.sensor import DEVICE_CLASS_TEMPERATURE, SensorEntity +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -63,15 +63,15 @@ class PlaatoSensor(PlaatoEntity, SensorEntity): """Representation of a Plaato Sensor.""" @property - def device_class(self) -> str | None: - """Return the class of this device, from component DEVICE_CLASSES.""" + def device_class(self) -> SensorDeviceClass | None: + """Return the class of this device, from SensorDeviceClass.""" if ( self._coordinator is not None and self._sensor_type == PlaatoKeg.Pins.TEMPERATURE ): - return DEVICE_CLASS_TEMPERATURE + return SensorDeviceClass.TEMPERATURE if self._sensor_type == ATTR_TEMP: - return DEVICE_CLASS_TEMPERATURE + return SensorDeviceClass.TEMPERATURE return None @property From 12ae684c96087bf0c3646137a071c40b71c78e65 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 16 Dec 2021 16:07:18 +0100 Subject: [PATCH 0570/2644] Minor refactor of template fan (#61856) --- homeassistant/components/template/fan.py | 115 +++++++---------------- 1 file changed, 36 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index d9481e8f7af..b264678e81e 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -36,8 +36,12 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.script import Script -from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN -from .template_entity import TemplateEntity +from .const import DOMAIN +from .template_entity import ( + TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY, + TemplateEntity, + rewrite_common_legacy_to_modern_conf, +) _LOGGER = logging.getLogger(__name__) @@ -70,7 +74,6 @@ FAN_SCHEMA = vol.All( vol.Optional(CONF_PRESET_MODE_TEMPLATE): cv.template, vol.Optional(CONF_OSCILLATING_TEMPLATE): cv.template, vol.Optional(CONF_DIRECTION_TEMPLATE): cv.template, - vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SET_PERCENTAGE_ACTION): cv.SCRIPT_SCHEMA, @@ -82,7 +85,7 @@ FAN_SCHEMA = vol.All( vol.Optional(CONF_ENTITY_ID): cv.entity_ids, vol.Optional(CONF_UNIQUE_ID): cv.string, } - ), + ).extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY.schema), ) PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( @@ -94,48 +97,17 @@ async def _async_create_entities(hass, config): """Create the Template Fans.""" fans = [] - for device, device_config in config[CONF_FANS].items(): - friendly_name = device_config.get(CONF_FRIENDLY_NAME, device) + for object_id, entity_config in config[CONF_FANS].items(): - state_template = device_config[CONF_VALUE_TEMPLATE] - percentage_template = device_config.get(CONF_PERCENTAGE_TEMPLATE) - preset_mode_template = device_config.get(CONF_PRESET_MODE_TEMPLATE) - oscillating_template = device_config.get(CONF_OSCILLATING_TEMPLATE) - direction_template = device_config.get(CONF_DIRECTION_TEMPLATE) - availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) + entity_config = rewrite_common_legacy_to_modern_conf(entity_config) - on_action = device_config[CONF_ON_ACTION] - off_action = device_config[CONF_OFF_ACTION] - set_speed_action = device_config.get(CONF_SET_SPEED_ACTION) - set_percentage_action = device_config.get(CONF_SET_PERCENTAGE_ACTION) - set_preset_mode_action = device_config.get(CONF_SET_PRESET_MODE_ACTION) - set_oscillating_action = device_config.get(CONF_SET_OSCILLATING_ACTION) - set_direction_action = device_config.get(CONF_SET_DIRECTION_ACTION) - - speed_count = device_config.get(CONF_SPEED_COUNT) - preset_modes = device_config.get(CONF_PRESET_MODES) - unique_id = device_config.get(CONF_UNIQUE_ID) + unique_id = entity_config.get(CONF_UNIQUE_ID) fans.append( TemplateFan( hass, - device, - friendly_name, - state_template, - percentage_template, - preset_mode_template, - oscillating_template, - direction_template, - availability_template, - on_action, - off_action, - set_speed_action, - set_percentage_action, - set_preset_mode_action, - set_oscillating_action, - set_direction_action, - speed_count, - preset_modes, + object_id, + entity_config, unique_id, ) ) @@ -154,69 +126,54 @@ class TemplateFan(TemplateEntity, FanEntity): def __init__( self, hass, - device_id, - friendly_name, - state_template, - percentage_template, - preset_mode_template, - oscillating_template, - direction_template, - availability_template, - on_action, - off_action, - set_speed_action, - set_percentage_action, - set_preset_mode_action, - set_oscillating_action, - set_direction_action, - speed_count, - preset_modes, + object_id, + config, unique_id, ): """Initialize the fan.""" - super().__init__(availability_template=availability_template) + super().__init__(config=config) self.hass = hass self.entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, device_id, hass=hass + ENTITY_ID_FORMAT, object_id, hass=hass ) - self._name = friendly_name + self._name = friendly_name = config.get(CONF_FRIENDLY_NAME, object_id) - self._template = state_template - self._percentage_template = percentage_template - self._preset_mode_template = preset_mode_template - self._oscillating_template = oscillating_template - self._direction_template = direction_template + self._template = config[CONF_VALUE_TEMPLATE] + self._percentage_template = config.get(CONF_PERCENTAGE_TEMPLATE) + self._preset_mode_template = config.get(CONF_PRESET_MODE_TEMPLATE) + self._oscillating_template = config.get(CONF_OSCILLATING_TEMPLATE) + self._direction_template = config.get(CONF_DIRECTION_TEMPLATE) self._supported_features = 0 - self._on_script = Script(hass, on_action, friendly_name, DOMAIN) - self._off_script = Script(hass, off_action, friendly_name, DOMAIN) + self._on_script = Script(hass, config[CONF_ON_ACTION], friendly_name, DOMAIN) + self._off_script = Script(hass, config[CONF_OFF_ACTION], friendly_name, DOMAIN) self._set_speed_script = None - if set_speed_action: + if set_speed_action := config.get(CONF_SET_SPEED_ACTION): self._set_speed_script = Script( hass, set_speed_action, friendly_name, DOMAIN ) self._set_percentage_script = None - if set_percentage_action: + if set_percentage_action := config.get(CONF_SET_PERCENTAGE_ACTION): self._set_percentage_script = Script( hass, set_percentage_action, friendly_name, DOMAIN ) self._set_preset_mode_script = None - if set_preset_mode_action: + if set_preset_mode_action := config.get(CONF_SET_PRESET_MODE_ACTION): self._set_preset_mode_script = Script( hass, set_preset_mode_action, friendly_name, DOMAIN ) self._set_oscillating_script = None - if set_oscillating_action: + if set_oscillating_action := config.get(CONF_SET_OSCILLATING_ACTION): self._set_oscillating_script = Script( hass, set_oscillating_action, friendly_name, DOMAIN ) self._set_direction_script = None - if set_direction_action: + if set_direction_action := config.get(CONF_SET_DIRECTION_ACTION): self._set_direction_script = Script( hass, set_direction_action, friendly_name, DOMAIN ) @@ -227,9 +184,15 @@ class TemplateFan(TemplateEntity, FanEntity): self._oscillating = None self._direction = None + # Number of valid speeds + self._speed_count = config.get(CONF_SPEED_COUNT) + + # List of valid preset modes + self._preset_modes = config.get(CONF_PRESET_MODES) + if self._percentage_template: self._supported_features |= SUPPORT_SET_SPEED - if self._preset_mode_template and preset_modes: + if self._preset_mode_template and self._preset_modes: self._supported_features |= SUPPORT_PRESET_MODE if self._oscillating_template: self._supported_features |= SUPPORT_OSCILLATE @@ -238,12 +201,6 @@ class TemplateFan(TemplateEntity, FanEntity): self._unique_id = unique_id - # Number of valid speeds - self._speed_count = speed_count - - # List of valid preset modes - self._preset_modes = preset_modes - @property def name(self): """Return the display name of this fan.""" From 1fb69fb69ae6fade594bc3dcbe960e813017c2ab Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 10:09:19 -0500 Subject: [PATCH 0571/2644] Use enums in rpi_power (#62046) --- homeassistant/components/rpi_power/binary_sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rpi_power/binary_sensor.py b/homeassistant/components/rpi_power/binary_sensor.py index f223bbf2c91..f70581a8075 100644 --- a/homeassistant/components/rpi_power/binary_sensor.py +++ b/homeassistant/components/rpi_power/binary_sensor.py @@ -8,7 +8,7 @@ import logging from rpi_bad_power import UnderVoltage, new_under_voltage from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_PROBLEM, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -34,7 +34,7 @@ async def async_setup_entry( class RaspberryChargerBinarySensor(BinarySensorEntity): """Binary sensor representing the rpi power status.""" - _attr_device_class = DEVICE_CLASS_PROBLEM + _attr_device_class = BinarySensorDeviceClass.PROBLEM _attr_icon = "mdi:raspberry-pi" _attr_name = "RPi Power status" _attr_unique_id = "rpi_power" # only one sensor possible From a0b6edc894fe1f0dacd108068afcbbc54e4827a7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 16 Dec 2021 16:11:23 +0100 Subject: [PATCH 0572/2644] Minor refactor of template light (#61857) --- homeassistant/components/template/light.py | 142 +++++---------------- 1 file changed, 34 insertions(+), 108 deletions(-) diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index 5d172489840..a96d01fe4fa 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -21,9 +21,7 @@ from homeassistant.components.light import ( ) from homeassistant.const import ( CONF_ENTITY_ID, - CONF_ENTITY_PICTURE_TEMPLATE, CONF_FRIENDLY_NAME, - CONF_ICON_TEMPLATE, CONF_LIGHTS, CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, @@ -37,8 +35,12 @@ from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.script import Script -from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN -from .template_entity import TemplateEntity +from .const import DOMAIN +from .template_entity import ( + TEMPLATE_ENTITY_COMMON_SCHEMA_LEGACY, + TemplateEntity, + rewrite_common_legacy_to_modern_conf, +) _LOGGER = logging.getLogger(__name__) _VALID_STATES = [STATE_ON, STATE_OFF, "true", "false"] @@ -67,9 +69,6 @@ LIGHT_SCHEMA = vol.All( vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_ICON_TEMPLATE): cv.template, - vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE): cv.template, - vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, vol.Optional(CONF_LEVEL_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_LEVEL_TEMPLATE): cv.template, vol.Optional(CONF_FRIENDLY_NAME): cv.string, @@ -88,7 +87,7 @@ LIGHT_SCHEMA = vol.All( vol.Optional(CONF_SUPPORTS_TRANSITION): cv.template, vol.Optional(CONF_UNIQUE_ID): cv.string, } - ), + ).extend(TEMPLATE_ENTITY_COMMON_SCHEMA_LEGACY.schema), ) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -100,64 +99,15 @@ async def _async_create_entities(hass, config): """Create the Template Lights.""" lights = [] - for device, device_config in config[CONF_LIGHTS].items(): - friendly_name = device_config.get(CONF_FRIENDLY_NAME, device) - - state_template = device_config.get(CONF_VALUE_TEMPLATE) - icon_template = device_config.get(CONF_ICON_TEMPLATE) - entity_picture_template = device_config.get(CONF_ENTITY_PICTURE_TEMPLATE) - availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) - unique_id = device_config.get(CONF_UNIQUE_ID) - - on_action = device_config[CONF_ON_ACTION] - off_action = device_config[CONF_OFF_ACTION] - - level_action = device_config.get(CONF_LEVEL_ACTION) - level_template = device_config.get(CONF_LEVEL_TEMPLATE) - - temperature_action = device_config.get(CONF_TEMPERATURE_ACTION) - temperature_template = device_config.get(CONF_TEMPERATURE_TEMPLATE) - - color_action = device_config.get(CONF_COLOR_ACTION) - color_template = device_config.get(CONF_COLOR_TEMPLATE) - - white_value_action = device_config.get(CONF_WHITE_VALUE_ACTION) - white_value_template = device_config.get(CONF_WHITE_VALUE_TEMPLATE) - - effect_action = device_config.get(CONF_EFFECT_ACTION) - effect_list_template = device_config.get(CONF_EFFECT_LIST_TEMPLATE) - effect_template = device_config.get(CONF_EFFECT_TEMPLATE) - - max_mireds_template = device_config.get(CONF_MAX_MIREDS_TEMPLATE) - min_mireds_template = device_config.get(CONF_MIN_MIREDS_TEMPLATE) - - supports_transition_template = device_config.get(CONF_SUPPORTS_TRANSITION) + for object_id, entity_config in config[CONF_LIGHTS].items(): + entity_config = rewrite_common_legacy_to_modern_conf(entity_config) + unique_id = entity_config.get(CONF_UNIQUE_ID) lights.append( LightTemplate( hass, - device, - friendly_name, - state_template, - icon_template, - entity_picture_template, - availability_template, - on_action, - off_action, - level_action, - level_template, - temperature_action, - temperature_template, - color_action, - color_template, - white_value_action, - white_value_template, - effect_action, - effect_list_template, - effect_template, - max_mireds_template, - min_mireds_template, - supports_transition_template, + object_id, + entity_config, unique_id, ) ) @@ -176,71 +126,47 @@ class LightTemplate(TemplateEntity, LightEntity): def __init__( self, hass, - device_id, - friendly_name, - state_template, - icon_template, - entity_picture_template, - availability_template, - on_action, - off_action, - level_action, - level_template, - temperature_action, - temperature_template, - color_action, - color_template, - white_value_action, - white_value_template, - effect_action, - effect_list_template, - effect_template, - max_mireds_template, - min_mireds_template, - supports_transition_template, + object_id, + config, unique_id, ): """Initialize the light.""" - super().__init__( - availability_template=availability_template, - icon_template=icon_template, - entity_picture_template=entity_picture_template, - ) + super().__init__(config=config) self.entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, device_id, hass=hass + ENTITY_ID_FORMAT, object_id, hass=hass ) - self._name = friendly_name - self._template = state_template - self._on_script = Script(hass, on_action, friendly_name, DOMAIN) - self._off_script = Script(hass, off_action, friendly_name, DOMAIN) + self._name = friendly_name = config.get(CONF_FRIENDLY_NAME, object_id) + self._template = config.get(CONF_VALUE_TEMPLATE) + self._on_script = Script(hass, config[CONF_ON_ACTION], friendly_name, DOMAIN) + self._off_script = Script(hass, config[CONF_OFF_ACTION], friendly_name, DOMAIN) self._level_script = None - if level_action is not None: + if (level_action := config.get(CONF_LEVEL_ACTION)) is not None: self._level_script = Script(hass, level_action, friendly_name, DOMAIN) - self._level_template = level_template + self._level_template = config.get(CONF_LEVEL_TEMPLATE) self._temperature_script = None - if temperature_action is not None: + if (temperature_action := config.get(CONF_TEMPERATURE_ACTION)) is not None: self._temperature_script = Script( hass, temperature_action, friendly_name, DOMAIN ) - self._temperature_template = temperature_template + self._temperature_template = config.get(CONF_TEMPERATURE_TEMPLATE) self._color_script = None - if color_action is not None: + if (color_action := config.get(CONF_COLOR_ACTION)) is not None: self._color_script = Script(hass, color_action, friendly_name, DOMAIN) - self._color_template = color_template + self._color_template = config.get(CONF_COLOR_TEMPLATE) self._white_value_script = None - if white_value_action is not None: + if (white_value_action := config.get(CONF_WHITE_VALUE_ACTION)) is not None: self._white_value_script = Script( hass, white_value_action, friendly_name, DOMAIN ) - self._white_value_template = white_value_template + self._white_value_template = config.get(CONF_WHITE_VALUE_TEMPLATE) self._effect_script = None - if effect_action is not None: + if (effect_action := config.get(CONF_EFFECT_ACTION)) is not None: self._effect_script = Script(hass, effect_action, friendly_name, DOMAIN) - self._effect_list_template = effect_list_template - self._effect_template = effect_template - self._max_mireds_template = max_mireds_template - self._min_mireds_template = min_mireds_template - self._supports_transition_template = supports_transition_template + self._effect_list_template = config.get(CONF_EFFECT_LIST_TEMPLATE) + self._effect_template = config.get(CONF_EFFECT_TEMPLATE) + self._max_mireds_template = config.get(CONF_MAX_MIREDS_TEMPLATE) + self._min_mireds_template = config.get(CONF_MIN_MIREDS_TEMPLATE) + self._supports_transition_template = config.get(CONF_SUPPORTS_TRANSITION) self._state = False self._brightness = None From 1b66f3208d2c476858a9b1c4f4a34830912d8f09 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 10:11:55 -0500 Subject: [PATCH 0573/2644] Use enums in ozw (#62088) --- homeassistant/components/ozw/binary_sensor.py | 62 ++++++++----------- homeassistant/components/ozw/cover.py | 6 +- homeassistant/components/ozw/sensor.py | 25 +++----- 3 files changed, 39 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/ozw/binary_sensor.py b/homeassistant/components/ozw/binary_sensor.py index 91b26218c97..a3efd36e0ab 100644 --- a/homeassistant/components/ozw/binary_sensor.py +++ b/homeassistant/components/ozw/binary_sensor.py @@ -2,18 +2,8 @@ from openzwavemqtt.const import CommandClass, ValueIndex, ValueType from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_DOOR, - DEVICE_CLASS_GAS, - DEVICE_CLASS_HEAT, - DEVICE_CLASS_LOCK, - DEVICE_CLASS_MOISTURE, - DEVICE_CLASS_MOTION, - DEVICE_CLASS_POWER, - DEVICE_CLASS_PROBLEM, - DEVICE_CLASS_SAFETY, - DEVICE_CLASS_SMOKE, - DEVICE_CLASS_SOUND, DOMAIN as BINARY_SENSOR_DOMAIN, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.core import callback @@ -38,79 +28,79 @@ NOTIFICATION_SENSORS = [ # Assuming here that Value 1 and 2 are not present at the same time NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_SMOKE_ALARM, NOTIFICATION_VALUES: [1, 2], - NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_SMOKE, + NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.SMOKE, }, { # Index 1: Smoke Alarm - All other Value Id's # Create as disabled sensors NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_SMOKE_ALARM, NOTIFICATION_VALUES: [3, 4, 5, 6, 7, 8], - NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_SMOKE, + NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.SMOKE, NOTIFICATION_SENSOR_ENABLED: False, }, { # Index 2: Carbon Monoxide - Value Id's 1 and 2 NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_CARBON_MONOOXIDE, NOTIFICATION_VALUES: [1, 2], - NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_GAS, + NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.GAS, }, { # Index 2: Carbon Monoxide - All other Value Id's NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_CARBON_MONOOXIDE, NOTIFICATION_VALUES: [4, 5, 7], - NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_GAS, + NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.GAS, NOTIFICATION_SENSOR_ENABLED: False, }, { # Index 3: Carbon Dioxide - Value Id's 1 and 2 NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_CARBON_DIOXIDE, NOTIFICATION_VALUES: [1, 2], - NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_GAS, + NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.GAS, }, { # Index 3: Carbon Dioxide - All other Value Id's NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_CARBON_DIOXIDE, NOTIFICATION_VALUES: [4, 5, 7], - NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_GAS, + NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.GAS, NOTIFICATION_SENSOR_ENABLED: False, }, { # Index 4: Heat - Value Id's 1, 2, 5, 6 (heat/underheat) NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_HEAT, NOTIFICATION_VALUES: [1, 2, 5, 6], - NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_HEAT, + NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.HEAT, }, { # Index 4: Heat - All other Value Id's NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_HEAT, NOTIFICATION_VALUES: [3, 4, 8, 10, 11], - NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_HEAT, + NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.HEAT, NOTIFICATION_SENSOR_ENABLED: False, }, { # Index 5: Water - Value Id's 1, 2, 3, 4 NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_WATER, NOTIFICATION_VALUES: [1, 2, 3, 4], - NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_MOISTURE, + NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.MOISTURE, }, { # Index 5: Water - All other Value Id's NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_WATER, NOTIFICATION_VALUES: [5], - NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_MOISTURE, + NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.MOISTURE, NOTIFICATION_SENSOR_ENABLED: False, }, { # Index 6: Access Control - Value Id's 1, 2, 3, 4 (Lock) NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_ACCESS_CONTROL, NOTIFICATION_VALUES: [1, 2, 3, 4], - NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_LOCK, + NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.LOCK, }, { # Index 6: Access Control - Value Id 22 (door/window open) NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_ACCESS_CONTROL, NOTIFICATION_VALUES: [22], - NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_DOOR, + NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.DOOR, NOTIFICATION_OFF_VALUE: 23, }, { @@ -118,32 +108,32 @@ NOTIFICATION_SENSORS = [ # Assuming that value 1 and 2 are not present at the same time NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_HOME_SECURITY, NOTIFICATION_VALUES: [1, 2], - NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_SAFETY, + NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.SAFETY, }, { # Index 7: Home Security - Value Id's 3, 4, 9 (tampering) NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_HOME_SECURITY, NOTIFICATION_VALUES: [3, 4, 9], - NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_SAFETY, + NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.SAFETY, }, { # Index 7: Home Security - Value Id's 5, 6 (glass breakage) # Assuming that value 5 and 6 are not present at the same time NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_HOME_SECURITY, NOTIFICATION_VALUES: [5, 6], - NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_SAFETY, + NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.SAFETY, }, { # Index 7: Home Security - Value Id's 7, 8 (motion) NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_HOME_SECURITY, NOTIFICATION_VALUES: [7, 8], - NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_MOTION, + NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.MOTION, }, { # Index 8: Power management - Values 1...9 NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_POWER_MANAGEMENT, NOTIFICATION_VALUES: [1, 2, 3, 4, 5, 6, 7, 8, 9], - NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_POWER, + NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.POWER, NOTIFICATION_SENSOR_ENABLED: False, }, { @@ -151,7 +141,7 @@ NOTIFICATION_SENSORS = [ # Battery values (mutually exclusive) NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_POWER_MANAGEMENT, NOTIFICATION_VALUES: [10, 11, 12, 13, 14, 15], - NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_POWER, + NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.POWER, NOTIFICATION_SENSOR_ENABLED: False, NOTIFICATION_OFF_VALUE: None, }, @@ -159,14 +149,14 @@ NOTIFICATION_SENSORS = [ # Index 9: System - Value Id's 1, 2, 6, 7 NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_SYSTEM, NOTIFICATION_VALUES: [1, 2, 6, 7], - NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_PROBLEM, + NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.PROBLEM, NOTIFICATION_SENSOR_ENABLED: False, }, { # Index 10: Emergency - Value Id's 1, 2, 3 NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_EMERGENCY, NOTIFICATION_VALUES: [1, 2, 3], - NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_PROBLEM, + NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.PROBLEM, }, { # Index 11: Clock - Value Id's 1, 2 @@ -213,20 +203,20 @@ NOTIFICATION_SENSORS = [ # Index 14: Siren NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_SIREN, NOTIFICATION_VALUES: [1], - NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_SOUND, + NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.SOUND, }, { # Index 15: Water valve # ignore non-boolean values NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_WATER_VALVE, NOTIFICATION_VALUES: [3, 4], - NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_PROBLEM, + NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.PROBLEM, }, { # Index 16: Weather NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_WEATHER, NOTIFICATION_VALUES: [1, 2], - NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_PROBLEM, + NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.PROBLEM, }, { # Index 17: Irrigation @@ -239,13 +229,13 @@ NOTIFICATION_SENSORS = [ # Index 18: Gas NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_GAS, NOTIFICATION_VALUES: [1, 2, 3, 4], - NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_GAS, + NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.GAS, }, { # Index 18: Gas NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_GAS, NOTIFICATION_VALUES: [6], - NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_PROBLEM, + NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.PROBLEM, }, ] diff --git a/homeassistant/components/ozw/cover.py b/homeassistant/components/ozw/cover.py index 1c708b55ffb..e8f1fc99872 100644 --- a/homeassistant/components/ozw/cover.py +++ b/homeassistant/components/ozw/cover.py @@ -3,10 +3,10 @@ from openzwavemqtt.const import CommandClass from homeassistant.components.cover import ( ATTR_POSITION, - DEVICE_CLASS_GARAGE, DOMAIN as COVER_DOMAIN, SUPPORT_CLOSE, SUPPORT_OPEN, + CoverDeviceClass, CoverEntity, ) from homeassistant.core import callback @@ -94,8 +94,8 @@ class ZwaveGarageDoorBarrier(ZWaveDeviceEntity, CoverEntity): @property def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return DEVICE_CLASS_GARAGE + """Return the class of this device, from CoverDeviceClass.""" + return CoverDeviceClass.GARAGE @property def is_opening(self): diff --git a/homeassistant/components/ozw/sensor.py b/homeassistant/components/ozw/sensor.py index 97b7b01d4d4..0813870e584 100644 --- a/homeassistant/components/ozw/sensor.py +++ b/homeassistant/components/ozw/sensor.py @@ -5,13 +5,8 @@ import logging from openzwavemqtt.const import CommandClass, ValueType from homeassistant.components.sensor import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_POWER, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, DOMAIN as SENSOR_DOMAIN, + SensorDeviceClass, SensorEntity, ) from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT @@ -65,23 +60,23 @@ class ZwaveSensorBase(ZWaveDeviceEntity, SensorEntity): def device_class(self): """Return the device class of the sensor.""" if self.values.primary.command_class == CommandClass.BATTERY: - return DEVICE_CLASS_BATTERY + return SensorDeviceClass.BATTERY if self.values.primary.command_class == CommandClass.METER: - return DEVICE_CLASS_POWER + return SensorDeviceClass.POWER if "Temperature" in self.values.primary.label: - return DEVICE_CLASS_TEMPERATURE + return SensorDeviceClass.TEMPERATURE if "Illuminance" in self.values.primary.label: - return DEVICE_CLASS_ILLUMINANCE + return SensorDeviceClass.ILLUMINANCE if "Humidity" in self.values.primary.label: - return DEVICE_CLASS_HUMIDITY + return SensorDeviceClass.HUMIDITY if "Power" in self.values.primary.label: - return DEVICE_CLASS_POWER + return SensorDeviceClass.POWER if "Energy" in self.values.primary.label: - return DEVICE_CLASS_POWER + return SensorDeviceClass.POWER if "Electric" in self.values.primary.label: - return DEVICE_CLASS_POWER + return SensorDeviceClass.POWER if "Pressure" in self.values.primary.label: - return DEVICE_CLASS_PRESSURE + return SensorDeviceClass.PRESSURE return None @property From 9bf7e25e6c87900311e8bfcae38a5fd446f22dc7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 16 Dec 2021 16:12:24 +0100 Subject: [PATCH 0574/2644] Minor refactor of template lock (#61858) --- homeassistant/components/template/lock.py | 48 ++++++++--------------- 1 file changed, 16 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/template/lock.py b/homeassistant/components/template/lock.py index a078ce778b6..8f73796c37c 100644 --- a/homeassistant/components/template/lock.py +++ b/homeassistant/components/template/lock.py @@ -22,8 +22,12 @@ from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.script import Script -from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN -from .template_entity import TemplateEntity +from .const import DOMAIN +from .template_entity import ( + TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY, + TemplateEntity, + rewrite_common_legacy_to_modern_conf, +) CONF_LOCK = "lock" CONF_UNLOCK = "unlock" @@ -37,31 +41,16 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Required(CONF_LOCK): cv.SCRIPT_SCHEMA, vol.Required(CONF_UNLOCK): cv.SCRIPT_SCHEMA, vol.Required(CONF_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_UNIQUE_ID): cv.string, } -) +).extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY.schema) async def _async_create_entities(hass, config): """Create the Template lock.""" - device = config.get(CONF_NAME) - value_template = config.get(CONF_VALUE_TEMPLATE) - availability_template = config.get(CONF_AVAILABILITY_TEMPLATE) - - return [ - TemplateLock( - hass, - device, - value_template, - availability_template, - config.get(CONF_LOCK), - config.get(CONF_UNLOCK), - config.get(CONF_OPTIMISTIC), - config.get(CONF_UNIQUE_ID), - ) - ] + config = rewrite_common_legacy_to_modern_conf(config) + return [TemplateLock(hass, config, config.get(CONF_UNIQUE_ID))] async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -75,22 +64,17 @@ class TemplateLock(TemplateEntity, LockEntity): def __init__( self, hass, - name, - value_template, - availability_template, - command_lock, - command_unlock, - optimistic, + config, unique_id, ): """Initialize the lock.""" - super().__init__(availability_template=availability_template) + super().__init__(config=config) self._state = None - self._name = name - self._state_template = value_template - self._command_lock = Script(hass, command_lock, name, DOMAIN) - self._command_unlock = Script(hass, command_unlock, name, DOMAIN) - self._optimistic = optimistic + self._name = name = config.get(CONF_NAME) + self._state_template = config.get(CONF_VALUE_TEMPLATE) + self._command_lock = Script(hass, config[CONF_LOCK], name, DOMAIN) + self._command_unlock = Script(hass, config[CONF_UNLOCK], name, DOMAIN) + self._optimistic = config.get(CONF_OPTIMISTIC) self._unique_id = unique_id @property From c9320b5ca10b46871570bbcb34212d90e6cac55c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 16 Dec 2021 16:12:43 +0100 Subject: [PATCH 0575/2644] Fix none-check in template light (#62089) --- homeassistant/components/template/light.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index a96d01fe4fa..317e1349dc9 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -567,9 +567,13 @@ class LightTemplate(TemplateEntity, LightEntity): @callback def _update_color(self, render): """Update the hs_color from the template.""" + if render is None: + self._color = None + return + h_str = s_str = None if isinstance(render, str): - if render in (None, "None", ""): + if render in ("None", ""): self._color = None return h_str, s_str = map( From bb3a3bbc1b71df4def3e7070760b6834fd235c6d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 16 Dec 2021 16:22:17 +0100 Subject: [PATCH 0576/2644] Minor refactor of template number (#61863) --- homeassistant/components/template/number.py | 90 +++++++------------ .../components/template/template_entity.py | 12 +++ 2 files changed, 45 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/template/number.py b/homeassistant/components/template/number.py index da2272c3138..fd8b99837b5 100644 --- a/homeassistant/components/template/number.py +++ b/homeassistant/components/template/number.py @@ -17,22 +17,20 @@ from homeassistant.components.number.const import ( DEFAULT_MIN_VALUE, DOMAIN as NUMBER_DOMAIN, ) -from homeassistant.const import ( - CONF_ICON, - CONF_NAME, - CONF_OPTIMISTIC, - CONF_STATE, - CONF_UNIQUE_ID, -) +from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_STATE, CONF_UNIQUE_ID from homeassistant.core import Config, HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.script import Script -from homeassistant.helpers.template import Template, TemplateError +from homeassistant.helpers.template import TemplateError from . import TriggerUpdateCoordinator -from .const import CONF_AVAILABILITY, DOMAIN -from .template_entity import TemplateEntity +from .const import DOMAIN +from .template_entity import ( + TEMPLATE_ENTITY_AVAILABILITY_SCHEMA, + TEMPLATE_ENTITY_ICON_SCHEMA, + TemplateEntity, +) from .trigger_entity import TriggerEntity _LOGGER = logging.getLogger(__name__) @@ -42,19 +40,21 @@ CONF_SET_VALUE = "set_value" DEFAULT_NAME = "Template Number" DEFAULT_OPTIMISTIC = False -NUMBER_SCHEMA = vol.Schema( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.template, - vol.Required(CONF_STATE): cv.template, - vol.Required(CONF_SET_VALUE): cv.SCRIPT_SCHEMA, - vol.Required(ATTR_STEP): cv.template, - vol.Optional(ATTR_MIN, default=DEFAULT_MIN_VALUE): cv.template, - vol.Optional(ATTR_MAX, default=DEFAULT_MAX_VALUE): cv.template, - vol.Optional(CONF_AVAILABILITY): cv.template, - vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, - vol.Optional(CONF_UNIQUE_ID): cv.string, - vol.Optional(CONF_ICON): cv.template, - } +NUMBER_SCHEMA = ( + vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.template, + vol.Required(CONF_STATE): cv.template, + vol.Required(CONF_SET_VALUE): cv.SCRIPT_SCHEMA, + vol.Required(ATTR_STEP): cv.template, + vol.Optional(ATTR_MIN, default=DEFAULT_MIN_VALUE): cv.template, + vol.Optional(ATTR_MAX, default=DEFAULT_MAX_VALUE): cv.template, + vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, + vol.Optional(CONF_UNIQUE_ID): cv.string, + } + ) + .extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA.schema) + .extend(TEMPLATE_ENTITY_ICON_SCHEMA.schema) ) @@ -67,21 +67,7 @@ async def _async_create_entities( unique_id = definition.get(CONF_UNIQUE_ID) if unique_id and unique_id_prefix: unique_id = f"{unique_id_prefix}-{unique_id}" - entities.append( - TemplateNumber( - hass, - definition[CONF_NAME], - definition[CONF_STATE], - definition.get(CONF_AVAILABILITY), - definition[CONF_SET_VALUE], - definition[ATTR_STEP], - definition[ATTR_MIN], - definition[ATTR_MAX], - definition[CONF_OPTIMISTIC], - unique_id, - definition.get(CONF_ICON), - ) - ) + entities.append(TemplateNumber(hass, definition, unique_id)) return entities @@ -118,34 +104,24 @@ class TemplateNumber(TemplateEntity, NumberEntity): def __init__( self, hass: HomeAssistant, - name_template: Template, - value_template: Template, - availability_template: Template | None, - command_set_value: dict[str, Any], - step_template: Template, - minimum_template: Template | None, - maximum_template: Template | None, - optimistic: bool, + config, unique_id: str | None, - icon_template: Template | None, ) -> None: """Initialize the number.""" - super().__init__( - availability_template=availability_template, icon_template=icon_template - ) + super().__init__(config=config) self._attr_name = DEFAULT_NAME - self._name_template = name_template + self._name_template = name_template = config[CONF_NAME] name_template.hass = hass with contextlib.suppress(TemplateError): self._attr_name = name_template.async_render(parse_result=False) - self._value_template = value_template + self._value_template = config[CONF_STATE] self._command_set_value = Script( - hass, command_set_value, self._attr_name, DOMAIN + hass, config[CONF_SET_VALUE], self._attr_name, DOMAIN ) - self._step_template = step_template - self._min_value_template = minimum_template - self._max_value_template = maximum_template - self._attr_assumed_state = self._optimistic = optimistic + self._step_template = config[ATTR_STEP] + self._min_value_template = config[ATTR_MIN] + self._max_value_template = config[ATTR_MAX] + self._attr_assumed_state = self._optimistic = config[CONF_OPTIMISTIC] self._attr_unique_id = unique_id self._attr_value = None self._attr_step = None diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index 55c9dbcf45b..11b9edc7626 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -37,6 +37,18 @@ from .const import ( _LOGGER = logging.getLogger(__name__) +TEMPLATE_ENTITY_AVAILABILITY_SCHEMA = vol.Schema( + { + vol.Optional(CONF_AVAILABILITY): cv.template, + } +) + +TEMPLATE_ENTITY_ICON_SCHEMA = vol.Schema( + { + vol.Optional(CONF_ICON): cv.template, + } +) + TEMPLATE_ENTITY_COMMON_SCHEMA = vol.Schema( { vol.Optional(CONF_ATTRIBUTES): vol.Schema({cv.string: cv.template}), From e39dcd7152f3ba9ff674e41c63d4eb5ca9b6d1b8 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 10:33:50 -0500 Subject: [PATCH 0577/2644] Use enums in openuv (#62085) --- homeassistant/components/openuv/sensor.py | 25 ++++++++++++----------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/openuv/sensor.py b/homeassistant/components/openuv/sensor.py index 0660ca740ac..467ef0dfe3a 100644 --- a/homeassistant/components/openuv/sensor.py +++ b/homeassistant/components/openuv/sensor.py @@ -2,12 +2,13 @@ from __future__ import annotations from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import DEVICE_CLASS_OZONE, TIME_MINUTES, UV_INDEX +from homeassistant.const import TIME_MINUTES, UV_INDEX from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import as_local, parse_datetime @@ -49,16 +50,16 @@ SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=TYPE_CURRENT_OZONE_LEVEL, name="Current Ozone Level", - device_class=DEVICE_CLASS_OZONE, + device_class=SensorDeviceClass.OZONE, native_unit_of_measurement="du", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_CURRENT_UV_INDEX, name="Current UV Index", icon="mdi:weather-sunny", native_unit_of_measurement=UV_INDEX, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_CURRENT_UV_LEVEL, @@ -70,49 +71,49 @@ SENSOR_DESCRIPTIONS = ( name="Max UV Index", icon="mdi:weather-sunny", native_unit_of_measurement=UV_INDEX, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_1, name="Skin Type 1 Safe Exposure Time", icon="mdi:timer-outline", native_unit_of_measurement=TIME_MINUTES, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_2, name="Skin Type 2 Safe Exposure Time", icon="mdi:timer-outline", native_unit_of_measurement=TIME_MINUTES, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_3, name="Skin Type 3 Safe Exposure Time", icon="mdi:timer-outline", native_unit_of_measurement=TIME_MINUTES, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_4, name="Skin Type 4 Safe Exposure Time", icon="mdi:timer-outline", native_unit_of_measurement=TIME_MINUTES, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_5, name="Skin Type 5 Safe Exposure Time", icon="mdi:timer-outline", native_unit_of_measurement=TIME_MINUTES, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_6, name="Skin Type 6 Safe Exposure Time", icon="mdi:timer-outline", native_unit_of_measurement=TIME_MINUTES, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), ) From f9cc6c069eb1ba8837d50e4fcd48d70cd5419681 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 10:35:18 -0500 Subject: [PATCH 0578/2644] Use enums in recollect_waste (#62060) --- homeassistant/components/recollect_waste/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py index 0eb2f584fcc..6d27dfd1aed 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -3,9 +3,9 @@ from __future__ import annotations from aiorecollect.client import PickupType -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_FRIENDLY_NAME, DEVICE_CLASS_DATE +from homeassistant.const import CONF_FRIENDLY_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( @@ -47,7 +47,7 @@ async def async_setup_entry( class ReCollectWasteSensor(CoordinatorEntity, SensorEntity): """ReCollect Waste Sensor.""" - _attr_device_class = DEVICE_CLASS_DATE + _attr_device_class = SensorDeviceClass.DATE def __init__(self, coordinator: DataUpdateCoordinator, entry: ConfigEntry) -> None: """Initialize the sensor.""" From d660d68c346c90f452fe160055221194086deb42 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 10:36:16 -0500 Subject: [PATCH 0579/2644] Use enums in rainmachine (#62058) --- .../components/rainmachine/binary_sensor.py | 18 +++++------ .../components/rainmachine/sensor.py | 32 ++++++++----------- .../components/rainmachine/switch.py | 5 +-- 3 files changed, 26 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/rainmachine/binary_sensor.py b/homeassistant/components/rainmachine/binary_sensor.py index 853b9f24b33..1c850915e26 100644 --- a/homeassistant/components/rainmachine/binary_sensor.py +++ b/homeassistant/components/rainmachine/binary_sensor.py @@ -7,8 +7,8 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import RainMachineEntity @@ -51,28 +51,28 @@ BINARY_SENSOR_DESCRIPTIONS = ( key=TYPE_FREEZE, name="Freeze Restrictions", icon="mdi:cancel", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, api_category=DATA_RESTRICTIONS_CURRENT, ), RainMachineBinarySensorDescription( key=TYPE_FREEZE_PROTECTION, name="Freeze Protection", icon="mdi:weather-snowy", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, api_category=DATA_RESTRICTIONS_UNIVERSAL, ), RainMachineBinarySensorDescription( key=TYPE_HOT_DAYS, name="Extra Water on Hot Days", icon="mdi:thermometer-lines", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, api_category=DATA_RESTRICTIONS_UNIVERSAL, ), RainMachineBinarySensorDescription( key=TYPE_HOURLY, name="Hourly Restrictions", icon="mdi:cancel", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, api_category=DATA_RESTRICTIONS_CURRENT, ), @@ -80,7 +80,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( key=TYPE_MONTH, name="Month Restrictions", icon="mdi:cancel", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, api_category=DATA_RESTRICTIONS_CURRENT, ), @@ -88,7 +88,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( key=TYPE_RAINDELAY, name="Rain Delay Restrictions", icon="mdi:cancel", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, api_category=DATA_RESTRICTIONS_CURRENT, ), @@ -96,7 +96,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( key=TYPE_RAINSENSOR, name="Rain Sensor Restrictions", icon="mdi:cancel", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, api_category=DATA_RESTRICTIONS_CURRENT, ), @@ -104,7 +104,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( key=TYPE_WEEKDAY, name="Weekday Restrictions", icon="mdi:cancel", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, api_category=DATA_RESTRICTIONS_CURRENT, ), diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index dee0f1f6e57..2db16ec9058 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -5,19 +5,15 @@ from dataclasses import dataclass from functools import partial from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - DEVICE_CLASS_TEMPERATURE, - ENTITY_CATEGORY_DIAGNOSTIC, - TEMP_CELSIUS, - VOLUME_CUBIC_METERS, -) +from homeassistant.const import TEMP_CELSIUS, VOLUME_CUBIC_METERS from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import RainMachineEntity @@ -50,26 +46,26 @@ SENSOR_DESCRIPTIONS = ( name="Flow Sensor Clicks per Cubic Meter", icon="mdi:water-pump", native_unit_of_measurement=f"clicks/{VOLUME_CUBIC_METERS}", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, api_category=DATA_PROVISION_SETTINGS, ), RainMachineSensorEntityDescription( key=TYPE_FLOW_SENSOR_CONSUMED_LITERS, name="Flow Sensor Consumed Liters", icon="mdi:water-pump", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement="liter", entity_registry_enabled_default=False, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, api_category=DATA_PROVISION_SETTINGS, ), RainMachineSensorEntityDescription( key=TYPE_FLOW_SENSOR_START_INDEX, name="Flow Sensor Start Index", icon="mdi:water-pump", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement="index", entity_registry_enabled_default=False, api_category=DATA_PROVISION_SETTINGS, @@ -78,20 +74,20 @@ SENSOR_DESCRIPTIONS = ( key=TYPE_FLOW_SENSOR_WATERING_CLICKS, name="Flow Sensor Clicks", icon="mdi:water-pump", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement="clicks", entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, api_category=DATA_PROVISION_SETTINGS, ), RainMachineSensorEntityDescription( key=TYPE_FREEZE_TEMP, name="Freeze Protect Temperature", icon="mdi:thermometer", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, api_category=DATA_RESTRICTIONS_UNIVERSAL, ), ) diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index 5a178718c9b..ddda36c13e8 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -13,10 +13,11 @@ import voluptuous as vol from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ID, ENTITY_CATEGORY_CONFIG +from homeassistant.const import ATTR_ID from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -181,7 +182,7 @@ async def async_setup_entry( RainMachineSwitchDescription( key=f"{kind}_{uid}_enabled", name=f"{data['name']} Enabled", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, icon="mdi:cog", uid=uid, ), From 682f29f13107e393b71c5b55f108c588804c03b4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 16 Dec 2021 16:39:44 +0100 Subject: [PATCH 0580/2644] Minor refactor of template weather (#61861) --- homeassistant/components/template/weather.py | 61 +++++--------------- 1 file changed, 15 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/template/weather.py b/homeassistant/components/template/weather.py index eabafb89803..4882dc5fd99 100644 --- a/homeassistant/components/template/weather.py +++ b/homeassistant/components/template/weather.py @@ -78,34 +78,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Template weather.""" - name = config[CONF_NAME] - condition_template = config[CONF_CONDITION_TEMPLATE] - temperature_template = config[CONF_TEMPERATURE_TEMPLATE] - humidity_template = config[CONF_HUMIDITY_TEMPLATE] - attribution_template = config.get(CONF_ATTRIBUTION_TEMPLATE) - pressure_template = config.get(CONF_PRESSURE_TEMPLATE) - wind_speed_template = config.get(CONF_WIND_SPEED_TEMPLATE) - wind_bearing_template = config.get(CONF_WIND_BEARING_TEMPLATE) - ozone_template = config.get(CONF_OZONE_TEMPLATE) - visibility_template = config.get(CONF_VISIBILITY_TEMPLATE) - forecast_template = config.get(CONF_FORECAST_TEMPLATE) unique_id = config.get(CONF_UNIQUE_ID) async_add_entities( [ WeatherTemplate( hass, - name, - condition_template, - temperature_template, - humidity_template, - attribution_template, - pressure_template, - wind_speed_template, - wind_bearing_template, - ozone_template, - visibility_template, - forecast_template, + config, unique_id, ) ] @@ -118,33 +97,23 @@ class WeatherTemplate(TemplateEntity, WeatherEntity): def __init__( self, hass, - name, - condition_template, - temperature_template, - humidity_template, - attribution_template, - pressure_template, - wind_speed_template, - wind_bearing_template, - ozone_template, - visibility_template, - forecast_template, + config, unique_id, ): - """Initialize the Demo weather.""" - super().__init__() + """Initialize the Template weather.""" + super().__init__(config=config) - self._name = name - self._condition_template = condition_template - self._temperature_template = temperature_template - self._humidity_template = humidity_template - self._attribution_template = attribution_template - self._pressure_template = pressure_template - self._wind_speed_template = wind_speed_template - self._wind_bearing_template = wind_bearing_template - self._ozone_template = ozone_template - self._visibility_template = visibility_template - self._forecast_template = forecast_template + self._name = name = config[CONF_NAME] + self._condition_template = config[CONF_CONDITION_TEMPLATE] + self._temperature_template = config[CONF_TEMPERATURE_TEMPLATE] + self._humidity_template = config[CONF_HUMIDITY_TEMPLATE] + self._attribution_template = config.get(CONF_ATTRIBUTION_TEMPLATE) + self._pressure_template = config.get(CONF_PRESSURE_TEMPLATE) + self._wind_speed_template = config.get(CONF_WIND_SPEED_TEMPLATE) + self._wind_bearing_template = config.get(CONF_WIND_BEARING_TEMPLATE) + self._ozone_template = config.get(CONF_OZONE_TEMPLATE) + self._visibility_template = config.get(CONF_VISIBILITY_TEMPLATE) + self._forecast_template = config.get(CONF_FORECAST_TEMPLATE) self._unique_id = unique_id self.entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, name, hass=hass) From 859bcb6eb4dbb9a8b87b6e4e888e074502db5df1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 16 Dec 2021 16:41:40 +0100 Subject: [PATCH 0581/2644] Minor refactor of template alarm (#61862) --- .../template/alarm_control_panel.py | 54 ++++++------------- 1 file changed, 16 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/template/alarm_control_panel.py b/homeassistant/components/template/alarm_control_panel.py index 05006049b02..932c15d85c0 100644 --- a/homeassistant/components/template/alarm_control_panel.py +++ b/homeassistant/components/template/alarm_control_panel.py @@ -97,29 +97,14 @@ async def _async_create_entities(hass, config): """Create Template Alarm Control Panels.""" alarm_control_panels = [] - for device, device_config in config[CONF_ALARM_CONTROL_PANELS].items(): - name = device_config.get(CONF_NAME, device) - state_template = device_config.get(CONF_VALUE_TEMPLATE) - disarm_action = device_config.get(CONF_DISARM_ACTION) - arm_away_action = device_config.get(CONF_ARM_AWAY_ACTION) - arm_home_action = device_config.get(CONF_ARM_HOME_ACTION) - arm_night_action = device_config.get(CONF_ARM_NIGHT_ACTION) - code_arm_required = device_config[CONF_CODE_ARM_REQUIRED] - code_format = device_config[CONF_CODE_FORMAT] - unique_id = device_config.get(CONF_UNIQUE_ID) + for object_id, entity_config in config[CONF_ALARM_CONTROL_PANELS].items(): + unique_id = entity_config.get(CONF_UNIQUE_ID) alarm_control_panels.append( AlarmControlPanelTemplate( hass, - device, - name, - state_template, - disarm_action, - arm_away_action, - arm_home_action, - arm_night_action, - code_arm_required, - code_format, + object_id, + entity_config, unique_id, ) ) @@ -138,37 +123,30 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity): def __init__( self, hass, - device_id, - name, - state_template, - disarm_action, - arm_away_action, - arm_home_action, - arm_night_action, - code_arm_required, - code_format, + object_id, + config, unique_id, ): """Initialize the panel.""" - super().__init__() + super().__init__(config=config) self.entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, device_id, hass=hass + ENTITY_ID_FORMAT, object_id, hass=hass ) - self._name = name - self._template = state_template + self._name = name = config.get(CONF_NAME, object_id) + self._template = config.get(CONF_VALUE_TEMPLATE) self._disarm_script = None - self._code_arm_required = code_arm_required - self._code_format = code_format - if disarm_action is not None: + self._code_arm_required = config[CONF_CODE_ARM_REQUIRED] + self._code_format = config[CONF_CODE_FORMAT] + if (disarm_action := config.get(CONF_DISARM_ACTION)) is not None: self._disarm_script = Script(hass, disarm_action, name, DOMAIN) self._arm_away_script = None - if arm_away_action is not None: + if (arm_away_action := config.get(CONF_ARM_AWAY_ACTION)) is not None: self._arm_away_script = Script(hass, arm_away_action, name, DOMAIN) self._arm_home_script = None - if arm_home_action is not None: + if (arm_home_action := config.get(CONF_ARM_HOME_ACTION)) is not None: self._arm_home_script = Script(hass, arm_home_action, name, DOMAIN) self._arm_night_script = None - if arm_night_action is not None: + if (arm_night_action := config.get(CONF_ARM_NIGHT_ACTION)) is not None: self._arm_night_script = Script(hass, arm_night_action, name, DOMAIN) self._state = None From b28c821bc3f6015641e9a287dd9d08a1210839ba Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 11:04:47 -0500 Subject: [PATCH 0582/2644] Use enums in ondilo_ico (#62081) --- homeassistant/components/ondilo_ico/sensor.py | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/ondilo_ico/sensor.py b/homeassistant/components/ondilo_ico/sensor.py index e45f3139529..b84d8f0d093 100644 --- a/homeassistant/components/ondilo_ico/sensor.py +++ b/homeassistant/components/ondilo_ico/sensor.py @@ -7,15 +7,13 @@ import logging from ondilo import OndiloError from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_SIGNAL_STRENGTH, - DEVICE_CLASS_TEMPERATURE, ELECTRIC_POTENTIAL_MILLIVOLT, PERCENTAGE, TEMP_CELSIUS, @@ -35,8 +33,8 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, icon=None, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="orp", @@ -44,7 +42,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( native_unit_of_measurement=ELECTRIC_POTENTIAL_MILLIVOLT, icon="mdi:pool", device_class=None, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="ph", @@ -52,7 +50,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( native_unit_of_measurement=None, icon="mdi:pool", device_class=None, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="tds", @@ -60,23 +58,23 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, icon="mdi:pool", device_class=None, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="battery", name="Battery", native_unit_of_measurement=PERCENTAGE, icon=None, - device_class=DEVICE_CLASS_BATTERY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="rssi", name="RSSI", native_unit_of_measurement=PERCENTAGE, icon=None, - device_class=DEVICE_CLASS_SIGNAL_STRENGTH, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="salt", @@ -84,7 +82,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( native_unit_of_measurement="mg/L", icon="mdi:pool", device_class=None, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), ) From ec3efb4b1affb84d4678fb68c473a7c8cf9ffdb2 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 11:06:16 -0500 Subject: [PATCH 0583/2644] Use enums in poolsense (#62071) --- .../components/poolsense/binary_sensor.py | 6 +++--- homeassistant/components/poolsense/sensor.py | 15 ++++++++------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/poolsense/binary_sensor.py b/homeassistant/components/poolsense/binary_sensor.py index 1b45ee15f0f..2f48539adb3 100644 --- a/homeassistant/components/poolsense/binary_sensor.py +++ b/homeassistant/components/poolsense/binary_sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_PROBLEM, + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -15,12 +15,12 @@ BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( BinarySensorEntityDescription( key="pH Status", name="pH Status", - device_class=DEVICE_CLASS_PROBLEM, + device_class=BinarySensorDeviceClass.PROBLEM, ), BinarySensorEntityDescription( key="Chlorine Status", name="Chlorine Status", - device_class=DEVICE_CLASS_PROBLEM, + device_class=BinarySensorDeviceClass.PROBLEM, ), ) diff --git a/homeassistant/components/poolsense/sensor.py b/homeassistant/components/poolsense/sensor.py index 82df8b4d208..d4ac0d0f292 100644 --- a/homeassistant/components/poolsense/sensor.py +++ b/homeassistant/components/poolsense/sensor.py @@ -1,12 +1,13 @@ """Sensor platform for the PoolSense sensor.""" from __future__ import annotations -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) from homeassistant.const import ( CONF_EMAIL, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_TIMESTAMP, ELECTRIC_POTENTIAL_MILLIVOLT, PERCENTAGE, TEMP_CELSIUS, @@ -31,20 +32,20 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="Battery", native_unit_of_measurement=PERCENTAGE, name="Battery", - device_class=DEVICE_CLASS_BATTERY, + device_class=SensorDeviceClass.BATTERY, ), SensorEntityDescription( key="Water Temp", native_unit_of_measurement=TEMP_CELSIUS, icon="mdi:coolant-temperature", name="Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( key="Last Seen", icon="mdi:clock", name="Last Seen", - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, ), SensorEntityDescription( key="Chlorine High", From e6956acb4b593e0cc388450102155d73ac6356e2 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 11:07:50 -0500 Subject: [PATCH 0584/2644] Use enums in octoprint (#62079) --- homeassistant/components/octoprint/sensor.py | 21 ++++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/octoprint/sensor.py b/homeassistant/components/octoprint/sensor.py index 68313a16bd3..58187e52e4d 100644 --- a/homeassistant/components/octoprint/sensor.py +++ b/homeassistant/components/octoprint/sensor.py @@ -6,14 +6,13 @@ import logging from pyoctoprintapi import OctoprintJobInfo, OctoprintPrinterInfo -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_TIMESTAMP, - PERCENTAGE, - TEMP_CELSIUS, +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorStateClass, ) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -150,7 +149,7 @@ class OctoPrintJobPercentageSensor(OctoPrintSensorBase): class OctoPrintEstimatedFinishTimeSensor(OctoPrintSensorBase): """Representation of an OctoPrint sensor.""" - _attr_device_class = DEVICE_CLASS_TIMESTAMP + _attr_device_class = SensorDeviceClass.TIMESTAMP def __init__( self, coordinator: OctoprintDataUpdateCoordinator, device_id: str @@ -177,7 +176,7 @@ class OctoPrintEstimatedFinishTimeSensor(OctoPrintSensorBase): class OctoPrintStartTimeSensor(OctoPrintSensorBase): """Representation of an OctoPrint sensor.""" - _attr_device_class = DEVICE_CLASS_TIMESTAMP + _attr_device_class = SensorDeviceClass.TIMESTAMP def __init__( self, coordinator: OctoprintDataUpdateCoordinator, device_id: str @@ -206,8 +205,8 @@ class OctoPrintTemperatureSensor(OctoPrintSensorBase): """Representation of an OctoPrint sensor.""" _attr_native_unit_of_measurement = TEMP_CELSIUS - _attr_device_class = DEVICE_CLASS_TEMPERATURE - _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_device_class = SensorDeviceClass.TEMPERATURE + _attr_state_class = SensorStateClass.MEASUREMENT def __init__( self, From 597045149f0c0ffbf8c40fe6803781fb3fc76ba5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 16 Dec 2021 17:11:27 +0100 Subject: [PATCH 0585/2644] Minor refactor of template select (#62091) --- homeassistant/components/template/select.py | 77 ++++++++------------- 1 file changed, 29 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/template/select.py b/homeassistant/components/template/select.py index c3312ac2375..074dda8f4fb 100644 --- a/homeassistant/components/template/select.py +++ b/homeassistant/components/template/select.py @@ -13,22 +13,20 @@ from homeassistant.components.select.const import ( ATTR_OPTIONS, DOMAIN as SELECT_DOMAIN, ) -from homeassistant.const import ( - CONF_ICON, - CONF_NAME, - CONF_OPTIMISTIC, - CONF_STATE, - CONF_UNIQUE_ID, -) +from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_STATE, CONF_UNIQUE_ID from homeassistant.core import Config, HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.script import Script -from homeassistant.helpers.template import Template, TemplateError +from homeassistant.helpers.template import TemplateError from . import TriggerUpdateCoordinator -from .const import CONF_AVAILABILITY, DOMAIN -from .template_entity import TemplateEntity +from .const import DOMAIN +from .template_entity import ( + TEMPLATE_ENTITY_AVAILABILITY_SCHEMA, + TEMPLATE_ENTITY_ICON_SCHEMA, + TemplateEntity, +) from .trigger_entity import TriggerEntity _LOGGER = logging.getLogger(__name__) @@ -38,17 +36,19 @@ CONF_SELECT_OPTION = "select_option" DEFAULT_NAME = "Template Select" DEFAULT_OPTIMISTIC = False -SELECT_SCHEMA = vol.Schema( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.template, - vol.Required(CONF_STATE): cv.template, - vol.Required(CONF_SELECT_OPTION): cv.SCRIPT_SCHEMA, - vol.Required(ATTR_OPTIONS): cv.template, - vol.Optional(CONF_AVAILABILITY): cv.template, - vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, - vol.Optional(CONF_UNIQUE_ID): cv.string, - vol.Optional(CONF_ICON): cv.template, - } +SELECT_SCHEMA = ( + vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.template, + vol.Required(CONF_STATE): cv.template, + vol.Required(CONF_SELECT_OPTION): cv.SCRIPT_SCHEMA, + vol.Required(ATTR_OPTIONS): cv.template, + vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, + vol.Optional(CONF_UNIQUE_ID): cv.string, + } + ) + .extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA.schema) + .extend(TEMPLATE_ENTITY_ICON_SCHEMA.schema) ) @@ -61,19 +61,7 @@ async def _async_create_entities( unique_id = definition.get(CONF_UNIQUE_ID) if unique_id and unique_id_prefix: unique_id = f"{unique_id_prefix}-{unique_id}" - entities.append( - TemplateSelect( - hass, - definition.get(CONF_NAME, DEFAULT_NAME), - definition[CONF_STATE], - definition.get(CONF_AVAILABILITY), - definition[CONF_SELECT_OPTION], - definition[ATTR_OPTIONS], - definition.get(CONF_OPTIMISTIC, DEFAULT_OPTIMISTIC), - unique_id, - definition.get(CONF_ICON), - ) - ) + entities.append(TemplateSelect(hass, definition, unique_id)) return entities @@ -110,30 +98,23 @@ class TemplateSelect(TemplateEntity, SelectEntity): def __init__( self, hass: HomeAssistant, - name_template: Template | None, - value_template: Template, - availability_template: Template | None, - command_select_option: dict[str, Any], - options_template: Template, - optimistic: bool, + config: dict[str, Any], unique_id: str | None, - icon_template: Template | None, ) -> None: """Initialize the select.""" - super().__init__( - availability_template=availability_template, icon_template=icon_template - ) + super().__init__(config=config) self._attr_name = DEFAULT_NAME + name_template = config[CONF_NAME] name_template.hass = hass with contextlib.suppress(TemplateError): self._attr_name = name_template.async_render(parse_result=False) self._name_template = name_template - self._value_template = value_template + self._value_template = config[CONF_STATE] self._command_select_option = Script( - hass, command_select_option, self._attr_name, DOMAIN + hass, config[CONF_SELECT_OPTION], self._attr_name, DOMAIN ) - self._options_template = options_template - self._attr_assumed_state = self._optimistic = optimistic + self._options_template = config[ATTR_OPTIONS] + self._attr_assumed_state = self._optimistic = config[CONF_OPTIMISTIC] self._attr_unique_id = unique_id self._attr_options = None self._attr_current_option = None From 6acf45566edc75a9c4b6fda678da6da2c3e9c3b5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 16 Dec 2021 17:11:47 +0100 Subject: [PATCH 0586/2644] Minor refactor of template switch (#61859) --- homeassistant/components/template/switch.py | 63 +++++++-------------- 1 file changed, 20 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/template/switch.py b/homeassistant/components/template/switch.py index a5b85e4b408..9614eab0fd1 100644 --- a/homeassistant/components/template/switch.py +++ b/homeassistant/components/template/switch.py @@ -10,8 +10,6 @@ from homeassistant.components.switch import ( from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, - CONF_ENTITY_PICTURE_TEMPLATE, - CONF_ICON_TEMPLATE, CONF_SWITCHES, CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, @@ -25,8 +23,12 @@ from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.script import Script -from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN -from .template_entity import TemplateEntity +from .const import DOMAIN +from .template_entity import ( + TEMPLATE_ENTITY_COMMON_SCHEMA_LEGACY, + TemplateEntity, + rewrite_common_legacy_to_modern_conf, +) _VALID_STATES = [STATE_ON, STATE_OFF, "true", "false"] @@ -38,16 +40,13 @@ SWITCH_SCHEMA = vol.All( vol.Schema( { vol.Optional(CONF_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_ICON_TEMPLATE): cv.template, - vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE): cv.template, - vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, vol.Required(ON_ACTION): cv.SCRIPT_SCHEMA, vol.Required(OFF_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(ATTR_FRIENDLY_NAME): cv.string, vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(CONF_UNIQUE_ID): cv.string, } - ), + ).extend(TEMPLATE_ENTITY_COMMON_SCHEMA_LEGACY.schema), ) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -59,27 +58,15 @@ async def _async_create_entities(hass, config): """Create the Template switches.""" switches = [] - for device, device_config in config[CONF_SWITCHES].items(): - friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device) - state_template = device_config.get(CONF_VALUE_TEMPLATE) - icon_template = device_config.get(CONF_ICON_TEMPLATE) - entity_picture_template = device_config.get(CONF_ENTITY_PICTURE_TEMPLATE) - availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) - on_action = device_config[ON_ACTION] - off_action = device_config[OFF_ACTION] - unique_id = device_config.get(CONF_UNIQUE_ID) + for object_id, entity_config in config[CONF_SWITCHES].items(): + entity_config = rewrite_common_legacy_to_modern_conf(entity_config) + unique_id = entity_config.get(CONF_UNIQUE_ID) switches.append( SwitchTemplate( hass, - device, - friendly_name, - state_template, - icon_template, - entity_picture_template, - availability_template, - on_action, - off_action, + object_id, + entity_config, unique_id, ) ) @@ -98,29 +85,19 @@ class SwitchTemplate(TemplateEntity, SwitchEntity, RestoreEntity): def __init__( self, hass, - device_id, - friendly_name, - state_template, - icon_template, - entity_picture_template, - availability_template, - on_action, - off_action, + object_id, + config, unique_id, ): """Initialize the Template switch.""" - super().__init__( - availability_template=availability_template, - icon_template=icon_template, - entity_picture_template=entity_picture_template, - ) + super().__init__(config=config) self.entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, device_id, hass=hass + ENTITY_ID_FORMAT, object_id, hass=hass ) - self._name = friendly_name - self._template = state_template - self._on_script = Script(hass, on_action, friendly_name, DOMAIN) - self._off_script = Script(hass, off_action, friendly_name, DOMAIN) + self._name = friendly_name = config.get(ATTR_FRIENDLY_NAME, object_id) + self._template = config.get(CONF_VALUE_TEMPLATE) + self._on_script = Script(hass, config[ON_ACTION], friendly_name, DOMAIN) + self._off_script = Script(hass, config[OFF_ACTION], friendly_name, DOMAIN) self._state = False self._unique_id = unique_id From 521458d9813f6674c7bd558f308f30b2be480d0a Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 11:12:18 -0500 Subject: [PATCH 0587/2644] Use enums in openverse (#62082) --- homeassistant/components/openevse/sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/openevse/sensor.py b/homeassistant/components/openevse/sensor.py index 3459671c829..46999770971 100644 --- a/homeassistant/components/openevse/sensor.py +++ b/homeassistant/components/openevse/sensor.py @@ -9,13 +9,13 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, + SensorDeviceClass, SensorEntity, SensorEntityDescription, ) from homeassistant.const import ( CONF_HOST, CONF_MONITORED_VARIABLES, - DEVICE_CLASS_TEMPERATURE, ENERGY_KILO_WATT_HOUR, TEMP_CELSIUS, TIME_MINUTES, @@ -38,19 +38,19 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="ambient_temp", name="Ambient Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( key="ir_temp", name="IR Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( key="rtc_temp", name="RTC Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( key="usage_session", From 21e46e318d57785566bce2b0177d119d1f7a7b4c Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 11:42:24 -0500 Subject: [PATCH 0588/2644] Use enums in venstar (#61993) --- .../components/venstar/binary_sensor.py | 4 ++-- homeassistant/components/venstar/sensor.py | 20 +++++++++---------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/venstar/binary_sensor.py b/homeassistant/components/venstar/binary_sensor.py index 1d6c8f49bd8..4a4884cf095 100644 --- a/homeassistant/components/venstar/binary_sensor.py +++ b/homeassistant/components/venstar/binary_sensor.py @@ -1,6 +1,6 @@ """Alarm sensors for the Venstar Thermostat.""" from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_PROBLEM, + BinarySensorDeviceClass, BinarySensorEntity, ) @@ -23,7 +23,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities) -> None: class VenstarBinarySensor(VenstarEntity, BinarySensorEntity): """Represent a Venstar alert.""" - _attr_device_class = DEVICE_CLASS_PROBLEM + _attr_device_class = BinarySensorDeviceClass.PROBLEM def __init__(self, coordinator, config, alert): """Initialize the alert.""" diff --git a/homeassistant/components/venstar/sensor.py b/homeassistant/components/venstar/sensor.py index d7b806927ae..eaa4eb3b927 100644 --- a/homeassistant/components/venstar/sensor.py +++ b/homeassistant/components/venstar/sensor.py @@ -6,12 +6,10 @@ from dataclasses import dataclass from typing import Any from homeassistant.components.sensor import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, TEMP_CELSIUS, TEMP_FAHRENHEIT, TIME_MINUTES @@ -145,8 +143,8 @@ class VenstarSensor(VenstarEntity, SensorEntity): SENSOR_ENTITIES: tuple[VenstarSensorEntityDescription, ...] = ( VenstarSensorEntityDescription( key="hum", - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, uom_fn=lambda coordinator: PERCENTAGE, value_fn=lambda coordinator, sensor_name: coordinator.client.get_sensor( sensor_name, "hum" @@ -155,8 +153,8 @@ SENSOR_ENTITIES: tuple[VenstarSensorEntityDescription, ...] = ( ), VenstarSensorEntityDescription( key="temp", - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, uom_fn=temperature_unit, value_fn=lambda coordinator, sensor_name: round( float(coordinator.client.get_sensor(sensor_name, "temp")), 1 @@ -165,8 +163,8 @@ SENSOR_ENTITIES: tuple[VenstarSensorEntityDescription, ...] = ( ), VenstarSensorEntityDescription( key="battery", - device_class=DEVICE_CLASS_BATTERY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, uom_fn=lambda coordinator: PERCENTAGE, value_fn=lambda coordinator, sensor_name: coordinator.client.get_sensor( sensor_name, "battery" @@ -177,7 +175,7 @@ SENSOR_ENTITIES: tuple[VenstarSensorEntityDescription, ...] = ( RUNTIME_ENTITY = VenstarSensorEntityDescription( key="runtime", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, uom_fn=lambda coordinator: TIME_MINUTES, value_fn=lambda coordinator, sensor_name: coordinator.runtimes[-1][sensor_name], name_fn=lambda coordinator, sensor_name: f"{coordinator.client.name} {RUNTIME_ATTRIBUTES[sensor_name]} Runtime", From 31bef18e9a0a8e4098c8cba8d50eeb8d545b1a33 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 12:38:32 -0500 Subject: [PATCH 0589/2644] Use enums in screenlogic (#62049) --- homeassistant/components/screenlogic/binary_sensor.py | 4 ++-- homeassistant/components/screenlogic/sensor.py | 10 +++------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/screenlogic/binary_sensor.py b/homeassistant/components/screenlogic/binary_sensor.py index 136f74d0d6c..4c961f9b12a 100644 --- a/homeassistant/components/screenlogic/binary_sensor.py +++ b/homeassistant/components/screenlogic/binary_sensor.py @@ -2,14 +2,14 @@ from screenlogicpy.const import DATA as SL_DATA, DEVICE_TYPE, EQUIPMENT, ON_OFF from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_PROBLEM, + BinarySensorDeviceClass, BinarySensorEntity, ) from . import ScreenlogicEntity from .const import DOMAIN -SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS = {DEVICE_TYPE.ALARM: DEVICE_CLASS_PROBLEM} +SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS = {DEVICE_TYPE.ALARM: BinarySensorDeviceClass.PROBLEM} async def async_setup_entry(hass, config_entry, async_add_entities): diff --git a/homeassistant/components/screenlogic/sensor.py b/homeassistant/components/screenlogic/sensor.py index 7ab20164400..0f2d140f669 100644 --- a/homeassistant/components/screenlogic/sensor.py +++ b/homeassistant/components/screenlogic/sensor.py @@ -6,11 +6,7 @@ from screenlogicpy.const import ( EQUIPMENT, ) -from homeassistant.components.sensor import ( - DEVICE_CLASS_POWER, - DEVICE_CLASS_TEMPERATURE, - SensorEntity, -) +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from . import ScreenlogicEntity from .const import DOMAIN @@ -41,8 +37,8 @@ SUPPORTED_SCG_SENSORS = ( SUPPORTED_PUMP_SENSORS = ("currentWatts", "currentRPM", "currentGPM") SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS = { - DEVICE_TYPE.TEMPERATURE: DEVICE_CLASS_TEMPERATURE, - DEVICE_TYPE.ENERGY: DEVICE_CLASS_POWER, + DEVICE_TYPE.TEMPERATURE: SensorDeviceClass.TEMPERATURE, + DEVICE_TYPE.ENERGY: SensorDeviceClass.POWER, } From 619529b40c0c843cf069314b7ba77445697956a2 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 12:38:56 -0500 Subject: [PATCH 0590/2644] Use enums in Powerwall (#62072) --- .../components/powerwall/binary_sensor.py | 14 +++++------ homeassistant/components/powerwall/sensor.py | 23 +++++++------------ 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/powerwall/binary_sensor.py b/homeassistant/components/powerwall/binary_sensor.py index b4104b70f39..9f444d9ab7d 100644 --- a/homeassistant/components/powerwall/binary_sensor.py +++ b/homeassistant/components/powerwall/binary_sensor.py @@ -2,11 +2,9 @@ from tesla_powerwall import GridStatus, MeterType from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_BATTERY_CHARGING, - DEVICE_CLASS_CONNECTIVITY, + BinarySensorDeviceClass, BinarySensorEntity, ) -from homeassistant.const import DEVICE_CLASS_POWER from .const import ( DOMAIN, @@ -61,7 +59,7 @@ class PowerWallRunningSensor(PowerWallEntity, BinarySensorEntity): @property def device_class(self): """Device Class.""" - return DEVICE_CLASS_POWER + return BinarySensorDeviceClass.POWER @property def unique_id(self): @@ -85,7 +83,7 @@ class PowerWallConnectedSensor(PowerWallEntity, BinarySensorEntity): @property def device_class(self): """Device Class.""" - return DEVICE_CLASS_CONNECTIVITY + return BinarySensorDeviceClass.CONNECTIVITY @property def unique_id(self): @@ -109,7 +107,7 @@ class PowerWallGridServicesActiveSensor(PowerWallEntity, BinarySensorEntity): @property def device_class(self): """Device Class.""" - return DEVICE_CLASS_POWER + return BinarySensorDeviceClass.POWER @property def unique_id(self): @@ -133,7 +131,7 @@ class PowerWallGridStatusSensor(PowerWallEntity, BinarySensorEntity): @property def device_class(self): """Device Class.""" - return DEVICE_CLASS_POWER + return BinarySensorDeviceClass.POWER @property def unique_id(self): @@ -157,7 +155,7 @@ class PowerWallChargingStatusSensor(PowerWallEntity, BinarySensorEntity): @property def device_class(self): """Device Class.""" - return DEVICE_CLASS_BATTERY_CHARGING + return BinarySensorDeviceClass.BATTERY_CHARGING @property def unique_id(self): diff --git a/homeassistant/components/powerwall/sensor.py b/homeassistant/components/powerwall/sensor.py index 8c45a142206..556159fa893 100644 --- a/homeassistant/components/powerwall/sensor.py +++ b/homeassistant/components/powerwall/sensor.py @@ -4,18 +4,11 @@ import logging from tesla_powerwall import MeterType from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, + SensorStateClass, ) -from homeassistant.const import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - ENERGY_KILO_WATT_HOUR, - PERCENTAGE, - POWER_KILO_WATT, -) +from homeassistant.const import ENERGY_KILO_WATT_HOUR, PERCENTAGE, POWER_KILO_WATT from .const import ( ATTR_FREQUENCY, @@ -92,7 +85,7 @@ class PowerWallChargeSensor(PowerWallEntity, SensorEntity): _attr_name = "Powerwall Charge" _attr_native_unit_of_measurement = PERCENTAGE - _attr_device_class = DEVICE_CLASS_BATTERY + _attr_device_class = SensorDeviceClass.BATTERY @property def unique_id(self): @@ -108,9 +101,9 @@ class PowerWallChargeSensor(PowerWallEntity, SensorEntity): class PowerWallEnergySensor(PowerWallEntity, SensorEntity): """Representation of an Powerwall Energy sensor.""" - _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_state_class = SensorStateClass.MEASUREMENT _attr_native_unit_of_measurement = POWER_KILO_WATT - _attr_device_class = DEVICE_CLASS_POWER + _attr_device_class = SensorDeviceClass.POWER def __init__( self, @@ -155,9 +148,9 @@ class PowerWallEnergySensor(PowerWallEntity, SensorEntity): class PowerWallEnergyDirectionSensor(PowerWallEntity, SensorEntity): """Representation of an Powerwall Direction Energy sensor.""" - _attr_state_class = STATE_CLASS_TOTAL_INCREASING + _attr_state_class = SensorStateClass.TOTAL_INCREASING _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR - _attr_device_class = DEVICE_CLASS_ENERGY + _attr_device_class = SensorDeviceClass.ENERGY def __init__( self, From 80833aa7fbdcbced372b00dadbfab9e938f98053 Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Thu, 16 Dec 2021 18:57:51 +0100 Subject: [PATCH 0591/2644] Add config flow to Azure Event Hub integration (#61155) * config flow added, no tests yet * added tests * refinement of tests * small reverses of hub code * fix small bug * test fixes from review * test fixes from review * further refinement of tests and config flow * removed true return from hub and added failed reason for import * added deepcopy to default options * deleted max_delay from options, can still be in yaml for now * updated dropped message * mistaken period at eol --- .../components/azure_event_hub/__init__.py | 164 ++++---- .../components/azure_event_hub/client.py | 71 ++++ .../components/azure_event_hub/config_flow.py | 196 ++++++++++ .../components/azure_event_hub/const.py | 14 +- .../components/azure_event_hub/manifest.json | 3 +- .../components/azure_event_hub/strings.json | 49 +++ homeassistant/generated/config_flows.py | 1 + tests/components/azure_event_hub/conftest.py | 126 ++++++ tests/components/azure_event_hub/const.py | 56 +++ .../azure_event_hub/test_config_flow.py | 188 +++++++++ tests/components/azure_event_hub/test_init.py | 362 +++++++++--------- 11 files changed, 980 insertions(+), 250 deletions(-) create mode 100644 homeassistant/components/azure_event_hub/client.py create mode 100644 homeassistant/components/azure_event_hub/config_flow.py create mode 100644 homeassistant/components/azure_event_hub/strings.json create mode 100644 tests/components/azure_event_hub/conftest.py create mode 100644 tests/components/azure_event_hub/const.py create mode 100644 tests/components/azure_event_hub/test_config_flow.py diff --git a/homeassistant/components/azure_event_hub/__init__.py b/homeassistant/components/azure_event_hub/__init__.py index 039542f9ed6..adbfe68fe4d 100644 --- a/homeassistant/components/azure_event_hub/__init__.py +++ b/homeassistant/components/azure_event_hub/__init__.py @@ -9,16 +9,11 @@ import time from typing import Any from azure.eventhub import EventData, EventDataBatch -from azure.eventhub.aio import EventHubProducerClient, EventHubSharedKeyCredential from azure.eventhub.exceptions import EventHubError import voluptuous as vol -from homeassistant.const import ( - EVENT_HOMEASSISTANT_STOP, - MATCH_ALL, - STATE_UNAVAILABLE, - STATE_UNKNOWN, -) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry, ConfigEntryNotReady +from homeassistant.const import MATCH_ALL, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import Event, HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import FILTER_SCHEMA @@ -26,8 +21,8 @@ from homeassistant.helpers.event import async_call_later from homeassistant.helpers.json import JSONEncoder from homeassistant.helpers.typing import ConfigType +from .client import AzureEventHubClient from .const import ( - ADDITIONAL_ARGS, CONF_EVENT_HUB_CON_STRING, CONF_EVENT_HUB_INSTANCE_NAME, CONF_EVENT_HUB_NAMESPACE, @@ -36,6 +31,9 @@ from .const import ( CONF_FILTER, CONF_MAX_DELAY, CONF_SEND_INTERVAL, + DATA_FILTER, + DATA_HUB, + DEFAULT_MAX_DELAY, DOMAIN, ) @@ -45,18 +43,15 @@ CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( { - vol.Required(CONF_EVENT_HUB_INSTANCE_NAME): cv.string, - vol.Exclusive(CONF_EVENT_HUB_CON_STRING, "setup_methods"): cv.string, - vol.Exclusive(CONF_EVENT_HUB_NAMESPACE, "setup_methods"): cv.string, + vol.Optional(CONF_EVENT_HUB_INSTANCE_NAME): cv.string, + vol.Optional(CONF_EVENT_HUB_CON_STRING): cv.string, + vol.Optional(CONF_EVENT_HUB_NAMESPACE): cv.string, vol.Optional(CONF_EVENT_HUB_SAS_POLICY): cv.string, vol.Optional(CONF_EVENT_HUB_SAS_KEY): cv.string, - vol.Optional(CONF_SEND_INTERVAL, default=5): cv.positive_int, - vol.Optional(CONF_MAX_DELAY, default=30): cv.positive_int, + vol.Optional(CONF_SEND_INTERVAL): cv.positive_int, + vol.Optional(CONF_MAX_DELAY): cv.positive_int, vol.Optional(CONF_FILTER, default={}): FILTER_SCHEMA, }, - cv.has_at_least_one_key( - CONF_EVENT_HUB_CON_STRING, CONF_EVENT_HUB_NAMESPACE - ), ) }, extra=vol.ALLOW_EXTRA, @@ -64,35 +59,62 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool: - """Activate Azure EH component.""" - config = yaml_config[DOMAIN] - if config.get(CONF_EVENT_HUB_CON_STRING): - client_args = { - "conn_str": config[CONF_EVENT_HUB_CON_STRING], - "eventhub_name": config[CONF_EVENT_HUB_INSTANCE_NAME], - } - conn_str_client = True - else: - client_args = { - "fully_qualified_namespace": f"{config[CONF_EVENT_HUB_NAMESPACE]}.servicebus.windows.net", - "eventhub_name": config[CONF_EVENT_HUB_INSTANCE_NAME], - "credential": EventHubSharedKeyCredential( - policy=config[CONF_EVENT_HUB_SAS_POLICY], - key=config[CONF_EVENT_HUB_SAS_KEY], - ), - } - conn_str_client = False + """Activate Azure EH component from yaml. - instance = hass.data[DOMAIN] = AzureEventHub( - hass, - client_args, - conn_str_client, - config[CONF_FILTER], - config[CONF_SEND_INTERVAL], - config[CONF_MAX_DELAY], + Adds an empty filter to hass data. + Tries to get a filter from yaml, if present set to hass data. + If config is empty after getting the filter, return, otherwise emit + deprecated warning and pass the rest to the config flow. + """ + hass.data.setdefault(DOMAIN, {DATA_FILTER: FILTER_SCHEMA({})}) + if DOMAIN not in yaml_config: + return True + hass.data[DOMAIN][DATA_FILTER] = yaml_config[DOMAIN].pop(CONF_FILTER) + + if not yaml_config[DOMAIN]: + return True + _LOGGER.warning( + "Loading Azure Event Hub completely via yaml config is deprecated; Only the \ + Filter can be set in yaml, the rest is done through a config flow and has \ + been imported, all other keys but filter can be deleted from configuration.yaml" ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=yaml_config[DOMAIN] + ) + ) + return True - hass.async_create_task(instance.async_start()) + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Do the setup based on the config entry and the filter from yaml.""" + hass.data.setdefault(DOMAIN, {DATA_FILTER: FILTER_SCHEMA({})}) + hub = AzureEventHub( + hass, + AzureEventHubClient.from_input(**entry.data), + hass.data[DOMAIN][DATA_FILTER], + entry.options[CONF_SEND_INTERVAL], + entry.options.get(CONF_MAX_DELAY), + ) + try: + await hub.async_test_connection() + except EventHubError as err: + raise ConfigEntryNotReady("Could not connect to Azure Event Hub") from err + hass.data[DOMAIN][DATA_HUB] = hub + entry.async_on_unload(entry.add_update_listener(async_update_listener)) + await hub.async_start() + return True + + +async def async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Update listener for options.""" + hass.data[DOMAIN][DATA_HUB].update_options(entry.options) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + hub = hass.data[DOMAIN].pop(DATA_HUB) + await hub.async_stop() return True @@ -102,40 +124,45 @@ class AzureEventHub: def __init__( self, hass: HomeAssistant, - client_args: dict[str, Any], - conn_str_client: bool, + client: AzureEventHubClient, entities_filter: vol.Schema, send_interval: int, - max_delay: int, + max_delay: int | None = None, ) -> None: """Initialize the listener.""" self.hass = hass self.queue: asyncio.PriorityQueue[ # pylint: disable=unsubscriptable-object tuple[int, tuple[float, Event | None]] ] = asyncio.PriorityQueue() - self._client_args = client_args - self._conn_str_client = conn_str_client + self._client = client self._entities_filter = entities_filter self._send_interval = send_interval - self._max_delay = max_delay + send_interval + self._max_delay = max_delay if max_delay else DEFAULT_MAX_DELAY self._listener_remover: Callable[[], None] | None = None self._next_send_remover: Callable[[], None] | None = None self.shutdown = False async def async_start(self) -> None: - """Start the recorder, suppress logging and register the callbacks and do the first send after five seconds, to capture the startup events.""" - # suppress the INFO and below logging on the underlying packages, they are very verbose, even at INFO + """Start the hub. + + This suppresses logging and register the listener and + schedules the first send. + """ + # suppress the INFO and below logging on the underlying packages, + # they are very verbose, even at INFO logging.getLogger("uamqp").setLevel(logging.WARNING) logging.getLogger("azure.eventhub").setLevel(logging.WARNING) - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_shutdown) self._listener_remover = self.hass.bus.async_listen( MATCH_ALL, self.async_listen ) - # schedule the first send after 10 seconds to capture startup events, after that each send will schedule the next after the interval. - self._next_send_remover = async_call_later(self.hass, 10, self.async_send) + # schedule the first send after 10 seconds to capture startup events, + # after that each send will schedule the next after the interval. + self._next_send_remover = async_call_later( + self.hass, self._send_interval, self.async_send + ) - async def async_shutdown(self, _: Event) -> None: + async def async_stop(self) -> None: """Shut down the AEH by queueing None and calling send.""" if self._next_send_remover: self._next_send_remover() @@ -144,13 +171,17 @@ class AzureEventHub: await self.queue.put((3, (time.monotonic(), None))) await self.async_send(None) + async def async_test_connection(self) -> None: + """Test the connection to the event hub.""" + await self._client.test_connection() + async def async_listen(self, event: Event) -> None: """Listen for new messages on the bus and queue them for AEH.""" await self.queue.put((2, (time.monotonic(), event))) async def async_send(self, _) -> None: """Write preprocessed events to eventhub, with retry.""" - async with self._get_client() as client: + async with self._client.client as client: while not self.queue.empty(): data_batch, dequeue_count = await self.fill_batch(client) _LOGGER.debug( @@ -175,9 +206,12 @@ class AzureEventHub: async def fill_batch(self, client) -> tuple[EventDataBatch, int]: """Return a batch of events formatted for writing. - Uses get_nowait instead of await get, because the functions batches and doesn't wait for each single event, the send function is called. + Uses get_nowait instead of await get, because the functions batches and + doesn't wait for each single event, the send function is called. - Throws ValueError on add to batch when the EventDataBatch object reaches max_size. Put the item back in the queue and the next batch will include it. + Throws ValueError on add to batch when the EventDataBatch object reaches + max_size. Put the item back in the queue and the next batch will include + it. """ event_batch = await client.create_batch() dequeue_count = 0 @@ -194,10 +228,12 @@ class AzureEventHub: event_data = self._event_to_filtered_event_data(event) if not event_data: continue - if time.monotonic() - timestamp <= self._max_delay: + if time.monotonic() - timestamp <= self._max_delay + self._send_interval: try: event_batch.add(event_data) except ValueError: + dequeue_count -= 1 + self.queue.task_done() self.queue.put_nowait((1, (timestamp, event))) break else: @@ -205,7 +241,7 @@ class AzureEventHub: if dropped: _LOGGER.warning( - "Dropped %d old events, consider increasing the max_delay", dropped + "Dropped %d old events, consider filtering messages", dropped ) return event_batch, dequeue_count @@ -221,10 +257,6 @@ class AzureEventHub: return None return EventData(json.dumps(obj=state, cls=JSONEncoder).encode("utf-8")) - def _get_client(self) -> EventHubProducerClient: - """Get a Event Producer Client.""" - if self._conn_str_client: - return EventHubProducerClient.from_connection_string( - **self._client_args, **ADDITIONAL_ARGS - ) - return EventHubProducerClient(**self._client_args, **ADDITIONAL_ARGS) + def update_options(self, new_options: dict[str, Any]) -> None: + """Update options.""" + self._send_interval = new_options[CONF_SEND_INTERVAL] diff --git a/homeassistant/components/azure_event_hub/client.py b/homeassistant/components/azure_event_hub/client.py new file mode 100644 index 00000000000..1a5aa330cc8 --- /dev/null +++ b/homeassistant/components/azure_event_hub/client.py @@ -0,0 +1,71 @@ +"""File for Azure Event Hub models.""" +from __future__ import annotations + +from dataclasses import dataclass +import logging + +from azure.eventhub.aio import EventHubProducerClient, EventHubSharedKeyCredential + +from .const import ADDITIONAL_ARGS, CONF_EVENT_HUB_CON_STRING + +_LOGGER = logging.getLogger(__name__) + + +@dataclass +class AzureEventHubClient: + """Class for the Azure Event Hub client. Use from_input to initialize.""" + + event_hub_instance_name: str + + @property + def client(self) -> EventHubProducerClient: + """Return the client.""" + + async def test_connection(self) -> None: + """Test connection, will throw EventHubError when it cannot connect.""" + async with self.client as client: + await client.get_eventhub_properties() + + @classmethod + def from_input(cls, **kwargs) -> AzureEventHubClient: + """Create the right class.""" + if CONF_EVENT_HUB_CON_STRING in kwargs: + return AzureEventHubClientConnectionString(**kwargs) + return AzureEventHubClientSAS(**kwargs) + + +@dataclass +class AzureEventHubClientConnectionString(AzureEventHubClient): + """Class for Connection String based Azure Event Hub Client.""" + + event_hub_connection_string: str + + @property + def client(self) -> EventHubProducerClient: + """Return the client.""" + return EventHubProducerClient.from_connection_string( + conn_str=self.event_hub_connection_string, + eventhub_name=self.event_hub_instance_name, + **ADDITIONAL_ARGS, + ) + + +@dataclass +class AzureEventHubClientSAS(AzureEventHubClient): + """Class for SAS based Azure Event Hub Client.""" + + event_hub_namespace: str + event_hub_sas_policy: str + event_hub_sas_key: str + + @property + def client(self) -> EventHubProducerClient: + """Get a Event Producer Client.""" + return EventHubProducerClient( + fully_qualified_namespace=f"{self.event_hub_namespace}.servicebus.windows.net", + eventhub_name=self.event_hub_instance_name, + credential=EventHubSharedKeyCredential( # type: ignore + policy=self.event_hub_sas_policy, key=self.event_hub_sas_key + ), + **ADDITIONAL_ARGS, + ) diff --git a/homeassistant/components/azure_event_hub/config_flow.py b/homeassistant/components/azure_event_hub/config_flow.py new file mode 100644 index 00000000000..a0dded5f487 --- /dev/null +++ b/homeassistant/components/azure_event_hub/config_flow.py @@ -0,0 +1,196 @@ +"""Config flow for azure_event_hub integration.""" +from __future__ import annotations + +from copy import deepcopy +import logging +from typing import Any + +from azure.eventhub.exceptions import EventHubError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult + +from .client import AzureEventHubClient +from .const import ( + CONF_EVENT_HUB_CON_STRING, + CONF_EVENT_HUB_INSTANCE_NAME, + CONF_EVENT_HUB_NAMESPACE, + CONF_EVENT_HUB_SAS_KEY, + CONF_EVENT_HUB_SAS_POLICY, + CONF_MAX_DELAY, + CONF_SEND_INTERVAL, + CONF_USE_CONN_STRING, + DEFAULT_OPTIONS, + DOMAIN, + STEP_CONN_STRING, + STEP_SAS, + STEP_USER, +) + +_LOGGER = logging.getLogger(__name__) + +BASE_SCHEMA = vol.Schema( + { + vol.Required(CONF_EVENT_HUB_INSTANCE_NAME): str, + vol.Optional(CONF_USE_CONN_STRING, default=False): bool, + } +) + +CONN_STRING_SCHEMA = vol.Schema( + { + vol.Required(CONF_EVENT_HUB_CON_STRING): str, + } +) + +SAS_SCHEMA = vol.Schema( + { + vol.Required(CONF_EVENT_HUB_NAMESPACE): str, + vol.Required(CONF_EVENT_HUB_SAS_POLICY): str, + vol.Required(CONF_EVENT_HUB_SAS_KEY): str, + } +) + + +async def validate_data(data: dict[str, Any]) -> dict[str, str] | None: + """Validate the input.""" + client = AzureEventHubClient.from_input(**data) + try: + await client.test_connection() + except EventHubError: + return {"base": "cannot_connect"} + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unknown error") + return {"base": "unknown"} + return None + + +class AEHConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for azure event hub.""" + + VERSION: int = 1 + + def __init__(self): + """Initialize the config flow.""" + self._data: dict[str, Any] = {} + self._options: dict[str, Any] = deepcopy(DEFAULT_OPTIONS) + self._conn_string: bool | None = None + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return AEHOptionsFlowHandler(config_entry) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial user step.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + if user_input is None: + return self.async_show_form(step_id=STEP_USER, data_schema=BASE_SCHEMA) + + self._conn_string = user_input.pop(CONF_USE_CONN_STRING) + self._data = user_input + + if self._conn_string: + return await self.async_step_conn_string() + return await self.async_step_sas() + + async def async_step_conn_string( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the connection string steps.""" + errors = await self.async_update_and_validate_data(user_input) + if user_input is None or errors is not None: + return self.async_show_form( + step_id=STEP_CONN_STRING, + data_schema=CONN_STRING_SCHEMA, + errors=errors, + description_placeholders=self._data[CONF_EVENT_HUB_INSTANCE_NAME], + last_step=True, + ) + + return self.async_create_entry( + title=self._data[CONF_EVENT_HUB_INSTANCE_NAME], + data=self._data, + options=self._options, + ) + + async def async_step_sas( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the sas steps.""" + errors = await self.async_update_and_validate_data(user_input) + if user_input is None or errors is not None: + return self.async_show_form( + step_id=STEP_SAS, + data_schema=SAS_SCHEMA, + errors=errors, + description_placeholders=self._data[CONF_EVENT_HUB_INSTANCE_NAME], + last_step=True, + ) + + return self.async_create_entry( + title=self._data[CONF_EVENT_HUB_INSTANCE_NAME], + data=self._data, + options=self._options, + ) + + async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: + """Import config from configuration.yaml.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + if CONF_SEND_INTERVAL in import_config: + self._options[CONF_SEND_INTERVAL] = import_config.pop(CONF_SEND_INTERVAL) + if CONF_MAX_DELAY in import_config: + self._options[CONF_MAX_DELAY] = import_config.pop(CONF_MAX_DELAY) + self._data = import_config + errors = await validate_data(self._data) + if errors: + return self.async_abort(reason=errors["base"]) + return self.async_create_entry( + title=self._data[CONF_EVENT_HUB_INSTANCE_NAME], + data=self._data, + options=self._options, + ) + + async def async_update_and_validate_data( + self, user_input: dict[str, Any] | None + ) -> dict[str, str] | None: + """Validate the input.""" + if user_input is None: + return None + self._data.update(user_input) + return await validate_data(self._data) + + +class AEHOptionsFlowHandler(config_entries.OptionsFlow): + """Handle azure event hub options.""" + + def __init__(self, config_entry): + """Initialize AEH options flow.""" + self.config_entry = config_entry + self.options = deepcopy(dict(config_entry.options)) + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage the AEH options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Required( + CONF_SEND_INTERVAL, + default=self.options.get(CONF_SEND_INTERVAL), + ): int + } + ), + last_step=True, + ) diff --git a/homeassistant/components/azure_event_hub/const.py b/homeassistant/components/azure_event_hub/const.py index fdb5180fe4e..3aa0765545a 100644 --- a/homeassistant/components/azure_event_hub/const.py +++ b/homeassistant/components/azure_event_hub/const.py @@ -5,6 +5,7 @@ from typing import Any DOMAIN = "azure_event_hub" +CONF_USE_CONN_STRING = "use_connection_string" CONF_EVENT_HUB_NAMESPACE = "event_hub_namespace" CONF_EVENT_HUB_INSTANCE_NAME = "event_hub_instance_name" CONF_EVENT_HUB_SAS_POLICY = "event_hub_sas_policy" @@ -12,6 +13,17 @@ CONF_EVENT_HUB_SAS_KEY = "event_hub_sas_key" CONF_EVENT_HUB_CON_STRING = "event_hub_connection_string" CONF_SEND_INTERVAL = "send_interval" CONF_MAX_DELAY = "max_delay" -CONF_FILTER = "filter" +CONF_FILTER = DATA_FILTER = "filter" +DATA_HUB = "hub" + +STEP_USER = "user" +STEP_SAS = "sas" +STEP_CONN_STRING = "conn_string" + +DEFAULT_SEND_INTERVAL: int = 5 +DEFAULT_MAX_DELAY: int = 30 +DEFAULT_OPTIONS: dict[str, Any] = { + CONF_SEND_INTERVAL: DEFAULT_SEND_INTERVAL, +} ADDITIONAL_ARGS: dict[str, Any] = {"logging_enable": False} diff --git a/homeassistant/components/azure_event_hub/manifest.json b/homeassistant/components/azure_event_hub/manifest.json index 9c63af35340..52125b5a79c 100644 --- a/homeassistant/components/azure_event_hub/manifest.json +++ b/homeassistant/components/azure_event_hub/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/azure_event_hub", "requirements": ["azure-eventhub==5.5.0"], "codeowners": ["@eavanvalkenburg"], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "config_flow": true } diff --git a/homeassistant/components/azure_event_hub/strings.json b/homeassistant/components/azure_event_hub/strings.json new file mode 100644 index 00000000000..a7e714d8442 --- /dev/null +++ b/homeassistant/components/azure_event_hub/strings.json @@ -0,0 +1,49 @@ +{ + "config": { + "step": { + "user": { + "title": "Setup your Azure Event Hub integration", + "data": { + "event_hub_instance_name": "Event Hub Instance Name", + "use_connection_string": "Use Connection String" + } + }, + "conn_string": { + "title": "Connection String method", + "description": "Please enter the connection string for: {event_hub_instance_name}", + "data": { + "event_hub_connection_string": "Event Hub Connection String" + } + }, + "sas": { + "title": "SAS Credentials method", + "description": "Please enter the SAS (shared access signature) credentials for: {event_hub_instance_name}", + "data": { + "event_hub_namespace": "Event Hub Namespace", + "event_hub_sas_policy": "Event Hub SAS Policy", + "event_hub_sas_key": "Event Hub SAS Key" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "cannot_connect": "Connecting with the credentails from the configuration.yaml failed, please remove from yaml and use the config flow.", + "unknown": "Connecting with the credentails from the configuration.yaml failed with an unknown error, please remove from yaml and use the config flow." + } + }, + "options": { + "step": { + "options": { + "title": "Options for the Azure Event Hub.", + "data": { + "send_interval": "Interval between sending batches to the hub." + } + } + } + } + } \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 1663c3f131e..469f5ae3c90 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -36,6 +36,7 @@ FLOWS = [ "awair", "axis", "azure_devops", + "azure_event_hub", "balboa", "blebox", "blink", diff --git a/tests/components/azure_event_hub/conftest.py b/tests/components/azure_event_hub/conftest.py new file mode 100644 index 00000000000..18f44b10480 --- /dev/null +++ b/tests/components/azure_event_hub/conftest.py @@ -0,0 +1,126 @@ +"""Test fixtures for AEH.""" +from dataclasses import dataclass +from datetime import timedelta +import logging +from unittest.mock import MagicMock, patch + +from azure.eventhub.aio import EventHubProducerClient +import pytest + +from homeassistant.components.azure_event_hub.const import ( + CONF_FILTER, + CONF_SEND_INTERVAL, + DOMAIN, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import STATE_ON +from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow + +from .const import AZURE_EVENT_HUB_PATH, BASIC_OPTIONS, PRODUCER_PATH, SAS_CONFIG_FULL + +from tests.common import MockConfigEntry, async_fire_time_changed + +_LOGGER = logging.getLogger(__name__) + + +# fixtures for both init and config flow tests +@pytest.fixture(autouse=True, name="mock_get_eventhub_properties") +def mock_get_eventhub_properties_fixture(): + """Mock azure event hub properties, used to test the connection.""" + with patch(f"{PRODUCER_PATH}.get_eventhub_properties") as get_eventhub_properties: + yield get_eventhub_properties + + +@pytest.fixture(name="filter_schema") +def mock_filter_schema(): + """Return an empty filter.""" + return {} + + +@pytest.fixture(name="entry") +async def mock_entry_fixture(hass, filter_schema, mock_create_batch, mock_send_batch): + """Create the setup in HA.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=SAS_CONFIG_FULL, + title="test-instance", + options=BASIC_OPTIONS, + ) + entry.add_to_hass(hass) + assert await async_setup_component( + hass, DOMAIN, {DOMAIN: {CONF_FILTER: filter_schema}} + ) + assert entry.state == ConfigEntryState.LOADED + + # Clear the component_loaded event from the queue. + async_fire_time_changed( + hass, + utcnow() + timedelta(seconds=entry.options[CONF_SEND_INTERVAL]), + ) + await hass.async_block_till_done() + return entry + + +# fixtures for init tests +@pytest.fixture(name="entry_with_one_event") +async def mock_entry_with_one_event(hass, entry): + """Use the entry and add a single test event to the queue.""" + assert entry.state == ConfigEntryState.LOADED + hass.states.async_set("sensor.test", STATE_ON) + return entry + + +@dataclass +class FilterTest: + """Class for capturing a filter test.""" + + entity_id: str + expected_count: int + + +@pytest.fixture(name="mock_send_batch") +def mock_send_batch_fixture(): + """Mock send_batch.""" + with patch(f"{PRODUCER_PATH}.send_batch") as mock_send_batch: + yield mock_send_batch + + +@pytest.fixture(autouse=True, name="mock_client") +def mock_client_fixture(mock_send_batch): + """Mock the azure event hub producer client.""" + with patch(f"{PRODUCER_PATH}.close") as mock_close: + yield ( + mock_send_batch, + mock_close, + ) + + +@pytest.fixture(name="mock_create_batch") +def mock_create_batch_fixture(): + """Mock batch creator and return mocked batch object.""" + mock_batch = MagicMock() + with patch(f"{PRODUCER_PATH}.create_batch", return_value=mock_batch): + yield mock_batch + + +# fixtures for config flow tests +@pytest.fixture(name="mock_from_connection_string") +def mock_from_connection_string_fixture(): + """Mock AEH from connection string creation.""" + mock_aeh = MagicMock(spec=EventHubProducerClient) + mock_aeh.__aenter__.return_value = mock_aeh + with patch( + f"{PRODUCER_PATH}.from_connection_string", + return_value=mock_aeh, + ) as from_conn_string: + yield from_conn_string + + +@pytest.fixture(name="mock_setup_entry") +def mock_setup_entry(): + """Mock the setup entry call, used for config flow tests.""" + with patch( + f"{AZURE_EVENT_HUB_PATH}.async_setup_entry", return_value=True + ) as setup_entry: + yield setup_entry diff --git a/tests/components/azure_event_hub/const.py b/tests/components/azure_event_hub/const.py new file mode 100644 index 00000000000..1daf100238c --- /dev/null +++ b/tests/components/azure_event_hub/const.py @@ -0,0 +1,56 @@ +"""Constants for testing AEH.""" +from homeassistant.components.azure_event_hub.const import ( + CONF_EVENT_HUB_CON_STRING, + CONF_EVENT_HUB_INSTANCE_NAME, + CONF_EVENT_HUB_NAMESPACE, + CONF_EVENT_HUB_SAS_KEY, + CONF_EVENT_HUB_SAS_POLICY, + CONF_MAX_DELAY, + CONF_SEND_INTERVAL, + CONF_USE_CONN_STRING, +) + +AZURE_EVENT_HUB_PATH = "homeassistant.components.azure_event_hub" +PRODUCER_PATH = f"{AZURE_EVENT_HUB_PATH}.client.EventHubProducerClient" +CLIENT_PATH = f"{AZURE_EVENT_HUB_PATH}.client.AzureEventHubClient" +CONFIG_FLOW_PATH = f"{AZURE_EVENT_HUB_PATH}.config_flow" + +BASE_CONFIG_CS = { + CONF_EVENT_HUB_INSTANCE_NAME: "test-instance", + CONF_USE_CONN_STRING: True, +} +BASE_CONFIG_SAS = { + CONF_EVENT_HUB_INSTANCE_NAME: "test-instance", + CONF_USE_CONN_STRING: False, +} + +CS_CONFIG = {CONF_EVENT_HUB_CON_STRING: "test-cs"} +SAS_CONFIG = { + CONF_EVENT_HUB_NAMESPACE: "test-ns", + CONF_EVENT_HUB_SAS_POLICY: "test-policy", + CONF_EVENT_HUB_SAS_KEY: "test-key", +} +CS_CONFIG_FULL = { + CONF_EVENT_HUB_INSTANCE_NAME: "test-instance", + CONF_EVENT_HUB_CON_STRING: "test-cs", +} +SAS_CONFIG_FULL = { + CONF_EVENT_HUB_INSTANCE_NAME: "test-instance", + CONF_EVENT_HUB_NAMESPACE: "test-ns", + CONF_EVENT_HUB_SAS_POLICY: "test-policy", + CONF_EVENT_HUB_SAS_KEY: "test-key", +} + +IMPORT_CONFIG = { + CONF_EVENT_HUB_INSTANCE_NAME: "test-instance", + CONF_EVENT_HUB_NAMESPACE: "test-ns", + CONF_EVENT_HUB_SAS_POLICY: "test-policy", + CONF_EVENT_HUB_SAS_KEY: "test-key", + CONF_SEND_INTERVAL: 5, + CONF_MAX_DELAY: 10, +} + +BASIC_OPTIONS = { + CONF_SEND_INTERVAL: 5, +} +UPDATE_OPTIONS = {CONF_SEND_INTERVAL: 100} diff --git a/tests/components/azure_event_hub/test_config_flow.py b/tests/components/azure_event_hub/test_config_flow.py new file mode 100644 index 00000000000..4e135d55555 --- /dev/null +++ b/tests/components/azure_event_hub/test_config_flow.py @@ -0,0 +1,188 @@ +"""Test the AEH config flow.""" +import logging + +from azure.eventhub.exceptions import EventHubError +import pytest + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.azure_event_hub.const import ( + CONF_MAX_DELAY, + CONF_SEND_INTERVAL, + DOMAIN, + STEP_CONN_STRING, + STEP_SAS, +) + +from .const import ( + BASE_CONFIG_CS, + BASE_CONFIG_SAS, + CS_CONFIG, + CS_CONFIG_FULL, + IMPORT_CONFIG, + SAS_CONFIG, + SAS_CONFIG_FULL, + UPDATE_OPTIONS, +) + +from tests.common import MockConfigEntry + +_LOGGER = logging.getLogger(__name__) + + +@pytest.mark.parametrize( + "step1_config, step_id, step2_config, data_config", + [ + (BASE_CONFIG_CS, STEP_CONN_STRING, CS_CONFIG, CS_CONFIG_FULL), + (BASE_CONFIG_SAS, STEP_SAS, SAS_CONFIG, SAS_CONFIG_FULL), + ], + ids=["connection_string", "sas"], +) +async def test_form( + hass, + mock_setup_entry, + mock_from_connection_string, + step1_config, + step_id, + step2_config, + data_config, +): + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None + ) + assert result["type"] == "form" + assert result["errors"] is None + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + step1_config.copy(), + ) + + assert result2["type"] == "form" + assert result2["step_id"] == step_id + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + step2_config.copy(), + ) + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["title"] == "test-instance" + assert result3["data"] == data_config + mock_setup_entry.assert_called_once() + + +async def test_import(hass, mock_setup_entry): + """Test we get the form.""" + + import_config = IMPORT_CONFIG.copy() + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=IMPORT_CONFIG.copy(), + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "test-instance" + options = { + CONF_SEND_INTERVAL: import_config.pop(CONF_SEND_INTERVAL), + CONF_MAX_DELAY: import_config.pop(CONF_MAX_DELAY), + } + assert result["data"] == import_config + assert result["options"] == options + mock_setup_entry.assert_called_once() + + +@pytest.mark.parametrize( + "source", + [config_entries.SOURCE_USER, config_entries.SOURCE_IMPORT], + ids=["user", "import"], +) +async def test_single_instance(hass, source): + """Test uniqueness of username.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=CS_CONFIG_FULL, + title="test-instance", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": source}, + data=BASE_CONFIG_CS.copy(), + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "single_instance_allowed" + + +@pytest.mark.parametrize( + "side_effect, error_message", + [(EventHubError("test"), "cannot_connect"), (Exception, "unknown")], + ids=["cannot_connect", "unknown"], +) +async def test_connection_error_sas( + hass, + mock_get_eventhub_properties, + side_effect, + error_message, +): + """Test we handle connection errors.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=BASE_CONFIG_SAS.copy(), + ) + assert result["type"] == "form" + assert result["errors"] is None + + mock_get_eventhub_properties.side_effect = side_effect + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + SAS_CONFIG.copy(), + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": error_message} + + +@pytest.mark.parametrize( + "side_effect, error_message", + [(EventHubError("test"), "cannot_connect"), (Exception, "unknown")], + ids=["cannot_connect", "unknown"], +) +async def test_connection_error_cs( + hass, + mock_from_connection_string, + side_effect, + error_message, +): + """Test we handle connection errors.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=BASE_CONFIG_CS.copy(), + ) + assert result["type"] == "form" + assert result["errors"] is None + mock_from_connection_string.return_value.get_eventhub_properties.side_effect = ( + side_effect + ) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + CS_CONFIG.copy(), + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": error_message} + + +async def test_options_flow(hass, entry): + """Test options flow.""" + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + assert result["last_step"] + + updated = await hass.config_entries.options.async_configure( + result["flow_id"], UPDATE_OPTIONS + ) + assert updated["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert updated["data"] == UPDATE_OPTIONS + await hass.async_block_till_done() diff --git a/tests/components/azure_event_hub/test_init.py b/tests/components/azure_event_hub/test_init.py index dd588ad7499..74985266821 100644 --- a/tests/components/azure_event_hub/test_init.py +++ b/tests/components/azure_event_hub/test_init.py @@ -1,83 +1,31 @@ -"""The tests for the Azure Event Hub component.""" -from dataclasses import dataclass -from unittest.mock import MagicMock, patch +"""Test the init functions for AEH.""" +from datetime import timedelta +import logging +from time import monotonic +from unittest.mock import patch +from azure.eventhub.exceptions import EventHubError import pytest -import homeassistant.components.azure_event_hub as azure_event_hub +from homeassistant.components import azure_event_hub +from homeassistant.components.azure_event_hub.const import CONF_SEND_INTERVAL, DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_ON from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow -AZURE_EVENT_HUB_PATH = "homeassistant.components.azure_event_hub" -PRODUCER_PATH = f"{AZURE_EVENT_HUB_PATH}.EventHubProducerClient" -MIN_CONFIG = { - "event_hub_namespace": "namespace", - "event_hub_instance_name": "name", - "event_hub_sas_policy": "policy", - "event_hub_sas_key": "key", -} +from .conftest import FilterTest +from .const import AZURE_EVENT_HUB_PATH, BASIC_OPTIONS, CS_CONFIG_FULL, SAS_CONFIG_FULL + +from tests.common import MockConfigEntry, async_fire_time_changed + +_LOGGER = logging.getLogger(__name__) -@dataclass -class FilterTest: - """Class for capturing a filter test.""" - - id: str - should_pass: bool - - -@pytest.fixture(autouse=True, name="mock_client", scope="module") -def mock_client_fixture(): - """Mock the azure event hub producer client.""" - with patch(f"{PRODUCER_PATH}.send_batch") as mock_send_batch, patch( - f"{PRODUCER_PATH}.close" - ) as mock_close, patch(f"{PRODUCER_PATH}.__init__", return_value=None) as mock_init: - yield ( - mock_init, - mock_send_batch, - mock_close, - ) - - -@pytest.fixture(autouse=True, name="mock_batch") -def mock_batch_fixture(): - """Mock batch creator and return mocked batch object.""" - mock_batch = MagicMock() - with patch(f"{PRODUCER_PATH}.create_batch", return_value=mock_batch): - yield mock_batch - - -@pytest.fixture(autouse=True, name="mock_policy") -def mock_policy_fixture(): - """Mock azure shared key credential.""" - with patch(f"{AZURE_EVENT_HUB_PATH}.EventHubSharedKeyCredential") as policy: - yield policy - - -@pytest.fixture(autouse=True, name="mock_event_data") -def mock_event_data_fixture(): - """Mock the azure event data component.""" - with patch(f"{AZURE_EVENT_HUB_PATH}.EventData") as event_data: - yield event_data - - -@pytest.fixture(autouse=True, name="mock_call_later") -def mock_call_later_fixture(): - """Mock async_call_later to allow queue processing on demand.""" - with patch(f"{AZURE_EVENT_HUB_PATH}.async_call_later") as mock_call_later: - yield mock_call_later - - -async def test_minimal_config(hass): - """Test the minimal config and defaults of component.""" - config = {azure_event_hub.DOMAIN: MIN_CONFIG} - assert await async_setup_component(hass, azure_event_hub.DOMAIN, config) - - -async def test_full_config(hass): - """Test the full config of component.""" +async def test_import(hass): + """Test the popping of the filter and further import of the config.""" config = { - azure_event_hub.DOMAIN: { + DOMAIN: { "send_interval": 10, "max_delay": 10, "filter": { @@ -90,128 +38,178 @@ async def test_full_config(hass): }, } } - config[azure_event_hub.DOMAIN].update(MIN_CONFIG) - assert await async_setup_component(hass, azure_event_hub.DOMAIN, config) + config[DOMAIN].update(CS_CONFIG_FULL) + assert await async_setup_component(hass, DOMAIN, config) -async def _setup(hass, mock_call_later, filter_config): - """Shared set up for filtering tests.""" - config = {azure_event_hub.DOMAIN: {"filter": filter_config}} - config[azure_event_hub.DOMAIN].update(MIN_CONFIG) +async def test_filter_only_config(hass): + """Test the popping of the filter and further import of the config.""" + config = { + DOMAIN: { + "filter": { + "include_domains": ["light"], + "include_entity_globs": ["sensor.included_*"], + "include_entities": ["binary_sensor.included"], + "exclude_domains": ["light"], + "exclude_entity_globs": ["sensor.excluded_*"], + "exclude_entities": ["binary_sensor.excluded"], + }, + } + } + assert await async_setup_component(hass, DOMAIN, config) - assert await async_setup_component(hass, azure_event_hub.DOMAIN, config) + +async def test_unload_entry(hass, entry, mock_create_batch): + """Test being able to unload an entry. + + Queue should be empty, so adding events to the batch should not be called, + this verifies that the unload, calls async_stop, which calls async_send and + shuts down the hub. + """ + assert await hass.config_entries.async_unload(entry.entry_id) + mock_create_batch.add.assert_not_called() + assert entry.state == ConfigEntryState.NOT_LOADED + + +async def test_failed_test_connection(hass, mock_get_eventhub_properties): + """Test being able to unload an entry.""" + entry = MockConfigEntry( + domain=azure_event_hub.DOMAIN, + data=SAS_CONFIG_FULL, + title="test-instance", + options=BASIC_OPTIONS, + ) + entry.add_to_hass(hass) + mock_get_eventhub_properties.side_effect = EventHubError("Test") + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_send_batch_error(hass, entry_with_one_event, mock_send_batch): + """Test a error in send_batch, including recovering at the next interval.""" + mock_send_batch.reset_mock() + mock_send_batch.side_effect = [EventHubError("Test"), None] + async_fire_time_changed( + hass, + utcnow() + timedelta(seconds=entry_with_one_event.options[CONF_SEND_INTERVAL]), + ) await hass.async_block_till_done() - mock_call_later.assert_called_once() - return mock_call_later.call_args[0][2] + mock_send_batch.assert_called_once() + mock_send_batch.reset_mock() + + async_fire_time_changed( + hass, + utcnow() + timedelta(seconds=entry_with_one_event.options[CONF_SEND_INTERVAL]), + ) + await hass.async_block_till_done() + mock_send_batch.assert_called_once() -async def _run_filter_tests(hass, tests, process_queue, mock_batch): - """Run a series of filter tests on azure event hub.""" - for test in tests: - hass.states.async_set(test.id, STATE_ON) +async def test_late_event(hass, entry_with_one_event, mock_create_batch): + """Test the check on late events.""" + with patch( + f"{AZURE_EVENT_HUB_PATH}.time.monotonic", + return_value=monotonic() + timedelta(hours=1).seconds, + ): + async_fire_time_changed( + hass, + utcnow() + + timedelta(seconds=entry_with_one_event.options[CONF_SEND_INTERVAL]), + ) await hass.async_block_till_done() - await process_queue(None) - - if test.should_pass: - mock_batch.add.assert_called_once() - mock_batch.add.reset_mock() - else: - mock_batch.add.assert_not_called() + mock_create_batch.add.assert_not_called() -async def test_allowlist(hass, mock_batch, mock_call_later): - """Test an allowlist only config.""" - process_queue = await _setup( +async def test_full_batch(hass, entry_with_one_event, mock_create_batch): + """Test the full batch behaviour.""" + mock_create_batch.add.side_effect = [ValueError, None] + async_fire_time_changed( hass, - mock_call_later, - { - "include_domains": ["light"], - "include_entity_globs": ["sensor.included_*"], - "include_entities": ["binary_sensor.included"], - }, + utcnow() + timedelta(seconds=entry_with_one_event.options[CONF_SEND_INTERVAL]), ) - - tests = [ - FilterTest("climate.excluded", False), - FilterTest("light.included", True), - FilterTest("sensor.excluded_test", False), - FilterTest("sensor.included_test", True), - FilterTest("binary_sensor.included", True), - FilterTest("binary_sensor.excluded", False), - ] - - await _run_filter_tests(hass, tests, process_queue, mock_batch) + await hass.async_block_till_done() + assert mock_create_batch.add.call_count == 2 -async def test_denylist(hass, mock_batch, mock_call_later): - """Test a denylist only config.""" - process_queue = await _setup( - hass, - mock_call_later, - { - "exclude_domains": ["climate"], - "exclude_entity_globs": ["sensor.excluded_*"], - "exclude_entities": ["binary_sensor.excluded"], - }, - ) +@pytest.mark.parametrize( + "filter_schema, tests", + [ + ( + { + "include_domains": ["light"], + "include_entity_globs": ["sensor.included_*"], + "include_entities": ["binary_sensor.included"], + }, + [ + FilterTest("climate.excluded", 0), + FilterTest("light.included", 1), + FilterTest("sensor.excluded_test", 0), + FilterTest("sensor.included_test", 1), + FilterTest("binary_sensor.included", 1), + FilterTest("binary_sensor.excluded", 0), + ], + ), + ( + { + "exclude_domains": ["climate"], + "exclude_entity_globs": ["sensor.excluded_*"], + "exclude_entities": ["binary_sensor.excluded"], + }, + [ + FilterTest("climate.excluded", 0), + FilterTest("light.included", 1), + FilterTest("sensor.excluded_test", 0), + FilterTest("sensor.included_test", 1), + FilterTest("binary_sensor.included", 1), + FilterTest("binary_sensor.excluded", 0), + ], + ), + ( + { + "include_domains": ["light"], + "include_entity_globs": ["*.included_*"], + "exclude_domains": ["climate"], + "exclude_entity_globs": ["*.excluded_*"], + "exclude_entities": ["light.excluded"], + }, + [ + FilterTest("light.included", 1), + FilterTest("light.excluded_test", 0), + FilterTest("light.excluded", 0), + FilterTest("sensor.included_test", 1), + FilterTest("climate.included_test", 0), + ], + ), + ( + { + "include_entities": ["climate.included", "sensor.excluded_test"], + "exclude_domains": ["climate"], + "exclude_entity_globs": ["*.excluded_*"], + "exclude_entities": ["light.excluded"], + }, + [ + FilterTest("climate.excluded", 0), + FilterTest("climate.included", 1), + FilterTest("switch.excluded_test", 0), + FilterTest("sensor.excluded_test", 1), + FilterTest("light.excluded", 0), + FilterTest("light.included", 1), + ], + ), + ], + ids=["allowlist", "denylist", "filtered_allowlist", "filtered_denylist"], +) +async def test_filter(hass, entry, tests, mock_create_batch): + """Test different filters. - tests = [ - FilterTest("climate.excluded", False), - FilterTest("light.included", True), - FilterTest("sensor.excluded_test", False), - FilterTest("sensor.included_test", True), - FilterTest("binary_sensor.included", True), - FilterTest("binary_sensor.excluded", False), - ] - - await _run_filter_tests(hass, tests, process_queue, mock_batch) - - -async def test_filtered_allowlist(hass, mock_batch, mock_call_later): - """Test an allowlist config with a filtering denylist.""" - process_queue = await _setup( - hass, - mock_call_later, - { - "include_domains": ["light"], - "include_entity_globs": ["*.included_*"], - "exclude_domains": ["climate"], - "exclude_entity_globs": ["*.excluded_*"], - "exclude_entities": ["light.excluded"], - }, - ) - - tests = [ - FilterTest("light.included", True), - FilterTest("light.excluded_test", False), - FilterTest("light.excluded", False), - FilterTest("sensor.included_test", True), - FilterTest("climate.included_test", False), - ] - - await _run_filter_tests(hass, tests, process_queue, mock_batch) - - -async def test_filtered_denylist(hass, mock_batch, mock_call_later): - """Test a denylist config with a filtering allowlist.""" - process_queue = await _setup( - hass, - mock_call_later, - { - "include_entities": ["climate.included", "sensor.excluded_test"], - "exclude_domains": ["climate"], - "exclude_entity_globs": ["*.excluded_*"], - "exclude_entities": ["light.excluded"], - }, - ) - - tests = [ - FilterTest("climate.excluded", False), - FilterTest("climate.included", True), - FilterTest("switch.excluded_test", False), - FilterTest("sensor.excluded_test", True), - FilterTest("light.excluded", False), - FilterTest("light.included", True), - ] - - await _run_filter_tests(hass, tests, process_queue, mock_batch) + Filter_schema is also a fixture which is replaced by the filter_schema + in the parametrize and added to the entry fixture. + """ + for test in tests: + hass.states.async_set(test.entity_id, STATE_ON) + async_fire_time_changed( + hass, utcnow() + timedelta(seconds=entry.options[CONF_SEND_INTERVAL]) + ) + await hass.async_block_till_done() + assert mock_create_batch.add.call_count == test.expected_count + mock_create_batch.add.reset_mock() From 0e28fbbe83bf02e5af83b97e12c5a3460a3da841 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 16 Dec 2021 20:53:19 +0100 Subject: [PATCH 0592/2644] Minor refactor of template vacuum (#61860) --- .../components/template/template_entity.py | 8 ++ homeassistant/components/template/vacuum.py | 105 ++++++------------ 2 files changed, 39 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index 11b9edc7626..5d4c663c7ca 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -58,6 +58,14 @@ TEMPLATE_ENTITY_COMMON_SCHEMA = vol.Schema( } ) +TEMPLATE_ENTITY_ATTRIBUTES_SCHEMA_LEGACY = vol.Schema( + { + vol.Optional(CONF_ATTRIBUTE_TEMPLATES, default={}): vol.Schema( + {cv.string: cv.template} + ), + } +) + TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY = vol.Schema( { vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index a6b8bf0f379..02d9b0f3720 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -43,8 +43,13 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.script import Script -from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN -from .template_entity import TemplateEntity +from .const import DOMAIN +from .template_entity import ( + TEMPLATE_ENTITY_ATTRIBUTES_SCHEMA_LEGACY, + TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY, + TemplateEntity, + rewrite_common_legacy_to_modern_conf, +) _LOGGER = logging.getLogger(__name__) @@ -52,7 +57,6 @@ CONF_VACUUMS = "vacuums" CONF_BATTERY_LEVEL_TEMPLATE = "battery_level_template" CONF_FAN_SPEED_LIST = "fan_speeds" CONF_FAN_SPEED_TEMPLATE = "fan_speed_template" -CONF_ATTRIBUTE_TEMPLATES = "attribute_templates" ENTITY_ID_FORMAT = VACUUM_DOMAIN + ".{}" _VALID_STATES = [ @@ -72,10 +76,6 @@ VACUUM_SCHEMA = vol.All( vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_BATTERY_LEVEL_TEMPLATE): cv.template, vol.Optional(CONF_FAN_SPEED_TEMPLATE): cv.template, - vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, - vol.Optional(CONF_ATTRIBUTE_TEMPLATES, default={}): vol.Schema( - {cv.string: cv.template} - ), vol.Required(SERVICE_START): cv.SCRIPT_SCHEMA, vol.Optional(SERVICE_PAUSE): cv.SCRIPT_SCHEMA, vol.Optional(SERVICE_STOP): cv.SCRIPT_SCHEMA, @@ -87,7 +87,9 @@ VACUUM_SCHEMA = vol.All( vol.Optional(CONF_ENTITY_ID): cv.entity_ids, vol.Optional(CONF_UNIQUE_ID): cv.string, } - ), + ) + .extend(TEMPLATE_ENTITY_ATTRIBUTES_SCHEMA_LEGACY.schema) + .extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY.schema), ) PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( @@ -99,44 +101,15 @@ async def _async_create_entities(hass, config): """Create the Template Vacuums.""" vacuums = [] - for device, device_config in config[CONF_VACUUMS].items(): - friendly_name = device_config.get(CONF_FRIENDLY_NAME, device) - - state_template = device_config.get(CONF_VALUE_TEMPLATE) - battery_level_template = device_config.get(CONF_BATTERY_LEVEL_TEMPLATE) - fan_speed_template = device_config.get(CONF_FAN_SPEED_TEMPLATE) - availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) - attribute_templates = device_config.get(CONF_ATTRIBUTE_TEMPLATES) - - start_action = device_config[SERVICE_START] - pause_action = device_config.get(SERVICE_PAUSE) - stop_action = device_config.get(SERVICE_STOP) - return_to_base_action = device_config.get(SERVICE_RETURN_TO_BASE) - clean_spot_action = device_config.get(SERVICE_CLEAN_SPOT) - locate_action = device_config.get(SERVICE_LOCATE) - set_fan_speed_action = device_config.get(SERVICE_SET_FAN_SPEED) - - fan_speed_list = device_config[CONF_FAN_SPEED_LIST] - unique_id = device_config.get(CONF_UNIQUE_ID) + for object_id, entity_config in config[CONF_VACUUMS].items(): + entity_config = rewrite_common_legacy_to_modern_conf(entity_config) + unique_id = entity_config.get(CONF_UNIQUE_ID) vacuums.append( TemplateVacuum( hass, - device, - friendly_name, - state_template, - battery_level_template, - fan_speed_template, - availability_template, - start_action, - pause_action, - stop_action, - return_to_base_action, - clean_spot_action, - locate_action, - set_fan_speed_action, - fan_speed_list, - attribute_templates, + object_id, + entity_config, unique_id, ) ) @@ -155,71 +128,55 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity): def __init__( self, hass, - device_id, - friendly_name, - state_template, - battery_level_template, - fan_speed_template, - availability_template, - start_action, - pause_action, - stop_action, - return_to_base_action, - clean_spot_action, - locate_action, - set_fan_speed_action, - fan_speed_list, - attribute_templates, + object_id, + config, unique_id, ): """Initialize the vacuum.""" - super().__init__( - attribute_templates=attribute_templates, - availability_template=availability_template, - ) + super().__init__(config=config) self.entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, device_id, hass=hass + ENTITY_ID_FORMAT, object_id, hass=hass ) - self._name = friendly_name + self._name = friendly_name = config.get(CONF_FRIENDLY_NAME, object_id) - self._template = state_template - self._battery_level_template = battery_level_template - self._fan_speed_template = fan_speed_template + self._template = config.get(CONF_VALUE_TEMPLATE) + self._battery_level_template = config.get(CONF_BATTERY_LEVEL_TEMPLATE) + self._fan_speed_template = config.get(CONF_FAN_SPEED_TEMPLATE) self._supported_features = SUPPORT_START - self._start_script = Script(hass, start_action, friendly_name, DOMAIN) + self._start_script = Script(hass, config[SERVICE_START], friendly_name, DOMAIN) self._pause_script = None - if pause_action: + if pause_action := config.get(SERVICE_PAUSE): self._pause_script = Script(hass, pause_action, friendly_name, DOMAIN) self._supported_features |= SUPPORT_PAUSE self._stop_script = None - if stop_action: + if stop_action := config.get(SERVICE_STOP): self._stop_script = Script(hass, stop_action, friendly_name, DOMAIN) self._supported_features |= SUPPORT_STOP self._return_to_base_script = None - if return_to_base_action: + if return_to_base_action := config.get(SERVICE_RETURN_TO_BASE): self._return_to_base_script = Script( hass, return_to_base_action, friendly_name, DOMAIN ) self._supported_features |= SUPPORT_RETURN_HOME self._clean_spot_script = None - if clean_spot_action: + if clean_spot_action := config.get(SERVICE_CLEAN_SPOT): self._clean_spot_script = Script( hass, clean_spot_action, friendly_name, DOMAIN ) self._supported_features |= SUPPORT_CLEAN_SPOT self._locate_script = None - if locate_action: + if locate_action := config.get(SERVICE_LOCATE): self._locate_script = Script(hass, locate_action, friendly_name, DOMAIN) self._supported_features |= SUPPORT_LOCATE self._set_fan_speed_script = None - if set_fan_speed_action: + if set_fan_speed_action := config.get(SERVICE_SET_FAN_SPEED): self._set_fan_speed_script = Script( hass, set_fan_speed_action, friendly_name, DOMAIN ) @@ -237,7 +194,7 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity): self._unique_id = unique_id # List of valid fan speeds - self._fan_speed_list = fan_speed_list + self._fan_speed_list = config[CONF_FAN_SPEED_LIST] @property def name(self): From 4f7182a41adb352acbcf44d528a56604e319deea Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Thu, 16 Dec 2021 19:59:25 +0000 Subject: [PATCH 0593/2644] Use DeviceClass Enums in airly tests (#61989) --- tests/components/airly/test_sensor.py | 39 +++++++++++++-------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/tests/components/airly/test_sensor.py b/tests/components/airly/test_sensor.py index 71912bb768b..2c53ce36d1b 100644 --- a/tests/components/airly/test_sensor.py +++ b/tests/components/airly/test_sensor.py @@ -2,20 +2,17 @@ from datetime import timedelta from homeassistant.components.airly.sensor import ATTRIBUTION -from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + SensorDeviceClass, + SensorStateClass, +) from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - DEVICE_CLASS_AQI, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_PM1, - DEVICE_CLASS_PM10, - DEVICE_CLASS_PM25, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, PERCENTAGE, PRESSURE_HPA, STATE_UNAVAILABLE, @@ -41,7 +38,7 @@ async def test_sensor(hass, aioclient_mock): assert state.state == "23" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "CAQI" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_AQI + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.AQI entry = registry.async_get("sensor.home_caqi") assert entry @@ -52,8 +49,8 @@ async def test_sensor(hass, aioclient_mock): assert state.state == "92.8" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_HUMIDITY - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.HUMIDITY + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT entry = registry.async_get("sensor.home_humidity") assert entry @@ -67,8 +64,8 @@ async def test_sensor(hass, aioclient_mock): state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PM1 - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM1 + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT entry = registry.async_get("sensor.home_pm1") assert entry @@ -82,8 +79,8 @@ async def test_sensor(hass, aioclient_mock): state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PM25 - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM25 + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT entry = registry.async_get("sensor.home_pm2_5") assert entry @@ -97,8 +94,8 @@ async def test_sensor(hass, aioclient_mock): state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PM10 - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM10 + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT entry = registry.async_get("sensor.home_pm10") assert entry @@ -109,8 +106,8 @@ async def test_sensor(hass, aioclient_mock): assert state.state == "1001" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_HPA - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PRESSURE - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT entry = registry.async_get("sensor.home_pressure") assert entry @@ -121,8 +118,8 @@ async def test_sensor(hass, aioclient_mock): assert state.state == "14.2" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT entry = registry.async_get("sensor.home_temperature") assert entry From 1a594d2a8c0009c0358868bdd2eb48300fa0b47f Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Thu, 16 Dec 2021 19:59:55 +0000 Subject: [PATCH 0594/2644] Use DeviceClass Enums in abode tests (#61980) --- tests/components/abode/test_binary_sensor.py | 4 ++-- tests/components/abode/test_sensor.py | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/components/abode/test_binary_sensor.py b/tests/components/abode/test_binary_sensor.py index e4aa08c7f5f..d8f19a19b77 100644 --- a/tests/components/abode/test_binary_sensor.py +++ b/tests/components/abode/test_binary_sensor.py @@ -2,8 +2,8 @@ from homeassistant.components.abode import ATTR_DEVICE_ID from homeassistant.components.abode.const import ATTRIBUTION from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_WINDOW, DOMAIN as BINARY_SENSOR_DOMAIN, + BinarySensorDeviceClass, ) from homeassistant.const import ( ATTR_ATTRIBUTION, @@ -37,4 +37,4 @@ async def test_attributes(hass): assert not state.attributes.get("no_response") assert state.attributes.get("device_type") == "Door Contact" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Front Door" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_WINDOW + assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.WINDOW diff --git a/tests/components/abode/test_sensor.py b/tests/components/abode/test_sensor.py index 5e3195430ab..3d68e4d3d8a 100644 --- a/tests/components/abode/test_sensor.py +++ b/tests/components/abode/test_sensor.py @@ -1,11 +1,10 @@ """Tests for the Abode sensor device.""" from homeassistant.components.abode import ATTR_DEVICE_ID -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, - DEVICE_CLASS_HUMIDITY, PERCENTAGE, TEMP_CELSIUS, ) @@ -35,7 +34,7 @@ async def test_attributes(hass): assert state.attributes.get("device_type") == "LM" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Environment Sensor Humidity" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_HUMIDITY + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.HUMIDITY state = hass.states.get("sensor.environment_sensor_lux") assert state.state == "1.0" From 4a7a3b046968dffc8b174cdc08bb120bff18448d Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Thu, 16 Dec 2021 20:05:12 +0000 Subject: [PATCH 0595/2644] Use DeviceClass Enums in advantage_air tests (#61986) --- tests/components/advantage_air/test_cover.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/advantage_air/test_cover.py b/tests/components/advantage_air/test_cover.py index 363db076ada..2868179b3ee 100644 --- a/tests/components/advantage_air/test_cover.py +++ b/tests/components/advantage_air/test_cover.py @@ -8,11 +8,11 @@ from homeassistant.components.advantage_air.const import ( ) from homeassistant.components.cover import ( ATTR_POSITION, - DEVICE_CLASS_DAMPER, DOMAIN as COVER_DOMAIN, SERVICE_CLOSE_COVER, SERVICE_OPEN_COVER, SERVICE_SET_COVER_POSITION, + CoverDeviceClass, ) from homeassistant.const import ATTR_ENTITY_ID, STATE_OPEN from homeassistant.helpers import entity_registry as er @@ -49,7 +49,7 @@ async def test_cover_async_setup_entry(hass, aioclient_mock): state = hass.states.get(entity_id) assert state assert state.state == STATE_OPEN - assert state.attributes.get("device_class") == DEVICE_CLASS_DAMPER + assert state.attributes.get("device_class") == CoverDeviceClass.DAMPER assert state.attributes.get("current_position") == 100 entry = registry.async_get(entity_id) From fe08668a87df6878ca74f5945b41cbb50ea2c2b9 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Thu, 16 Dec 2021 20:05:56 +0000 Subject: [PATCH 0596/2644] Use DeviceClass Enums in accuweather tests (#61990) --- tests/components/accuweather/test_sensor.py | 48 ++++++++++----------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/tests/components/accuweather/test_sensor.py b/tests/components/accuweather/test_sensor.py index f7f7e30adfc..dc7c978d554 100644 --- a/tests/components/accuweather/test_sensor.py +++ b/tests/components/accuweather/test_sensor.py @@ -7,7 +7,8 @@ from homeassistant.components.accuweather.const import ATTRIBUTION, DOMAIN from homeassistant.components.sensor import ( ATTR_STATE_CLASS, DOMAIN as SENSOR_DOMAIN, - STATE_CLASS_MEASUREMENT, + SensorDeviceClass, + SensorStateClass, ) from homeassistant.const import ( ATTR_ATTRIBUTION, @@ -16,7 +17,6 @@ from homeassistant.const import ( ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, CONCENTRATION_PARTS_PER_CUBIC_METER, - DEVICE_CLASS_TEMPERATURE, LENGTH_FEET, LENGTH_METERS, LENGTH_MILLIMETERS, @@ -47,7 +47,7 @@ async def test_sensor_without_forecast(hass): assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_ICON) == "mdi:weather-fog" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_METERS - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT entry = registry.async_get("sensor.home_cloud_ceiling") assert entry @@ -60,7 +60,7 @@ async def test_sensor_without_forecast(hass): assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_MILLIMETERS assert state.attributes.get(ATTR_ICON) == "mdi:weather-rainy" assert state.attributes.get("type") is None - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT entry = registry.async_get("sensor.home_precipitation") assert entry @@ -83,8 +83,8 @@ async def test_sensor_without_forecast(hass): assert state.state == "25.1" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT entry = registry.async_get("sensor.home_realfeel_temperature") assert entry @@ -96,7 +96,7 @@ async def test_sensor_without_forecast(hass): assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UV_INDEX assert state.attributes.get("level") == "High" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT entry = registry.async_get("sensor.home_uv_index") assert entry @@ -125,7 +125,7 @@ async def test_sensor_with_forecast(hass): assert state.state == "29.8" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_STATE_CLASS) is None entry = registry.async_get("sensor.home_realfeel_temperature_max_0d") @@ -136,7 +136,7 @@ async def test_sensor_with_forecast(hass): assert state.state == "15.1" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_STATE_CLASS) is None entry = registry.async_get("sensor.home_realfeel_temperature_min_0d") @@ -360,8 +360,8 @@ async def test_sensor_enabled_without_forecast(hass): assert state.state == "22.8" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT entry = registry.async_get("sensor.home_apparent_temperature") assert entry @@ -373,7 +373,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.attributes.get(ATTR_ICON) == "mdi:weather-cloudy" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT entry = registry.async_get("sensor.home_cloud_cover") assert entry @@ -384,8 +384,8 @@ async def test_sensor_enabled_without_forecast(hass): assert state.state == "16.2" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT entry = registry.async_get("sensor.home_dew_point") assert entry @@ -396,8 +396,8 @@ async def test_sensor_enabled_without_forecast(hass): assert state.state == "21.1" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT entry = registry.async_get("sensor.home_realfeel_temperature_shade") assert entry @@ -408,8 +408,8 @@ async def test_sensor_enabled_without_forecast(hass): assert state.state == "18.6" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT entry = registry.async_get("sensor.home_wet_bulb_temperature") assert entry @@ -420,8 +420,8 @@ async def test_sensor_enabled_without_forecast(hass): assert state.state == "22.8" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT entry = registry.async_get("sensor.home_wind_chill_temperature") assert entry @@ -433,7 +433,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SPEED_KILOMETERS_PER_HOUR assert state.attributes.get(ATTR_ICON) == "mdi:weather-windy" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT entry = registry.async_get("sensor.home_wind_gust") assert entry @@ -445,7 +445,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SPEED_KILOMETERS_PER_HOUR assert state.attributes.get(ATTR_ICON) == "mdi:weather-windy" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT entry = registry.async_get("sensor.home_wind") assert entry @@ -537,7 +537,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state.state == "28.0" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_STATE_CLASS) is None entry = registry.async_get("sensor.home_realfeel_temperature_shade_max_0d") @@ -549,7 +549,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state.state == "15.1" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE entry = registry.async_get("sensor.home_realfeel_temperature_shade_min_0d") assert entry From 0dbd94886732c27e31ac7de75c21c98ce772fa59 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 16 Dec 2021 21:12:33 +0100 Subject: [PATCH 0597/2644] Add Open-Meteo integration (second attempt) (#61742) --- .coveragerc | 1 + .pre-commit-config.yaml | 2 +- .strict-typing | 1 + CODEOWNERS | 1 + .../components/open_meteo/__init__.py | 56 + .../components/open_meteo/config_flow.py | 54 + homeassistant/components/open_meteo/const.py | 56 + .../components/open_meteo/manifest.json | 10 + .../components/open_meteo/strings.json | 12 + .../open_meteo/translations/en.json | 12 + .../components/open_meteo/weather.py | 80 + homeassistant/generated/config_flows.py | 1 + mypy.ini | 11 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/open_meteo/__init__.py | 1 + tests/components/open_meteo/conftest.py | 49 + .../open_meteo/fixtures/forecast.json | 6660 +++++++++++++++++ .../components/open_meteo/test_config_flow.py | 33 + tests/components/open_meteo/test_init.py | 68 + 20 files changed, 7113 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/open_meteo/__init__.py create mode 100644 homeassistant/components/open_meteo/config_flow.py create mode 100644 homeassistant/components/open_meteo/const.py create mode 100644 homeassistant/components/open_meteo/manifest.json create mode 100644 homeassistant/components/open_meteo/strings.json create mode 100644 homeassistant/components/open_meteo/translations/en.json create mode 100644 homeassistant/components/open_meteo/weather.py create mode 100644 tests/components/open_meteo/__init__.py create mode 100644 tests/components/open_meteo/conftest.py create mode 100644 tests/components/open_meteo/fixtures/forecast.json create mode 100644 tests/components/open_meteo/test_config_flow.py create mode 100644 tests/components/open_meteo/test_init.py diff --git a/.coveragerc b/.coveragerc index 7a13c433b83..78bc9e0ccb1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -771,6 +771,7 @@ omit = homeassistant/components/onvif/event.py homeassistant/components/onvif/parsers.py homeassistant/components/onvif/sensor.py + homeassistant/components/open_meteo/weather.py homeassistant/components/opencv/* homeassistant/components/openevse/sensor.py homeassistant/components/openexchangerates/sensor.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 018c0063f9e..d4d9cf65354 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: hooks: - id: codespell args: - - --ignore-words-list=hass,alot,datas,dof,dur,ether,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing,iam,incomfort,ba,haa + - --ignore-words-list=hass,alot,datas,dof,dur,ether,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,rime,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing,iam,incomfort,ba,haa - --skip="./.*,*.csv,*.json" - --quiet-level=2 exclude_types: [csv, json] diff --git a/.strict-typing b/.strict-typing index 5546086a456..f663ec6a6b9 100644 --- a/.strict-typing +++ b/.strict-typing @@ -95,6 +95,7 @@ homeassistant.components.notify.* homeassistant.components.notion.* homeassistant.components.number.* homeassistant.components.onewire.* +homeassistant.components.open_meteo.* homeassistant.components.openuv.* homeassistant.components.persistent_notification.* homeassistant.components.pi_hole.* diff --git a/CODEOWNERS b/CODEOWNERS index 7f011d3f182..d54ed4827c6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -379,6 +379,7 @@ homeassistant/components/onboarding/* @home-assistant/core homeassistant/components/ondilo_ico/* @JeromeHXP homeassistant/components/onewire/* @garbled1 @epenet homeassistant/components/onvif/* @hunterjm +homeassistant/components/open_meteo/* @frenck homeassistant/components/openerz/* @misialq homeassistant/components/opengarage/* @danielhiversen homeassistant/components/openhome/* @bazwilliams diff --git a/homeassistant/components/open_meteo/__init__.py b/homeassistant/components/open_meteo/__init__.py new file mode 100644 index 00000000000..348f0616556 --- /dev/null +++ b/homeassistant/components/open_meteo/__init__.py @@ -0,0 +1,56 @@ +"""Support for Open-Meteo.""" +from __future__ import annotations + +from open_meteo import Forecast, OpenMeteo, OpenMeteoError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, CONF_ZONE, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN, LOGGER, SCAN_INTERVAL + +PLATFORMS = [Platform.WEATHER] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Open-Meteo from a config entry.""" + session = async_get_clientsession(hass) + open_meteo = OpenMeteo(session=session) + + async def async_update_forecast() -> Forecast: + zone = hass.states.get(entry.data[CONF_ZONE]) + if zone is None: + raise UpdateFailed(f"Zone '{entry.data[CONF_ZONE]}' not found") + + try: + return await open_meteo.forecast( + latitude=zone.attributes[ATTR_LATITUDE], + longitude=zone.attributes[ATTR_LONGITUDE], + current_weather=True, + ) + except OpenMeteoError as err: + raise UpdateFailed("Open-Meteo API communication error") from err + + coordinator: DataUpdateCoordinator[Forecast] = DataUpdateCoordinator( + hass, + LOGGER, + name=f"{DOMAIN}_{entry.data[CONF_ZONE]}", + update_interval=SCAN_INTERVAL, + update_method=async_update_forecast, + ) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload Open-Meteo config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + del hass.data[DOMAIN][entry.entry_id] + return unload_ok diff --git a/homeassistant/components/open_meteo/config_flow.py b/homeassistant/components/open_meteo/config_flow.py new file mode 100644 index 00000000000..76d5436f565 --- /dev/null +++ b/homeassistant/components/open_meteo/config_flow.py @@ -0,0 +1,54 @@ +"""Config flow to configure the Open-Meteo integration.""" +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN, ENTITY_ID_HOME +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_ZONE +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +class OpenMeteoFlowHandler(ConfigFlow, domain=DOMAIN): + """Config flow for OpenMeteo.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + if user_input is not None: + await self.async_set_unique_id(user_input[CONF_ZONE]) + self._abort_if_unique_id_configured() + + zone = self.hass.states.get(user_input[CONF_ZONE]) + return self.async_create_entry( + title=zone.name if zone else "Open-Meteo", + data={CONF_ZONE: user_input[CONF_ZONE]}, + ) + + zones: dict[str, str] = { + entity_id: state.name + for entity_id in self.hass.states.async_entity_ids(ZONE_DOMAIN) + if (state := self.hass.states.get(entity_id)) is not None + } + zones = dict(sorted(zones.items(), key=lambda x: x[1], reverse=True)) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_ZONE): vol.In( + { + ENTITY_ID_HOME: zones.pop(ENTITY_ID_HOME), + **zones, + } + ), + } + ), + ) diff --git a/homeassistant/components/open_meteo/const.py b/homeassistant/components/open_meteo/const.py new file mode 100644 index 00000000000..94e27293bad --- /dev/null +++ b/homeassistant/components/open_meteo/const.py @@ -0,0 +1,56 @@ +"""Constants for the Open-Meteo integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging +from typing import Final + +from homeassistant.components.weather import ( + ATTR_CONDITION_CLOUDY, + ATTR_CONDITION_FOG, + ATTR_CONDITION_LIGHTNING, + ATTR_CONDITION_PARTLYCLOUDY, + ATTR_CONDITION_POURING, + ATTR_CONDITION_RAINY, + ATTR_CONDITION_SNOWY, + ATTR_CONDITION_SUNNY, +) + +DOMAIN: Final = "open_meteo" + +LOGGER = logging.getLogger(__package__) +SCAN_INTERVAL = timedelta(minutes=30) + +# World Meteorological Organization Weather Code +# mapped to Home Assistant weather conditions. +# https://www.weather.gov/tg/wmo +WMO_TO_HA_CONDITION_MAP = { + 0: ATTR_CONDITION_SUNNY, # Clear sky + 1: ATTR_CONDITION_SUNNY, # Mainly clear + 2: ATTR_CONDITION_PARTLYCLOUDY, # Partly cloudy + 3: ATTR_CONDITION_CLOUDY, # Overcast + 45: ATTR_CONDITION_FOG, # Fog + 48: ATTR_CONDITION_FOG, # Depositing rime fog + 51: ATTR_CONDITION_RAINY, # Drizzle: Light intensity + 53: ATTR_CONDITION_RAINY, # Drizzle: Moderate intensity + 55: ATTR_CONDITION_RAINY, # Drizzle: Dense intensity + 56: ATTR_CONDITION_RAINY, # Freezing Drizzle: Light intensity + 57: ATTR_CONDITION_RAINY, # Freezing Drizzle: Dense intensity + 61: ATTR_CONDITION_RAINY, # Rain: Slight intensity + 63: ATTR_CONDITION_RAINY, # Rain: Moderate intensity + 65: ATTR_CONDITION_POURING, # Rain: Heavy intensity + 66: ATTR_CONDITION_RAINY, # Freezing Rain: Light intensity + 67: ATTR_CONDITION_POURING, # Freezing Rain: Heavy intensity + 71: ATTR_CONDITION_SNOWY, # Snow fall: Slight intensity + 73: ATTR_CONDITION_SNOWY, # Snow fall: Moderate intensity + 75: ATTR_CONDITION_SNOWY, # Snow fall: Heavy intensity + 77: ATTR_CONDITION_SNOWY, # Snow grains + 80: ATTR_CONDITION_RAINY, # Rain showers: Slight intensity + 81: ATTR_CONDITION_RAINY, # Rain showers: Moderate intensity + 82: ATTR_CONDITION_POURING, # Rain showers: Violent intensity + 85: ATTR_CONDITION_SNOWY, # Snow showers: Slight intensity + 86: ATTR_CONDITION_SNOWY, # Snow showers: Heavy intensity + 95: ATTR_CONDITION_LIGHTNING, # Thunderstorm: Slight and moderate intensity + 96: ATTR_CONDITION_LIGHTNING, # Thunderstorm with slight hail + 99: ATTR_CONDITION_LIGHTNING, # Thunderstorm with heavy hail +} diff --git a/homeassistant/components/open_meteo/manifest.json b/homeassistant/components/open_meteo/manifest.json new file mode 100644 index 00000000000..ccfc7dbd51d --- /dev/null +++ b/homeassistant/components/open_meteo/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "open_meteo", + "name": "Open-Meteo", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/open_meteo", + "requirements": ["open-meteo==0.2.1"], + "dependencies": ["zone"], + "codeowners": ["@frenck"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/open_meteo/strings.json b/homeassistant/components/open_meteo/strings.json new file mode 100644 index 00000000000..f2f22413403 --- /dev/null +++ b/homeassistant/components/open_meteo/strings.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "description": "Select location to use for weather forecasting", + "data": { + "zone": "Zone" + } + } + } + } +} diff --git a/homeassistant/components/open_meteo/translations/en.json b/homeassistant/components/open_meteo/translations/en.json new file mode 100644 index 00000000000..7736b1da63e --- /dev/null +++ b/homeassistant/components/open_meteo/translations/en.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "zone": "Zone" + }, + "description": "Select location to use for weather forecasting" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/open_meteo/weather.py b/homeassistant/components/open_meteo/weather.py new file mode 100644 index 00000000000..b34de06efe5 --- /dev/null +++ b/homeassistant/components/open_meteo/weather.py @@ -0,0 +1,80 @@ +"""Support for Open-Meteo weather.""" +from __future__ import annotations + +from open_meteo import Forecast as OpenMeteoForecast + +from homeassistant.components.weather import WeatherEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import TEMP_CELSIUS +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import DOMAIN, WMO_TO_HA_CONDITION_MAP + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Open-Meteo weather entity based on a config entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities([OpenMeteoWeatherEntity(entry=entry, coordinator=coordinator)]) + + +class OpenMeteoWeatherEntity(CoordinatorEntity, WeatherEntity): + """Defines an Open-Meteo weather entity.""" + + _attr_temperature_unit = TEMP_CELSIUS + coordinator: DataUpdateCoordinator[OpenMeteoForecast] + + def __init__( + self, *, entry: ConfigEntry, coordinator: DataUpdateCoordinator + ) -> None: + """Initialize Open-Meteo weather entity.""" + super().__init__(coordinator=coordinator) + self._attr_unique_id = entry.entry_id + self._attr_name = entry.title + + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, entry.entry_id)}, + manufacturer="Open-Meteo", + name=entry.title, + ) + + @property + def condition(self) -> str | None: + """Return the current condition.""" + if not self.coordinator.data.current_weather: + return None + return WMO_TO_HA_CONDITION_MAP.get( + self.coordinator.data.current_weather.weather_code + ) + + @property + def temperature(self) -> float | None: + """Return the platform temperature.""" + if not self.coordinator.data.current_weather: + return None + return self.coordinator.data.current_weather.temperature + + @property + def wind_speed(self) -> float | None: + """Return the wind speed.""" + if not self.coordinator.data.current_weather: + return None + return self.coordinator.data.current_weather.wind_speed + + @property + def wind_bearing(self) -> float | str | None: + """Return the wind bearing.""" + if not self.coordinator.data.current_weather: + return None + return self.coordinator.data.current_weather.wind_direction diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 469f5ae3c90..74a90052fa4 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -218,6 +218,7 @@ FLOWS = [ "ondilo_ico", "onewire", "onvif", + "open_meteo", "opengarage", "opentherm_gw", "openuv", diff --git a/mypy.ini b/mypy.ini index 47450bab8ed..475840c3b4d 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1056,6 +1056,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.open_meteo.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.openuv.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 95a0e08e830..53d76bf5a6f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1140,6 +1140,9 @@ onvif-zeep-async==1.2.0 # homeassistant.components.opengarage open-garage==0.2.0 +# homeassistant.components.open_meteo +open-meteo==0.2.1 + # homeassistant.components.opencv # opencv-python-headless==4.5.2.54 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d52f464f1eb..c34dfb8e6f6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -705,6 +705,9 @@ onvif-zeep-async==1.2.0 # homeassistant.components.opengarage open-garage==0.2.0 +# homeassistant.components.open_meteo +open-meteo==0.2.1 + # homeassistant.components.openerz openerz-api==0.1.0 diff --git a/tests/components/open_meteo/__init__.py b/tests/components/open_meteo/__init__.py new file mode 100644 index 00000000000..11ac51700a8 --- /dev/null +++ b/tests/components/open_meteo/__init__.py @@ -0,0 +1 @@ +"""Tests for the Open-Meteo integration.""" diff --git a/tests/components/open_meteo/conftest.py b/tests/components/open_meteo/conftest.py new file mode 100644 index 00000000000..cb950dcc442 --- /dev/null +++ b/tests/components/open_meteo/conftest.py @@ -0,0 +1,49 @@ +"""Fixtures for the Open-Meteo integration tests.""" +from __future__ import annotations + +from collections.abc import Generator +from unittest.mock import MagicMock, patch + +from open_meteo import Forecast +import pytest + +from homeassistant.components.open_meteo.const import DOMAIN +from homeassistant.const import CONF_ZONE + +from tests.common import MockConfigEntry, load_fixture + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="Home", + domain=DOMAIN, + data={CONF_ZONE: "zone.home"}, + unique_id="zone.home", + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[None, None, None]: + """Mock setting up a config entry.""" + with patch( + "homeassistant.components.open_meteo.async_setup_entry", return_value=True + ): + yield + + +@pytest.fixture +def mock_open_meteo(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]: + """Return a mocked Open-Meteo client.""" + fixture: str = "forecast.json" + if hasattr(request, "param") and request.param: + fixture = request.param + + forecast = Forecast.parse_raw(load_fixture(fixture, DOMAIN)) + with patch( + "homeassistant.components.open_meteo.OpenMeteo", autospec=True + ) as open_meteo_mock: + open_meteo = open_meteo_mock.return_value + open_meteo.forecast.return_value = forecast + yield open_meteo diff --git a/tests/components/open_meteo/fixtures/forecast.json b/tests/components/open_meteo/fixtures/forecast.json new file mode 100644 index 00000000000..e9510cb2d2c --- /dev/null +++ b/tests/components/open_meteo/fixtures/forecast.json @@ -0,0 +1,6660 @@ +{ + "generationtime_ms": 2.886056900024414, + "latitude": 52.52, + "daily_units": { + "winddirection_10m_dominant": "°", + "temperature_2m_max": "°C", + "windspeed_10m_max": "km\/h", + "sunrise": "iso8601", + "precipitation_hours": "h", + "temperature_2m_min": "°C", + "apparent_temperature_min": "°C", + "sunset": "iso8601", + "apparent_temperature_max": "°C", + "weathercode": "wmo code", + "windgusts_10m_max": "km\/h", + "shortwave_radiation_sum": "MJ\/m²", + "time": "iso8601", + "precipitation_sum": "mm" + }, + "hourly": { + "soil_moisture_3_9cm": [ + 0.308, + 0.308, + 0.308, + 0.308, + 0.309, + 0.309, + 0.309, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.306, + 0.306, + 0.306, + 0.307, + 0.307, + 0.306, + 0.306, + 0.307, + 0.307, + 0.308, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.305, + 0.305, + 0.306, + 0.308, + 0.31, + 0.306, + 0.306, + 0.306, + 0.307, + 0.307, + 0.308, + 0.308, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.306, + 0.306, + 0.307, + 0.308, + 0.308, + 0.308, + 0.31, + 0.311, + 0.311, + 0.311, + 0.311, + 0.31, + 0.31, + 0.309, + 0.309, + 0.309, + 0.309, + 0.309, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.307, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.301, + 0.301, + 0.301, + 0.301, + 0.301, + 0.301, + 0.301, + 0.301, + 0.301, + 0.301, + 0.301, + 0.301, + 0.302, + 0.303, + 0.305, + 0.305, + 0.306, + 0.306, + 0.306, + 0.305, + 0.305, + 0.304, + 0.304, + 0.304, + 0.303, + 0.303, + 0.303, + 0.303, + 0.303, + 0.303, + 0.302, + 0.302, + 0.303, + 0.308 + ], + "soil_temperature_0cm": [ + 6.6, + 6.6, + 6.5, + 6.1, + 6.1, + 6, + 6.1, + 5.7, + 5.4, + 6.3, + 7, + 7.6, + 7.9, + 8.1, + 7.9, + 7.3, + 6.7, + 6.3, + 6.3, + 5.7, + 5.5, + 5.6, + 5.6, + 4.9, + 5.1, + 4.7, + 4.8, + 4.7, + 3.6, + 2.3, + 1.4, + 0.8, + 0.4, + 1.7, + 2.7, + 3.8, + 4.8, + 5.5, + 4.8, + 4.5, + 4.1, + 3.7, + 3.6, + 3.7, + 3.7, + 3.5, + 3.5, + 3.4, + 3.6, + 3.6, + 3.2, + 3.3, + 3.3, + 3.2, + 3, + 3.1, + 3.1, + 3.3, + 4.5, + 4.9, + 5.3, + 5.5, + 5.2, + 4.6, + 3.7, + 3.3, + 3.1, + 2.8, + 2.2, + 1.9, + 1.8, + 1.7, + 1.4, + 1.1, + 0.3, + -0, + -0, + -0, + -0.1, + -0.1, + -0.2, + -0.2, + 1.4, + 2.8, + 4.6, + 5.4, + 5, + 3.7, + 2.5, + 2.6, + 2.4, + 2.6, + 2.4, + 2.1, + 1.6, + 1.7, + 0.8, + -0, + -0.2, + -0.2, + -0.2, + -0.4, + -0.6, + -0.7, + -0.3, + 0.2, + 1, + 1.9, + 2.9, + 3.8, + 3.6, + 3.1, + 2.3, + 2, + 1.7, + 1.3, + 1, + 0.7, + 0.4, + 0.3, + 0.3, + 0.2, + 0.1, + 0.1, + -0.1, + -0.2, + -0.4, + -0.5, + -0.4, + -0.1, + 0.3, + 0.8, + 1.5, + 2.1, + 2.4, + 1.8, + 1.1, + 1.1, + 1.4, + 1.6, + 1.7, + 1.8, + 1.8, + 1.7, + 1.6, + 1.4, + 1.4, + 1.4, + 1.3, + 1.1, + 0.9, + 0.6, + 0.5, + 0.5, + 0.8, + 1.5, + 2.5, + 3.4, + 3.1, + 2.5, + 1.8, + 1.8, + 2.1, + 2.4, + 2.5, + 2.5, + 2.6, + 2.7 + ], + "weathercode": [ + 3, + 3, + 3, + 3, + 51, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 61, + 61, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 2, + 1, + 1, + 1, + 2, + 3, + 3, + 3, + 3, + 3, + 3, + 61, + 3, + 3, + 3, + 61, + 61, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 61, + 61, + 61, + 61, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 2, + 3, + 3, + 3, + 0, + 0, + 1, + 1, + 2, + 3, + 61, + 3, + 3, + 3, + 3, + 3, + 3, + 1, + 0, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 61, + 61, + 61, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 77, + 77, + 77, + 3, + 3, + 3, + 3, + 3, + 3, + 51, + 51, + 51, + 3, + 3, + 3, + 3, + 3, + 3, + 53, + 53, + 53, + 3, + 3, + 3, + 2, + 2, + 2, + 1, + 1, + 1, + 3, + 3, + 3, + 61, + 61, + 61, + 61, + 61, + 61, + 80 + ], + "windspeed_180m": [ + 26.8, + 26.6, + 27.5, + 24.6, + 27.8, + 28.5, + 27, + 24.9, + 26.6, + 22.8, + 21.1, + 18.6, + 14.3, + 14, + 12.3, + 13.3, + 10.8, + 8.6, + 10.7, + 12, + 14, + 16.4, + 17.1, + 17.9, + 20, + 22.4, + 20.9, + 19.5, + 16.9, + 16.5, + 14.5, + 13.8, + 16.2, + 20.7, + 16.3, + 12.2, + 13.8, + 14.6, + 22.7, + 26.5, + 22.9, + 30.1, + 34.3, + 35.4, + 31.3, + 30.7, + 33.5, + 34.4, + 32.8, + 31.4, + 29.8, + 30.4, + 30.5, + 27.5, + 26.1, + 27.1, + 25.9, + 27.8, + 25.4, + 22.1, + 20.1, + 16.7, + 17.1, + 17, + 20.1, + 16.7, + 20.3, + 23.3, + 32.4, + 34.8, + 36.2, + 35.8, + 34.2, + 33, + 36.1, + 38.7, + 38.8, + 37, + 35.5, + 35.7, + 35.9, + 36.9, + 37.6, + 35.1, + 30, + 25, + 21.1, + 24.5, + 29.8, + 30.4, + 29.5, + 29.8, + 31.9, + 32.2, + 24.2, + 25.7, + 27.5, + 25.7, + 26.5, + 27.4, + 28.5, + 28.8, + 28.9, + 28.6, + 28, + 27.1, + 25.8, + 24.6, + 23.2, + 21.7, + 21.5, + 21.6, + 20.9, + 18.8, + 16.6, + 16.5, + 18.7, + 21.9, + 24.6, + 24.1, + 22.4, + 20.4, + 20.1, + 20.3, + 20, + 19.2, + 18.2, + 17.1, + 16.1, + 15.2, + 15.1, + 16, + 17.6, + 19.5, + 21.8, + 25.4, + 30, + 33.1, + 36.4, + 40, + 41.9, + 43.2, + 43.6, + 41.9, + 39, + 35.9, + 35.1, + 34.9, + 34.7, + 34.5, + 34.3, + 33, + 30.8, + 28, + 24.4, + 21.9, + 19.5, + 17.8, + 18.4, + 20, + 23.4, + 28.5, + 36.2, + 44.9, + 47.4, + 47.8, + 47.9, + 48.8 + ], + "soil_moisture_9_27cm": [ + 0.315, + 0.315, + 0.314, + 0.314, + 0.315, + 0.315, + 0.315, + 0.315, + 0.315, + 0.315, + 0.315, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.314, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.313, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.308, + 0.309, + 0.309, + 0.309, + 0.309, + 0.309, + 0.309, + 0.309, + 0.309, + 0.309, + 0.309, + 0.309, + 0.309, + 0.309, + 0.309, + 0.309 + ], + "shortwave_radiation": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.7, + 23, + 42.1, + 69.7, + 83, + 80.3, + 64.1, + 28, + 7.9, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.7, + 32.4, + 77.2, + 109.4, + 124.7, + 120.1, + 93.1, + 26.9, + 14.6, + 0, + -0, + 0, + -0, + 0, + 0, + -0, + -0, + -0, + 0, + 0, + -0, + -0, + 0, + -0, + 0.3, + 17.4, + 58.5, + 92.6, + 107.1, + 114.2, + 92.2, + 46.9, + 12.3, + 0, + -0, + 0, + -0, + 0.1, + -0, + -0.1, + 0.1, + -0.1, + 0, + 0.1, + -0, + 0.1, + 0, + -0.2, + 0.3, + 27.8, + 71.4, + 115.2, + 117.8, + 83.3, + 87.2, + 54.4, + 11.1, + -0.2, + -0.1, + -0, + -0, + -0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -0, + -0, + 0, + 0, + 51.3, + 121.4, + 182, + 227.4, + 239.7, + 191.8, + 114.5, + 32.3, + 0, + 0, + 0, + -0, + -0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -0, + -0, + 0, + 0, + 44.9, + 103.3, + 139.2, + 150.5, + 141.8, + 116.6, + 74.9, + 23.4, + 0, + 0, + 0, + -0, + -0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -0, + -0, + 0, + 0, + 30.4, + 75.2, + 126.8, + 179.4, + 208.1, + 169.6, + 100.9, + 28.2, + 0, + 0, + 0, + -0, + -0, + 0, + 0 + ], + "cloudcover_mid": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 66, + 88, + 100, + 64, + 63, + 0, + 0, + 0, + 0, + 0, + 0, + 6, + 62, + 62, + 78, + 100, + 92, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 98, + 94, + 95, + 100, + 100, + 100, + 85, + 29, + 21, + 10, + 51, + 0, + 5, + 10, + 16, + 52, + 98, + 100, + 100, + 100, + 100, + 64, + 39, + 35, + 12, + 0, + 33, + 67, + 100, + 95, + 90, + 85, + 90, + 95, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 90, + 81, + 71, + 77, + 82, + 88, + 92, + 96, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 93, + 86, + 79, + 71, + 63, + 55, + 44, + 64, + 84, + 86, + 88, + 90, + 41, + 42, + 42, + 50, + 59, + 67, + 56, + 44, + 33, + 22, + 11, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 20, + 40, + 60, + 73, + 87, + 100, + 100, + 100, + 100, + 94 + ], + "cloudcover": [ + 100, + 100, + 98, + 100, + 100, + 98, + 96, + 88, + 88, + 100, + 94, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 83, + 87, + 100, + 100, + 100, + 79, + 34, + 7, + 0, + 49, + 100, + 99, + 100, + 100, + 95, + 100, + 100, + 100, + 100, + 96, + 95, + 100, + 100, + 100, + 96, + 97, + 100, + 91, + 93, + 100, + 91, + 98, + 100, + 99, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 74, + 100, + 100, + 100, + 0, + 5, + 13, + 18, + 52, + 99, + 100, + 100, + 100, + 100, + 94, + 86, + 88, + 43, + 0, + 33, + 67, + 100, + 99, + 98, + 97, + 98, + 99, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 94, + 88, + 82, + 87, + 92, + 97, + 98, + 99, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 98, + 97, + 95, + 97, + 98, + 100, + 99, + 98, + 98, + 98, + 98, + 98, + 94, + 97, + 99, + 98, + 98, + 97, + 96, + 95, + 95, + 94, + 93, + 92, + 86, + 81, + 75, + 61, + 47, + 33, + 54, + 75, + 96, + 97, + 99, + 100, + 100, + 100, + 100, + 100 + ], + "relativehumidity_2m": [ + 95, + 95, + 94, + 93, + 95, + 95, + 94, + 94, + 95, + 94, + 92, + 88, + 85, + 83, + 82, + 84, + 89, + 89, + 93, + 93, + 95, + 96, + 95, + 96, + 94, + 92, + 89, + 86, + 87, + 90, + 93, + 96, + 97, + 100, + 96, + 88, + 85, + 83, + 79, + 82, + 84, + 83, + 85, + 85, + 84, + 84, + 85, + 86, + 87, + 89, + 90, + 91, + 92, + 91, + 92, + 90, + 88, + 87, + 84, + 80, + 77, + 74, + 73, + 75, + 82, + 86, + 89, + 89, + 83, + 82, + 80, + 78, + 78, + 78, + 79, + 79, + 79, + 81, + 82, + 83, + 83, + 85, + 82, + 77, + 73, + 69, + 68, + 70, + 76, + 82, + 84, + 84, + 83, + 84, + 86, + 87, + 89, + 92, + 92, + 92, + 92, + 93, + 93, + 93, + 92, + 90, + 88, + 85, + 81, + 77, + 77, + 79, + 81, + 83, + 84, + 86, + 87, + 88, + 89, + 88, + 88, + 87, + 87, + 87, + 88, + 88, + 88, + 89, + 90, + 91, + 92, + 89, + 86, + 82, + 81, + 82, + 84, + 87, + 91, + 95, + 94, + 93, + 93, + 93, + 93, + 94, + 94, + 95, + 94, + 93, + 92, + 90, + 89, + 88, + 87, + 83, + 79, + 76, + 77, + 81, + 85, + 88, + 90, + 91, + 91, + 89, + 87, + 88 + ], + "cloudcover_low": [ + 100, + 100, + 98, + 100, + 100, + 98, + 96, + 88, + 88, + 100, + 94, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 83, + 87, + 100, + 100, + 100, + 79, + 34, + 7, + 0, + 49, + 100, + 99, + 100, + 100, + 95, + 100, + 100, + 100, + 91, + 92, + 92, + 91, + 85, + 90, + 96, + 97, + 100, + 91, + 93, + 100, + 86, + 95, + 100, + 97, + 100, + 96, + 93, + 64, + 81, + 62, + 80, + 83, + 89, + 86, + 79, + 20, + 20, + 48, + 71, + 64, + 49, + 19, + 0, + 0, + 0, + 31, + 4, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 0, + 70, + 80, + 89, + 100, + 64, + 88, + 75, + 76, + 34, + 0, + 3, + 6, + 9, + 11, + 12, + 14, + 25, + 36, + 47, + 38, + 29, + 19, + 40, + 61, + 81, + 79, + 77, + 75, + 75, + 75, + 74, + 69, + 64, + 59, + 59, + 58, + 58, + 65, + 72, + 79, + 81, + 84, + 86, + 91, + 95, + 100, + 99, + 98, + 97, + 93, + 90, + 87, + 88, + 93, + 98, + 97, + 96, + 95, + 93, + 91, + 89, + 90, + 91, + 92, + 86, + 81, + 75, + 61, + 47, + 33, + 52, + 71, + 90, + 93, + 96, + 99, + 89, + 80, + 70, + 80 + ], + "soil_moisture_27_81cm": [ + 0.332, + 0.332, + 0.332, + 0.332, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.331, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.33, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.329, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324, + 0.324 + ], + "temperature_2m": [ + 6.9, + 6.8, + 6.8, + 6.6, + 6.5, + 6.4, + 6.4, + 6.2, + 5.9, + 6, + 6.5, + 6.9, + 7.3, + 7.6, + 7.6, + 7.5, + 7.2, + 6.9, + 6.6, + 6.4, + 6, + 5.9, + 5.8, + 5.5, + 5.4, + 5.1, + 5, + 4.9, + 4.2, + 3.2, + 2.2, + 1.5, + 0.8, + 0.2, + 0.6, + 1.8, + 2.7, + 3.8, + 4.5, + 4.5, + 4.4, + 4.2, + 3.9, + 3.9, + 4, + 3.9, + 3.9, + 3.8, + 3.7, + 3.6, + 3.4, + 3.3, + 3.2, + 3.2, + 3.1, + 3.1, + 3.1, + 3.1, + 3.5, + 3.9, + 4.3, + 4.6, + 4.8, + 4.6, + 4, + 3.5, + 3.3, + 3, + 2.7, + 2.3, + 2, + 1.8, + 1.6, + 1.2, + 0.7, + 0.4, + 0.3, + 0.2, + 0.2, + 0.1, + -0, + -0.1, + 0.6, + 1.7, + 2.9, + 4, + 4.5, + 4.2, + 3.5, + 3, + 2.8, + 2.8, + 2.8, + 2.6, + 2.3, + 2, + 1.6, + 0.9, + 0.6, + 0.4, + 0.1, + -0, + -0.2, + -0.2, + -0.1, + 0, + 0.5, + 1.2, + 2.2, + 3.1, + 3.4, + 3.4, + 3.2, + 3, + 2.7, + 2.2, + 1.9, + 1.5, + 1.1, + 0.9, + 0.8, + 0.6, + 0.4, + 0.2, + -0.1, + -0.3, + -0.4, + -0.5, + -0.5, + -0.5, + -0.3, + 0.3, + 1.1, + 2, + 2.2, + 2, + 1.7, + 1.4, + 1, + 0.7, + 2, + 1.9, + 1.8, + 1.8, + 1.7, + 1.6, + 1.5, + 1.5, + 1.3, + 1.1, + 0.8, + 0.5, + 0.1, + -0.2, + -0.3, + 0.2, + 1, + 1.9, + 2.2, + 2.4, + 2.6, + 2.6, + 2.5, + 2.4, + 2.5, + 2.8, + 3, + 3 + ], + "direct_radiation": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.7, + 1.1, + 6.3, + 3.8, + 3.6, + 6.7, + 0.1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.1, + 2.1, + 5.7, + 14.6, + 14.3, + 15.2, + 12.3, + 1.6, + 0.3, + 0, + -0, + 0, + -0, + 0, + 0, + -0, + -0, + 0, + 0, + 0, + -0, + 0, + 0, + -0, + 0, + 0.1, + 1.8, + 2.5, + 1.9, + 1.7, + 0.9, + 0, + -0, + 0, + -0, + 0, + 0, + -0, + -0, + -0.1, + 0.1, + -0.2, + 0, + 0, + -0, + 0.2, + -0.1, + -0.1, + 0.1, + 1.2, + 5.2, + 22, + 27.9, + 15.4, + 26.4, + 3.6, + 0.1, + -0.2, + 0, + -0, + -0, + -0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -0, + -0, + 0, + 0, + 29.8, + 72.1, + 117.5, + 160.8, + 177.8, + 135.5, + 71.8, + 16.7, + 0, + 0, + 0, + -0, + -0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -0, + -0, + 0, + 0, + 18.6, + 42.2, + 53.5, + 52.3, + 43.3, + 38.3, + 23.5, + 7, + 0, + 0, + 0, + -0, + -0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -0, + -0, + 0, + 0, + 5.7, + 17.3, + 44.9, + 84.7, + 113.8, + 92.4, + 52, + 13.2, + 0, + 0, + 0, + -0, + -0, + 0, + 0 + ], + "dewpoint_2m": [ + 6.2, + 6.1, + 5.9, + 5.6, + 5.7, + 5.7, + 5.6, + 5.3, + 5.2, + 5.2, + 5.3, + 5.1, + 4.9, + 4.9, + 4.8, + 5, + 5.6, + 5.2, + 5.5, + 5.4, + 5.3, + 5.2, + 5.1, + 4.8, + 4.5, + 3.9, + 3.4, + 2.7, + 2.3, + 1.7, + 1.2, + 0.9, + 0.4, + 0.1, + -0, + 0, + 0.5, + 1.1, + 1.2, + 1.7, + 1.9, + 1.4, + 1.6, + 1.6, + 1.5, + 1.5, + 1.6, + 1.7, + 1.8, + 2, + 2, + 2, + 2, + 1.9, + 1.9, + 1.6, + 1.3, + 1.2, + 1.1, + 0.8, + 0.6, + 0.4, + 0.4, + 0.6, + 1.2, + 1.3, + 1.6, + 1.4, + 0.7, + 0.5, + 0.1, + -0.4, + -0.6, + -1.1, + -1.7, + -2.3, + -2.3, + -2.5, + -2.5, + -2.5, + -2.4, + -2.1, + -1.8, + -1.4, + -0.9, + -0.5, + -0.2, + 0.1, + 0.6, + 0.9, + 1.1, + 1, + 1, + 1, + 0.8, + 0.4, + -0.3, + -1, + -1.3, + -1.4, + -1.5, + -1.6, + -1.7, + -1.7, + -1.7, + -1.7, + -1.6, + -1.4, + -1.2, + -0.9, + -0.8, + -0.8, + -0.7, + -0.6, + -0.5, + -0.5, + -0.5, + -0.6, + -0.7, + -0.8, + -1, + -1.2, + -1.2, + -1.2, + -1.1, + -1.1, + -1.1, + -1, + -1, + -1, + -1, + -0.8, + -0.6, + -0.4, + -0.4, + -0.5, + -0.5, + -0.1, + 0.5, + 1, + 1.1, + 1, + 0.8, + 0.7, + 0.7, + 0.7, + 0.7, + 0.7, + 0.5, + 0.1, + -0.3, + -1, + -1.4, + -1.9, + -2.3, + -2.4, + -2.3, + -2, + -1.4, + -0.6, + 0.3, + 0.7, + 1, + 1.2, + 1.2, + 1.1, + 1.1, + 1.2 + ], + "freezinglevel_height": [ + 2432, + 2434, + 2512, + 2448, + 2512, + 2524, + 2608, + 2458, + 2446, + 2454, + 2454, + 2438, + 2544, + 2554, + 2544, + 2540, + 2550, + 2578, + 2516, + 2586, + 2540, + 2516, + 2508, + 2440, + 2464, + 2464, + 2412, + 2432, + 2260, + 2003, + 1897, + 1774, + 702, + 702, + 693, + 1511, + 635, + 634, + 522, + 668, + 624, + 697, + 674, + 586, + 910, + 856, + 846, + 697, + 665, + 623, + 611, + 633, + 562, + 566, + 620, + 438, + 399, + 398, + 412, + 466, + 501, + 537, + 548, + 513, + 492, + 488, + 448, + 462, + 564, + 569, + 544, + 555, + 540, + 514, + 565, + 471, + 464, + 530, + 507, + 562, + 603, + 566, + 574, + 489, + 462, + 472, + 569, + 562, + 567, + 579, + 597, + 605, + 604, + 598, + 589, + 585, + 582, + 573, + 562, + 548, + 524, + 498, + 468, + 434, + 416, + 402, + 394, + 399, + 412, + 438, + 469, + 508, + 548, + 559, + 562, + 551, + 529, + 497, + 456, + 431, + 407, + 379, + 366, + 356, + 331, + 291, + 241, + 176, + 125, + 72, + 50, + 105, + 199, + 299, + 326, + 308, + 298, + 329, + 376, + 420, + 424, + 411, + 381, + 344, + 297, + 245, + 227, + 219, + 202, + 172, + 135, + 93, + 67, + 44, + 41, + 68, + 113, + 191, + 258, + 335, + 455, + 565, + 687, + 845, + 950, + 1049, + 1187, + 1320 + ], + "soil_temperature_54cm": [ + 8.5, + 8.5, + 8.5, + 8.5, + 8.4, + 8.4, + 8.4, + 8.4, + 8.4, + 8.4, + 8.4, + 8.4, + 8.4, + 8.4, + 8.4, + 8.4, + 8.4, + 8.4, + 8.4, + 8.4, + 8.4, + 8.4, + 8.4, + 8.4, + 8.4, + 8.4, + 8.3, + 8.3, + 8.3, + 8.3, + 8.3, + 8.3, + 8.3, + 8.3, + 8.3, + 8.3, + 8.3, + 8.2, + 8.2, + 8.2, + 8.2, + 8.2, + 8.2, + 8.2, + 8.1, + 8.1, + 8.1, + 8.1, + 8.1, + 8.1, + 8.1, + 8, + 8, + 8, + 8, + 8, + 8, + 8, + 7.9, + 7.9, + 7.9, + 7.9, + 7.9, + 7.9, + 7.9, + 7.8, + 7.8, + 7.8, + 7.5, + 7.5, + 7.5, + 7.5, + 7.5, + 7.4, + 7.4, + 7.4, + 7.4, + 7.4, + 7.4, + 7.3, + 7.3, + 7.3, + 7.3, + 7.3, + 7.2, + 7.2, + 7.2, + 7.2, + 7.2, + 7.1, + 7.1, + 7.1, + 7.1, + 7.1, + 7.1, + 7, + 7, + 7, + 7, + 7, + 7, + 6.9, + 6.9, + 6.9, + 6.9, + 6.9, + 6.8, + 6.8, + 6.8, + 6.8, + 6.8, + 6.8, + 6.7, + 6.7, + 6.7, + 6.7, + 6.7, + 6.7, + 6.6, + 6.6, + 6.6, + 6.6, + 6.6, + 6.6, + 6.5, + 6.5, + 6.5, + 6.5, + 6.5, + 6.4, + 6.4, + 6.4, + 6.4, + 6.4, + 6.4, + 6.3, + 6.3, + 6.3, + 6.3, + 6.3, + 6.3, + 6.3, + 6.2, + 6.2, + 6.2, + 6.2, + 6.2, + 6.2, + 6.2, + 6.1, + 6.1, + 6.1, + 6.1, + 6.1, + 6.1, + 6.1, + 6.1, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 5.9, + 5.9 + ], + "soil_temperature_6cm": [ + 6.4, + 6.4, + 6.4, + 6.4, + 6.3, + 6.3, + 6.3, + 6.1, + 6, + 6, + 6.3, + 6.7, + 7.1, + 7.5, + 7.6, + 7.5, + 7.3, + 7, + 6.8, + 6.5, + 6.3, + 6.2, + 6.1, + 6, + 5.8, + 5.6, + 5.5, + 5.5, + 5.2, + 4.7, + 4, + 3.4, + 2.9, + 2.7, + 3.1, + 3.6, + 4.2, + 4.7, + 5, + 5, + 4.8, + 4.6, + 4.4, + 4.4, + 4.3, + 4.3, + 4.2, + 4.2, + 4.2, + 4.2, + 4.1, + 4, + 4, + 3.9, + 3.9, + 3.8, + 3.8, + 3.8, + 4, + 4.4, + 4.8, + 5.1, + 5.3, + 5.2, + 4.8, + 4.5, + 4.2, + 4, + 3.5, + 3.2, + 3, + 2.9, + 2.7, + 2.6, + 2.2, + 1.9, + 1.6, + 1.5, + 1.4, + 1.3, + 1.2, + 1.2, + 1.4, + 2, + 2.8, + 3.7, + 4.2, + 4.2, + 3.8, + 3.4, + 3.2, + 3.1, + 3.1, + 2.9, + 2.7, + 2.6, + 2.4, + 1.9, + 1.7, + 1.5, + 1.2, + 1.1, + 1, + 0.9, + 0.8, + 0.8, + 0.9, + 1.4, + 2, + 2.6, + 2.9, + 3.1, + 3.1, + 3, + 2.8, + 2.5, + 2.2, + 2, + 1.7, + 1.6, + 1.5, + 1.3, + 1.2, + 1.1, + 1, + 0.9, + 0.9, + 0.8, + 0.7, + 0.6, + 0.7, + 1, + 1.4, + 1.9, + 2.5, + 2.5, + 2.4, + 2.3, + 2.2, + 2.1, + 2.1, + 2.1, + 2.1, + 2.1, + 2.1, + 2.1, + 2.1, + 2.1, + 2.1, + 2, + 1.9, + 1.7, + 1.5, + 1.3, + 1.2, + 1.6, + 2.1, + 2.7, + 2.8, + 2.8, + 2.8, + 2.7, + 2.7, + 2.6, + 2.6, + 2.7, + 2.8, + 2.8 + ], + "soil_moisture_1_3cm": [ + 0.305, + 0.305, + 0.305, + 0.305, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.306, + 0.305, + 0.305, + 0.305, + 0.305, + 0.305, + 0.305, + 0.305, + 0.305, + 0.305, + 0.305, + 0.305, + 0.305, + 0.305, + 0.305, + 0.305, + 0.305, + 0.305, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.303, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.306, + 0.306, + 0.305, + 0.305, + 0.305, + 0.305, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.303, + 0.303, + 0.303, + 0.303, + 0.302, + 0.303, + 0.308, + 0.31, + 0.312, + 0.303, + 0.303, + 0.303, + 0.303, + 0.303, + 0.303, + 0.303, + 0.303, + 0.303, + 0.303, + 0.303, + 0.303, + 0.303, + 0.303, + 0.303, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.303, + 0.303, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.303, + 0.303, + 0.303, + 0.303, + 0.303, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.303, + 0.303, + 0.303, + 0.303, + 0.303, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.303, + 0.304, + 0.304, + 0.299, + 0.299, + 0.298, + 0.298, + 0.299, + 0.299, + 0.299, + 0.299, + 0.299, + 0.299, + 0.3, + 0.301, + 0.303, + 0.305, + 0.307, + 0.307, + 0.306, + 0.304, + 0.304, + 0.303, + 0.303, + 0.302, + 0.302, + 0.301, + 0.301, + 0.301, + 0.301, + 0.301, + 0.301, + 0.301, + 0.3, + 0.299, + 0.302, + 0.312 + ], + "winddirection_80m": [ + 263, + 268, + 277, + 271, + 278, + 278, + 276, + 273, + 264, + 271, + 263, + 257, + 257, + 266, + 253, + 258, + 240, + 232, + 194, + 198, + 183, + 186, + 179, + 178, + 165, + 167, + 171, + 168, + 154, + 156, + 166, + 173, + 182, + 192, + 202, + 199, + 204, + 208, + 246, + 217, + 230, + 227, + 232, + 235, + 241, + 239, + 245, + 255, + 257, + 260, + 254, + 251, + 257, + 252, + 240, + 243, + 247, + 256, + 253, + 252, + 244, + 239, + 234, + 217, + 204, + 208, + 173, + 172, + 179, + 177, + 173, + 170, + 167, + 159, + 158, + 159, + 158, + 158, + 152, + 147, + 145, + 143, + 141, + 140, + 145, + 138, + 142, + 143, + 148, + 150, + 151, + 144, + 153, + 169, + 167, + 170, + 179, + 181, + 178, + 171, + 162, + 157, + 152, + 148, + 148, + 150, + 152, + 150, + 145, + 140, + 142, + 146, + 153, + 160, + 171, + 188, + 194, + 195, + 195, + 196, + 196, + 198, + 199, + 201, + 203, + 206, + 210, + 216, + 220, + 225, + 235, + 247, + 258, + 266, + 270, + 267, + 265, + 266, + 268, + 270, + 290, + 287, + 285, + 285, + 287, + 289, + 290, + 290, + 290, + 291, + 291, + 291, + 289, + 286, + 281, + 278, + 274, + 268, + 260, + 249, + 231, + 218, + 208, + 202, + 200, + 199, + 199, + 198 + ], + "winddirection_10m": [ + 261, + 265, + 274, + 268, + 275, + 272, + 271, + 269, + 258, + 268, + 261, + 255, + 256, + 263, + 251, + 256, + 233, + 218, + 185, + 188, + 173, + 175, + 168, + 168, + 158, + 160, + 165, + 162, + 149, + 151, + 159, + 158, + 170, + 190, + 202, + 197, + 202, + 205, + 245, + 214, + 223, + 224, + 229, + 231, + 238, + 236, + 243, + 253, + 255, + 258, + 251, + 248, + 254, + 249, + 238, + 240, + 245, + 254, + 252, + 251, + 243, + 237, + 233, + 216, + 201, + 207, + 167, + 167, + 167, + 165, + 162, + 163, + 160, + 152, + 149, + 149, + 148, + 148, + 140, + 136, + 134, + 133, + 135, + 136, + 142, + 135, + 139, + 141, + 144, + 142, + 137, + 130, + 144, + 157, + 146, + 154, + 164, + 160, + 156, + 147, + 136, + 130, + 125, + 123, + 126, + 132, + 137, + 135, + 131, + 127, + 128, + 132, + 137, + 141, + 146, + 156, + 166, + 174, + 181, + 182, + 182, + 182, + 186, + 189, + 194, + 196, + 198, + 203, + 206, + 211, + 222, + 237, + 253, + 262, + 268, + 264, + 260, + 261, + 263, + 265, + 286, + 284, + 282, + 283, + 285, + 287, + 287, + 287, + 288, + 289, + 290, + 290, + 288, + 284, + 280, + 277, + 273, + 267, + 259, + 244, + 212, + 202, + 199, + 198, + 197, + 195, + 194, + 194 + ], + "time": [ + "2021-11-24T00:00", + "2021-11-24T01:00", + "2021-11-24T02:00", + "2021-11-24T03:00", + "2021-11-24T04:00", + "2021-11-24T05:00", + "2021-11-24T06:00", + "2021-11-24T07:00", + "2021-11-24T08:00", + "2021-11-24T09:00", + "2021-11-24T10:00", + "2021-11-24T11:00", + "2021-11-24T12:00", + "2021-11-24T13:00", + "2021-11-24T14:00", + "2021-11-24T15:00", + "2021-11-24T16:00", + "2021-11-24T17:00", + "2021-11-24T18:00", + "2021-11-24T19:00", + "2021-11-24T20:00", + "2021-11-24T21:00", + "2021-11-24T22:00", + "2021-11-24T23:00", + "2021-11-25T00:00", + "2021-11-25T01:00", + "2021-11-25T02:00", + "2021-11-25T03:00", + "2021-11-25T04:00", + "2021-11-25T05:00", + "2021-11-25T06:00", + "2021-11-25T07:00", + "2021-11-25T08:00", + "2021-11-25T09:00", + "2021-11-25T10:00", + "2021-11-25T11:00", + "2021-11-25T12:00", + "2021-11-25T13:00", + "2021-11-25T14:00", + "2021-11-25T15:00", + "2021-11-25T16:00", + "2021-11-25T17:00", + "2021-11-25T18:00", + "2021-11-25T19:00", + "2021-11-25T20:00", + "2021-11-25T21:00", + "2021-11-25T22:00", + "2021-11-25T23:00", + "2021-11-26T00:00", + "2021-11-26T01:00", + "2021-11-26T02:00", + "2021-11-26T03:00", + "2021-11-26T04:00", + "2021-11-26T05:00", + "2021-11-26T06:00", + "2021-11-26T07:00", + "2021-11-26T08:00", + "2021-11-26T09:00", + "2021-11-26T10:00", + "2021-11-26T11:00", + "2021-11-26T12:00", + "2021-11-26T13:00", + "2021-11-26T14:00", + "2021-11-26T15:00", + "2021-11-26T16:00", + "2021-11-26T17:00", + "2021-11-26T18:00", + "2021-11-26T19:00", + "2021-11-26T20:00", + "2021-11-26T21:00", + "2021-11-26T22:00", + "2021-11-26T23:00", + "2021-11-27T00:00", + "2021-11-27T01:00", + "2021-11-27T02:00", + "2021-11-27T03:00", + "2021-11-27T04:00", + "2021-11-27T05:00", + "2021-11-27T06:00", + "2021-11-27T07:00", + "2021-11-27T08:00", + "2021-11-27T09:00", + "2021-11-27T10:00", + "2021-11-27T11:00", + "2021-11-27T12:00", + "2021-11-27T13:00", + "2021-11-27T14:00", + "2021-11-27T15:00", + "2021-11-27T16:00", + "2021-11-27T17:00", + "2021-11-27T18:00", + "2021-11-27T19:00", + "2021-11-27T20:00", + "2021-11-27T21:00", + "2021-11-27T22:00", + "2021-11-27T23:00", + "2021-11-28T00:00", + "2021-11-28T01:00", + "2021-11-28T02:00", + "2021-11-28T03:00", + "2021-11-28T04:00", + "2021-11-28T05:00", + "2021-11-28T06:00", + "2021-11-28T07:00", + "2021-11-28T08:00", + "2021-11-28T09:00", + "2021-11-28T10:00", + "2021-11-28T11:00", + "2021-11-28T12:00", + "2021-11-28T13:00", + "2021-11-28T14:00", + "2021-11-28T15:00", + "2021-11-28T16:00", + "2021-11-28T17:00", + "2021-11-28T18:00", + "2021-11-28T19:00", + "2021-11-28T20:00", + "2021-11-28T21:00", + "2021-11-28T22:00", + "2021-11-28T23:00", + "2021-11-29T00:00", + "2021-11-29T01:00", + "2021-11-29T02:00", + "2021-11-29T03:00", + "2021-11-29T04:00", + "2021-11-29T05:00", + "2021-11-29T06:00", + "2021-11-29T07:00", + "2021-11-29T08:00", + "2021-11-29T09:00", + "2021-11-29T10:00", + "2021-11-29T11:00", + "2021-11-29T12:00", + "2021-11-29T13:00", + "2021-11-29T14:00", + "2021-11-29T15:00", + "2021-11-29T16:00", + "2021-11-29T17:00", + "2021-11-29T18:00", + "2021-11-29T19:00", + "2021-11-29T20:00", + "2021-11-29T21:00", + "2021-11-29T22:00", + "2021-11-29T23:00", + "2021-11-30T00:00", + "2021-11-30T01:00", + "2021-11-30T02:00", + "2021-11-30T03:00", + "2021-11-30T04:00", + "2021-11-30T05:00", + "2021-11-30T06:00", + "2021-11-30T07:00", + "2021-11-30T08:00", + "2021-11-30T09:00", + "2021-11-30T10:00", + "2021-11-30T11:00", + "2021-11-30T12:00", + "2021-11-30T13:00", + "2021-11-30T14:00", + "2021-11-30T15:00", + "2021-11-30T16:00", + "2021-11-30T17:00", + "2021-11-30T18:00", + "2021-11-30T19:00", + "2021-11-30T20:00", + "2021-11-30T21:00", + "2021-11-30T22:00", + "2021-11-30T23:00" + ], + "soil_moisture_0_1cm": [ + 0.304, + 0.304, + 0.304, + 0.304, + 0.305, + 0.305, + 0.305, + 0.305, + 0.305, + 0.305, + 0.305, + 0.304, + 0.304, + 0.304, + 0.303, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.304, + 0.303, + 0.303, + 0.303, + 0.304, + 0.304, + 0.303, + 0.303, + 0.303, + 0.303, + 0.302, + 0.303, + 0.304, + 0.303, + 0.303, + 0.303, + 0.305, + 0.306, + 0.305, + 0.305, + 0.304, + 0.304, + 0.304, + 0.303, + 0.303, + 0.303, + 0.303, + 0.303, + 0.303, + 0.303, + 0.303, + 0.303, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.303, + 0.31, + 0.312, + 0.313, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.301, + 0.301, + 0.301, + 0.301, + 0.301, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.301, + 0.301, + 0.301, + 0.301, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.302, + 0.303, + 0.304, + 0.304, + 0.298, + 0.298, + 0.297, + 0.297, + 0.298, + 0.298, + 0.298, + 0.298, + 0.298, + 0.299, + 0.3, + 0.302, + 0.304, + 0.307, + 0.309, + 0.308, + 0.306, + 0.303, + 0.302, + 0.302, + 0.302, + 0.301, + 0.301, + 0.3, + 0.3, + 0.3, + 0.3, + 0.3, + 0.3, + 0.301, + 0.3, + 0.299, + 0.302, + 0.314 + ], + "direct_normal_irradiance": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.4, + 8.5, + 5.8, + 25.1, + 13.4, + 13, + 28, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1.5, + 24.6, + 32.2, + 58.9, + 50.7, + 54.7, + 52.4, + 10.4, + 3.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1.4, + 10.5, + 10.4, + 6.8, + 6, + 4, + 0.3, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1.4, + 13.3, + 30.5, + 91.3, + 101.2, + 56.8, + 114.8, + 23.7, + 1.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 341.5, + 430.3, + 493.5, + 589.6, + 660.9, + 596.3, + 477.7, + 191.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 213.7, + 256.9, + 227.8, + 193.9, + 162.6, + 170.3, + 158.4, + 80.4, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 65.4, + 107.1, + 193.9, + 317.5, + 431.7, + 415.9, + 356.1, + 151.7, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "soil_temperature_18cm": [ + 6.3, + 6.4, + 6.4, + 6.5, + 6.5, + 6.6, + 6.6, + 6.6, + 6.6, + 6.6, + 6.6, + 6.7, + 6.7, + 6.8, + 6.9, + 7, + 7.1, + 7.1, + 7.1, + 7.1, + 7.1, + 7.1, + 7, + 7, + 6.9, + 6.9, + 6.8, + 6.8, + 6.7, + 6.6, + 6.5, + 6.3, + 6.1, + 5.9, + 5.7, + 5.6, + 5.5, + 5.5, + 5.6, + 5.6, + 5.6, + 5.6, + 5.6, + 5.6, + 5.5, + 5.5, + 5.5, + 5.5, + 5.4, + 5.4, + 5.4, + 5.3, + 5.3, + 5.3, + 5.2, + 5.2, + 5.2, + 5.1, + 5.1, + 5.1, + 5.2, + 5.2, + 5.3, + 5.4, + 5.4, + 5.4, + 5.4, + 5.3, + 4.9, + 4.9, + 4.8, + 4.7, + 4.6, + 4.6, + 4.5, + 4.4, + 4.2, + 4.1, + 4, + 3.9, + 3.8, + 3.7, + 3.6, + 3.5, + 3.5, + 3.6, + 3.7, + 3.9, + 4, + 4, + 4.1, + 4.1, + 4.1, + 4.1, + 4.1, + 4, + 4, + 3.9, + 3.8, + 3.8, + 3.6, + 3.5, + 3.4, + 3.3, + 3.2, + 3.1, + 3.1, + 3, + 3, + 3.1, + 3.1, + 3.2, + 3.4, + 3.4, + 3.4, + 3.5, + 3.5, + 3.4, + 3.4, + 3.4, + 3.3, + 3.2, + 3.2, + 3.1, + 3, + 3, + 2.9, + 2.8, + 2.8, + 2.7, + 2.7, + 2.6, + 2.6, + 2.6, + 2.9, + 2.9, + 3, + 3.1, + 3.1, + 3.1, + 3.1, + 3.1, + 3.1, + 3.1, + 3.1, + 3.1, + 3.1, + 3.1, + 3.1, + 3.1, + 3.1, + 3.1, + 3.1, + 3, + 2.9, + 2.9, + 2.9, + 3, + 3, + 3.1, + 3.2, + 3.2, + 3.2, + 3.3, + 3.3, + 3.3, + 3.3, + 3.4 + ], + "winddirection_180m": [ + 266, + 271, + 284, + 279, + 287, + 290, + 283, + 277, + 277, + 274, + 274, + 267, + 258, + 267, + 254, + 259, + 253, + 246, + 208, + 208, + 196, + 199, + 194, + 191, + 174, + 178, + 181, + 177, + 165, + 168, + 186, + 199, + 201, + 207, + 219, + 222, + 222, + 214, + 247, + 229, + 241, + 236, + 245, + 247, + 247, + 243, + 250, + 257, + 259, + 263, + 257, + 255, + 261, + 256, + 247, + 248, + 250, + 257, + 254, + 253, + 245, + 240, + 234, + 218, + 205, + 209, + 185, + 186, + 200, + 198, + 196, + 193, + 192, + 187, + 183, + 180, + 180, + 181, + 178, + 173, + 170, + 169, + 167, + 168, + 168, + 156, + 144, + 145, + 157, + 166, + 175, + 168, + 177, + 192, + 200, + 195, + 200, + 202, + 199, + 196, + 192, + 190, + 188, + 186, + 185, + 184, + 183, + 183, + 182, + 182, + 181, + 180, + 182, + 189, + 203, + 219, + 221, + 218, + 216, + 217, + 219, + 222, + 223, + 223, + 225, + 230, + 236, + 246, + 253, + 260, + 268, + 271, + 271, + 272, + 302, + 298, + 297, + 300, + 305, + 308, + 306, + 303, + 298, + 296, + 294, + 291, + 291, + 291, + 292, + 293, + 293, + 293, + 291, + 287, + 282, + 278, + 275, + 268, + 264, + 261, + 252, + 239, + 228, + 220, + 215, + 211, + 207, + 206 + ], + "apparent_temperature": [ + 4.4, + 4.2, + 4.3, + 4.4, + 4.1, + 4, + 3.9, + 3.7, + 3.6, + 3.4, + 4.1, + 4.6, + 4.9, + 5.2, + 5.3, + 5.4, + 5.5, + 5.3, + 4.8, + 4.7, + 4.2, + 4, + 3.9, + 3.4, + 3.2, + 2.8, + 2.5, + 2.3, + 1.6, + 0.5, + -0.5, + -1.2, + -2, + -2.9, + -2.6, + -1.3, + -0.3, + 0.7, + 0.9, + 1.1, + 1.4, + 0.8, + 0.4, + 0.5, + 0.5, + 0.3, + 0.2, + 0.1, + 0.1, + -0, + -0.1, + -0.3, + -0.5, + -0.2, + -0.2, + -0.3, + -0.4, + -1, + -0.2, + 0.2, + 0.7, + 1.3, + 1.4, + 1.3, + 0.7, + 0.4, + 0.5, + 0, + -0.6, + -1.1, + -1.5, + -1.9, + -2.2, + -2.6, + -3.2, + -3.5, + -3.6, + -3.6, + -3.7, + -3.8, + -3.9, + -4, + -3.4, + -2, + -0.9, + 0.2, + 0.6, + 0.5, + 0, + -0.3, + -0.3, + -0.4, + -0.4, + -0.6, + -0.8, + -1, + -1.4, + -2.1, + -2.4, + -2.7, + -3, + -3.2, + -3.4, + -3.5, + -3.5, + -3.3, + -2.8, + -2, + -1, + 0, + 0.3, + 0.4, + 0.3, + 0.2, + -0.1, + -0.5, + -0.9, + -1.4, + -1.9, + -2.1, + -2.3, + -2.5, + -2.8, + -3, + -3.3, + -3.5, + -3.6, + -3.6, + -3.6, + -3.6, + -3.3, + -2.7, + -1.9, + -1.2, + -1.4, + -1.4, + -1.6, + -1.9, + -2.4, + -2.9, + -1.6, + -1.8, + -2, + -2.1, + -2.3, + -2.4, + -2.4, + -2.4, + -2.5, + -2.9, + -3.3, + -3.9, + -4.2, + -4.5, + -4.6, + -4, + -3.1, + -2, + -1.3, + -0.7, + -0.3, + -0.5, + -1, + -1.4, + -1.5, + -1.4, + -1.3, + -1.2 + ], + "cloudcover_high": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 3, + 28, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 92, + 1, + 0, + 0, + 0, + 0, + 0, + 83, + 100, + 0, + 7, + 100, + 100, + 100, + 3, + 0, + 0, + 0, + 0, + 0, + 14, + 62, + 1, + 58, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 87, + 99, + 100, + 100, + 100, + 100, + 100, + 100, + 55, + 100, + 100, + 100, + 0, + 0, + 4, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 33, + 67, + 100, + 94, + 87, + 81, + 75, + 70, + 65, + 43, + 22, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 7, + 15, + 22, + 15, + 7, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 11, + 22, + 33, + 22, + 11, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 8, + 16, + 24, + 49, + 75, + 100, + 100, + 100, + 100, + 67 + ], + "pressure_msl": [ + 1025.2, + 1025.7, + 1025.7, + 1025.2, + 1024.7, + 1024.2, + 1024.2, + 1024.2, + 1024.2, + 1023.7, + 1024.2, + 1023.7, + 1023.2, + 1022.2, + 1021.2, + 1021.2, + 1020.7, + 1020.2, + 1020.2, + 1019.7, + 1018.7, + 1018.7, + 1018.2, + 1017.7, + 1016.7, + 1015.7, + 1015.2, + 1014.2, + 1013.7, + 1012.7, + 1012.2, + 1011.1, + 1011.1, + 1010.6, + 1010.1, + 1009.6, + 1008.6, + 1008.1, + 1007.6, + 1007.1, + 1007.1, + 1006.6, + 1006.1, + 1006.1, + 1005.6, + 1005.6, + 1005.1, + 1005.1, + 1005.1, + 1005.1, + 1004.6, + 1004.1, + 1004.1, + 1003.6, + 1003.6, + 1003.6, + 1003.1, + 1002.6, + 1002.6, + 1002.1, + 1001.6, + 1000.6, + 1000.1, + 999.6, + 999.6, + 999.1, + 998.6, + 998.1, + 997.2, + 996.7, + 996.2, + 995.7, + 995.2, + 994.7, + 994.2, + 993.7, + 993.2, + 992.7, + 992.7, + 992.2, + 992.7, + 992.7, + 992.7, + 992.7, + 992.2, + 991.7, + 991.7, + 991.7, + 992.2, + 992.2, + 992.7, + 992.7, + 992.7, + 993.2, + 993.2, + 993.2, + 993.7, + 993.2, + 993.7, + 993.7, + 993.7, + 993.7, + 994.2, + 994.2, + 994.7, + 994.7, + 995.2, + 995.2, + 995.2, + 995.2, + 995.2, + 995.2, + 995.7, + 995.7, + 996.2, + 996.2, + 996.7, + 996.7, + 996.7, + 996.7, + 997.2, + 997.2, + 997.2, + 997.7, + 997.7, + 997.7, + 998.2, + 998.7, + 999.3, + 999.3, + 999.8, + 1000.3, + 1000.3, + 1000.3, + 1000.6, + 1001.1, + 1001.1, + 1001.6, + 1002.1, + 1002.1, + 1006.6, + 1007.1, + 1007.1, + 1007.6, + 1007.6, + 1008.2, + 1008.2, + 1008.7, + 1008.7, + 1008.7, + 1008.7, + 1009.2, + 1009.2, + 1009.7, + 1009.7, + 1009.7, + 1009.2, + 1008.7, + 1008.2, + 1007.6, + 1006.6, + 1005.6, + 1004.6, + 1003.6, + 1002.1, + 1001.1, + 999.1, + 998.1 + ], + "diffuse_radiation": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.7, + 22.3, + 41, + 63.3, + 79.1, + 76.7, + 57.4, + 27.9, + 7.9, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.6, + 30.3, + 71.4, + 94.8, + 110.4, + 104.9, + 80.8, + 25.3, + 14.3, + -0, + 0, + 0, + -0, + 0, + -0, + 0, + 0, + -0, + 0, + 0, + 0, + -0, + 0, + -0, + 0.3, + 17.3, + 56.7, + 90.1, + 105.3, + 112.5, + 91.3, + 46.8, + 12.3, + 0, + -0, + 0, + -0.1, + 0.1, + -0, + -0, + -0, + 0, + -0, + 0.1, + -0, + -0.1, + 0.1, + -0, + 0.2, + 26.7, + 66.2, + 93.2, + 89.9, + 67.9, + 60.8, + 50.8, + 11, + 0, + -0.1, + -0, + -0, + -0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -0, + -0, + 0, + 0, + 21.5, + 49.3, + 64.5, + 66.6, + 62, + 56.3, + 42.7, + 15.6, + 0, + 0, + 0, + -0, + -0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -0, + -0, + 0, + 0, + 26.2, + 61.1, + 85.6, + 98.2, + 98.6, + 78.3, + 51.4, + 16.4, + 0, + 0, + 0, + -0, + -0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -0, + -0, + 0, + 0, + 24.8, + 57.9, + 81.8, + 94.8, + 94.4, + 77.1, + 48.9, + 15, + 0, + 0, + 0, + -0, + -0, + 0, + 0 + ], + "snow_depth": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "windspeed_10m": [ + 10.4, + 10.9, + 10.1, + 7.9, + 8.8, + 8.9, + 9.3, + 9.4, + 8.1, + 9.6, + 8.5, + 7.6, + 7.9, + 7.4, + 7.1, + 6.4, + 4, + 3.1, + 5, + 4.3, + 4.6, + 4.9, + 5.3, + 5.3, + 5.8, + 6, + 6.3, + 5.9, + 5.9, + 6, + 5.2, + 4.7, + 5.1, + 7, + 6.9, + 6.6, + 6.6, + 7.8, + 11.5, + 10.5, + 8.1, + 10.1, + 10.9, + 10.9, + 11.2, + 11.8, + 12.3, + 12.9, + 12.7, + 12.5, + 12.1, + 12.3, + 12.6, + 10.9, + 10, + 10.8, + 10.8, + 14.8, + 12.1, + 11.5, + 10.5, + 8.8, + 9.2, + 8.7, + 9.2, + 7.8, + 6.3, + 7.5, + 7.9, + 7.9, + 8.7, + 9.5, + 9.2, + 9.4, + 9.6, + 9.4, + 9.6, + 9.1, + 9.2, + 9.4, + 9.2, + 9.5, + 10.6, + 9.2, + 9.9, + 10.2, + 10.7, + 10.4, + 8.9, + 8.2, + 7.2, + 7.6, + 7.7, + 7.4, + 6.5, + 6.6, + 6.2, + 5.8, + 5.7, + 5.6, + 5.9, + 6.2, + 6.6, + 7, + 7, + 6.9, + 6.8, + 6.6, + 6.2, + 5.9, + 5.8, + 5.8, + 5.6, + 5.1, + 4.5, + 4.1, + 4.3, + 4.9, + 5.6, + 5.7, + 5.5, + 5.3, + 5.4, + 5.7, + 5.9, + 5.6, + 5.2, + 4.7, + 4.5, + 4.4, + 4.3, + 4.6, + 5.4, + 6.5, + 8.7, + 7.7, + 6.8, + 7.5, + 8.6, + 9.8, + 11.1, + 11.7, + 12.5, + 13, + 13.4, + 13.6, + 13.4, + 12.9, + 12.5, + 12.8, + 13.5, + 14, + 13.8, + 13.3, + 12.4, + 11.9, + 11.3, + 10, + 8.2, + 6, + 5.5, + 7.4, + 10, + 13, + 14.3, + 15.2, + 15.8, + 16.1 + ], + "vapor_pressure_deficit": [ + 0.05, + 0.05, + 0.06, + 0.07, + 0.05, + 0.05, + 0.05, + 0.05, + 0.05, + 0.05, + 0.08, + 0.12, + 0.16, + 0.17, + 0.19, + 0.16, + 0.11, + 0.11, + 0.07, + 0.06, + 0.05, + 0.04, + 0.04, + 0.04, + 0.05, + 0.07, + 0.09, + 0.12, + 0.1, + 0.08, + 0.05, + 0.03, + 0.02, + 0, + 0.03, + 0.08, + 0.11, + 0.14, + 0.18, + 0.15, + 0.14, + 0.14, + 0.12, + 0.12, + 0.13, + 0.13, + 0.12, + 0.11, + 0.1, + 0.08, + 0.08, + 0.07, + 0.06, + 0.07, + 0.06, + 0.08, + 0.09, + 0.1, + 0.13, + 0.16, + 0.19, + 0.22, + 0.23, + 0.21, + 0.14, + 0.11, + 0.09, + 0.08, + 0.13, + 0.13, + 0.14, + 0.15, + 0.15, + 0.15, + 0.14, + 0.14, + 0.13, + 0.12, + 0.11, + 0.11, + 0.1, + 0.09, + 0.12, + 0.16, + 0.21, + 0.25, + 0.27, + 0.25, + 0.18, + 0.14, + 0.12, + 0.12, + 0.13, + 0.12, + 0.1, + 0.09, + 0.08, + 0.06, + 0.05, + 0.05, + 0.05, + 0.05, + 0.04, + 0.04, + 0.05, + 0.06, + 0.08, + 0.1, + 0.14, + 0.17, + 0.18, + 0.17, + 0.15, + 0.13, + 0.12, + 0.1, + 0.09, + 0.08, + 0.08, + 0.08, + 0.08, + 0.08, + 0.08, + 0.08, + 0.08, + 0.07, + 0.07, + 0.07, + 0.06, + 0.05, + 0.05, + 0.07, + 0.09, + 0.13, + 0.14, + 0.13, + 0.11, + 0.09, + 0.06, + 0.03, + 0.04, + 0.05, + 0.05, + 0.05, + 0.05, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.05, + 0.06, + 0.07, + 0.07, + 0.08, + 0.1, + 0.14, + 0.17, + 0.16, + 0.14, + 0.11, + 0.09, + 0.07, + 0.06, + 0.07, + 0.08, + 0.1, + 0.09 + ], + "windgusts_10m": [ + 22.1, + 23.8, + 23.5, + 21.5, + 18.4, + 19.6, + 19.9, + 20.3, + 19.6, + 20.8, + 25, + 19.1, + 17.9, + 17.6, + 16.8, + 15.7, + 14, + 8.5, + 10.4, + 11, + 9.5, + 10.2, + 11.1, + 11.8, + 12.2, + 13, + 14.4, + 13.3, + 13.2, + 12.5, + 12.7, + 10.6, + 10.4, + 15.6, + 16.8, + 18.8, + 15.9, + 17.8, + 25.3, + 24.6, + 22.5, + 21.4, + 23.4, + 23.3, + 24.7, + 25.3, + 26.2, + 29.2, + 28.2, + 27.9, + 26.9, + 26.8, + 28, + 27.2, + 23.5, + 24.9, + 24.6, + 31.8, + 32.6, + 27.5, + 25.1, + 23.1, + 20.4, + 20.2, + 24, + 20.1, + 16.8, + 16, + 20.5, + 20.1, + 21.1, + 22.2, + 23.6, + 24.4, + 25.7, + 26.4, + 26.3, + 27.2, + 27.1, + 27, + 26.4, + 26.4, + 27.1, + 28.7, + 28.1, + 27.2, + 26.5, + 25.5, + 23.6, + 23.6, + 22.2, + 21.4, + 20.4, + 19.4, + 18.4, + 18.1, + 17.7, + 17.4, + 17.6, + 17.9, + 18.1, + 18.4, + 18.6, + 18.9, + 19.2, + 19.5, + 19.9, + 21.5, + 23, + 24.6, + 24.1, + 23.6, + 23.1, + 21.4, + 19.8, + 18.1, + 15.8, + 13.5, + 11.2, + 10.9, + 10.6, + 10.3, + 8.9, + 7.5, + 6, + 5.1, + 4.2, + 3.3, + 5.5, + 7.8, + 10, + 14.4, + 18.8, + 23.2, + 24, + 25.6, + 27.2, + 26.1, + 25, + 24, + 25.6, + 27.1, + 28.7, + 29.6, + 30.5, + 31.4, + 30.5, + 29.7, + 28.8, + 30.3, + 31.8, + 33.3, + 32.3, + 31.3, + 30.4, + 28.3, + 26.2, + 24.2, + 21.1, + 18, + 14.9, + 19.9, + 24.9, + 29.9, + 32, + 34.1, + 36.3, + 36.9 + ], + "winddirection_120m": [ + 265, + 269, + 280, + 274, + 281, + 282, + 279, + 275, + 270, + 273, + 266, + 259, + 257, + 266, + 254, + 258, + 245, + 240, + 202, + 205, + 189, + 192, + 186, + 184, + 169, + 172, + 176, + 172, + 158, + 161, + 174, + 185, + 192, + 201, + 203, + 201, + 205, + 210, + 246, + 221, + 235, + 229, + 235, + 238, + 243, + 241, + 247, + 256, + 258, + 262, + 255, + 252, + 258, + 253, + 243, + 245, + 248, + 256, + 253, + 253, + 245, + 239, + 234, + 218, + 204, + 209, + 179, + 179, + 189, + 188, + 184, + 181, + 179, + 172, + 170, + 169, + 169, + 169, + 164, + 159, + 156, + 155, + 153, + 153, + 147, + 140, + 143, + 144, + 153, + 157, + 163, + 156, + 164, + 179, + 183, + 182, + 190, + 192, + 190, + 185, + 179, + 175, + 172, + 168, + 168, + 168, + 168, + 166, + 164, + 161, + 161, + 162, + 166, + 174, + 187, + 205, + 208, + 207, + 205, + 206, + 207, + 209, + 211, + 212, + 214, + 218, + 223, + 230, + 236, + 243, + 253, + 259, + 265, + 269, + 271, + 272, + 272, + 273, + 274, + 275, + 298, + 293, + 288, + 288, + 289, + 290, + 291, + 291, + 292, + 292, + 293, + 292, + 290, + 286, + 281, + 278, + 275, + 268, + 262, + 255, + 242, + 227, + 215, + 207, + 204, + 203, + 202, + 201 + ], + "windspeed_120m": [ + 24.7, + 24.5, + 23.8, + 20.2, + 21.5, + 22, + 23.5, + 22.3, + 22.2, + 21.2, + 18.6, + 14.6, + 13.8, + 13.2, + 12, + 12.7, + 9.1, + 8.1, + 11.3, + 12.5, + 13.6, + 15, + 15.3, + 16.1, + 17.6, + 18, + 17.9, + 16, + 14.8, + 15, + 14.5, + 12.9, + 14.8, + 18.2, + 10.1, + 9.9, + 9.7, + 13, + 21.6, + 22.1, + 19.3, + 23.4, + 26.6, + 25.5, + 26.5, + 27.7, + 29.1, + 30.2, + 29, + 27.8, + 27, + 26.9, + 27.4, + 23.8, + 22.1, + 23.9, + 23.4, + 28.4, + 23.9, + 21.3, + 19.3, + 15.9, + 16.5, + 16.4, + 19, + 15.9, + 16.9, + 19.5, + 27.9, + 29.4, + 30.7, + 31.2, + 29.3, + 28.6, + 31.6, + 34.1, + 35.2, + 33.5, + 32.8, + 33.5, + 33.4, + 34.1, + 33.3, + 27.9, + 19.2, + 18.6, + 20.4, + 23.3, + 27.3, + 27.1, + 26.5, + 27.4, + 29.3, + 28.8, + 22.7, + 23.9, + 25, + 24.2, + 24, + 23.5, + 23.2, + 23.4, + 23.7, + 24.1, + 24.4, + 24.6, + 24, + 22.1, + 19.5, + 17, + 17.2, + 18.2, + 18.5, + 16.5, + 14, + 13.5, + 15.8, + 19.2, + 22.4, + 22.1, + 20.5, + 18.9, + 19.1, + 19.8, + 20, + 19.1, + 17.7, + 15.9, + 14.8, + 13.8, + 13.4, + 14.1, + 15.5, + 17.2, + 18.2, + 20.8, + 23.9, + 26, + 28, + 30.1, + 34.7, + 33.6, + 32.6, + 32.7, + 32.9, + 33.2, + 33.3, + 33.4, + 33.2, + 33, + 32.6, + 31.4, + 29.6, + 27.3, + 24.3, + 22, + 19.6, + 17.3, + 16.7, + 17, + 18.5, + 21.8, + 27.2, + 34.1, + 37, + 38.7, + 39.8, + 40.2 + ], + "windspeed_80m": [ + 20.9, + 21.3, + 19.9, + 16.7, + 18.1, + 18.4, + 19.3, + 18.9, + 17.9, + 18.3, + 15.5, + 13, + 13, + 12.3, + 11.4, + 11.5, + 7.7, + 7.2, + 10.1, + 10.6, + 11.3, + 12, + 12.3, + 12.7, + 14.1, + 13.8, + 14, + 12.1, + 12.3, + 12.8, + 12.3, + 11.4, + 12.5, + 11.3, + 9.7, + 9.2, + 9.3, + 12, + 20, + 19.4, + 16.4, + 20.5, + 21.8, + 21.8, + 22.3, + 23.6, + 24.6, + 25.8, + 25.1, + 24.1, + 23.4, + 23.6, + 24, + 20.7, + 19.2, + 20.8, + 20.6, + 26.7, + 21.8, + 19.9, + 17.9, + 14.8, + 15.5, + 15.4, + 17.2, + 14.5, + 13.1, + 14.8, + 21.2, + 21.6, + 22.6, + 21.9, + 20.4, + 20.2, + 22.7, + 24.1, + 25.2, + 24.1, + 24.8, + 25, + 24.5, + 25.1, + 21.7, + 16.7, + 17.6, + 17.5, + 19.2, + 20.9, + 20.8, + 20.1, + 19.4, + 20.7, + 21.7, + 21.6, + 18.2, + 18.9, + 19, + 18.7, + 18.5, + 18.2, + 18.3, + 18.7, + 19.3, + 20, + 20.3, + 20.5, + 19.7, + 17.6, + 14.7, + 12.2, + 12.8, + 14.5, + 15.8, + 14.3, + 12.2, + 11.3, + 13, + 15.5, + 17.9, + 17.6, + 16.4, + 15.1, + 15, + 15.2, + 15.1, + 14.5, + 13.6, + 12.4, + 11.5, + 10.7, + 10.2, + 10.6, + 11.9, + 13.5, + 16.1, + 15.4, + 15.3, + 16.5, + 18.4, + 20.4, + 24.6, + 25.3, + 26.3, + 27.2, + 28, + 28.4, + 27.8, + 26.7, + 25.7, + 26.2, + 27.1, + 27.6, + 26.7, + 25.1, + 22.8, + 21, + 19, + 16.5, + 14.9, + 13.8, + 14.3, + 17, + 21.4, + 26.9, + 29.7, + 31.7, + 33.3, + 33.9 + ], + "evapotranspiration": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.01, + 0.01, + 0.02, + 0.02, + 0.02, + 0.02, + 0.01, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.01, + 0.01, + 0.01, + 0.01, + 0, + 0, + 0, + 0, + 0.01, + 0.02, + 0.02, + 0.02, + 0.03, + 0.02, + 0.02, + 0.01, + 0.01, + 0.01, + 0.02, + 0.02, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.02, + 0.02, + 0.03, + 0.02, + 0.02, + 0.02, + 0.02, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.01, + 0.01, + 0.02, + 0.02, + 0.03, + 0.03, + 0.03, + 0.02, + 0.02, + 0.02, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.01, + 0.01, + 0.02, + 0.02, + 0.03, + 0.03, + 0.02, + 0.02, + 0.01, + 0.01, + 0.01, + 0.01, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.03, + 0.03, + 0.03, + 0.02, + 0.02, + 0.02, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.01, + 0.02 + ], + "precipitation": [ + 0, + 0, + 0.01, + 0, + 0.03, + 0.01, + 0, + 0, + 0, + 0, + 0, + 0, + 0.01, + 0.01, + 0, + 0.06, + 0.06, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.03, + 0.07, + 0, + 0, + 0, + 0.09, + 0.09, + 0.01, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.01, + 0.1, + 0.3, + 0.2, + 0.15, + 0, + 0, + 0, + 0, + 0.01, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.1, + 0.01, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.05, + 0.05, + 0.05, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.01, + 0.01, + 0.01, + 0.03, + 0.03, + 0.03, + 0.02, + 0.02, + 0.02, + 0.13, + 0.13, + 0.13, + 0, + 0, + 0, + 0.07, + 0.07, + 0.07, + 0.16, + 0.16, + 0.16, + 0.01, + 0.01, + 0.01, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.03, + 0.03, + 0.03, + 0.04, + 0.04, + 0.04, + 0.88 + ] + }, + "daily": { + "temperature_2m_max": [ + 7.6, + 5.4, + 4.8, + 4.5, + 3.4, + 2.2, + 3 + ], + "precipitation_hours": [ + 7, + 5, + 5, + 3, + 3, + 13, + 15 + ], + "shortwave_radiation_sum": [ + 1.44, + 2.16, + 1.95, + 2.05, + 4.18, + 2.86, + 3.31 + ], + "winddirection_10m_dominant": [ + 251, + 210, + 230, + 143, + 143, + 248, + 256 + ], + "windspeed_10m_max": [ + 10.9, + 12.9, + 14.8, + 10.7, + 7, + 13, + 16.1 + ], + "apparent_temperature_min": [ + 3.4, + -2.9, + -1.9, + -4, + -3.5, + -3.6, + -4.6 + ], + "sunset": [ + "2021-11-24T16:04", + "2021-11-25T16:03", + "2021-11-26T16:02", + "2021-11-27T16:01", + "2021-11-28T16:00", + "2021-11-29T15:59", + "2021-11-30T15:59" + ], + "weathercode": [ + 61, + 61, + 61, + 61, + 61, + 77, + 80 + ], + "sunrise": [ + "2021-11-24T07:41", + "2021-11-25T07:43", + "2021-11-26T07:45", + "2021-11-27T07:46", + "2021-11-28T07:48", + "2021-11-29T07:49", + "2021-11-30T07:51" + ], + "apparent_temperature_max": [ + 5.5, + 3.2, + 1.4, + 0.6, + 0.4, + -1.2, + -0.3 + ], + "temperature_2m_min": [ + 5.5, + 0.2, + 1.8, + -0.1, + -0.2, + -0.5, + -0.3 + ], + "windgusts_10m_max": [ + 8.5, + 10.4, + 16, + 18.1, + 10.9, + 3.3, + 14.9 + ], + "precipitation_sum": [ + 0.19, + 0.29, + 0.76, + 0.12, + 0.15, + 0.64, + 1.74 + ], + "time": [ + "2021-11-24", + "2021-11-25", + "2021-11-26", + "2021-11-27", + "2021-11-28", + "2021-11-29", + "2021-11-30" + ] + }, + "utc_offset_seconds": 3600, + "hourly_units": { + "precipitation": "mm", + "shortwave_radiation": "W\/m²", + "soil_moisture_0_1cm": "m³\/m³", + "pressure_msl": "hPa", + "soil_moisture_3_9cm": "m³\/m³", + "soil_temperature_54cm": "°C", + "soil_temperature_18cm": "°C", + "winddirection_120m": "°", + "vapor_pressure_deficit": "kPa", + "dewpoint_2m": "°C", + "winddirection_180m": "°", + "windspeed_10m": "km\/h", + "cloudcover_low": "%", + "cloudcover_mid": "%", + "cloudcover_high": "%", + "windgusts_10m": "km\/h", + "soil_moisture_9_27cm": "m³\/m³", + "windspeed_120m": "km\/h", + "winddirection_10m": "°", + "time": "iso8601", + "soil_temperature_6cm": "°C", + "apparent_temperature": "°C", + "windspeed_80m": "km\/h", + "soil_moisture_1_3cm": "m³\/m³", + "diffuse_radiation": "W\/m²", + "snow_depth": "m", + "windspeed_180m": "km\/h", + "weathercode": "wmo code", + "direct_normal_irradiance": "W\/m²", + "relativehumidity_2m": "%", + "soil_moisture_27_81cm": "m³\/m³", + "winddirection_80m": "°", + "freezinglevel_height": "m", + "evapotranspiration": "mm", + "cloudcover": "%", + "soil_temperature_0cm": "°C", + "direct_radiation": "W\/m²", + "temperature_2m": "°C" + }, + "longitude": 13.419998, + "elevation": 44.8125, + "current_weather": { + "temperature": 5.5, + "windspeed": 5.3, + "weathercode": 3, + "winddirection": 168, + "time": "2021-11-24T23:00" + } +} \ No newline at end of file diff --git a/tests/components/open_meteo/test_config_flow.py b/tests/components/open_meteo/test_config_flow.py new file mode 100644 index 00000000000..f985e2a6193 --- /dev/null +++ b/tests/components/open_meteo/test_config_flow.py @@ -0,0 +1,33 @@ +"""Tests for the Open-Meteo config flow.""" + +from unittest.mock import MagicMock + +from homeassistant.components.open_meteo.const import DOMAIN +from homeassistant.components.zone import ENTITY_ID_HOME +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_ZONE +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM + + +async def test_full_user_flow( + hass: HomeAssistant, + mock_setup_entry: MagicMock, +) -> None: + """Test the full user configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == SOURCE_USER + assert "flow_id" in result + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_ZONE: ENTITY_ID_HOME}, + ) + + assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("title") == "test home" + assert result2.get("data") == {CONF_ZONE: ENTITY_ID_HOME} diff --git a/tests/components/open_meteo/test_init.py b/tests/components/open_meteo/test_init.py new file mode 100644 index 00000000000..38619bc09db --- /dev/null +++ b/tests/components/open_meteo/test_init.py @@ -0,0 +1,68 @@ +"""Tests for the Open-Meteo integration.""" +from unittest.mock import AsyncMock, MagicMock, patch + +from open_meteo import OpenMeteoConnectionError +from pytest import LogCaptureFixture + +from homeassistant.components.open_meteo.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_ZONE +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_load_unload_config_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_open_meteo: AsyncMock, +) -> None: + """Test the Open-Meteo configuration entry loading/unloading.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert not hass.data.get(DOMAIN) + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + +@patch( + "homeassistant.components.open_meteo.OpenMeteo.forecast", + side_effect=OpenMeteoConnectionError, +) +async def test_config_entry_not_ready( + mock_forecast: MagicMock, + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the Open-Meteo configuration entry not ready.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_forecast.call_count == 1 + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_config_entry_zone_removed( + hass: HomeAssistant, + caplog: LogCaptureFixture, +) -> None: + """Test the Open-Meteo configuration entry not ready.""" + mock_config_entry = MockConfigEntry( + title="My Castle", + domain=DOMAIN, + data={CONF_ZONE: "zone.castle"}, + unique_id="zone.castle", + ) + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + assert "Zone 'zone.castle' not found" in caplog.text From 84dad5d6785790f426e4c3bff8d97fa526f99b88 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Thu, 16 Dec 2021 20:22:04 +0000 Subject: [PATCH 0598/2644] Use DeviceClass Enums in brother tests (#62110) --- tests/components/brother/test_sensor.py | 38 ++++++++++++------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/tests/components/brother/test_sensor.py b/tests/components/brother/test_sensor.py index d0eb0819303..2ffaf3a2a23 100644 --- a/tests/components/brother/test_sensor.py +++ b/tests/components/brother/test_sensor.py @@ -8,14 +8,14 @@ from homeassistant.components.brother.sensor import UNIT_PAGES from homeassistant.components.sensor import ( ATTR_STATE_CLASS, DOMAIN as SENSOR_DOMAIN, - STATE_CLASS_MEASUREMENT, + SensorDeviceClass, + SensorStateClass, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, - DEVICE_CLASS_TIMESTAMP, PERCENTAGE, STATE_UNAVAILABLE, ) @@ -67,7 +67,7 @@ async def test_sensors(hass): assert state.attributes.get(ATTR_ICON) == "mdi:printer-3d-nozzle" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "75" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT entry = registry.async_get("sensor.hl_l2340dw_black_toner_remaining") assert entry @@ -78,7 +78,7 @@ async def test_sensors(hass): assert state.attributes.get(ATTR_ICON) == "mdi:printer-3d-nozzle" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "10" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT entry = registry.async_get("sensor.hl_l2340dw_cyan_toner_remaining") assert entry @@ -89,7 +89,7 @@ async def test_sensors(hass): assert state.attributes.get(ATTR_ICON) == "mdi:printer-3d-nozzle" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "8" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT entry = registry.async_get("sensor.hl_l2340dw_magenta_toner_remaining") assert entry @@ -100,7 +100,7 @@ async def test_sensors(hass): assert state.attributes.get(ATTR_ICON) == "mdi:printer-3d-nozzle" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "2" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT entry = registry.async_get("sensor.hl_l2340dw_yellow_toner_remaining") assert entry @@ -113,7 +113,7 @@ async def test_sensors(hass): assert state.attributes.get(ATTR_COUNTER) == 986 assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "92" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT entry = registry.async_get("sensor.hl_l2340dw_drum_remaining_life") assert entry @@ -126,7 +126,7 @@ async def test_sensors(hass): assert state.attributes.get(ATTR_COUNTER) == 1611 assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "92" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT entry = registry.async_get("sensor.hl_l2340dw_black_drum_remaining_life") assert entry @@ -139,7 +139,7 @@ async def test_sensors(hass): assert state.attributes.get(ATTR_COUNTER) == 1611 assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "92" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT entry = registry.async_get("sensor.hl_l2340dw_cyan_drum_remaining_life") assert entry @@ -152,7 +152,7 @@ async def test_sensors(hass): assert state.attributes.get(ATTR_COUNTER) == 1611 assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "92" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT entry = registry.async_get("sensor.hl_l2340dw_magenta_drum_remaining_life") assert entry @@ -165,7 +165,7 @@ async def test_sensors(hass): assert state.attributes.get(ATTR_COUNTER) == 1611 assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "92" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT entry = registry.async_get("sensor.hl_l2340dw_yellow_drum_remaining_life") assert entry @@ -176,7 +176,7 @@ async def test_sensors(hass): assert state.attributes.get(ATTR_ICON) == "mdi:water-outline" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "97" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT entry = registry.async_get("sensor.hl_l2340dw_fuser_remaining_life") assert entry @@ -187,7 +187,7 @@ async def test_sensors(hass): assert state.attributes.get(ATTR_ICON) == "mdi:current-ac" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "97" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT entry = registry.async_get("sensor.hl_l2340dw_belt_unit_remaining_life") assert entry @@ -198,7 +198,7 @@ async def test_sensors(hass): assert state.attributes.get(ATTR_ICON) == "mdi:printer-3d" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "98" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT entry = registry.async_get("sensor.hl_l2340dw_pf_kit_1_remaining_life") assert entry @@ -209,7 +209,7 @@ async def test_sensors(hass): assert state.attributes.get(ATTR_ICON) == "mdi:file-document-outline" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES assert state.state == "986" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT entry = registry.async_get("sensor.hl_l2340dw_page_counter") assert entry @@ -220,7 +220,7 @@ async def test_sensors(hass): assert state.attributes.get(ATTR_ICON) == "mdi:file-document-outline" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES assert state.state == "538" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT entry = registry.async_get("sensor.hl_l2340dw_duplex_unit_pages_counter") assert entry @@ -231,7 +231,7 @@ async def test_sensors(hass): assert state.attributes.get(ATTR_ICON) == "mdi:file-document-outline" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES assert state.state == "709" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT entry = registry.async_get("sensor.hl_l2340dw_b_w_counter") assert entry @@ -242,7 +242,7 @@ async def test_sensors(hass): assert state.attributes.get(ATTR_ICON) == "mdi:file-document-outline" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES assert state.state == "902" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT entry = registry.async_get("sensor.hl_l2340dw_color_counter") assert entry @@ -252,7 +252,7 @@ async def test_sensors(hass): assert state assert state.attributes.get(ATTR_ICON) is None assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TIMESTAMP + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP assert state.state == "2019-09-24T12:14:56+00:00" assert state.attributes.get(ATTR_STATE_CLASS) is None From 0409665907e5db5add7345a7b30e07e4754ca95a Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Thu, 16 Dec 2021 20:22:26 +0000 Subject: [PATCH 0599/2644] Use DeviceClass Enums in ambee tests (#62108) --- tests/components/ambee/test_sensor.py | 28 +++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/components/ambee/test_sensor.py b/tests/components/ambee/test_sensor.py index 32bf5938456..a32398139d0 100644 --- a/tests/components/ambee/test_sensor.py +++ b/tests/components/ambee/test_sensor.py @@ -7,7 +7,8 @@ from homeassistant.components.ambee.const import DEVICE_CLASS_AMBEE_RISK, DOMAIN from homeassistant.components.sensor import ( ATTR_STATE_CLASS, DOMAIN as SENSOR_DOMAIN, - STATE_CLASS_MEASUREMENT, + SensorDeviceClass, + SensorStateClass, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -18,7 +19,6 @@ from homeassistant.const import ( CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_MILLION, - DEVICE_CLASS_CO, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -42,7 +42,7 @@ async def test_air_quality( assert entry.unique_id == f"{entry_id}_air_quality_particulate_matter_2_5" assert state.state == "3.14" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Particulate Matter < 2.5 μm" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER @@ -57,7 +57,7 @@ async def test_air_quality( assert entry.unique_id == f"{entry_id}_air_quality_particulate_matter_10" assert state.state == "5.24" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Particulate Matter < 10 μm" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER @@ -72,7 +72,7 @@ async def test_air_quality( assert entry.unique_id == f"{entry_id}_air_quality_sulphur_dioxide" assert state.state == "0.031" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Sulphur Dioxide (SO2)" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_PARTS_PER_BILLION @@ -87,7 +87,7 @@ async def test_air_quality( assert entry.unique_id == f"{entry_id}_air_quality_nitrogen_dioxide" assert state.state == "0.66" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Nitrogen Dioxide (NO2)" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_PARTS_PER_BILLION @@ -102,7 +102,7 @@ async def test_air_quality( assert entry.unique_id == f"{entry_id}_air_quality_ozone" assert state.state == "17.067" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Ozone" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_PARTS_PER_BILLION @@ -116,9 +116,9 @@ async def test_air_quality( assert state assert entry.unique_id == f"{entry_id}_air_quality_carbon_monoxide" assert state.state == "0.105" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_CO + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.CO assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Carbon Monoxide (CO)" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_PARTS_PER_MILLION @@ -132,7 +132,7 @@ async def test_air_quality( assert entry.unique_id == f"{entry_id}_air_quality_air_quality_index" assert state.state == "13" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Air Quality Index (AQI)" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ATTR_DEVICE_CLASS not in state.attributes assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes assert ATTR_ICON not in state.attributes @@ -165,7 +165,7 @@ async def test_pollen( assert state.state == "190" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Grass Pollen" assert state.attributes.get(ATTR_ICON) == "mdi:grass" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_PARTS_PER_CUBIC_METER @@ -180,7 +180,7 @@ async def test_pollen( assert state.state == "127" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Tree Pollen" assert state.attributes.get(ATTR_ICON) == "mdi:tree" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_PARTS_PER_CUBIC_METER @@ -195,7 +195,7 @@ async def test_pollen( assert state.state == "95" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Weed Pollen" assert state.attributes.get(ATTR_ICON) == "mdi:sprout" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_PARTS_PER_CUBIC_METER @@ -337,7 +337,7 @@ async def test_pollen_enable_disable_by_defaults( assert state.state == value assert state.attributes.get(ATTR_FRIENDLY_NAME) == name assert state.attributes.get(ATTR_ICON) == icon - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_PARTS_PER_CUBIC_METER From 4de4cc7bd4ff2bed8f111b10d5434239340b0a2a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 16 Dec 2021 21:25:24 +0100 Subject: [PATCH 0600/2644] Small cleanup of Luftdaten constants (#61757) --- .../components/luftdaten/__init__.py | 20 ++++++------------- homeassistant/components/luftdaten/sensor.py | 19 +++--------------- 2 files changed, 9 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py index f525bcb69bc..e449507b194 100644 --- a/homeassistant/components/luftdaten/__init__.py +++ b/homeassistant/components/luftdaten/__init__.py @@ -32,55 +32,47 @@ _LOGGER = logging.getLogger(__name__) DATA_LUFTDATEN = "luftdaten" DATA_LUFTDATEN_CLIENT = "data_luftdaten_client" DATA_LUFTDATEN_LISTENER = "data_luftdaten_listener" -DEFAULT_ATTRIBUTION = "Data provided by luftdaten.info" PLATFORMS = [Platform.SENSOR] -SENSOR_HUMIDITY = "humidity" -SENSOR_PM10 = "P1" -SENSOR_PM2_5 = "P2" -SENSOR_PRESSURE = "pressure" -SENSOR_PRESSURE_AT_SEALEVEL = "pressure_at_sealevel" -SENSOR_TEMPERATURE = "temperature" - TOPIC_UPDATE = f"{DOMAIN}_data_update" SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( - key=SENSOR_TEMPERATURE, + key="temperature", name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( - key=SENSOR_HUMIDITY, + key="humidity", name="Humidity", icon="mdi:water-percent", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( - key=SENSOR_PRESSURE, + key="pressure", name="Pressure", icon="mdi:arrow-down-bold", native_unit_of_measurement=PRESSURE_PA, device_class=SensorDeviceClass.PRESSURE, ), SensorEntityDescription( - key=SENSOR_PRESSURE_AT_SEALEVEL, + key="pressure_at_sealevel", name="Pressure at sealevel", icon="mdi:download", native_unit_of_measurement=PRESSURE_PA, device_class=SensorDeviceClass.PRESSURE, ), SensorEntityDescription( - key=SENSOR_PM10, + key="P1", name="PM10", icon="mdi:thought-bubble", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, ), SensorEntityDescription( - key=SENSOR_PM2_5, + key="P2", name="PM2.5", icon="mdi:thought-bubble-outline", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, diff --git a/homeassistant/components/luftdaten/sensor.py b/homeassistant/components/luftdaten/sensor.py index aa4995490ca..5b02869c23d 100644 --- a/homeassistant/components/luftdaten/sensor.py +++ b/homeassistant/components/luftdaten/sensor.py @@ -1,22 +1,10 @@ """Support for Luftdaten sensors.""" from homeassistant.components.sensor import SensorEntity, SensorEntityDescription -from homeassistant.const import ( - ATTR_ATTRIBUTION, - ATTR_LATITUDE, - ATTR_LONGITUDE, - CONF_SHOW_ON_MAP, -) +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, CONF_SHOW_ON_MAP from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import ( - DATA_LUFTDATEN, - DATA_LUFTDATEN_CLIENT, - DEFAULT_ATTRIBUTION, - DOMAIN, - SENSOR_TYPES, - TOPIC_UPDATE, -) +from . import DATA_LUFTDATEN, DATA_LUFTDATEN_CLIENT, DOMAIN, SENSOR_TYPES, TOPIC_UPDATE from .const import ATTR_SENSOR_ID @@ -36,6 +24,7 @@ async def async_setup_entry(hass, entry, async_add_entities): class LuftdatenSensor(SensorEntity): """Implementation of a Luftdaten sensor.""" + _attr_attribution = "Data provided by luftdaten.info" _attr_should_poll = False def __init__(self, luftdaten, description: SensorEntityDescription, show): @@ -68,8 +57,6 @@ class LuftdatenSensor(SensorEntity): @property def extra_state_attributes(self): """Return the state attributes.""" - self._attrs[ATTR_ATTRIBUTION] = DEFAULT_ATTRIBUTION - if self._data is not None: try: self._attrs[ATTR_SENSOR_ID] = self._data["sensor_id"] From 4353b1e62ce81c20f080bd6a4b1837fbcdfab50e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 16 Dec 2021 21:31:37 +0100 Subject: [PATCH 0601/2644] Improve tests for template binary sensor (#62103) --- .../components/template/test_binary_sensor.py | 520 +++++++++++++----- 1 file changed, 372 insertions(+), 148 deletions(-) diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index 981ff63af50..0605759b16b 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -6,7 +6,7 @@ from unittest.mock import patch import pytest from homeassistant import setup -from homeassistant.components import binary_sensor +from homeassistant.components import binary_sensor, template from homeassistant.const import ( ATTR_DEVICE_CLASS, EVENT_HOMEASSISTANT_START, @@ -24,56 +24,150 @@ ON = "on" OFF = "off" -@pytest.mark.parametrize("count,domain", [(1, binary_sensor.DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( - "config", + "config,domain,entity_id,name,attributes", [ - { - "binary_sensor": { - "platform": "template", - "sensors": { - "test": { - "friendly_name": "virtual thingy", - "value_template": "{{ True }}", + ( + { + "binary_sensor": { + "platform": "template", + "sensors": { + "test": { + "value_template": "{{ True }}", + } + }, + }, + }, + binary_sensor.DOMAIN, + "binary_sensor.test", + "test", + {"friendly_name": "test"}, + ), + ( + { + "template": { + "binary_sensor": { + "state": "{{ True }}", + } + }, + }, + template.DOMAIN, + "binary_sensor.unnamed_device", + "unnamed device", + {}, + ), + ], +) +async def test_setup_minimal(hass, start_ha, entity_id, name, attributes): + """Test the setup.""" + state = hass.states.get(entity_id) + assert state is not None + assert state.name == name + assert state.state == ON + assert state.attributes == attributes + + +@pytest.mark.parametrize("count", [1]) +@pytest.mark.parametrize( + "config,domain,entity_id", + [ + ( + { + "binary_sensor": { + "platform": "template", + "sensors": { + "test": { + "friendly_name": "virtual thingy", + "value_template": "{{ True }}", + "device_class": "motion", + } + }, + }, + }, + binary_sensor.DOMAIN, + "binary_sensor.test", + ), + ( + { + "template": { + "binary_sensor": { + "name": "virtual thingy", + "state": "{{ True }}", "device_class": "motion", } }, }, - }, + template.DOMAIN, + "binary_sensor.virtual_thingy", + ), ], ) -async def test_setup_legacy(hass, start_ha): +async def test_setup(hass, start_ha, entity_id): """Test the setup.""" - state = hass.states.get("binary_sensor.test") + state = hass.states.get(entity_id) assert state is not None assert state.name == "virtual thingy" assert state.state == ON assert state.attributes["device_class"] == "motion" -@pytest.mark.parametrize("count,domain", [(0, binary_sensor.DOMAIN)]) +@pytest.mark.parametrize("count", [0]) @pytest.mark.parametrize( - "config", + "config,domain", [ - {"binary_sensor": {"platform": "template"}}, - {"binary_sensor": {"platform": "template", "sensors": {"foo bar": {}}}}, - { - "binary_sensor": { - "platform": "template", - "sensors": { - "test": { - "value_template": "{{ foo }}", + # No legacy binary sensors + ( + {"binary_sensor": {"platform": "template"}}, + binary_sensor.DOMAIN, + ), + # Legacy binary sensor missing mandatory config + ( + {"binary_sensor": {"platform": "template", "sensors": {"foo bar": {}}}}, + binary_sensor.DOMAIN, + ), + # Binary sensor missing mandatory config + ( + {"template": {"binary_sensor": {}}}, + template.DOMAIN, + ), + # Legacy binary sensor with invalid device class + ( + { + "binary_sensor": { + "platform": "template", + "sensors": { + "test": { + "value_template": "{{ foo }}", + "device_class": "foobarnotreal", + } + }, + } + }, + binary_sensor.DOMAIN, + ), + # Binary sensor with invalid device class + ( + { + "template": { + "binary_sensor": { + "state": "{{ foo }}", "device_class": "foobarnotreal", } - }, - } - }, - { - "binary_sensor": { - "platform": "template", - "sensors": {"test": {"device_class": "motion"}}, - } - }, + } + }, + template.DOMAIN, + ), + # Legacy binary sensor missing mandatory config + ( + { + "binary_sensor": { + "platform": "template", + "sensors": {"test": {"device_class": "motion"}}, + } + }, + binary_sensor.DOMAIN, + ), ], ) async def test_setup_invalid_sensors(hass, count, start_ha): @@ -81,17 +175,35 @@ async def test_setup_invalid_sensors(hass, count, start_ha): assert len(hass.states.async_entity_ids("binary_sensor")) == count -@pytest.mark.parametrize("count,domain", [(1, binary_sensor.DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( - "config", + "config,domain,entity_id", [ - { - "binary_sensor": { - "platform": "template", - "sensors": { - "test_template_sensor": { - "value_template": "{{ states.sensor.xyz.state }}", - "icon_template": "{% if " + ( + { + "binary_sensor": { + "platform": "template", + "sensors": { + "test_template_sensor": { + "value_template": "{{ states.sensor.xyz.state }}", + "icon_template": "{% if " + "states.binary_sensor.test_state.state == " + "'Works' %}" + "mdi:check" + "{% endif %}", + }, + }, + }, + }, + binary_sensor.DOMAIN, + "binary_sensor.test_template_sensor", + ), + ( + { + "template": { + "binary_sensor": { + "state": "{{ states.sensor.xyz.state }}", + "icon": "{% if " "states.binary_sensor.test_state.state == " "'Works' %}" "mdi:check" @@ -99,31 +211,51 @@ async def test_setup_invalid_sensors(hass, count, start_ha): }, }, }, - }, + template.DOMAIN, + "binary_sensor.unnamed_device", + ), ], ) -async def test_icon_template(hass, start_ha): +async def test_icon_template(hass, start_ha, entity_id): """Test icon template.""" - state = hass.states.get("binary_sensor.test_template_sensor") + state = hass.states.get(entity_id) assert state.attributes.get("icon") == "" hass.states.async_set("binary_sensor.test_state", "Works") await hass.async_block_till_done() - state = hass.states.get("binary_sensor.test_template_sensor") + state = hass.states.get(entity_id) assert state.attributes["icon"] == "mdi:check" -@pytest.mark.parametrize("count,domain", [(1, binary_sensor.DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( - "config", + "config,domain,entity_id", [ - { - "binary_sensor": { - "platform": "template", - "sensors": { - "test_template_sensor": { - "value_template": "{{ states.sensor.xyz.state }}", - "entity_picture_template": "{% if " + ( + { + "binary_sensor": { + "platform": "template", + "sensors": { + "test_template_sensor": { + "value_template": "{{ states.sensor.xyz.state }}", + "entity_picture_template": "{% if " + "states.binary_sensor.test_state.state == " + "'Works' %}" + "/local/sensor.png" + "{% endif %}", + }, + }, + }, + }, + binary_sensor.DOMAIN, + "binary_sensor.test_template_sensor", + ), + ( + { + "template": { + "binary_sensor": { + "state": "{{ states.sensor.xyz.state }}", + "picture": "{% if " "states.binary_sensor.test_state.state == " "'Works' %}" "/local/sensor.png" @@ -131,48 +263,68 @@ async def test_icon_template(hass, start_ha): }, }, }, - }, + template.DOMAIN, + "binary_sensor.unnamed_device", + ), ], ) -async def test_entity_picture_template(hass, start_ha): +async def test_entity_picture_template(hass, start_ha, entity_id): """Test entity_picture template.""" - state = hass.states.get("binary_sensor.test_template_sensor") + state = hass.states.get(entity_id) assert state.attributes.get("entity_picture") == "" hass.states.async_set("binary_sensor.test_state", "Works") await hass.async_block_till_done() - state = hass.states.get("binary_sensor.test_template_sensor") + state = hass.states.get(entity_id) assert state.attributes["entity_picture"] == "/local/sensor.png" -@pytest.mark.parametrize("count,domain", [(1, binary_sensor.DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( - "config", + "config,domain,entity_id", [ - { - "binary_sensor": { - "platform": "template", - "sensors": { - "test_template_sensor": { - "value_template": "{{ states.sensor.xyz.state }}", - "attribute_templates": { + ( + { + "binary_sensor": { + "platform": "template", + "sensors": { + "test_template_sensor": { + "value_template": "{{ states.sensor.xyz.state }}", + "attribute_templates": { + "test_attribute": "It {{ states.sensor.test_state.state }}." + }, + }, + }, + }, + }, + binary_sensor.DOMAIN, + "binary_sensor.test_template_sensor", + ), + ( + { + "template": { + "binary_sensor": { + "state": "{{ states.sensor.xyz.state }}", + "attributes": { "test_attribute": "It {{ states.sensor.test_state.state }}." }, }, }, }, - }, + template.DOMAIN, + "binary_sensor.unnamed_device", + ), ], ) -async def test_attribute_templates(hass, start_ha): +async def test_attribute_templates(hass, start_ha, entity_id): """Test attribute_templates template.""" - state = hass.states.get("binary_sensor.test_template_sensor") + state = hass.states.get(entity_id) assert state.attributes.get("test_attribute") == "It ." hass.states.async_set("sensor.test_state", "Works2") await hass.async_block_till_done() hass.states.async_set("sensor.test_state", "Works") await hass.async_block_till_done() - state = hass.states.get("binary_sensor.test_template_sensor") + state = hass.states.get(entity_id) assert state.attributes["test_attribute"] == "It Works." @@ -247,67 +399,102 @@ async def test_event(hass, start_ha): assert state.state == ON -@pytest.mark.parametrize("count,domain", [(1, binary_sensor.DOMAIN)]) @pytest.mark.parametrize( - "config", + "config,count,domain", [ - { - "binary_sensor": { - "platform": "template", - "sensors": { - "test_on": { - "friendly_name": "virtual thingy", - "value_template": "{{ states.sensor.test_state.state == 'on' }}", - "device_class": "motion", - "delay_on": 5, - }, - "test_off": { - "friendly_name": "virtual thingy", - "value_template": "{{ states.sensor.test_state.state == 'on' }}", - "device_class": "motion", - "delay_off": 5, + ( + { + "binary_sensor": { + "platform": "template", + "sensors": { + "test_on": { + "friendly_name": "virtual thingy", + "value_template": "{{ states.sensor.test_state.state == 'on' }}", + "device_class": "motion", + "delay_on": 5, + }, + "test_off": { + "friendly_name": "virtual thingy", + "value_template": "{{ states.sensor.test_state.state == 'on' }}", + "device_class": "motion", + "delay_off": 5, + }, }, }, }, - }, - { - "binary_sensor": { - "platform": "template", - "sensors": { - "test_on": { - "friendly_name": "virtual thingy", - "value_template": "{{ states.sensor.test_state.state == 'on' }}", - "device_class": "motion", - "delay_on": '{{ ({ "seconds": 10 / 2 }) }}', + 1, + binary_sensor.DOMAIN, + ), + ( + { + "template": [ + { + "binary_sensor": { + "name": "test on", + "state": "{{ states.sensor.test_state.state == 'on' }}", + "device_class": "motion", + "delay_on": 5, + }, }, - "test_off": { - "friendly_name": "virtual thingy", - "value_template": "{{ states.sensor.test_state.state == 'on' }}", - "device_class": "motion", - "delay_off": '{{ ({ "seconds": 10 / 2 }) }}', + { + "binary_sensor": { + "name": "test off", + "state": "{{ states.sensor.test_state.state == 'on' }}", + "device_class": "motion", + "delay_off": 5, + }, + }, + ] + }, + 2, + template.DOMAIN, + ), + ( + { + "binary_sensor": { + "platform": "template", + "sensors": { + "test_on": { + "friendly_name": "virtual thingy", + "value_template": "{{ states.sensor.test_state.state == 'on' }}", + "device_class": "motion", + "delay_on": '{{ ({ "seconds": 10 / 2 }) }}', + }, + "test_off": { + "friendly_name": "virtual thingy", + "value_template": "{{ states.sensor.test_state.state == 'on' }}", + "device_class": "motion", + "delay_off": '{{ ({ "seconds": 10 / 2 }) }}', + }, }, }, }, - }, - { - "binary_sensor": { - "platform": "template", - "sensors": { - "test_on": { - "friendly_name": "virtual thingy", - "value_template": "{{ states.sensor.test_state.state == 'on' }}", - "device_class": "motion", - "delay_on": '{{ ({ "seconds": states("input_number.delay")|int }) }}', - }, - "test_off": { - "friendly_name": "virtual thingy", - "value_template": "{{ states.sensor.test_state.state == 'on' }}", - "device_class": "motion", - "delay_off": '{{ ({ "seconds": states("input_number.delay")|int }) }}', + 1, + binary_sensor.DOMAIN, + ), + ( + { + "binary_sensor": { + "platform": "template", + "sensors": { + "test_on": { + "friendly_name": "virtual thingy", + "value_template": "{{ states.sensor.test_state.state == 'on' }}", + "device_class": "motion", + "delay_on": '{{ ({ "seconds": states("input_number.delay")|int }) }}', + }, + "test_off": { + "friendly_name": "virtual thingy", + "value_template": "{{ states.sensor.test_state.state == 'on' }}", + "device_class": "motion", + "delay_off": '{{ ({ "seconds": states("input_number.delay")|int }) }}', + }, }, }, }, - }, + 1, + binary_sensor.DOMAIN, + ), ], ) async def test_template_delay_on_off(hass, start_ha): @@ -349,64 +536,101 @@ async def test_template_delay_on_off(hass, start_ha): assert hass.states.get("binary_sensor.test_off").state == OFF -@pytest.mark.parametrize("count,domain", [(1, binary_sensor.DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( - "config", + "config,domain,entity_id", [ - { - "binary_sensor": { - "platform": "template", - "sensors": { - "test": { - "friendly_name": "virtual thingy", - "value_template": "true", + ( + { + "binary_sensor": { + "platform": "template", + "sensors": { + "test": { + "friendly_name": "virtual thingy", + "value_template": "true", + "device_class": "motion", + "delay_off": 5, + }, + }, + }, + }, + binary_sensor.DOMAIN, + "binary_sensor.test", + ), + ( + { + "template": { + "binary_sensor": { + "name": "virtual thingy", + "state": "true", "device_class": "motion", "delay_off": 5, }, }, }, - }, + template.DOMAIN, + "binary_sensor.virtual_thingy", + ), ], ) -async def test_available_without_availability_template(hass, start_ha): +async def test_available_without_availability_template(hass, start_ha, entity_id): """Ensure availability is true without an availability_template.""" - state = hass.states.get("binary_sensor.test") + state = hass.states.get(entity_id) assert state.state != STATE_UNAVAILABLE assert state.attributes[ATTR_DEVICE_CLASS] == "motion" -@pytest.mark.parametrize("count,domain", [(1, binary_sensor.DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( - "config", + "config,domain,entity_id", [ - { - "binary_sensor": { - "platform": "template", - "sensors": { - "test": { - "friendly_name": "virtual thingy", - "value_template": "true", - "device_class": "motion", - "delay_off": 5, - "availability_template": "{{ is_state('sensor.test_state','on') }}", + ( + { + "binary_sensor": { + "platform": "template", + "sensors": { + "test": { + "friendly_name": "virtual thingy", + "value_template": "true", + "device_class": "motion", + "delay_off": 5, + "availability_template": "{{ is_state('sensor.test_state','on') }}", + }, }, }, }, - }, + binary_sensor.DOMAIN, + "binary_sensor.test", + ), + ( + { + "template": { + "binary_sensor": { + "name": "virtual thingy", + "state": "true", + "device_class": "motion", + "delay_off": 5, + "availability": "{{ is_state('sensor.test_state','on') }}", + }, + }, + }, + template.DOMAIN, + "binary_sensor.virtual_thingy", + ), ], ) -async def test_availability_template(hass, start_ha): +async def test_availability_template(hass, start_ha, entity_id): """Test availability template.""" hass.states.async_set("sensor.test_state", STATE_OFF) await hass.async_block_till_done() - assert hass.states.get("binary_sensor.test").state == STATE_UNAVAILABLE + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE hass.states.async_set("sensor.test_state", STATE_ON) await hass.async_block_till_done() - state = hass.states.get("binary_sensor.test") + state = hass.states.get(entity_id) assert state.state != STATE_UNAVAILABLE assert state.attributes[ATTR_DEVICE_CLASS] == "motion" From 7a1b05d166c65d8662a029f730ae16d41bbd312a Mon Sep 17 00:00:00 2001 From: Maximilian <43999966+DeerMaximum@users.noreply.github.com> Date: Thu, 16 Dec 2021 20:39:49 +0000 Subject: [PATCH 0602/2644] Add missing timezone information (#62106) --- homeassistant/components/vallox/sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/vallox/sensor.py b/homeassistant/components/vallox/sensor.py index 69d6f7bf003..2fa83f84917 100644 --- a/homeassistant/components/vallox/sensor.py +++ b/homeassistant/components/vallox/sensor.py @@ -19,6 +19,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.util import dt as dt_util from . import ValloxDataUpdateCoordinator from .const import ( @@ -104,7 +105,7 @@ class ValloxFilterRemainingSensor(ValloxSensor): days_remaining_delta = timedelta(days=days_remaining) now = datetime.utcnow().replace(hour=13, minute=0, second=0, microsecond=0) - return now + days_remaining_delta + return (now + days_remaining_delta).astimezone(dt_util.UTC) class ValloxCellStateSensor(ValloxSensor): From 329d90b56887bc136b662dcfc254c72d78868cf1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 16 Dec 2021 21:42:05 +0100 Subject: [PATCH 0603/2644] Use new enums in nzbget (#61946) --- homeassistant/components/nzbget/sensor.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py index 9f94d458f42..42cacfc8ab5 100644 --- a/homeassistant/components/nzbget/sensor.py +++ b/homeassistant/components/nzbget/sensor.py @@ -4,13 +4,16 @@ from __future__ import annotations from datetime import timedelta import logging -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_NAME, DATA_MEGABYTES, DATA_RATE_MEGABYTES_PER_SECOND, - DEVICE_CLASS_TIMESTAMP, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -69,7 +72,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="UpTimeSec", name="Uptime", - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, ), ) From 373790cb1568833233b237ee225b9d72e20a778a Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Thu, 16 Dec 2021 20:46:01 +0000 Subject: [PATCH 0604/2644] Use DeviceClass Enums in deconz tests (#62114) --- tests/components/deconz/test_binary_sensor.py | 29 +++++++++------- tests/components/deconz/test_sensor.py | 34 +++++++++---------- 2 files changed, 33 insertions(+), 30 deletions(-) diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index 036e66f6e46..32c7f1c3eb9 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -2,11 +2,7 @@ from unittest.mock import patch -from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_MOTION, - DEVICE_CLASS_TAMPER, - DEVICE_CLASS_VIBRATION, -) +from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.deconz.const import ( CONF_ALLOW_CLIP_SENSOR, CONF_ALLOW_NEW_DEVICES, @@ -14,15 +10,15 @@ from homeassistant.components.deconz.const import ( DOMAIN as DECONZ_DOMAIN, ) from homeassistant.components.deconz.services import SERVICE_DEVICE_REFRESH +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( ATTR_DEVICE_CLASS, - DEVICE_CLASS_TEMPERATURE, - ENTITY_CATEGORY_DIAGNOSTIC, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, ) from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_registry import async_entries_for_config_entry from .test_gateway import ( @@ -83,18 +79,23 @@ async def test_binary_sensors(hass, aioclient_mock, mock_deconz_websocket): assert len(hass.states.async_all()) == 5 presence_sensor = hass.states.get("binary_sensor.presence_sensor") assert presence_sensor.state == STATE_OFF - assert presence_sensor.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_MOTION + assert ( + presence_sensor.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.MOTION + ) presence_temp = hass.states.get("sensor.presence_sensor_temperature") assert presence_temp.state == "0.1" - assert presence_temp.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_TEMPERATURE + assert presence_temp.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE assert hass.states.get("binary_sensor.temperature_sensor") is None assert hass.states.get("binary_sensor.clip_presence_sensor") is None vibration_sensor = hass.states.get("binary_sensor.vibration_sensor") assert vibration_sensor.state == STATE_ON - assert vibration_sensor.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_VIBRATION + assert ( + vibration_sensor.attributes[ATTR_DEVICE_CLASS] + == BinarySensorDeviceClass.VIBRATION + ) vibration_temp = hass.states.get("sensor.vibration_sensor_temperature") assert vibration_temp.state == "0.1" - assert vibration_temp.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_TEMPERATURE + assert vibration_temp.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE event_changed_sensor = { "t": "event", @@ -139,7 +140,9 @@ async def test_tampering_sensor(hass, aioclient_mock, mock_deconz_websocket): assert len(hass.states.async_all()) == 3 presence_tamper = hass.states.get("binary_sensor.presence_sensor_tampered") assert presence_tamper.state == STATE_OFF - assert presence_tamper.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_TAMPER + assert ( + presence_tamper.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.TAMPER + ) event_changed_sensor = { "t": "event", @@ -154,7 +157,7 @@ async def test_tampering_sensor(hass, aioclient_mock, mock_deconz_websocket): assert hass.states.get("binary_sensor.presence_sensor_tampered").state == STATE_ON assert ( ent_reg.async_get("binary_sensor.presence_sensor_tampered").entity_category - == ENTITY_CATEGORY_DIAGNOSTIC + == EntityCategory.DIAGNOSTIC ) await hass.config_entries.async_unload(config_entry.entry_id) diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index 5f9762c339b..d188d5504ca 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -5,19 +5,11 @@ from unittest.mock import patch from homeassistant.components.deconz.const import CONF_ALLOW_CLIP_SENSOR from homeassistant.components.deconz.sensor import ATTR_DAYLIGHT -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY -from homeassistant.const import ( - ATTR_DEVICE_CLASS, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_POWER, - DEVICE_CLASS_TEMPERATURE, - ENTITY_CATEGORY_DIAGNOSTIC, - STATE_UNAVAILABLE, -) +from homeassistant.const import ATTR_DEVICE_CLASS, STATE_UNAVAILABLE from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity import EntityCategory from homeassistant.util import dt from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration @@ -97,12 +89,17 @@ async def test_sensors(hass, aioclient_mock, mock_deconz_websocket): light_level_sensor = hass.states.get("sensor.light_level_sensor") assert light_level_sensor.state == "999.8" - assert light_level_sensor.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_ILLUMINANCE + assert ( + light_level_sensor.attributes[ATTR_DEVICE_CLASS] + == SensorDeviceClass.ILLUMINANCE + ) assert light_level_sensor.attributes[ATTR_DAYLIGHT] == 6955 light_level_temp = hass.states.get("sensor.light_level_sensor_temperature") assert light_level_temp.state == "0.1" - assert light_level_temp.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_TEMPERATURE + assert ( + light_level_temp.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE + ) assert not hass.states.get("sensor.presence_sensor") assert not hass.states.get("sensor.switch_1") @@ -111,21 +108,24 @@ async def test_sensors(hass, aioclient_mock, mock_deconz_websocket): switch_2_battery_level = hass.states.get("sensor.switch_2_battery_level") assert switch_2_battery_level.state == "100" - assert switch_2_battery_level.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_BATTERY + assert ( + switch_2_battery_level.attributes[ATTR_DEVICE_CLASS] + == SensorDeviceClass.BATTERY + ) assert ( ent_reg.async_get("sensor.switch_2_battery_level").entity_category - == ENTITY_CATEGORY_DIAGNOSTIC + == EntityCategory.DIAGNOSTIC ) assert not hass.states.get("sensor.daylight_sensor") power_sensor = hass.states.get("sensor.power_sensor") assert power_sensor.state == "6" - assert power_sensor.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_POWER + assert power_sensor.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.POWER consumption_sensor = hass.states.get("sensor.consumption_sensor") assert consumption_sensor.state == "0.002" - assert consumption_sensor.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_ENERGY + assert consumption_sensor.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENERGY assert not hass.states.get("sensor.clip_light_level_sensor") From cabd6375d19ee017e79d25dfc20d5b17aa934097 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Thu, 16 Dec 2021 20:47:59 +0000 Subject: [PATCH 0605/2644] Use DeviceClass Enums in canary tests (#62113) --- tests/components/canary/test_sensor.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/tests/components/canary/test_sensor.py b/tests/components/canary/test_sensor.py index 530ca1cccc1..0c95152338c 100644 --- a/tests/components/canary/test_sensor.py +++ b/tests/components/canary/test_sensor.py @@ -9,12 +9,9 @@ from homeassistant.components.canary.sensor import ( STATE_AIR_QUALITY_NORMAL, STATE_AIR_QUALITY_VERY_ABNORMAL, ) +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_SIGNAL_STRENGTH, - DEVICE_CLASS_TEMPERATURE, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, TEMP_CELSIUS, @@ -56,14 +53,14 @@ async def test_sensors_pro(hass, canary) -> None: "20_temperature", "21.12", TEMP_CELSIUS, - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, None, ), "home_dining_room_humidity": ( "20_humidity", "50.46", PERCENTAGE, - DEVICE_CLASS_HUMIDITY, + SensorDeviceClass.HUMIDITY, None, ), "home_dining_room_air_quality": ( @@ -182,14 +179,14 @@ async def test_sensors_flex(hass, canary) -> None: "20_battery", "70.46", PERCENTAGE, - DEVICE_CLASS_BATTERY, + SensorDeviceClass.BATTERY, None, ), "home_dining_room_wifi": ( "20_wifi", "-57.0", SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - DEVICE_CLASS_SIGNAL_STRENGTH, + SensorDeviceClass.SIGNAL_STRENGTH, None, ), } From 3b3ab2c19ccefbb07635c4a78fcae66378c44058 Mon Sep 17 00:00:00 2001 From: rianadon Date: Thu, 16 Dec 2021 13:01:32 -0800 Subject: [PATCH 0606/2644] Handle None values in weather entity forecast (#61467) --- homeassistant/components/weather/__init__.py | 22 ++++++++++--------- tests/components/weather/test_init.py | 23 ++++++++++++++++++++ 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index b9fa7e2ae39..0a562a40f64 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -262,29 +262,31 @@ class WeatherEntity(Entity): self.temperature_unit, self.precision, ) - if ATTR_FORECAST_PRESSURE in forecast_entry: + if ( + native_pressure := forecast_entry.get(ATTR_FORECAST_PRESSURE) + ) is not None: if (unit := self.pressure_unit) is not None: pressure = round( - self.hass.config.units.pressure( - forecast_entry[ATTR_FORECAST_PRESSURE], unit - ), + self.hass.config.units.pressure(native_pressure, unit), ROUNDING_PRECISION, ) forecast_entry[ATTR_FORECAST_PRESSURE] = pressure - if ATTR_FORECAST_WIND_SPEED in forecast_entry: + if ( + native_wind_speed := forecast_entry.get(ATTR_FORECAST_WIND_SPEED) + ) is not None: if (unit := self.wind_speed_unit) is not None: wind_speed = round( - self.hass.config.units.wind_speed( - forecast_entry[ATTR_FORECAST_WIND_SPEED], unit - ), + self.hass.config.units.wind_speed(native_wind_speed, unit), ROUNDING_PRECISION, ) forecast_entry[ATTR_FORECAST_WIND_SPEED] = wind_speed - if ATTR_FORECAST_PRECIPITATION in forecast_entry: + if ( + native_precip := forecast_entry.get(ATTR_FORECAST_PRECIPITATION) + ) is not None: if (unit := self.precipitation_unit) is not None: precipitation = round( self.hass.config.units.accumulated_precipitation( - forecast_entry[ATTR_FORECAST_PRECIPITATION], unit + native_precip, unit ), ROUNDING_PRECISION, ) diff --git a/tests/components/weather/test_init.py b/tests/components/weather/test_init.py index 4125e94749a..9849a6abe18 100644 --- a/tests/components/weather/test_init.py +++ b/tests/components/weather/test_init.py @@ -168,3 +168,26 @@ async def test_precipitation_conversion( native_value, native_unit, unit_system.accumulated_precipitation_unit ) assert float(forecast[ATTR_FORECAST_PRECIPITATION]) == approx(expected, rel=1e-2) + + +async def test_none_forecast( + hass, + enable_custom_integrations, +): + """Test that conversion with None values succeeds.""" + entity0 = await create_entity( + hass, + pressure=None, + pressure_unit=PRESSURE_INHG, + wind_speed=None, + wind_speed_unit=SPEED_METERS_PER_SECOND, + precipitation=None, + precipitation_unit=LENGTH_MILLIMETERS, + ) + + state = hass.states.get(entity0.entity_id) + forecast = state.attributes[ATTR_FORECAST][0] + + assert forecast[ATTR_FORECAST_PRESSURE] is None + assert forecast[ATTR_FORECAST_WIND_SPEED] is None + assert forecast[ATTR_FORECAST_PRECIPITATION] is None From 9e4f7205912b6851302ee8f5c668539c6648fb89 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Thu, 16 Dec 2021 21:07:52 +0000 Subject: [PATCH 0607/2644] Use DeviceClass Enums in elgato tests (#62121) --- tests/components/elgato/test_button.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/tests/components/elgato/test_button.py b/tests/components/elgato/test_button.py index 21211596d0c..80a906e114f 100644 --- a/tests/components/elgato/test_button.py +++ b/tests/components/elgato/test_button.py @@ -5,14 +5,10 @@ from elgato import ElgatoError import pytest from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS -from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_ICON, - ENTITY_CATEGORY_CONFIG, - STATE_UNKNOWN, -) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_ICON, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity import EntityCategory from tests.components.elgato import init_integration from tests.test_util.aiohttp import AiohttpClientMocker @@ -35,7 +31,7 @@ async def test_button_identify( entry = entity_registry.async_get("button.identify") assert entry assert entry.unique_id == "CN11A1A00001_identify" - assert entry.entity_category == ENTITY_CATEGORY_CONFIG + assert entry.entity_category == EntityCategory.CONFIG with patch( "homeassistant.components.elgato.light.Elgato.identify" From 773ac289dd7b1903e032edb4f028bf439dc3d7ec Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 16 Dec 2021 22:08:13 +0100 Subject: [PATCH 0608/2644] Tweak core add_job and async_add_job docstrings (#62112) --- homeassistant/core.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 2f5783de443..cb2a132307d 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -323,7 +323,10 @@ class HomeAssistant: _async_create_timer(self) def add_job(self, target: Callable[..., Any], *args: Any) -> None: - """Add job to the executor pool. + """Add a job to be executed by the event loop or by an executor. + + If the job is either a coroutine or decorated with @callback, it will be + run by the event loop, if not it will be run by an executor. target: target to call. args: parameters for method to call. @@ -336,7 +339,10 @@ class HomeAssistant: def async_add_job( self, target: Callable[..., Any], *args: Any ) -> asyncio.Future | None: - """Add a job from within the event loop. + """Add a job to be executed by the event loop or by an executor. + + If the job is either a coroutine or decorated with @callback, it will be + run by the event loop, if not it will be run by an executor. This method must be run in the event loop. From dab1a786a5151af567b4948244e2f7af563b03d6 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Thu, 16 Dec 2021 21:11:08 +0000 Subject: [PATCH 0609/2644] Use DeviceClass Enums in arlo tests (#62095) --- tests/components/arlo/test_sensor.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/tests/components/arlo/test_sensor.py b/tests/components/arlo/test_sensor.py index 4d6e8f6f228..1948eca14a4 100644 --- a/tests/components/arlo/test_sensor.py +++ b/tests/components/arlo/test_sensor.py @@ -6,12 +6,8 @@ import pytest from homeassistant.components.arlo import DATA_ARLO, sensor as arlo from homeassistant.components.arlo.sensor import SENSOR_TYPES -from homeassistant.const import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_TEMPERATURE, - PERCENTAGE, -) +from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.const import PERCENTAGE def _get_named_tuple(input_dict): @@ -157,12 +153,12 @@ def test_sensor_state_default(default_sensor): def test_sensor_device_class__battery(battery_sensor): """Test the battery device_class.""" - assert battery_sensor.device_class == DEVICE_CLASS_BATTERY + assert battery_sensor.device_class == SensorDeviceClass.BATTERY def test_sensor_device_class(temperature_sensor): """Test the device_class property.""" - assert temperature_sensor.device_class == DEVICE_CLASS_TEMPERATURE + assert temperature_sensor.device_class == SensorDeviceClass.TEMPERATURE def test_unit_of_measure(default_sensor, battery_sensor): @@ -174,8 +170,8 @@ def test_unit_of_measure(default_sensor, battery_sensor): def test_device_class(default_sensor, temperature_sensor, humidity_sensor): """Test the device_class property.""" assert default_sensor.device_class is None - assert temperature_sensor.device_class == DEVICE_CLASS_TEMPERATURE - assert humidity_sensor.device_class == DEVICE_CLASS_HUMIDITY + assert temperature_sensor.device_class == SensorDeviceClass.TEMPERATURE + assert humidity_sensor.device_class == SensorDeviceClass.HUMIDITY def test_attribution(default_sensor, temperature_sensor, humidity_sensor): From 06f670272fb266d91a7c9b36b17eac85a2194c69 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 16:18:28 -0500 Subject: [PATCH 0610/2644] Use enums in obihai (#62078) --- homeassistant/components/obihai/sensor.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/obihai/sensor.py b/homeassistant/components/obihai/sensor.py index 7ba28ee0741..f54611283a6 100644 --- a/homeassistant/components/obihai/sensor.py +++ b/homeassistant/components/obihai/sensor.py @@ -5,13 +5,12 @@ import logging from pyobihai import PyObihai import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import ( - CONF_HOST, - CONF_PASSWORD, - CONF_USERNAME, - DEVICE_CLASS_TIMESTAMP, +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorDeviceClass, + SensorEntity, ) +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -105,7 +104,7 @@ class ObihaiServiceSensors(SensorEntity): def device_class(self): """Return the device class for uptime sensor.""" if self._service_name == "Last Reboot": - return DEVICE_CLASS_TIMESTAMP + return SensorDeviceClass.TIMESTAMP return None @property From b30dd6857f4592468b490fa26ec00b848d6a6038 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 16:21:21 -0500 Subject: [PATCH 0611/2644] Use enums in opengarage (#62083) --- homeassistant/components/opengarage/cover.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/opengarage/cover.py b/homeassistant/components/opengarage/cover.py index 8737a0499fb..204ed093a9d 100644 --- a/homeassistant/components/opengarage/cover.py +++ b/homeassistant/components/opengarage/cover.py @@ -5,10 +5,10 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components.cover import ( - DEVICE_CLASS_GARAGE, PLATFORM_SCHEMA, SUPPORT_CLOSE, SUPPORT_OPEN, + CoverDeviceClass, CoverEntity, ) from homeassistant.const import ( @@ -76,7 +76,7 @@ async def async_setup_entry(hass, entry, async_add_entities): class OpenGarageCover(OpenGarageEntity, CoverEntity): """Representation of a OpenGarage cover.""" - _attr_device_class = DEVICE_CLASS_GARAGE + _attr_device_class = CoverDeviceClass.GARAGE _attr_supported_features = SUPPORT_OPEN | SUPPORT_CLOSE def __init__(self, open_garage_data_coordinator, device_id): From 438fd79d235a908b757d15b15a65d49b48c1e604 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 16:24:06 -0500 Subject: [PATCH 0612/2644] Use enums in p1_monitor (#62061) --- homeassistant/components/p1_monitor/sensor.py | 95 +++++++++---------- 1 file changed, 45 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/p1_monitor/sensor.py b/homeassistant/components/p1_monitor/sensor.py index 1b0eedc7554..edc076382ec 100644 --- a/homeassistant/components/p1_monitor/sensor.py +++ b/homeassistant/components/p1_monitor/sensor.py @@ -5,20 +5,15 @@ from typing import Literal from homeassistant.components.sensor import ( DOMAIN as SENSOR_DOMAIN, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_HOST, CURRENCY_EURO, - DEVICE_CLASS_CURRENT, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_GAS, - DEVICE_CLASS_POWER, - DEVICE_CLASS_VOLTAGE, ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, @@ -50,50 +45,50 @@ SENSORS: dict[ name="Gas Consumption", entity_registry_enabled_default=False, native_unit_of_measurement=VOLUME_CUBIC_METERS, - device_class=DEVICE_CLASS_GAS, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.GAS, + state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="power_consumption", name="Power Consumption", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="energy_consumption_high", name="Energy Consumption - High Tariff", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="energy_consumption_low", name="Energy Consumption - Low Tariff", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="power_production", name="Power Production", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="energy_production_high", name="Energy Production - High Tariff", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="energy_production_low", name="Energy Production - Low Tariff", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="energy_tariff_period", @@ -106,85 +101,85 @@ SENSORS: dict[ key="voltage_phase_l1", name="Voltage Phase L1", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="voltage_phase_l2", name="Voltage Phase L2", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="voltage_phase_l3", name="Voltage Phase L3", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="current_phase_l1", name="Current Phase L1", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="current_phase_l2", name="Current Phase L2", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="current_phase_l3", name="Current Phase L3", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="power_consumed_phase_l1", name="Power Consumed Phase L1", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="power_consumed_phase_l2", name="Power Consumed Phase L2", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="power_consumed_phase_l3", name="Power Consumed Phase L3", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="power_produced_phase_l1", name="Power Produced Phase L1", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="power_produced_phase_l2", name="Power Produced Phase L2", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="power_produced_phase_l3", name="Power Produced Phase L3", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), ), SERVICE_SETTINGS: ( @@ -192,31 +187,31 @@ SENSORS: dict[ key="gas_consumption_price", name="Gas Consumption Price", entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=f"{CURRENCY_EURO}/{VOLUME_CUBIC_METERS}", ), SensorEntityDescription( key="energy_consumption_price_low", name="Energy Consumption Price - Low", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}", ), SensorEntityDescription( key="energy_consumption_price_high", name="Energy Consumption Price - High", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}", ), SensorEntityDescription( key="energy_production_price_low", name="Energy Production Price - Low", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}", ), SensorEntityDescription( key="energy_production_price_high", name="Energy Production Price - High", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}", ), ), From 5a268419f5b94c35766644624e6e9eed2343f259 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 16:25:04 -0500 Subject: [PATCH 0613/2644] Use enums in pvoutput (#62073) --- homeassistant/components/pvoutput/sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/pvoutput/sensor.py b/homeassistant/components/pvoutput/sensor.py index 1d8b3400d8b..4f745666020 100644 --- a/homeassistant/components/pvoutput/sensor.py +++ b/homeassistant/components/pvoutput/sensor.py @@ -9,10 +9,10 @@ import voluptuous as vol from homeassistant.components.rest.data import RestData from homeassistant.components.sensor import ( - DEVICE_CLASS_ENERGY, PLATFORM_SCHEMA, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, + SensorStateClass, ) from homeassistant.const import ( ATTR_DATE, @@ -74,8 +74,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class PvoutputSensor(SensorEntity): """Representation of a PVOutput sensor.""" - _attr_state_class = STATE_CLASS_TOTAL_INCREASING - _attr_device_class = DEVICE_CLASS_ENERGY + _attr_state_class = SensorStateClass.TOTAL_INCREASING + _attr_device_class = SensorDeviceClass.ENERGY _attr_native_unit_of_measurement = ENERGY_WATT_HOUR def __init__(self, rest, name): From 2ab13f3603369121900f8cefd8f5a71f6bb7d29c Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 16:25:49 -0500 Subject: [PATCH 0614/2644] Use enums in openweathermap (#62086) --- .../components/openweathermap/const.py | 47 +++++++++---------- .../components/openweathermap/sensor.py | 7 ++- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index 08568b02a17..b623ed86c3a 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -2,8 +2,9 @@ from __future__ import annotations from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, + SensorDeviceClass, SensorEntityDescription, + SensorStateClass, ) from homeassistant.components.weather import ( ATTR_CONDITION_CLOUDY, @@ -30,10 +31,6 @@ from homeassistant.components.weather import ( ) from homeassistant.const import ( DEGREE, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_TIMESTAMP, LENGTH_MILLIMETERS, PERCENTAGE, PRESSURE_HPA, @@ -175,66 +172,66 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key=ATTR_API_DEW_POINT, name="Dew Point", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_TEMPERATURE, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_FEELS_LIKE_TEMPERATURE, name="Feels like temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_WIND_SPEED, name="Wind speed", native_unit_of_measurement=SPEED_METERS_PER_SECOND, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_WIND_BEARING, name="Wind bearing", native_unit_of_measurement=DEGREE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_HUMIDITY, name="Humidity", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_PRESSURE, name="Pressure", native_unit_of_measurement=PRESSURE_HPA, - device_class=DEVICE_CLASS_PRESSURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_CLOUDS, name="Cloud coverage", native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_RAIN, name="Rain", native_unit_of_measurement=LENGTH_MILLIMETERS, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_SNOW, name="Snow", native_unit_of_measurement=LENGTH_MILLIMETERS, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_PRECIPITATION_KIND, @@ -244,7 +241,7 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key=ATTR_API_UV_INDEX, name="UV Index", native_unit_of_measurement=UV_INDEX, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_CONDITION, @@ -274,24 +271,24 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key=ATTR_FORECAST_PRESSURE, name="Pressure", native_unit_of_measurement=PRESSURE_HPA, - device_class=DEVICE_CLASS_PRESSURE, + device_class=SensorDeviceClass.PRESSURE, ), SensorEntityDescription( key=ATTR_FORECAST_TEMP, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( key=ATTR_FORECAST_TEMP_LOW, name="Temperature Low", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( key=ATTR_FORECAST_TIME, name="Time", - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, ), SensorEntityDescription( key=ATTR_API_WIND_BEARING, diff --git a/homeassistant/components/openweathermap/sensor.py b/homeassistant/components/openweathermap/sensor.py index fd18ef32725..4a34069e036 100644 --- a/homeassistant/components/openweathermap/sensor.py +++ b/homeassistant/components/openweathermap/sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import datetime from homeassistant.components.sensor import ( - DEVICE_CLASS_TIMESTAMP, + SensorDeviceClass, SensorEntity, SensorEntityDescription, ) @@ -157,7 +157,10 @@ class OpenWeatherMapForecastSensor(AbstractOpenWeatherMapSensor): return None value = forecasts[0].get(self.entity_description.key, None) - if value and self.entity_description.device_class == DEVICE_CLASS_TIMESTAMP: + if ( + value + and self.entity_description.device_class is SensorDeviceClass.TIMESTAMP + ): return dt_util.parse_datetime(value) return value From 10e5780fbb78d83473e5bd275da76b79e5f9a8c6 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 16:26:49 -0500 Subject: [PATCH 0615/2644] Use enums in Omnilogic (#62080) --- homeassistant/components/omnilogic/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/omnilogic/sensor.py b/homeassistant/components/omnilogic/sensor.py index f0382c01342..d147b942a88 100644 --- a/homeassistant/components/omnilogic/sensor.py +++ b/homeassistant/components/omnilogic/sensor.py @@ -1,5 +1,5 @@ """Definition and setup of the Omnilogic Sensors for Home Assistant.""" -from homeassistant.components.sensor import DEVICE_CLASS_TEMPERATURE, SensorEntity +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, ELECTRIC_POTENTIAL_MILLIVOLT, @@ -249,7 +249,7 @@ SENSOR_TYPES = { "entity_classes": {"airTemp": OmniLogicTemperatureSensor}, "name": "Air Temperature", "kind": "air_temperature", - "device_class": DEVICE_CLASS_TEMPERATURE, + "device_class": SensorDeviceClass.TEMPERATURE, "icon": None, "unit": TEMP_FAHRENHEIT, "guard_condition": [{}], @@ -260,7 +260,7 @@ SENSOR_TYPES = { "entity_classes": {"waterTemp": OmniLogicTemperatureSensor}, "name": "Water Temperature", "kind": "water_temperature", - "device_class": DEVICE_CLASS_TEMPERATURE, + "device_class": SensorDeviceClass.TEMPERATURE, "icon": None, "unit": TEMP_FAHRENHEIT, "guard_condition": [{}], From 74a9f8e81d5d8bd4694028af7312d80f134e8cf0 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 16:28:55 -0500 Subject: [PATCH 0616/2644] Use enums in opentherm_gw (#62084) --- .../components/opentherm_gw/const.py | 75 +++++++++---------- 1 file changed, 36 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/opentherm_gw/const.py b/homeassistant/components/opentherm_gw/const.py index 09713a69e54..7ad97e865e3 100644 --- a/homeassistant/components/opentherm_gw/const.py +++ b/homeassistant/components/opentherm_gw/const.py @@ -1,9 +1,9 @@ """Constants for the opentherm_gw integration.""" import pyotgw.vars as gw_vars -from homeassistant.components.binary_sensor import DEVICE_CLASS_PROBLEM +from homeassistant.components.binary_sensor import BinarySensorDeviceClass +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( - DEVICE_CLASS_TEMPERATURE, PERCENTAGE, PRESSURE_BAR, TEMP_CELSIUS, @@ -26,9 +26,6 @@ CONF_TEMPORARY_OVRD_MODE = "temporary_override_mode" DATA_GATEWAYS = "gateways" DATA_OPENTHERM_GW = "opentherm_gw" -DEVICE_CLASS_COLD = "cold" -DEVICE_CLASS_HEAT = "heat" - DOMAIN = "opentherm_gw" SERVICE_RESET_GATEWAY = "reset_gateway" @@ -80,37 +77,37 @@ BINARY_SENSOR_INFO = { [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_SLAVE_FAULT_IND: [ - DEVICE_CLASS_PROBLEM, + BinarySensorDeviceClass.PROBLEM, "Boiler Fault {}", [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_SLAVE_CH_ACTIVE: [ - DEVICE_CLASS_HEAT, + BinarySensorDeviceClass.HEAT, "Boiler Central Heating {}", [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_SLAVE_DHW_ACTIVE: [ - DEVICE_CLASS_HEAT, + BinarySensorDeviceClass.HEAT, "Boiler Hot Water {}", [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_SLAVE_FLAME_ON: [ - DEVICE_CLASS_HEAT, + BinarySensorDeviceClass.HEAT, "Boiler Flame {}", [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_SLAVE_COOLING_ACTIVE: [ - DEVICE_CLASS_COLD, + BinarySensorDeviceClass.COLD, "Boiler Cooling {}", [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_SLAVE_CH2_ACTIVE: [ - DEVICE_CLASS_HEAT, + BinarySensorDeviceClass.HEAT, "Boiler Central Heating 2 {}", [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_SLAVE_DIAG_IND: [ - DEVICE_CLASS_PROBLEM, + BinarySensorDeviceClass.PROBLEM, "Boiler Diagnostics {}", [gw_vars.BOILER, gw_vars.THERMOSTAT], ], @@ -145,7 +142,7 @@ BINARY_SENSOR_INFO = { [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_SLAVE_SERVICE_REQ: [ - DEVICE_CLASS_PROBLEM, + BinarySensorDeviceClass.PROBLEM, "Boiler Service Required {}", [gw_vars.BOILER, gw_vars.THERMOSTAT], ], @@ -155,22 +152,22 @@ BINARY_SENSOR_INFO = { [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_SLAVE_LOW_WATER_PRESS: [ - DEVICE_CLASS_PROBLEM, + BinarySensorDeviceClass.PROBLEM, "Boiler Low Water Pressure {}", [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_SLAVE_GAS_FAULT: [ - DEVICE_CLASS_PROBLEM, + BinarySensorDeviceClass.PROBLEM, "Boiler Gas Fault {}", [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_SLAVE_AIR_PRESS_FAULT: [ - DEVICE_CLASS_PROBLEM, + BinarySensorDeviceClass.PROBLEM, "Boiler Air Pressure Fault {}", [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_SLAVE_WATER_OVERTEMP: [ - DEVICE_CLASS_PROBLEM, + BinarySensorDeviceClass.PROBLEM, "Boiler Water Overtemperature {}", [gw_vars.BOILER, gw_vars.THERMOSTAT], ], @@ -217,7 +214,7 @@ BINARY_SENSOR_INFO = { SENSOR_INFO = { # [device_class, unit, friendly_name, [status source, ...]] gw_vars.DATA_CONTROL_SETPOINT: [ - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, "Control Setpoint {}", [gw_vars.BOILER, gw_vars.THERMOSTAT], @@ -247,13 +244,13 @@ SENSOR_INFO = { [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_CONTROL_SETPOINT_2: [ - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, "Control Setpoint 2 {}", [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_ROOM_SETPOINT_OVRD: [ - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, "Room Setpoint Override {}", [gw_vars.BOILER, gw_vars.THERMOSTAT], @@ -277,7 +274,7 @@ SENSOR_INFO = { [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_ROOM_SETPOINT: [ - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, "Room Setpoint {}", [gw_vars.BOILER, gw_vars.THERMOSTAT], @@ -301,103 +298,103 @@ SENSOR_INFO = { [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_ROOM_SETPOINT_2: [ - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, "Room Setpoint 2 {}", [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_ROOM_TEMP: [ - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, "Room Temperature {}", [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_CH_WATER_TEMP: [ - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, "Central Heating Water Temperature {}", [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_DHW_TEMP: [ - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, "Hot Water Temperature {}", [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_OUTSIDE_TEMP: [ - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, "Outside Temperature {}", [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_RETURN_WATER_TEMP: [ - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, "Return Water Temperature {}", [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_SOLAR_STORAGE_TEMP: [ - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, "Solar Storage Temperature {}", [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_SOLAR_COLL_TEMP: [ - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, "Solar Collector Temperature {}", [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_CH_WATER_TEMP_2: [ - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, "Central Heating 2 Water Temperature {}", [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_DHW_TEMP_2: [ - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, "Hot Water 2 Temperature {}", [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_EXHAUST_TEMP: [ - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, "Exhaust Temperature {}", [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_SLAVE_DHW_MAX_SETP: [ - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, "Hot Water Maximum Setpoint {}", [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_SLAVE_DHW_MIN_SETP: [ - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, "Hot Water Minimum Setpoint {}", [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_SLAVE_CH_MAX_SETP: [ - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, "Boiler Maximum Central Heating Setpoint {}", [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_SLAVE_CH_MIN_SETP: [ - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, "Boiler Minimum Central Heating Setpoint {}", [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_DHW_SETPOINT: [ - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, "Hot Water Setpoint {}", [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_MAX_CH_SETPOINT: [ - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, "Maximum Central Heating Setpoint {}", [gw_vars.BOILER, gw_vars.THERMOSTAT], @@ -511,7 +508,7 @@ SENSOR_INFO = { gw_vars.OTGW_GPIO_A: [None, None, "Gateway GPIO A Mode {}", [gw_vars.OTGW]], gw_vars.OTGW_GPIO_B: [None, None, "Gateway GPIO B Mode {}", [gw_vars.OTGW]], gw_vars.OTGW_SB_TEMP: [ - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, "Gateway Setback Temperature {}", [gw_vars.OTGW], From 32e1a3d063784cbaeef3a503cd116f1cc08bf647 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 16:29:54 -0500 Subject: [PATCH 0617/2644] Use enums in Point (#62070) --- .../components/point/binary_sensor.py | 24 +++++++------------ homeassistant/components/point/sensor.py | 13 ++++------ 2 files changed, 13 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/point/binary_sensor.py b/homeassistant/components/point/binary_sensor.py index 0bcd2a33a2e..688af9b38ec 100644 --- a/homeassistant/components/point/binary_sensor.py +++ b/homeassistant/components/point/binary_sensor.py @@ -4,14 +4,8 @@ import logging from pypoint import EVENTS from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_COLD, - DEVICE_CLASS_CONNECTIVITY, - DEVICE_CLASS_HEAT, - DEVICE_CLASS_MOISTURE, - DEVICE_CLASS_MOTION, - DEVICE_CLASS_SOUND, DOMAIN, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.core import callback @@ -25,17 +19,17 @@ _LOGGER = logging.getLogger(__name__) DEVICES = { "alarm": {"icon": "mdi:alarm-bell"}, - "battery": {"device_class": DEVICE_CLASS_BATTERY}, + "battery": {"device_class": BinarySensorDeviceClass.BATTERY}, "button_press": {"icon": "mdi:gesture-tap-button"}, - "cold": {"device_class": DEVICE_CLASS_COLD}, - "connectivity": {"device_class": DEVICE_CLASS_CONNECTIVITY}, + "cold": {"device_class": BinarySensorDeviceClass.COLD}, + "connectivity": {"device_class": BinarySensorDeviceClass.CONNECTIVITY}, "dry": {"icon": "mdi:water"}, "glass": {"icon": "mdi:window-closed-variant"}, - "heat": {"device_class": DEVICE_CLASS_HEAT}, - "moisture": {"device_class": DEVICE_CLASS_MOISTURE}, - "motion": {"device_class": DEVICE_CLASS_MOTION}, + "heat": {"device_class": BinarySensorDeviceClass.HEAT}, + "moisture": {"device_class": BinarySensorDeviceClass.MOISTURE}, + "motion": {"device_class": BinarySensorDeviceClass.MOTION}, "noise": {"icon": "mdi:volume-high"}, - "sound": {"device_class": DEVICE_CLASS_SOUND}, + "sound": {"device_class": BinarySensorDeviceClass.SOUND}, "tamper_old": {"icon": "mdi:shield-alert"}, "tamper": {"icon": "mdi:shield-alert"}, } @@ -118,7 +112,7 @@ class MinutPointBinarySensor(MinutPointEntity, BinarySensorEntity): @property def is_on(self): """Return the state of the binary sensor.""" - if self.device_class == DEVICE_CLASS_CONNECTIVITY: + if self.device_class == BinarySensorDeviceClass.CONNECTIVITY: # connectivity is the other way around. return not self._is_on return self._is_on diff --git a/homeassistant/components/point/sensor.py b/homeassistant/components/point/sensor.py index bb98ccb53d9..027869d033d 100644 --- a/homeassistant/components/point/sensor.py +++ b/homeassistant/components/point/sensor.py @@ -6,16 +6,11 @@ import logging from homeassistant.components.sensor import ( DOMAIN, + SensorDeviceClass, SensorEntity, SensorEntityDescription, ) -from homeassistant.const import ( - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_TEMPERATURE, - PERCENTAGE, - SOUND_PRESSURE_WEIGHTED_DBA, - TEMP_CELSIUS, -) +from homeassistant.const import PERCENTAGE, SOUND_PRESSURE_WEIGHTED_DBA, TEMP_CELSIUS from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util.dt import parse_datetime @@ -45,13 +40,13 @@ SENSOR_TYPES: tuple[MinutPointSensorEntityDescription, ...] = ( MinutPointSensorEntityDescription( key="temperature", precision=1, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, ), MinutPointSensorEntityDescription( key="humidity", precision=1, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=PERCENTAGE, ), MinutPointSensorEntityDescription( From 83cb2d11d51cc28c3b3c4632c04ea63e1232e4b4 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 16:31:51 -0500 Subject: [PATCH 0618/2644] Use enums in xiaomi_miio (#61979) --- .../components/xiaomi_miio/binary_sensor.py | 32 ++--- .../components/xiaomi_miio/humidifier.py | 9 +- .../components/xiaomi_miio/number.py | 21 +-- .../components/xiaomi_miio/select.py | 4 +- .../components/xiaomi_miio/sensor.py | 128 ++++++++---------- .../components/xiaomi_miio/switch.py | 22 +-- 6 files changed, 101 insertions(+), 115 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/binary_sensor.py b/homeassistant/components/xiaomi_miio/binary_sensor.py index 26fa82b7df2..c9fd97f209b 100644 --- a/homeassistant/components/xiaomi_miio/binary_sensor.py +++ b/homeassistant/components/xiaomi_miio/binary_sensor.py @@ -6,14 +6,12 @@ from dataclasses import dataclass import logging from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_CONNECTIVITY, - DEVICE_CLASS_PLUG, - DEVICE_CLASS_PROBLEM, + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import callback +from homeassistant.helpers.entity import EntityCategory from . import VacuumCoordinatorDataAttributes from .const import ( @@ -56,21 +54,21 @@ BINARY_SENSOR_TYPES = ( key=ATTR_NO_WATER, name="Water Tank Empty", icon="mdi:water-off-outline", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), XiaomiMiioBinarySensorDescription( key=ATTR_WATER_TANK_DETACHED, name="Water Tank", icon="mdi:car-coolant-level", - device_class=DEVICE_CLASS_CONNECTIVITY, + device_class=BinarySensorDeviceClass.CONNECTIVITY, value=lambda value: not value, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), XiaomiMiioBinarySensorDescription( key=ATTR_POWERSUPPLY_ATTACHED, name="Power Supply", - device_class=DEVICE_CLASS_PLUG, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.PLUG, + entity_category=EntityCategory.DIAGNOSTIC, ), ) @@ -83,8 +81,8 @@ VACUUM_SENSORS = { icon="mdi:square-rounded", parent_key=VacuumCoordinatorDataAttributes.status, entity_registry_enabled_default=True, - device_class=DEVICE_CLASS_CONNECTIVITY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=EntityCategory.DIAGNOSTIC, ), ATTR_WATER_BOX_ATTACHED: XiaomiMiioBinarySensorDescription( key=ATTR_WATER_BOX_ATTACHED, @@ -92,8 +90,8 @@ VACUUM_SENSORS = { icon="mdi:water", parent_key=VacuumCoordinatorDataAttributes.status, entity_registry_enabled_default=True, - device_class=DEVICE_CLASS_CONNECTIVITY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=EntityCategory.DIAGNOSTIC, ), ATTR_WATER_SHORTAGE: XiaomiMiioBinarySensorDescription( key=ATTR_WATER_SHORTAGE, @@ -101,8 +99,8 @@ VACUUM_SENSORS = { icon="mdi:water", parent_key=VacuumCoordinatorDataAttributes.status, entity_registry_enabled_default=True, - device_class=DEVICE_CLASS_PROBLEM, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, ), } @@ -114,8 +112,8 @@ VACUUM_SENSORS_SEPARATE_MOP = { icon="mdi:square-rounded", parent_key=VacuumCoordinatorDataAttributes.status, entity_registry_enabled_default=True, - device_class=DEVICE_CLASS_CONNECTIVITY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=EntityCategory.DIAGNOSTIC, ), } diff --git a/homeassistant/components/xiaomi_miio/humidifier.py b/homeassistant/components/xiaomi_miio/humidifier.py index 9896bf8f0ea..f674cc770b8 100644 --- a/homeassistant/components/xiaomi_miio/humidifier.py +++ b/homeassistant/components/xiaomi_miio/humidifier.py @@ -6,11 +6,8 @@ from miio.airhumidifier import OperationMode as AirhumidifierOperationMode from miio.airhumidifier_miot import OperationMode as AirhumidifierMiotOperationMode from miio.airhumidifier_mjjsq import OperationMode as AirhumidifierMjjsqOperationMode -from homeassistant.components.humidifier import HumidifierEntity -from homeassistant.components.humidifier.const import ( - DEVICE_CLASS_HUMIDIFIER, - SUPPORT_MODES, -) +from homeassistant.components.humidifier import HumidifierDeviceClass, HumidifierEntity +from homeassistant.components.humidifier.const import SUPPORT_MODES from homeassistant.const import ATTR_MODE from homeassistant.core import callback from homeassistant.util.percentage import percentage_to_ranged_value @@ -105,7 +102,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class XiaomiGenericHumidifier(XiaomiCoordinatedMiioEntity, HumidifierEntity): """Representation of a generic Xiaomi humidifier device.""" - _attr_device_class = DEVICE_CLASS_HUMIDIFIER + _attr_device_class = HumidifierDeviceClass.HUMIDIFIER _attr_supported_features = SUPPORT_MODES def __init__(self, name, device, entry, unique_id, coordinator): diff --git a/homeassistant/components/xiaomi_miio/number.py b/homeassistant/components/xiaomi_miio/number.py index f8516c66e84..cd05218760d 100644 --- a/homeassistant/components/xiaomi_miio/number.py +++ b/homeassistant/components/xiaomi_miio/number.py @@ -4,8 +4,9 @@ from __future__ import annotations from dataclasses import dataclass from homeassistant.components.number import NumberEntity, NumberEntityDescription -from homeassistant.const import DEGREE, ENTITY_CATEGORY_CONFIG, TIME_MINUTES +from homeassistant.const import DEGREE, TIME_MINUTES from homeassistant.core import callback +from homeassistant.helpers.entity import EntityCategory from .const import ( CONF_DEVICE, @@ -106,7 +107,7 @@ NUMBER_TYPES = { step=10, available_with_device_off=False, method="async_set_motor_speed", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), FEATURE_SET_FAVORITE_LEVEL: XiaomiMiioNumberDescription( key=ATTR_FAVORITE_LEVEL, @@ -116,7 +117,7 @@ NUMBER_TYPES = { max_value=17, step=1, method="async_set_favorite_level", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), FEATURE_SET_FAN_LEVEL: XiaomiMiioNumberDescription( key=ATTR_FAN_LEVEL, @@ -126,7 +127,7 @@ NUMBER_TYPES = { max_value=3, step=1, method="async_set_fan_level", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), FEATURE_SET_VOLUME: XiaomiMiioNumberDescription( key=ATTR_VOLUME, @@ -136,7 +137,7 @@ NUMBER_TYPES = { max_value=100, step=1, method="async_set_volume", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), FEATURE_SET_OSCILLATION_ANGLE: XiaomiMiioNumberDescription( key=ATTR_OSCILLATION_ANGLE, @@ -147,7 +148,7 @@ NUMBER_TYPES = { max_value=120, step=1, method="async_set_oscillation_angle", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), FEATURE_SET_DELAY_OFF_COUNTDOWN: XiaomiMiioNumberDescription( key=ATTR_DELAY_OFF_COUNTDOWN, @@ -158,7 +159,7 @@ NUMBER_TYPES = { max_value=480, step=1, method="async_set_delay_off_countdown", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), FEATURE_SET_LED_BRIGHTNESS: XiaomiMiioNumberDescription( key=ATTR_LED_BRIGHTNESS, @@ -168,7 +169,7 @@ NUMBER_TYPES = { max_value=100, step=1, method="async_set_led_brightness", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), FEATURE_SET_LED_BRIGHTNESS_LEVEL: XiaomiMiioNumberDescription( key=ATTR_LED_BRIGHTNESS_LEVEL, @@ -178,7 +179,7 @@ NUMBER_TYPES = { max_value=8, step=1, method="async_set_led_brightness_level", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), FEATURE_SET_FAVORITE_RPM: XiaomiMiioNumberDescription( key=ATTR_FAVORITE_RPM, @@ -189,7 +190,7 @@ NUMBER_TYPES = { max_value=2200, step=10, method="async_set_favorite_rpm", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), } diff --git a/homeassistant/components/xiaomi_miio/select.py b/homeassistant/components/xiaomi_miio/select.py index ec1be6f3219..d2a806f8305 100644 --- a/homeassistant/components/xiaomi_miio/select.py +++ b/homeassistant/components/xiaomi_miio/select.py @@ -11,8 +11,8 @@ from miio.airpurifier_miot import LedBrightness as AirpurifierMiotLedBrightness from miio.fan import LedBrightness as FanLedBrightness from homeassistant.components.select import SelectEntity, SelectEntityDescription -from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import callback +from homeassistant.helpers.entity import EntityCategory from .const import ( CONF_DEVICE, @@ -63,7 +63,7 @@ SELECTOR_TYPES = { icon="mdi:brightness-6", device_class="xiaomi_miio__led_brightness", options=("bright", "dim", "off"), - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), } diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index ccf55a04e17..1104ff90117 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -15,10 +15,10 @@ from miio.gateway.gateway import ( ) from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.const import ( AREA_SQUARE_METERS, @@ -28,17 +28,6 @@ from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, CONF_HOST, CONF_TOKEN, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_CO2, - DEVICE_CLASS_GAS, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_PM25, - DEVICE_CLASS_POWER, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_TIMESTAMP, - ENTITY_CATEGORY_DIAGNOSTIC, LIGHT_LUX, PERCENTAGE, POWER_WATT, @@ -49,6 +38,7 @@ from homeassistant.const import ( VOLUME_CUBIC_METERS, ) from homeassistant.core import callback +from homeassistant.helpers.entity import EntityCategory from homeassistant.util import dt as dt_util from . import VacuumCoordinatorDataAttributes @@ -148,137 +138,137 @@ SENSOR_TYPES = { key=ATTR_TEMPERATURE, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), ATTR_HUMIDITY: XiaomiMiioSensorDescription( key=ATTR_HUMIDITY, name="Humidity", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), ATTR_PRESSURE: XiaomiMiioSensorDescription( key=ATTR_PRESSURE, name="Pressure", native_unit_of_measurement=PRESSURE_HPA, - device_class=DEVICE_CLASS_PRESSURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, ), ATTR_LOAD_POWER: XiaomiMiioSensorDescription( key=ATTR_LOAD_POWER, name="Load Power", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, ), ATTR_WATER_LEVEL: XiaomiMiioSensorDescription( key=ATTR_WATER_LEVEL, name="Water Level", native_unit_of_measurement=PERCENTAGE, icon="mdi:water-check", - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), ATTR_ACTUAL_SPEED: XiaomiMiioSensorDescription( key=ATTR_ACTUAL_SPEED, name="Actual Speed", native_unit_of_measurement="rpm", icon="mdi:fast-forward", - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), ATTR_MOTOR_SPEED: XiaomiMiioSensorDescription( key=ATTR_MOTOR_SPEED, name="Motor Speed", native_unit_of_measurement="rpm", icon="mdi:fast-forward", - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), ATTR_MOTOR2_SPEED: XiaomiMiioSensorDescription( key=ATTR_MOTOR2_SPEED, name="Second Motor Speed", native_unit_of_measurement="rpm", icon="mdi:fast-forward", - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), ATTR_USE_TIME: XiaomiMiioSensorDescription( key=ATTR_USE_TIME, name="Use Time", native_unit_of_measurement=TIME_SECONDS, icon="mdi:progress-clock", - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, entity_registry_enabled_default=False, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), ATTR_ILLUMINANCE: XiaomiMiioSensorDescription( key=ATTR_ILLUMINANCE, name="Illuminance", native_unit_of_measurement=UNIT_LUMEN, - device_class=DEVICE_CLASS_ILLUMINANCE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, ), ATTR_ILLUMINANCE_LUX: XiaomiMiioSensorDescription( key=ATTR_ILLUMINANCE, name="Illuminance", native_unit_of_measurement=LIGHT_LUX, - device_class=DEVICE_CLASS_ILLUMINANCE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, ), ATTR_AIR_QUALITY: XiaomiMiioSensorDescription( key=ATTR_AIR_QUALITY, native_unit_of_measurement="AQI", icon="mdi:cloud", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), ATTR_PM25: XiaomiMiioSensorDescription( key=ATTR_AQI, name="PM2.5", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - device_class=DEVICE_CLASS_PM25, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.PM25, + state_class=SensorStateClass.MEASUREMENT, ), ATTR_FILTER_LIFE_REMAINING: XiaomiMiioSensorDescription( key=ATTR_FILTER_LIFE_REMAINING, name="Filter Life Remaining", native_unit_of_measurement=PERCENTAGE, icon="mdi:air-filter", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, attributes=("filter_type",), - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), ATTR_FILTER_USE: XiaomiMiioSensorDescription( key=ATTR_FILTER_HOURS_USED, name="Filter Use", native_unit_of_measurement=TIME_HOURS, icon="mdi:clock-outline", - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), ATTR_CARBON_DIOXIDE: XiaomiMiioSensorDescription( key=ATTR_CARBON_DIOXIDE, name="Carbon Dioxide", native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, - device_class=DEVICE_CLASS_CO2, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CO2, + state_class=SensorStateClass.MEASUREMENT, ), ATTR_PURIFY_VOLUME: XiaomiMiioSensorDescription( key=ATTR_PURIFY_VOLUME, name="Purify Volume", native_unit_of_measurement=VOLUME_CUBIC_METERS, - device_class=DEVICE_CLASS_GAS, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.GAS, + state_class=SensorStateClass.TOTAL_INCREASING, entity_registry_enabled_default=False, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), ATTR_BATTERY: XiaomiMiioSensorDescription( key=ATTR_BATTERY, name="Battery", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_BATTERY, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), } @@ -408,35 +398,35 @@ VACUUM_SENSORS = { key=ATTR_DND_START, icon="mdi:minus-circle-off", name="DnD Start", - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, parent_key=VacuumCoordinatorDataAttributes.dnd_status, entity_registry_enabled_default=False, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), f"dnd_{ATTR_DND_END}": XiaomiMiioSensorDescription( key=ATTR_DND_END, icon="mdi:minus-circle-off", name="DnD End", - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, parent_key=VacuumCoordinatorDataAttributes.dnd_status, entity_registry_enabled_default=False, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), f"last_clean_{ATTR_LAST_CLEAN_START}": XiaomiMiioSensorDescription( key=ATTR_LAST_CLEAN_START, icon="mdi:clock-time-twelve", name="Last Clean Start", - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, parent_key=VacuumCoordinatorDataAttributes.last_clean_details, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), f"last_clean_{ATTR_LAST_CLEAN_END}": XiaomiMiioSensorDescription( key=ATTR_LAST_CLEAN_END, icon="mdi:clock-time-twelve", - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, parent_key=VacuumCoordinatorDataAttributes.last_clean_details, name="Last Clean End", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), f"last_clean_{ATTR_LAST_CLEAN_TIME}": XiaomiMiioSensorDescription( native_unit_of_measurement=TIME_SECONDS, @@ -444,7 +434,7 @@ VACUUM_SENSORS = { key=ATTR_LAST_CLEAN_TIME, parent_key=VacuumCoordinatorDataAttributes.last_clean_details, name="Last Clean Duration", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), f"last_clean_{ATTR_LAST_CLEAN_AREA}": XiaomiMiioSensorDescription( native_unit_of_measurement=AREA_SQUARE_METERS, @@ -452,7 +442,7 @@ VACUUM_SENSORS = { key=ATTR_LAST_CLEAN_AREA, parent_key=VacuumCoordinatorDataAttributes.last_clean_details, name="Last Clean Area", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), f"clean_history_{ATTR_CLEAN_HISTORY_TOTAL_DURATION}": XiaomiMiioSensorDescription( native_unit_of_measurement=TIME_SECONDS, @@ -461,7 +451,7 @@ VACUUM_SENSORS = { parent_key=VacuumCoordinatorDataAttributes.clean_history_status, name="Total duration", entity_registry_enabled_default=False, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), f"clean_history_{ATTR_CLEAN_HISTORY_TOTAL_AREA}": XiaomiMiioSensorDescription( native_unit_of_measurement=AREA_SQUARE_METERS, @@ -470,17 +460,17 @@ VACUUM_SENSORS = { parent_key=VacuumCoordinatorDataAttributes.clean_history_status, name="Total Clean Area", entity_registry_enabled_default=False, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), f"clean_history_{ATTR_CLEAN_HISTORY_COUNT}": XiaomiMiioSensorDescription( native_unit_of_measurement="", icon="mdi:counter", - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, key=ATTR_CLEAN_HISTORY_COUNT, parent_key=VacuumCoordinatorDataAttributes.clean_history_status, name="Total Clean Count", entity_registry_enabled_default=False, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), f"clean_history_{ATTR_CLEAN_HISTORY_DUST_COLLECTION_COUNT}": XiaomiMiioSensorDescription( native_unit_of_measurement="", @@ -490,7 +480,7 @@ VACUUM_SENSORS = { parent_key=VacuumCoordinatorDataAttributes.clean_history_status, name="Total Dust Collection Count", entity_registry_enabled_default=False, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), f"consumable_{ATTR_CONSUMABLE_STATUS_MAIN_BRUSH_LEFT}": XiaomiMiioSensorDescription( native_unit_of_measurement=TIME_SECONDS, @@ -499,7 +489,7 @@ VACUUM_SENSORS = { parent_key=VacuumCoordinatorDataAttributes.consumable_status, name="Main Brush Left", entity_registry_enabled_default=False, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), f"consumable_{ATTR_CONSUMABLE_STATUS_SIDE_BRUSH_LEFT}": XiaomiMiioSensorDescription( native_unit_of_measurement=TIME_SECONDS, @@ -508,7 +498,7 @@ VACUUM_SENSORS = { parent_key=VacuumCoordinatorDataAttributes.consumable_status, name="Side Brush Left", entity_registry_enabled_default=False, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), f"consumable_{ATTR_CONSUMABLE_STATUS_FILTER_LEFT}": XiaomiMiioSensorDescription( native_unit_of_measurement=TIME_SECONDS, @@ -517,7 +507,7 @@ VACUUM_SENSORS = { parent_key=VacuumCoordinatorDataAttributes.consumable_status, name="Filter Left", entity_registry_enabled_default=False, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), f"consumable_{ATTR_CONSUMABLE_STATUS_SENSOR_DIRTY_LEFT}": XiaomiMiioSensorDescription( native_unit_of_measurement=TIME_SECONDS, @@ -526,7 +516,7 @@ VACUUM_SENSORS = { parent_key=VacuumCoordinatorDataAttributes.consumable_status, name="Sensor Dirty Left", entity_registry_enabled_default=False, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), } @@ -700,7 +690,7 @@ class XiaomiGenericSensor(XiaomiCoordinatedMiioEntity, SensorEntity): ) if ( - self.device_class == DEVICE_CLASS_TIMESTAMP + self.device_class == SensorDeviceClass.TIMESTAMP and native_value is not None and (native_datetime := dt_util.parse_datetime(str(native_value))) is not None diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index ab825e2485d..1bbb5c65c49 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -11,7 +11,7 @@ from miio.powerstrip import PowerMode import voluptuous as vol from homeassistant.components.switch import ( - DEVICE_CLASS_SWITCH, + SwitchDeviceClass, SwitchEntity, SwitchEntityDescription, ) @@ -21,10 +21,10 @@ from homeassistant.const import ( ATTR_TEMPERATURE, CONF_HOST, CONF_TOKEN, - ENTITY_CATEGORY_CONFIG, ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import EntityCategory from .const import ( CONF_DEVICE, @@ -206,7 +206,7 @@ SWITCH_TYPES = ( icon="mdi:volume-high", method_on="async_set_buzzer_on", method_off="async_set_buzzer_off", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), XiaomiMiioSwitchDescription( key=ATTR_CHILD_LOCK, @@ -215,7 +215,7 @@ SWITCH_TYPES = ( icon="mdi:lock", method_on="async_set_child_lock_on", method_off="async_set_child_lock_off", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), XiaomiMiioSwitchDescription( key=ATTR_DRY, @@ -224,7 +224,7 @@ SWITCH_TYPES = ( icon="mdi:hair-dryer", method_on="async_set_dry_on", method_off="async_set_dry_off", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), XiaomiMiioSwitchDescription( key=ATTR_CLEAN, @@ -234,7 +234,7 @@ SWITCH_TYPES = ( method_on="async_set_clean_on", method_off="async_set_clean_off", available_with_device_off=False, - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), XiaomiMiioSwitchDescription( key=ATTR_LED, @@ -243,7 +243,7 @@ SWITCH_TYPES = ( icon="mdi:led-outline", method_on="async_set_led_on", method_off="async_set_led_off", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), XiaomiMiioSwitchDescription( key=ATTR_LEARN_MODE, @@ -252,7 +252,7 @@ SWITCH_TYPES = ( icon="mdi:school-outline", method_on="async_set_learn_mode_on", method_off="async_set_learn_mode_off", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), XiaomiMiioSwitchDescription( key=ATTR_AUTO_DETECT, @@ -260,7 +260,7 @@ SWITCH_TYPES = ( name="Auto Detect", method_on="async_set_auto_detect_on", method_off="async_set_auto_detect_off", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), XiaomiMiioSwitchDescription( key=ATTR_IONIZER, @@ -269,7 +269,7 @@ SWITCH_TYPES = ( icon="mdi:shimmer", method_on="async_set_ionizer_on", method_off="async_set_ionizer_off", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), ) @@ -621,7 +621,7 @@ class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity): class XiaomiGatewaySwitch(XiaomiGatewayDevice, SwitchEntity): """Representation of a XiaomiGatewaySwitch.""" - _attr_device_class = DEVICE_CLASS_SWITCH + _attr_device_class = SwitchDeviceClass.SWITCH def __init__(self, coordinator, sub_device, entry, variable): """Initialize the XiaomiSensor.""" From 6ba11fe6c72e77e373174285708c261d081cfbd2 Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Thu, 16 Dec 2021 22:33:03 +0100 Subject: [PATCH 0619/2644] Brunt dependency bump to 1.0.2 (#62014) --- homeassistant/components/brunt/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/brunt/manifest.json b/homeassistant/components/brunt/manifest.json index 1ddbbb62f56..7b9307e8ef2 100644 --- a/homeassistant/components/brunt/manifest.json +++ b/homeassistant/components/brunt/manifest.json @@ -3,7 +3,7 @@ "name": "Brunt Blind Engine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/brunt", - "requirements": ["brunt==1.0.1"], + "requirements": ["brunt==1.0.2"], "codeowners": ["@eavanvalkenburg"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 53d76bf5a6f..5a21ad9293f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -446,7 +446,7 @@ brother==1.1.0 brottsplatskartan==0.0.1 # homeassistant.components.brunt -brunt==1.0.1 +brunt==1.0.2 # homeassistant.components.bsblan bsblan==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c34dfb8e6f6..55bb1469d7f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -287,7 +287,7 @@ broadlink==0.18.0 brother==1.1.0 # homeassistant.components.brunt -brunt==1.0.1 +brunt==1.0.2 # homeassistant.components.bsblan bsblan==0.4.0 From a16f9636056df4030e7c049fd301c7aaff0c586a Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 16:34:59 -0500 Subject: [PATCH 0620/2644] Use enums in pi_hole (#62064) --- homeassistant/components/pi_hole/const.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/pi_hole/const.py b/homeassistant/components/pi_hole/const.py index d13c83c7b28..38819d29df4 100644 --- a/homeassistant/components/pi_hole/const.py +++ b/homeassistant/components/pi_hole/const.py @@ -9,7 +9,7 @@ from typing import Any from hole import Hole from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_UPDATE, + BinarySensorDeviceClass, BinarySensorEntityDescription, ) from homeassistant.components.sensor import SensorEntityDescription @@ -122,7 +122,7 @@ BINARY_SENSOR_TYPES: tuple[PiHoleBinarySensorEntityDescription, ...] = ( PiHoleBinarySensorEntityDescription( key="core_update_available", name="Core Update Available", - device_class=DEVICE_CLASS_UPDATE, + device_class=BinarySensorDeviceClass.UPDATE, extra_value=lambda api: { "current_version": api.versions["core_current"], "latest_version": api.versions["core_latest"], @@ -132,7 +132,7 @@ BINARY_SENSOR_TYPES: tuple[PiHoleBinarySensorEntityDescription, ...] = ( PiHoleBinarySensorEntityDescription( key="web_update_available", name="Web Update Available", - device_class=DEVICE_CLASS_UPDATE, + device_class=BinarySensorDeviceClass.UPDATE, extra_value=lambda api: { "current_version": api.versions["web_current"], "latest_version": api.versions["web_latest"], @@ -142,7 +142,7 @@ BINARY_SENSOR_TYPES: tuple[PiHoleBinarySensorEntityDescription, ...] = ( PiHoleBinarySensorEntityDescription( key="ftl_update_available", name="FTL Update Available", - device_class=DEVICE_CLASS_UPDATE, + device_class=BinarySensorDeviceClass.UPDATE, extra_value=lambda api: { "current_version": api.versions["FTL_current"], "latest_version": api.versions["FTL_latest"], From 0ee5691f770bfe95cce5d1889753be719d523512 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 16:36:01 -0500 Subject: [PATCH 0621/2644] Use enums in roomba (#62045) --- homeassistant/components/roomba/sensor.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/roomba/sensor.py b/homeassistant/components/roomba/sensor.py index 4552159677c..ae33930b549 100644 --- a/homeassistant/components/roomba/sensor.py +++ b/homeassistant/components/roomba/sensor.py @@ -1,11 +1,8 @@ """Sensor for checking the battery level of Roomba.""" -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.components.vacuum import STATE_DOCKED -from homeassistant.const import ( - DEVICE_CLASS_BATTERY, - ENTITY_CATEGORY_DIAGNOSTIC, - PERCENTAGE, -) +from homeassistant.const import PERCENTAGE +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.icon import icon_for_battery_level from .const import BLID, DOMAIN, ROOMBA_SESSION @@ -24,7 +21,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class RoombaBattery(IRobotEntity, SensorEntity): """Class to hold Roomba Sensor basic info.""" - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_entity_category = EntityCategory.DIAGNOSTIC @property def name(self): @@ -39,7 +36,7 @@ class RoombaBattery(IRobotEntity, SensorEntity): @property def device_class(self): """Return the device class of the sensor.""" - return DEVICE_CLASS_BATTERY + return SensorDeviceClass.BATTERY @property def native_unit_of_measurement(self): From 6f54636baff03961f5c8c6c28a1eda67239bcc0e Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 16:38:26 -0500 Subject: [PATCH 0622/2644] Use enums in rainforest_eagle (#62057) --- .../components/rainforest_eagle/sensor.py | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/rainforest_eagle/sensor.py b/homeassistant/components/rainforest_eagle/sensor.py index bdd6ac541a9..0f2c4f62125 100644 --- a/homeassistant/components/rainforest_eagle/sensor.py +++ b/homeassistant/components/rainforest_eagle/sensor.py @@ -2,19 +2,14 @@ from __future__ import annotations from homeassistant.components.sensor import ( - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, StateType, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - DEVICE_CLASS_POWER, - ENERGY_KILO_WATT_HOUR, - POWER_KILO_WATT, -) +from homeassistant.const import ENERGY_KILO_WATT_HOUR, POWER_KILO_WATT from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -29,22 +24,22 @@ SENSORS = ( # We can drop the "Eagle-200" part of the name in HA 2021.12 name="Eagle-200 Meter Power Demand", native_unit_of_measurement=POWER_KILO_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="zigbee:CurrentSummationDelivered", name="Eagle-200 Total Meter Energy Delivered", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="zigbee:CurrentSummationReceived", name="Eagle-200 Total Meter Energy Received", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), ) @@ -64,7 +59,7 @@ async def async_setup_entry( key="zigbee:Price", name="Meter Price", native_unit_of_measurement=f"{coordinator.data['zigbee:PriceCurrency']}/{ENERGY_KILO_WATT_HOUR}", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), ) ) From 4bfc3eb22f56baabcdceb98ad3552689d10a1d0a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 16 Dec 2021 22:42:29 +0100 Subject: [PATCH 0623/2644] Use new enums in netatmo (#61941) --- homeassistant/components/netatmo/sensor.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 5b3416e3b09..defcd757d0a 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -19,7 +19,6 @@ from homeassistant.const import ( ATTR_LONGITUDE, CONCENTRATION_PARTS_PER_MILLION, DEGREE, - ENTITY_CATEGORY_DIAGNOSTIC, LENGTH_MILLIMETERS, PERCENTAGE, PRESSURE_MBAR, @@ -35,6 +34,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( @@ -174,7 +174,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( name="Battery Percent", netatmo_name="battery_percent", entity_registry_enabled_default=True, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.BATTERY, @@ -234,7 +234,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( name="Reachability", netatmo_name="reachable", entity_registry_enabled_default=False, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:signal", ), NetatmoSensorEntityDescription( @@ -242,7 +242,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( name="Radio", netatmo_name="rf_status", entity_registry_enabled_default=False, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:signal", ), NetatmoSensorEntityDescription( @@ -250,7 +250,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( name="Radio Level", netatmo_name="rf_status", entity_registry_enabled_default=False, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, @@ -260,7 +260,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( name="Wifi", netatmo_name="wifi_status", entity_registry_enabled_default=False, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:wifi", ), NetatmoSensorEntityDescription( @@ -268,7 +268,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( name="Wifi Level", netatmo_name="wifi_status", entity_registry_enabled_default=False, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, @@ -589,7 +589,7 @@ class NetatmoClimateBatterySensor(NetatmoBase, SensorEntity): name="Battery Percent", netatmo_name="battery_percent", entity_registry_enabled_default=True, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.BATTERY, From 890dcfee11a9942a83132e2f9dfe0b1f7427539c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 16 Dec 2021 22:42:52 +0100 Subject: [PATCH 0624/2644] Use new enums in netgear_lte (#61955) --- homeassistant/components/netgear_lte/sensor_types.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netgear_lte/sensor_types.py b/homeassistant/components/netgear_lte/sensor_types.py index 644fe35c8c3..45cfb873ef8 100644 --- a/homeassistant/components/netgear_lte/sensor_types.py +++ b/homeassistant/components/netgear_lte/sensor_types.py @@ -1,6 +1,6 @@ """Define possible sensor types.""" -from homeassistant.components.binary_sensor import DEVICE_CLASS_CONNECTIVITY +from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.const import ( DATA_MEBIBYTES, PERCENTAGE, @@ -31,8 +31,8 @@ BINARY_SENSOR_MOBILE_CONNECTED = "mobile_connected" BINARY_SENSOR_CLASSES = { "roaming": None, - "wire_connected": DEVICE_CLASS_CONNECTIVITY, - BINARY_SENSOR_MOBILE_CONNECTED: DEVICE_CLASS_CONNECTIVITY, + "wire_connected": BinarySensorDeviceClass.CONNECTIVITY, + BINARY_SENSOR_MOBILE_CONNECTED: BinarySensorDeviceClass.CONNECTIVITY, } ALL_SENSORS = list(SENSOR_UNITS) From 81ea811b7467a02cfe7fb78bd6f7e74041e45337 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 16 Dec 2021 22:44:17 +0100 Subject: [PATCH 0625/2644] Use new enums in nextbus (#61954) --- homeassistant/components/nextbus/sensor.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nextbus/sensor.py b/homeassistant/components/nextbus/sensor.py index 3756c1853b7..604bd0c6602 100644 --- a/homeassistant/components/nextbus/sensor.py +++ b/homeassistant/components/nextbus/sensor.py @@ -5,8 +5,12 @@ import logging from py_nextbus import NextBusClient import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import CONF_NAME, DEVICE_CLASS_TIMESTAMP +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorDeviceClass, + SensorEntity, +) +from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.util.dt import utc_from_timestamp @@ -112,7 +116,7 @@ class NextBusDepartureSensor(SensorEntity): the future using fuzzy logic and matching. """ - _attr_device_class = DEVICE_CLASS_TIMESTAMP + _attr_device_class = SensorDeviceClass.TIMESTAMP _attr_icon = "mdi:bus" def __init__(self, client, agency, route, stop, name=None): From 39590f9917e6a5d704b5029bf8a0cb7db9022604 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 16 Dec 2021 22:44:50 +0100 Subject: [PATCH 0626/2644] Use new enums in nissan_leaf (#61951) --- homeassistant/components/nissan_leaf/binary_sensor.py | 7 +++---- homeassistant/components/nissan_leaf/sensor.py | 6 +++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/nissan_leaf/binary_sensor.py b/homeassistant/components/nissan_leaf/binary_sensor.py index 13fe666a3a8..5212eec3e82 100644 --- a/homeassistant/components/nissan_leaf/binary_sensor.py +++ b/homeassistant/components/nissan_leaf/binary_sensor.py @@ -2,8 +2,7 @@ import logging from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_BATTERY_CHARGING, - DEVICE_CLASS_PLUG, + BinarySensorDeviceClass, BinarySensorEntity, ) @@ -29,7 +28,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class LeafPluggedInSensor(LeafEntity, BinarySensorEntity): """Plugged In Sensor class.""" - _attr_device_class = DEVICE_CLASS_PLUG + _attr_device_class = BinarySensorDeviceClass.PLUG @property def name(self): @@ -50,7 +49,7 @@ class LeafPluggedInSensor(LeafEntity, BinarySensorEntity): class LeafChargingSensor(LeafEntity, BinarySensorEntity): """Charging Sensor class.""" - _attr_device_class = DEVICE_CLASS_BATTERY_CHARGING + _attr_device_class = BinarySensorDeviceClass.BATTERY_CHARGING @property def name(self): diff --git a/homeassistant/components/nissan_leaf/sensor.py b/homeassistant/components/nissan_leaf/sensor.py index bed4d264bd4..8d43e9bcf85 100644 --- a/homeassistant/components/nissan_leaf/sensor.py +++ b/homeassistant/components/nissan_leaf/sensor.py @@ -1,8 +1,8 @@ """Battery Charge and Range Support for the Nissan Leaf.""" import logging -from homeassistant.components.sensor import SensorEntity -from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.const import PERCENTAGE from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.util.distance import LENGTH_KILOMETERS, LENGTH_MILES from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM @@ -47,7 +47,7 @@ class LeafBatterySensor(LeafEntity, SensorEntity): @property def device_class(self): """Return the device class of the sensor.""" - return DEVICE_CLASS_BATTERY + return SensorDeviceClass.BATTERY @property def native_value(self): From c4c9dc8cee5eef717b5d81f5d291ee42e4654d01 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 16 Dec 2021 22:45:27 +0100 Subject: [PATCH 0627/2644] Use new enums in neurio_energy (#61953) --- .../components/neurio_energy/sensor.py | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/neurio_energy/sensor.py b/homeassistant/components/neurio_energy/sensor.py index b316281faa1..99bc41e84ce 100644 --- a/homeassistant/components/neurio_energy/sensor.py +++ b/homeassistant/components/neurio_energy/sensor.py @@ -10,17 +10,11 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, + SensorStateClass, ) -from homeassistant.const import ( - CONF_API_KEY, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - ENERGY_KILO_WATT_HOUR, - POWER_WATT, -) +from homeassistant.const import CONF_API_KEY, ENERGY_KILO_WATT_HOUR, POWER_WATT import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle import homeassistant.util.dt as dt_util @@ -148,12 +142,12 @@ class NeurioEnergy(SensorEntity): if sensor_type == ACTIVE_TYPE: self._unit_of_measurement = POWER_WATT - self._attr_device_class = DEVICE_CLASS_POWER - self._attr_state_class = STATE_CLASS_MEASUREMENT + self._attr_device_class = SensorDeviceClass.POWER + self._attr_state_class = SensorStateClass.MEASUREMENT elif sensor_type == DAILY_TYPE: self._unit_of_measurement = ENERGY_KILO_WATT_HOUR - self._attr_device_class = DEVICE_CLASS_ENERGY - self._attr_state_class = STATE_CLASS_TOTAL_INCREASING + self._attr_device_class = SensorDeviceClass.ENERGY + self._attr_state_class = SensorStateClass.TOTAL_INCREASING @property def name(self): From b3105dc218da43916d4f639baae26d849af867d6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 16 Dec 2021 22:45:48 +0100 Subject: [PATCH 0628/2644] Use new enums in nuki (#61949) --- homeassistant/components/nuki/binary_sensor.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nuki/binary_sensor.py b/homeassistant/components/nuki/binary_sensor.py index 3b79eef324f..52cf1090d9e 100644 --- a/homeassistant/components/nuki/binary_sensor.py +++ b/homeassistant/components/nuki/binary_sensor.py @@ -4,7 +4,10 @@ import logging from pynuki import STATE_DOORSENSOR_OPENED -from homeassistant.components.binary_sensor import DEVICE_CLASS_DOOR, BinarySensorEntity +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, +) from . import NukiEntity from .const import ATTR_NUKI_ID, DATA_COORDINATOR, DATA_LOCKS, DOMAIN as NUKI_DOMAIN @@ -29,7 +32,7 @@ async def async_setup_entry(hass, entry, async_add_entities): class NukiDoorsensorEntity(NukiEntity, BinarySensorEntity): """Representation of a Nuki Lock Doorsensor.""" - _attr_device_class = DEVICE_CLASS_DOOR + _attr_device_class = BinarySensorDeviceClass.DOOR @property def name(self): From bb538a9782de07090971122f2273d2bd5f68efee Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 16 Dec 2021 22:47:23 +0100 Subject: [PATCH 0629/2644] Use new enums in motion_blinds (#61931) --- .../components/motion_blinds/cover.py | 41 ++++++++----------- .../components/motion_blinds/sensor.py | 13 ++---- 2 files changed, 22 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index ad846e2f690..495c2f7078a 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -8,12 +8,7 @@ import voluptuous as vol from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, - DEVICE_CLASS_AWNING, - DEVICE_CLASS_BLIND, - DEVICE_CLASS_CURTAIN, - DEVICE_CLASS_GATE, - DEVICE_CLASS_SHADE, - DEVICE_CLASS_SHUTTER, + CoverDeviceClass, CoverEntity, ) from homeassistant.helpers import config_validation as cv, entity_platform @@ -35,29 +30,29 @@ _LOGGER = logging.getLogger(__name__) POSITION_DEVICE_MAP = { - BlindType.RollerBlind: DEVICE_CLASS_SHADE, - BlindType.RomanBlind: DEVICE_CLASS_SHADE, - BlindType.HoneycombBlind: DEVICE_CLASS_SHADE, - BlindType.DimmingBlind: DEVICE_CLASS_SHADE, - BlindType.DayNightBlind: DEVICE_CLASS_SHADE, - BlindType.RollerShutter: DEVICE_CLASS_SHUTTER, - BlindType.Switch: DEVICE_CLASS_SHUTTER, - BlindType.RollerGate: DEVICE_CLASS_GATE, - BlindType.Awning: DEVICE_CLASS_AWNING, - BlindType.Curtain: DEVICE_CLASS_CURTAIN, - BlindType.CurtainLeft: DEVICE_CLASS_CURTAIN, - BlindType.CurtainRight: DEVICE_CLASS_CURTAIN, + BlindType.RollerBlind: CoverDeviceClass.SHADE, + BlindType.RomanBlind: CoverDeviceClass.SHADE, + BlindType.HoneycombBlind: CoverDeviceClass.SHADE, + BlindType.DimmingBlind: CoverDeviceClass.SHADE, + BlindType.DayNightBlind: CoverDeviceClass.SHADE, + BlindType.RollerShutter: CoverDeviceClass.SHUTTER, + BlindType.Switch: CoverDeviceClass.SHUTTER, + BlindType.RollerGate: CoverDeviceClass.GATE, + BlindType.Awning: CoverDeviceClass.AWNING, + BlindType.Curtain: CoverDeviceClass.CURTAIN, + BlindType.CurtainLeft: CoverDeviceClass.CURTAIN, + BlindType.CurtainRight: CoverDeviceClass.CURTAIN, } TILT_DEVICE_MAP = { - BlindType.VenetianBlind: DEVICE_CLASS_BLIND, - BlindType.ShangriLaBlind: DEVICE_CLASS_BLIND, - BlindType.DoubleRoller: DEVICE_CLASS_SHADE, - BlindType.VerticalBlind: DEVICE_CLASS_BLIND, + BlindType.VenetianBlind: CoverDeviceClass.BLIND, + BlindType.ShangriLaBlind: CoverDeviceClass.BLIND, + BlindType.DoubleRoller: CoverDeviceClass.SHADE, + BlindType.VerticalBlind: CoverDeviceClass.BLIND, } TDBU_DEVICE_MAP = { - BlindType.TopDownBottomUp: DEVICE_CLASS_SHADE, + BlindType.TopDownBottomUp: CoverDeviceClass.SHADE, } diff --git a/homeassistant/components/motion_blinds/sensor.py b/homeassistant/components/motion_blinds/sensor.py index c46798d81bf..126c1607864 100644 --- a/homeassistant/components/motion_blinds/sensor.py +++ b/homeassistant/components/motion_blinds/sensor.py @@ -1,13 +1,8 @@ """Support for Motion Blinds sensors.""" from motionblinds import BlindType -from homeassistant.components.sensor import SensorEntity -from homeassistant.const import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_SIGNAL_STRENGTH, - PERCENTAGE, - SIGNAL_STRENGTH_DECIBELS_MILLIWATT, -) +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.const import PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -43,7 +38,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class MotionBatterySensor(CoordinatorEntity, SensorEntity): """Representation of a Motion Battery Sensor.""" - _attr_device_class = DEVICE_CLASS_BATTERY + _attr_device_class = SensorDeviceClass.BATTERY _attr_native_unit_of_measurement = PERCENTAGE def __init__(self, coordinator, blind): @@ -119,7 +114,7 @@ class MotionTDBUBatterySensor(MotionBatterySensor): class MotionSignalStrengthSensor(CoordinatorEntity, SensorEntity): """Representation of a Motion Signal Strength Sensor.""" - _attr_device_class = DEVICE_CLASS_SIGNAL_STRENGTH + _attr_device_class = SensorDeviceClass.SIGNAL_STRENGTH _attr_entity_registry_enabled_default = False _attr_native_unit_of_measurement = SIGNAL_STRENGTH_DECIBELS_MILLIWATT From 6778e4058e643f36017e7c0f16ce410c9e71d85d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 16 Dec 2021 22:57:15 +0100 Subject: [PATCH 0630/2644] Use attr** in lightwave (#61881) Co-authored-by: epenet --- homeassistant/components/lightwave/climate.py | 86 +++++-------------- homeassistant/components/lightwave/light.py | 47 +++------- homeassistant/components/lightwave/sensor.py | 27 +++--- homeassistant/components/lightwave/switch.py | 28 ++---- 4 files changed, 49 insertions(+), 139 deletions(-) diff --git a/homeassistant/components/lightwave/climate.py b/homeassistant/components/lightwave/climate.py index 44b1e29ff34..9a3b08869a4 100644 --- a/homeassistant/components/lightwave/climate.py +++ b/homeassistant/components/lightwave/climate.py @@ -32,57 +32,46 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class LightwaveTrv(ClimateEntity): """Representation of a LightWaveRF TRV.""" + _attr_hvac_mode = HVAC_MODE_HEAT + _attr_hvac_modes = [HVAC_MODE_HEAT, HVAC_MODE_OFF] + _attr_min_temp = DEFAULT_MIN_TEMP + _attr_max_temp = DEFAULT_MAX_TEMP + _attr_supported_features = SUPPORT_TARGET_TEMPERATURE + _attr_target_temperature_step = 0.5 + _attr_temperature_unit = TEMP_CELSIUS + def __init__(self, name, device_id, lwlink, serial): """Initialize LightwaveTrv entity.""" - self._name = name + self._attr_name = name self._device_id = device_id - self._state = None - self._current_temperature = None - self._target_temperature = None - self._hvac_action = None self._lwlink = lwlink self._serial = serial self._attr_unique_id = f"{serial}-trv" # inhibit is used to prevent race condition on update. If non zero, skip next update cycle. self._inhibit = 0 - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_TARGET_TEMPERATURE - def update(self): """Communicate with a Lightwave RTF Proxy to get state.""" (temp, targ, _, trv_output) = self._lwlink.read_trv_status(self._serial) if temp is not None: - self._current_temperature = temp + self._attr_current_temperature = temp if targ is not None: if self._inhibit == 0: - self._target_temperature = targ + self._attr_target_temperature = targ if targ == 0: # TRV off - self._target_temperature = None + self._attr_target_temperature = None if targ >= 40: # Call for heat mode, or TRV in a fixed position - self._target_temperature = None + self._attr_target_temperature = None else: # Done the job - use proxy next iteration self._inhibit = 0 if trv_output is not None: if trv_output > 0: - self._hvac_action = CURRENT_HVAC_HEAT + self._attr_hvac_action = CURRENT_HVAC_HEAT else: - self._hvac_action = CURRENT_HVAC_OFF - - @property - def name(self): - """Lightwave trv name.""" - return self._name - - @property - def current_temperature(self): - """Property giving the current room temperature.""" - return self._current_temperature + self._attr_hvac_action = CURRENT_HVAC_OFF @property def target_temperature(self): @@ -92,51 +81,16 @@ class LightwaveTrv(ClimateEntity): # propagated, the target temp is set back to the # old target on the next poll, showing a false # reading temporarily. - self._target_temperature = self._inhibit - return self._target_temperature - - @property - def hvac_modes(self): - """HVAC modes.""" - return [HVAC_MODE_HEAT, HVAC_MODE_OFF] - - @property - def hvac_mode(self): - """HVAC mode.""" - return HVAC_MODE_HEAT - - @property - def hvac_action(self): - """HVAC action.""" - return self._hvac_action - - @property - def min_temp(self): - """Min Temp.""" - return DEFAULT_MIN_TEMP - - @property - def max_temp(self): - """Max Temp.""" - return DEFAULT_MAX_TEMP - - @property - def temperature_unit(self): - """Set temperature unit.""" - return TEMP_CELSIUS - - @property - def target_temperature_step(self): - """Set temperature step.""" - return 0.5 + self._attr_target_temperature = self._inhibit + return self._attr_target_temperature def set_temperature(self, **kwargs): """Set TRV target temperature.""" if ATTR_TEMPERATURE in kwargs: - self._target_temperature = kwargs[ATTR_TEMPERATURE] - self._inhibit = self._target_temperature + self._attr_target_temperature = kwargs[ATTR_TEMPERATURE] + self._inhibit = self._attr_target_temperature self._lwlink.set_temperature( - self._device_id, self._target_temperature, self._name + self._device_id, self._attr_target_temperature, self._attr_name ) async def async_set_hvac_mode(self, hvac_mode): diff --git a/homeassistant/components/lightwave/light.py b/homeassistant/components/lightwave/light.py index d441e80b4fa..51952633c6a 100644 --- a/homeassistant/components/lightwave/light.py +++ b/homeassistant/components/lightwave/light.py @@ -29,57 +29,34 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class LWRFLight(LightEntity): """Representation of a LightWaveRF light.""" + _attr_supported_features = SUPPORT_BRIGHTNESS + _attr_should_poll = False + def __init__(self, name, device_id, lwlink): """Initialize LWRFLight entity.""" - self._name = name + self._attr_name = name self._device_id = device_id - self._state = None - self._brightness = MAX_BRIGHTNESS + self._attr_brightness = MAX_BRIGHTNESS self._lwlink = lwlink - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_BRIGHTNESS - - @property - def should_poll(self): - """No polling needed for a LightWave light.""" - return False - - @property - def name(self): - """Lightwave light name.""" - return self._name - - @property - def brightness(self): - """Brightness of this light between 0..MAX_BRIGHTNESS.""" - return self._brightness - - @property - def is_on(self): - """Lightwave light is on state.""" - return self._state - async def async_turn_on(self, **kwargs): """Turn the LightWave light on.""" - self._state = True + self._attr_is_on = True if ATTR_BRIGHTNESS in kwargs: - self._brightness = kwargs[ATTR_BRIGHTNESS] + self._attr_brightness = kwargs[ATTR_BRIGHTNESS] - if self._brightness != MAX_BRIGHTNESS: + if self._attr_brightness != MAX_BRIGHTNESS: self._lwlink.turn_on_with_brightness( - self._device_id, self._name, self._brightness + self._device_id, self._attr_name, self._attr_brightness ) else: - self._lwlink.turn_on_light(self._device_id, self._name) + self._lwlink.turn_on_light(self._device_id, self._attr_name) self.async_write_ha_state() async def async_turn_off(self, **kwargs): """Turn the LightWave light off.""" - self._state = False - self._lwlink.turn_off(self._device_id, self._name) + self._attr_is_on = False + self._lwlink.turn_off(self._device_id, self._attr_name) self.async_write_ha_state() diff --git a/homeassistant/components/lightwave/sensor.py b/homeassistant/components/lightwave/sensor.py index 369256ce403..60117b3c2d4 100644 --- a/homeassistant/components/lightwave/sensor.py +++ b/homeassistant/components/lightwave/sensor.py @@ -1,6 +1,10 @@ """Support for LightwaveRF TRV - Associated Battery.""" -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity -from homeassistant.const import CONF_NAME, DEVICE_CLASS_BATTERY, PERCENTAGE +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorStateClass, +) +from homeassistant.const import CONF_NAME, PERCENTAGE from . import CONF_SERIAL, LIGHTWAVE_LINK @@ -25,31 +29,20 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class LightwaveBattery(SensorEntity): """Lightwave TRV Battery.""" - _attr_device_class = DEVICE_CLASS_BATTERY + _attr_device_class = SensorDeviceClass.BATTERY _attr_native_unit_of_measurement = PERCENTAGE - _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_state_class = SensorStateClass.MEASUREMENT def __init__(self, name, lwlink, serial): """Initialize the Lightwave Trv battery sensor.""" - self._name = name - self._state = None + self._attr_name = name self._lwlink = lwlink self._serial = serial self._attr_unique_id = f"{serial}-trv-battery" - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state - def update(self): """Communicate with a Lightwave RTF Proxy to get state.""" (dummy_temp, dummy_targ, battery, dummy_output) = self._lwlink.read_trv_status( self._serial ) - self._state = battery + self._attr_native_value = battery diff --git a/homeassistant/components/lightwave/switch.py b/homeassistant/components/lightwave/switch.py index 7fa075a0834..c907128308a 100644 --- a/homeassistant/components/lightwave/switch.py +++ b/homeassistant/components/lightwave/switch.py @@ -23,36 +23,22 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class LWRFSwitch(SwitchEntity): """Representation of a LightWaveRF switch.""" + _attr_should_poll = False + def __init__(self, name, device_id, lwlink): """Initialize LWRFSwitch entity.""" - self._name = name + self._attr_name = name self._device_id = device_id - self._state = None self._lwlink = lwlink - @property - def should_poll(self): - """No polling needed for a LightWave light.""" - return False - - @property - def name(self): - """Lightwave switch name.""" - return self._name - - @property - def is_on(self): - """Lightwave switch is on state.""" - return self._state - async def async_turn_on(self, **kwargs): """Turn the LightWave switch on.""" - self._state = True - self._lwlink.turn_on_switch(self._device_id, self._name) + self._attr_is_on = True + self._lwlink.turn_on_switch(self._device_id, self._attr_name) self.async_write_ha_state() async def async_turn_off(self, **kwargs): """Turn the LightWave switch off.""" - self._state = False - self._lwlink.turn_off(self._device_id, self._name) + self._attr_is_on = False + self._lwlink.turn_off(self._device_id, self._attr_name) self.async_write_ha_state() From 98e1b7c95de1a44143236c195e51cbd0c18c6945 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 16 Dec 2021 22:59:05 +0100 Subject: [PATCH 0631/2644] Use attr** in linux-battery (#61883) Co-authored-by: epenet --- .../components/linux_battery/sensor.py | 34 ++++++------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/linux_battery/sensor.py b/homeassistant/components/linux_battery/sensor.py index 18f1c81e368..e78a8a58525 100644 --- a/homeassistant/components/linux_battery/sensor.py +++ b/homeassistant/components/linux_battery/sensor.py @@ -5,8 +5,12 @@ import os from batinfo import Batteries import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import ATTR_NAME, CONF_NAME, DEVICE_CLASS_BATTERY, PERCENTAGE +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorDeviceClass, + SensorEntity, +) +from homeassistant.const import ATTR_NAME, CONF_NAME, PERCENTAGE import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -70,35 +74,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class LinuxBatterySensor(SensorEntity): """Representation of a Linux Battery sensor.""" + _attr_device_class = SensorDeviceClass.BATTERY + _attr_native_unit_of_measurement = PERCENTAGE + def __init__(self, name, battery_id, system): """Initialize the battery sensor.""" self._battery = Batteries() - self._name = name + self._attr_name = name self._battery_stat = None self._battery_id = battery_id - 1 self._system = system - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def device_class(self): - """Return the device class of the sensor.""" - return DEVICE_CLASS_BATTERY - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._battery_stat.capacity - - @property - def native_unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return PERCENTAGE - @property def extra_state_attributes(self): """Return the state attributes of the sensor.""" @@ -131,3 +118,4 @@ class LinuxBatterySensor(SensorEntity): """Get the latest data and updates the states.""" self._battery.update() self._battery_stat = self._battery.stat[self._battery_id] + self._attr_native_value = self._battery_stat.capacity From d26454a313dda721d8f95920f64a917b15330745 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 16 Dec 2021 22:59:29 +0100 Subject: [PATCH 0632/2644] Use _attr_attribution in flipr (#61889) Co-authored-by: epenet --- homeassistant/components/flipr/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/flipr/__init__.py b/homeassistant/components/flipr/__init__.py index 6294d56d850..8379845982a 100644 --- a/homeassistant/components/flipr/__init__.py +++ b/homeassistant/components/flipr/__init__.py @@ -5,7 +5,7 @@ import logging from flipr_api import FliprAPIRestClient from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION, CONF_EMAIL, CONF_PASSWORD, Platform +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import ( @@ -76,7 +76,7 @@ class FliprDataUpdateCoordinator(DataUpdateCoordinator): class FliprEntity(CoordinatorEntity): """Implements a common class elements representing the Flipr component.""" - _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + _attr_attribution = ATTRIBUTION def __init__( self, coordinator: DataUpdateCoordinator, description: EntityDescription From 863a139b6f709038be44eb36a84296ef3fe65d79 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 16 Dec 2021 23:00:04 +0100 Subject: [PATCH 0633/2644] Use _attr_attribution in goalzero (#61890) Co-authored-by: epenet --- homeassistant/components/goalzero/__init__.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/goalzero/__init__.py b/homeassistant/components/goalzero/__init__.py index 6e527adb485..e014c4780ad 100644 --- a/homeassistant/components/goalzero/__init__.py +++ b/homeassistant/components/goalzero/__init__.py @@ -6,13 +6,7 @@ import logging from goalzero import Yeti, exceptions from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_ATTRIBUTION, - ATTR_MODEL, - CONF_HOST, - CONF_NAME, - Platform, -) +from homeassistant.const import ATTR_MODEL, CONF_HOST, CONF_NAME, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr @@ -87,7 +81,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class YetiEntity(CoordinatorEntity): """Representation of a Goal Zero Yeti entity.""" - _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + _attr_attribution = ATTRIBUTION def __init__( self, From e20029d87ffc8a46ec4fed90bbb03a520792ac8b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 16 Dec 2021 23:01:31 +0100 Subject: [PATCH 0634/2644] Use attr** in meteoalarm (#61895) Co-authored-by: epenet --- .../components/meteoalarm/binary_sensor.py | 40 +++++-------------- 1 file changed, 10 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/meteoalarm/binary_sensor.py b/homeassistant/components/meteoalarm/binary_sensor.py index 079747afd3b..38979bfe5b2 100644 --- a/homeassistant/components/meteoalarm/binary_sensor.py +++ b/homeassistant/components/meteoalarm/binary_sensor.py @@ -6,11 +6,11 @@ from meteoalertapi import Meteoalert import voluptuous as vol from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_SAFETY, PLATFORM_SCHEMA, + BinarySensorDeviceClass, BinarySensorEntity, ) -from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME +from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util @@ -56,43 +56,23 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class MeteoAlertBinarySensor(BinarySensorEntity): """Representation of a MeteoAlert binary sensor.""" + _attr_attribution = ATTRIBUTION + _attr_device_class = BinarySensorDeviceClass.SAFETY + def __init__(self, api, name): """Initialize the MeteoAlert binary sensor.""" - self._name = name - self._attributes = {} - self._state = None + self._attr_name = name self._api = api - @property - def name(self): - """Return the name of the binary sensor.""" - return self._name - - @property - def is_on(self): - """Return the status of the binary sensor.""" - return self._state - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - self._attributes[ATTR_ATTRIBUTION] = ATTRIBUTION - return self._attributes - - @property - def device_class(self): - """Return the device class of this binary sensor.""" - return DEVICE_CLASS_SAFETY - def update(self): """Update device state.""" - self._attributes = {} - self._state = False + self._attr_extra_state_attributes = None + self._attr_is_on = False if alert := self._api.get_alert(): expiration_date = dt_util.parse_datetime(alert["expires"]) now = dt_util.utcnow() if expiration_date > now: - self._attributes = alert - self._state = True + self._attr_extra_state_attributes = alert + self._attr_is_on = True From 61cdc04f3ff2eb32c30ada0a458f19fd3d47b3ed Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 16 Dec 2021 23:03:37 +0100 Subject: [PATCH 0635/2644] Use new enums in nanoleaf (#61938) --- homeassistant/components/nanoleaf/button.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nanoleaf/button.py b/homeassistant/components/nanoleaf/button.py index bf7f9fba9f7..2d9388196c1 100644 --- a/homeassistant/components/nanoleaf/button.py +++ b/homeassistant/components/nanoleaf/button.py @@ -4,8 +4,8 @@ from aionanoleaf import Nanoleaf from homeassistant.components.button import ButtonEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import NanoleafEntryData @@ -30,7 +30,7 @@ class NanoleafIdentifyButton(NanoleafEntity, ButtonEntity): self._attr_unique_id = f"{nanoleaf.serial_no}_identify" self._attr_name = f"Identify {nanoleaf.name}" self._attr_icon = "mdi:magnify" - self._attr_entity_category = ENTITY_CATEGORY_CONFIG + self._attr_entity_category = EntityCategory.CONFIG async def async_press(self) -> None: """Identify the Nanoleaf.""" From 42c7c38515be0438ea2fd22c4800cc197c4dda66 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 16 Dec 2021 23:05:34 +0100 Subject: [PATCH 0636/2644] Use new enums in nam (#61940) --- homeassistant/components/nam/button.py | 4 +- homeassistant/components/nam/const.py | 102 ++++++++++++------------- 2 files changed, 49 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/nam/button.py b/homeassistant/components/nam/button.py index 73b5169abcd..f73307d09ce 100644 --- a/homeassistant/components/nam/button.py +++ b/homeassistant/components/nam/button.py @@ -9,8 +9,8 @@ from homeassistant.components.button import ( ButtonEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -25,7 +25,7 @@ RESTART_BUTTON: ButtonEntityDescription = ButtonEntityDescription( key="restart", name=f"{DEFAULT_NAME} Restart", device_class=ButtonDeviceClass.RESTART, - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ) diff --git a/homeassistant/components/nam/const.py b/homeassistant/components/nam/const.py index 8f1ba356fc9..b81e7337a9f 100644 --- a/homeassistant/components/nam/const.py +++ b/homeassistant/components/nam/const.py @@ -5,27 +5,19 @@ from datetime import timedelta from typing import Final from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, + SensorDeviceClass, SensorEntityDescription, + SensorStateClass, ) from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_MILLION, - DEVICE_CLASS_CO2, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_PM1, - DEVICE_CLASS_PM10, - DEVICE_CLASS_PM25, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_SIGNAL_STRENGTH, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_TIMESTAMP, - ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, PRESSURE_HPA, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, TEMP_CELSIUS, ) +from homeassistant.helpers.entity import EntityCategory SUFFIX_P0: Final = "_p0" SUFFIX_P1: Final = "_p1" @@ -72,156 +64,156 @@ SENSORS: Final[tuple[SensorEntityDescription, ...]] = ( key=ATTR_BME280_HUMIDITY, name=f"{DEFAULT_NAME} BME280 Humidity", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_BME280_PRESSURE, name=f"{DEFAULT_NAME} BME280 Pressure", native_unit_of_measurement=PRESSURE_HPA, - device_class=DEVICE_CLASS_PRESSURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_BME280_TEMPERATURE, name=f"{DEFAULT_NAME} BME280 Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_BMP180_PRESSURE, name=f"{DEFAULT_NAME} BMP180 Pressure", native_unit_of_measurement=PRESSURE_HPA, - device_class=DEVICE_CLASS_PRESSURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_BMP180_TEMPERATURE, name=f"{DEFAULT_NAME} BMP180 Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_BMP280_PRESSURE, name=f"{DEFAULT_NAME} BMP280 Pressure", native_unit_of_measurement=PRESSURE_HPA, - device_class=DEVICE_CLASS_PRESSURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_BMP280_TEMPERATURE, name=f"{DEFAULT_NAME} BMP280 Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_HECA_HUMIDITY, name=f"{DEFAULT_NAME} HECA Humidity", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_HECA_TEMPERATURE, name=f"{DEFAULT_NAME} HECA Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_MHZ14A_CARBON_DIOXIDE, name=f"{DEFAULT_NAME} MH-Z14A Carbon Dioxide", native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, - device_class=DEVICE_CLASS_CO2, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CO2, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_SDS011_P1, name=f"{DEFAULT_NAME} SDS011 Particulate Matter 10", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - device_class=DEVICE_CLASS_PM10, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.PM10, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_SDS011_P2, name=f"{DEFAULT_NAME} SDS011 Particulate Matter 2.5", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - device_class=DEVICE_CLASS_PM25, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.PM25, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_SHT3X_HUMIDITY, name=f"{DEFAULT_NAME} SHT3X Humidity", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_SHT3X_TEMPERATURE, name=f"{DEFAULT_NAME} SHT3X Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_SPS30_P0, name=f"{DEFAULT_NAME} SPS30 Particulate Matter 1.0", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - device_class=DEVICE_CLASS_PM1, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.PM1, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_SPS30_P1, name=f"{DEFAULT_NAME} SPS30 Particulate Matter 10", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - device_class=DEVICE_CLASS_PM10, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.PM10, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_SPS30_P2, name=f"{DEFAULT_NAME} SPS30 Particulate Matter 2.5", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - device_class=DEVICE_CLASS_PM25, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.PM25, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_SPS30_P4, name=f"{DEFAULT_NAME} SPS30 Particulate Matter 4.0", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, icon="mdi:molecule", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_DHT22_HUMIDITY, name=f"{DEFAULT_NAME} DHT22 Humidity", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_DHT22_TEMPERATURE, name=f"{DEFAULT_NAME} DHT22 Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=ATTR_SIGNAL_STRENGTH, name=f"{DEFAULT_NAME} Signal Strength", native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_UPTIME, name=f"{DEFAULT_NAME} Uptime", - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, entity_registry_enabled_default=False, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), ) From dce9d551f88604c91475f76d42514704f4e55fb4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 16 Dec 2021 23:13:26 +0100 Subject: [PATCH 0637/2644] Use new SensorDeviceClass in juicenet (#61828) --- homeassistant/components/juicenet/sensor.py | 27 +++++++++------------ 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/juicenet/sensor.py b/homeassistant/components/juicenet/sensor.py index 0b39bcd3507..02b0c62a9e8 100644 --- a/homeassistant/components/juicenet/sensor.py +++ b/homeassistant/components/juicenet/sensor.py @@ -2,17 +2,12 @@ from __future__ import annotations from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.const import ( - DEVICE_CLASS_CURRENT, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_VOLTAGE, ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_WATT_HOUR, @@ -33,28 +28,28 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="temperature", name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="voltage", name="Voltage", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, + device_class=SensorDeviceClass.VOLTAGE, ), SensorEntityDescription( key="amps", name="Amps", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="watts", name="Watts", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="charge_time", @@ -66,8 +61,8 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="energy_added", name="Energy added", native_unit_of_measurement=ENERGY_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), ) From 72462b5dd146c07a63c225d804241d5d765ed1c1 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 17:24:46 -0500 Subject: [PATCH 0638/2644] Use enums in satel_integra (#62048) --- homeassistant/components/satel_integra/binary_sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/satel_integra/binary_sensor.py b/homeassistant/components/satel_integra/binary_sensor.py index 5acc49d1d1b..0e9688f0d37 100644 --- a/homeassistant/components/satel_integra/binary_sensor.py +++ b/homeassistant/components/satel_integra/binary_sensor.py @@ -1,6 +1,6 @@ """Support for Satel Integra zone states- represented as binary sensors.""" from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_SMOKE, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.core import callback @@ -88,7 +88,7 @@ class SatelIntegraBinarySensor(BinarySensorEntity): @property def icon(self): """Icon for device by its type.""" - if self._zone_type == DEVICE_CLASS_SMOKE: + if self._zone_type is BinarySensorDeviceClass.SMOKE: return "mdi:fire" @property From 2f3f64c339c6269d57d8c2e7382ce62f1dfcab56 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Thu, 16 Dec 2021 23:25:34 +0100 Subject: [PATCH 0639/2644] Use DeviceClass Enum in KNX schema (#61960) --- homeassistant/components/knx/schema.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index 4b7105f15f1..af475e9c380 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -14,10 +14,12 @@ from xknx.io import DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT from xknx.telegram.address import IndividualAddress, parse_device_group_address from homeassistant.components.binary_sensor import ( - DEVICE_CLASSES as BINARY_SENSOR_DEVICE_CLASSES, + DEVICE_CLASSES_SCHEMA as BINARY_SENSOR_DEVICE_CLASSES_SCHEMA, ) from homeassistant.components.climate.const import HVAC_MODE_HEAT, HVAC_MODES -from homeassistant.components.cover import DEVICE_CLASSES as COVER_DEVICE_CLASSES +from homeassistant.components.cover import ( + DEVICE_CLASSES_SCHEMA as COVER_DEVICE_CLASSES_SCHEMA, +) from homeassistant.components.number import NumberMode from homeassistant.components.sensor import CONF_STATE_CLASS, STATE_CLASSES_SCHEMA from homeassistant.const import ( @@ -316,7 +318,7 @@ class BinarySensorSchema(KNXPlatformSchema): vol.Optional(CONF_CONTEXT_TIMEOUT): vol.All( vol.Coerce(float), vol.Range(min=0, max=10) ), - vol.Optional(CONF_DEVICE_CLASS): vol.In(BINARY_SENSOR_DEVICE_CLASSES), + vol.Optional(CONF_DEVICE_CLASS): BINARY_SENSOR_DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_RESET_AFTER): cv.positive_float, vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, } @@ -552,7 +554,7 @@ class CoverSchema(KNXPlatformSchema): ): cv.positive_float, vol.Optional(CONF_INVERT_POSITION, default=False): cv.boolean, vol.Optional(CONF_INVERT_ANGLE, default=False): cv.boolean, - vol.Optional(CONF_DEVICE_CLASS): vol.In(COVER_DEVICE_CLASSES), + vol.Optional(CONF_DEVICE_CLASS): COVER_DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, } ), From 16e152b7972504ce952ce150b4def0ce11a49993 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Thu, 16 Dec 2021 22:26:23 +0000 Subject: [PATCH 0640/2644] Use DeviceClass Enums in axis tests (#62096) --- tests/components/axis/test_binary_sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/axis/test_binary_sensor.py b/tests/components/axis/test_binary_sensor.py index 2429ec61855..0fe862263ff 100644 --- a/tests/components/axis/test_binary_sensor.py +++ b/tests/components/axis/test_binary_sensor.py @@ -2,8 +2,8 @@ from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_MOTION, DOMAIN as BINARY_SENSOR_DOMAIN, + BinarySensorDeviceClass, ) from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component @@ -63,9 +63,9 @@ async def test_binary_sensors(hass, mock_rtsp_event): pir = hass.states.get(f"{BINARY_SENSOR_DOMAIN}.{NAME}_pir_0") assert pir.state == STATE_OFF assert pir.name == f"{NAME} PIR 0" - assert pir.attributes["device_class"] == DEVICE_CLASS_MOTION + assert pir.attributes["device_class"] == BinarySensorDeviceClass.MOTION vmd4 = hass.states.get(f"{BINARY_SENSOR_DOMAIN}.{NAME}_vmd4_profile_1") assert vmd4.state == STATE_ON assert vmd4.name == f"{NAME} VMD4 Profile 1" - assert vmd4.attributes["device_class"] == DEVICE_CLASS_MOTION + assert vmd4.attributes["device_class"] == BinarySensorDeviceClass.MOTION From da60680b2f1cd3683e50395b98d3b42af03f2fd1 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 17:26:53 -0500 Subject: [PATCH 0641/2644] Use enums in rachio (#62056) --- homeassistant/components/rachio/binary_sensor.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/rachio/binary_sensor.py b/homeassistant/components/rachio/binary_sensor.py index 495001e39b9..e50fea28e56 100644 --- a/homeassistant/components/rachio/binary_sensor.py +++ b/homeassistant/components/rachio/binary_sensor.py @@ -3,8 +3,7 @@ from abc import abstractmethod import logging from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_CONNECTIVITY, - DEVICE_CLASS_MOISTURE, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.core import callback @@ -89,9 +88,9 @@ class RachioControllerOnlineBinarySensor(RachioControllerBinarySensor): return f"{self._controller.controller_id}-online" @property - def device_class(self) -> str: - """Return the class of this device, from component DEVICE_CLASSES.""" - return DEVICE_CLASS_CONNECTIVITY + def device_class(self) -> BinarySensorDeviceClass: + """Return the class of this device, from BinarySensorDeviceClass.""" + return BinarySensorDeviceClass.CONNECTIVITY @property def icon(self) -> str: @@ -138,9 +137,9 @@ class RachioRainSensor(RachioControllerBinarySensor): return f"{self._controller.controller_id}-rain_sensor" @property - def device_class(self) -> str: + def device_class(self) -> BinarySensorDeviceClass: """Return the class of this device.""" - return DEVICE_CLASS_MOISTURE + return BinarySensorDeviceClass.MOISTURE @property def icon(self) -> str: From b7ece5ae00518fa3fe9a9901d1b8b1e5b01307bc Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 16 Dec 2021 17:27:15 -0500 Subject: [PATCH 0642/2644] Use enums in pvpc_hourly_pricing (#62076) --- homeassistant/components/pvpc_hourly_pricing/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/pvpc_hourly_pricing/sensor.py b/homeassistant/components/pvpc_hourly_pricing/sensor.py index 1000d23b5bf..544c02571c2 100644 --- a/homeassistant/components/pvpc_hourly_pricing/sensor.py +++ b/homeassistant/components/pvpc_hourly_pricing/sensor.py @@ -7,7 +7,7 @@ from typing import Any from aiopvpc import PVPCData -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity +from homeassistant.components.sensor import SensorEntity, SensorStateClass from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, CURRENCY_EURO, ENERGY_KILO_WATT_HOUR from homeassistant.core import HomeAssistant, callback @@ -54,7 +54,7 @@ class ElecPriceSensor(RestoreEntity, SensorEntity): _attr_icon = ICON _attr_native_unit_of_measurement = UNIT _attr_should_poll = False - _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_state_class = SensorStateClass.MEASUREMENT def __init__(self, name, unique_id, pvpc_data_handler): """Initialize the sensor object.""" From 61f2f9d9ac54d30e06baeb31029aaaf82fa91034 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Thu, 16 Dec 2021 22:27:44 +0000 Subject: [PATCH 0643/2644] Use DeviceClass Enums in devolo_home_control tests (#62116) --- .../devolo_home_control/test_binary_sensor.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/tests/components/devolo_home_control/test_binary_sensor.py b/tests/components/devolo_home_control/test_binary_sensor.py index 9b13220ad7c..4bce2ebd19e 100644 --- a/tests/components/devolo_home_control/test_binary_sensor.py +++ b/tests/components/devolo_home_control/test_binary_sensor.py @@ -4,14 +4,10 @@ from unittest.mock import patch import pytest from homeassistant.components.binary_sensor import DOMAIN -from homeassistant.const import ( - ENTITY_CATEGORY_DIAGNOSTIC, - STATE_OFF, - STATE_ON, - STATE_UNAVAILABLE, -) +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry +from homeassistant.helpers.entity import EntityCategory from . import configure_integration from .mocks import ( @@ -42,9 +38,7 @@ async def test_binary_sensor(hass: HomeAssistant): state = hass.states.get(f"{DOMAIN}.test_2") assert state is not None er = entity_registry.async_get(hass) - assert ( - er.async_get(f"{DOMAIN}.test_2").entity_category == ENTITY_CATEGORY_DIAGNOSTIC - ) + assert er.async_get(f"{DOMAIN}.test_2").entity_category == EntityCategory.DIAGNOSTIC # Emulate websocket message: sensor turned on test_gateway.publisher.dispatch("Test", ("Test", True)) From f02e9eb70f9ed8babaf2485baf35773559572ee1 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Thu, 16 Dec 2021 22:34:07 +0000 Subject: [PATCH 0644/2644] Use DeviceClass Enums in directv tests (#62118) --- tests/components/directv/test_media_player.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/components/directv/test_media_player.py b/tests/components/directv/test_media_player.py index 41431748d36..cfade8bc4e7 100644 --- a/tests/components/directv/test_media_player.py +++ b/tests/components/directv/test_media_player.py @@ -12,7 +12,7 @@ from homeassistant.components.directv.media_player import ( ATTR_MEDIA_RECORDED, ATTR_MEDIA_START_TIME, ) -from homeassistant.components.media_player import DEVICE_CLASS_RECEIVER +from homeassistant.components.media_player import MediaPlayerDeviceClass from homeassistant.components.media_player.const import ( ATTR_INPUT_SOURCE, ATTR_MEDIA_ALBUM_NAME, @@ -160,15 +160,15 @@ async def test_unique_id( entity_registry = er.async_get(hass) main = entity_registry.async_get(MAIN_ENTITY_ID) - assert main.original_device_class == DEVICE_CLASS_RECEIVER + assert main.original_device_class == MediaPlayerDeviceClass.RECEIVER assert main.unique_id == "028877455858" client = entity_registry.async_get(CLIENT_ENTITY_ID) - assert client.original_device_class == DEVICE_CLASS_RECEIVER + assert client.original_device_class == MediaPlayerDeviceClass.RECEIVER assert client.unique_id == "2CA17D1CD30X" unavailable_client = entity_registry.async_get(UNAVAILABLE_ENTITY_ID) - assert unavailable_client.original_device_class == DEVICE_CLASS_RECEIVER + assert unavailable_client.original_device_class == MediaPlayerDeviceClass.RECEIVER assert unavailable_client.unique_id == "9XXXXXXXXXX9" From f92baffd090cc53b59b73fbeb5af371b5317bc47 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Thu, 16 Dec 2021 22:40:24 +0000 Subject: [PATCH 0645/2644] Use DeviceClass Enums in energy tests (#62122) --- tests/components/energy/test_sensor.py | 44 ++++++++++++-------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/tests/components/energy/test_sensor.py b/tests/components/energy/test_sensor.py index a2021af3e2a..3971df86862 100644 --- a/tests/components/energy/test_sensor.py +++ b/tests/components/energy/test_sensor.py @@ -9,15 +9,13 @@ from homeassistant.components.energy import data from homeassistant.components.sensor import ( ATTR_LAST_RESET, ATTR_STATE_CLASS, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, + SensorStateClass, ) from homeassistant.components.sensor.recorder import compile_statistics from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, - DEVICE_CLASS_MONETARY, ENERGY_KILO_WATT_HOUR, ENERGY_MEGA_WATT_HOUR, ENERGY_WATT_HOUR, @@ -108,7 +106,7 @@ async def test_cost_sensor_price_entity_total_increasing( energy_attributes = { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, } await async_init_recorder_component(hass) @@ -164,10 +162,10 @@ async def test_cost_sensor_price_entity_total_increasing( state = hass.states.get(cost_sensor_entity_id) assert state.state == initial_cost - assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_MONETARY + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.MONETARY if initial_cost != "unknown": assert state.attributes[ATTR_LAST_RESET] == last_reset_cost_sensor - assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_TOTAL + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.TOTAL assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "EUR" # Optional late setup of dependent entities @@ -182,9 +180,9 @@ async def test_cost_sensor_price_entity_total_increasing( state = hass.states.get(cost_sensor_entity_id) assert state.state == "0.0" - assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_MONETARY + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.MONETARY assert state.attributes[ATTR_LAST_RESET] == last_reset_cost_sensor - assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_TOTAL + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.TOTAL assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "EUR" # # Unique ID temp disabled @@ -369,10 +367,10 @@ async def test_cost_sensor_price_entity_total( state = hass.states.get(cost_sensor_entity_id) assert state.state == initial_cost - assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_MONETARY + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.MONETARY if initial_cost != "unknown": assert state.attributes[ATTR_LAST_RESET] == last_reset_cost_sensor - assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_TOTAL + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.TOTAL assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "EUR" # Optional late setup of dependent entities @@ -387,9 +385,9 @@ async def test_cost_sensor_price_entity_total( state = hass.states.get(cost_sensor_entity_id) assert state.state == "0.0" - assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_MONETARY + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.MONETARY assert state.attributes[ATTR_LAST_RESET] == last_reset_cost_sensor - assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_TOTAL + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.TOTAL assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "EUR" # # Unique ID temp disabled @@ -574,10 +572,10 @@ async def test_cost_sensor_price_entity_total_no_reset( state = hass.states.get(cost_sensor_entity_id) assert state.state == initial_cost - assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_MONETARY + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.MONETARY if initial_cost != "unknown": assert state.attributes[ATTR_LAST_RESET] == last_reset_cost_sensor - assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_TOTAL + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.TOTAL assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "EUR" # Optional late setup of dependent entities @@ -592,9 +590,9 @@ async def test_cost_sensor_price_entity_total_no_reset( state = hass.states.get(cost_sensor_entity_id) assert state.state == "0.0" - assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_MONETARY + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.MONETARY assert state.attributes[ATTR_LAST_RESET] == last_reset_cost_sensor - assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_TOTAL + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.TOTAL assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "EUR" # # Unique ID temp disabled @@ -677,7 +675,7 @@ async def test_cost_sensor_handle_energy_units( """Test energy cost price from sensor entity.""" energy_attributes = { ATTR_UNIT_OF_MEASUREMENT: energy_unit, - ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, } energy_data = data.EnergyManager.default_preferences() energy_data["energy_sources"].append( @@ -743,11 +741,11 @@ async def test_cost_sensor_handle_price_units( """Test energy cost price from sensor entity.""" energy_attributes = { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, } price_attributes = { ATTR_UNIT_OF_MEASUREMENT: price_unit, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, } energy_data = data.EnergyManager.default_preferences() energy_data["energy_sources"].append( @@ -804,7 +802,7 @@ async def test_cost_sensor_handle_gas(hass, hass_storage) -> None: """Test gas cost price from sensor entity.""" energy_attributes = { ATTR_UNIT_OF_MEASUREMENT: VOLUME_CUBIC_METERS, - ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, } energy_data = data.EnergyManager.default_preferences() energy_data["energy_sources"].append( @@ -853,7 +851,7 @@ async def test_cost_sensor_handle_gas_kwh(hass, hass_storage) -> None: """Test gas cost price from sensor entity.""" energy_attributes = { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, } energy_data = data.EnergyManager.default_preferences() energy_data["energy_sources"].append( @@ -960,7 +958,7 @@ async def test_cost_sensor_wrong_state_class( assert state.state == STATE_UNKNOWN -@pytest.mark.parametrize("state_class", [STATE_CLASS_MEASUREMENT]) +@pytest.mark.parametrize("state_class", [SensorStateClass.MEASUREMENT]) async def test_cost_sensor_state_class_measurement_no_reset( hass, hass_storage, caplog, state_class ) -> None: From 82e11aeccabe70c7bb73a592c63ce353a63ede92 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Thu, 16 Dec 2021 22:40:58 +0000 Subject: [PATCH 0646/2644] Use DeviceClass Enums in efergy tests (#62119) --- tests/components/efergy/test_sensor.py | 59 ++++++++++++-------------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/tests/components/efergy/test_sensor.py b/tests/components/efergy/test_sensor.py index a4b0d4ab238..4df383a5487 100644 --- a/tests/components/efergy/test_sensor.py +++ b/tests/components/efergy/test_sensor.py @@ -5,15 +5,12 @@ from homeassistant.components.efergy.sensor import SENSOR_TYPES from homeassistant.components.sensor import ( ATTR_STATE_CLASS, DOMAIN as SENSOR_DOMAIN, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, + SensorStateClass, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_MONETARY, - DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR, POWER_WATT, STATE_UNAVAILABLE, @@ -40,9 +37,9 @@ async def test_sensor_readings( state = hass.states.get("sensor.power_usage") assert state.state == "1580" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT state = hass.states.get("sensor.energy_budget") assert state.state == "ok" assert state.attributes.get(ATTR_DEVICE_CLASS) is None @@ -50,44 +47,44 @@ async def test_sensor_readings( assert state.attributes.get(ATTR_STATE_CLASS) is None state = hass.states.get("sensor.daily_consumption") assert state.state == "38.21" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING state = hass.states.get("sensor.weekly_consumption") assert state.state == "267.47" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING state = hass.states.get("sensor.monthly_consumption") assert state.state == "1069.88" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING state = hass.states.get("sensor.yearly_consumption") assert state.state == "13373.50" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING state = hass.states.get("sensor.daily_energy_cost") assert state.state == "5.27" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_MONETARY + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.MONETARY assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "EUR" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING state = hass.states.get("sensor.weekly_energy_cost") assert state.state == "36.89" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_MONETARY + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.MONETARY assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "EUR" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING state = hass.states.get("sensor.monthly_energy_cost") assert state.state == "147.56" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_MONETARY + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.MONETARY assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "EUR" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING state = hass.states.get("sensor.yearly_energy_cost") assert state.state == "1844.50" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_MONETARY + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.MONETARY assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "EUR" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING entity = ent_reg.async_get("sensor.power_usage_728386") assert entity.disabled_by is er.RegistryEntryDisabler.INTEGRATION ent_reg.async_update_entity(entity.entity_id, **{"disabled_by": None}) @@ -95,9 +92,9 @@ async def test_sensor_readings( await hass.async_block_till_done() state = hass.states.get("sensor.power_usage_728386") assert state.state == "1628" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT async def test_multi_sensor_readings( @@ -109,19 +106,19 @@ async def test_multi_sensor_readings( await setup_platform(hass, aioclient_mock, SENSOR_DOMAIN, MULTI_SENSOR_TOKEN) state = hass.states.get("sensor.power_usage_728386") assert state.state == "218" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT state = hass.states.get("sensor.power_usage_0") assert state.state == "1808" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT state = hass.states.get("sensor.power_usage_728387") assert state.state == "312" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT async def test_failed_update_and_reconnection( From ec4a9be4e6c29b6a515cc99db5a3e6a694acc212 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Thu, 16 Dec 2021 22:41:17 +0000 Subject: [PATCH 0647/2644] Use EntityCategory Enums in elkm1 (#62123) --- homeassistant/components/elkm1/sensor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/elkm1/sensor.py b/homeassistant/components/elkm1/sensor.py index b5bab021bca..a488ce02ece 100644 --- a/homeassistant/components/elkm1/sensor.py +++ b/homeassistant/components/elkm1/sensor.py @@ -9,9 +9,10 @@ from elkm1_lib.util import pretty_const, username import voluptuous as vol from homeassistant.components.sensor import SensorEntity -from homeassistant.const import ELECTRIC_POTENTIAL_VOLT, ENTITY_CATEGORY_DIAGNOSTIC +from homeassistant.const import ELECTRIC_POTENTIAL_VOLT from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform +from homeassistant.helpers.entity import EntityCategory from . import ElkAttachedEntity, create_elk_entities from .const import ATTR_VALUE, DOMAIN, ELK_USER_CODE_SERVICE_SCHEMA @@ -158,7 +159,7 @@ class ElkKeypad(ElkSensor): class ElkPanel(ElkSensor): """Representation of an Elk-M1 Panel.""" - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_entity_category = EntityCategory.DIAGNOSTIC @property def icon(self): From d4f8a7c0561836bc4db357a7c2a0f0144d0bbfa2 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 17 Dec 2021 00:14:01 +0000 Subject: [PATCH 0648/2644] [ci skip] Translation update --- .../accuweather/translations/ja.json | 2 +- .../components/adax/translations/bg.json | 4 +- .../components/adax/translations/no.json | 4 +- .../components/airly/translations/ja.json | 2 +- .../alarm_control_panel/translations/fr.json | 2 +- .../azure_event_hub/translations/bg.json | 12 +++++ .../azure_event_hub/translations/ca.json | 35 +++++++++++++ .../azure_event_hub/translations/en.json | 49 +++++++++++++++++++ .../azure_event_hub/translations/it.json | 49 +++++++++++++++++++ .../bmw_connected_drive/translations/ja.json | 2 +- .../components/cloud/translations/ja.json | 6 +-- .../components/elmax/translations/bg.json | 1 + .../components/gios/translations/ja.json | 2 +- .../components/hangouts/translations/ja.json | 2 +- .../components/hue/translations/ja.json | 2 +- .../components/ipma/translations/ja.json | 2 +- .../components/isy994/translations/ja.json | 2 +- .../components/knx/translations/bg.json | 10 ++++ .../components/knx/translations/fr.json | 2 + .../components/knx/translations/id.json | 2 + .../components/knx/translations/it.json | 2 + .../components/knx/translations/no.json | 2 + .../components/knx/translations/ru.json | 2 + .../components/lcn/translations/no.json | 2 +- .../open_meteo/translations/ca.json | 12 +++++ .../pvpc_hourly_pricing/translations/ja.json | 2 +- .../components/rfxtrx/translations/id.json | 3 +- .../components/sentry/translations/id.json | 2 + .../simplisafe/translations/bg.json | 1 + .../components/smarthab/translations/ja.json | 2 +- .../smartthings/translations/ja.json | 2 +- .../components/spotify/translations/ja.json | 2 +- .../components/twinkly/translations/bg.json | 5 ++ .../wolflink/translations/sensor.id.json | 14 +++++- .../wolflink/translations/sensor.no.json | 2 +- 35 files changed, 223 insertions(+), 24 deletions(-) create mode 100644 homeassistant/components/azure_event_hub/translations/bg.json create mode 100644 homeassistant/components/azure_event_hub/translations/ca.json create mode 100644 homeassistant/components/azure_event_hub/translations/en.json create mode 100644 homeassistant/components/azure_event_hub/translations/it.json create mode 100644 homeassistant/components/open_meteo/translations/ca.json diff --git a/homeassistant/components/accuweather/translations/ja.json b/homeassistant/components/accuweather/translations/ja.json index 00c659fade9..10fe398ba04 100644 --- a/homeassistant/components/accuweather/translations/ja.json +++ b/homeassistant/components/accuweather/translations/ja.json @@ -34,7 +34,7 @@ }, "system_health": { "info": { - "can_reach_server": "AccuWeather\u30b5\u30fc\u30d0\u30fc\u306b\u5230\u9054", + "can_reach_server": "AccuWeather\u30b5\u30fc\u30d0\u30fc\u3078\u306e\u30a2\u30af\u30bb\u30b9", "remaining_requests": "\u6b8b\u308a\u306e\u8a31\u53ef\u3055\u308c\u305f\u30ea\u30af\u30a8\u30b9\u30c8" } } diff --git a/homeassistant/components/adax/translations/bg.json b/homeassistant/components/adax/translations/bg.json index 5cc751762da..d76109bfe01 100644 --- a/homeassistant/components/adax/translations/bg.json +++ b/homeassistant/components/adax/translations/bg.json @@ -16,8 +16,8 @@ }, "local": { "data": { - "wifi_pswd": "\u041f\u0430\u0440\u043e\u043b\u0430 \u0437\u0430 Wi-Fi", - "wifi_ssid": "Wifi ssid" + "wifi_pswd": "Wi-Fi \u043f\u0430\u0440\u043e\u043b\u0430", + "wifi_ssid": "Wi-Fi SSID" } }, "user": { diff --git a/homeassistant/components/adax/translations/no.json b/homeassistant/components/adax/translations/no.json index 54c154c98c2..23e82250673 100644 --- a/homeassistant/components/adax/translations/no.json +++ b/homeassistant/components/adax/translations/no.json @@ -19,8 +19,8 @@ }, "local": { "data": { - "wifi_pswd": "Wifi-passord", - "wifi_ssid": "Wifi ssid" + "wifi_pswd": "Wi-Fi-passord", + "wifi_ssid": "Wi-Fi SSID" }, "description": "Tilbakestill varmeren ved \u00e5 trykke p\u00e5 + og OK til displayet viser 'Tilbakestill'. Trykk deretter p\u00e5 og hold inne OK-knappen p\u00e5 varmeren til den bl\u00e5 lampen begynner \u00e5 blinke f\u00f8r du trykker p\u00e5 Send. Det kan ta noen minutter \u00e5 konfigurere varmeapparatet." }, diff --git a/homeassistant/components/airly/translations/ja.json b/homeassistant/components/airly/translations/ja.json index 347868bc35e..6835412e6db 100644 --- a/homeassistant/components/airly/translations/ja.json +++ b/homeassistant/components/airly/translations/ja.json @@ -22,7 +22,7 @@ }, "system_health": { "info": { - "can_reach_server": "Airly\u30b5\u30fc\u30d0\u30fc\u306b\u5230\u9054", + "can_reach_server": "Airly\u30b5\u30fc\u30d0\u30fc\u3078\u306e\u30a2\u30af\u30bb\u30b9", "requests_per_day": "1\u65e5\u3042\u305f\u308a\u306e\u8a31\u53ef\u3055\u308c\u305f\u30ea\u30af\u30a8\u30b9\u30c8", "requests_remaining": "\u6b8b\u308a\u306e\u8a31\u53ef\u3055\u308c\u305f\u30ea\u30af\u30a8\u30b9\u30c8" } diff --git a/homeassistant/components/alarm_control_panel/translations/fr.json b/homeassistant/components/alarm_control_panel/translations/fr.json index 277233bdfb4..596c999d105 100644 --- a/homeassistant/components/alarm_control_panel/translations/fr.json +++ b/homeassistant/components/alarm_control_panel/translations/fr.json @@ -1,7 +1,7 @@ { "device_automation": { "action_type": { - "arm_away": "Armer {entity_name} en mode \"sortie\"", + "arm_away": "Armer {entity_name} mode sortie", "arm_home": "Armer {entity_name} en mode \"maison\"", "arm_night": "Armer {entity_name} en mode \"nuit\"", "arm_vacation": "Armer {entity_name} vacances", diff --git a/homeassistant/components/azure_event_hub/translations/bg.json b/homeassistant/components/azure_event_hub/translations/bg.json new file mode 100644 index 00000000000..d361944ea4e --- /dev/null +++ b/homeassistant/components/azure_event_hub/translations/bg.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430", + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/azure_event_hub/translations/ca.json b/homeassistant/components/azure_event_hub/translations/ca.json new file mode 100644 index 00000000000..055811f53e4 --- /dev/null +++ b/homeassistant/components/azure_event_hub/translations/ca.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat", + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "unknown": "Error inesperat" + }, + "step": { + "sas": { + "data": { + "event_hub_namespace": "Event Hub Namespace", + "event_hub_sas_key": "Clau SAS de l'Event Hub", + "event_hub_sas_policy": "Pol\u00edtica SAS de l'Event Hub" + }, + "title": "M\u00e8tode de credencials SAS" + }, + "user": { + "data": { + "event_hub_instance_name": "Nom de la inst\u00e0ncia Event Hub" + }, + "title": "Configuraci\u00f3 de la integraci\u00f3 Azure Event Hub" + } + } + }, + "options": { + "step": { + "options": { + "title": "Opcions d'Azure Event Hub." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/azure_event_hub/translations/en.json b/homeassistant/components/azure_event_hub/translations/en.json new file mode 100644 index 00000000000..60f9ea8f36c --- /dev/null +++ b/homeassistant/components/azure_event_hub/translations/en.json @@ -0,0 +1,49 @@ +{ + "config": { + "abort": { + "already_configured": "Service is already configured", + "cannot_connect": "Connecting with the credentails from the configuration.yaml failed, please remove from yaml and use the config flow.", + "single_instance_allowed": "Already configured. Only a single configuration possible.", + "unknown": "Connecting with the credentails from the configuration.yaml failed with an unknown error, please remove from yaml and use the config flow." + }, + "error": { + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error" + }, + "step": { + "conn_string": { + "data": { + "event_hub_connection_string": "Event Hub Connection String" + }, + "description": "Please enter the connection string for: {event_hub_instance_name}", + "title": "Connection String method" + }, + "sas": { + "data": { + "event_hub_namespace": "Event Hub Namespace", + "event_hub_sas_key": "Event Hub SAS Key", + "event_hub_sas_policy": "Event Hub SAS Policy" + }, + "description": "Please enter the SAS (shared access signature) credentials for: {event_hub_instance_name}", + "title": "SAS Credentials method" + }, + "user": { + "data": { + "event_hub_instance_name": "Event Hub Instance Name", + "use_connection_string": "Use Connection String" + }, + "title": "Setup your Azure Event Hub integration" + } + } + }, + "options": { + "step": { + "options": { + "data": { + "send_interval": "Interval between sending batches to the hub." + }, + "title": "Options for the Azure Event Hub." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/azure_event_hub/translations/it.json b/homeassistant/components/azure_event_hub/translations/it.json new file mode 100644 index 00000000000..edf1770ef92 --- /dev/null +++ b/homeassistant/components/azure_event_hub/translations/it.json @@ -0,0 +1,49 @@ +{ + "config": { + "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", + "cannot_connect": "Connessione con le credenziali da configuration.yaml non riuscita, rimuovere da yaml e utilizzare il flusso di configurazione.", + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione.", + "unknown": "La connessione con le credenziali da configuration.yaml non \u00e8 riuscita con un errore sconosciuto, rimuovere da yaml e utilizzare il flusso di configurazione." + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "unknown": "Errore imprevisto" + }, + "step": { + "conn_string": { + "data": { + "event_hub_connection_string": "Stringa di connessione dell'Event Hub" + }, + "description": "Inserisci la stringa di connessione per: {event_hub_instance_name}", + "title": "Metodo della stringa di connessione" + }, + "sas": { + "data": { + "event_hub_namespace": "Spazio dei nomi dell'Event Hub", + "event_hub_sas_key": "Chiave SAS dell'Event Hub", + "event_hub_sas_policy": "Criteri SAS dell'Event Hub" + }, + "description": "Inserisci le credenziali SAS (firma di accesso condiviso) per: {event_hub_instance_name}", + "title": "Metodo delle credenziali SAS" + }, + "user": { + "data": { + "event_hub_instance_name": "Nome dell'istanza dell'Event Hub", + "use_connection_string": "Usa stringa di connessione" + }, + "title": "Configura l'integrazione di Azure Event Hub" + } + } + }, + "options": { + "step": { + "options": { + "data": { + "send_interval": "Intervallo tra l'invio di batch all'hub." + }, + "title": "Opzioni per Azure Event Hub." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/translations/ja.json b/homeassistant/components/bmw_connected_drive/translations/ja.json index 5e33b26f423..a3fa86348fe 100644 --- a/homeassistant/components/bmw_connected_drive/translations/ja.json +++ b/homeassistant/components/bmw_connected_drive/translations/ja.json @@ -22,7 +22,7 @@ "account_options": { "data": { "read_only": "\u30ea\u30fc\u30c9\u30aa\u30f3\u30ea\u30fc(\u30bb\u30f3\u30b5\u30fc\u3068\u901a\u77e5\u306e\u307f\u3001\u30b5\u30fc\u30d3\u30b9\u306e\u5b9f\u884c\u306f\u4e0d\u53ef\u3001\u30ed\u30c3\u30af\u4e0d\u53ef)", - "use_location": "Home Assistant\u306e\u5834\u6240\u3092\u3001\u8eca\u306e\u4f4d\u7f6e\u3068\u3057\u3066\u30dd\u30fc\u30ea\u30f3\u30b0\u306b\u4f7f\u7528\u3059\u308b((2014\u5e747\u67087\u65e5\u4ee5\u524d\u306b\u751f\u7523\u3055\u308c\u305f\u3001i3/i8\u4ee5\u5916\u306e\u8eca\u4e21\u3067\u306f\u5fc5\u9808)" + "use_location": "Home Assistant\u306e\u5834\u6240\u3092\u3001\u8eca\u306e\u4f4d\u7f6e\u3068\u3057\u3066\u30dd\u30fc\u30ea\u30f3\u30b0\u306b\u4f7f\u7528\u3059\u308b(2014\u5e747\u67087\u65e5\u4ee5\u524d\u306b\u751f\u7523\u3055\u308c\u305f\u3001i3/i8\u4ee5\u5916\u306e\u8eca\u4e21\u3067\u306f\u5fc5\u9808)" } } } diff --git a/homeassistant/components/cloud/translations/ja.json b/homeassistant/components/cloud/translations/ja.json index d9c6674fa6b..298163c3d41 100644 --- a/homeassistant/components/cloud/translations/ja.json +++ b/homeassistant/components/cloud/translations/ja.json @@ -2,9 +2,9 @@ "system_health": { "info": { "alexa_enabled": "Alexa\u6709\u52b9", - "can_reach_cert_server": "\u8a3c\u660e\u66f8\u30b5\u30fc\u30d0\u30fc\u306b\u5230\u9054\u30a2\u30af\u30bb\u30b9\u3059\u308b", - "can_reach_cloud": "Home AssistantCloud\u306b\u30a2\u30af\u30bb\u30b9\u3059\u308b", - "can_reach_cloud_auth": "\u8a8d\u8a3c\u30b5\u30fc\u30d0\u30fc\u306b\u5230\u9054", + "can_reach_cert_server": "\u8a3c\u660e\u66f8\u30b5\u30fc\u30d0\u30fc\u3078\u306e\u30a2\u30af\u30bb\u30b9", + "can_reach_cloud": "Home Assistant Cloud\u3078\u306e\u30a2\u30af\u30bb\u30b9", + "can_reach_cloud_auth": "\u8a8d\u8a3c\u30b5\u30fc\u30d0\u30fc\u3078\u306e\u30a2\u30af\u30bb\u30b9", "google_enabled": "Google\u6709\u52b9", "logged_in": "\u30ed\u30b0\u30a4\u30f3\u6e08", "relayer_connected": "\u63a5\u7d9a\u3055\u308c\u305f\u518d\u30ec\u30a4\u30e4\u30fc", diff --git a/homeassistant/components/elmax/translations/bg.json b/homeassistant/components/elmax/translations/bg.json index 58dfefd31cf..643587133a7 100644 --- a/homeassistant/components/elmax/translations/bg.json +++ b/homeassistant/components/elmax/translations/bg.json @@ -6,6 +6,7 @@ "error": { "bad_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", "invalid_pin": "\u041f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0435\u043d\u0438\u044f\u0442 \u041f\u0418\u041d \u0435 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d", + "network_error": "\u0412\u044a\u0437\u043d\u0438\u043a\u043d\u0430 \u043c\u0440\u0435\u0436\u043e\u0432\u0430 \u0433\u0440\u0435\u0448\u043a\u0430", "unknown_error": "\u0412\u044a\u0437\u043d\u0438\u043a\u043d\u0430 \u043d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { diff --git a/homeassistant/components/gios/translations/ja.json b/homeassistant/components/gios/translations/ja.json index 2bcc20414bb..ac05abbf923 100644 --- a/homeassistant/components/gios/translations/ja.json +++ b/homeassistant/components/gios/translations/ja.json @@ -21,7 +21,7 @@ }, "system_health": { "info": { - "can_reach_server": "Reach GIO\u015a server" + "can_reach_server": "GIO\u015a\u30b5\u30fc\u30d0\u30fc\u3078\u306e\u30a2\u30af\u30bb\u30b9" } } } \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/ja.json b/homeassistant/components/hangouts/translations/ja.json index 72f5ce09313..14637ee1155 100644 --- a/homeassistant/components/hangouts/translations/ja.json +++ b/homeassistant/components/hangouts/translations/ja.json @@ -7,7 +7,7 @@ "error": { "invalid_2fa": "2\u8981\u7d20\u8a8d\u8a3c\u304c\u7121\u52b9\u3067\u3059\u3002\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", "invalid_2fa_method": "2\u8981\u7d20\u8a8d\u8a3c\u304c\u7121\u52b9(\u96fb\u8a71\u3067\u78ba\u8a8d)", - "invalid_login": "\u30ed\u30b0\u30a4\u30f3\u304c\u7121\u52b9\u3067\u3059\u3001\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002" + "invalid_login": "\u30ed\u30b0\u30a4\u30f3\u3067\u304d\u307e\u305b\u3093\u3001\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002" }, "step": { "2fa": { diff --git a/homeassistant/components/hue/translations/ja.json b/homeassistant/components/hue/translations/ja.json index 31fea90149d..7b9e1dc4610 100644 --- a/homeassistant/components/hue/translations/ja.json +++ b/homeassistant/components/hue/translations/ja.json @@ -69,7 +69,7 @@ "data": { "allow_hue_groups": "Hue(\u8272\u76f8)\u30b0\u30eb\u30fc\u30d7\u306e\u8a31\u53ef", "allow_hue_scenes": "Hue\u30b7\u30fc\u30f3\u3092\u8a31\u53ef", - "allow_unreachable": "\u5230\u9054\u3067\u304d\u306a\u304b\u3063\u305f\u96fb\u7403(bulbs)\u304c\u305d\u306e\u72b6\u614b\u3092\u6b63\u3057\u304f\u5831\u544a\u3067\u304d\u308b\u3088\u3046\u306b\u3059\u308b" + "allow_unreachable": "\u30a2\u30af\u30bb\u30b9\u3067\u304d\u306a\u304b\u3063\u305f\u96fb\u7403(bulbs)\u304c\u305d\u306e\u72b6\u614b\u3092\u6b63\u3057\u304f\u5831\u544a\u3067\u304d\u308b\u3088\u3046\u306b\u3059\u308b" } } } diff --git a/homeassistant/components/ipma/translations/ja.json b/homeassistant/components/ipma/translations/ja.json index 078026d8333..541a53f6a32 100644 --- a/homeassistant/components/ipma/translations/ja.json +++ b/homeassistant/components/ipma/translations/ja.json @@ -18,7 +18,7 @@ }, "system_health": { "info": { - "api_endpoint_reachable": "IPMA API\u30a8\u30f3\u30c9\u30dd\u30a4\u30f3\u30c8\u306b\u5230\u9054\u53ef\u80fd" + "api_endpoint_reachable": "IPMA API\u30a8\u30f3\u30c9\u30dd\u30a4\u30f3\u30c8\u306b\u30a2\u30af\u30bb\u30b9\u53ef\u80fd" } } } \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/ja.json b/homeassistant/components/isy994/translations/ja.json index 438eadfcd83..f1021cbd6e9 100644 --- a/homeassistant/components/isy994/translations/ja.json +++ b/homeassistant/components/isy994/translations/ja.json @@ -40,7 +40,7 @@ "system_health": { "info": { "device_connected": "ISY\u63a5\u7d9a\u6e08", - "host_reachable": "\u30db\u30b9\u30c8\u5230\u9054\u53ef\u80fd", + "host_reachable": "\u30db\u30b9\u30c8\u306b\u30a2\u30af\u30bb\u30b9\u53ef\u80fd", "last_heartbeat": "\u6700\u5f8c\u306e\u30cf\u30fc\u30c8\u30d3\u30fc\u30c8\u30bf\u30a4\u30e0", "websocket_status": "\u30a4\u30d9\u30f3\u30c8\u30bd\u30b1\u30c3\u30c8 \u30b9\u30c6\u30fc\u30bf\u30b9" } diff --git a/homeassistant/components/knx/translations/bg.json b/homeassistant/components/knx/translations/bg.json index f1c253ed5f1..07785249026 100644 --- a/homeassistant/components/knx/translations/bg.json +++ b/homeassistant/components/knx/translations/bg.json @@ -14,11 +14,21 @@ "local_ip": "\u041b\u043e\u043a\u0430\u043b\u0435\u043d IP (\u043e\u0441\u0442\u0430\u0432\u0435\u0442\u0435 \u043f\u0440\u0430\u0437\u043d\u043e, \u0430\u043a\u043e \u043d\u0435 \u0441\u0442\u0435 \u0441\u0438\u0433\u0443\u0440\u043d\u0438)", "port": "\u041f\u043e\u0440\u0442" } + }, + "routing": { + "data": { + "local_ip": "\u041b\u043e\u043a\u0430\u043b\u0435\u043d IP \u0430\u0434\u0440\u0435\u0441 (\u043e\u0441\u0442\u0430\u0432\u0435\u0442\u0435 \u043f\u0440\u0430\u0437\u043d\u043e, \u0430\u043a\u043e \u043d\u0435 \u0441\u0442\u0435 \u0441\u0438\u0433\u0443\u0440\u043d\u0438)" + } } } }, "options": { "step": { + "init": { + "data": { + "local_ip": "\u041b\u043e\u043a\u0430\u043b\u0435\u043d IP \u0430\u0434\u0440\u0435\u0441 (\u043e\u0441\u0442\u0430\u0432\u0435\u0442\u0435 \u043f\u0440\u0430\u0437\u043d\u043e, \u0430\u043a\u043e \u043d\u0435 \u0441\u0442\u0435 \u0441\u0438\u0433\u0443\u0440\u043d\u0438)" + } + }, "tunnel": { "data": { "host": "\u0425\u043e\u0441\u0442", diff --git a/homeassistant/components/knx/translations/fr.json b/homeassistant/components/knx/translations/fr.json index 9efac4d94d0..3b48ebaf393 100644 --- a/homeassistant/components/knx/translations/fr.json +++ b/homeassistant/components/knx/translations/fr.json @@ -21,6 +21,7 @@ "routing": { "data": { "individual_address": "Adresse individuelle pour la connexion de routage", + "local_ip": "IP locale (laisser vide en cas de doute)", "multicast_group": "Le groupe multicast utilis\u00e9 pour le routage", "multicast_port": "Le port multicast utilis\u00e9 pour le routage" }, @@ -46,6 +47,7 @@ "data": { "connection_type": "Type de connexion KNX", "individual_address": "Adresse individuelle par d\u00e9faut", + "local_ip": "IP locale (laisser vide en cas de doute)", "multicast_group": "Groupe de multidiffusion utilis\u00e9 pour le routage et la d\u00e9couverte", "multicast_port": "Port de multidiffusion utilis\u00e9 pour le routage et la d\u00e9couverte", "rate_limit": "Nombre maximal de t\u00e9l\u00e9grammes sortants par seconde", diff --git a/homeassistant/components/knx/translations/id.json b/homeassistant/components/knx/translations/id.json index 8c73be7fdb0..e93689f3bec 100644 --- a/homeassistant/components/knx/translations/id.json +++ b/homeassistant/components/knx/translations/id.json @@ -21,6 +21,7 @@ "routing": { "data": { "individual_address": "Alamat individu untuk koneksi routing", + "local_ip": "IP lokal (kosongkan jika tidak yakin)", "multicast_group": "Grup multicast yang digunakan untuk routing", "multicast_port": "Port multicast yang digunakan untuk routing" }, @@ -46,6 +47,7 @@ "data": { "connection_type": "Jenis Koneksi KNX", "individual_address": "Alamat individu default", + "local_ip": "IP lokal (kosongkan jika tidak yakin)", "multicast_group": "Grup multicast yang digunakan untuk routing dan penemuan", "multicast_port": "Port multicast yang digunakan untuk routing dan penemuan", "rate_limit": "Jumlah maksimal telegram keluar per detik", diff --git a/homeassistant/components/knx/translations/it.json b/homeassistant/components/knx/translations/it.json index ec4e71e9c46..b41d06e5dae 100644 --- a/homeassistant/components/knx/translations/it.json +++ b/homeassistant/components/knx/translations/it.json @@ -21,6 +21,7 @@ "routing": { "data": { "individual_address": "Indirizzo individuale per la connessione di routing", + "local_ip": "IP locale (lasciare vuoto se non si \u00e8 sicuri)", "multicast_group": "Il gruppo multicast utilizzato per il routing", "multicast_port": "La porta multicast usata per il routing" }, @@ -46,6 +47,7 @@ "data": { "connection_type": "Tipo di connessione KNX", "individual_address": "Indirizzo individuale predefinito", + "local_ip": "IP locale (lasciare vuoto se non si \u00e8 sicuri)", "multicast_group": "Gruppo multicast utilizzato per il routing e il rilevamento", "multicast_port": "Porta multicast utilizzata per il routing e il rilevamento", "rate_limit": "Numero massimo di telegrammi in uscita al secondo", diff --git a/homeassistant/components/knx/translations/no.json b/homeassistant/components/knx/translations/no.json index 75a13da35c0..b4a3642aad3 100644 --- a/homeassistant/components/knx/translations/no.json +++ b/homeassistant/components/knx/translations/no.json @@ -21,6 +21,7 @@ "routing": { "data": { "individual_address": "Individuell adresse for ruteforbindelsen", + "local_ip": "Lokal IP (la st\u00e5 tomt hvis du er usikker)", "multicast_group": "Multicast-gruppen som brukes til ruting", "multicast_port": "Multicast-porten som brukes til ruting" }, @@ -46,6 +47,7 @@ "data": { "connection_type": "KNX tilkoblingstype", "individual_address": "Standard individuell adresse", + "local_ip": "Lokal IP (la st\u00e5 tomt hvis du er usikker)", "multicast_group": "Multicast-gruppe brukt til ruting og oppdagelse", "multicast_port": "Multicast-port som brukes til ruting og oppdagelse", "rate_limit": "Maksimalt utg\u00e5ende telegrammer per sekund", diff --git a/homeassistant/components/knx/translations/ru.json b/homeassistant/components/knx/translations/ru.json index 35c58bc2667..dab1cc09d37 100644 --- a/homeassistant/components/knx/translations/ru.json +++ b/homeassistant/components/knx/translations/ru.json @@ -21,6 +21,7 @@ "routing": { "data": { "individual_address": "\u0418\u043d\u0434\u0438\u0432\u0438\u0434\u0443\u0430\u043b\u044c\u043d\u044b\u0439 \u0430\u0434\u0440\u0435\u0441 \u0434\u043b\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0438\u0440\u0443\u0435\u043c\u043e\u0433\u043e \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f", + "local_ip": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441 (\u0435\u0441\u043b\u0438 \u043d\u0435 \u0437\u043d\u0430\u0435\u0442\u0435, \u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u043e\u043b\u0435 \u043f\u0443\u0441\u0442\u044b\u043c)", "multicast_group": "\u0413\u0440\u0443\u043f\u043f\u0430 \u043c\u043d\u043e\u0433\u043e\u0430\u0434\u0440\u0435\u0441\u043d\u043e\u0439 \u0440\u0430\u0441\u0441\u044b\u043b\u043a\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0430\u044f \u0434\u043b\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438", "multicast_port": "\u041f\u043e\u0440\u0442 \u043c\u043d\u043e\u0433\u043e\u0430\u0434\u0440\u0435\u0441\u043d\u043e\u0439 \u0440\u0430\u0441\u0441\u044b\u043b\u043a\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0439 \u0434\u043b\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438" }, @@ -46,6 +47,7 @@ "data": { "connection_type": "\u0422\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f KNX", "individual_address": "\u0418\u043d\u0434\u0438\u0432\u0438\u0434\u0443\u0430\u043b\u044c\u043d\u044b\u0439 \u0430\u0434\u0440\u0435\u0441 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e", + "local_ip": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441 (\u0435\u0441\u043b\u0438 \u043d\u0435 \u0437\u043d\u0430\u0435\u0442\u0435, \u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u043e\u043b\u0435 \u043f\u0443\u0441\u0442\u044b\u043c)", "multicast_group": "\u0413\u0440\u0443\u043f\u043f\u0430 \u043c\u043d\u043e\u0433\u043e\u0430\u0434\u0440\u0435\u0441\u043d\u043e\u0439 \u0440\u0430\u0441\u0441\u044b\u043b\u043a\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0430\u044f \u0434\u043b\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f", "multicast_port": "\u041f\u043e\u0440\u0442 \u043c\u043d\u043e\u0433\u043e\u0430\u0434\u0440\u0435\u0441\u043d\u043e\u0439 \u0440\u0430\u0441\u0441\u044b\u043b\u043a\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0439 \u0434\u043b\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f", "rate_limit": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445 \u0442\u0435\u043b\u0435\u0433\u0440\u0430\u043c\u043c \u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0443", diff --git a/homeassistant/components/lcn/translations/no.json b/homeassistant/components/lcn/translations/no.json index 5f4a8a79ba7..7ae5b70fe2f 100644 --- a/homeassistant/components/lcn/translations/no.json +++ b/homeassistant/components/lcn/translations/no.json @@ -4,7 +4,7 @@ "fingerprint": "fingeravtrykkkode mottatt", "send_keys": "sende n\u00f8kler mottatt", "transmitter": "senderkode mottatt", - "transponder": "transpoderkode mottatt" + "transponder": "transponderkode mottatt" } } } \ No newline at end of file diff --git a/homeassistant/components/open_meteo/translations/ca.json b/homeassistant/components/open_meteo/translations/ca.json new file mode 100644 index 00000000000..4fb1a502b6f --- /dev/null +++ b/homeassistant/components/open_meteo/translations/ca.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "zone": "Zona" + }, + "description": "Selecciona la ubicaci\u00f3 que s'utilitzar\u00e0 per a la predicci\u00f3 meteorol\u00f2gica" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/ja.json b/homeassistant/components/pvpc_hourly_pricing/translations/ja.json index 1021f47a38a..919186d0e62 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/ja.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/ja.json @@ -24,7 +24,7 @@ "power_p3": "\u8c37\u9593(valley period) P3 (kW)\u306e\u5951\u7d04\u96fb\u529b", "tariff": "\u5730\u57df\u5225\u9069\u7528\u95a2\u7a0e" }, - "description": "\u3053\u306e\u30bb\u30f3\u30b5\u30fc\u306f\u3001\u516c\u5f0fAPI\u3092\u4f7f\u7528\u3057\u3066\u3001\u30b9\u30da\u30a4\u30f3\u3067\u306e[\u96fb\u6c17\u306e\u6642\u9593\u4fa1\u683c((hourly pricing of electricity)PVPC)](https://www.esios.ree.es/es/pvpc) \u3092\u53d6\u5f97\u3057\u307e\u3059\u3002\n\u3088\u308a\u6b63\u78ba\u306a\u8aac\u660e\u306b\u3064\u3044\u3066\u306f\u3001[\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3 \u30c9\u30ad\u30e5\u30e1\u30f3\u30c8](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/) \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "description": "\u3053\u306e\u30bb\u30f3\u30b5\u30fc\u306f\u3001\u516c\u5f0fAPI\u3092\u4f7f\u7528\u3057\u3066\u3001\u30b9\u30da\u30a4\u30f3\u3067\u306e[\u96fb\u6c17\u306e\u6642\u9593\u4fa1\u683c(hourly pricing of electricity)PVPC)](https://www.esios.ree.es/es/pvpc) \u3092\u53d6\u5f97\u3057\u307e\u3059\u3002\n\u3088\u308a\u6b63\u78ba\u306a\u8aac\u660e\u306b\u3064\u3044\u3066\u306f\u3001[\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3 \u30c9\u30ad\u30e5\u30e1\u30f3\u30c8](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/) \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "\u30bb\u30f3\u30b5\u30fc\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } diff --git a/homeassistant/components/rfxtrx/translations/id.json b/homeassistant/components/rfxtrx/translations/id.json index 5ef4220f84a..e04b0685050 100644 --- a/homeassistant/components/rfxtrx/translations/id.json +++ b/homeassistant/components/rfxtrx/translations/id.json @@ -74,7 +74,8 @@ "off_delay": "Penundaan mematikan", "off_delay_enabled": "Aktifkan penundaan mematikan", "replace_device": "Pilih perangkat yang akan diganti", - "signal_repetitions": "Jumlah pengulangan sinyal" + "signal_repetitions": "Jumlah pengulangan sinyal", + "venetian_blind_mode": "Mode penutup kerai Venesia" }, "title": "Konfigurasi opsi perangkat" } diff --git a/homeassistant/components/sentry/translations/id.json b/homeassistant/components/sentry/translations/id.json index fbd94fa6565..b2004e8e3d5 100644 --- a/homeassistant/components/sentry/translations/id.json +++ b/homeassistant/components/sentry/translations/id.json @@ -25,6 +25,8 @@ "event_custom_components": "Kirim event dari komponen khusus", "event_handled": "Kirim event yang ditangani", "event_third_party_packages": "Kirim event dari paket pihak ketiga", + "logging_event_level": "Tingkat log Sentry yang didaftarkan untuk peristiwa", + "logging_level": "Tingkat log Sentry yang direkam sebagai jalur", "tracing": "Aktifkan pelacakan kinerja", "tracing_sample_rate": "Laju sampel pelacakan; antara 0,0 dan 1,0 (1,0 = 100%)" } diff --git a/homeassistant/components/simplisafe/translations/bg.json b/homeassistant/components/simplisafe/translations/bg.json index 75f9480f5fd..21cdaabf9a2 100644 --- a/homeassistant/components/simplisafe/translations/bg.json +++ b/homeassistant/components/simplisafe/translations/bg.json @@ -22,6 +22,7 @@ }, "user": { "data": { + "auth_code": "\u041a\u043e\u0434 \u0437\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f", "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "username": "E-mail \u0430\u0434\u0440\u0435\u0441" }, diff --git a/homeassistant/components/smarthab/translations/ja.json b/homeassistant/components/smarthab/translations/ja.json index 826692925d7..a463f259c2a 100644 --- a/homeassistant/components/smarthab/translations/ja.json +++ b/homeassistant/components/smarthab/translations/ja.json @@ -2,7 +2,7 @@ "config": { "error": { "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", - "service": "SmartHab\u306b\u5230\u9054\u3057\u3088\u3046\u3068\u3057\u3066\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\u30b5\u30fc\u30d3\u30b9\u304c\u30c0\u30a6\u30f3\u3057\u3066\u3044\u308b\u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059\u3002\u63a5\u7d9a\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "service": "SmartHab\u306b\u30a2\u30af\u30bb\u30b9\u3057\u3088\u3046\u3068\u3057\u3066\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\u30b5\u30fc\u30d3\u30b9\u304c\u30c0\u30a6\u30f3\u3057\u3066\u3044\u308b\u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059\u3002\u63a5\u7d9a\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { diff --git a/homeassistant/components/smartthings/translations/ja.json b/homeassistant/components/smartthings/translations/ja.json index 29149174ae9..b8cd8a28ff7 100644 --- a/homeassistant/components/smartthings/translations/ja.json +++ b/homeassistant/components/smartthings/translations/ja.json @@ -26,7 +26,7 @@ "data": { "location_id": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3" }, - "description": "Home Assistant\u306b\u8ffd\u52a0\u3057\u305f\u3044SmartThings\u306e\u5834\u6240\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044\u3002 \u3059\u308b\u3068\u3001\u65b0\u3057\u3044\u30a6\u30a3\u30f3\u30c9\u30a6\u304c\u958b\u304f\u306e\u3067\u30ed\u30b0\u30a4\u30f3\u3057\u3066\u3001\u9078\u629e\u3057\u305f\u5834\u6240\u3078\u306eHome Assistant\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3092\u627f\u8a8d\u3059\u308b\u3088\u3046\u6c42\u3081\u3089\u308c\u307e\u3059\u3002", + "description": "Home Assistant\u306b\u8ffd\u52a0\u3057\u305f\u3044SmartThings\u306e\u5834\u6240\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u3059\u308b\u3068\u3001\u65b0\u3057\u3044\u30a6\u30a3\u30f3\u30c9\u30a6\u304c\u958b\u304f\u306e\u3067\u30ed\u30b0\u30a4\u30f3\u3057\u3066\u3001\u9078\u629e\u3057\u305f\u5834\u6240\u3078\u306eHome Assistant\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3092\u627f\u8a8d\u3059\u308b\u3088\u3046\u6c42\u3081\u3089\u308c\u307e\u3059\u3002", "title": "\u5834\u6240\u3092\u9078\u629e" }, "user": { diff --git a/homeassistant/components/spotify/translations/ja.json b/homeassistant/components/spotify/translations/ja.json index d21e6919bf7..9c0e308dc18 100644 --- a/homeassistant/components/spotify/translations/ja.json +++ b/homeassistant/components/spotify/translations/ja.json @@ -21,7 +21,7 @@ }, "system_health": { "info": { - "api_endpoint_reachable": "Spotify API\u30a8\u30f3\u30c9\u30dd\u30a4\u30f3\u30c8\u306b\u5230\u9054\u53ef\u80fd" + "api_endpoint_reachable": "Spotify API\u30a8\u30f3\u30c9\u30dd\u30a4\u30f3\u30c8\u306b\u30a2\u30af\u30bb\u30b9\u53ef\u80fd" } } } \ No newline at end of file diff --git a/homeassistant/components/twinkly/translations/bg.json b/homeassistant/components/twinkly/translations/bg.json index cd6031ffaf0..a83b075fe7d 100644 --- a/homeassistant/components/twinkly/translations/bg.json +++ b/homeassistant/components/twinkly/translations/bg.json @@ -5,6 +5,11 @@ }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "step": { + "discovery_confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name} - {model} ({host})?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/sensor.id.json b/homeassistant/components/wolflink/translations/sensor.id.json index b3d31b620a2..9e7932b2dd7 100644 --- a/homeassistant/components/wolflink/translations/sensor.id.json +++ b/homeassistant/components/wolflink/translations/sensor.id.json @@ -2,6 +2,7 @@ "state": { "wolflink__state": { "1_x_warmwasser": "1 x DHW", + "abgasklappe": "Katup saluran buang gas", "aktiviert": "Diaktifkan", "antilegionellenfunktion": "Fungsi Anti-legionella", "aus": "Dinonaktifkan", @@ -19,6 +20,12 @@ "ein": "Diaktifkan", "externe_deaktivierung": "Penonaktifan eksternal", "fernschalter_ein": "Kontrol jarak jauh diaktifkan", + "frostschutz": "Perlindungan beku", + "gasdruck": "Tekanan gas", + "glt_betrieb": "Mode BMS", + "gradienten_uberwachung": "Pemantauan gradien", + "heizbetrieb": "Mode pemanasan", + "heizgerat_mit_speicher": "Ketel dengan silinder", "heizung": "Memanaskan", "initialisierung": "Inisialisasi", "kalibration": "Kalibrasi", @@ -27,6 +34,8 @@ "kalibration_warmwasserbetrieb": "Kalibrasi DHW", "kaskadenbetrieb": "Operasi bertingkat", "kombibetrieb": "Mode kombi", + "kombigerat": "Ketel kombinasi", + "kombigerat_mit_solareinbindung": "Ketel kombinasi dengan integrasi tenaga surya", "mindest_kombizeit": "Waktu kombi minimum", "nur_heizgerat": "Hanya boiler", "parallelbetrieb": "Mode paralel", @@ -44,8 +53,11 @@ "standby": "Siaga", "start": "Mulai", "storung": "Kesalahan", + "telefonfernschalter": "Saklar jarak jauh per telepon", + "test": "Pengujian", "urlaubsmodus": "Mode liburan", - "ventilprufung": "Uji katup" + "ventilprufung": "Uji katup", + "zunden": "Pengapian" } } } \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/sensor.no.json b/homeassistant/components/wolflink/translations/sensor.no.json index 75aa91bcf9c..487548468bc 100644 --- a/homeassistant/components/wolflink/translations/sensor.no.json +++ b/homeassistant/components/wolflink/translations/sensor.no.json @@ -65,7 +65,7 @@ "sparen": "\u00d8konomi", "spreizung_hoch": "dT for bred", "spreizung_kf": "Spre KF", - "stabilisierung": "Stablisering", + "stabilisierung": "Stabilisering", "standby": "Avventer", "start": "Start", "storung": "Feil", From 569d5644abe832ec8fe63924d327540aa877c33e Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Fri, 17 Dec 2021 06:50:46 +0000 Subject: [PATCH 0649/2644] Use DeviceClass Enums in google_assistant tests (#62142) --- .../components/google_assistant/test_trait.py | 53 +++++++++++-------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index a396c1bc91d..e20f08f8702 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -2444,7 +2444,11 @@ async def test_openclose_cover_no_position(hass): @pytest.mark.parametrize( "device_class", - (cover.DEVICE_CLASS_DOOR, cover.DEVICE_CLASS_GARAGE, cover.DEVICE_CLASS_GATE), + ( + cover.CoverDeviceClass.DOOR, + cover.CoverDeviceClass.GARAGE, + cover.CoverDeviceClass.GATE, + ), ) async def test_openclose_cover_secure(hass, device_class): """Test OpenClose trait support for cover domain.""" @@ -2507,11 +2511,11 @@ async def test_openclose_cover_secure(hass, device_class): @pytest.mark.parametrize( "device_class", ( - binary_sensor.DEVICE_CLASS_DOOR, - binary_sensor.DEVICE_CLASS_GARAGE_DOOR, - binary_sensor.DEVICE_CLASS_LOCK, - binary_sensor.DEVICE_CLASS_OPENING, - binary_sensor.DEVICE_CLASS_WINDOW, + binary_sensor.BinarySensorDeviceClass.DOOR, + binary_sensor.BinarySensorDeviceClass.GARAGE_DOOR, + binary_sensor.BinarySensorDeviceClass.LOCK, + binary_sensor.BinarySensorDeviceClass.OPENING, + binary_sensor.BinarySensorDeviceClass.WINDOW, ), ) async def test_openclose_binary_sensor(hass, device_class): @@ -2728,14 +2732,14 @@ async def test_media_player_mute(hass): async def test_temperature_control_sensor(hass): """Test TemperatureControl trait support for temperature sensor.""" assert ( - helpers.get_google_type(sensor.DOMAIN, sensor.DEVICE_CLASS_TEMPERATURE) + helpers.get_google_type(sensor.DOMAIN, sensor.SensorDeviceClass.TEMPERATURE) is not None ) assert not trait.TemperatureControlTrait.supported( - sensor.DOMAIN, 0, sensor.DEVICE_CLASS_HUMIDITY, None + sensor.DOMAIN, 0, sensor.SensorDeviceClass.HUMIDITY, None ) assert trait.TemperatureControlTrait.supported( - sensor.DOMAIN, 0, sensor.DEVICE_CLASS_TEMPERATURE, None + sensor.DOMAIN, 0, sensor.SensorDeviceClass.TEMPERATURE, None ) @@ -2755,7 +2759,9 @@ async def test_temperature_control_sensor_data(hass, unit_in, unit_out, state, a trt = trait.TemperatureControlTrait( hass, State( - "sensor.test", state, {ATTR_DEVICE_CLASS: sensor.DEVICE_CLASS_TEMPERATURE} + "sensor.test", + state, + {ATTR_DEVICE_CLASS: sensor.SensorDeviceClass.TEMPERATURE}, ), BASIC_CONFIG, ) @@ -2779,13 +2785,14 @@ async def test_temperature_control_sensor_data(hass, unit_in, unit_out, state, a async def test_humidity_setting_sensor(hass): """Test HumiditySetting trait support for humidity sensor.""" assert ( - helpers.get_google_type(sensor.DOMAIN, sensor.DEVICE_CLASS_HUMIDITY) is not None + helpers.get_google_type(sensor.DOMAIN, sensor.SensorDeviceClass.HUMIDITY) + is not None ) assert not trait.HumiditySettingTrait.supported( - sensor.DOMAIN, 0, sensor.DEVICE_CLASS_TEMPERATURE, None + sensor.DOMAIN, 0, sensor.SensorDeviceClass.TEMPERATURE, None ) assert trait.HumiditySettingTrait.supported( - sensor.DOMAIN, 0, sensor.DEVICE_CLASS_HUMIDITY, None + sensor.DOMAIN, 0, sensor.SensorDeviceClass.HUMIDITY, None ) @@ -2796,7 +2803,9 @@ async def test_humidity_setting_sensor_data(hass, state, ambient): """Test HumiditySetting trait support for humidity sensor.""" trt = trait.HumiditySettingTrait( hass, - State("sensor.test", state, {ATTR_DEVICE_CLASS: sensor.DEVICE_CLASS_HUMIDITY}), + State( + "sensor.test", state, {ATTR_DEVICE_CLASS: sensor.SensorDeviceClass.HUMIDITY} + ), BASIC_CONFIG, ) @@ -2983,7 +2992,7 @@ async def test_channel(hass): assert trait.ChannelTrait.supported( media_player.DOMAIN, media_player.SUPPORT_PLAY_MEDIA, - media_player.DEVICE_CLASS_TV, + media_player.MediaPlayerDeviceClass.TV, None, ) assert ( @@ -3029,12 +3038,12 @@ async def test_channel(hass): async def test_sensorstate(hass): """Test SensorState trait support for sensor domain.""" sensor_types = { - sensor.DEVICE_CLASS_AQI: ("AirQuality", "AQI"), - sensor.DEVICE_CLASS_CO: ("CarbonDioxideLevel", "PARTS_PER_MILLION"), - sensor.DEVICE_CLASS_CO2: ("CarbonMonoxideLevel", "PARTS_PER_MILLION"), - sensor.DEVICE_CLASS_PM25: ("PM2.5", "MICROGRAMS_PER_CUBIC_METER"), - sensor.DEVICE_CLASS_PM10: ("PM10", "MICROGRAMS_PER_CUBIC_METER"), - sensor.DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS: ( + sensor.SensorDeviceClass.AQI: ("AirQuality", "AQI"), + sensor.SensorDeviceClass.CO: ("CarbonDioxideLevel", "PARTS_PER_MILLION"), + sensor.SensorDeviceClass.CO2: ("CarbonMonoxideLevel", "PARTS_PER_MILLION"), + sensor.SensorDeviceClass.PM25: ("PM2.5", "MICROGRAMS_PER_CUBIC_METER"), + sensor.SensorDeviceClass.PM10: ("PM10", "MICROGRAMS_PER_CUBIC_METER"), + sensor.SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: ( "VolatileOrganicCompounds", "PARTS_PER_MILLION", ), @@ -3073,7 +3082,7 @@ async def test_sensorstate(hass): assert helpers.get_google_type(sensor.DOMAIN, None) is not None assert ( trait.SensorStateTrait.supported( - sensor.DOMAIN, None, sensor.DEVICE_CLASS_MONETARY, None + sensor.DOMAIN, None, sensor.SensorDeviceClass.MONETARY, None ) is False ) From c710958261c0f3413c691dc5a0e9dcef899791bd Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Fri, 17 Dec 2021 06:52:25 +0000 Subject: [PATCH 0650/2644] Use DeviceClass Enums in goalzero tests (#62136) --- .../components/goalzero/test_binary_sensor.py | 24 +++++------ tests/components/goalzero/test_sensor.py | 43 ++++++++----------- 2 files changed, 28 insertions(+), 39 deletions(-) diff --git a/tests/components/goalzero/test_binary_sensor.py b/tests/components/goalzero/test_binary_sensor.py index 1c130013283..4f2ffd751b5 100644 --- a/tests/components/goalzero/test_binary_sensor.py +++ b/tests/components/goalzero/test_binary_sensor.py @@ -1,16 +1,7 @@ """Binary sensor tests for the Goalzero integration.""" -from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_BATTERY_CHARGING, - DEVICE_CLASS_CONNECTIVITY, - DOMAIN, -) +from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDeviceClass from homeassistant.components.goalzero.const import DEFAULT_NAME -from homeassistant.const import ( - ATTR_DEVICE_CLASS, - DEVICE_CLASS_POWER, - STATE_OFF, - STATE_ON, -) +from homeassistant.const import ATTR_DEVICE_CLASS, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from . import async_setup_platform @@ -27,10 +18,15 @@ async def test_binary_sensors(hass: HomeAssistant, aioclient_mock: AiohttpClient assert state.attributes.get(ATTR_DEVICE_CLASS) is None state = hass.states.get(f"binary_sensor.{DEFAULT_NAME}_app_online") assert state.state == STATE_OFF - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_CONNECTIVITY + assert ( + state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.CONNECTIVITY + ) state = hass.states.get(f"binary_sensor.{DEFAULT_NAME}_charging") assert state.state == STATE_OFF - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_BATTERY_CHARGING + assert ( + state.attributes.get(ATTR_DEVICE_CLASS) + == BinarySensorDeviceClass.BATTERY_CHARGING + ) state = hass.states.get(f"binary_sensor.{DEFAULT_NAME}_input_detected") assert state.state == STATE_OFF - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER + assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.POWER diff --git a/tests/components/goalzero/test_sensor.py b/tests/components/goalzero/test_sensor.py index 592c43b5d43..67878d702db 100644 --- a/tests/components/goalzero/test_sensor.py +++ b/tests/components/goalzero/test_sensor.py @@ -4,19 +4,12 @@ from homeassistant.components.goalzero.sensor import SENSOR_TYPES from homeassistant.components.sensor import ( ATTR_STATE_CLASS, DOMAIN, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, + SensorStateClass, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_CURRENT, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_SIGNAL_STRENGTH, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_VOLTAGE, ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_WATT_HOUR, @@ -42,42 +35,42 @@ async def test_sensors(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) state = hass.states.get(f"sensor.{DEFAULT_NAME}_watts_in") assert state.state == "0.0" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT state = hass.states.get(f"sensor.{DEFAULT_NAME}_amps_in") assert state.state == "0.0" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_CURRENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.CURRENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ELECTRIC_CURRENT_AMPERE - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT state = hass.states.get(f"sensor.{DEFAULT_NAME}_watts_out") assert state.state == "50.5" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT state = hass.states.get(f"sensor.{DEFAULT_NAME}_amps_out") assert state.state == "2.1" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_CURRENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.CURRENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ELECTRIC_CURRENT_AMPERE - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT state = hass.states.get(f"sensor.{DEFAULT_NAME}_wh_out") assert state.state == "5.23" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_WATT_HOUR - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING state = hass.states.get(f"sensor.{DEFAULT_NAME}_wh_stored") assert state.state == "1330" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_WATT_HOUR - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT state = hass.states.get(f"sensor.{DEFAULT_NAME}_volts") assert state.state == "12.0" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_VOLTAGE + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.VOLTAGE assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ELECTRIC_POTENTIAL_VOLT assert state.attributes.get(ATTR_STATE_CLASS) is None state = hass.states.get(f"sensor.{DEFAULT_NAME}_state_of_charge_percent") assert state.state == "95" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_BATTERY + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.BATTERY assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.attributes.get(ATTR_STATE_CLASS) is None state = hass.states.get(f"sensor.{DEFAULT_NAME}_time_to_empty_full") @@ -87,12 +80,12 @@ async def test_sensors(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) assert state.attributes.get(ATTR_STATE_CLASS) is None state = hass.states.get(f"sensor.{DEFAULT_NAME}_temperature") assert state.state == "25" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS assert state.attributes.get(ATTR_STATE_CLASS) is None state = hass.states.get(f"sensor.{DEFAULT_NAME}_wifi_strength") assert state.state == "-62" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_SIGNAL_STRENGTH + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.SIGNAL_STRENGTH assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SIGNAL_STRENGTH_DECIBELS assert state.attributes.get(ATTR_STATE_CLASS) is None state = hass.states.get(f"sensor.{DEFAULT_NAME}_total_run_time") From 8bf58df624d05c3765839aa63cf6dbce0d70de8c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Dec 2021 01:17:32 -0600 Subject: [PATCH 0651/2644] Fix Non-thread-safe operation in homekit light events (#62147) --- homeassistant/components/homekit/type_lights.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 90c55d52153..f925f0a15a4 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -120,10 +120,11 @@ class Light(HomeAccessory): if self._event_timer: self._event_timer() self._event_timer = async_call_later( - self.hass, CHANGE_COALESCE_TIME_WINDOW, self._send_events + self.hass, CHANGE_COALESCE_TIME_WINDOW, self._async_send_events ) - def _send_events(self, *_): + @callback + def _async_send_events(self, *_): """Process all changes at once.""" _LOGGER.debug("Coalesced _set_chars: %s", self._pending_events) char_values = self._pending_events From ccf8dcd14ac29d41f1df23acc0e56a575c237fa0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Dec 2021 01:19:07 -0600 Subject: [PATCH 0652/2644] Fix Non-thread-safe operation in logbook (#62148) --- homeassistant/components/logbook/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 89e70f346ed..43cdbec2530 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -112,6 +112,7 @@ def log_entry(hass, name, message, domain=None, entity_id=None, context=None): hass.add_job(async_log_entry, hass, name, message, domain, entity_id, context) +@callback @bind_hass def async_log_entry(hass, name, message, domain=None, entity_id=None, context=None): """Add an entry to the logbook.""" From f55668ff40c4c0a53411e53d6334e51d64990867 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Fri, 17 Dec 2021 07:28:31 +0000 Subject: [PATCH 0653/2644] Use DeviceClass Enums in blebox tests (#62109) --- tests/components/blebox/test_cover.py | 10 ++++------ tests/components/blebox/test_sensor.py | 4 ++-- tests/components/blebox/test_switch.py | 8 ++++---- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/tests/components/blebox/test_cover.py b/tests/components/blebox/test_cover.py index e8cf67dad01..3c77dae562a 100644 --- a/tests/components/blebox/test_cover.py +++ b/tests/components/blebox/test_cover.py @@ -8,9 +8,6 @@ import pytest from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, ATTR_POSITION, - DEVICE_CLASS_DOOR, - DEVICE_CLASS_GATE, - DEVICE_CLASS_SHUTTER, STATE_CLOSED, STATE_CLOSING, STATE_OPEN, @@ -19,6 +16,7 @@ from homeassistant.components.cover import ( SUPPORT_OPEN, SUPPORT_SET_POSITION, SUPPORT_STOP, + CoverDeviceClass, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -106,7 +104,7 @@ async def test_init_gatecontroller(gatecontroller, hass, config): state = hass.states.get(entity_id) assert state.name == "gateController-position" - assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_GATE + assert state.attributes[ATTR_DEVICE_CLASS] == CoverDeviceClass.GATE supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] assert supported_features & SUPPORT_OPEN @@ -136,7 +134,7 @@ async def test_init_shutterbox(shutterbox, hass, config): state = hass.states.get(entity_id) assert state.name == "shutterBox-position" - assert entry.original_device_class == DEVICE_CLASS_SHUTTER + assert entry.original_device_class == CoverDeviceClass.SHUTTER supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] assert supported_features & SUPPORT_OPEN @@ -166,7 +164,7 @@ async def test_init_gatebox(gatebox, hass, config): state = hass.states.get(entity_id) assert state.name == "gateBox-position" - assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_DOOR + assert state.attributes[ATTR_DEVICE_CLASS] == CoverDeviceClass.DOOR supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] assert supported_features & SUPPORT_OPEN diff --git a/tests/components/blebox/test_sensor.py b/tests/components/blebox/test_sensor.py index 2281c4ea68c..b7f6d421a12 100644 --- a/tests/components/blebox/test_sensor.py +++ b/tests/components/blebox/test_sensor.py @@ -5,10 +5,10 @@ from unittest.mock import AsyncMock, PropertyMock import blebox_uniapi import pytest +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, - DEVICE_CLASS_TEMPERATURE, STATE_UNKNOWN, TEMP_CELSIUS, ) @@ -45,7 +45,7 @@ async def test_init(tempsensor, hass, config): state = hass.states.get(entity_id) assert state.name == "tempSensor-0.temperature" - assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_TEMPERATURE + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS assert state.state == STATE_UNKNOWN diff --git a/tests/components/blebox/test_switch.py b/tests/components/blebox/test_switch.py index e67c0479cb3..74aeadc33bb 100644 --- a/tests/components/blebox/test_switch.py +++ b/tests/components/blebox/test_switch.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, PropertyMock import blebox_uniapi import pytest -from homeassistant.components.switch import DEVICE_CLASS_SWITCH +from homeassistant.components.switch import SwitchDeviceClass from homeassistant.const import ( ATTR_DEVICE_CLASS, SERVICE_TURN_OFF, @@ -54,7 +54,7 @@ async def test_switchbox_init(switchbox, hass, config): state = hass.states.get(entity_id) assert state.name == "switchBox-0.relay" - assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_SWITCH + assert state.attributes[ATTR_DEVICE_CLASS] == SwitchDeviceClass.SWITCH assert state.state == STATE_OFF @@ -201,7 +201,7 @@ async def test_switchbox_d_init(switchbox_d, hass, config): state = hass.states.get(entity_ids[0]) assert state.name == "switchBoxD-0.relay" - assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_SWITCH + assert state.attributes[ATTR_DEVICE_CLASS] == SwitchDeviceClass.SWITCH assert state.state == STATE_OFF # NOTE: should instead be STATE_UNKNOWN? device_registry = dr.async_get(hass) @@ -218,7 +218,7 @@ async def test_switchbox_d_init(switchbox_d, hass, config): state = hass.states.get(entity_ids[1]) assert state.name == "switchBoxD-1.relay" - assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_SWITCH + assert state.attributes[ATTR_DEVICE_CLASS] == SwitchDeviceClass.SWITCH assert state.state == STATE_OFF # NOTE: should instead be STATE_UNKNOWN? device_registry = dr.async_get(hass) From b25595289ef28b73e9bf2d7ae332046bef679846 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Fri, 17 Dec 2021 07:38:37 +0000 Subject: [PATCH 0654/2644] Use DeviceClass Enums in forecast_solar tests (#62132) --- .../components/forecast_solar/test_sensor.py | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/tests/components/forecast_solar/test_sensor.py b/tests/components/forecast_solar/test_sensor.py index 7698a230751..ee8afe5794b 100644 --- a/tests/components/forecast_solar/test_sensor.py +++ b/tests/components/forecast_solar/test_sensor.py @@ -7,16 +7,14 @@ from homeassistant.components.forecast_solar.const import DOMAIN from homeassistant.components.sensor import ( ATTR_STATE_CLASS, DOMAIN as SENSOR_DOMAIN, - STATE_CLASS_MEASUREMENT, + SensorDeviceClass, + SensorStateClass, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_TIMESTAMP, ENERGY_KILO_WATT_HOUR, POWER_WATT, ) @@ -47,7 +45,7 @@ async def test_sensors( ) assert state.attributes.get(ATTR_STATE_CLASS) is None assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert ATTR_ICON not in state.attributes state = hass.states.get("sensor.energy_production_tomorrow") @@ -62,7 +60,7 @@ async def test_sensors( ) assert state.attributes.get(ATTR_STATE_CLASS) is None assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert ATTR_ICON not in state.attributes state = hass.states.get("sensor.power_highest_peak_time_today") @@ -73,7 +71,7 @@ async def test_sensors( assert state.state == "2021-06-27T13:00:00+00:00" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Highest Power Peak Time - Today" assert state.attributes.get(ATTR_STATE_CLASS) is None - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TIMESTAMP + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes assert ATTR_ICON not in state.attributes @@ -87,7 +85,7 @@ async def test_sensors( state.attributes.get(ATTR_FRIENDLY_NAME) == "Highest Power Peak Time - Tomorrow" ) assert state.attributes.get(ATTR_STATE_CLASS) is None - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TIMESTAMP + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes assert ATTR_ICON not in state.attributes @@ -100,9 +98,9 @@ async def test_sensors( assert ( state.attributes.get(ATTR_FRIENDLY_NAME) == "Estimated Power Production - Now" ) - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER assert ATTR_ICON not in state.attributes state = hass.states.get("sensor.energy_current_hour") @@ -117,7 +115,7 @@ async def test_sensors( ) assert state.attributes.get(ATTR_STATE_CLASS) is None assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert ATTR_ICON not in state.attributes state = hass.states.get("sensor.energy_next_hour") @@ -132,7 +130,7 @@ async def test_sensors( ) assert state.attributes.get(ATTR_STATE_CLASS) is None assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert ATTR_ICON not in state.attributes assert entry.device_id @@ -224,5 +222,5 @@ async def test_enabling_disable_by_default( assert state.attributes.get(ATTR_FRIENDLY_NAME) == name assert state.attributes.get(ATTR_STATE_CLASS) is None assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER assert ATTR_ICON not in state.attributes From 28e373297f5b1c4bc4df23a93ea9f513d0f49089 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Fri, 17 Dec 2021 07:43:03 +0000 Subject: [PATCH 0655/2644] Use DeviceClass Enums in gios tests (#62135) --- tests/components/gios/test_sensor.py | 52 ++++++++++++---------------- 1 file changed, 23 insertions(+), 29 deletions(-) diff --git a/tests/components/gios/test_sensor.py b/tests/components/gios/test_sensor.py index adf151f4819..6603f67e359 100644 --- a/tests/components/gios/test_sensor.py +++ b/tests/components/gios/test_sensor.py @@ -14,7 +14,8 @@ from homeassistant.components.gios.const import ( from homeassistant.components.sensor import ( ATTR_STATE_CLASS, DOMAIN as PLATFORM, - STATE_CLASS_MEASUREMENT, + SensorDeviceClass, + SensorStateClass, ) from homeassistant.const import ( ATTR_ATTRIBUTION, @@ -22,13 +23,6 @@ from homeassistant.const import ( ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - DEVICE_CLASS_AQI, - DEVICE_CLASS_CO, - DEVICE_CLASS_NITROGEN_DIOXIDE, - DEVICE_CLASS_OZONE, - DEVICE_CLASS_PM10, - DEVICE_CLASS_PM25, - DEVICE_CLASS_SULPHUR_DIOXIDE, STATE_UNAVAILABLE, ) from homeassistant.helpers import entity_registry as er @@ -48,7 +42,7 @@ async def test_sensor(hass): assert state.state == "0" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_STATION) == "Test Name 1" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER @@ -65,8 +59,8 @@ async def test_sensor(hass): assert state.state == "252" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_STATION) == "Test Name 1" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_CO - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.CO + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER @@ -82,8 +76,8 @@ async def test_sensor(hass): assert state.state == "7" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_STATION) == "Test Name 1" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_NITROGEN_DIOXIDE - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.NITROGEN_DIOXIDE + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER @@ -99,8 +93,8 @@ async def test_sensor(hass): assert state.state == "96" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_STATION) == "Test Name 1" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_OZONE - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.OZONE + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER @@ -116,8 +110,8 @@ async def test_sensor(hass): assert state.state == "17" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_STATION) == "Test Name 1" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PM10 - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM10 + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER @@ -133,8 +127,8 @@ async def test_sensor(hass): assert state.state == "4" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_STATION) == "Test Name 1" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PM25 - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM25 + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER @@ -150,8 +144,8 @@ async def test_sensor(hass): assert state.state == "4" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_STATION) == "Test Name 1" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_SULPHUR_DIOXIDE - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.SULPHUR_DIOXIDE + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER @@ -167,7 +161,7 @@ async def test_sensor(hass): assert state.state == "dobry" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_STATION) == "Test Name 1" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_AQI + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.AQI assert state.attributes.get(ATTR_STATE_CLASS) is None assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None @@ -228,7 +222,7 @@ async def test_invalid_indexes(hass): assert state.state == "0" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_STATION) == "Test Name 1" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER @@ -245,7 +239,7 @@ async def test_invalid_indexes(hass): assert state.state == "252" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_STATION) == "Test Name 1" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER @@ -261,7 +255,7 @@ async def test_invalid_indexes(hass): assert state.state == "7" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_STATION) == "Test Name 1" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER @@ -277,7 +271,7 @@ async def test_invalid_indexes(hass): assert state.state == "96" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_STATION) == "Test Name 1" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER @@ -293,7 +287,7 @@ async def test_invalid_indexes(hass): assert state.state == "17" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_STATION) == "Test Name 1" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER @@ -309,7 +303,7 @@ async def test_invalid_indexes(hass): assert state.state == "4" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_STATION) == "Test Name 1" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER @@ -325,7 +319,7 @@ async def test_invalid_indexes(hass): assert state.state == "4" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_STATION) == "Test Name 1" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER From 4418b0a43e02b0f6adcab5889822ce93f08b21ea Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Fri, 17 Dec 2021 07:45:39 +0000 Subject: [PATCH 0656/2644] Use DeviceClass Enums in gogogate2 tests (#62137) --- tests/components/gogogate2/test_cover.py | 7 +++---- tests/components/gogogate2/test_sensor.py | 7 +++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/tests/components/gogogate2/test_cover.py b/tests/components/gogogate2/test_cover.py index d3507283426..9f36dbc9008 100644 --- a/tests/components/gogogate2/test_cover.py +++ b/tests/components/gogogate2/test_cover.py @@ -16,11 +16,10 @@ from ismartgate.common import ( ) from homeassistant.components.cover import ( - DEVICE_CLASS_GARAGE, - DEVICE_CLASS_GATE, DOMAIN as COVER_DOMAIN, SUPPORT_CLOSE, SUPPORT_OPEN, + CoverDeviceClass, ) from homeassistant.components.gogogate2.const import ( DEVICE_TYPE_GOGOGATE2, @@ -282,11 +281,11 @@ async def test_availability(ismartgateapi_mock, hass: HomeAssistant) -> None: assert hass.states.get("cover.door1") assert ( hass.states.get("cover.door1").attributes[ATTR_DEVICE_CLASS] - == DEVICE_CLASS_GARAGE + == CoverDeviceClass.GARAGE ) assert ( hass.states.get("cover.door2").attributes[ATTR_DEVICE_CLASS] - == DEVICE_CLASS_GATE + == CoverDeviceClass.GATE ) api.async_info.side_effect = Exception("Error") diff --git a/tests/components/gogogate2/test_sensor.py b/tests/components/gogogate2/test_sensor.py index 129fcac504f..8df88b2b4b7 100644 --- a/tests/components/gogogate2/test_sensor.py +++ b/tests/components/gogogate2/test_sensor.py @@ -17,6 +17,7 @@ from ismartgate.common import ( ) from homeassistant.components.gogogate2.const import DEVICE_TYPE_ISMARTGATE, DOMAIN +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.config_entries import SOURCE_USER from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -25,8 +26,6 @@ from homeassistant.const import ( CONF_IP_ADDRESS, CONF_PASSWORD, CONF_USERNAME, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_TEMPERATURE, STATE_UNAVAILABLE, STATE_UNKNOWN, ) @@ -296,11 +295,11 @@ async def test_availability(ismartgateapi_mock, hass: HomeAssistant) -> None: assert hass.states.get("sensor.door3_temperature") is None assert ( hass.states.get("sensor.door1_battery").attributes[ATTR_DEVICE_CLASS] - == DEVICE_CLASS_BATTERY + == SensorDeviceClass.BATTERY ) assert ( hass.states.get("sensor.door1_temperature").attributes[ATTR_DEVICE_CLASS] - == DEVICE_CLASS_TEMPERATURE + == SensorDeviceClass.TEMPERATURE ) assert ( hass.states.get("sensor.door1_temperature").attributes[ATTR_UNIT_OF_MEASUREMENT] From beb824c4524967b2ede5a3f90e3ec4085cb79206 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 17 Dec 2021 00:07:51 -0800 Subject: [PATCH 0657/2644] Bump google-nest-sdm to 0.4.9 (#62160) --- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 0c14e59babc..bca37ce41c6 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["ffmpeg", "http", "media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.8"], + "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.9"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 5a21ad9293f..6082f761907 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -747,7 +747,7 @@ google-cloud-pubsub==2.1.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==0.4.8 +google-nest-sdm==0.4.9 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 55bb1469d7f..8de9b54a683 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -470,7 +470,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.1.0 # homeassistant.components.nest -google-nest-sdm==0.4.8 +google-nest-sdm==0.4.9 # homeassistant.components.google_travel_time googlemaps==2.5.1 From 72e1fa13928ebabea247840042f7fb863dce1de8 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 17 Dec 2021 09:16:02 +0100 Subject: [PATCH 0658/2644] Improve availability for Shelly Valve (#62129) --- homeassistant/components/shelly/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index cdbff59fa02..d76b66ce181 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -187,7 +187,7 @@ class BlockSleepingClimate( """Device availability.""" if self.device_block is not None: return not cast(bool, self.device_block.valveError) - return True + return self.wrapper.last_update_success @property def hvac_mode(self) -> str: From 67255d4b529d64e123abcaf7cf35e290d7aa6d53 Mon Sep 17 00:00:00 2001 From: sindudas Date: Fri, 17 Dec 2021 09:34:27 +0100 Subject: [PATCH 0659/2644] Update ebusdpy version (#59899) --- homeassistant/components/ebusd/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ebusd/manifest.json b/homeassistant/components/ebusd/manifest.json index 347fee0bc85..390e8efe7d5 100644 --- a/homeassistant/components/ebusd/manifest.json +++ b/homeassistant/components/ebusd/manifest.json @@ -2,7 +2,7 @@ "domain": "ebusd", "name": "ebusd", "documentation": "https://www.home-assistant.io/integrations/ebusd", - "requirements": ["ebusdpy==0.0.16"], + "requirements": ["ebusdpy==0.0.17"], "codeowners": [], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 6082f761907..78e573cb07b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -579,7 +579,7 @@ dweepy==0.3.0 dynalite_devices==0.1.46 # homeassistant.components.ebusd -ebusdpy==0.0.16 +ebusdpy==0.0.17 # homeassistant.components.ecoal_boiler ecoaliface==0.4.0 From a6cfbd4072e3bfb8004a686cfdd545dcdef007fb Mon Sep 17 00:00:00 2001 From: Ian Date: Fri, 17 Dec 2021 00:39:55 -0800 Subject: [PATCH 0660/2644] Nextbus upcoming sort as integer (#61416) --- homeassistant/components/nextbus/sensor.py | 2 +- tests/components/nextbus/test_sensor.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nextbus/sensor.py b/homeassistant/components/nextbus/sensor.py index 604bd0c6602..28fbaeb75d3 100644 --- a/homeassistant/components/nextbus/sensor.py +++ b/homeassistant/components/nextbus/sensor.py @@ -218,7 +218,7 @@ class NextBusDepartureSensor(SensorEntity): # Generate list of upcoming times self._attributes["upcoming"] = ", ".join( - sorted(p["minutes"] for p in predictions) + sorted((p["minutes"] for p in predictions), key=int) ) latest_prediction = maybe_first(predictions) diff --git a/tests/components/nextbus/test_sensor.py b/tests/components/nextbus/test_sensor.py index 016afed2b0f..f113d5c83da 100644 --- a/tests/components/nextbus/test_sensor.py +++ b/tests/components/nextbus/test_sensor.py @@ -40,6 +40,7 @@ BASIC_RESULTS = { {"minutes": "1", "epochTime": "1553807371000"}, {"minutes": "2", "epochTime": "1553807372000"}, {"minutes": "3", "epochTime": "1553807373000"}, + {"minutes": "10", "epochTime": "1553807380000"}, ], }, } @@ -128,7 +129,7 @@ async def test_verify_valid_state( assert state.attributes["route"] == VALID_ROUTE_TITLE assert state.attributes["stop"] == VALID_STOP_TITLE assert state.attributes["direction"] == "Outbound" - assert state.attributes["upcoming"] == "1, 2, 3" + assert state.attributes["upcoming"] == "1, 2, 3, 10" async def test_message_dict( From ad171944da5696e1e2a620e5ee552d8036506aa3 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Fri, 17 Dec 2021 10:50:10 +0100 Subject: [PATCH 0661/2644] Add guard in call to activate_scene in Hue (#62177) --- homeassistant/components/hue/services.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hue/services.py b/homeassistant/components/hue/services.py index 72e88f0d956..6e68bbffbb8 100644 --- a/homeassistant/components/hue/services.py +++ b/homeassistant/components/hue/services.py @@ -146,8 +146,10 @@ async def hue_activate_scene_v2( continue # found match! if transition: - transition = transition * 100 # in steps of 100ms - await api.scenes.recall(scene.id, dynamic=dynamic, duration=transition) + transition = transition * 1000 # transition is in ms + await bridge.async_request_call( + api.scenes.recall, scene.id, dynamic=dynamic, duration=transition + ) return True LOGGER.debug( "Unable to find scene %s for group %s on bridge %s", From 07532c3153e3e614a70ade43477ab6fceec28894 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Fri, 17 Dec 2021 05:07:18 -0500 Subject: [PATCH 0662/2644] Use enums in sensor tests (#62152) --- .../sensor/test_device_condition.py | 18 +++----- .../components/sensor/test_device_trigger.py | 18 +++----- tests/components/sensor/test_init.py | 41 ++++++++++--------- .../sensor/test_significant_change.py | 22 ++++------ 4 files changed, 40 insertions(+), 59 deletions(-) diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py index 5742f7b47c4..5504bd997af 100644 --- a/tests/components/sensor/test_device_condition.py +++ b/tests/components/sensor/test_device_condition.py @@ -2,14 +2,9 @@ import pytest import homeassistant.components.automation as automation -from homeassistant.components.sensor import DOMAIN +from homeassistant.components.sensor import DEVICE_CLASSES, DOMAIN, SensorDeviceClass from homeassistant.components.sensor.device_condition import ENTITY_CONDITIONS -from homeassistant.const import ( - CONF_PLATFORM, - DEVICE_CLASS_BATTERY, - PERCENTAGE, - STATE_UNKNOWN, -) +from homeassistant.const import CONF_PLATFORM, PERCENTAGE, STATE_UNKNOWN from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component @@ -22,10 +17,7 @@ from tests.common import ( mock_registry, ) from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401 -from tests.testing_config.custom_components.test.sensor import ( - DEVICE_CLASSES, - UNITS_OF_MEASUREMENT, -) +from tests.testing_config.custom_components.test.sensor import UNITS_OF_MEASUREMENT @pytest.fixture @@ -126,8 +118,8 @@ async def test_get_conditions_no_state(hass, device_reg, entity_reg): @pytest.mark.parametrize( "set_state,device_class_reg,device_class_state,unit_reg,unit_state", [ - (False, DEVICE_CLASS_BATTERY, None, PERCENTAGE, None), - (True, None, DEVICE_CLASS_BATTERY, None, PERCENTAGE), + (False, SensorDeviceClass.BATTERY, None, PERCENTAGE, None), + (True, None, SensorDeviceClass.BATTERY, None, PERCENTAGE), ], ) async def test_get_condition_capabilities( diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index b8b3ee46a43..41671a4faa8 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -4,14 +4,9 @@ from datetime import timedelta import pytest import homeassistant.components.automation as automation -from homeassistant.components.sensor import DOMAIN +from homeassistant.components.sensor import DEVICE_CLASSES, DOMAIN, SensorDeviceClass from homeassistant.components.sensor.device_trigger import ENTITY_TRIGGERS -from homeassistant.const import ( - CONF_PLATFORM, - DEVICE_CLASS_BATTERY, - PERCENTAGE, - STATE_UNKNOWN, -) +from homeassistant.const import CONF_PLATFORM, PERCENTAGE, STATE_UNKNOWN from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -26,10 +21,7 @@ from tests.common import ( mock_registry, ) from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401 -from tests.testing_config.custom_components.test.sensor import ( - DEVICE_CLASSES, - UNITS_OF_MEASUREMENT, -) +from tests.testing_config.custom_components.test.sensor import UNITS_OF_MEASUREMENT @pytest.fixture @@ -93,8 +85,8 @@ async def test_get_triggers(hass, device_reg, entity_reg, enable_custom_integrat @pytest.mark.parametrize( "set_state,device_class_reg,device_class_state,unit_reg,unit_state", [ - (False, DEVICE_CLASS_BATTERY, None, PERCENTAGE, None), - (True, None, DEVICE_CLASS_BATTERY, None, PERCENTAGE), + (False, SensorDeviceClass.BATTERY, None, PERCENTAGE, None), + (True, None, SensorDeviceClass.BATTERY, None, PERCENTAGE), ], ) async def test_get_trigger_capabilities( diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index d5deee41679..919736fb59e 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -4,12 +4,9 @@ from datetime import date, datetime, timezone import pytest from pytest import approx -from homeassistant.components.sensor import SensorEntityDescription +from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, - DEVICE_CLASS_DATE, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_TIMESTAMP, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT, @@ -45,7 +42,7 @@ async def test_temperature_conversion( name="Test", native_value=str(native_value), native_unit_of_measurement=native_unit, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ) entity0 = platform.ENTITIES["0"] @@ -124,21 +121,23 @@ async def test_datetime_conversion(hass, caplog, enable_custom_integrations): platform = getattr(hass.components, "test.sensor") platform.init(empty=True) platform.ENTITIES["0"] = platform.MockSensor( - name="Test", native_value=test_timestamp, device_class=DEVICE_CLASS_TIMESTAMP + name="Test", + native_value=test_timestamp, + device_class=SensorDeviceClass.TIMESTAMP, ) platform.ENTITIES["1"] = platform.MockSensor( - name="Test", native_value=test_date, device_class=DEVICE_CLASS_DATE + name="Test", native_value=test_date, device_class=SensorDeviceClass.DATE ) platform.ENTITIES["2"] = platform.MockSensor( - name="Test", native_value=None, device_class=DEVICE_CLASS_TIMESTAMP + name="Test", native_value=None, device_class=SensorDeviceClass.TIMESTAMP ) platform.ENTITIES["3"] = platform.MockSensor( - name="Test", native_value=None, device_class=DEVICE_CLASS_DATE + name="Test", native_value=None, device_class=SensorDeviceClass.DATE ) platform.ENTITIES["4"] = platform.MockSensor( name="Test", native_value=test_local_timestamp, - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, ) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) @@ -163,44 +162,44 @@ async def test_datetime_conversion(hass, caplog, enable_custom_integrations): @pytest.mark.parametrize( "device_class,native_value,state_value", [ - (DEVICE_CLASS_DATE, "2021-11-09", "2021-11-09"), + (SensorDeviceClass.DATE, "2021-11-09", "2021-11-09"), ( - DEVICE_CLASS_DATE, + SensorDeviceClass.DATE, "2021-01-09T12:00:00+00:00", "2021-01-09", ), ( - DEVICE_CLASS_DATE, + SensorDeviceClass.DATE, "2021-01-09T00:00:00+01:00", "2021-01-08", ), ( - DEVICE_CLASS_TIMESTAMP, + SensorDeviceClass.TIMESTAMP, "2021-01-09T12:00:00+00:00", "2021-01-09T12:00:00+00:00", ), ( - DEVICE_CLASS_TIMESTAMP, + SensorDeviceClass.TIMESTAMP, "2021-01-09 12:00:00+00:00", "2021-01-09T12:00:00+00:00", ), ( - DEVICE_CLASS_TIMESTAMP, + SensorDeviceClass.TIMESTAMP, "2021-01-09T12:00:00+04:00", "2021-01-09T08:00:00+00:00", ), ( - DEVICE_CLASS_TIMESTAMP, + SensorDeviceClass.TIMESTAMP, "2021-01-09 12:00:00+01:00", "2021-01-09T11:00:00+00:00", ), ( - DEVICE_CLASS_TIMESTAMP, + SensorDeviceClass.TIMESTAMP, "2021-01-09 12:00:00", "2021-01-09T12:00:00", ), ( - DEVICE_CLASS_TIMESTAMP, + SensorDeviceClass.TIMESTAMP, "2021-01-09T12:00:00", "2021-01-09T12:00:00", ), @@ -237,7 +236,9 @@ async def test_reject_timezoneless_datetime_str( platform = getattr(hass.components, "test.sensor") platform.init(empty=True) platform.ENTITIES["0"] = platform.MockSensor( - name="Test", native_value=test_timestamp, device_class=DEVICE_CLASS_TIMESTAMP + name="Test", + native_value=test_timestamp, + device_class=SensorDeviceClass.TIMESTAMP, ) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) diff --git a/tests/components/sensor/test_significant_change.py b/tests/components/sensor/test_significant_change.py index 22a2c22ecc7..051a92f3b07 100644 --- a/tests/components/sensor/test_significant_change.py +++ b/tests/components/sensor/test_significant_change.py @@ -1,13 +1,7 @@ """Test the sensor significant change platform.""" import pytest -from homeassistant.components.sensor.significant_change import ( - DEVICE_CLASS_AQI, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_TEMPERATURE, - async_check_significant_change, -) +from homeassistant.components.sensor import SensorDeviceClass, significant_change from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, @@ -16,24 +10,24 @@ from homeassistant.const import ( ) AQI_ATTRS = { - ATTR_DEVICE_CLASS: DEVICE_CLASS_AQI, + ATTR_DEVICE_CLASS: SensorDeviceClass.AQI, } BATTERY_ATTRS = { - ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY, + ATTR_DEVICE_CLASS: SensorDeviceClass.BATTERY, } HUMIDITY_ATTRS = { - ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_DEVICE_CLASS: SensorDeviceClass.HUMIDITY, } TEMP_CELSIUS_ATTRS = { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, } TEMP_FREEDOM_ATTRS = { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT, } @@ -63,6 +57,8 @@ TEMP_FREEDOM_ATTRS = { async def test_significant_change_temperature(old_state, new_state, attrs, result): """Detect temperature significant changes.""" assert ( - async_check_significant_change(None, old_state, attrs, new_state, attrs) + significant_change.async_check_significant_change( + None, old_state, attrs, new_state, attrs + ) is result ) From 626a3f35f5dc5d7999b2fe5950b7c1a34ad29ed4 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Fri, 17 Dec 2021 05:09:24 -0500 Subject: [PATCH 0663/2644] Use enums in helpers tests (#62141) --- tests/helpers/test_condition.py | 8 ++++---- tests/helpers/test_significant_change.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index c4ceff89b64..d958c633b62 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -6,7 +6,7 @@ import pytest from homeassistant.components import sun import homeassistant.components.automation as automation -from homeassistant.components.sensor import DEVICE_CLASS_TIMESTAMP +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( ATTR_DEVICE_CLASS, CONF_CONDITION, @@ -854,12 +854,12 @@ async def test_time_using_sensor(hass): hass.states.async_set( "sensor.am", "2021-06-03 13:00:00.000000+00:00", # 6 am local time - {ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP}, + {ATTR_DEVICE_CLASS: SensorDeviceClass.TIMESTAMP}, ) hass.states.async_set( "sensor.pm", "2020-06-01 01:00:00.000000+00:00", # 6 pm local time - {ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP}, + {ATTR_DEVICE_CLASS: SensorDeviceClass.TIMESTAMP}, ) hass.states.async_set( "sensor.no_device_class", @@ -868,7 +868,7 @@ async def test_time_using_sensor(hass): hass.states.async_set( "sensor.invalid_timestamp", "This is not a timestamp", - {ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP}, + {ATTR_DEVICE_CLASS: SensorDeviceClass.TIMESTAMP}, ) with patch( diff --git a/tests/helpers/test_significant_change.py b/tests/helpers/test_significant_change.py index 79f3dd3fe3e..b707e369931 100644 --- a/tests/helpers/test_significant_change.py +++ b/tests/helpers/test_significant_change.py @@ -1,7 +1,7 @@ """Test significant change helper.""" import pytest -from homeassistant.components.sensor import DEVICE_CLASS_BATTERY +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ATTR_DEVICE_CLASS, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import State from homeassistant.helpers import significant_change @@ -26,7 +26,7 @@ async def checker_fixture(hass): async def test_signicant_change(hass, checker): """Test initialize helper works.""" ent_id = "test_domain.test_entity" - attrs = {ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY} + attrs = {ATTR_DEVICE_CLASS: SensorDeviceClass.BATTERY} assert checker.async_is_significant_change(State(ent_id, "100", attrs)) @@ -50,7 +50,7 @@ async def test_signicant_change(hass, checker): async def test_significant_change_extra(hass, checker): """Test extra significant checker works.""" ent_id = "test_domain.test_entity" - attrs = {ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY} + attrs = {ATTR_DEVICE_CLASS: SensorDeviceClass.BATTERY} assert checker.async_is_significant_change(State(ent_id, "100", attrs), extra_arg=1) assert checker.async_is_significant_change(State(ent_id, "200", attrs), extra_arg=1) From 499cae9900168771c42c791852cd47951a300747 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Fri, 17 Dec 2021 05:10:06 -0500 Subject: [PATCH 0664/2644] Use enums for testing_config (#62140) --- .../custom_components/test/sensor.py | 57 ++++++++++--------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/tests/testing_config/custom_components/test/sensor.py b/tests/testing_config/custom_components/test/sensor.py index ea3b0fe7080..436bafc11ff 100644 --- a/tests/testing_config/custom_components/test/sensor.py +++ b/tests/testing_config/custom_components/test/sensor.py @@ -3,7 +3,11 @@ Provide a mock sensor platform. Call init before using it in your tests to ensure clean test data. """ -import homeassistant.components.sensor as sensor +from homeassistant.components.sensor import ( + DEVICE_CLASSES, + SensorDeviceClass, + SensorEntity, +) from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_MILLION, @@ -16,34 +20,33 @@ from homeassistant.const import ( from tests.common import MockEntity -DEVICE_CLASSES = list(sensor.DEVICE_CLASSES) DEVICE_CLASSES.append("none") UNITS_OF_MEASUREMENT = { - sensor.DEVICE_CLASS_BATTERY: PERCENTAGE, # % of battery that is left - sensor.DEVICE_CLASS_CO: CONCENTRATION_PARTS_PER_MILLION, # ppm of CO concentration - sensor.DEVICE_CLASS_CO2: CONCENTRATION_PARTS_PER_MILLION, # ppm of CO2 concentration - sensor.DEVICE_CLASS_HUMIDITY: PERCENTAGE, # % of humidity in the air - sensor.DEVICE_CLASS_ILLUMINANCE: "lm", # current light level (lx/lm) - sensor.DEVICE_CLASS_NITROGEN_DIOXIDE: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, # µg/m³ of nitrogen dioxide - sensor.DEVICE_CLASS_NITROGEN_MONOXIDE: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, # µg/m³ of nitrogen monoxide - sensor.DEVICE_CLASS_NITROUS_OXIDE: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, # µg/m³ of nitrogen oxide - sensor.DEVICE_CLASS_OZONE: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, # µg/m³ of ozone - sensor.DEVICE_CLASS_PM1: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, # µg/m³ of PM1 - sensor.DEVICE_CLASS_PM10: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, # µg/m³ of PM10 - sensor.DEVICE_CLASS_PM25: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, # µg/m³ of PM2.5 - sensor.DEVICE_CLASS_SIGNAL_STRENGTH: SIGNAL_STRENGTH_DECIBELS, # signal strength (dB/dBm) - sensor.DEVICE_CLASS_SULPHUR_DIOXIDE: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, # µg/m³ of sulphur dioxide - sensor.DEVICE_CLASS_TEMPERATURE: "C", # temperature (C/F) - sensor.DEVICE_CLASS_PRESSURE: PRESSURE_HPA, # pressure (hPa/mbar) - sensor.DEVICE_CLASS_POWER: "kW", # power (W/kW) - sensor.DEVICE_CLASS_CURRENT: "A", # current (A) - sensor.DEVICE_CLASS_ENERGY: "kWh", # energy (Wh/kWh) - sensor.DEVICE_CLASS_FREQUENCY: FREQUENCY_GIGAHERTZ, # energy (Hz/kHz/MHz/GHz) - sensor.DEVICE_CLASS_POWER_FACTOR: PERCENTAGE, # power factor (no unit, min: -1.0, max: 1.0) - sensor.DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, # µg/m³ of vocs - sensor.DEVICE_CLASS_VOLTAGE: "V", # voltage (V) - sensor.DEVICE_CLASS_GAS: VOLUME_CUBIC_METERS, # gas (m³) + SensorDeviceClass.BATTERY: PERCENTAGE, # % of battery that is left + SensorDeviceClass.CO: CONCENTRATION_PARTS_PER_MILLION, # ppm of CO concentration + SensorDeviceClass.CO2: CONCENTRATION_PARTS_PER_MILLION, # ppm of CO2 concentration + SensorDeviceClass.HUMIDITY: PERCENTAGE, # % of humidity in the air + SensorDeviceClass.ILLUMINANCE: "lm", # current light level (lx/lm) + SensorDeviceClass.NITROGEN_DIOXIDE: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, # µg/m³ of nitrogen dioxide + SensorDeviceClass.NITROGEN_MONOXIDE: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, # µg/m³ of nitrogen monoxide + SensorDeviceClass.NITROUS_OXIDE: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, # µg/m³ of nitrogen oxide + SensorDeviceClass.OZONE: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, # µg/m³ of ozone + SensorDeviceClass.PM1: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, # µg/m³ of PM1 + SensorDeviceClass.PM10: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, # µg/m³ of PM10 + SensorDeviceClass.PM25: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, # µg/m³ of PM2.5 + SensorDeviceClass.SIGNAL_STRENGTH: SIGNAL_STRENGTH_DECIBELS, # signal strength (dB/dBm) + SensorDeviceClass.SULPHUR_DIOXIDE: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, # µg/m³ of sulphur dioxide + SensorDeviceClass.TEMPERATURE: "C", # temperature (C/F) + SensorDeviceClass.PRESSURE: PRESSURE_HPA, # pressure (hPa/mbar) + SensorDeviceClass.POWER: "kW", # power (W/kW) + SensorDeviceClass.CURRENT: "A", # current (A) + SensorDeviceClass.ENERGY: "kWh", # energy (Wh/kWh) + SensorDeviceClass.FREQUENCY: FREQUENCY_GIGAHERTZ, # energy (Hz/kHz/MHz/GHz) + SensorDeviceClass.POWER_FACTOR: PERCENTAGE, # power factor (no unit, min: -1.0, max: 1.0) + SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, # µg/m³ of vocs + SensorDeviceClass.VOLTAGE: "V", # voltage (V) + SensorDeviceClass.GAS: VOLUME_CUBIC_METERS, # gas (m³) } ENTITIES = {} @@ -75,7 +78,7 @@ async def async_setup_platform( async_add_entities_callback(list(ENTITIES.values())) -class MockSensor(MockEntity, sensor.SensorEntity): +class MockSensor(MockEntity, SensorEntity): """Mock Sensor class.""" @property From 85c122f3e560e088221d8194904d3952b3f88e36 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Fri, 17 Dec 2021 05:12:43 -0500 Subject: [PATCH 0665/2644] Use enums in unifi tests (#62151) --- tests/components/unifi/test_sensor.py | 6 +++--- tests/components/unifi/test_switch.py | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index 3794c46988d..c0b45efb119 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -15,9 +15,9 @@ from homeassistant.components.unifi.const import ( CONF_TRACK_DEVICES, DOMAIN as UNIFI_DOMAIN, ) -from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity import EntityCategory import homeassistant.util.dt as dt_util from .test_controller import setup_unifi_integration @@ -79,7 +79,7 @@ async def test_bandwidth_sensors(hass, aioclient_mock, mock_unifi_websocket): ent_reg = er.async_get(hass) assert ( ent_reg.async_get("sensor.wired_client_rx").entity_category - == ENTITY_CATEGORY_DIAGNOSTIC + is EntityCategory.DIAGNOSTIC ) # Verify state update @@ -190,7 +190,7 @@ async def test_uptime_sensors( ent_reg = er.async_get(hass) assert ( ent_reg.async_get("sensor.client1_uptime").entity_category - == ENTITY_CATEGORY_DIAGNOSTIC + is EntityCategory.DIAGNOSTIC ) # Verify normal new event doesn't change uptime diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index 29640b7a4b4..38231ef9609 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -16,9 +16,10 @@ from homeassistant.components.unifi.const import ( DOMAIN as UNIFI_DOMAIN, ) from homeassistant.components.unifi.switch import POE_SWITCH -from homeassistant.const import ENTITY_CATEGORY_CONFIG, STATE_OFF, STATE_ON +from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity import EntityCategory from .test_controller import ( CONTROLLER_HOST, @@ -408,7 +409,7 @@ async def test_switches(hass, aioclient_mock): "switch.block_client_1", "switch.block_media_streaming", ): - assert ent_reg.async_get(entry_id).entity_category == ENTITY_CATEGORY_CONFIG + assert ent_reg.async_get(entry_id).entity_category is EntityCategory.CONFIG # Block and unblock client From 112e259437c12d11bee1da93d6acdf839d80b743 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 17 Dec 2021 11:41:54 +0100 Subject: [PATCH 0666/2644] Fix threading error in scripts with repeat or choose actions (#62168) --- homeassistant/helpers/script.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index d4d37e1b4ac..20a1dbb8aec 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -1052,6 +1052,7 @@ class Script: if self._change_listener_job: self._hass.async_run_hass_job(self._change_listener_job) + @callback def _chain_change_listener(self, sub_script: Script) -> None: if sub_script.is_running: self.last_action = sub_script.last_action From 9164a74fcc0088fb894b116f97dd97404220d58e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 17 Dec 2021 11:42:15 +0100 Subject: [PATCH 0667/2644] Fix threading error in zha (#62170) --- homeassistant/components/zha/core/discovery.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 1d9edb82980..780d7bc384b 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -234,6 +234,7 @@ class GroupProbe: unsub() self._unsubs.remove(unsub) + @callback def _reprobe_group(self, group_id: int) -> None: """Reprobe a group for entities after its members change.""" zha_gateway = self._hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY] From 8c777011b65c58266a568e9e3e254db43f0312b4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 17 Dec 2021 12:03:29 +0100 Subject: [PATCH 0668/2644] Fix threading error in qwikswitch (#62173) --- homeassistant/components/qwikswitch/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/qwikswitch/__init__.py b/homeassistant/components/qwikswitch/__init__.py index eea02fb9f54..be9cff87250 100644 --- a/homeassistant/components/qwikswitch/__init__.py +++ b/homeassistant/components/qwikswitch/__init__.py @@ -221,7 +221,7 @@ async def async_setup(hass, config): @callback def async_start(_): """Start listening.""" - hass.async_add_job(qsusb.listen, callback_qs_listen) + qsusb.listen(callback_qs_listen) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, async_start) From 0f2a3c074e855e917cb044c1125af16ec3b6b694 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Fri, 17 Dec 2021 11:33:41 +0000 Subject: [PATCH 0669/2644] Force Lyric token refresh on first authentication failure (#62100) --- homeassistant/components/lyric/__init__.py | 27 ++++++++++++++++++---- homeassistant/components/lyric/api.py | 12 ++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/lyric/__init__.py b/homeassistant/components/lyric/__init__.py index 623abb54c4b..3709fa5445f 100644 --- a/homeassistant/components/lyric/__init__.py +++ b/homeassistant/components/lyric/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import timedelta +from http import HTTPStatus import logging from aiohttp.client_exceptions import ClientResponseError @@ -30,7 +31,11 @@ from homeassistant.helpers.update_coordinator import ( UpdateFailed, ) -from .api import ConfigEntryLyricClient, LyricLocalOAuth2Implementation +from .api import ( + ConfigEntryLyricClient, + LyricLocalOAuth2Implementation, + OAuth2SessionLyric, +) from .config_flow import OAuth2FlowHandler from .const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN @@ -84,21 +89,35 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) session = aiohttp_client.async_get_clientsession(hass) - oauth_session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) + oauth_session = OAuth2SessionLyric(hass, entry, implementation) client = ConfigEntryLyricClient(session, oauth_session) client_id = hass.data[DOMAIN][CONF_CLIENT_ID] lyric = Lyric(client, client_id) - async def async_update_data() -> Lyric: + async def async_update_data(force_refresh_token: bool = False) -> Lyric: """Fetch data from Lyric.""" - await oauth_session.async_ensure_token_valid() + try: + if not force_refresh_token: + await oauth_session.async_ensure_token_valid() + else: + await oauth_session.force_refresh_token() + except ClientResponseError as exception: + if exception.status in (HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN): + raise ConfigEntryAuthFailed from exception + raise UpdateFailed(exception) from exception + try: async with async_timeout.timeout(60): await lyric.get_locations() return lyric except LyricAuthenticationException as exception: + # Attempt to refresh the token before failing. + # Honeywell appear to have issues keeping tokens saved. + _LOGGER.debug("Authentication failed. Attempting to refresh token") + if not force_refresh_token: + return await async_update_data(force_refresh_token=True) raise ConfigEntryAuthFailed from exception except (LyricException, ClientResponseError) as exception: raise UpdateFailed(exception) from exception diff --git a/homeassistant/components/lyric/api.py b/homeassistant/components/lyric/api.py index 3b23f802ded..4a8aa44417f 100644 --- a/homeassistant/components/lyric/api.py +++ b/homeassistant/components/lyric/api.py @@ -8,6 +8,18 @@ from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.aiohttp_client import async_get_clientsession +class OAuth2SessionLyric(config_entry_oauth2_flow.OAuth2Session): + """OAuth2Session for Lyric.""" + + async def force_refresh_token(self) -> None: + """Force a token refresh.""" + new_token = await self.implementation.async_refresh_token(self.token) + + self.hass.config_entries.async_update_entry( + self.config_entry, data={**self.config_entry.data, "token": new_token} + ) + + class ConfigEntryLyricClient(LyricClient): """Provide Honeywell Lyric authentication tied to an OAuth2 based config entry.""" From e771421ed0fd1dafce5c09d0b8e84e3c1c9c82bb Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Fri, 17 Dec 2021 12:40:32 +0100 Subject: [PATCH 0670/2644] Move Solarlog state to entity description (#62093) * Move value to const * Move value to const * remove cast * Remove Statetype import * Add in and output for callable * fix mypy * Add int to callable * fix callable * Only convert value * Add datetime import * Update homeassistant/components/solarlog/const.py Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/solarlog/const.py | 37 +++++++++++++-------- homeassistant/components/solarlog/sensor.py | 16 +++------ 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/solarlog/const.py b/homeassistant/components/solarlog/const.py index 3159bc46218..769064089b2 100644 --- a/homeassistant/components/solarlog/const.py +++ b/homeassistant/components/solarlog/const.py @@ -1,7 +1,9 @@ """Constants for the Solar-Log integration.""" from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass +from datetime import datetime from homeassistant.components.sensor import ( SensorDeviceClass, @@ -14,6 +16,7 @@ from homeassistant.const import ( PERCENTAGE, POWER_WATT, ) +from homeassistant.util.dt import as_local DOMAIN = "solarlog" @@ -26,7 +29,7 @@ DEFAULT_NAME = "solarlog" class SolarLogSensorEntityDescription(SensorEntityDescription): """Describes Solarlog sensor entity.""" - factor: float | None = None + value: Callable[[float | int], float] | Callable[[datetime], datetime] | None = None SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( @@ -34,6 +37,7 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( key="time", name="last update", device_class=SensorDeviceClass.TIMESTAMP, + value=as_local, ), SolarLogSensorEntityDescription( key="power_ac", @@ -68,36 +72,41 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( name="yield day", icon="mdi:solar-power", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - factor=0.001, + device_class=SensorDeviceClass.ENERGY, + value=lambda value: round(value / 1000, 3), ), SolarLogSensorEntityDescription( key="yield_yesterday", name="yield yesterday", icon="mdi:solar-power", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - factor=0.001, + device_class=SensorDeviceClass.ENERGY, + value=lambda value: round(value / 1000, 3), ), SolarLogSensorEntityDescription( key="yield_month", name="yield month", icon="mdi:solar-power", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - factor=0.001, + device_class=SensorDeviceClass.ENERGY, + value=lambda value: round(value / 1000, 3), ), SolarLogSensorEntityDescription( key="yield_year", name="yield year", icon="mdi:solar-power", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - factor=0.001, + device_class=SensorDeviceClass.ENERGY, + value=lambda value: round(value / 1000, 3), ), SolarLogSensorEntityDescription( key="yield_total", name="yield total", icon="mdi:solar-power", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, - factor=0.001, + value=lambda value: round(value / 1000, 3), ), SolarLogSensorEntityDescription( key="consumption_ac", @@ -111,28 +120,28 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( name="consumption day", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, - factor=0.001, + value=lambda value: round(value / 1000, 3), ), SolarLogSensorEntityDescription( key="consumption_yesterday", name="consumption yesterday", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, - factor=0.001, + value=lambda value: round(value / 1000, 3), ), SolarLogSensorEntityDescription( key="consumption_month", name="consumption month", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, - factor=0.001, + value=lambda value: round(value / 1000, 3), ), SolarLogSensorEntityDescription( key="consumption_year", name="consumption year", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, - factor=0.001, + value=lambda value: round(value / 1000, 3), ), SolarLogSensorEntityDescription( key="consumption_total", @@ -140,7 +149,7 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, - factor=0.001, + value=lambda value: round(value / 1000, 3), ), SolarLogSensorEntityDescription( key="total_power", @@ -164,7 +173,7 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.POWER_FACTOR, state_class=SensorStateClass.MEASUREMENT, - factor=100, + value=lambda value: round(value * 100, 1), ), SolarLogSensorEntityDescription( key="efficiency", @@ -172,7 +181,7 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.POWER_FACTOR, state_class=SensorStateClass.MEASUREMENT, - factor=100, + value=lambda value: round(value * 100, 1), ), SolarLogSensorEntityDescription( key="power_available", @@ -188,6 +197,6 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.POWER_FACTOR, state_class=SensorStateClass.MEASUREMENT, - factor=100, + value=lambda value: round(value * 100, 1), ), ) diff --git a/homeassistant/components/solarlog/sensor.py b/homeassistant/components/solarlog/sensor.py index 5c4c2bfad28..511f29338b8 100644 --- a/homeassistant/components/solarlog/sensor.py +++ b/homeassistant/components/solarlog/sensor.py @@ -2,7 +2,6 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.helpers import update_coordinator from homeassistant.helpers.entity import DeviceInfo -from homeassistant.util.dt import as_local from . import SolarlogData from .const import DOMAIN, SENSOR_TYPES, SolarLogSensorEntityDescription @@ -41,14 +40,7 @@ class SolarlogSensor(update_coordinator.CoordinatorEntity, SensorEntity): @property def native_value(self): """Return the native sensor value.""" - if self.entity_description.key == "time": - state = as_local( - getattr(self.coordinator.data, self.entity_description.key) - ) - else: - result = getattr(self.coordinator.data, self.entity_description.key) - if self.entity_description.factor: - state = round(result * self.entity_description.factor, 3) - else: - state = result - return state + raw_attr = getattr(self.coordinator.data, self.entity_description.key) + if self.entity_description.value: + return self.entity_description.value(raw_attr) + return raw_attr From 8cde2e805b0129222695fbab4e5c67da8cbae6e1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 17 Dec 2021 12:44:29 +0100 Subject: [PATCH 0671/2644] Use new SensorDeviceClass enum in climacell (#61362) Co-authored-by: epenet --- homeassistant/components/climacell/const.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/climacell/const.py b/homeassistant/components/climacell/const.py index 3b5bc360d7c..83152cc38f2 100644 --- a/homeassistant/components/climacell/const.py +++ b/homeassistant/components/climacell/const.py @@ -17,7 +17,7 @@ from pyclimacell.const import ( WeatherCode, ) -from homeassistant.components.sensor import SensorEntityDescription +from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLOUDY, @@ -37,9 +37,6 @@ from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, - DEVICE_CLASS_CO, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT, IRRADIATION_WATTS_PER_SQUARE_METER, LENGTH_KILOMETERS, @@ -180,7 +177,7 @@ CC_SENSOR_TYPES = ( unit_metric=TEMP_CELSIUS, metric_conversion=lambda val: temp_convert(val, TEMP_FAHRENHEIT, TEMP_CELSIUS), is_metric_check=True, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), ClimaCellSensorEntityDescription( key=CC_ATTR_DEW_POINT, @@ -189,7 +186,7 @@ CC_SENSOR_TYPES = ( unit_metric=TEMP_CELSIUS, metric_conversion=lambda val: temp_convert(val, TEMP_FAHRENHEIT, TEMP_CELSIUS), is_metric_check=True, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), ClimaCellSensorEntityDescription( key=CC_ATTR_PRESSURE_SURFACE_LEVEL, @@ -200,7 +197,7 @@ CC_SENSOR_TYPES = ( val, PRESSURE_INHG, PRESSURE_HPA ), is_metric_check=True, - device_class=DEVICE_CLASS_PRESSURE, + device_class=SensorDeviceClass.PRESSURE, ), ClimaCellSensorEntityDescription( key=CC_ATTR_SOLAR_GHI, @@ -283,7 +280,7 @@ CC_SENSOR_TYPES = ( name="Carbon Monoxide", unit_imperial=CONCENTRATION_PARTS_PER_MILLION, unit_metric=CONCENTRATION_PARTS_PER_MILLION, - device_class=DEVICE_CLASS_CO, + device_class=SensorDeviceClass.CO, ), ClimaCellSensorEntityDescription( key=CC_ATTR_SULFUR_DIOXIDE, @@ -436,7 +433,7 @@ CC_V3_SENSOR_TYPES = ( name="Carbon Monoxide", unit_imperial=CONCENTRATION_PARTS_PER_MILLION, unit_metric=CONCENTRATION_PARTS_PER_MILLION, - device_class=DEVICE_CLASS_CO, + device_class=SensorDeviceClass.CO, ), ClimaCellSensorEntityDescription( key=CC_V3_ATTR_SULFUR_DIOXIDE, From 6b9447e3a27facbdb0e685e70b29eb97cc72d4ad Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 17 Dec 2021 12:45:53 +0100 Subject: [PATCH 0672/2644] Fix threading error in demo vacuum (#62165) --- homeassistant/components/demo/vacuum.py | 5 +++-- tests/components/demo/test_vacuum.py | 10 +++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/demo/vacuum.py b/homeassistant/components/demo/vacuum.py index 39413a1b9f7..a5cffed1be8 100644 --- a/homeassistant/components/demo/vacuum.py +++ b/homeassistant/components/demo/vacuum.py @@ -22,6 +22,7 @@ from homeassistant.components.vacuum import ( StateVacuumEntity, VacuumEntity, ) +from homeassistant.helpers import event SUPPORT_MINIMAL_SERVICES = SUPPORT_TURN_ON | SUPPORT_TURN_OFF @@ -328,7 +329,7 @@ class StateDemoVacuum(StateVacuumEntity): self._state = STATE_RETURNING self.schedule_update_ha_state() - self.hass.loop.call_later(30, self.__set_state_to_dock) + event.call_later(self.hass, 30, self.__set_state_to_dock) def clean_spot(self, **kwargs): """Perform a spot clean-up.""" @@ -349,6 +350,6 @@ class StateDemoVacuum(StateVacuumEntity): self._fan_speed = fan_speed self.schedule_update_ha_state() - def __set_state_to_dock(self): + def __set_state_to_dock(self, _): self._state = STATE_DOCKED self.schedule_update_ha_state() diff --git a/tests/components/demo/test_vacuum.py b/tests/components/demo/test_vacuum.py index d07d7b3d4b2..3c0f40069dc 100644 --- a/tests/components/demo/test_vacuum.py +++ b/tests/components/demo/test_vacuum.py @@ -1,4 +1,6 @@ """The tests for the Demo vacuum platform.""" +from datetime import timedelta + import pytest from homeassistant.components import vacuum @@ -35,8 +37,9 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.setup import async_setup_component +from homeassistant.util import dt -from tests.common import async_mock_service +from tests.common import async_fire_time_changed, async_mock_service from tests.components.vacuum import common ENTITY_VACUUM_BASIC = f"{DOMAIN}.{DEMO_VACUUM_BASIC}".lower() @@ -175,6 +178,11 @@ async def test_methods(hass): state = hass.states.get(ENTITY_VACUUM_STATE) assert state.state == STATE_RETURNING + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=31)) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_VACUUM_STATE) + assert state.state == STATE_DOCKED + await common.async_set_fan_speed( hass, FAN_SPEEDS[-1], entity_id=ENTITY_VACUUM_STATE ) From 1c15e36afcbe5731f89646cd02cead0b6646374a Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Fri, 17 Dec 2021 06:46:21 -0500 Subject: [PATCH 0673/2644] Use enums in wallbox (#61997) --- homeassistant/components/wallbox/sensor.py | 34 ++++++++++------------ 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/wallbox/sensor.py b/homeassistant/components/wallbox/sensor.py index dd45c237c45..89119bbc671 100644 --- a/homeassistant/components/wallbox/sensor.py +++ b/homeassistant/components/wallbox/sensor.py @@ -6,17 +6,13 @@ import logging from typing import cast from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_CURRENT, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, ELECTRIC_CURRENT_AMPERE, ENERGY_KILO_WATT_HOUR, LENGTH_KILOMETERS, @@ -63,23 +59,23 @@ SENSOR_TYPES: dict[str, WallboxSensorEntityDescription] = { name="Charging Power", precision=2, native_unit_of_measurement=POWER_KILO_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), CONF_MAX_AVAILABLE_POWER_KEY: WallboxSensorEntityDescription( key=CONF_MAX_AVAILABLE_POWER_KEY, name="Max Available Power", precision=0, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, ), CONF_CHARGING_SPEED_KEY: WallboxSensorEntityDescription( key=CONF_CHARGING_SPEED_KEY, icon="mdi:speedometer", name="Charging Speed", precision=0, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), CONF_ADDED_RANGE_KEY: WallboxSensorEntityDescription( key=CONF_ADDED_RANGE_KEY, @@ -87,28 +83,28 @@ SENSOR_TYPES: dict[str, WallboxSensorEntityDescription] = { name="Added Range", precision=0, native_unit_of_measurement=LENGTH_KILOMETERS, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, ), CONF_ADDED_ENERGY_KEY: WallboxSensorEntityDescription( key=CONF_ADDED_ENERGY_KEY, name="Added Energy", precision=2, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), CONF_COST_KEY: WallboxSensorEntityDescription( key=CONF_COST_KEY, icon="mdi:ev-station", name="Cost", - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, ), CONF_STATE_OF_CHARGE_KEY: WallboxSensorEntityDescription( key=CONF_STATE_OF_CHARGE_KEY, name="State of Charge", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_BATTERY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, ), CONF_CURRENT_MODE_KEY: WallboxSensorEntityDescription( key=CONF_CURRENT_MODE_KEY, @@ -130,8 +126,8 @@ SENSOR_TYPES: dict[str, WallboxSensorEntityDescription] = { key=CONF_MAX_CHARGING_CURRENT_KEY, name="Max. Charging Current", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, ), } From 9c749682bf6617cba9a950d8979dd6112f6e2cee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Fri, 17 Dec 2021 12:55:16 +0100 Subject: [PATCH 0674/2644] Add Tibber peak hour, monthly consumption and monthly cost sensors (#61853) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Tibber, Consumption data Signed-off-by: Daniel Hjelseth Høyer * Tibber sensors Signed-off-by: Daniel Hjelseth Høyer * Tibber, Consumption data Signed-off-by: Daniel Hjelseth Høyer * Add peak_hour_time Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/tibber/sensor.py | 69 ++++++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 9bc7502e9c2..58e9a7c5e6f 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -193,6 +193,33 @@ RT_SENSORS: tuple[SensorEntityDescription, ...] = ( ), ) +SENSORS: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="month_cost", + name="Monthly cost", + device_class=SensorDeviceClass.MONETARY, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="peak_hour", + name="Month peak hour consumption", + device_class=SensorDeviceClass.ENERGY, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ), + SensorEntityDescription( + key="peak_hour_time", + name="Time of max hour consumption", + device_class=SensorDeviceClass.TIMESTAMP, + ), + SensorEntityDescription( + key="month_cons", + name="Monthly net consumption", + device_class=SensorDeviceClass.ENERGY, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=SensorStateClass.TOTAL_INCREASING, + ), +) + async def async_setup_entry(hass, entry, async_add_entities): """Set up the Tibber sensor.""" @@ -202,6 +229,7 @@ async def async_setup_entry(hass, entry, async_add_entities): entity_registry = async_get_entity_reg(hass) device_registry = async_get_dev_reg(hass) + coordinator = None entities = [] for home in tibber_connection.get_homes(only_active=False): try: @@ -221,6 +249,17 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities, home, hass ).async_set_updated_data ) + if home.has_active_subscription and not home.has_real_time_consumption: + if coordinator is None: + coordinator = update_coordinator.DataUpdateCoordinator( + hass, + _LOGGER, + name=f"Tibber {tibber_connection.name}", + update_method=tibber_connection.fetch_consumption_data_active_homes, + update_interval=timedelta(hours=1), + ) + for entity_description in SENSORS: + entities.append(TibberDataSensor(home, coordinator, entity_description)) # migrate old_id = home.info["viewer"]["home"]["meteringPointData"]["consumptionEan"] @@ -301,7 +340,7 @@ class TibberSensorElPrice(TibberSensor): self._attr_unique_id = self._tibber_home.home_id self._model = "Price Sensor" - self._device_name = self._attr_name + self._device_name = self._home_name async def async_update(self): """Get the latest data and updates the states.""" @@ -349,6 +388,34 @@ class TibberSensorElPrice(TibberSensor): ]["estimatedAnnualConsumption"] +class TibberDataSensor(TibberSensor, update_coordinator.CoordinatorEntity): + """Representation of a Tibber sensor.""" + + def __init__( + self, + tibber_home, + coordinator: update_coordinator.DataUpdateCoordinator, + entity_description: SensorEntityDescription, + ): + """Initialize the sensor.""" + super().__init__(coordinator=coordinator, tibber_home=tibber_home) + self.entity_description = entity_description + + self._attr_unique_id = ( + f"{self._tibber_home.home_id}_{self.entity_description.key}" + ) + self._attr_name = f"{entity_description.name} {self._home_name}" + if entity_description.key == "month_cost": + self._attr_native_unit_of_measurement = self._tibber_home.currency + + self._device_name = self._home_name + + @property + def native_value(self): + """Return the value of the sensor.""" + return getattr(self._tibber_home, self.entity_description.key) + + class TibberSensorRT(TibberSensor, update_coordinator.CoordinatorEntity): """Representation of a Tibber sensor for real time consumption.""" From f7f1d9b15d61bed48d0bb0b694affd8be47a13ef Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 17 Dec 2021 13:16:44 +0100 Subject: [PATCH 0675/2644] Fix threading error in litejet (#62185) --- homeassistant/components/litejet/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/litejet/__init__.py b/homeassistant/components/litejet/__init__.py index 413a886798a..2da02368e93 100644 --- a/homeassistant/components/litejet/__init__.py +++ b/homeassistant/components/litejet/__init__.py @@ -34,7 +34,7 @@ CONFIG_SCHEMA = vol.Schema( ) -def setup(hass, config): +async def async_setup(hass, config): """Set up the LiteJet component.""" if DOMAIN in config and not hass.config_entries.async_entries(DOMAIN): # No config entry exists and configuration.yaml config exists, trigger the import flow. From 474ef54477f0772a5fde2d63553f06f23bffbb0a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 17 Dec 2021 13:17:48 +0100 Subject: [PATCH 0676/2644] Fix threading error in recorder tests (#62187) --- tests/components/recorder/test_init.py | 2 +- tests/components/recorder/test_statistics.py | 27 ++++++++++++++------ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 64560e4d33b..ef5ebff44c4 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -531,7 +531,7 @@ def test_saving_state_and_removing_entity(hass, hass_recorder): entity_id = "lock.mine" hass.states.set(entity_id, STATE_LOCKED) hass.states.set(entity_id, STATE_UNLOCKED) - hass.states.async_remove(entity_id) + hass.states.remove(entity_id) wait_recording_done(hass) diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 0f4468019e9..bd10d1e9612 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -29,6 +29,7 @@ from homeassistant.components.recorder.statistics import ( ) from homeassistant.components.recorder.util import session_scope from homeassistant.const import TEMP_CELSIUS +from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util @@ -230,13 +231,19 @@ def test_rename_entity(hass_recorder): setup_component(hass, "sensor", {}) entity_reg = mock_registry(hass) - reg_entry = entity_reg.async_get_or_create( - "sensor", - "test", - "unique_0000", - suggested_object_id="test1", - ) - assert reg_entry.entity_id == "sensor.test1" + + @callback + def add_entry(): + reg_entry = entity_reg.async_get_or_create( + "sensor", + "test", + "unique_0000", + suggested_object_id="test1", + ) + assert reg_entry.entity_id == "sensor.test1" + + hass.add_job(add_entry) + hass.block_till_done() zero, four, states = record_states(hass) hist = history.get_significant_states(hass, zero, four) @@ -274,7 +281,11 @@ def test_rename_entity(hass_recorder): stats = statistics_during_period(hass, zero, period="5minute") assert stats == {"sensor.test1": expected_stats1, "sensor.test2": expected_stats2} - entity_reg.async_update_entity(reg_entry.entity_id, new_entity_id="sensor.test99") + @callback + def rename_entry(): + entity_reg.async_update_entity("sensor.test1", new_entity_id="sensor.test99") + + hass.add_job(rename_entry) hass.block_till_done() stats = statistics_during_period(hass, zero, period="5minute") From 9cd82e0f004178c0a3292172be92dc94b90e6fe3 Mon Sep 17 00:00:00 2001 From: Kim Frellsen Date: Fri, 17 Dec 2021 14:20:23 +0100 Subject: [PATCH 0677/2644] Update fortios device_tracker (#61970) * FortiOS 7.0 support Added support for FortiOS 7.0 and retaining FortiOS 6.4 support. Since an API was deprecated in FortiOS 7.0 and replace by a new API the integration now also support FortiOS 7.0. It is planned to deprecate the support for FortiOS 6.4 in a year * updated requirement to fortios * Update device_tracker.py indentation fix * Update device_tracker.py run flake8 fixes * flake8 fixes * Update device_tracker.py black fixing line breaks * Update device_tracker.py black fixes * Update device_tracker.py linter fixes * Update device_tracker.py linter fixes * Update device_tracker.py linter fix * Update device_tracker.py removed comment that pylint does not like :-~ * Update homeassistant/components/fortios/device_tracker.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/fortios/device_tracker.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/fortios/device_tracker.py Co-authored-by: Martin Hjelmare * Update device_tracker.py to resolve double guard for supported versions. * updated fortios device tracker Deprecated old api. cleaned up code. better checking with try-catch removed unnecessary error output. * Update device_tracker.py lint compliance. * Update device_tracker.py lint updates * Update device_tracker.py lint updates * Update device_tracker.py lint updates * Update device_tracker.py lint updates * Update device_tracker.py updated to use awesomeversion component. * Update device_tracker.py pylint updates * Update device_tracker.py pylint updates * Clean up * Simplify Co-authored-by: Martin Hjelmare --- .../components/fortios/device_tracker.py | 45 +++++++++---------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/fortios/device_tracker.py b/homeassistant/components/fortios/device_tracker.py index a1507af99dc..e160268e3fc 100644 --- a/homeassistant/components/fortios/device_tracker.py +++ b/homeassistant/components/fortios/device_tracker.py @@ -5,6 +5,7 @@ This component is part of the device_tracker platform. """ import logging +from awesomeversion import AwesomeVersion from fortiosapi import FortiOSAPI import voluptuous as vol @@ -47,49 +48,43 @@ def get_scanner(hass, config): return None status_json = fgt.monitor("system/status", "") - fos_major_version = int(status_json["version"][1]) - if fos_major_version < 6 or fos_major_version > 7: + current_version = AwesomeVersion(status_json["version"]) + minimum_version = AwesomeVersion("6.4.3") + if current_version < minimum_version: _LOGGER.error( - "Unsupported FortiOS version, fos_major_version = %s", - fos_major_version, + "Unsupported FortiOS version: %s. Version %s and newer are supported", + current_version, + minimum_version, ) return None - api_url = "user/device/query" - if fos_major_version == 6: - api_url = "user/device/select" - - return FortiOSDeviceScanner(fgt, fos_major_version, api_url) + return FortiOSDeviceScanner(fgt) class FortiOSDeviceScanner(DeviceScanner): """This class queries a FortiOS unit for connected devices.""" - def __init__(self, fgt, fos_major_version, api_url) -> None: + def __init__(self, fgt) -> None: """Initialize the scanner.""" self._clients = {} self._clients_json = {} self._fgt = fgt - self._fos_major_version = fos_major_version - self._api_url = api_url def update(self): """Update clients from the device.""" - clients_json = self._fgt.monitor(self._api_url, "") + clients_json = self._fgt.monitor("user/device/query", "") self._clients_json = clients_json self._clients = [] if clients_json: - if self._fos_major_version == 6: - for client in clients_json["results"]: - if client["last_seen"] < 180: - self._clients.append(client["mac"].upper()) - elif self._fos_major_version == 7: + try: for client in clients_json["results"]: if client["is_online"]: self._clients.append(client["mac"].upper()) + except KeyError as kex: + _LOGGER.error("Key not found in clients: %s", kex) def scan_devices(self): """Scan for new devices and return a list with found device IDs.""" @@ -109,15 +104,15 @@ class FortiOSDeviceScanner(DeviceScanner): for client in data["results"]: if client["mac"] == device: try: - name = "" - if self._fos_major_version == 6: - name = client["host"]["name"] - elif self._fos_major_version == 7: - name = client["hostname"] + name = client["hostname"] _LOGGER.debug("Getting device name=%s", name) return name except KeyError as kex: - _LOGGER.error("Name not found in client data: %s", kex) - return None + _LOGGER.debug( + "No hostname found for %s in client data: %s", + device, + kex, + ) + return device.replace(":", "_") return None From 571b245b7e56e9abe73b4f433041a1a45c35f077 Mon Sep 17 00:00:00 2001 From: Peeter N Date: Fri, 17 Dec 2021 16:02:28 +0200 Subject: [PATCH 0678/2644] Add battery entity for Maxcube devices (#58699) --- .../components/maxcube/binary_sensor.py | 59 ++++++++++++------- tests/components/maxcube/conftest.py | 3 + .../maxcube/test_maxcube_binary_sensor.py | 35 ++++++++++- 3 files changed, 75 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/maxcube/binary_sensor.py b/homeassistant/components/maxcube/binary_sensor.py index 999d7af01c5..f24f56e52d7 100644 --- a/homeassistant/components/maxcube/binary_sensor.py +++ b/homeassistant/components/maxcube/binary_sensor.py @@ -1,8 +1,9 @@ """Support for MAX! binary sensors via MAX! Cube.""" from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_WINDOW, + BinarySensorDeviceClass, BinarySensorEntity, ) +from homeassistant.helpers.entity import EntityCategory from . import DATA_KEY @@ -12,6 +13,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): devices = [] for handler in hass.data[DATA_KEY].values(): for device in handler.cube.devices: + devices.append(MaxCubeBattery(handler, device)) # Only add Window Shutters if device.is_windowshutter(): devices.append(MaxCubeShutter(handler, device)) @@ -20,36 +22,53 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices) -class MaxCubeShutter(BinarySensorEntity): - """Representation of a MAX! Cube Binary Sensor device.""" +class MaxCubeBinarySensorBase(BinarySensorEntity): + """Base class for maxcube binary sensors.""" + + _attr_entity_category = EntityCategory.DIAGNOSTIC def __init__(self, handler, device): """Initialize MAX! Cube BinarySensorEntity.""" - room = handler.cube.room_by_id(device.room_id) - self._name = f"{room.name} {device.name}" self._cubehandle = handler self._device = device + self._room = handler.cube.room_by_id(device.room_id) - @property - def name(self): - """Return the name of the BinarySensorEntity.""" - return self._name + def update(self): + """Get latest data from MAX! Cube.""" + self._cubehandle.update() - @property - def unique_id(self): - """Return a unique ID.""" - return self._device.serial - @property - def device_class(self): - """Return the class of this sensor.""" - return DEVICE_CLASS_WINDOW +class MaxCubeShutter(MaxCubeBinarySensorBase): + """Representation of a MAX! Cube Binary Sensor device.""" + + _attr_device_class = BinarySensorDeviceClass.WINDOW + + def __init__(self, handler, device): + """Initialize MAX! Cube BinarySensorEntity.""" + super().__init__(handler, device) + + self._attr_name = f"{self._room.name} {self._device.name}" + self._attr_unique_id = self._device.serial @property def is_on(self): """Return true if the binary sensor is on/open.""" return self._device.is_open - def update(self): - """Get latest data from MAX! Cube.""" - self._cubehandle.update() + +class MaxCubeBattery(MaxCubeBinarySensorBase): + """Representation of a MAX! Cube Binary Sensor device.""" + + _attr_device_class = BinarySensorDeviceClass.BATTERY + + def __init__(self, handler, device): + """Initialize MAX! Cube BinarySensorEntity.""" + super().__init__(handler, device) + + self._attr_name = f"{self._room.name} {device.name} battery" + self._attr_unique_id = f"{self._device.serial}_battery" + + @property + def is_on(self): + """Return true if the binary sensor is on/open.""" + return self._device.battery == 1 diff --git a/tests/components/maxcube/conftest.py b/tests/components/maxcube/conftest.py index b36072190c4..f0dd12eb6c6 100644 --- a/tests/components/maxcube/conftest.py +++ b/tests/components/maxcube/conftest.py @@ -41,6 +41,7 @@ def thermostat(): t.max_temperature = None t.min_temperature = None t.valve_position = 25 # 25% + t.battery = 1 return t @@ -62,6 +63,7 @@ def wallthermostat(): t.actual_temperature = 19.0 t.max_temperature = 29.0 t.min_temperature = 4.5 + t.battery = 1 return t @@ -77,6 +79,7 @@ def windowshutter(): shutter.is_thermostat.return_value = False shutter.is_wallthermostat.return_value = False shutter.is_windowshutter.return_value = True + shutter.battery = 1 return shutter diff --git a/tests/components/maxcube/test_maxcube_binary_sensor.py b/tests/components/maxcube/test_maxcube_binary_sensor.py index 48d34a0df4e..39e1fb54740 100644 --- a/tests/components/maxcube/test_maxcube_binary_sensor.py +++ b/tests/components/maxcube/test_maxcube_binary_sensor.py @@ -4,7 +4,7 @@ from datetime import timedelta from maxcube.cube import MaxCube from maxcube.windowshutter import MaxWindowShutter -from homeassistant.components.binary_sensor import DEVICE_CLASS_WINDOW +from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, @@ -12,11 +12,13 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity import EntityCategory from homeassistant.util import utcnow from tests.common import async_fire_time_changed ENTITY_ID = "binary_sensor.testroom_testshutter" +BATTERY_ENTITY_ID = f"{ENTITY_ID}_battery" async def test_window_shuttler(hass, cube: MaxCube, windowshutter: MaxWindowShutter): @@ -25,12 +27,13 @@ async def test_window_shuttler(hass, cube: MaxCube, windowshutter: MaxWindowShut assert entity_registry.async_is_registered(ENTITY_ID) entity = entity_registry.async_get(ENTITY_ID) assert entity.unique_id == "AABBCCDD03" + assert entity.entity_category == EntityCategory.DIAGNOSTIC state = hass.states.get(ENTITY_ID) assert state is not None assert state.state == STATE_ON assert state.attributes.get(ATTR_FRIENDLY_NAME) == "TestRoom TestShutter" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_WINDOW + assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.WINDOW windowshutter.is_open = False async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) @@ -38,3 +41,31 @@ async def test_window_shuttler(hass, cube: MaxCube, windowshutter: MaxWindowShut state = hass.states.get(ENTITY_ID) assert state.state == STATE_OFF + + +async def test_window_shuttler_battery( + hass, cube: MaxCube, windowshutter: MaxWindowShutter +): + """Test battery binary_state with a shuttler device.""" + entity_registry = er.async_get(hass) + assert entity_registry.async_is_registered(BATTERY_ENTITY_ID) + entity = entity_registry.async_get(BATTERY_ENTITY_ID) + assert entity.unique_id == "AABBCCDD03_battery" + assert entity.entity_category == EntityCategory.DIAGNOSTIC + + state = hass.states.get(BATTERY_ENTITY_ID) + assert state is not None + assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.BATTERY + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "TestRoom TestShutter battery" + + windowshutter.battery = 1 # maxcube-api MAX_DEVICE_BATTERY_LOW + async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) + await hass.async_block_till_done() + state = hass.states.get(BATTERY_ENTITY_ID) + assert state.state == STATE_ON # on means low + + windowshutter.battery = 0 # maxcube-api MAX_DEVICE_BATTERY_OK + async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) + await hass.async_block_till_done() + state = hass.states.get(BATTERY_ENTITY_ID) + assert state.state == STATE_OFF # off means normal From 177ffa3aa67c856545022b13ca63f1b3e93f8985 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Fri, 17 Dec 2021 15:23:04 +0100 Subject: [PATCH 0679/2644] Upgrades P1 Monitor to v1.0.1 (#62201) --- homeassistant/components/p1_monitor/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/p1_monitor/manifest.json b/homeassistant/components/p1_monitor/manifest.json index 00b50bb029b..1f952d04fc9 100644 --- a/homeassistant/components/p1_monitor/manifest.json +++ b/homeassistant/components/p1_monitor/manifest.json @@ -3,7 +3,7 @@ "name": "P1 Monitor", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/p1_monitor", - "requirements": ["p1monitor==1.0.0"], + "requirements": ["p1monitor==1.0.1"], "codeowners": ["@klaasnicolaas"], "quality_scale": "platinum", "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 78e573cb07b..a7134b339d6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1177,7 +1177,7 @@ orvibo==1.1.1 ovoenergy==1.1.12 # homeassistant.components.p1_monitor -p1monitor==1.0.0 +p1monitor==1.0.1 # homeassistant.components.mqtt # homeassistant.components.shiftr diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8de9b54a683..51d7172b520 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -715,7 +715,7 @@ openerz-api==0.1.0 ovoenergy==1.1.12 # homeassistant.components.p1_monitor -p1monitor==1.0.0 +p1monitor==1.0.1 # homeassistant.components.mqtt # homeassistant.components.shiftr From ed1ce7d9f97fda00c90e85cacf89c9f87dae2131 Mon Sep 17 00:00:00 2001 From: Hans Oischinger Date: Fri, 17 Dec 2021 15:43:41 +0100 Subject: [PATCH 0680/2644] Add vicare strings (#61593) * Add vicare strings * Remove duplicates * Remove duplicates from english translation * Add missing strings --- homeassistant/components/vicare/strings.json | 26 +++++++++++++++++++ .../components/vicare/translations/en.json | 26 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 homeassistant/components/vicare/strings.json create mode 100644 homeassistant/components/vicare/translations/en.json diff --git a/homeassistant/components/vicare/strings.json b/homeassistant/components/vicare/strings.json new file mode 100644 index 00000000000..bf6b40fb6b2 --- /dev/null +++ b/homeassistant/components/vicare/strings.json @@ -0,0 +1,26 @@ +{ + "config": { + "flow_title": "{name} ({host})", + "step": { + "user": { + "title": "{name}", + "description": "Set up ViCare integration. To generate API key go to https://developer.viessmann.com", + "data": { + "name": "[%key:common::config_flow::data::name%]", + "scan_interval": "Scan Interval (seconds)", + "username": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]", + "client_id": "[%key:common::config_flow::data::api_key%]", + "heating_type": "Heating type" + } + } + }, + "error": { + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vicare/translations/en.json b/homeassistant/components/vicare/translations/en.json new file mode 100644 index 00000000000..d693cbe76cc --- /dev/null +++ b/homeassistant/components/vicare/translations/en.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Already configured. Only a single configuration possible.", + "unknown": "Unexpected error" + }, + "error": { + "invalid_auth": "Invalid authentication" + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "name": "Name", + "scan_interval": "Scan Interval (seconds)", + "client_id": "API Key", + "heating_type": "Heating type", + "password": "Password", + "username": "Email" + }, + "title": "{name}", + "description": "Set up ViCare integration. To generate API key go to https://developer.viessmann.com" + } + } + } +} \ No newline at end of file From 703b6891837587f9facdcb44d12209a50ccb513f Mon Sep 17 00:00:00 2001 From: Maximilian <43999966+DeerMaximum@users.noreply.github.com> Date: Fri, 17 Dec 2021 15:14:59 +0000 Subject: [PATCH 0681/2644] Address late review of nina (#61915) --- homeassistant/components/nina/__init__.py | 44 ++++++++++--------- .../components/nina/binary_sensor.py | 20 ++++----- homeassistant/components/nina/config_flow.py | 34 ++++++-------- tests/components/nina/test_binary_sensor.py | 38 ++++------------ tests/components/nina/test_config_flow.py | 4 ++ tests/components/nina/test_init.py | 20 +++++++++ 6 files changed, 79 insertions(+), 81 deletions(-) diff --git a/homeassistant/components/nina/__init__.py b/homeassistant/components/nina/__init__.py index 143699174ce..6fa5cab6f7a 100644 --- a/homeassistant/components/nina/__init__.py +++ b/homeassistant/components/nina/__init__.py @@ -1,17 +1,18 @@ """The Nina integration.""" from __future__ import annotations -from datetime import timedelta +import datetime as dt from typing import Any from async_timeout import timeout -from pynina import ApiError, Nina, Warning as NinaWarning +from pynina import ApiError, Nina from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util import dt as dt_util from .const import ( _LOGGER, @@ -59,33 +60,26 @@ class NINADataUpdateCoordinator(DataUpdateCoordinator): self.warnings: dict[str, Any] = {} self.corona_filter: bool = corona_filter - for region in regions.keys(): + for region in regions: self._nina.addRegion(region) - update_interval: timedelta = SCAN_INTERVAL - - super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) + super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) async def _async_update_data(self) -> dict[str, Any]: """Update data.""" - - try: - async with timeout(10): + async with timeout(10): + try: await self._nina.update() - return self._parse_data() - except ApiError as err: - raise UpdateFailed(err) from err + except ApiError as err: + raise UpdateFailed(err) from err + return self._parse_data() def _parse_data(self) -> dict[str, Any]: """Parse warning data.""" return_data: dict[str, Any] = {} - for ( - region_id - ) in self._nina.warnings: # pylint: disable=consider-using-dict-items - raw_warnings: list[NinaWarning] = self._nina.warnings[region_id] - + for region_id, raw_warnings in self._nina.warnings.items(): warnings_for_regions: list[Any] = [] for raw_warn in raw_warnings: @@ -95,12 +89,22 @@ class NINADataUpdateCoordinator(DataUpdateCoordinator): warn_obj: dict[str, Any] = { ATTR_ID: raw_warn.id, ATTR_HEADLINE: raw_warn.headline, - ATTR_SENT: raw_warn.sent or "", - ATTR_START: raw_warn.start or "", - ATTR_EXPIRES: raw_warn.expires or "", + ATTR_SENT: self._to_utc(raw_warn.sent), + ATTR_START: self._to_utc(raw_warn.start), + ATTR_EXPIRES: self._to_utc(raw_warn.expires), } warnings_for_regions.append(warn_obj) return_data[region_id] = warnings_for_regions return return_data + + @staticmethod + def _to_utc(input_time: str) -> str | None: + if input_time: + return ( + dt.datetime.fromisoformat(input_time) + .astimezone(dt_util.UTC) + .isoformat() + ) + return None diff --git a/homeassistant/components/nina/binary_sensor.py b/homeassistant/components/nina/binary_sensor.py index e0d01c1bfb4..eca6668d0f5 100644 --- a/homeassistant/components/nina/binary_sensor.py +++ b/homeassistant/components/nina/binary_sensor.py @@ -53,37 +53,33 @@ class NINAMessage(CoordinatorEntity, BinarySensorEntity): self, coordinator: NINADataUpdateCoordinator, region: str, - regionName: str, - slotID: int, + region_name: str, + slot_id: int, ) -> None: """Initialize.""" super().__init__(coordinator) self._region: str = region - self._region_name: str = regionName - self._slot_id: int = slotID - self._warning_index: int = slotID - 1 + self._warning_index: int = slot_id - 1 - self._coordinator: NINADataUpdateCoordinator = coordinator - - self._attr_name: str = f"Warning: {self._region_name} {self._slot_id}" - self._attr_unique_id: str = f"{self._region}-{self._slot_id}" + self._attr_name: str = f"Warning: {region_name} {slot_id}" + self._attr_unique_id: str = f"{region}-{slot_id}" self._attr_device_class: str = BinarySensorDeviceClass.SAFETY @property def is_on(self) -> bool: """Return the state of the sensor.""" - return len(self._coordinator.data[self._region]) > self._warning_index + return len(self.coordinator.data[self._region]) > self._warning_index @property def extra_state_attributes(self) -> dict[str, Any]: """Return extra attributes of the sensor.""" if ( - not len(self._coordinator.data[self._region]) > self._warning_index + not len(self.coordinator.data[self._region]) > self._warning_index ) or not self.is_on: return {} - data: dict[str, Any] = self._coordinator.data[self._region][self._warning_index] + data: dict[str, Any] = self.coordinator.data[self._region][self._warning_index] return { ATTR_HEADLINE: data[ATTR_HEADLINE], diff --git a/homeassistant/components/nina/config_flow.py b/homeassistant/components/nina/config_flow.py index cb3fc35bc45..0574de96681 100644 --- a/homeassistant/components/nina/config_flow.py +++ b/homeassistant/components/nina/config_flow.py @@ -8,6 +8,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from .const import ( @@ -45,53 +46,46 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") - has_error: bool = False + if not self._all_region_codes_sorted: + nina: Nina = Nina(async_get_clientsession(self.hass)) - if len(self._all_region_codes_sorted) == 0: try: - nina: Nina = Nina() - self._all_region_codes_sorted = self.swap_key_value( await nina.getAllRegionalCodes() ) - - self.split_regions() - - except ApiError as err: - _LOGGER.warning("NINA setup error: %s", err) + except ApiError: errors["base"] = "cannot_connect" - has_error = True except Exception as err: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception: %s", err) - errors["base"] = "unknown" return self.async_abort(reason="unknown") - if user_input is not None and not has_error: - config: dict[str, Any] = user_input + self.split_regions() - config[CONF_REGIONS] = [] + if user_input is not None and not errors: + user_input[CONF_REGIONS] = [] for group in CONST_REGIONS: if group_input := user_input.get(group): - config[CONF_REGIONS] += group_input + user_input[CONF_REGIONS] += group_input - if len(config[CONF_REGIONS]) > 0: + if user_input[CONF_REGIONS]: tmp: dict[str, Any] = {} - for reg in config[CONF_REGIONS]: + for reg in user_input[CONF_REGIONS]: tmp[self._all_region_codes_sorted[reg]] = reg.split("_", 1)[0] compact: dict[str, Any] = {} for key, val in tmp.items(): if val in compact: + # Abenberg, St + Abenberger Wald compact[val] = f"{compact[val]} + {key}" break compact[val] = key - config[CONF_REGIONS] = compact + user_input[CONF_REGIONS] = compact - return self.async_create_entry(title="NINA", data=config) + return self.async_create_entry(title="NINA", data=user_input) errors["base"] = "no_selection" @@ -122,7 +116,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): all_region_codes_swaped[value] = key else: for i in range(len(dict_to_sort)): - tmp_value: str = value + "_" + str(i) + tmp_value: str = f"{value}_{i}" if tmp_value not in all_region_codes_swaped: all_region_codes_swaped[tmp_value] = key break diff --git a/tests/components/nina/test_binary_sensor.py b/tests/components/nina/test_binary_sensor.py index 8016a255d46..9b2bfd17cfb 100644 --- a/tests/components/nina/test_binary_sensor.py +++ b/tests/components/nina/test_binary_sensor.py @@ -3,8 +3,6 @@ import json from typing import Any, Dict from unittest.mock import patch -from pynina import ApiError - from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.nina.const import ( ATTR_EXPIRES, @@ -64,9 +62,9 @@ async def test_sensors(hass: HomeAssistant) -> None: assert state_w1.state == STATE_ON assert state_w1.attributes.get(ATTR_HEADLINE) == "Ausfall Notruf 112" assert state_w1.attributes.get(ATTR_ID) == "mow.DE-NW-BN-SE030-20201014-30-000" - assert state_w1.attributes.get(ATTR_SENT) == "2021-10-11T05:20:00+01:00" - assert state_w1.attributes.get(ATTR_START) == "2021-11-01T05:20:00+01:00" - assert state_w1.attributes.get(ATTR_EXPIRES) == "3021-11-22T05:19:00+01:00" + assert state_w1.attributes.get(ATTR_SENT) == "2021-10-11T04:20:00+00:00" + assert state_w1.attributes.get(ATTR_START) == "2021-11-01T04:20:00+00:00" + assert state_w1.attributes.get(ATTR_EXPIRES) == "3021-11-22T04:19:00+00:00" assert entry_w1.unique_id == "083350000000-1" assert state_w1.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY @@ -157,9 +155,9 @@ async def test_sensors_without_corona_filter(hass: HomeAssistant) -> None: == "Corona-Verordnung des Landes: Warnstufe durch Landesgesundheitsamt ausgerufen" ) assert state_w1.attributes.get(ATTR_ID) == "mow.DE-BW-S-SE018-20211102-18-001" - assert state_w1.attributes.get(ATTR_SENT) == "2021-11-02T20:07:16+01:00" - assert state_w1.attributes.get(ATTR_START) == "" - assert state_w1.attributes.get(ATTR_EXPIRES) == "" + assert state_w1.attributes.get(ATTR_SENT) == "2021-11-02T19:07:16+00:00" + assert state_w1.attributes.get(ATTR_START) is None + assert state_w1.attributes.get(ATTR_EXPIRES) is None assert entry_w1.unique_id == "083350000000-1" assert state_w1.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY @@ -170,9 +168,9 @@ async def test_sensors_without_corona_filter(hass: HomeAssistant) -> None: assert state_w2.state == STATE_ON assert state_w2.attributes.get(ATTR_HEADLINE) == "Ausfall Notruf 112" assert state_w2.attributes.get(ATTR_ID) == "mow.DE-NW-BN-SE030-20201014-30-000" - assert state_w2.attributes.get(ATTR_SENT) == "2021-10-11T05:20:00+01:00" - assert state_w2.attributes.get(ATTR_START) == "2021-11-01T05:20:00+01:00" - assert state_w2.attributes.get(ATTR_EXPIRES) == "3021-11-22T05:19:00+01:00" + assert state_w2.attributes.get(ATTR_SENT) == "2021-10-11T04:20:00+00:00" + assert state_w2.attributes.get(ATTR_START) == "2021-11-01T04:20:00+00:00" + assert state_w2.attributes.get(ATTR_EXPIRES) == "3021-11-22T04:19:00+00:00" assert entry_w2.unique_id == "083350000000-2" assert state_w2.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY @@ -215,21 +213,3 @@ async def test_sensors_without_corona_filter(hass: HomeAssistant) -> None: assert entry_w5.unique_id == "083350000000-5" assert state_w5.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY - - -async def test_sensors_connection_error(hass: HomeAssistant) -> None: - """Test the creation and values of the NINA sensors with no connected.""" - with patch( - "pynina.baseApi.BaseAPI._makeRequest", - side_effect=ApiError("Could not connect to Api"), - ): - conf_entry: MockConfigEntry = MockConfigEntry( - domain=DOMAIN, title="NINA", data=ENTRY_DATA - ) - - conf_entry.add_to_hass(hass) - - await hass.config_entries.async_setup(conf_entry.entry_id) - await hass.async_block_till_done() - - assert conf_entry.state == ConfigEntryState.SETUP_RETRY diff --git a/tests/components/nina/test_config_flow.py b/tests/components/nina/test_config_flow.py index 052c1adeb38..a1aa97e0fbe 100644 --- a/tests/components/nina/test_config_flow.py +++ b/tests/components/nina/test_config_flow.py @@ -87,6 +87,9 @@ async def test_step_user(hass: HomeAssistant) -> None: with patch( "pynina.baseApi.BaseAPI._makeRequest", return_value=DUMMY_RESPONSE, + ), patch( + "homeassistant.components.nina.async_setup_entry", + return_value=True, ): result: dict[str, Any] = await hass.config_entries.flow.async_init( @@ -128,3 +131,4 @@ async def test_step_user_already_configured(hass: HomeAssistant) -> None: ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/nina/test_init.py b/tests/components/nina/test_init.py index 4246c014748..2f60c5d8e89 100644 --- a/tests/components/nina/test_init.py +++ b/tests/components/nina/test_init.py @@ -3,6 +3,8 @@ import json from typing import Any, Dict from unittest.mock import patch +from pynina import ApiError + from homeassistant.components.nina.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant @@ -44,3 +46,21 @@ async def test_config_entry_not_ready(hass: HomeAssistant) -> None: entry: MockConfigEntry = await init_integration(hass) assert entry.state == ConfigEntryState.LOADED + + +async def test_sensors_connection_error(hass: HomeAssistant) -> None: + """Test the creation and values of the NINA sensors with no connected.""" + with patch( + "pynina.baseApi.BaseAPI._makeRequest", + side_effect=ApiError("Could not connect to Api"), + ): + conf_entry: MockConfigEntry = MockConfigEntry( + domain=DOMAIN, title="NINA", data=ENTRY_DATA + ) + + conf_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(conf_entry.entry_id) + await hass.async_block_till_done() + + assert conf_entry.state == ConfigEntryState.SETUP_RETRY From 1a32b10af8abc432e7d42b842f93225ae48b5632 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Fri, 17 Dec 2021 15:19:41 +0000 Subject: [PATCH 0682/2644] Use DeviceClass Enums in greeneye_monitor tests (#62143) * Use DeviceClass Enums in greeneye_monitor tests * Use is for comparisons --- tests/components/greeneye_monitor/conftest.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/tests/components/greeneye_monitor/conftest.py b/tests/components/greeneye_monitor/conftest.py index a68cc9e8b96..a1ef0a9d89d 100644 --- a/tests/components/greeneye_monitor/conftest.py +++ b/tests/components/greeneye_monitor/conftest.py @@ -5,13 +5,8 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest from homeassistant.components.greeneye_monitor import DOMAIN -from homeassistant.const import ( - DEVICE_CLASS_POWER, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_VOLTAGE, - ELECTRIC_POTENTIAL_VOLT, - POWER_WATT, -) +from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.const import ELECTRIC_POTENTIAL_VOLT, POWER_WATT from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_registry import ( RegistryEntry, @@ -45,7 +40,7 @@ def assert_temperature_sensor_registered( ): """Assert that a temperature sensor entity was registered properly.""" sensor = assert_sensor_registered(hass, serial_number, "temp", number, name) - assert sensor.original_device_class == DEVICE_CLASS_TEMPERATURE + assert sensor.original_device_class is SensorDeviceClass.TEMPERATURE def assert_pulse_counter_registered( @@ -67,7 +62,7 @@ def assert_power_sensor_registered( """Assert that a power sensor entity was registered properly.""" sensor = assert_sensor_registered(hass, serial_number, "current", number, name) assert sensor.unit_of_measurement == POWER_WATT - assert sensor.original_device_class == DEVICE_CLASS_POWER + assert sensor.original_device_class is SensorDeviceClass.POWER def assert_voltage_sensor_registered( @@ -76,7 +71,7 @@ def assert_voltage_sensor_registered( """Assert that a voltage sensor entity was registered properly.""" sensor = assert_sensor_registered(hass, serial_number, "volts", number, name) assert sensor.unit_of_measurement == ELECTRIC_POTENTIAL_VOLT - assert sensor.original_device_class == DEVICE_CLASS_VOLTAGE + assert sensor.original_device_class is SensorDeviceClass.VOLTAGE def assert_sensor_registered( From 26d8d820612276207e126141c2daa9d6d4e0d7b8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 17 Dec 2021 16:21:32 +0100 Subject: [PATCH 0683/2644] Fix codeowners for tests in hassfest (#62204) --- CODEOWNERS | 449 ++++++++++++++++++++++++++++++++++ script/hassfest/codeowners.py | 7 +- 2 files changed, 454 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index d54ed4827c6..709d0c9d395 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -20,613 +20,1062 @@ homeassistant/scripts/check_config.py @kellerza # Integrations homeassistant/components/abode/* @shred86 +tests/components/abode/* @shred86 homeassistant/components/accuweather/* @bieniu +tests/components/accuweather/* @bieniu homeassistant/components/acmeda/* @atmurray +tests/components/acmeda/* @atmurray homeassistant/components/adax/* @danielhiversen +tests/components/adax/* @danielhiversen homeassistant/components/adguard/* @frenck +tests/components/adguard/* @frenck homeassistant/components/advantage_air/* @Bre77 +tests/components/advantage_air/* @Bre77 homeassistant/components/aemet/* @noltari +tests/components/aemet/* @noltari homeassistant/components/agent_dvr/* @ispysoftware +tests/components/agent_dvr/* @ispysoftware homeassistant/components/airly/* @bieniu +tests/components/airly/* @bieniu homeassistant/components/airnow/* @asymworks +tests/components/airnow/* @asymworks homeassistant/components/airthings/* @danielhiversen +tests/components/airthings/* @danielhiversen homeassistant/components/airtouch4/* @LonePurpleWolf +tests/components/airtouch4/* @LonePurpleWolf homeassistant/components/airvisual/* @bachya +tests/components/airvisual/* @bachya homeassistant/components/alarmdecoder/* @ajschmidt8 +tests/components/alarmdecoder/* @ajschmidt8 homeassistant/components/alexa/* @home-assistant/cloud @ochlocracy +tests/components/alexa/* @home-assistant/cloud @ochlocracy homeassistant/components/almond/* @gcampax @balloob +tests/components/almond/* @gcampax @balloob homeassistant/components/alpha_vantage/* @fabaff homeassistant/components/ambee/* @frenck +tests/components/ambee/* @frenck homeassistant/components/amberelectric/* @madpilot +tests/components/amberelectric/* @madpilot homeassistant/components/ambiclimate/* @danielhiversen +tests/components/ambiclimate/* @danielhiversen homeassistant/components/ambient_station/* @bachya +tests/components/ambient_station/* @bachya homeassistant/components/amcrest/* @flacjacket homeassistant/components/analytics/* @home-assistant/core @ludeeus +tests/components/analytics/* @home-assistant/core @ludeeus homeassistant/components/androidtv/* @JeffLIrion +tests/components/androidtv/* @JeffLIrion homeassistant/components/apache_kafka/* @bachya +tests/components/apache_kafka/* @bachya homeassistant/components/api/* @home-assistant/core +tests/components/api/* @home-assistant/core homeassistant/components/apple_tv/* @postlund +tests/components/apple_tv/* @postlund homeassistant/components/apprise/* @caronc +tests/components/apprise/* @caronc homeassistant/components/aprs/* @PhilRW +tests/components/aprs/* @PhilRW homeassistant/components/arcam_fmj/* @elupus +tests/components/arcam_fmj/* @elupus homeassistant/components/arduino/* @fabaff homeassistant/components/arest/* @fabaff homeassistant/components/arris_tg2492lg/* @vanbalken homeassistant/components/aseko_pool_live/* @milanmeu +tests/components/aseko_pool_live/* @milanmeu homeassistant/components/asuswrt/* @kennedyshead @ollo69 +tests/components/asuswrt/* @kennedyshead @ollo69 homeassistant/components/atag/* @MatsNL +tests/components/atag/* @MatsNL homeassistant/components/aten_pe/* @mtdcr homeassistant/components/atome/* @baqs homeassistant/components/august/* @bdraco +tests/components/august/* @bdraco homeassistant/components/aurora/* @djtimca +tests/components/aurora/* @djtimca homeassistant/components/aurora_abb_powerone/* @davet2001 +tests/components/aurora_abb_powerone/* @davet2001 homeassistant/components/auth/* @home-assistant/core +tests/components/auth/* @home-assistant/core homeassistant/components/automation/* @home-assistant/core +tests/components/automation/* @home-assistant/core homeassistant/components/avea/* @pattyland homeassistant/components/awair/* @ahayworth @danielsjf +tests/components/awair/* @ahayworth @danielsjf homeassistant/components/axis/* @Kane610 +tests/components/axis/* @Kane610 homeassistant/components/azure_devops/* @timmo001 +tests/components/azure_devops/* @timmo001 homeassistant/components/azure_event_hub/* @eavanvalkenburg +tests/components/azure_event_hub/* @eavanvalkenburg homeassistant/components/azure_service_bus/* @hfurubotten homeassistant/components/balboa/* @garbled1 +tests/components/balboa/* @garbled1 homeassistant/components/beewi_smartclim/* @alemuro homeassistant/components/bitcoin/* @fabaff homeassistant/components/bizkaibus/* @UgaitzEtxebarria homeassistant/components/blebox/* @bbx-a @bbx-jp +tests/components/blebox/* @bbx-a @bbx-jp homeassistant/components/blink/* @fronzbot +tests/components/blink/* @fronzbot homeassistant/components/blueprint/* @home-assistant/core +tests/components/blueprint/* @home-assistant/core homeassistant/components/bluesound/* @thrawnarn homeassistant/components/bmp280/* @belidzs homeassistant/components/bmw_connected_drive/* @gerard33 @rikroe +tests/components/bmw_connected_drive/* @gerard33 @rikroe homeassistant/components/bond/* @bdraco @prystupa @joshs85 +tests/components/bond/* @bdraco @prystupa @joshs85 homeassistant/components/bosch_shc/* @tschamm +tests/components/bosch_shc/* @tschamm homeassistant/components/braviatv/* @bieniu @Drafteed +tests/components/braviatv/* @bieniu @Drafteed homeassistant/components/broadlink/* @danielhiversen @felipediel @L-I-Am +tests/components/broadlink/* @danielhiversen @felipediel @L-I-Am homeassistant/components/brother/* @bieniu +tests/components/brother/* @bieniu homeassistant/components/brunt/* @eavanvalkenburg +tests/components/brunt/* @eavanvalkenburg homeassistant/components/bsblan/* @liudger +tests/components/bsblan/* @liudger homeassistant/components/bt_smarthub/* @jxwolstenholme homeassistant/components/buienradar/* @mjj4791 @ties @Robbie1221 +tests/components/buienradar/* @mjj4791 @ties @Robbie1221 homeassistant/components/button/* @home-assistant/core +tests/components/button/* @home-assistant/core homeassistant/components/cast/* @emontnemery +tests/components/cast/* @emontnemery homeassistant/components/cert_expiry/* @Cereal2nd @jjlawren +tests/components/cert_expiry/* @Cereal2nd @jjlawren homeassistant/components/circuit/* @braam homeassistant/components/cisco_ios/* @fbradyirl homeassistant/components/cisco_mobility_express/* @fbradyirl homeassistant/components/cisco_webex_teams/* @fbradyirl homeassistant/components/climacell/* @raman325 +tests/components/climacell/* @raman325 homeassistant/components/cloud/* @home-assistant/cloud +tests/components/cloud/* @home-assistant/cloud homeassistant/components/cloudflare/* @ludeeus @ctalkington +tests/components/cloudflare/* @ludeeus @ctalkington homeassistant/components/coinbase/* @tombrien +tests/components/coinbase/* @tombrien homeassistant/components/color_extractor/* @GenericStudent +tests/components/color_extractor/* @GenericStudent homeassistant/components/comfoconnect/* @michaelarnauts +tests/components/comfoconnect/* @michaelarnauts homeassistant/components/compensation/* @Petro31 +tests/components/compensation/* @Petro31 homeassistant/components/config/* @home-assistant/core +tests/components/config/* @home-assistant/core homeassistant/components/configurator/* @home-assistant/core +tests/components/configurator/* @home-assistant/core homeassistant/components/control4/* @lawtancool +tests/components/control4/* @lawtancool homeassistant/components/conversation/* @home-assistant/core +tests/components/conversation/* @home-assistant/core homeassistant/components/coolmaster/* @OnFreund +tests/components/coolmaster/* @OnFreund homeassistant/components/coronavirus/* @home-assistant/core +tests/components/coronavirus/* @home-assistant/core homeassistant/components/counter/* @fabaff +tests/components/counter/* @fabaff homeassistant/components/cover/* @home-assistant/core +tests/components/cover/* @home-assistant/core homeassistant/components/cpuspeed/* @fabaff homeassistant/components/crownstone/* @Crownstone @RicArch97 +tests/components/crownstone/* @Crownstone @RicArch97 homeassistant/components/cups/* @fabaff homeassistant/components/daikin/* @fredrike +tests/components/daikin/* @fredrike homeassistant/components/darksky/* @fabaff +tests/components/darksky/* @fabaff homeassistant/components/debugpy/* @frenck +tests/components/debugpy/* @frenck homeassistant/components/deconz/* @Kane610 +tests/components/deconz/* @Kane610 homeassistant/components/delijn/* @bollewolle @Emilv2 homeassistant/components/demo/* @home-assistant/core +tests/components/demo/* @home-assistant/core homeassistant/components/denonavr/* @ol-iver @starkillerOG +tests/components/denonavr/* @ol-iver @starkillerOG homeassistant/components/derivative/* @afaucogney +tests/components/derivative/* @afaucogney homeassistant/components/device_automation/* @home-assistant/core +tests/components/device_automation/* @home-assistant/core homeassistant/components/devolo_home_control/* @2Fake @Shutgun +tests/components/devolo_home_control/* @2Fake @Shutgun homeassistant/components/devolo_home_network/* @2Fake @Shutgun +tests/components/devolo_home_network/* @2Fake @Shutgun homeassistant/components/dexcom/* @gagebenne +tests/components/dexcom/* @gagebenne homeassistant/components/dhcp/* @bdraco +tests/components/dhcp/* @bdraco homeassistant/components/dht/* @thegardenmonkey homeassistant/components/digital_ocean/* @fabaff homeassistant/components/discogs/* @thibmaek homeassistant/components/dlna_dmr/* @StevenLooman @chishm +tests/components/dlna_dmr/* @StevenLooman @chishm homeassistant/components/doorbird/* @oblogic7 @bdraco +tests/components/doorbird/* @oblogic7 @bdraco homeassistant/components/dsmr/* @Robbie1221 @frenck +tests/components/dsmr/* @Robbie1221 @frenck homeassistant/components/dsmr_reader/* @depl0y homeassistant/components/dunehd/* @bieniu +tests/components/dunehd/* @bieniu homeassistant/components/dwd_weather_warnings/* @runningman84 @stephan192 @Hummel95 homeassistant/components/dweet/* @fabaff homeassistant/components/dynalite/* @ziv1234 +tests/components/dynalite/* @ziv1234 homeassistant/components/eafm/* @Jc2k +tests/components/eafm/* @Jc2k homeassistant/components/ecobee/* @marthoc +tests/components/ecobee/* @marthoc homeassistant/components/econet/* @vangorra @w1ll1am23 +tests/components/econet/* @vangorra @w1ll1am23 homeassistant/components/ecovacs/* @OverloadUT homeassistant/components/edl21/* @mtdcr homeassistant/components/efergy/* @tkdrob +tests/components/efergy/* @tkdrob homeassistant/components/egardia/* @jeroenterheerdt homeassistant/components/eight_sleep/* @mezz64 @raman325 homeassistant/components/elgato/* @frenck +tests/components/elgato/* @frenck homeassistant/components/elkm1/* @gwww @bdraco +tests/components/elkm1/* @gwww @bdraco homeassistant/components/elmax/* @albertogeniola +tests/components/elmax/* @albertogeniola homeassistant/components/elv/* @majuss homeassistant/components/emby/* @mezz64 homeassistant/components/emoncms/* @borpin homeassistant/components/emonitor/* @bdraco +tests/components/emonitor/* @bdraco homeassistant/components/emulated_kasa/* @kbickar +tests/components/emulated_kasa/* @kbickar homeassistant/components/energy/* @home-assistant/core +tests/components/energy/* @home-assistant/core homeassistant/components/enigma2/* @fbradyirl homeassistant/components/enocean/* @bdurrer +tests/components/enocean/* @bdurrer homeassistant/components/enphase_envoy/* @gtdiehl +tests/components/enphase_envoy/* @gtdiehl homeassistant/components/entur_public_transport/* @hfurubotten homeassistant/components/environment_canada/* @gwww @michaeldavie +tests/components/environment_canada/* @gwww @michaeldavie homeassistant/components/ephember/* @ttroy50 homeassistant/components/epson/* @pszafer +tests/components/epson/* @pszafer homeassistant/components/epsonworkforce/* @ThaStealth homeassistant/components/eq3btsmart/* @rytilahti homeassistant/components/esphome/* @OttoWinter @jesserockz +tests/components/esphome/* @OttoWinter @jesserockz homeassistant/components/evil_genius_labs/* @balloob +tests/components/evil_genius_labs/* @balloob homeassistant/components/evohome/* @zxdavb homeassistant/components/ezviz/* @RenierM26 @baqs +tests/components/ezviz/* @RenierM26 @baqs homeassistant/components/faa_delays/* @ntilley905 +tests/components/faa_delays/* @ntilley905 homeassistant/components/fastdotcom/* @rohankapoorcom homeassistant/components/file/* @fabaff +tests/components/file/* @fabaff homeassistant/components/filter/* @dgomes +tests/components/filter/* @dgomes homeassistant/components/fireservicerota/* @cyberjunky +tests/components/fireservicerota/* @cyberjunky homeassistant/components/firmata/* @DaAwesomeP +tests/components/firmata/* @DaAwesomeP homeassistant/components/fixer/* @fabaff homeassistant/components/fjaraskupan/* @elupus +tests/components/fjaraskupan/* @elupus homeassistant/components/flick_electric/* @ZephireNZ +tests/components/flick_electric/* @ZephireNZ homeassistant/components/flipr/* @cnico +tests/components/flipr/* @cnico homeassistant/components/flo/* @dmulcahey +tests/components/flo/* @dmulcahey homeassistant/components/flock/* @fabaff homeassistant/components/flume/* @ChrisMandich @bdraco +tests/components/flume/* @ChrisMandich @bdraco homeassistant/components/flunearyou/* @bachya +tests/components/flunearyou/* @bachya homeassistant/components/flux_led/* @icemanch +tests/components/flux_led/* @icemanch homeassistant/components/forecast_solar/* @klaasnicolaas @frenck +tests/components/forecast_solar/* @klaasnicolaas @frenck homeassistant/components/forked_daapd/* @uvjustin +tests/components/forked_daapd/* @uvjustin homeassistant/components/fortios/* @kimfrellsen homeassistant/components/foscam/* @skgsergio +tests/components/foscam/* @skgsergio homeassistant/components/freebox/* @hacf-fr @Quentame +tests/components/freebox/* @hacf-fr @Quentame homeassistant/components/freedompro/* @stefano055415 +tests/components/freedompro/* @stefano055415 homeassistant/components/fritz/* @mammuth @AaronDavidSchneider @chemelli74 +tests/components/fritz/* @mammuth @AaronDavidSchneider @chemelli74 homeassistant/components/fritzbox/* @mib1185 @flabbamann +tests/components/fritzbox/* @mib1185 @flabbamann homeassistant/components/fronius/* @nielstron @farmio +tests/components/fronius/* @nielstron @farmio homeassistant/components/frontend/* @home-assistant/frontend +tests/components/frontend/* @home-assistant/frontend homeassistant/components/garages_amsterdam/* @klaasnicolaas +tests/components/garages_amsterdam/* @klaasnicolaas homeassistant/components/gdacs/* @exxamalte +tests/components/gdacs/* @exxamalte homeassistant/components/generic_hygrostat/* @Shulyaka +tests/components/generic_hygrostat/* @Shulyaka homeassistant/components/geniushub/* @zxdavb homeassistant/components/geo_json_events/* @exxamalte +tests/components/geo_json_events/* @exxamalte homeassistant/components/geo_rss_events/* @exxamalte +tests/components/geo_rss_events/* @exxamalte homeassistant/components/geonetnz_quakes/* @exxamalte +tests/components/geonetnz_quakes/* @exxamalte homeassistant/components/geonetnz_volcano/* @exxamalte +tests/components/geonetnz_volcano/* @exxamalte homeassistant/components/gios/* @bieniu +tests/components/gios/* @bieniu homeassistant/components/github/* @timmo001 @ludeeus homeassistant/components/gitter/* @fabaff homeassistant/components/glances/* @fabaff @engrbm87 +tests/components/glances/* @fabaff @engrbm87 homeassistant/components/goalzero/* @tkdrob +tests/components/goalzero/* @tkdrob homeassistant/components/gogogate2/* @vangorra @bdraco +tests/components/gogogate2/* @vangorra @bdraco homeassistant/components/google_assistant/* @home-assistant/cloud +tests/components/google_assistant/* @home-assistant/cloud homeassistant/components/google_cloud/* @lufton homeassistant/components/gpsd/* @fabaff homeassistant/components/gree/* @cmroche +tests/components/gree/* @cmroche homeassistant/components/greeneye_monitor/* @jkeljo +tests/components/greeneye_monitor/* @jkeljo homeassistant/components/group/* @home-assistant/core +tests/components/group/* @home-assistant/core homeassistant/components/growatt_server/* @indykoning @muppet3000 @JasperPlant +tests/components/growatt_server/* @indykoning @muppet3000 @JasperPlant homeassistant/components/guardian/* @bachya +tests/components/guardian/* @bachya homeassistant/components/habitica/* @ASMfreaK @leikoilja +tests/components/habitica/* @ASMfreaK @leikoilja homeassistant/components/harmony/* @ehendrix23 @bramkragten @bdraco @mkeesey @Aohzan +tests/components/harmony/* @ehendrix23 @bramkragten @bdraco @mkeesey @Aohzan homeassistant/components/hassio/* @home-assistant/supervisor +tests/components/hassio/* @home-assistant/supervisor homeassistant/components/heatmiser/* @andylockran homeassistant/components/heos/* @andrewsayre +tests/components/heos/* @andrewsayre homeassistant/components/here_travel_time/* @eifinger +tests/components/here_travel_time/* @eifinger homeassistant/components/hikvision/* @mezz64 homeassistant/components/hikvisioncam/* @fbradyirl homeassistant/components/hisense_aehw4a1/* @bannhead +tests/components/hisense_aehw4a1/* @bannhead homeassistant/components/history/* @home-assistant/core +tests/components/history/* @home-assistant/core homeassistant/components/hive/* @Rendili @KJonline +tests/components/hive/* @Rendili @KJonline homeassistant/components/hlk_sw16/* @jameshilliard +tests/components/hlk_sw16/* @jameshilliard homeassistant/components/home_connect/* @DavidMStraub +tests/components/home_connect/* @DavidMStraub homeassistant/components/home_plus_control/* @chemaaa +tests/components/home_plus_control/* @chemaaa homeassistant/components/homeassistant/* @home-assistant/core +tests/components/homeassistant/* @home-assistant/core homeassistant/components/homekit/* @bdraco +tests/components/homekit/* @bdraco homeassistant/components/homekit_controller/* @Jc2k @bdraco +tests/components/homekit_controller/* @Jc2k @bdraco homeassistant/components/homematic/* @pvizeli @danielperna84 +tests/components/homematic/* @pvizeli @danielperna84 homeassistant/components/honeywell/* @rdfurman +tests/components/honeywell/* @rdfurman homeassistant/components/http/* @home-assistant/core +tests/components/http/* @home-assistant/core homeassistant/components/huawei_lte/* @scop @fphammerle +tests/components/huawei_lte/* @scop @fphammerle homeassistant/components/hue/* @balloob @marcelveldt +tests/components/hue/* @balloob @marcelveldt homeassistant/components/huisbaasje/* @dennisschroer +tests/components/huisbaasje/* @dennisschroer homeassistant/components/humidifier/* @home-assistant/core @Shulyaka +tests/components/humidifier/* @home-assistant/core @Shulyaka homeassistant/components/hunterdouglas_powerview/* @bdraco +tests/components/hunterdouglas_powerview/* @bdraco homeassistant/components/hvv_departures/* @vigonotion +tests/components/hvv_departures/* @vigonotion homeassistant/components/hydrawise/* @ptcryan homeassistant/components/hyperion/* @dermotduffy +tests/components/hyperion/* @dermotduffy homeassistant/components/ialarm/* @RyuzakiKK +tests/components/ialarm/* @RyuzakiKK homeassistant/components/iammeter/* @lewei50 homeassistant/components/iaqualink/* @flz +tests/components/iaqualink/* @flz homeassistant/components/icloud/* @Quentame @nzapponi +tests/components/icloud/* @Quentame @nzapponi homeassistant/components/ign_sismologia/* @exxamalte +tests/components/ign_sismologia/* @exxamalte homeassistant/components/image/* @home-assistant/core +tests/components/image/* @home-assistant/core homeassistant/components/incomfort/* @zxdavb homeassistant/components/influxdb/* @fabaff @mdegat01 +tests/components/influxdb/* @fabaff @mdegat01 homeassistant/components/input_boolean/* @home-assistant/core +tests/components/input_boolean/* @home-assistant/core homeassistant/components/input_datetime/* @home-assistant/core +tests/components/input_datetime/* @home-assistant/core homeassistant/components/input_number/* @home-assistant/core +tests/components/input_number/* @home-assistant/core homeassistant/components/input_select/* @home-assistant/core +tests/components/input_select/* @home-assistant/core homeassistant/components/input_text/* @home-assistant/core +tests/components/input_text/* @home-assistant/core homeassistant/components/insteon/* @teharris1 +tests/components/insteon/* @teharris1 homeassistant/components/integration/* @dgomes +tests/components/integration/* @dgomes homeassistant/components/intent/* @home-assistant/core +tests/components/intent/* @home-assistant/core homeassistant/components/intesishome/* @jnimmo homeassistant/components/ios/* @robbiet480 +tests/components/ios/* @robbiet480 homeassistant/components/iotawatt/* @gtdiehl @jyavenard +tests/components/iotawatt/* @gtdiehl @jyavenard homeassistant/components/iperf3/* @rohankapoorcom homeassistant/components/ipma/* @dgomes @abmantis +tests/components/ipma/* @dgomes @abmantis homeassistant/components/ipp/* @ctalkington +tests/components/ipp/* @ctalkington homeassistant/components/iqvia/* @bachya +tests/components/iqvia/* @bachya homeassistant/components/irish_rail_transport/* @ttroy50 homeassistant/components/islamic_prayer_times/* @engrbm87 +tests/components/islamic_prayer_times/* @engrbm87 homeassistant/components/isy994/* @bdraco @shbatm +tests/components/isy994/* @bdraco @shbatm homeassistant/components/izone/* @Swamp-Ig +tests/components/izone/* @Swamp-Ig homeassistant/components/jellyfin/* @j-stienstra +tests/components/jellyfin/* @j-stienstra homeassistant/components/jewish_calendar/* @tsvi +tests/components/jewish_calendar/* @tsvi homeassistant/components/juicenet/* @jesserockz +tests/components/juicenet/* @jesserockz homeassistant/components/kaiterra/* @Michsior14 homeassistant/components/keba/* @dannerph homeassistant/components/keenetic_ndms2/* @foxel +tests/components/keenetic_ndms2/* @foxel homeassistant/components/kef/* @basnijholt homeassistant/components/keyboard_remote/* @bendavid @lanrat homeassistant/components/kmtronic/* @dgomes +tests/components/kmtronic/* @dgomes homeassistant/components/knx/* @Julius2342 @farmio @marvin-w +tests/components/knx/* @Julius2342 @farmio @marvin-w homeassistant/components/kodi/* @OnFreund @cgtobi +tests/components/kodi/* @OnFreund @cgtobi homeassistant/components/konnected/* @heythisisnate @kit-klein +tests/components/konnected/* @heythisisnate @kit-klein homeassistant/components/kostal_plenticore/* @stegm +tests/components/kostal_plenticore/* @stegm homeassistant/components/kraken/* @eifinger +tests/components/kraken/* @eifinger homeassistant/components/kulersky/* @emlove +tests/components/kulersky/* @emlove homeassistant/components/lametric/* @robbiet480 homeassistant/components/launch_library/* @ludeeus homeassistant/components/lcn/* @alengwenus +tests/components/lcn/* @alengwenus homeassistant/components/lg_netcast/* @Drafteed homeassistant/components/life360/* @pnbruckner homeassistant/components/linux_battery/* @fabaff homeassistant/components/litejet/* @joncar +tests/components/litejet/* @joncar homeassistant/components/litterrobot/* @natekspencer +tests/components/litterrobot/* @natekspencer homeassistant/components/local_ip/* @issacg +tests/components/local_ip/* @issacg homeassistant/components/logger/* @home-assistant/core +tests/components/logger/* @home-assistant/core homeassistant/components/logi_circle/* @evanjd +tests/components/logi_circle/* @evanjd homeassistant/components/lookin/* @ANMalko @bdraco +tests/components/lookin/* @ANMalko @bdraco homeassistant/components/lovelace/* @home-assistant/frontend +tests/components/lovelace/* @home-assistant/frontend homeassistant/components/luci/* @mzdrale homeassistant/components/luftdaten/* @fabaff +tests/components/luftdaten/* @fabaff homeassistant/components/lupusec/* @majuss homeassistant/components/lutron/* @JonGilmore homeassistant/components/lutron_caseta/* @swails @bdraco +tests/components/lutron_caseta/* @swails @bdraco homeassistant/components/lyric/* @timmo001 +tests/components/lyric/* @timmo001 homeassistant/components/mastodon/* @fabaff homeassistant/components/matrix/* @tinloaf homeassistant/components/mazda/* @bdr99 +tests/components/mazda/* @bdr99 homeassistant/components/mcp23017/* @jardiamj homeassistant/components/media_source/* @hunterjm +tests/components/media_source/* @hunterjm homeassistant/components/mediaroom/* @dgomes homeassistant/components/melcloud/* @vilppuvuorinen +tests/components/melcloud/* @vilppuvuorinen homeassistant/components/melissa/* @kennedyshead +tests/components/melissa/* @kennedyshead homeassistant/components/met/* @danielhiversen @thimic +tests/components/met/* @danielhiversen @thimic homeassistant/components/met_eireann/* @DylanGore +tests/components/met_eireann/* @DylanGore homeassistant/components/meteo_france/* @hacf-fr @oncleben31 @Quentame +tests/components/meteo_france/* @hacf-fr @oncleben31 @Quentame homeassistant/components/meteoalarm/* @rolfberkenbosch homeassistant/components/meteoclimatic/* @adrianmo +tests/components/meteoclimatic/* @adrianmo homeassistant/components/metoffice/* @MrHarcombe +tests/components/metoffice/* @MrHarcombe homeassistant/components/miflora/* @danielhiversen @basnijholt homeassistant/components/mikrotik/* @engrbm87 +tests/components/mikrotik/* @engrbm87 homeassistant/components/mill/* @danielhiversen +tests/components/mill/* @danielhiversen homeassistant/components/min_max/* @fabaff +tests/components/min_max/* @fabaff homeassistant/components/minecraft_server/* @elmurato +tests/components/minecraft_server/* @elmurato homeassistant/components/minio/* @tkislan +tests/components/minio/* @tkislan homeassistant/components/mobile_app/* @robbiet480 +tests/components/mobile_app/* @robbiet480 homeassistant/components/modbus/* @adamchengtkc @janiversen @vzahradnik +tests/components/modbus/* @adamchengtkc @janiversen @vzahradnik homeassistant/components/modem_callerid/* @tkdrob +tests/components/modem_callerid/* @tkdrob homeassistant/components/modern_forms/* @wonderslug +tests/components/modern_forms/* @wonderslug homeassistant/components/monoprice/* @etsinko @OnFreund +tests/components/monoprice/* @etsinko @OnFreund homeassistant/components/moon/* @fabaff +tests/components/moon/* @fabaff homeassistant/components/motion_blinds/* @starkillerOG +tests/components/motion_blinds/* @starkillerOG homeassistant/components/motioneye/* @dermotduffy +tests/components/motioneye/* @dermotduffy homeassistant/components/mpd/* @fabaff homeassistant/components/mqtt/* @emontnemery +tests/components/mqtt/* @emontnemery homeassistant/components/msteams/* @peroyvind homeassistant/components/mullvad/* @meichthys +tests/components/mullvad/* @meichthys homeassistant/components/mutesync/* @currentoor +tests/components/mutesync/* @currentoor homeassistant/components/my/* @home-assistant/core +tests/components/my/* @home-assistant/core homeassistant/components/myq/* @bdraco @ehendrix23 +tests/components/myq/* @bdraco @ehendrix23 homeassistant/components/mysensors/* @MartinHjelmare @functionpointer +tests/components/mysensors/* @MartinHjelmare @functionpointer homeassistant/components/mystrom/* @fabaff homeassistant/components/nam/* @bieniu +tests/components/nam/* @bieniu homeassistant/components/nanoleaf/* @milanmeu +tests/components/nanoleaf/* @milanmeu homeassistant/components/neato/* @dshokouhi @Santobert +tests/components/neato/* @dshokouhi @Santobert homeassistant/components/nederlandse_spoorwegen/* @YarmoM homeassistant/components/ness_alarm/* @nickw444 +tests/components/ness_alarm/* @nickw444 homeassistant/components/nest/* @allenporter +tests/components/nest/* @allenporter homeassistant/components/netatmo/* @cgtobi +tests/components/netatmo/* @cgtobi homeassistant/components/netdata/* @fabaff homeassistant/components/netgear/* @hacf-fr @Quentame @starkillerOG +tests/components/netgear/* @hacf-fr @Quentame @starkillerOG homeassistant/components/nexia/* @bdraco +tests/components/nexia/* @bdraco homeassistant/components/nextbus/* @vividboarder +tests/components/nextbus/* @vividboarder homeassistant/components/nextcloud/* @meichthys homeassistant/components/nfandroidtv/* @tkdrob +tests/components/nfandroidtv/* @tkdrob homeassistant/components/nightscout/* @marciogranzotto +tests/components/nightscout/* @marciogranzotto homeassistant/components/nilu/* @hfurubotten homeassistant/components/nina/* @DeerMaximum +tests/components/nina/* @DeerMaximum homeassistant/components/nissan_leaf/* @filcole homeassistant/components/nmbs/* @thibmaek homeassistant/components/no_ip/* @fabaff +tests/components/no_ip/* @fabaff homeassistant/components/noaa_tides/* @jdelaney72 homeassistant/components/notify/* @home-assistant/core +tests/components/notify/* @home-assistant/core homeassistant/components/notify_events/* @matrozov @papajojo +tests/components/notify_events/* @matrozov @papajojo homeassistant/components/notion/* @bachya +tests/components/notion/* @bachya homeassistant/components/nsw_fuel_station/* @nickw444 +tests/components/nsw_fuel_station/* @nickw444 homeassistant/components/nsw_rural_fire_service_feed/* @exxamalte +tests/components/nsw_rural_fire_service_feed/* @exxamalte homeassistant/components/nuki/* @pschmitt @pvizeli @pree +tests/components/nuki/* @pschmitt @pvizeli @pree homeassistant/components/numato/* @clssn +tests/components/numato/* @clssn homeassistant/components/number/* @home-assistant/core @Shulyaka +tests/components/number/* @home-assistant/core @Shulyaka homeassistant/components/nut/* @bdraco @ollo69 +tests/components/nut/* @bdraco @ollo69 homeassistant/components/nws/* @MatthewFlamm +tests/components/nws/* @MatthewFlamm homeassistant/components/nzbget/* @chriscla +tests/components/nzbget/* @chriscla homeassistant/components/obihai/* @dshokouhi homeassistant/components/octoprint/* @rfleming71 +tests/components/octoprint/* @rfleming71 homeassistant/components/ohmconnect/* @robbiet480 homeassistant/components/ombi/* @larssont homeassistant/components/omnilogic/* @oliver84 @djtimca @gentoosu +tests/components/omnilogic/* @oliver84 @djtimca @gentoosu homeassistant/components/onboarding/* @home-assistant/core +tests/components/onboarding/* @home-assistant/core homeassistant/components/ondilo_ico/* @JeromeHXP +tests/components/ondilo_ico/* @JeromeHXP homeassistant/components/onewire/* @garbled1 @epenet +tests/components/onewire/* @garbled1 @epenet homeassistant/components/onvif/* @hunterjm +tests/components/onvif/* @hunterjm homeassistant/components/open_meteo/* @frenck +tests/components/open_meteo/* @frenck homeassistant/components/openerz/* @misialq +tests/components/openerz/* @misialq homeassistant/components/opengarage/* @danielhiversen +tests/components/opengarage/* @danielhiversen homeassistant/components/openhome/* @bazwilliams homeassistant/components/opentherm_gw/* @mvn23 +tests/components/opentherm_gw/* @mvn23 homeassistant/components/openuv/* @bachya +tests/components/openuv/* @bachya homeassistant/components/openweathermap/* @fabaff @freekode @nzapponi +tests/components/openweathermap/* @fabaff @freekode @nzapponi homeassistant/components/opnsense/* @mtreinish +tests/components/opnsense/* @mtreinish homeassistant/components/orangepi_gpio/* @pascallj homeassistant/components/oru/* @bvlaicu homeassistant/components/ovo_energy/* @timmo001 +tests/components/ovo_energy/* @timmo001 homeassistant/components/ozw/* @cgarwood @marcelveldt @MartinHjelmare +tests/components/ozw/* @cgarwood @marcelveldt @MartinHjelmare homeassistant/components/p1_monitor/* @klaasnicolaas +tests/components/p1_monitor/* @klaasnicolaas homeassistant/components/panel_custom/* @home-assistant/frontend +tests/components/panel_custom/* @home-assistant/frontend homeassistant/components/panel_iframe/* @home-assistant/frontend +tests/components/panel_iframe/* @home-assistant/frontend homeassistant/components/pcal9535a/* @Shulyaka homeassistant/components/persistent_notification/* @home-assistant/core +tests/components/persistent_notification/* @home-assistant/core homeassistant/components/philips_js/* @elupus +tests/components/philips_js/* @elupus homeassistant/components/pi4ioe5v9xxxx/* @antonverburg homeassistant/components/pi_hole/* @fabaff @johnluetke @shenxn +tests/components/pi_hole/* @fabaff @johnluetke @shenxn homeassistant/components/picnic/* @corneyl +tests/components/picnic/* @corneyl homeassistant/components/pilight/* @trekky12 +tests/components/pilight/* @trekky12 homeassistant/components/plaato/* @JohNan +tests/components/plaato/* @JohNan homeassistant/components/plex/* @jjlawren +tests/components/plex/* @jjlawren homeassistant/components/plugwise/* @CoMPaTech @bouwew @brefra +tests/components/plugwise/* @CoMPaTech @bouwew @brefra homeassistant/components/plum_lightpad/* @ColinHarrington @prystupa +tests/components/plum_lightpad/* @ColinHarrington @prystupa homeassistant/components/point/* @fredrike +tests/components/point/* @fredrike homeassistant/components/poolsense/* @haemishkyd +tests/components/poolsense/* @haemishkyd homeassistant/components/powerwall/* @bdraco @jrester +tests/components/powerwall/* @bdraco @jrester homeassistant/components/profiler/* @bdraco +tests/components/profiler/* @bdraco homeassistant/components/progettihwsw/* @ardaseremet +tests/components/progettihwsw/* @ardaseremet homeassistant/components/prometheus/* @knyar +tests/components/prometheus/* @knyar homeassistant/components/prosegur/* @dgomes +tests/components/prosegur/* @dgomes homeassistant/components/proxmoxve/* @k4ds3 @jhollowe @Corbeno homeassistant/components/ps4/* @ktnrg45 +tests/components/ps4/* @ktnrg45 homeassistant/components/push/* @dgomes +tests/components/push/* @dgomes homeassistant/components/pvoutput/* @fabaff homeassistant/components/pvpc_hourly_pricing/* @azogue +tests/components/pvpc_hourly_pricing/* @azogue homeassistant/components/qbittorrent/* @geoffreylagaisse homeassistant/components/qld_bushfire/* @exxamalte +tests/components/qld_bushfire/* @exxamalte homeassistant/components/qnap/* @colinodell homeassistant/components/quantum_gateway/* @cisasteelersfan homeassistant/components/qvr_pro/* @oblogic7 homeassistant/components/qwikswitch/* @kellerza +tests/components/qwikswitch/* @kellerza homeassistant/components/rachio/* @bdraco +tests/components/rachio/* @bdraco homeassistant/components/radiotherm/* @vinnyfuria homeassistant/components/rainbird/* @konikvranik homeassistant/components/raincloud/* @vanstinator homeassistant/components/rainforest_eagle/* @gtdiehl @jcalbert +tests/components/rainforest_eagle/* @gtdiehl @jcalbert homeassistant/components/rainmachine/* @bachya +tests/components/rainmachine/* @bachya homeassistant/components/random/* @fabaff +tests/components/random/* @fabaff homeassistant/components/rdw/* @frenck +tests/components/rdw/* @frenck homeassistant/components/recollect_waste/* @bachya +tests/components/recollect_waste/* @bachya homeassistant/components/recorder/* @home-assistant/core +tests/components/recorder/* @home-assistant/core homeassistant/components/rejseplanen/* @DarkFox homeassistant/components/renault/* @epenet +tests/components/renault/* @epenet homeassistant/components/repetier/* @MTrab homeassistant/components/rflink/* @javicalle +tests/components/rflink/* @javicalle homeassistant/components/rfxtrx/* @danielhiversen @elupus @RobBie1221 +tests/components/rfxtrx/* @danielhiversen @elupus @RobBie1221 homeassistant/components/ridwell/* @bachya +tests/components/ridwell/* @bachya homeassistant/components/ring/* @balloob +tests/components/ring/* @balloob homeassistant/components/risco/* @OnFreund +tests/components/risco/* @OnFreund homeassistant/components/rituals_perfume_genie/* @milanmeu +tests/components/rituals_perfume_genie/* @milanmeu homeassistant/components/rmvtransport/* @cgtobi +tests/components/rmvtransport/* @cgtobi homeassistant/components/roku/* @ctalkington +tests/components/roku/* @ctalkington homeassistant/components/roomba/* @pschmitt @cyr-ius @shenxn +tests/components/roomba/* @pschmitt @cyr-ius @shenxn homeassistant/components/roon/* @pavoni +tests/components/roon/* @pavoni homeassistant/components/rpi_gpio_pwm/* @soldag homeassistant/components/rpi_power/* @shenxn @swetoast +tests/components/rpi_power/* @shenxn @swetoast homeassistant/components/ruckus_unleashed/* @gabe565 +tests/components/ruckus_unleashed/* @gabe565 homeassistant/components/safe_mode/* @home-assistant/core +tests/components/safe_mode/* @home-assistant/core homeassistant/components/saj/* @fredericvl homeassistant/components/samsungtv/* @escoand @chemelli74 +tests/components/samsungtv/* @escoand @chemelli74 homeassistant/components/scene/* @home-assistant/core +tests/components/scene/* @home-assistant/core homeassistant/components/schluter/* @prairieapps homeassistant/components/scrape/* @fabaff homeassistant/components/screenlogic/* @dieselrabbit @bdraco +tests/components/screenlogic/* @dieselrabbit @bdraco homeassistant/components/script/* @home-assistant/core +tests/components/script/* @home-assistant/core homeassistant/components/search/* @home-assistant/core +tests/components/search/* @home-assistant/core homeassistant/components/select/* @home-assistant/core +tests/components/select/* @home-assistant/core homeassistant/components/sense/* @kbickar +tests/components/sense/* @kbickar homeassistant/components/sensibo/* @andrey-git homeassistant/components/sentry/* @dcramer @frenck +tests/components/sentry/* @dcramer @frenck homeassistant/components/serial/* @fabaff homeassistant/components/seven_segments/* @fabaff homeassistant/components/sharkiq/* @ajmarks +tests/components/sharkiq/* @ajmarks homeassistant/components/shell_command/* @home-assistant/core +tests/components/shell_command/* @home-assistant/core homeassistant/components/shelly/* @balloob @bieniu @thecode @chemelli74 +tests/components/shelly/* @balloob @bieniu @thecode @chemelli74 homeassistant/components/shiftr/* @fabaff homeassistant/components/shodan/* @fabaff homeassistant/components/sia/* @eavanvalkenburg +tests/components/sia/* @eavanvalkenburg homeassistant/components/sighthound/* @robmarkcole +tests/components/sighthound/* @robmarkcole homeassistant/components/signal_messenger/* @bbernhard +tests/components/signal_messenger/* @bbernhard homeassistant/components/simplisafe/* @bachya +tests/components/simplisafe/* @bachya homeassistant/components/sinch/* @bendikrb homeassistant/components/siren/* @home-assistant/core @raman325 +tests/components/siren/* @home-assistant/core @raman325 homeassistant/components/sisyphus/* @jkeljo homeassistant/components/sky_hub/* @rogerselwyn homeassistant/components/slack/* @bachya +tests/components/slack/* @bachya homeassistant/components/slide/* @ualex73 homeassistant/components/sma/* @kellerza @rklomp +tests/components/sma/* @kellerza @rklomp homeassistant/components/smappee/* @bsmappee +tests/components/smappee/* @bsmappee homeassistant/components/smart_meter_texas/* @grahamwetzler +tests/components/smart_meter_texas/* @grahamwetzler homeassistant/components/smarthab/* @outadoc +tests/components/smarthab/* @outadoc homeassistant/components/smartthings/* @andrewsayre +tests/components/smartthings/* @andrewsayre homeassistant/components/smarttub/* @mdz +tests/components/smarttub/* @mdz homeassistant/components/smarty/* @z0mbieprocess homeassistant/components/sms/* @ocalvo homeassistant/components/smtp/* @fabaff +tests/components/smtp/* @fabaff homeassistant/components/solaredge/* @frenck +tests/components/solaredge/* @frenck homeassistant/components/solaredge_local/* @drobtravels @scheric homeassistant/components/solarlog/* @Ernst79 +tests/components/solarlog/* @Ernst79 homeassistant/components/solax/* @squishykid homeassistant/components/soma/* @ratsept +tests/components/soma/* @ratsept homeassistant/components/somfy/* @tetienne +tests/components/somfy/* @tetienne homeassistant/components/sonarr/* @ctalkington +tests/components/sonarr/* @ctalkington homeassistant/components/songpal/* @rytilahti @shenxn +tests/components/songpal/* @rytilahti @shenxn homeassistant/components/sonos/* @cgtobi @jjlawren +tests/components/sonos/* @cgtobi @jjlawren homeassistant/components/spaceapi/* @fabaff +tests/components/spaceapi/* @fabaff homeassistant/components/speedtestdotnet/* @rohankapoorcom @engrbm87 +tests/components/speedtestdotnet/* @rohankapoorcom @engrbm87 homeassistant/components/spider/* @peternijssen +tests/components/spider/* @peternijssen homeassistant/components/splunk/* @Bre77 homeassistant/components/spotify/* @frenck +tests/components/spotify/* @frenck homeassistant/components/sql/* @dgomes +tests/components/sql/* @dgomes homeassistant/components/squeezebox/* @rajlaud +tests/components/squeezebox/* @rajlaud homeassistant/components/srp_energy/* @briglx +tests/components/srp_energy/* @briglx homeassistant/components/starline/* @anonym-tsk +tests/components/starline/* @anonym-tsk homeassistant/components/statistics/* @fabaff +tests/components/statistics/* @fabaff homeassistant/components/stiebel_eltron/* @fucm homeassistant/components/stookalert/* @fwestenberg @frenck +tests/components/stookalert/* @fwestenberg @frenck homeassistant/components/stream/* @hunterjm @uvjustin @allenporter +tests/components/stream/* @hunterjm @uvjustin @allenporter homeassistant/components/stt/* @pvizeli +tests/components/stt/* @pvizeli homeassistant/components/subaru/* @G-Two +tests/components/subaru/* @G-Two homeassistant/components/suez_water/* @ooii homeassistant/components/sun/* @Swamp-Ig +tests/components/sun/* @Swamp-Ig homeassistant/components/supla/* @mwegrzynek homeassistant/components/surepetcare/* @benleb @danielhiversen +tests/components/surepetcare/* @benleb @danielhiversen homeassistant/components/swiss_hydrological_data/* @fabaff homeassistant/components/swiss_public_transport/* @fabaff homeassistant/components/switchbot/* @danielhiversen @RenierM26 +tests/components/switchbot/* @danielhiversen @RenierM26 homeassistant/components/switcher_kis/* @tomerfi @thecode +tests/components/switcher_kis/* @tomerfi @thecode homeassistant/components/switchmate/* @danielhiversen homeassistant/components/syncthing/* @zhulik +tests/components/syncthing/* @zhulik homeassistant/components/syncthru/* @nielstron +tests/components/syncthru/* @nielstron homeassistant/components/synology_dsm/* @hacf-fr @Quentame @mib1185 +tests/components/synology_dsm/* @hacf-fr @Quentame @mib1185 homeassistant/components/synology_srm/* @aerialls homeassistant/components/syslog/* @fabaff homeassistant/components/system_bridge/* @timmo001 +tests/components/system_bridge/* @timmo001 homeassistant/components/tado/* @michaelarnauts @noltari +tests/components/tado/* @michaelarnauts @noltari homeassistant/components/tag/* @balloob @dmulcahey +tests/components/tag/* @balloob @dmulcahey homeassistant/components/tahoma/* @philklei homeassistant/components/tailscale/* @frenck +tests/components/tailscale/* @frenck homeassistant/components/tankerkoenig/* @guillempages homeassistant/components/tapsaff/* @bazwilliams homeassistant/components/tasmota/* @emontnemery +tests/components/tasmota/* @emontnemery homeassistant/components/tautulli/* @ludeeus homeassistant/components/tellduslive/* @fredrike +tests/components/tellduslive/* @fredrike homeassistant/components/template/* @PhracturedBlue @tetienne @home-assistant/core +tests/components/template/* @PhracturedBlue @tetienne @home-assistant/core homeassistant/components/tesla_wall_connector/* @einarhauks +tests/components/tesla_wall_connector/* @einarhauks homeassistant/components/tfiac/* @fredrike @mellado homeassistant/components/thethingsnetwork/* @fabaff homeassistant/components/threshold/* @fabaff +tests/components/threshold/* @fabaff homeassistant/components/tibber/* @danielhiversen +tests/components/tibber/* @danielhiversen homeassistant/components/tile/* @bachya +tests/components/tile/* @bachya homeassistant/components/time_date/* @fabaff +tests/components/time_date/* @fabaff homeassistant/components/tmb/* @alemuro homeassistant/components/todoist/* @boralyl homeassistant/components/tolo/* @MatthiasLohr +tests/components/tolo/* @MatthiasLohr homeassistant/components/totalconnect/* @austinmroczek +tests/components/totalconnect/* @austinmroczek homeassistant/components/tplink/* @rytilahti @thegardenmonkey +tests/components/tplink/* @rytilahti @thegardenmonkey homeassistant/components/traccar/* @ludeeus +tests/components/traccar/* @ludeeus homeassistant/components/trace/* @home-assistant/core +tests/components/trace/* @home-assistant/core homeassistant/components/tractive/* @Danielhiversen @zhulik @bieniu +tests/components/tractive/* @Danielhiversen @zhulik @bieniu homeassistant/components/trafikverket_train/* @endor-force homeassistant/components/trafikverket_weatherstation/* @endor-force +tests/components/trafikverket_weatherstation/* @endor-force homeassistant/components/transmission/* @engrbm87 @JPHutchins +tests/components/transmission/* @engrbm87 @JPHutchins homeassistant/components/tts/* @pvizeli +tests/components/tts/* @pvizeli homeassistant/components/tuya/* @Tuya @zlinoliver @METISU @frenck +tests/components/tuya/* @Tuya @zlinoliver @METISU @frenck homeassistant/components/twentemilieu/* @frenck +tests/components/twentemilieu/* @frenck homeassistant/components/twinkly/* @dr1rrb +tests/components/twinkly/* @dr1rrb homeassistant/components/ubus/* @noltari homeassistant/components/unifi/* @Kane610 +tests/components/unifi/* @Kane610 homeassistant/components/unifiled/* @florisvdk homeassistant/components/upb/* @gwww +tests/components/upb/* @gwww homeassistant/components/upc_connect/* @pvizeli @fabaff homeassistant/components/upcloud/* @scop +tests/components/upcloud/* @scop homeassistant/components/updater/* @home-assistant/core +tests/components/updater/* @home-assistant/core homeassistant/components/upnp/* @StevenLooman @ehendrix23 +tests/components/upnp/* @StevenLooman @ehendrix23 homeassistant/components/uptimerobot/* @ludeeus +tests/components/uptimerobot/* @ludeeus homeassistant/components/usb/* @bdraco +tests/components/usb/* @bdraco homeassistant/components/usgs_earthquakes_feed/* @exxamalte +tests/components/usgs_earthquakes_feed/* @exxamalte homeassistant/components/utility_meter/* @dgomes +tests/components/utility_meter/* @dgomes homeassistant/components/vallox/* @andre-richter homeassistant/components/velbus/* @Cereal2nd @brefra +tests/components/velbus/* @Cereal2nd @brefra homeassistant/components/velux/* @Julius2342 homeassistant/components/venstar/* @garbled1 +tests/components/venstar/* @garbled1 homeassistant/components/vera/* @pavoni +tests/components/vera/* @pavoni homeassistant/components/verisure/* @frenck +tests/components/verisure/* @frenck homeassistant/components/versasense/* @flamm3blemuff1n homeassistant/components/version/* @fabaff @ludeeus +tests/components/version/* @fabaff @ludeeus homeassistant/components/vesync/* @markperdue @webdjoe @thegardenmonkey +tests/components/vesync/* @markperdue @webdjoe @thegardenmonkey homeassistant/components/vicare/* @oischinger +tests/components/vicare/* @oischinger homeassistant/components/vilfo/* @ManneW +tests/components/vilfo/* @ManneW homeassistant/components/vivotek/* @HarlemSquirrel homeassistant/components/vizio/* @raman325 +tests/components/vizio/* @raman325 homeassistant/components/vlc_telnet/* @rodripf @dmcc @MartinHjelmare +tests/components/vlc_telnet/* @rodripf @dmcc @MartinHjelmare homeassistant/components/volkszaehler/* @fabaff homeassistant/components/volumio/* @OnFreund +tests/components/volumio/* @OnFreund homeassistant/components/volvooncall/* @molobrakos @decompil3d homeassistant/components/wake_on_lan/* @ntilley905 +tests/components/wake_on_lan/* @ntilley905 homeassistant/components/wallbox/* @hesselonline +tests/components/wallbox/* @hesselonline homeassistant/components/waqi/* @andrey-git homeassistant/components/watson_tts/* @rutkai homeassistant/components/watttime/* @bachya +tests/components/watttime/* @bachya homeassistant/components/weather/* @fabaff +tests/components/weather/* @fabaff homeassistant/components/webostv/* @bendavid @thecode +tests/components/webostv/* @bendavid @thecode homeassistant/components/websocket_api/* @home-assistant/core +tests/components/websocket_api/* @home-assistant/core homeassistant/components/wemo/* @esev +tests/components/wemo/* @esev homeassistant/components/whirlpool/* @abmantis +tests/components/whirlpool/* @abmantis homeassistant/components/wiffi/* @mampfes +tests/components/wiffi/* @mampfes homeassistant/components/wilight/* @leofig-rj +tests/components/wilight/* @leofig-rj homeassistant/components/wirelesstag/* @sergeymaysak homeassistant/components/withings/* @vangorra +tests/components/withings/* @vangorra homeassistant/components/wled/* @frenck +tests/components/wled/* @frenck homeassistant/components/wolflink/* @adamkrol93 +tests/components/wolflink/* @adamkrol93 homeassistant/components/workday/* @fabaff +tests/components/workday/* @fabaff homeassistant/components/worldclock/* @fabaff +tests/components/worldclock/* @fabaff homeassistant/components/xbox/* @hunterjm +tests/components/xbox/* @hunterjm homeassistant/components/xbox_live/* @MartinHjelmare homeassistant/components/xiaomi_aqara/* @danielhiversen @syssi +tests/components/xiaomi_aqara/* @danielhiversen @syssi homeassistant/components/xiaomi_miio/* @rytilahti @syssi @starkillerOG @bieniu +tests/components/xiaomi_miio/* @rytilahti @syssi @starkillerOG @bieniu homeassistant/components/xiaomi_tv/* @simse homeassistant/components/xmpp/* @fabaff @flowolf homeassistant/components/yale_smart_alarm/* @gjohansson-ST +tests/components/yale_smart_alarm/* @gjohansson-ST homeassistant/components/yamaha_musiccast/* @vigonotion @micha91 +tests/components/yamaha_musiccast/* @vigonotion @micha91 homeassistant/components/yandex_transport/* @rishatik92 @devbis +tests/components/yandex_transport/* @rishatik92 @devbis homeassistant/components/yeelight/* @rytilahti @zewelor @shenxn @starkillerOG +tests/components/yeelight/* @rytilahti @zewelor @shenxn @starkillerOG homeassistant/components/yeelightsunflower/* @lindsaymarkward homeassistant/components/yi/* @bachya homeassistant/components/youless/* @gjong +tests/components/youless/* @gjong homeassistant/components/zeroconf/* @bdraco +tests/components/zeroconf/* @bdraco homeassistant/components/zerproc/* @emlove +tests/components/zerproc/* @emlove homeassistant/components/zha/* @dmulcahey @adminiuga +tests/components/zha/* @dmulcahey @adminiuga homeassistant/components/zodiac/* @JulienTant +tests/components/zodiac/* @JulienTant homeassistant/components/zone/* @home-assistant/core +tests/components/zone/* @home-assistant/core homeassistant/components/zoneminder/* @rohankapoorcom homeassistant/components/zwave/* @home-assistant/z-wave +tests/components/zwave/* @home-assistant/z-wave homeassistant/components/zwave_js/* @home-assistant/z-wave +tests/components/zwave_js/* @home-assistant/z-wave # Individual files homeassistant/components/demo/weather @fabaff diff --git a/script/hassfest/codeowners.py b/script/hassfest/codeowners.py index 81c3c883965..91bd81efef5 100644 --- a/script/hassfest/codeowners.py +++ b/script/hassfest/codeowners.py @@ -33,7 +33,7 @@ homeassistant/components/demo/weather @fabaff """ -def generate_and_validate(integrations: dict[str, Integration]): +def generate_and_validate(integrations: dict[str, Integration], config: Config): """Generate CODEOWNERS.""" parts = [BASE] @@ -56,6 +56,9 @@ def generate_and_validate(integrations: dict[str, Integration]): parts.append(f"homeassistant/components/{domain}/* {' '.join(codeowners)}") + if (config.root / "tests/components" / domain).exists(): + parts.append(f"tests/components/{domain}/* {' '.join(codeowners)}") + parts.append(f"\n{INDIVIDUAL_FILES.strip()}") return "\n".join(parts) @@ -64,7 +67,7 @@ def generate_and_validate(integrations: dict[str, Integration]): def validate(integrations: dict[str, Integration], config: Config): """Validate CODEOWNERS.""" codeowners_path = config.root / "CODEOWNERS" - config.cache["codeowners"] = content = generate_and_validate(integrations) + config.cache["codeowners"] = content = generate_and_validate(integrations, config) if config.specific_integrations: return From 8683a30380e50c107825650b07032fac8fc7bd3d Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Fri, 17 Dec 2021 10:22:46 -0500 Subject: [PATCH 0684/2644] Use enums in utility_meter tests (#62190) * Use enums in utility_meter tests * uno mas --- tests/components/utility_meter/test_sensor.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index 8d9b819f610..51212580aaf 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -5,8 +5,8 @@ from unittest.mock import patch from homeassistant.components.sensor import ( ATTR_STATE_CLASS, - STATE_CLASS_TOTAL, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, + SensorStateClass, ) from homeassistant.components.utility_meter.const import ( ATTR_TARIFF, @@ -269,15 +269,15 @@ async def test_device_class(hass): state = hass.states.get("sensor.energy_meter") assert state is not None assert state.state == "0" - assert state.attributes.get(ATTR_DEVICE_CLASS) == "energy" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL + assert state.attributes.get(ATTR_DEVICE_CLASS) is SensorDeviceClass.ENERGY.value + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR state = hass.states.get("sensor.gas_meter") assert state is not None assert state.state == "0" assert state.attributes.get(ATTR_DEVICE_CLASS) is None - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL_INCREASING assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "some_archaic_unit" From d874eb261dfcf53da2d575d6019236c4e44115bf Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Fri, 17 Dec 2021 07:23:53 -0800 Subject: [PATCH 0685/2644] Fix Wemo create task for awaitable (#62159) --- homeassistant/components/wemo/wemo_device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/wemo/wemo_device.py b/homeassistant/components/wemo/wemo_device.py index a4f20eb55f5..a507338a4cd 100644 --- a/homeassistant/components/wemo/wemo_device.py +++ b/homeassistant/components/wemo/wemo_device.py @@ -61,7 +61,7 @@ class DeviceCoordinator(DataUpdateCoordinator): ) else: updated = self.wemo.subscription_update(event_type, params) - self.hass.add_job(self._async_subscription_callback(updated)) + self.hass.create_task(self._async_subscription_callback(updated)) async def _async_subscription_callback(self, updated: bool) -> None: """Update the state by the Wemo device.""" From 5e0eb0eb4dc044f30fd40abb91db9fa4d8a5eace Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Fri, 17 Dec 2021 10:25:37 -0500 Subject: [PATCH 0686/2644] Use enums in roku tests (#62196) --- tests/components/roku/test_media_player.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/roku/test_media_player.py b/tests/components/roku/test_media_player.py index 9d97e467c68..9874c23bec5 100644 --- a/tests/components/roku/test_media_player.py +++ b/tests/components/roku/test_media_player.py @@ -4,7 +4,7 @@ from unittest.mock import patch from rokuecp import RokuError -from homeassistant.components.media_player import DEVICE_CLASS_RECEIVER, DEVICE_CLASS_TV +from homeassistant.components.media_player import MediaPlayerDeviceClass from homeassistant.components.media_player.const import ( ATTR_APP_ID, ATTR_APP_NAME, @@ -89,7 +89,7 @@ async def test_setup(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) - assert hass.states.get(MAIN_ENTITY_ID) assert main - assert main.original_device_class == DEVICE_CLASS_RECEIVER + assert main.original_device_class is MediaPlayerDeviceClass.RECEIVER assert main.unique_id == UPNP_SERIAL @@ -121,7 +121,7 @@ async def test_tv_setup( assert hass.states.get(TV_ENTITY_ID) assert tv - assert tv.original_device_class == DEVICE_CLASS_TV + assert tv.original_device_class is MediaPlayerDeviceClass.TV assert tv.unique_id == TV_SERIAL From 3bd1f00b76e50818a458bf262e482d3e05a0315e Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Fri, 17 Dec 2021 10:26:53 -0500 Subject: [PATCH 0687/2644] Use enums in samsungtv tests (#62195) * Use enums in samsungtv tests * uno mas --- tests/components/samsungtv/test_media_player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 1d81769ad8b..f64634acd3b 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -9,7 +9,7 @@ from samsungctl import exceptions from samsungtvws.exceptions import ConnectionFailure from websocket import WebSocketException -from homeassistant.components.media_player import DEVICE_CLASS_TV +from homeassistant.components.media_player import MediaPlayerDeviceClass from homeassistant.components.media_player.const import ( ATTR_INPUT_SOURCE, ATTR_MEDIA_CONTENT_ID, @@ -517,7 +517,7 @@ async def test_device_class(hass, remote): """Test for device_class property.""" await setup_samsungtv(hass, MOCK_CONFIG) state = hass.states.get(ENTITY_ID) - assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_TV + assert state.attributes[ATTR_DEVICE_CLASS] is MediaPlayerDeviceClass.TV.value async def test_turn_off_websocket(hass, remotews): From a9fc750fe3f86e8f3cba42d5e6e452cf0b694f42 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 17 Dec 2021 08:31:23 -0700 Subject: [PATCH 0688/2644] Fix spurious RainMachine config entry reload (#62215) --- homeassistant/components/rainmachine/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/rainmachine/config_flow.py b/homeassistant/components/rainmachine/config_flow.py index 39884681967..989616f6367 100644 --- a/homeassistant/components/rainmachine/config_flow.py +++ b/homeassistant/components/rainmachine/config_flow.py @@ -85,7 +85,7 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ): await self.async_set_unique_id(controller.mac) self._abort_if_unique_id_configured( - updates={CONF_IP_ADDRESS: ip_address} + updates={CONF_IP_ADDRESS: ip_address}, reload_on_update=False ) # A new rain machine: We will change out the unique id From 3b97c544b1ebd3453d654c695078ff6c1064472f Mon Sep 17 00:00:00 2001 From: jkuettner <12213711+jkuettner@users.noreply.github.com> Date: Fri, 17 Dec 2021 16:54:19 +0100 Subject: [PATCH 0689/2644] Fix "vevent" KeyError in caldav component (#61718) --- homeassistant/components/caldav/calendar.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py index d27555beb2c..8ed8ea24607 100644 --- a/homeassistant/components/caldav/calendar.py +++ b/homeassistant/components/caldav/calendar.py @@ -161,6 +161,9 @@ class WebDavCalendarData: ) event_list = [] for event in vevent_list: + if not hasattr(event.instance, "vevent"): + _LOGGER.warning("Skipped event with missing 'vevent' property") + continue vevent = event.instance.vevent if not self.is_matching(vevent, self.search): continue @@ -198,6 +201,9 @@ class WebDavCalendarData: # and they would not be properly parsed using their original start/end dates. new_events = [] for event in results: + if not hasattr(event.instance, "vevent"): + _LOGGER.warning("Skipped event with missing 'vevent' property") + continue vevent = event.instance.vevent for start_dt in vevent.getrruleset() or []: _start_of_today = start_of_today From a0cd29bbcf761005114a58fedb04ec2bf97a6465 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Dec 2021 11:26:30 -0600 Subject: [PATCH 0690/2644] Fix threading error in stream tests (#62221) --- tests/components/stream/conftest.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/components/stream/conftest.py b/tests/components/stream/conftest.py index 10328a8f87b..4b8f21a3a7e 100644 --- a/tests/components/stream/conftest.py +++ b/tests/components/stream/conftest.py @@ -78,8 +78,9 @@ class SaveRecordWorkerSync: to avoid thread leaks in tests. """ - def __init__(self): + def __init__(self, hass): """Initialize SaveRecordWorkerSync.""" + self._hass = hass self._save_event = None self._segments = None self._save_thread = None @@ -91,7 +92,7 @@ class SaveRecordWorkerSync: assert self._save_thread is None self._segments = segments self._save_thread = threading.current_thread() - self._save_event.set() + self._hass.loop.call_soon_threadsafe(self._save_event.set) async def get_segments(self): """Return the recorded video segments.""" @@ -115,7 +116,7 @@ class SaveRecordWorkerSync: @pytest.fixture() def record_worker_sync(hass): """Patch recorder_save_worker for clean thread shutdown for test.""" - sync = SaveRecordWorkerSync() + sync = SaveRecordWorkerSync(hass) with patch( "homeassistant.components.stream.recorder.recorder_save_worker", side_effect=sync.recorder_save_worker, From c59ae54dc8c4ca44c07d2d64451ee333aa7a0884 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 17 Dec 2021 20:35:21 +0100 Subject: [PATCH 0691/2644] Use new enums in sonos (#62202) --- homeassistant/components/sonos/binary_sensor.py | 8 ++++---- homeassistant/components/sonos/number.py | 4 ++-- homeassistant/components/sonos/sensor.py | 15 ++++++--------- homeassistant/components/sonos/switch.py | 7 ++++--- 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/sonos/binary_sensor.py b/homeassistant/components/sonos/binary_sensor.py index 615ad24e655..bab153175e8 100644 --- a/homeassistant/components/sonos/binary_sensor.py +++ b/homeassistant/components/sonos/binary_sensor.py @@ -4,11 +4,11 @@ from __future__ import annotations from typing import Any from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_BATTERY_CHARGING, + BinarySensorDeviceClass, BinarySensorEntity, ) -from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import EntityCategory from .const import SONOS_CREATE_BATTERY from .entity import SonosEntity @@ -32,8 +32,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class SonosPowerEntity(SonosEntity, BinarySensorEntity): """Representation of a Sonos power entity.""" - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC - _attr_device_class = DEVICE_CLASS_BATTERY_CHARGING + _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_device_class = BinarySensorDeviceClass.BATTERY_CHARGING def __init__(self, speaker: SonosSpeaker) -> None: """Initialize the power entity binary sensor.""" diff --git a/homeassistant/components/sonos/number.py b/homeassistant/components/sonos/number.py index 2bcfe5cd5ec..c1b0ffcfd3b 100644 --- a/homeassistant/components/sonos/number.py +++ b/homeassistant/components/sonos/number.py @@ -2,9 +2,9 @@ from __future__ import annotations from homeassistant.components.number import NumberEntity -from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import EntityCategory from .const import SONOS_CREATE_LEVELS from .entity import SonosEntity @@ -32,7 +32,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class SonosLevelEntity(SonosEntity, NumberEntity): """Representation of a Sonos level entity.""" - _attr_entity_category = ENTITY_CATEGORY_CONFIG + _attr_entity_category = EntityCategory.CONFIG _attr_min_value = -10 _attr_max_value = 10 diff --git a/homeassistant/components/sonos/sensor.py b/homeassistant/components/sonos/sensor.py index 62017f4d541..6a1162a6aa6 100644 --- a/homeassistant/components/sonos/sensor.py +++ b/homeassistant/components/sonos/sensor.py @@ -1,14 +1,11 @@ """Entity representing a Sonos battery level.""" from __future__ import annotations -from homeassistant.components.sensor import SensorEntity -from homeassistant.const import ( - DEVICE_CLASS_BATTERY, - ENTITY_CATEGORY_DIAGNOSTIC, - PERCENTAGE, -) +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.const import PERCENTAGE from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import EntityCategory from .const import SONOS_CREATE_AUDIO_FORMAT_SENSOR, SONOS_CREATE_BATTERY from .entity import SonosEntity @@ -45,8 +42,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class SonosBatteryEntity(SonosEntity, SensorEntity): """Representation of a Sonos Battery entity.""" - _attr_device_class = DEVICE_CLASS_BATTERY - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_device_class = SensorDeviceClass.BATTERY + _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_native_unit_of_measurement = PERCENTAGE def __init__(self, speaker: SonosSpeaker) -> None: @@ -73,7 +70,7 @@ class SonosBatteryEntity(SonosEntity, SensorEntity): class SonosAudioInputFormatSensorEntity(SonosEntity, SensorEntity): """Representation of a Sonos audio import format sensor entity.""" - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_icon = "mdi:import" _attr_should_poll = True diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index e92263991ab..a94df01fdc9 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -7,10 +7,11 @@ import logging from soco.exceptions import SoCoException, SoCoSlaveException, SoCoUPnPException from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity -from homeassistant.const import ATTR_TIME, ENTITY_CATEGORY_CONFIG +from homeassistant.const import ATTR_TIME from homeassistant.core import callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import EntityCategory from .const import ( DATA_SONOS, @@ -140,7 +141,7 @@ class SonosSwitchEntity(SonosEntity, SwitchEntity): f"sonos_{speaker.zone_name}_{FRIENDLY_NAMES[feature_type]}" ) self.needs_coordinator = feature_type in COORDINATOR_FEATURES - self._attr_entity_category = ENTITY_CATEGORY_CONFIG + self._attr_entity_category = EntityCategory.CONFIG self._attr_name = f"{speaker.zone_name} {FRIENDLY_NAMES[feature_type]}" self._attr_unique_id = f"{speaker.soco.uid}-{feature_type}" self._attr_icon = FEATURE_ICONS.get(feature_type) @@ -194,7 +195,7 @@ class SonosSwitchEntity(SonosEntity, SwitchEntity): class SonosAlarmEntity(SonosEntity, SwitchEntity): """Representation of a Sonos Alarm entity.""" - _attr_entity_category = ENTITY_CATEGORY_CONFIG + _attr_entity_category = EntityCategory.CONFIG _attr_icon = "mdi:alarm" def __init__(self, alarm_id: str, speaker: SonosSpeaker) -> None: From 6ce99bfc8065e3460f3ca2526984d4b79d204353 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Fri, 17 Dec 2021 22:57:02 +0000 Subject: [PATCH 0692/2644] Bump async-upnp-client to 0.23.0 (#62223) --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/components/yeelight/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/dlna_dmr/test_media_player.py | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 9bc020f3693..fb942717817 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -3,7 +3,7 @@ "name": "DLNA Digital Media Renderer", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", - "requirements": ["async-upnp-client==0.22.12"], + "requirements": ["async-upnp-client==0.23.0"], "dependencies": ["ssdp"], "ssdp": [ { diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index ea8b8ff73a4..c11138e0816 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -2,7 +2,7 @@ "domain": "ssdp", "name": "Simple Service Discovery Protocol (SSDP)", "documentation": "https://www.home-assistant.io/integrations/ssdp", - "requirements": ["async-upnp-client==0.22.12"], + "requirements": ["async-upnp-client==0.23.0"], "dependencies": ["network"], "after_dependencies": ["zeroconf"], "codeowners": [], diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index da12c25c7d1..87c40346fed 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -3,7 +3,7 @@ "name": "UPnP/IGD", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/upnp", - "requirements": ["async-upnp-client==0.22.12"], + "requirements": ["async-upnp-client==0.23.0"], "dependencies": ["network", "ssdp"], "codeowners": ["@StevenLooman","@ehendrix23"], "ssdp": [ diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 60098514125..1b4246f478f 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -2,7 +2,7 @@ "domain": "yeelight", "name": "Yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight", - "requirements": ["yeelight==0.7.8", "async-upnp-client==0.22.12"], + "requirements": ["yeelight==0.7.8", "async-upnp-client==0.23.0"], "codeowners": ["@rytilahti", "@zewelor", "@shenxn", "@starkillerOG"], "config_flow": true, "dependencies": ["network"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 529f970cfc5..9d1c3083706 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodiscover==1.4.5 aiohttp==3.8.1 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.22.12 +async-upnp-client==0.23.0 async_timeout==4.0.0 atomicwrites==1.4.0 attrs==21.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index a7134b339d6..18cf542b232 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -342,7 +342,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.22.12 +async-upnp-client==0.23.0 # homeassistant.components.supla asyncpysupla==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 51d7172b520..88987cb4500 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -242,7 +242,7 @@ arcam-fmj==0.12.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.22.12 +async-upnp-client==0.23.0 # homeassistant.components.aurora auroranoaa==0.0.2 diff --git a/tests/components/dlna_dmr/test_media_player.py b/tests/components/dlna_dmr/test_media_player.py index fe2a916fdcc..b8c10b47b60 100644 --- a/tests/components/dlna_dmr/test_media_player.py +++ b/tests/components/dlna_dmr/test_media_player.py @@ -381,7 +381,7 @@ async def test_event_subscribe_rejected( Device state will instead be obtained via polling in async_update. """ - dmr_device_mock.async_subscribe_services.side_effect = UpnpResponseError(501) + dmr_device_mock.async_subscribe_services.side_effect = UpnpResponseError(status=501) mock_entity_id = await setup_mock_component(hass, config_entry_mock) mock_state = hass.states.get(mock_entity_id) From b50a5d32a7241d8543378733f8c221c88de2e064 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Sat, 18 Dec 2021 01:00:04 +0100 Subject: [PATCH 0693/2644] Remove myself from yeelight codeowners (#62239) --- CODEOWNERS | 4 ++-- homeassistant/components/yeelight/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 709d0c9d395..abc61c3fa99 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1055,8 +1055,8 @@ homeassistant/components/yamaha_musiccast/* @vigonotion @micha91 tests/components/yamaha_musiccast/* @vigonotion @micha91 homeassistant/components/yandex_transport/* @rishatik92 @devbis tests/components/yandex_transport/* @rishatik92 @devbis -homeassistant/components/yeelight/* @rytilahti @zewelor @shenxn @starkillerOG -tests/components/yeelight/* @rytilahti @zewelor @shenxn @starkillerOG +homeassistant/components/yeelight/* @zewelor @shenxn @starkillerOG +tests/components/yeelight/* @zewelor @shenxn @starkillerOG homeassistant/components/yeelightsunflower/* @lindsaymarkward homeassistant/components/yi/* @bachya homeassistant/components/youless/* @gjong diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 1b4246f478f..2e958c7a621 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -3,7 +3,7 @@ "name": "Yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight", "requirements": ["yeelight==0.7.8", "async-upnp-client==0.23.0"], - "codeowners": ["@rytilahti", "@zewelor", "@shenxn", "@starkillerOG"], + "codeowners": ["@zewelor", "@shenxn", "@starkillerOG"], "config_flow": true, "dependencies": ["network"], "quality_scale": "platinum", From 5fefb5985f9926f85b0e48b0718a677db6b5e74e Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 18 Dec 2021 00:13:26 +0000 Subject: [PATCH 0694/2644] [ci skip] Translation update --- .../azure_event_hub/translations/ca.json | 18 ++++++- .../azure_event_hub/translations/de.json | 49 +++++++++++++++++++ .../azure_event_hub/translations/et.json | 49 +++++++++++++++++++ .../azure_event_hub/translations/hu.json | 49 +++++++++++++++++++ .../azure_event_hub/translations/id.json | 49 +++++++++++++++++++ .../azure_event_hub/translations/ja.json | 49 +++++++++++++++++++ .../azure_event_hub/translations/pl.json | 49 +++++++++++++++++++ .../azure_event_hub/translations/ru.json | 49 +++++++++++++++++++ .../azure_event_hub/translations/tr.json | 49 +++++++++++++++++++ .../azure_event_hub/translations/zh-Hant.json | 49 +++++++++++++++++++ .../binary_sensor/translations/ja.json | 8 +-- .../components/braviatv/translations/ja.json | 10 ++-- .../components/brother/translations/ja.json | 2 +- .../components/cover/translations/ja.json | 2 +- .../components/dunehd/translations/ja.json | 2 +- .../components/group/translations/ja.json | 2 +- .../components/homekit/translations/ja.json | 8 +-- .../homekit_controller/translations/ja.json | 8 +-- .../components/knx/translations/tr.json | 2 + .../open_meteo/translations/de.json | 12 +++++ .../open_meteo/translations/et.json | 12 +++++ .../open_meteo/translations/hu.json | 12 +++++ .../open_meteo/translations/id.json | 12 +++++ .../open_meteo/translations/ja.json | 12 +++++ .../open_meteo/translations/pl.json | 12 +++++ .../open_meteo/translations/ru.json | 12 +++++ .../open_meteo/translations/tr.json | 12 +++++ .../open_meteo/translations/zh-Hant.json | 12 +++++ .../smartthings/translations/ja.json | 2 +- .../components/twinkly/translations/tr.json | 3 ++ .../components/vicare/translations/de.json | 26 ++++++++++ .../components/vicare/translations/en.json | 8 +-- .../components/vicare/translations/et.json | 26 ++++++++++ .../components/vicare/translations/pl.json | 26 ++++++++++ .../components/vicare/translations/tr.json | 26 ++++++++++ .../vicare/translations/zh-Hant.json | 26 ++++++++++ 36 files changed, 726 insertions(+), 28 deletions(-) create mode 100644 homeassistant/components/azure_event_hub/translations/de.json create mode 100644 homeassistant/components/azure_event_hub/translations/et.json create mode 100644 homeassistant/components/azure_event_hub/translations/hu.json create mode 100644 homeassistant/components/azure_event_hub/translations/id.json create mode 100644 homeassistant/components/azure_event_hub/translations/ja.json create mode 100644 homeassistant/components/azure_event_hub/translations/pl.json create mode 100644 homeassistant/components/azure_event_hub/translations/ru.json create mode 100644 homeassistant/components/azure_event_hub/translations/tr.json create mode 100644 homeassistant/components/azure_event_hub/translations/zh-Hant.json create mode 100644 homeassistant/components/open_meteo/translations/de.json create mode 100644 homeassistant/components/open_meteo/translations/et.json create mode 100644 homeassistant/components/open_meteo/translations/hu.json create mode 100644 homeassistant/components/open_meteo/translations/id.json create mode 100644 homeassistant/components/open_meteo/translations/ja.json create mode 100644 homeassistant/components/open_meteo/translations/pl.json create mode 100644 homeassistant/components/open_meteo/translations/ru.json create mode 100644 homeassistant/components/open_meteo/translations/tr.json create mode 100644 homeassistant/components/open_meteo/translations/zh-Hant.json create mode 100644 homeassistant/components/vicare/translations/de.json create mode 100644 homeassistant/components/vicare/translations/et.json create mode 100644 homeassistant/components/vicare/translations/pl.json create mode 100644 homeassistant/components/vicare/translations/tr.json create mode 100644 homeassistant/components/vicare/translations/zh-Hant.json diff --git a/homeassistant/components/azure_event_hub/translations/ca.json b/homeassistant/components/azure_event_hub/translations/ca.json index 055811f53e4..b3afd0bdb92 100644 --- a/homeassistant/components/azure_event_hub/translations/ca.json +++ b/homeassistant/components/azure_event_hub/translations/ca.json @@ -2,24 +2,35 @@ "config": { "abort": { "already_configured": "El servei ja est\u00e0 configurat", - "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + "cannot_connect": "No s'ha pogut connectar amb les credencials de configuration.yaml, elimina el yaml i utilitza el flux de configuraci\u00f3.", + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3.", + "unknown": "S'ha produ\u00eft un error desconegut i no s'ha pogut connectar amb les credencials de configuration.yaml, elimina el yaml i utilitza el flux de configuraci\u00f3." }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "unknown": "Error inesperat" }, "step": { + "conn_string": { + "data": { + "event_hub_connection_string": "Cadena de connexi\u00f3 d'Event Hub" + }, + "description": "Introdueix la cadena de connexi\u00f3 de: {event_hub_instance_name}", + "title": "M\u00e8tode cadena de connexi\u00f3" + }, "sas": { "data": { "event_hub_namespace": "Event Hub Namespace", "event_hub_sas_key": "Clau SAS de l'Event Hub", "event_hub_sas_policy": "Pol\u00edtica SAS de l'Event Hub" }, + "description": "Introdueix les credencials SAS (signatura d'acc\u00e9s compartit) de: {event_hub_instance_name}", "title": "M\u00e8tode de credencials SAS" }, "user": { "data": { - "event_hub_instance_name": "Nom de la inst\u00e0ncia Event Hub" + "event_hub_instance_name": "Nom de la inst\u00e0ncia Event Hub", + "use_connection_string": "Utilitza una cadena de connexi\u00f3" }, "title": "Configuraci\u00f3 de la integraci\u00f3 Azure Event Hub" } @@ -28,6 +39,9 @@ "options": { "step": { "options": { + "data": { + "send_interval": "Interval entre enviaments de lots al Hub." + }, "title": "Opcions d'Azure Event Hub." } } diff --git a/homeassistant/components/azure_event_hub/translations/de.json b/homeassistant/components/azure_event_hub/translations/de.json new file mode 100644 index 00000000000..4b86c6f9021 --- /dev/null +++ b/homeassistant/components/azure_event_hub/translations/de.json @@ -0,0 +1,49 @@ +{ + "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert", + "cannot_connect": "Die Verbindung mit den Anmeldeinformationen aus der configuration.yaml ist fehlgeschlagen. Bitte entferne sie aus der yaml und verwende den Konfigurationsablauf.", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", + "unknown": "Die Verbindung mit den Anmeldeinformationen aus der configuration.yaml ist mit einem unbekannten Fehler fehlgeschlagen. Bitte entferne sie aus der yaml und verwende den Konfigurationsablauf." + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "conn_string": { + "data": { + "event_hub_connection_string": "Event Hub-Verbindungszeichenfolge" + }, + "description": "Bitte gib die Verbindungszeichenfolge ein f\u00fcr: {event_hub_instance_name}", + "title": "Verbindungszeichenfolgenmethode" + }, + "sas": { + "data": { + "event_hub_namespace": "Event Hub-Namespace", + "event_hub_sas_key": "Event Hub SAS-Schl\u00fcssel", + "event_hub_sas_policy": "Event Hub SAS-Richtlinie" + }, + "description": "Bitte gib die SAS-Anmeldeinformationen (Shared Access Signature) ein f\u00fcr: {event_hub_instance_name}", + "title": "SAS-Anmeldeinformationen-Methode" + }, + "user": { + "data": { + "event_hub_instance_name": "Name der Event Hub-Instanz", + "use_connection_string": "Verbindungszeichenfolge verwenden" + }, + "title": "Einrichten deiner Azure Event Hub-Integration" + } + } + }, + "options": { + "step": { + "options": { + "data": { + "send_interval": "Intervall zwischen dem Senden von Batches an den Hub." + }, + "title": "Optionen f\u00fcr den Azure Event Hub." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/azure_event_hub/translations/et.json b/homeassistant/components/azure_event_hub/translations/et.json new file mode 100644 index 00000000000..3137da288a6 --- /dev/null +++ b/homeassistant/components/azure_event_hub/translations/et.json @@ -0,0 +1,49 @@ +{ + "config": { + "abort": { + "already_configured": "Teenus on juba h\u00e4\u00e4lestatud", + "cannot_connect": "\u00dchenduse loomine faili configuration.yaml mandaadiandmetega eba\u00f5nnestus, eemalda yamlist ja kasuta konfiguratsioonivoogu.", + "single_instance_allowed": "Lubatud on ainult \u00fcks sidumine", + "unknown": "\u00dchenduse loomine faili configuration.yaml mandaadiandmetega nurjus tundmatu vea t\u00f5ttu. Eemalda yamlist ja kasuta konfiguratsioonivoogu." + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "conn_string": { + "data": { + "event_hub_connection_string": "Event Hubi \u00fchendusstring" + }, + "description": "Sisesta \u00fchenduse string: {event_hub_instance_name}", + "title": "\u00dchendusstringi meetod" + }, + "sas": { + "data": { + "event_hub_namespace": "Event Hubi nimeruum", + "event_hub_sas_key": "Event Hub SAS v\u00f5ti", + "event_hub_sas_policy": "Event Hub SAS-i eeskirjad" + }, + "description": "Sisesta SAS-i (jagatud juurdep\u00e4\u00e4su allkiri) mandaat: {event_hub_instance_name}", + "title": "SAS mandaatide meetod" + }, + "user": { + "data": { + "event_hub_instance_name": "Event Hubi \u00fcksuse nimi", + "use_connection_string": "Kasuta \u00fchendusstringi" + }, + "title": "Seadista oma Azure Event Hubi sidumine" + } + } + }, + "options": { + "step": { + "options": { + "data": { + "send_interval": "Intervall partiide jaoturisse saatmise vahel." + }, + "title": "Azure Event Hubi suvandid." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/azure_event_hub/translations/hu.json b/homeassistant/components/azure_event_hub/translations/hu.json new file mode 100644 index 00000000000..21b6a4ba63f --- /dev/null +++ b/homeassistant/components/azure_event_hub/translations/hu.json @@ -0,0 +1,49 @@ +{ + "config": { + "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", + "cannot_connect": "A csatlakoz\u00e1s a configuration.yaml-ben szerepl\u0151 hiteles\u00edt\u0151 adatokkal nem siker\u00fclt, k\u00e9rj\u00fck, t\u00e1vol\u00edtsa el ezeket, \u00e9s haszn\u00e1lja a kezel\u0151 fel\u00fcletet a konfigur\u00e1l\u00e1shoz.", + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", + "unknown": "A csatlakoz\u00e1s a configuration.yaml-ben szerepl\u0151 hiteles\u00edt\u0151 adatokkal nem siker\u00fclt, k\u00e9rj\u00fck, t\u00e1vol\u00edtsa el ezeket, \u00e9s haszn\u00e1lja a kezel\u0151 fel\u00fcletet a konfigur\u00e1l\u00e1shoz." + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "conn_string": { + "data": { + "event_hub_connection_string": "Event Hub csatlakoz\u00e1si karaktersor" + }, + "description": "K\u00e9rj\u00fck, adja meg a csatlakoz\u00e1si karaktersort ehhez: {event_hub_instance_name}", + "title": "Csatlakoz\u00e1si karaktersor t\u00edpus" + }, + "sas": { + "data": { + "event_hub_namespace": "Event Hub n\u00e9vt\u00e9r", + "event_hub_sas_key": "Event Hub SAS kulcs", + "event_hub_sas_policy": "Event Hub SAS h\u00e1zirend" + }, + "description": "K\u00e9rj\u00fck, adja meg a SAS hiteles\u00edt\u0151 adatait ehhez: {event_hub_instance_name}", + "title": "SAS hiteles\u00edt\u00e9s t\u00edpus" + }, + "user": { + "data": { + "event_hub_instance_name": "Event Hub p\u00e9ld\u00e1ny neve", + "use_connection_string": "Csatlakoz\u00e1si karaktersor haszn\u00e1lata" + }, + "title": "Azure Event Hub integr\u00e1ci\u00f3 be\u00e1ll\u00edt\u00e1sa" + } + } + }, + "options": { + "step": { + "options": { + "data": { + "send_interval": "A csom\u00f3pontnak (hubnak) k\u00fcld\u00f6tt t\u00e9telek k\u00f6z\u00f6tti id\u0151k\u00f6z." + }, + "title": "Azure Event Hub be\u00e1ll\u00edt\u00e1sai." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/azure_event_hub/translations/id.json b/homeassistant/components/azure_event_hub/translations/id.json new file mode 100644 index 00000000000..d762032b485 --- /dev/null +++ b/homeassistant/components/azure_event_hub/translations/id.json @@ -0,0 +1,49 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi", + "cannot_connect": "Menghubungkan dengan kredensial dari configuration.yaml gagal, hapus dari yaml dan gunakan proses konfigurasi.", + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.", + "unknown": "Menghubungkan dengan kredensial dari configuration.yaml gagal disertai kesalahan yang tidak dikenal, hapus dari yaml dan gunakan proses konfigurasi." + }, + "error": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "conn_string": { + "data": { + "event_hub_connection_string": "String Koneksi Event Hub" + }, + "description": "Masukkan string koneksi untuk: {event_hub_instance_name}", + "title": "Metode String Koneksi" + }, + "sas": { + "data": { + "event_hub_namespace": "Namespace Event Hub", + "event_hub_sas_key": "Kunci SAS Event Hub", + "event_hub_sas_policy": "Kebijakan SAS Event Hub" + }, + "description": "Masukkan kredensial SAS (shared access signature) untuk: {event_hub_instance_name}", + "title": "Metode Kredensial SAS" + }, + "user": { + "data": { + "event_hub_instance_name": "Nama Instans Event Hub", + "use_connection_string": "Gunakan String Koneksi" + }, + "title": "Siapkan integrasi Azure Event Hub Anda" + } + } + }, + "options": { + "step": { + "options": { + "data": { + "send_interval": "Interval antara pengiriman batch ke hub." + }, + "title": "Opsi untuk Azure Event Hub." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/azure_event_hub/translations/ja.json b/homeassistant/components/azure_event_hub/translations/ja.json new file mode 100644 index 00000000000..ef86e5fdc41 --- /dev/null +++ b/homeassistant/components/azure_event_hub/translations/ja.json @@ -0,0 +1,49 @@ +{ + "config": { + "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "cannot_connect": "configuration.yaml\u3092\u4f7f\u7528\u3057\u305f\u8a8d\u8a3c\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002yaml\u3092\u524a\u9664\u3057\u3066\u69cb\u6210\u30d5\u30ed\u30fc(config flow)\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "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", + "unknown": "configuration.yaml\u3092\u4f7f\u7528\u3057\u305f\u8a8d\u8a3c\u63a5\u7d9a\u304c\u4e0d\u660e\u306a\u30a8\u30e9\u30fc\u3067\u5931\u6557\u3057\u307e\u3057\u305f\u3002yaml\u3092\u524a\u9664\u3057\u3066\u69cb\u6210\u30d5\u30ed\u30fc(config flow)\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "conn_string": { + "data": { + "event_hub_connection_string": "\u30a4\u30d9\u30f3\u30c8\u30cf\u30d6\u306e\u63a5\u7d9a\u6587\u5b57\u5217" + }, + "description": "\u6b21\u306e\u63a5\u7d9a\u6587\u5b57\u5217\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044: {event_hub_instance_name}", + "title": "\u63a5\u7d9a\u6587\u5b57\u5217\u30e1\u30bd\u30c3\u30c9" + }, + "sas": { + "data": { + "event_hub_namespace": "\u30a4\u30d9\u30f3\u30c8\u30cf\u30d6\u306e\u540d\u524d\u7a7a\u9593", + "event_hub_sas_key": "\u30a4\u30d9\u30f3\u30c8\u30cf\u30d6SAS\u30ad\u30fc", + "event_hub_sas_policy": "\u30a4\u30d9\u30f3\u30c8\u30cf\u30d6SAS\u30dd\u30ea\u30b7\u30fc" + }, + "description": "\u6b21\u306eSAS(\u5171\u6709\u30a2\u30af\u30bb\u30b9\u7f72\u540d)\u8cc7\u683c\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044: {event_hub_instance_name}", + "title": "SAS\u306e\u8cc7\u683c\u60c5\u5831\u65b9\u5f0f" + }, + "user": { + "data": { + "event_hub_instance_name": "\u30a4\u30d9\u30f3\u30c8\u30cf\u30d6\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u540d", + "use_connection_string": "\u63a5\u7d9a\u6587\u5b57\u5217\u3092\u4f7f\u7528\u3059\u308b" + }, + "title": "Azure Event Hub\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + } + } + }, + "options": { + "step": { + "options": { + "data": { + "send_interval": "\u30d0\u30c3\u30c1\u3092\u30cf\u30d6\u306b\u9001\u4fe1\u3059\u308b\u9593\u9694\u3002" + }, + "title": "Azure Event Hub\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/azure_event_hub/translations/pl.json b/homeassistant/components/azure_event_hub/translations/pl.json new file mode 100644 index 00000000000..14a08f96184 --- /dev/null +++ b/homeassistant/components/azure_event_hub/translations/pl.json @@ -0,0 +1,49 @@ +{ + "config": { + "abort": { + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", + "cannot_connect": "\u0141\u0105czenie si\u0119 z danymi uwierzytelniaj\u0105cymi z pliku configuration.yaml nie powiod\u0142o si\u0119. Usu\u0144 wpis z pliku yaml i u\u017cyj tego konfiguratora.", + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja.", + "unknown": "\u0141\u0105czenie z danymi uwierzytelniaj\u0105cymi z pliku configuration.yaml nie powiod\u0142o si\u0119 z powodu nieznanego b\u0142\u0119du, usu\u0144 go z pliku yaml i u\u017cyj konfiguratora." + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "conn_string": { + "data": { + "event_hub_connection_string": "Parametry po\u0142\u0105czenia z Event Hub" + }, + "description": "Wprowad\u017a parametry po\u0142\u0105czenia dla: {event_hub_instance_name}", + "title": "Metoda parametru po\u0142\u0105czenia" + }, + "sas": { + "data": { + "event_hub_namespace": "Event Hub", + "event_hub_sas_key": "Klucz SAS do Event Hub", + "event_hub_sas_policy": "Zasady SAS dotycz\u0105ce Event Hub" + }, + "description": "Wprowad\u017a po\u015bwiadczenia SAS (sygnatura dost\u0119pu wsp\u00f3\u0142dzielonego) dla: {event_hub_instance_name}", + "title": "Metoda po\u015bwiadcze\u0144 SAS" + }, + "user": { + "data": { + "event_hub_instance_name": "Nazwa instancji centrum zdarze\u0144", + "use_connection_string": "U\u017cyj parametru po\u0142\u0105czenia" + }, + "title": "Konfiguracja integracji us\u0142ugi Azure Event Hub" + } + } + }, + "options": { + "step": { + "options": { + "data": { + "send_interval": "Odst\u0119p czasu pomi\u0119dzy wysy\u0142aniem partii do huba." + }, + "title": "Opcje dla Azure Event Hub." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/azure_event_hub/translations/ru.json b/homeassistant/components/azure_event_hub/translations/ru.json new file mode 100644 index 00000000000..df58f5e8c34 --- /dev/null +++ b/homeassistant/components/azure_event_hub/translations/ru.json @@ -0,0 +1,49 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u0441 \u0443\u0447\u0435\u0442\u043d\u044b\u043c\u0438 \u0434\u0430\u043d\u043d\u044b\u043c\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml, \u0443\u0434\u0430\u043b\u0438\u0442\u0435 \u0438\u0445 \u0438\u0437 yaml \u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043c\u0430\u0441\u0442\u0435\u0440 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438.", + "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.", + "unknown": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u0441 \u0443\u0447\u0435\u0442\u043d\u044b\u043c\u0438 \u0434\u0430\u043d\u043d\u044b\u043c\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml, \u0443\u0434\u0430\u043b\u0438\u0442\u0435 \u0438\u0445 \u0438\u0437 yaml \u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043c\u0430\u0441\u0442\u0435\u0440 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "conn_string": { + "data": { + "event_hub_connection_string": "\u0421\u0442\u0440\u043e\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0442\u043e\u0440\u0430 \u0441\u043e\u0431\u044b\u0442\u0438\u0439" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0441\u0442\u0440\u043e\u043a\u0443 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0434\u043b\u044f: {event_hub_instance_name}", + "title": "\u041c\u0435\u0442\u043e\u0434 \u0441\u0442\u0440\u043e\u043a\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f" + }, + "sas": { + "data": { + "event_hub_namespace": "\u041f\u0440\u043e\u0441\u0442\u0440\u0430\u043d\u0441\u0442\u0432\u043e \u0438\u043c\u0435\u043d \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0442\u043e\u0440\u0430 \u0441\u043e\u0431\u044b\u0442\u0438\u0439", + "event_hub_sas_key": "\u041a\u043b\u044e\u0447 SAS \u0434\u043b\u044f \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0442\u043e\u0440\u0430 \u0441\u043e\u0431\u044b\u0442\u0438\u0439", + "event_hub_sas_policy": "\u041f\u043e\u043b\u0438\u0442\u0438\u043a\u0430 SAS \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0442\u043e\u0440\u0430 \u0441\u043e\u0431\u044b\u0442\u0438\u0439" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 SAS (\u043e\u0431\u0449\u0430\u044f \u043f\u043e\u0434\u043f\u0438\u0441\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0430) \u0434\u043b\u044f: {event_hub_instance_name}", + "title": "\u041c\u0435\u0442\u043e\u0434 \u0443\u0447\u0435\u0442\u043d\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445 SAS" + }, + "user": { + "data": { + "event_hub_instance_name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0442\u043e\u0440\u0430 \u0441\u043e\u0431\u044b\u0442\u0438\u0439", + "use_connection_string": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0441\u0442\u0440\u043e\u043a\u0443 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f" + }, + "title": "Azure Event Hub" + } + } + }, + "options": { + "step": { + "options": { + "data": { + "send_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043c\u0435\u0436\u0434\u0443 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u043e\u0439 \u043f\u0430\u043a\u0435\u0442\u043e\u0432 \u043d\u0430 \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0442\u043e\u0440." + }, + "title": "Azure Event Hub" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/azure_event_hub/translations/tr.json b/homeassistant/components/azure_event_hub/translations/tr.json new file mode 100644 index 00000000000..a2905eae6e9 --- /dev/null +++ b/homeassistant/components/azure_event_hub/translations/tr.json @@ -0,0 +1,49 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "configuration.yaml'deki kimlik bilgileriyle ba\u011flant\u0131 kurulamad\u0131, l\u00fctfen yaml'den \u00e7\u0131kar\u0131n ve yap\u0131land\u0131rma ak\u0131\u015f\u0131n\u0131 kullan\u0131n.", + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", + "unknown": "configuration.yaml'deki kimlik bilgileriyle ba\u011flant\u0131, bilinmeyen bir hatayla ba\u015far\u0131s\u0131z oldu, l\u00fctfen yaml'den kald\u0131r\u0131n ve yap\u0131land\u0131rma ak\u0131\u015f\u0131n\u0131 kullan\u0131n." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "conn_string": { + "data": { + "event_hub_connection_string": "Event Hub Ba\u011flant\u0131 Dizesi" + }, + "description": "L\u00fctfen \u015fu ba\u011flant\u0131 dizesini girin: {event_hub_instance_name}", + "title": "Ba\u011flant\u0131 Dizisi y\u00f6ntemi" + }, + "sas": { + "data": { + "event_hub_namespace": "Event Hub Ad Alan\u0131", + "event_hub_sas_key": "Event Hub SAS Anahtar\u0131", + "event_hub_sas_policy": "Event Hub SAS \u0130lkesi" + }, + "description": "{event_hub_instance_name} i\u00e7in SAS (payla\u015f\u0131lan eri\u015fim anahtar\u0131) kimlik bilgilerini girin", + "title": "SAS Kimlik Bilgileri y\u00f6ntemi" + }, + "user": { + "data": { + "event_hub_instance_name": "Event Hub \u00d6rnek Ad\u0131", + "use_connection_string": "Ba\u011flant\u0131 Dizesini Kullan" + }, + "title": "Azure Event Hub entegrasyonu kurun" + } + } + }, + "options": { + "step": { + "options": { + "data": { + "send_interval": "Hub'a toplu g\u00f6nderme aras\u0131ndaki aral\u0131k." + }, + "title": "Azure Event Hub i\u00e7in se\u00e7enekler." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/azure_event_hub/translations/zh-Hant.json b/homeassistant/components/azure_event_hub/translations/zh-Hant.json new file mode 100644 index 00000000000..fbc84038703 --- /dev/null +++ b/homeassistant/components/azure_event_hub/translations/zh-Hant.json @@ -0,0 +1,49 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_connect": "\u4f7f\u7528 configuration.yaml \u6240\u5305\u542b\u6191\u8b49\u9023\u7dda\u5931\u6557\uff0c\u8acb\u81ea Yaml \u79fb\u9664\u8a72\u6191\u8b49\u4e26\u4f7f\u7528\u8a2d\u5b9a\u6d41\u7a0b\u65b9\u5f0f\u3002", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", + "unknown": "\u4f7f\u7528 configuration.yaml \u6240\u5305\u542b\u6191\u8b49\u9023\u7dda\u767c\u751f\u672a\u77e5\u932f\u8aa4\uff0c\u8acb\u81ea Yaml \u79fb\u9664\u8a72\u6191\u8b49\u4e26\u4f7f\u7528\u8a2d\u5b9a\u6d41\u7a0b\u65b9\u5f0f\u3002" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "conn_string": { + "data": { + "event_hub_connection_string": "\u4e8b\u4ef6\u4e2d\u6a1e\u9023\u7dda\u5b57\u4e32" + }, + "description": "\u8acb\u8f38\u5165\u9023\u7dda\u5b57\u4e32\uff1a{event_hub_instance_name}", + "title": "\u9023\u63a5\u5b57\u4e32\u6a21\u5f0f" + }, + "sas": { + "data": { + "event_hub_namespace": "\u4e8b\u4ef6\u4e2d\u6a1e Namespace", + "event_hub_sas_key": "\u4e8b\u4ef6\u4e2d\u6a1e SAS \u5bc6\u9470", + "event_hub_sas_policy": "\u4e8b\u4ef6\u4e2d\u6a1e SAS \u96b1\u79c1\u653f\u7b56" + }, + "description": "\u8acb\u8f38\u5165 SAS \uff08\u5171\u7528\u5b58\u53d6\u7c3d\u7ae0\uff09\u6191\u8b49\uff1a{event_hub_instance_name}", + "title": "SAS \u6191\u8b49\u6a21\u5f0f" + }, + "user": { + "data": { + "event_hub_instance_name": "\u4e8b\u4ef6\u4e2d\u6a1e\u5be6\u4f8b\u540d\u7a31", + "use_connection_string": "\u4f7f\u7528\u9023\u63a5\u5b57\u4e32" + }, + "title": "\u8a2d\u5b9a Azure \u4e8b\u4ef6\u4e2d\u6a1e\u6574\u5408" + } + } + }, + "options": { + "step": { + "options": { + "data": { + "send_interval": "\u4e2d\u6a1e\u50b3\u9001\u6279\u6b21\u9593\u9694\u6642\u9593" + }, + "title": "Azure \u4e8b\u4ef6\u4e2d\u6a1e\u9078\u9805\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/ja.json b/homeassistant/components/binary_sensor/translations/ja.json index 1a4b14b2c68..c7a20493152 100644 --- a/homeassistant/components/binary_sensor/translations/ja.json +++ b/homeassistant/components/binary_sensor/translations/ja.json @@ -138,11 +138,11 @@ "on": "\u63a5\u7d9a\u6e08" }, "door": { - "off": "\u30af\u30ed\u30fc\u30ba\u30c9", + "off": "\u9589\u9396", "on": "\u30aa\u30fc\u30d7\u30f3" }, "garage_door": { - "off": "\u30af\u30ed\u30fc\u30ba\u30c9", + "off": "\u9589\u9396", "on": "\u30aa\u30fc\u30d7\u30f3" }, "gas": { @@ -178,7 +178,7 @@ "on": "\u691c\u51fa" }, "opening": { - "off": "\u30af\u30ed\u30fc\u30ba\u30c9", + "off": "\u9589\u9396", "on": "\u30aa\u30fc\u30d7\u30f3" }, "plug": { @@ -218,7 +218,7 @@ "on": "\u691c\u51fa" }, "window": { - "off": "\u30af\u30ed\u30fc\u30ba\u30c9", + "off": "\u9589\u9396", "on": "\u30aa\u30fc\u30d7\u30f3" } }, diff --git a/homeassistant/components/braviatv/translations/ja.json b/homeassistant/components/braviatv/translations/ja.json index 835e67e7b9f..87f903f0640 100644 --- a/homeassistant/components/braviatv/translations/ja.json +++ b/homeassistant/components/braviatv/translations/ja.json @@ -14,15 +14,15 @@ "data": { "pin": "PIN\u30b3\u30fc\u30c9" }, - "description": "\u30bd\u30cb\u30fc\u88fd\u306e\u30c6\u30ec\u30d3 \u30d6\u30e9\u30d3\u30a2\u306b\u8868\u793a\u3055\u308c\u3066\u3044\u308bPIN\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u307e\u3059\u3002 \n\nPIN\u30b3\u30fc\u30c9\u304c\u8868\u793a\u3055\u308c\u3066\u3044\u306a\u3044\u5834\u5408\u306f\u3001\u30c6\u30ec\u30d3\u304b\u3089Home Assistant\u306e\u767b\u9332\u3092\u89e3\u9664\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u306e\u3067\u3001\u6b21\u306e\u624b\u9806\u3067\u884c\u3063\u3066\u304f\u3060\u3055\u3044\u3002\u8a2d\u5b9a \u2192 \u30cd\u30c3\u30c8\u30ef\u30fc\u30af \u2192 \u30ea\u30e2\u30fc\u30c8\u30c7\u30d0\u30a4\u30b9\u306e\u8a2d\u5b9a \u2192 \u30ea\u30e2\u30fc\u30c8\u30c7\u30d0\u30a4\u30b9\u306e\u767b\u9332\u89e3\u9664 \u3092\u884c\u3063\u3066\u304f\u3060\u3055\u3044\u3002", - "title": "Sony Bravia TV\u3092\u8a8d\u8a3c\u3059\u308b" + "description": "\u30bd\u30cb\u30fc Bravia TV\u306b\u8868\u793a\u3055\u308c\u3066\u3044\u308bPIN\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u307e\u3059\u3002 \n\nPIN\u30b3\u30fc\u30c9\u304c\u8868\u793a\u3055\u308c\u3066\u3044\u306a\u3044\u5834\u5408\u306f\u3001\u30c6\u30ec\u30d3\u304b\u3089Home Assistant\u306e\u767b\u9332\u3092\u89e3\u9664\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u306e\u3067\u3001\u6b21\u306e\u624b\u9806\u3067\u884c\u3063\u3066\u304f\u3060\u3055\u3044\u3002\u8a2d\u5b9a \u2192 \u30cd\u30c3\u30c8\u30ef\u30fc\u30af \u2192 \u30ea\u30e2\u30fc\u30c8\u30c7\u30d0\u30a4\u30b9\u306e\u8a2d\u5b9a \u2192 \u30ea\u30e2\u30fc\u30c8\u30c7\u30d0\u30a4\u30b9\u306e\u767b\u9332\u89e3\u9664 \u3092\u884c\u3063\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "\u30bd\u30cb\u30fc Bravia TV\u3092\u8a8d\u8a3c\u3059\u308b" }, "user": { "data": { "host": "\u30db\u30b9\u30c8" }, - "description": "Sony Bravia TV\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u306b\u95a2\u3059\u308b\u554f\u984c\u304c\u767a\u751f\u3057\u305f\u5834\u5408\u306f\u3001\u6b21\u306e https://www.home-assistant.io/integrations/braviatv \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\n\n\u304d\u3061\u3093\u3068\u30c6\u30ec\u30d3\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u308b\u3053\u3068\u3082\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "title": "Sony Bravia\u30c6\u30ec\u30d3" + "description": "\u30bd\u30cb\u30fc Bravia TV\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u306b\u95a2\u3057\u3066\u554f\u984c\u304c\u767a\u751f\u3057\u305f\u5834\u5408\u306f\u3001https://www.home-assistant.io/integrations/braviatv \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\n\n\u304d\u3061\u3093\u3068\u30c6\u30ec\u30d3\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u308b\u3053\u3068\u3082\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "\u30bd\u30cb\u30fc Bravia TV" } } }, @@ -32,7 +32,7 @@ "data": { "ignored_sources": "\u7121\u8996\u3055\u308c\u305f\u30bd\u30fc\u30b9\u306e\u30ea\u30b9\u30c8" }, - "title": "Sony Bravia \u30c6\u30ec\u30d3\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" + "title": "\u30bd\u30cb\u30fc Bravia TV\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" } } } diff --git a/homeassistant/components/brother/translations/ja.json b/homeassistant/components/brother/translations/ja.json index c77ad218ea9..6c0e1e57767 100644 --- a/homeassistant/components/brother/translations/ja.json +++ b/homeassistant/components/brother/translations/ja.json @@ -16,7 +16,7 @@ "host": "\u30db\u30b9\u30c8", "type": "\u30d7\u30ea\u30f3\u30bf\u30fc\u306e\u7a2e\u985e" }, - "description": "\u30d6\u30e9\u30b6\u30fc\u793e\u88fd\u30d7\u30ea\u30f3\u30bf\u30fc\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u306b\u554f\u984c\u304c\u3042\u308b\u5834\u5408\u306f\u3001https://www.home-assistant.io/integrations/brother \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" + "description": "\u30d6\u30e9\u30b6\u30fc\u793e\u88fd\u30d7\u30ea\u30f3\u30bf\u30fc\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u306b\u95a2\u3057\u3066\u554f\u984c\u304c\u767a\u751f\u3057\u305f\u5834\u5408\u306f\u3001https://www.home-assistant.io/integrations/brother \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" }, "zeroconf_confirm": { "data": { diff --git a/homeassistant/components/cover/translations/ja.json b/homeassistant/components/cover/translations/ja.json index 59967ebe6f3..0542852a2d3 100644 --- a/homeassistant/components/cover/translations/ja.json +++ b/homeassistant/components/cover/translations/ja.json @@ -28,7 +28,7 @@ }, "state": { "_": { - "closed": "\u30af\u30ed\u30fc\u30ba\u30c9", + "closed": "\u9589\u9396", "closing": "\u9589\u3058\u3066\u3044\u307e\u3059", "open": "\u30aa\u30fc\u30d7\u30f3", "opening": "\u30aa\u30fc\u30d7\u30cb\u30f3\u30b0", diff --git a/homeassistant/components/dunehd/translations/ja.json b/homeassistant/components/dunehd/translations/ja.json index 2bf0ff94c48..79bb6b0746d 100644 --- a/homeassistant/components/dunehd/translations/ja.json +++ b/homeassistant/components/dunehd/translations/ja.json @@ -13,7 +13,7 @@ "data": { "host": "\u30db\u30b9\u30c8" }, - "description": "Dune HD\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u306b\u554f\u984c\u304c\u3042\u308b\u5834\u5408\u306f\u3001https://www.home-assistant.io/integrations/dunehd \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\n\n\u304d\u3061\u3093\u3068\u30d7\u30ec\u30fc\u30e4\u30fc\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u308b\u3053\u3068\u3082\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "description": "Dune HD\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u306b\u95a2\u3057\u3066\u554f\u984c\u304c\u767a\u751f\u3057\u305f\u5834\u5408\u306f\u3001https://www.home-assistant.io/integrations/dunehd \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\n\n\u304d\u3061\u3093\u3068\u30d7\u30ec\u30fc\u30e4\u30fc\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u308b\u3053\u3068\u3082\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "Dune HD" } } diff --git a/homeassistant/components/group/translations/ja.json b/homeassistant/components/group/translations/ja.json index 1faf42c68d4..02aff2e2b84 100644 --- a/homeassistant/components/group/translations/ja.json +++ b/homeassistant/components/group/translations/ja.json @@ -1,7 +1,7 @@ { "state": { "_": { - "closed": "\u30af\u30ed\u30fc\u30ba\u30c9", + "closed": "\u9589\u9396", "home": "\u5728\u5b85", "locked": "\u30ed\u30c3\u30af\u3055\u308c\u307e\u3057\u305f", "not_home": "\u96e2\u5e2d(away)", diff --git a/homeassistant/components/homekit/translations/ja.json b/homeassistant/components/homekit/translations/ja.json index 06a7d6bc1d3..769a3d0f1c7 100644 --- a/homeassistant/components/homekit/translations/ja.json +++ b/homeassistant/components/homekit/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "port_name_in_use": "\u540c\u3058\u540d\u524d\u3084\u30dd\u30fc\u30c8\u3092\u6301\u3064\u30a2\u30af\u30bb\u30b5\u30ea\u30fc\u3084\u30d6\u30ea\u30c3\u30b8\u304c\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002" + "port_name_in_use": "\u540c\u3058\u540d\u524d\u3084\u30dd\u30fc\u30c8\u3092\u6301\u3064\u30a2\u30af\u30bb\u30b5\u30ea\u3084\u30d6\u30ea\u30c3\u30b8\u304c\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002" }, "step": { "pairing": { @@ -12,7 +12,7 @@ "data": { "include_domains": "\u542b\u3081\u308b\u30c9\u30e1\u30a4\u30f3" }, - "description": "\u542b\u3081\u308b\u30c9\u30e1\u30a4\u30f3\u3092\u9078\u629e\u3057\u307e\u3059\u3002\u30c9\u30e1\u30a4\u30f3\u5185\u3067\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u308b\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u304c\u542b\u307e\u308c\u307e\u3059\u3002\u30a2\u30af\u30bb\u30b5\u30ea\u30fc \u30e2\u30fc\u30c9\u306e\u5225\u306e \u3001HomeKit\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306f\u3001\u5404 \u30c6\u30ec\u30d3\u30e1\u30c7\u30a3\u30a2 \u30d7\u30ec\u30fc\u30e4\u30fc\u3001\u30a2\u30af\u30c6\u30a3\u30d3\u30c6\u30a3 \u30d9\u30fc\u30b9\u306e\u30ea\u30e2\u30fc\u30c8\u3001\u30ed\u30c3\u30af\u3001\u304a\u3088\u3073\u30ab\u30e1\u30e9\u306b\u5bfe\u3057\u3066\u4f5c\u6210\u3055\u308c\u307e\u3059\u3002", + "description": "\u542b\u3081\u308b\u30c9\u30e1\u30a4\u30f3\u3092\u9078\u629e\u3057\u307e\u3059\u3002\u30c9\u30e1\u30a4\u30f3\u5185\u3067\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u308b\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u304c\u542b\u307e\u308c\u307e\u3059\u3002\u30a2\u30af\u30bb\u30b5\u30ea\u30e2\u30fc\u30c9\u306e\u5225\u306e \u3001HomeKit\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306f\u3001\u5404 \u30c6\u30ec\u30d3\u30e1\u30c7\u30a3\u30a2 \u30d7\u30ec\u30fc\u30e4\u30fc\u3001\u30a2\u30af\u30c6\u30a3\u30d3\u30c6\u30a3 \u30d9\u30fc\u30b9\u306e\u30ea\u30e2\u30fc\u30c8\u3001\u30ed\u30c3\u30af\u3001\u304a\u3088\u3073\u30ab\u30e1\u30e9\u306b\u5bfe\u3057\u3066\u4f5c\u6210\u3055\u308c\u307e\u3059\u3002", "title": "\u542b\u3081\u308b\u30c9\u30e1\u30a4\u30f3\u306e\u9078\u629e" } } @@ -40,7 +40,7 @@ "entities": "\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3", "mode": "\u30e2\u30fc\u30c9" }, - "description": "\u542b\u307e\u308c\u308b\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u9078\u629e\u3057\u307e\u3059\u3002\u30a2\u30af\u30bb\u30b5\u30ea\u30fc\u30e2\u30fc\u30c9\u3067\u306f\u30011\u3064\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u306e\u307f\u304c\u542b\u307e\u308c\u307e\u3059\u3002\u30d6\u30ea\u30c3\u30b8\u30a4\u30f3\u30af\u30eb\u30fc\u30c9\u30e2\u30fc\u30c9\u3067\u306f\u3001\u7279\u5b9a\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u304c\u9078\u629e\u3055\u308c\u3066\u3044\u306a\u3044\u9650\u308a\u3001\u30c9\u30e1\u30a4\u30f3\u5185\u306e\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u304c\u542b\u307e\u308c\u307e\u3059\u3002\u30d6\u30ea\u30c3\u30b8\u9664\u5916\u30e2\u30fc\u30c9\u3067\u306f\u3001\u9664\u5916\u3055\u308c\u305f\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u9664\u3044\u3066\u3001\u30c9\u30e1\u30a4\u30f3\u5185\u306e\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u304c\u542b\u307e\u308c\u307e\u3059\u3002\u6700\u9ad8\u306e\u30d1\u30d5\u30a9\u30fc\u30de\u30f3\u30b9\u3092\u5b9f\u73fe\u3059\u308b\u305f\u3081\u306b\u3001\u30c6\u30ec\u30d3\u30e1\u30c7\u30a3\u30a2\u30d7\u30ec\u30fc\u30e4\u30fc\u3001\u30a2\u30af\u30c6\u30a3\u30d3\u30c6\u30a3\u30d9\u30fc\u30b9\u306e\u30ea\u30e2\u30b3\u30f3(remote)\u3001\u30ed\u30c3\u30af\u3001\u30ab\u30e1\u30e9\u306b\u5bfe\u3057\u3066\u500b\u5225\u306b\u3001HomeKit\u30a2\u30af\u30bb\u30b5\u30ea\u30fc\u3092\u4f5c\u6210\u3057\u307e\u3059\u3002", + "description": "\u542b\u307e\u308c\u308b\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u9078\u629e\u3057\u307e\u3059\u3002\u30a2\u30af\u30bb\u30b5\u30ea \u30e2\u30fc\u30c9\u3067\u306f\u30011\u3064\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u306e\u307f\u304c\u542b\u307e\u308c\u307e\u3059\u3002\u30d6\u30ea\u30c3\u30b8\u30a4\u30f3\u30af\u30eb\u30fc\u30c9\u30e2\u30fc\u30c9\u3067\u306f\u3001\u7279\u5b9a\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u304c\u9078\u629e\u3055\u308c\u3066\u3044\u306a\u3044\u9650\u308a\u3001\u30c9\u30e1\u30a4\u30f3\u5185\u306e\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u304c\u542b\u307e\u308c\u307e\u3059\u3002\u30d6\u30ea\u30c3\u30b8\u9664\u5916\u30e2\u30fc\u30c9\u3067\u306f\u3001\u9664\u5916\u3055\u308c\u305f\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u9664\u3044\u3066\u3001\u30c9\u30e1\u30a4\u30f3\u5185\u306e\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u304c\u542b\u307e\u308c\u307e\u3059\u3002\u6700\u9ad8\u306e\u30d1\u30d5\u30a9\u30fc\u30de\u30f3\u30b9\u3092\u5b9f\u73fe\u3059\u308b\u305f\u3081\u306b\u3001\u30c6\u30ec\u30d3\u30e1\u30c7\u30a3\u30a2\u30d7\u30ec\u30fc\u30e4\u30fc\u3001\u30a2\u30af\u30c6\u30a3\u30d3\u30c6\u30a3\u30d9\u30fc\u30b9\u306e\u30ea\u30e2\u30b3\u30f3(remote)\u3001\u30ed\u30c3\u30af\u3001\u30ab\u30e1\u30e9\u306b\u5bfe\u3057\u3066\u500b\u5225\u306b\u3001HomeKit\u30a2\u30af\u30bb\u30b5\u30ea\u3092\u4f5c\u6210\u3057\u307e\u3059\u3002", "title": "\u542b\u3081\u308b\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u9078\u629e" }, "init": { @@ -48,7 +48,7 @@ "include_domains": "\u542b\u3081\u308b\u30c9\u30e1\u30a4\u30f3", "mode": "\u30e2\u30fc\u30c9" }, - "description": "HomeKit \u306f\u3001\u30d6\u30ea\u30c3\u30b8\u307e\u305f\u306f\u5358\u4e00\u306e\u30a2\u30af\u30bb\u30b5\u30ea\u3092\u516c\u958b\u3059\u308b\u3088\u3046\u306b\u69cb\u6210\u3067\u304d\u307e\u3059\u3002\u30a2\u30af\u30bb\u30b5\u30ea\u30fc\u30e2\u30fc\u30c9\u3067\u306f\u30011\u3064\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u306e\u307f\u304c\u4f7f\u7528\u3067\u304d\u307e\u3059\u3002TV\u30c7\u30d0\u30a4\u30b9\u30af\u30e9\u30b9\u3092\u6301\u3064\u30e1\u30c7\u30a3\u30a2\u30d7\u30ec\u30fc\u30e4\u30fc\u304c\u6b63\u5e38\u306b\u6a5f\u80fd\u3059\u308b\u305f\u3081\u306b\u306f\u3001\u30a2\u30af\u30bb\u30b5\u30ea\u30fc\u30e2\u30fc\u30c9\u304c\u5fc5\u8981\u3067\u3059\u3002\"\u542b\u3081\u308b\u30c9\u30e1\u30a4\u30f3(Domains to include)\"\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u306f\u3001 HomeKit \u306b\u542b\u307e\u308c\u307e\u3059\u3002\u6b21\u306e\u753b\u9762\u3067\u3053\u306e\u30ea\u30b9\u30c8\u306b\u542b\u3081\u308b\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3001\u307e\u305f\u306f\u9664\u5916\u3059\u308b\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u9078\u629e\u3067\u304d\u307e\u3059\u3002", + "description": "HomeKit \u306f\u3001\u30d6\u30ea\u30c3\u30b8\u307e\u305f\u306f\u5358\u4e00\u306e\u30a2\u30af\u30bb\u30b5\u30ea\u3092\u516c\u958b\u3059\u308b\u3088\u3046\u306b\u69cb\u6210\u3067\u304d\u307e\u3059\u3002\u30a2\u30af\u30bb\u30b5\u30ea\u30e2\u30fc\u30c9\u3067\u306f\u30011\u3064\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u306e\u307f\u304c\u4f7f\u7528\u3067\u304d\u307e\u3059\u3002TV\u30c7\u30d0\u30a4\u30b9\u30af\u30e9\u30b9\u3092\u6301\u3064\u30e1\u30c7\u30a3\u30a2\u30d7\u30ec\u30fc\u30e4\u30fc\u304c\u6b63\u5e38\u306b\u6a5f\u80fd\u3059\u308b\u305f\u3081\u306b\u306f\u3001\u30a2\u30af\u30bb\u30b5\u30ea\u30e2\u30fc\u30c9\u304c\u5fc5\u8981\u3067\u3059\u3002\"\u542b\u3081\u308b\u30c9\u30e1\u30a4\u30f3(Domains to include)\"\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u306f\u3001 HomeKit \u306b\u542b\u307e\u308c\u307e\u3059\u3002\u6b21\u306e\u753b\u9762\u3067\u3053\u306e\u30ea\u30b9\u30c8\u306b\u542b\u3081\u308b\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3001\u307e\u305f\u306f\u9664\u5916\u3059\u308b\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u9078\u629e\u3067\u304d\u307e\u3059\u3002", "title": "\u542b\u3081\u308b\u30c9\u30e1\u30a4\u30f3\u3092\u9078\u629e\u3057\u307e\u3059\u3002" }, "yaml": { diff --git a/homeassistant/components/homekit_controller/translations/ja.json b/homeassistant/components/homekit_controller/translations/ja.json index d02809b291a..cddf95badf5 100644 --- a/homeassistant/components/homekit_controller/translations/ja.json +++ b/homeassistant/components/homekit_controller/translations/ja.json @@ -8,11 +8,11 @@ "ignored_model": "\u3053\u306e\u30e2\u30c7\u30eb\u306eHomeKit\u3067\u306e\u5bfe\u5fdc\u306f\u3001\u3088\u308a\u5b8c\u5168\u3067\u30cd\u30a4\u30c6\u30a3\u30d6\u306a\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u4f7f\u7528\u53ef\u80fd\u306a\u305f\u3081\u3001\u30d6\u30ed\u30c3\u30af\u3055\u308c\u3066\u3044\u307e\u3059\u3002", "invalid_config_entry": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306f\u30da\u30a2\u30ea\u30f3\u30b0\u306e\u6e96\u5099\u304c\u3067\u304d\u3066\u3044\u308b\u3068\u8868\u793a\u3055\u308c\u3066\u3044\u307e\u3059\u304c\u3001Home Assistant\u306b\u306f\u3059\u3067\u306b\u7af6\u5408\u3059\u308b\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u304c\u3042\u308b\u305f\u3081\u3001\u5148\u306b\u3053\u308c\u3092\u524a\u9664\u3057\u3066\u304a\u304f\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", "invalid_properties": "\u7121\u52b9\u306a\u30d7\u30ed\u30d1\u30c6\u30a3\u304c\u30c7\u30d0\u30a4\u30b9\u306b\u3088\u3063\u3066\u77e5\u3089\u3055\u308c\u307e\u3057\u305f\u3002", - "no_devices": "\u30da\u30a2\u30ea\u30f3\u30b0\u3055\u308c\u3066\u3044\u306a\u3044\u30c7\u30d0\u30a4\u30b9\u306f\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f" + "no_devices": "\u30da\u30a2\u30ea\u30f3\u30b0\u3055\u308c\u3066\u3044\u306a\u3044\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f" }, "error": { "authentication_error": "HomeKit\u30b3\u30fc\u30c9\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093\u3002\u78ba\u8a8d\u3057\u3066\u3001\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "insecure_setup_code": "\u8981\u6c42\u3055\u308c\u305f\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u30b3\u30fc\u30c9\u306f\u3001\u5358\u7d14\u3059\u304e\u308b\u6027\u8cea\u306a\u305f\u3081\u5b89\u5168\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a2\u30af\u30bb\u30b5\u30ea\u306f\u3001\u57fa\u672c\u7684\u306a\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u8981\u4ef6\u3092\u6e80\u305f\u3057\u3066\u3044\u307e\u305b\u3093\u3002", + "insecure_setup_code": "\u8981\u6c42\u3055\u308c\u305f\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u30b3\u30fc\u30c9\u306f\u3001\u5358\u7d14\u3059\u304e\u308b\u306e\u3067\u5b89\u5168\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a2\u30af\u30bb\u30b5\u30ea\u306f\u3001\u57fa\u672c\u7684\u306a\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u8981\u4ef6\u3092\u6e80\u305f\u3057\u3066\u3044\u307e\u305b\u3093\u3002", "max_peers_error": "\u30c7\u30d0\u30a4\u30b9\u306b\u306f\u7121\u6599\u306e\u30da\u30a2\u30ea\u30f3\u30b0\u30b9\u30c8\u30ec\u30fc\u30b8\u304c\u306a\u3044\u305f\u3081\u3001\u30da\u30a2\u30ea\u30f3\u30b0\u306e\u8ffd\u52a0\u3092\u62d2\u5426\u3057\u307e\u3057\u305f\u3002", "pairing_failed": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u3068\u306e\u30da\u30a2\u30ea\u30f3\u30b0\u4e2d\u306b\u3001\u672a\u51e6\u7406\u306e\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\u3053\u308c\u306f\u4e00\u6642\u7684\u306a\u969c\u5bb3\u304b\u3001\u30c7\u30d0\u30a4\u30b9\u304c\u73fe\u5728\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u306a\u3044\u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059\u3002", "unable_to_pair": "\u30da\u30a2\u30ea\u30f3\u30b0\u3067\u304d\u307e\u305b\u3093\u3002\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002", @@ -33,12 +33,12 @@ "allow_insecure_setup_codes": "\u5b89\u5168\u3067\u306a\u3044\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u30b3\u30fc\u30c9\u3068\u306e\u30da\u30a2\u30ea\u30f3\u30b0\u3092\u8a31\u53ef\u3059\u308b\u3002", "pairing_code": "\u30da\u30a2\u30ea\u30f3\u30b0\u30b3\u30fc\u30c9" }, - "description": "HomeKit\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u306f\u3001\u5225\u306eHomeKit\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u3084iCloud\u3092\u4f7f\u7528\u305b\u305a\u306b\u3001\u30bb\u30ad\u30e5\u30a2\u306a\u6697\u53f7\u5316\u63a5\u7d9a\u3092\u4f7f\u7528\u3057\u3066\u30ed\u30fc\u30ab\u30eb\u30a8\u30ea\u30a2\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u3067 {name} \u3068\u901a\u4fe1\u3057\u307e\u3059\u3002\u3053\u306e\u30a2\u30af\u30bb\u30b5\u30ea\u30fc\u3092\u4f7f\u7528\u3059\u308b\u306b\u306f\u3001HomeKit \u306e\u30da\u30a2\u30ea\u30f3\u30b0\u30b3\u30fc\u30c9(XXX-XX-XXX \u306e\u5f62\u5f0f)\u5165\u529b\u3057\u307e\u3059\u3002\u3053\u306e\u30b3\u30fc\u30c9\u306f\u901a\u5e38\u3001\u30c7\u30d0\u30a4\u30b9\u672c\u4f53\u307e\u305f\u306f\u30d1\u30c3\u30b1\u30fc\u30b8\u306b\u8a18\u8f09\u3055\u308c\u3066\u3044\u307e\u3059\u3002", + "description": "HomeKit\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u306f\u3001\u5225\u306eHomeKit\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u3084iCloud\u3092\u4f7f\u7528\u305b\u305a\u306b\u3001\u30bb\u30ad\u30e5\u30a2\u306a\u6697\u53f7\u5316\u63a5\u7d9a\u3092\u4f7f\u7528\u3057\u3066\u30ed\u30fc\u30ab\u30eb\u30a8\u30ea\u30a2\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u3067 {name} \u3068\u901a\u4fe1\u3057\u307e\u3059\u3002\u3053\u306e\u30a2\u30af\u30bb\u30b5\u30ea\u3092\u4f7f\u7528\u3059\u308b\u306b\u306f\u3001HomeKit \u306e\u30da\u30a2\u30ea\u30f3\u30b0\u30b3\u30fc\u30c9(XXX-XX-XXX \u306e\u5f62\u5f0f)\u5165\u529b\u3057\u307e\u3059\u3002\u3053\u306e\u30b3\u30fc\u30c9\u306f\u901a\u5e38\u3001\u30c7\u30d0\u30a4\u30b9\u672c\u4f53\u307e\u305f\u306f\u30d1\u30c3\u30b1\u30fc\u30b8\u306b\u8a18\u8f09\u3055\u308c\u3066\u3044\u307e\u3059\u3002", "title": "HomeKit Accessory Protocol\u3092\u4ecb\u3057\u3066\u30c7\u30d0\u30a4\u30b9\u3068\u30da\u30a2\u30ea\u30f3\u30b0" }, "protocol_error": { "description": "\u30c7\u30d0\u30a4\u30b9\u304c\u30da\u30a2\u30ea\u30f3\u30b0\u30e2\u30fc\u30c9\u306b\u306a\u3063\u3066\u3044\u306a\u3044\u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059\u306e\u3067\u3001\u7269\u7406\u307e\u305f\u306f\u4eee\u60f3\u7684\u306a\u30dc\u30bf\u30f3\u3092\u62bc\u3059\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u30c7\u30d0\u30a4\u30b9\u304c\u30da\u30a2\u30ea\u30f3\u30b0\u30e2\u30fc\u30c9\u306b\u306a\u3063\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3059\u308b\u304b\u3001\u30c7\u30d0\u30a4\u30b9\u3092\u518d\u8d77\u52d5\u3057\u3066\u304b\u3089\u3001\u30da\u30a2\u30ea\u30f3\u30b0\u3092\u518d\u958b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "title": "\u30a2\u30af\u30bb\u30b5\u30ea\u30fc\u3068\u306e\u901a\u4fe1\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f" + "title": "\u30a2\u30af\u30bb\u30b5\u30ea\u3068\u306e\u901a\u4fe1\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f" }, "user": { "data": { diff --git a/homeassistant/components/knx/translations/tr.json b/homeassistant/components/knx/translations/tr.json index 89165632163..9e0e00de8be 100644 --- a/homeassistant/components/knx/translations/tr.json +++ b/homeassistant/components/knx/translations/tr.json @@ -21,6 +21,7 @@ "routing": { "data": { "individual_address": "Y\u00f6nlendirme ba\u011flant\u0131s\u0131 i\u00e7in bireysel adres", + "local_ip": "Yerel IP (emin de\u011filseniz bo\u015f b\u0131rak\u0131n)", "multicast_group": "Y\u00f6nlendirme i\u00e7in kullan\u0131lan \u00e7ok noktaya yay\u0131n grubu", "multicast_port": "Y\u00f6nlendirme i\u00e7in kullan\u0131lan \u00e7ok noktaya yay\u0131n ba\u011flant\u0131 noktas\u0131" }, @@ -46,6 +47,7 @@ "data": { "connection_type": "KNX Ba\u011flant\u0131 T\u00fcr\u00fc", "individual_address": "Varsay\u0131lan bireysel adres", + "local_ip": "Yerel IP (emin de\u011filseniz bo\u015f b\u0131rak\u0131n)", "multicast_group": "Y\u00f6nlendirme ve ke\u015fif i\u00e7in kullan\u0131lan \u00e7ok noktaya yay\u0131n grubu", "multicast_port": "Y\u00f6nlendirme ve ke\u015fif i\u00e7in kullan\u0131lan \u00e7ok noktaya yay\u0131n ba\u011flant\u0131 noktas\u0131", "rate_limit": "Saniyede maksimum giden telegram say\u0131s\u0131", diff --git a/homeassistant/components/open_meteo/translations/de.json b/homeassistant/components/open_meteo/translations/de.json new file mode 100644 index 00000000000..b73563ab5b3 --- /dev/null +++ b/homeassistant/components/open_meteo/translations/de.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "zone": "Zone" + }, + "description": "Standort f\u00fcr die Wettervorhersage ausw\u00e4hlen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/open_meteo/translations/et.json b/homeassistant/components/open_meteo/translations/et.json new file mode 100644 index 00000000000..9a59666f7f4 --- /dev/null +++ b/homeassistant/components/open_meteo/translations/et.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "zone": "Ala" + }, + "description": "Vali ilmaennustuse jaoks kasutatav asukoht." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/open_meteo/translations/hu.json b/homeassistant/components/open_meteo/translations/hu.json new file mode 100644 index 00000000000..3f95d98bf45 --- /dev/null +++ b/homeassistant/components/open_meteo/translations/hu.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "zone": "Z\u00f3na" + }, + "description": "V\u00e1lassza ki az id\u0151j\u00e1r\u00e1s-el\u0151rejelz\u00e9shez haszn\u00e1lni k\u00edv\u00e1nt helysz\u00ednt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/open_meteo/translations/id.json b/homeassistant/components/open_meteo/translations/id.json new file mode 100644 index 00000000000..18465b8e93c --- /dev/null +++ b/homeassistant/components/open_meteo/translations/id.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "zone": "Zona" + }, + "description": "Pilih lokasi yang akan digunakan untuk prakiraan cuaca" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/open_meteo/translations/ja.json b/homeassistant/components/open_meteo/translations/ja.json new file mode 100644 index 00000000000..728860e5184 --- /dev/null +++ b/homeassistant/components/open_meteo/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "zone": "\u30be\u30fc\u30f3" + }, + "description": "\u5929\u6c17\u4e88\u5831\u3067\u4f7f\u7528\u3059\u308b\u5834\u6240\u3092\u9078\u629e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/open_meteo/translations/pl.json b/homeassistant/components/open_meteo/translations/pl.json new file mode 100644 index 00000000000..4a9d42458ca --- /dev/null +++ b/homeassistant/components/open_meteo/translations/pl.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "zone": "Strefa" + }, + "description": "Wybierz lokalizacj\u0119, dla kt\u00f3rej chcesz u\u017cywa\u0107 pogodynki" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/open_meteo/translations/ru.json b/homeassistant/components/open_meteo/translations/ru.json new file mode 100644 index 00000000000..c1784e02f41 --- /dev/null +++ b/homeassistant/components/open_meteo/translations/ru.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "zone": "\u0417\u043e\u043d\u0430" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0434\u043b\u044f \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u043f\u043e\u0433\u043e\u0434\u044b." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/open_meteo/translations/tr.json b/homeassistant/components/open_meteo/translations/tr.json new file mode 100644 index 00000000000..c2a8eac9e8e --- /dev/null +++ b/homeassistant/components/open_meteo/translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "zone": "B\u00f6lge" + }, + "description": "Hava tahmini i\u00e7in kullan\u0131lacak konumu se\u00e7in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/open_meteo/translations/zh-Hant.json b/homeassistant/components/open_meteo/translations/zh-Hant.json new file mode 100644 index 00000000000..1e49e2d2224 --- /dev/null +++ b/homeassistant/components/open_meteo/translations/zh-Hant.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "zone": "\u5340\u57df" + }, + "description": "\u9078\u64c7\u5929\u6c23\u9810\u5831\u4f7f\u7528\u4f4d\u7f6e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/translations/ja.json b/homeassistant/components/smartthings/translations/ja.json index b8cd8a28ff7..e522744cd4d 100644 --- a/homeassistant/components/smartthings/translations/ja.json +++ b/homeassistant/components/smartthings/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "invalid_webhook_url": "SmartThings\u304b\u3089\u66f4\u65b0\u3092\u53d7\u4fe1\u3059\u308b\u3088\u3046\u306bHome Assistant\u304c\u6b63\u3057\u304f\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002 Webhook\u306eURL\u304c\u7121\u52b9\u3067\u3059:\n> {webhook_url}\n\n[\u8aac\u660e\u66f8]({component_url}) \u306b\u5f93\u3063\u3066\u8a2d\u5b9a\u3092\u66f4\u65b0\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u3066\u304b\u3089\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "invalid_webhook_url": "Home Assistant\u304cSmartThings\u304b\u3089\u30a2\u30c3\u30d7\u30c7\u30fc\u30c8\u3092\u53d7\u3051\u53d6\u308c\u308b\u3088\u3046\u306b\u6b63\u3057\u304f\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002Webhook\u306eURL\u304c\u7121\u52b9\u3067\u3059:\n> {webhook_url}\n\n[\u8aac\u660e\u66f8]({component_url}) \u306b\u5f93\u3063\u3066\u8a2d\u5b9a\u3092\u66f4\u65b0\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u3066\u304b\u3089\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "no_available_locations": "Home Assistant\u306b\u8a2d\u5b9a\u3067\u304d\u308bSmartThings\u306e\u5834\u6240\u304c\u3042\u308a\u307e\u305b\u3093\u3002" }, "error": { diff --git a/homeassistant/components/twinkly/translations/tr.json b/homeassistant/components/twinkly/translations/tr.json index d2e7173dad3..a669e00e8b4 100644 --- a/homeassistant/components/twinkly/translations/tr.json +++ b/homeassistant/components/twinkly/translations/tr.json @@ -7,6 +7,9 @@ "cannot_connect": "Ba\u011flanma hatas\u0131" }, "step": { + "discovery_confirm": { + "description": "{name} - {model} ( {host} ) kurulumunu yapmak istiyor musunuz?" + }, "user": { "data": { "host": "Twinkly cihaz\u0131n\u0131z\u0131n ana bilgisayar\u0131 (veya IP adresi)" diff --git a/homeassistant/components/vicare/translations/de.json b/homeassistant/components/vicare/translations/de.json new file mode 100644 index 00000000000..854c1cadead --- /dev/null +++ b/homeassistant/components/vicare/translations/de.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", + "unknown": "Unerwarteter Fehler" + }, + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung" + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "client_id": "API-Schl\u00fcssel", + "heating_type": "Art der Heizung", + "name": "Name", + "password": "Passwort", + "scan_interval": "Scanintervall (Sekunden)", + "username": "E-Mail" + }, + "description": "Richte die ViCare-Integration ein. Um einen API-Schl\u00fcssel zu generieren, gehe auf https://developer.viessmann.com", + "title": "{name}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vicare/translations/en.json b/homeassistant/components/vicare/translations/en.json index d693cbe76cc..ef7235f86f8 100644 --- a/homeassistant/components/vicare/translations/en.json +++ b/homeassistant/components/vicare/translations/en.json @@ -11,15 +11,15 @@ "step": { "user": { "data": { - "name": "Name", - "scan_interval": "Scan Interval (seconds)", "client_id": "API Key", "heating_type": "Heating type", + "name": "Name", "password": "Password", + "scan_interval": "Scan Interval (seconds)", "username": "Email" }, - "title": "{name}", - "description": "Set up ViCare integration. To generate API key go to https://developer.viessmann.com" + "description": "Set up ViCare integration. To generate API key go to https://developer.viessmann.com", + "title": "{name}" } } } diff --git a/homeassistant/components/vicare/translations/et.json b/homeassistant/components/vicare/translations/et.json new file mode 100644 index 00000000000..9214a527580 --- /dev/null +++ b/homeassistant/components/vicare/translations/et.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks sidumine.", + "unknown": "Ootamatu t\u00f5rge" + }, + "error": { + "invalid_auth": "Tuvastamine nurjus" + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "client_id": "API v\u00f5ti", + "heating_type": "K\u00fcttere\u017eiim", + "name": "Nimi", + "password": "Salas\u00f5na", + "scan_interval": "P\u00e4ringute intervall (sekundites)", + "username": "E-posti aadress" + }, + "description": "Seadista ViCare sidumine. API-v\u00f5tme loomiseks mine aadressile https://developer.viessmann.com", + "title": "{name}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vicare/translations/pl.json b/homeassistant/components/vicare/translations/pl.json new file mode 100644 index 00000000000..e8374d7cc46 --- /dev/null +++ b/homeassistant/components/vicare/translations/pl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja.", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "error": { + "invalid_auth": "Niepoprawne uwierzytelnienie" + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "client_id": "Klucz API", + "heating_type": "Typ ogrzewania", + "name": "Nazwa", + "password": "Has\u0142o", + "scan_interval": "Cz\u0119stotliwo\u015b\u0107 skanowania (w sekundach)", + "username": "Adres e-mail" + }, + "description": "Skonfiguruj integracj\u0119 ViCare. Aby wygenerowa\u0107 klucz API wejd\u017a na https://developer.viessmann.com", + "title": "{name}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vicare/translations/tr.json b/homeassistant/components/vicare/translations/tr.json new file mode 100644 index 00000000000..0e3bd45bdf7 --- /dev/null +++ b/homeassistant/components/vicare/translations/tr.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", + "unknown": "Beklenmeyen hata" + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "client_id": "API Anahtar\u0131", + "heating_type": "Is\u0131tma tipi", + "name": "Ad", + "password": "Parola", + "scan_interval": "Tarama Aral\u0131\u011f\u0131 (saniye)", + "username": "E-posta" + }, + "description": "ViCare entegrasyonunu ayarlay\u0131n. API anahtar\u0131 olu\u015fturmak i\u00e7in https://developer.viessmann.com adresine gidin.", + "title": "{name}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vicare/translations/zh-Hant.json b/homeassistant/components/vicare/translations/zh-Hant.json new file mode 100644 index 00000000000..10119fcce93 --- /dev/null +++ b/homeassistant/components/vicare/translations/zh-Hant.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "error": { + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "client_id": "API \u5bc6\u9470", + "heating_type": "\u6696\u6c23\u985e\u578b", + "name": "\u540d\u7a31", + "password": "\u5bc6\u78bc", + "scan_interval": "\u6383\u63cf\u9593\u8ddd\uff08\u79d2\uff09", + "username": "\u96fb\u5b50\u90f5\u4ef6" + }, + "description": "\u6b32\u8a2d\u5b9a ViCare \u6574\u5408\u3002\u8acb\u81f3 https://developer.viessmann.com \u7522\u751f API \u5bc6\u9470", + "title": "{name}" + } + } + } +} \ No newline at end of file From c4879d71a1332129803aab855fb28aae1bc9f107 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 18 Dec 2021 12:34:16 +0100 Subject: [PATCH 0695/2644] Add Netgear entity category and configuration url (#62260) * add entity category * add configuration_url * add import --- homeassistant/components/netgear/__init__.py | 2 ++ homeassistant/components/netgear/sensor.py | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/homeassistant/components/netgear/__init__.py b/homeassistant/components/netgear/__init__.py index c3abbd59e27..954658a5bef 100644 --- a/homeassistant/components/netgear/__init__.py +++ b/homeassistant/components/netgear/__init__.py @@ -1,5 +1,6 @@ """Support for Netgear routers.""" from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr @@ -30,6 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: name=router.device_name, model=router.model, sw_version=router.firmware_version, + configuration_url=f"http://{entry.data[CONF_HOST]}/", ) hass.config_entries.async_setup_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/netgear/sensor.py b/homeassistant/components/netgear/sensor.py index 1af78110ed4..227e6e28b09 100644 --- a/homeassistant/components/netgear/sensor.py +++ b/homeassistant/components/netgear/sensor.py @@ -7,6 +7,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .router import NetgearDeviceEntity, NetgearRouter, async_setup_netgear_entry @@ -15,25 +16,30 @@ SENSOR_TYPES = { "type": SensorEntityDescription( key="type", name="link type", + entity_category=EntityCategory.DIAGNOSTIC, ), "link_rate": SensorEntityDescription( key="link_rate", name="link rate", native_unit_of_measurement="Mbps", + entity_category=EntityCategory.DIAGNOSTIC, ), "signal": SensorEntityDescription( key="signal", name="signal strength", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=EntityCategory.DIAGNOSTIC, ), "ssid": SensorEntityDescription( key="ssid", name="ssid", + entity_category=EntityCategory.DIAGNOSTIC, ), "conn_ap_mac": SensorEntityDescription( key="conn_ap_mac", name="access point mac", + entity_category=EntityCategory.DIAGNOSTIC, ), } From ca9b26e829d993f15efe36984976d28f69c158b7 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 18 Dec 2021 13:29:52 +0100 Subject: [PATCH 0696/2644] bump pynetgear to 0.8.0 (#62261) --- homeassistant/components/netgear/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netgear/manifest.json b/homeassistant/components/netgear/manifest.json index aa4c57ecdde..1f4a15f1de1 100644 --- a/homeassistant/components/netgear/manifest.json +++ b/homeassistant/components/netgear/manifest.json @@ -2,7 +2,7 @@ "domain": "netgear", "name": "NETGEAR", "documentation": "https://www.home-assistant.io/integrations/netgear", - "requirements": ["pynetgear==0.7.0"], + "requirements": ["pynetgear==0.8.0"], "codeowners": ["@hacf-fr", "@Quentame", "@starkillerOG"], "iot_class": "local_polling", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 18cf542b232..ae3d4056492 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1673,7 +1673,7 @@ pymyq==3.1.4 pymysensors==0.22.1 # homeassistant.components.netgear -pynetgear==0.7.0 +pynetgear==0.8.0 # homeassistant.components.netio pynetio==0.1.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 88987cb4500..be904f2d691 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1031,7 +1031,7 @@ pymyq==3.1.4 pymysensors==0.22.1 # homeassistant.components.netgear -pynetgear==0.7.0 +pynetgear==0.8.0 # homeassistant.components.nina pynina==0.1.4 From c178fd0cc3c6e92e604340c6413eaae62c288642 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 18 Dec 2021 13:53:21 +0100 Subject: [PATCH 0697/2644] Add entity category to DSMR (#62262) --- homeassistant/components/dsmr/const.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/homeassistant/components/dsmr/const.py b/homeassistant/components/dsmr/const.py index 8f5620bfd7c..a8c4de930df 100644 --- a/homeassistant/components/dsmr/const.py +++ b/homeassistant/components/dsmr/const.py @@ -7,6 +7,7 @@ from dsmr_parser import obis_references from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.const import Platform +from homeassistant.helpers.entity import EntityCategory from .models import DSMRSensorEntityDescription @@ -137,6 +138,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, icon="mdi:flash-off", + entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( key=obis_references.LONG_POWER_FAILURE_COUNT, @@ -144,24 +146,28 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, icon="mdi:flash-off", + entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( key=obis_references.VOLTAGE_SAG_L1_COUNT, name="Voltage Sags Phase L1", dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( key=obis_references.VOLTAGE_SAG_L2_COUNT, name="Voltage Sags Phase L2", dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( key=obis_references.VOLTAGE_SAG_L3_COUNT, name="Voltage Sags Phase L3", dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( key=obis_references.VOLTAGE_SWELL_L1_COUNT, @@ -169,6 +175,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, icon="mdi:pulse", + entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( key=obis_references.VOLTAGE_SWELL_L2_COUNT, @@ -176,6 +183,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, icon="mdi:pulse", + entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( key=obis_references.VOLTAGE_SWELL_L3_COUNT, @@ -183,6 +191,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, icon="mdi:pulse", + entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( key=obis_references.INSTANTANEOUS_VOLTAGE_L1, @@ -190,6 +199,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.VOLTAGE, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( key=obis_references.INSTANTANEOUS_VOLTAGE_L2, @@ -197,6 +207,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.VOLTAGE, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( key=obis_references.INSTANTANEOUS_VOLTAGE_L3, @@ -204,6 +215,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.VOLTAGE, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( key=obis_references.INSTANTANEOUS_CURRENT_L1, @@ -211,6 +223,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.CURRENT, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( key=obis_references.INSTANTANEOUS_CURRENT_L2, @@ -218,6 +231,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.CURRENT, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( key=obis_references.INSTANTANEOUS_CURRENT_L3, @@ -225,6 +239,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.CURRENT, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( key=obis_references.LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL, From e2c65a30345d65d37609473f2baadc8b3e685eb6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 18 Dec 2021 14:18:31 +0100 Subject: [PATCH 0698/2644] Upgrade tailscale to 0.1.6 (#62267) --- homeassistant/components/tailscale/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tailscale/manifest.json b/homeassistant/components/tailscale/manifest.json index eaa51855d38..ac7cbe84459 100644 --- a/homeassistant/components/tailscale/manifest.json +++ b/homeassistant/components/tailscale/manifest.json @@ -3,7 +3,7 @@ "name": "Tailscale", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tailscale", - "requirements": ["tailscale==0.1.5"], + "requirements": ["tailscale==0.1.6"], "codeowners": ["@frenck"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index ae3d4056492..a17452d7489 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2287,7 +2287,7 @@ systembridge==2.2.3 tahoma-api==0.0.16 # homeassistant.components.tailscale -tailscale==0.1.5 +tailscale==0.1.6 # homeassistant.components.tank_utility tank_utility==1.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index be904f2d691..5d6d6e65547 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1367,7 +1367,7 @@ surepy==0.7.2 systembridge==2.2.3 # homeassistant.components.tailscale -tailscale==0.1.5 +tailscale==0.1.6 # homeassistant.components.tellduslive tellduslive==0.10.11 From 503a5cbd44bd73f7a79907044b14986df2b402b9 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 18 Dec 2021 14:23:34 +0100 Subject: [PATCH 0699/2644] Motion blinds add entity category (#62266) --- homeassistant/components/motion_blinds/sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/motion_blinds/sensor.py b/homeassistant/components/motion_blinds/sensor.py index 126c1607864..6414a010dc1 100644 --- a/homeassistant/components/motion_blinds/sensor.py +++ b/homeassistant/components/motion_blinds/sensor.py @@ -3,7 +3,7 @@ from motionblinds import BlindType from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.const import PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ATTR_AVAILABLE, DOMAIN, KEY_COORDINATOR, KEY_GATEWAY @@ -117,6 +117,7 @@ class MotionSignalStrengthSensor(CoordinatorEntity, SensorEntity): _attr_device_class = SensorDeviceClass.SIGNAL_STRENGTH _attr_entity_registry_enabled_default = False _attr_native_unit_of_measurement = SIGNAL_STRENGTH_DECIBELS_MILLIWATT + _attr_entity_category = EntityCategory.DIAGNOSTIC def __init__(self, coordinator, device, device_type): """Initialize the Motion Signal Strength Sensor.""" From c40b02896af86fec5dc4597874abc0c86a0027c2 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 18 Dec 2021 14:45:53 +0100 Subject: [PATCH 0700/2644] Hyperion add entity category (#62268) --- homeassistant/components/hyperion/switch.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hyperion/switch.py b/homeassistant/components/hyperion/switch.py index 1c884cca908..6cc90af3c4d 100644 --- a/homeassistant/components/hyperion/switch.py +++ b/homeassistant/components/hyperion/switch.py @@ -31,7 +31,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify @@ -127,6 +127,8 @@ async def async_setup_entry( class HyperionComponentSwitch(SwitchEntity): """ComponentBinarySwitch switch class.""" + _attr_entity_category = EntityCategory.CONFIG + def __init__( self, server_id: str, From b1ac1596d85fc20f3b987c7a0cc314793554f0a5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 18 Dec 2021 07:46:22 -0600 Subject: [PATCH 0701/2644] Add ATTR_HW_VERSION to homeassistant.const (#62258) --- homeassistant/const.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5afbd1daee1..2b9302c0fc9 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -400,6 +400,7 @@ ATTR_MANUFACTURER: Final = "manufacturer" ATTR_MODEL: Final = "model" ATTR_SUGGESTED_AREA: Final = "suggested_area" ATTR_SW_VERSION: Final = "sw_version" +ATTR_HW_VERSION: Final = "hw_version" ATTR_VIA_DEVICE: Final = "via_device" ATTR_BATTERY_CHARGING: Final = "battery_charging" From 051a6499b8bfe8ef17efbf6312fe6cd490db67d0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 18 Dec 2021 07:49:29 -0600 Subject: [PATCH 0702/2644] Add software version to screenlogic (#62255) --- homeassistant/components/screenlogic/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/screenlogic/__init__.py b/homeassistant/components/screenlogic/__init__.py index e88475ea5bb..eac3148ec3a 100644 --- a/homeassistant/components/screenlogic/__init__.py +++ b/homeassistant/components/screenlogic/__init__.py @@ -222,6 +222,7 @@ class ScreenlogicEntity(CoordinatorEntity): manufacturer="Pentair", model=equipment_model, name=self.gateway_name, + sw_version=self.gateway.version, ) async def _async_refresh(self): From ac0fa3cbf8d932c1718e37147877baaefc9d5dbe Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 18 Dec 2021 14:51:39 +0100 Subject: [PATCH 0703/2644] Use new enums in smarttub (#62216) Co-authored-by: epenet --- homeassistant/components/smarttub/binary_sensor.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/smarttub/binary_sensor.py b/homeassistant/components/smarttub/binary_sensor.py index ead0e6a0dce..f5af1655255 100644 --- a/homeassistant/components/smarttub/binary_sensor.py +++ b/homeassistant/components/smarttub/binary_sensor.py @@ -5,8 +5,7 @@ from smarttub import SpaError, SpaReminder import voluptuous as vol from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_CONNECTIVITY, - DEVICE_CLASS_PROBLEM, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.helpers import entity_platform @@ -71,7 +70,7 @@ async def async_setup_entry(hass, entry, async_add_entities): class SmartTubOnline(SmartTubSensorBase, BinarySensorEntity): """A binary sensor indicating whether the spa is currently online (connected to the cloud).""" - _attr_device_class = DEVICE_CLASS_CONNECTIVITY + _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY def __init__(self, coordinator, spa): """Initialize the entity.""" @@ -94,7 +93,7 @@ class SmartTubOnline(SmartTubSensorBase, BinarySensorEntity): class SmartTubReminder(SmartTubEntity, BinarySensorEntity): """Reminders for maintenance actions.""" - _attr_device_class = DEVICE_CLASS_PROBLEM + _attr_device_class = BinarySensorDeviceClass.PROBLEM def __init__(self, coordinator, spa, reminder): """Initialize the entity.""" @@ -145,7 +144,7 @@ class SmartTubError(SmartTubEntity, BinarySensorEntity): There may be 0 or more errors. If there are >0, we show the first one. """ - _attr_device_class = DEVICE_CLASS_PROBLEM + _attr_device_class = BinarySensorDeviceClass.PROBLEM def __init__(self, coordinator, spa): """Initialize the entity.""" From a39f0643e8f4733a507cbfc56bc26f82fc426dc7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 18 Dec 2021 14:53:50 +0100 Subject: [PATCH 0704/2644] Use new enums in smart_meter_texas (#62210) Co-authored-by: epenet --- homeassistant/components/smart_meter_texas/sensor.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/smart_meter_texas/sensor.py b/homeassistant/components/smart_meter_texas/sensor.py index e897048660f..e84cff9cacb 100644 --- a/homeassistant/components/smart_meter_texas/sensor.py +++ b/homeassistant/components/smart_meter_texas/sensor.py @@ -1,8 +1,12 @@ """Support for Smart Meter Texas sensors.""" from smart_meter_texas import Meter -from homeassistant.components.sensor import STATE_CLASS_TOTAL_INCREASING, SensorEntity -from homeassistant.const import CONF_ADDRESS, DEVICE_CLASS_ENERGY, ENERGY_KILO_WATT_HOUR +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorStateClass, +) +from homeassistant.const import CONF_ADDRESS, ENERGY_KILO_WATT_HOUR from homeassistant.core import callback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.update_coordinator import ( @@ -33,8 +37,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class SmartMeterTexasSensor(CoordinatorEntity, RestoreEntity, SensorEntity): """Representation of an Smart Meter Texas sensor.""" - _attr_device_class = DEVICE_CLASS_ENERGY - _attr_state_class = STATE_CLASS_TOTAL_INCREASING + _attr_device_class = SensorDeviceClass.ENERGY + _attr_state_class = SensorStateClass.TOTAL_INCREASING _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR def __init__(self, meter: Meter, coordinator: DataUpdateCoordinator) -> None: From af631b90e5f47c5b6d10d156f2cd46ceee612def Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sat, 18 Dec 2021 08:54:26 -0500 Subject: [PATCH 0705/2644] Use enums in statistics tests (#62191) --- homeassistant/components/statistics/sensor.py | 4 ++-- tests/components/statistics/test_sensor.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index b06589f52c0..e2608c888aa 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -10,8 +10,8 @@ from homeassistant.components.recorder.models import States from homeassistant.components.recorder.util import execute, session_scope from homeassistant.components.sensor import ( PLATFORM_SCHEMA, - STATE_CLASS_MEASUREMENT, SensorEntity, + SensorStateClass, ) from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, @@ -350,7 +350,7 @@ class StatisticsSensor(SensorEntity): """Return the state class of this entity.""" if self._state_characteristic in STATS_NOT_A_NUMBER: return None - return STATE_CLASS_MEASUREMENT + return SensorStateClass.MEASUREMENT @property def native_value(self): diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index e3ace35f0ae..51ab31e6ed5 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -4,7 +4,7 @@ import statistics from unittest.mock import patch from homeassistant import config as hass_config -from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT +from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorStateClass from homeassistant.components.statistics import DOMAIN as STATISTICS_DOMAIN from homeassistant.components.statistics.sensor import StatisticsSensor from homeassistant.const import ( @@ -82,7 +82,7 @@ async def test_sensor_defaults_numeric(hass): state = hass.states.get("sensor.test") assert state.state == str(round(sum(VALUES_NUMERIC) / len(VALUES_NUMERIC), 2)) assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get("buffer_usage_ratio") == round(9 / 20, 2) assert state.attributes.get("source_value_valid") is True assert "age_coverage_ratio" not in state.attributes @@ -167,7 +167,7 @@ async def test_sensor_defaults_binary(hass): state = hass.states.get("sensor.test") assert state.state == str(len(VALUES_BINARY)) assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get("buffer_usage_ratio") == round(9 / 20, 2) assert state.attributes.get("source_value_valid") is True assert "age_coverage_ratio" not in state.attributes @@ -444,7 +444,7 @@ async def test_state_class(hass): await hass.async_block_till_done() state = hass.states.get("sensor.test_normal") - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT state = hass.states.get("sensor.test_nan") assert state.attributes.get(ATTR_STATE_CLASS) is None From 93cba53860bf79a0099ea5d72f19084eb68b1e78 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 18 Dec 2021 14:56:13 +0100 Subject: [PATCH 0706/2644] Use new enums in smappee (#62209) Co-authored-by: epenet --- .../components/smappee/binary_sensor.py | 6 +- homeassistant/components/smappee/sensor.py | 81 +++++++++---------- 2 files changed, 42 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/smappee/binary_sensor.py b/homeassistant/components/smappee/binary_sensor.py index 7e46a1021e5..35aa103bd03 100644 --- a/homeassistant/components/smappee/binary_sensor.py +++ b/homeassistant/components/smappee/binary_sensor.py @@ -1,6 +1,6 @@ """Support for monitoring a Smappee appliance binary sensor.""" from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_PRESENCE, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.helpers.entity import DeviceInfo @@ -58,7 +58,7 @@ class SmappeePresence(BinarySensorEntity): @property def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" - return DEVICE_CLASS_PRESENCE + return BinarySensorDeviceClass.PRESENCE @property def unique_id( @@ -68,7 +68,7 @@ class SmappeePresence(BinarySensorEntity): return ( f"{self._service_location.device_serial_number}-" f"{self._service_location.service_location_id}-" - f"{DEVICE_CLASS_PRESENCE}" + f"{BinarySensorDeviceClass.PRESENCE}" ) @property diff --git a/homeassistant/components/smappee/sensor.py b/homeassistant/components/smappee/sensor.py index 595cc4da02d..e2eef5d06cf 100644 --- a/homeassistant/components/smappee/sensor.py +++ b/homeassistant/components/smappee/sensor.py @@ -4,15 +4,12 @@ from __future__ import annotations from dataclasses import dataclass, field from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.const import ( - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_VOLTAGE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, ENERGY_WATT_HOUR, @@ -55,8 +52,8 @@ TREND_SENSORS: tuple[SmappeePollingSensorEntityDescription, ...] = ( name="Total consumption - Active power", native_unit_of_measurement=POWER_WATT, sensor_id="total_power", - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, local_polling=True, # both cloud and local ), SmappeePollingSensorEntityDescription( @@ -64,40 +61,40 @@ TREND_SENSORS: tuple[SmappeePollingSensorEntityDescription, ...] = ( name="Always on - Active power", native_unit_of_measurement=POWER_WATT, sensor_id="alwayson", - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), SmappeePollingSensorEntityDescription( key="power_today", name="Total consumption - Today", native_unit_of_measurement=ENERGY_WATT_HOUR, sensor_id="power_today", - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), SmappeePollingSensorEntityDescription( key="power_current_hour", name="Total consumption - Current hour", native_unit_of_measurement=ENERGY_WATT_HOUR, sensor_id="power_current_hour", - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), SmappeePollingSensorEntityDescription( key="power_last_5_minutes", name="Total consumption - Last 5 minutes", native_unit_of_measurement=ENERGY_WATT_HOUR, sensor_id="power_last_5_minutes", - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), SmappeePollingSensorEntityDescription( key="alwayson_today", name="Always on - Today", native_unit_of_measurement=ENERGY_WATT_HOUR, sensor_id="alwayson_today", - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), ) REACTIVE_SENSORS: tuple[SmappeeSensorEntityDescription, ...] = ( @@ -106,8 +103,8 @@ REACTIVE_SENSORS: tuple[SmappeeSensorEntityDescription, ...] = ( name="Total consumption - Reactive power", native_unit_of_measurement=POWER_WATT, sensor_id="total_reactive_power", - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), ) SOLAR_SENSORS: tuple[SmappeePollingSensorEntityDescription, ...] = ( @@ -116,8 +113,8 @@ SOLAR_SENSORS: tuple[SmappeePollingSensorEntityDescription, ...] = ( name="Total production - Active power", native_unit_of_measurement=POWER_WATT, sensor_id="solar_power", - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, local_polling=True, # both cloud and local ), SmappeePollingSensorEntityDescription( @@ -125,16 +122,16 @@ SOLAR_SENSORS: tuple[SmappeePollingSensorEntityDescription, ...] = ( name="Total production - Today", native_unit_of_measurement=ENERGY_WATT_HOUR, sensor_id="solar_today", - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), SmappeePollingSensorEntityDescription( key="solar_current_hour", name="Total production - Current hour", native_unit_of_measurement=ENERGY_WATT_HOUR, sensor_id="solar_current_hour", - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), ) VOLTAGE_SENSORS: tuple[SmappeeVoltageSensorEntityDescription, ...] = ( @@ -143,8 +140,8 @@ VOLTAGE_SENSORS: tuple[SmappeeVoltageSensorEntityDescription, ...] = ( name="Phase voltages - A", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, sensor_id="phase_voltage_a", - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, phase_types={"ONE", "TWO", "THREE_STAR", "THREE_DELTA"}, ), SmappeeVoltageSensorEntityDescription( @@ -152,8 +149,8 @@ VOLTAGE_SENSORS: tuple[SmappeeVoltageSensorEntityDescription, ...] = ( name="Phase voltages - B", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, sensor_id="phase_voltage_b", - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, phase_types={"TWO", "THREE_STAR", "THREE_DELTA"}, ), SmappeeVoltageSensorEntityDescription( @@ -161,8 +158,8 @@ VOLTAGE_SENSORS: tuple[SmappeeVoltageSensorEntityDescription, ...] = ( name="Phase voltages - C", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, sensor_id="phase_voltage_c", - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, phase_types={"THREE_STAR"}, ), SmappeeVoltageSensorEntityDescription( @@ -170,8 +167,8 @@ VOLTAGE_SENSORS: tuple[SmappeeVoltageSensorEntityDescription, ...] = ( name="Line voltages - A", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, sensor_id="line_voltage_a", - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, phase_types={"ONE", "TWO", "THREE_STAR", "THREE_DELTA"}, ), SmappeeVoltageSensorEntityDescription( @@ -179,8 +176,8 @@ VOLTAGE_SENSORS: tuple[SmappeeVoltageSensorEntityDescription, ...] = ( name="Line voltages - B", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, sensor_id="line_voltage_b", - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, phase_types={"TWO", "THREE_STAR", "THREE_DELTA"}, ), SmappeeVoltageSensorEntityDescription( @@ -188,8 +185,8 @@ VOLTAGE_SENSORS: tuple[SmappeeVoltageSensorEntityDescription, ...] = ( name="Line voltages - C", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, sensor_id="line_voltage_c", - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, phase_types={"THREE_STAR", "THREE_DELTA"}, ), ) @@ -252,8 +249,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): name=measurement.name, native_unit_of_measurement=POWER_WATT, sensor_id=measurement_id, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), ) for measurement_id, measurement in service_location.measurements.items() @@ -296,7 +293,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ), native_unit_of_measurement=channel.get("uom"), sensor_id=f"{sensor_id}-{channel.get('channel')}", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), ) for sensor_id, sensor in service_location.sensors.items() @@ -315,8 +312,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): name=f"{actuator.name} - energy today", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, sensor_id=actuator_id, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), ) for actuator_id, actuator in service_location.actuators.items() From ff530dce0de2416f37455c1ffa5a3575145b3eaa Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 18 Dec 2021 15:00:07 +0100 Subject: [PATCH 0707/2644] Fix fitbit no SSL URL handling (#62270) --- homeassistant/components/fitbit/sensor.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index 2de121920af..0c638f0c455 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -112,10 +112,11 @@ def request_app_setup( Then come back here and hit the below button. """ except NoURLAvailableError: - error_msg = """Could not find a SSL enabled URL for your Home Assistant instance. - Fitbit requires that your Home Assistant instance is accessible via HTTPS. - """ - configurator.notify_errors(_CONFIGURING["fitbit"], error_msg) + _LOGGER.error( + "Could not find an SSL enabled URL for your Home Assistant instance. " + "Fitbit requires that your Home Assistant instance is accessible via HTTPS" + ) + return submit = "I have saved my Client ID and Client Secret into fitbit.conf." From 78f2866f9852fd70925280d6058edeaa8f3c06be Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sat, 18 Dec 2021 10:17:55 -0500 Subject: [PATCH 0708/2644] Finish using enums in wled (#62189) --- .../components/wled/binary_sensor.py | 4 ++-- homeassistant/components/wled/button.py | 6 +++--- homeassistant/components/wled/number.py | 6 +++--- homeassistant/components/wled/select.py | 6 +++--- homeassistant/components/wled/sensor.py | 20 +++++++++---------- homeassistant/components/wled/switch.py | 10 +++++----- tests/components/wled/test_binary_sensor.py | 13 ++++-------- tests/components/wled/test_button.py | 6 +++--- tests/components/wled/test_select.py | 4 ++-- tests/components/wled/test_sensor.py | 20 +++++++++---------- tests/components/wled/test_switch.py | 10 +++++----- 11 files changed, 50 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/wled/binary_sensor.py b/homeassistant/components/wled/binary_sensor.py index ce082691ffe..1a8033701da 100644 --- a/homeassistant/components/wled/binary_sensor.py +++ b/homeassistant/components/wled/binary_sensor.py @@ -6,8 +6,8 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN @@ -32,7 +32,7 @@ async def async_setup_entry( class WLEDUpdateBinarySensor(WLEDEntity, BinarySensorEntity): """Defines a WLED firmware binary sensor.""" - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_device_class = BinarySensorDeviceClass.UPDATE def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: diff --git a/homeassistant/components/wled/button.py b/homeassistant/components/wled/button.py index 191242fe0dc..5b2e13911bc 100644 --- a/homeassistant/components/wled/button.py +++ b/homeassistant/components/wled/button.py @@ -3,8 +3,8 @@ from __future__ import annotations from homeassistant.components.button import ButtonDeviceClass, ButtonEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN @@ -32,7 +32,7 @@ class WLEDRestartButton(WLEDEntity, ButtonEntity): """Defines a WLED restart button.""" _attr_device_class = ButtonDeviceClass.RESTART - _attr_entity_category = ENTITY_CATEGORY_CONFIG + _attr_entity_category = EntityCategory.CONFIG def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize the button entity.""" @@ -50,7 +50,7 @@ class WLEDUpdateButton(WLEDEntity, ButtonEntity): """Defines a WLED update button.""" _attr_device_class = ButtonDeviceClass.UPDATE - _attr_entity_category = ENTITY_CATEGORY_CONFIG + _attr_entity_category = EntityCategory.CONFIG def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize the button entity.""" diff --git a/homeassistant/components/wled/number.py b/homeassistant/components/wled/number.py index 48c92cf839c..d551072e452 100644 --- a/homeassistant/components/wled/number.py +++ b/homeassistant/components/wled/number.py @@ -5,8 +5,8 @@ from functools import partial from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ATTR_INTENSITY, ATTR_SPEED, DOMAIN @@ -40,7 +40,7 @@ NUMBERS = [ key=ATTR_SPEED, name="Speed", icon="mdi:speedometer", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, step=1, min_value=0, max_value=255, @@ -48,7 +48,7 @@ NUMBERS = [ NumberEntityDescription( key=ATTR_INTENSITY, name="Intensity", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, step=1, min_value=0, max_value=255, diff --git a/homeassistant/components/wled/select.py b/homeassistant/components/wled/select.py index d82f12cffd7..e555b3422ce 100644 --- a/homeassistant/components/wled/select.py +++ b/homeassistant/components/wled/select.py @@ -7,8 +7,8 @@ from wled import Live, Playlist, Preset from homeassistant.components.select import SelectEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DEVICE_CLASS_WLED_LIVE_OVERRIDE, DOMAIN @@ -49,7 +49,7 @@ class WLEDLiveOverrideSelect(WLEDEntity, SelectEntity): """Defined a WLED Live Override select.""" _attr_device_class = DEVICE_CLASS_WLED_LIVE_OVERRIDE - _attr_entity_category = ENTITY_CATEGORY_CONFIG + _attr_entity_category = EntityCategory.CONFIG _attr_icon = "mdi:theater" def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: @@ -138,7 +138,7 @@ class WLEDPlaylistSelect(WLEDEntity, SelectEntity): class WLEDPaletteSelect(WLEDEntity, SelectEntity): """Defines a WLED Palette select.""" - _attr_entity_category = ENTITY_CATEGORY_CONFIG + _attr_entity_category = EntityCategory.CONFIG _attr_icon = "mdi:palette-outline" _segment: int diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py index 0bde4107688..05d92a91e3d 100644 --- a/homeassistant/components/wled/sensor.py +++ b/homeassistant/components/wled/sensor.py @@ -17,11 +17,11 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DATA_BYTES, ELECTRIC_CURRENT_MILLIAMPERE, - ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.util.dt import utcnow @@ -52,20 +52,20 @@ SENSORS: tuple[WLEDSensorEntityDescription, ...] = ( native_unit_of_measurement=ELECTRIC_CURRENT_MILLIAMPERE, device_class=SensorDeviceClass.CURRENT, state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.info.leds.power, ), WLEDSensorEntityDescription( key="info_leds_count", name="LED Count", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.info.leds.count, ), WLEDSensorEntityDescription( key="info_leds_max_power", name="Max Current", native_unit_of_measurement=ELECTRIC_CURRENT_MILLIAMPERE, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorDeviceClass.CURRENT, value_fn=lambda device: device.info.leds.max_power, ), @@ -73,7 +73,7 @@ SENSORS: tuple[WLEDSensorEntityDescription, ...] = ( key="uptime", name="Uptime", device_class=SensorDeviceClass.TIMESTAMP, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda device: (utcnow() - timedelta(seconds=device.info.uptime)), ), @@ -83,7 +83,7 @@ SENSORS: tuple[WLEDSensorEntityDescription, ...] = ( icon="mdi:memory", native_unit_of_measurement=DATA_BYTES, state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda device: device.info.free_heap, ), @@ -92,7 +92,7 @@ SENSORS: tuple[WLEDSensorEntityDescription, ...] = ( name="Wi-Fi Signal", icon="mdi:wifi", native_unit_of_measurement=PERCENTAGE, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda device: device.info.wifi.signal if device.info.wifi else None, ), @@ -101,7 +101,7 @@ SENSORS: tuple[WLEDSensorEntityDescription, ...] = ( name="Wi-Fi RSSI", native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda device: device.info.wifi.rssi if device.info.wifi else None, ), @@ -109,7 +109,7 @@ SENSORS: tuple[WLEDSensorEntityDescription, ...] = ( key="wifi_channel", name="Wi-Fi Channel", icon="mdi:wifi", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda device: device.info.wifi.channel if device.info.wifi else None, ), @@ -117,7 +117,7 @@ SENSORS: tuple[WLEDSensorEntityDescription, ...] = ( key="wifi_bssid", name="Wi-Fi BSSID", icon="mdi:wifi", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda device: device.info.wifi.bssid if device.info.wifi else None, ), diff --git a/homeassistant/components/wled/switch.py b/homeassistant/components/wled/switch.py index a05a0ddaf08..e98b3494ad6 100644 --- a/homeassistant/components/wled/switch.py +++ b/homeassistant/components/wled/switch.py @@ -6,8 +6,8 @@ from typing import Any from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( @@ -54,7 +54,7 @@ class WLEDNightlightSwitch(WLEDEntity, SwitchEntity): """Defines a WLED nightlight switch.""" _attr_icon = "mdi:weather-night" - _attr_entity_category = ENTITY_CATEGORY_CONFIG + _attr_entity_category = EntityCategory.CONFIG def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED nightlight switch.""" @@ -91,7 +91,7 @@ class WLEDSyncSendSwitch(WLEDEntity, SwitchEntity): """Defines a WLED sync send switch.""" _attr_icon = "mdi:upload-network-outline" - _attr_entity_category = ENTITY_CATEGORY_CONFIG + _attr_entity_category = EntityCategory.CONFIG def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED sync send switch.""" @@ -124,7 +124,7 @@ class WLEDSyncReceiveSwitch(WLEDEntity, SwitchEntity): """Defines a WLED sync receive switch.""" _attr_icon = "mdi:download-network-outline" - _attr_entity_category = ENTITY_CATEGORY_CONFIG + _attr_entity_category = EntityCategory.CONFIG def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED sync receive switch.""" @@ -157,7 +157,7 @@ class WLEDReverseSwitch(WLEDEntity, SwitchEntity): """Defines a WLED reverse effect switch.""" _attr_icon = "mdi:swap-horizontal-bold" - _attr_entity_category = ENTITY_CATEGORY_CONFIG + _attr_entity_category = EntityCategory.CONFIG _segment: int def __init__(self, coordinator: WLEDDataUpdateCoordinator, segment: int) -> None: diff --git a/tests/components/wled/test_binary_sensor.py b/tests/components/wled/test_binary_sensor.py index 5c40f8833e5..311044d213d 100644 --- a/tests/components/wled/test_binary_sensor.py +++ b/tests/components/wled/test_binary_sensor.py @@ -4,15 +4,10 @@ from unittest.mock import MagicMock import pytest from homeassistant.components.binary_sensor import BinarySensorDeviceClass -from homeassistant.const import ( - ATTR_DEVICE_CLASS, - ATTR_ICON, - ENTITY_CATEGORY_DIAGNOSTIC, - STATE_OFF, - STATE_ON, -) +from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ICON, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity import EntityCategory from tests.common import MockConfigEntry @@ -32,7 +27,7 @@ async def test_update_available( entry = entity_registry.async_get("binary_sensor.wled_rgb_light_firmware") assert entry assert entry.unique_id == "aabbccddeeff_update" - assert entry.entity_category == ENTITY_CATEGORY_DIAGNOSTIC + assert entry.entity_category is EntityCategory.DIAGNOSTIC @pytest.mark.parametrize("mock_wled", ["wled/rgb_websocket.json"], indirect=True) @@ -51,4 +46,4 @@ async def test_no_update_available( entry = entity_registry.async_get("binary_sensor.wled_websocket_firmware") assert entry assert entry.unique_id == "aabbccddeeff_update" - assert entry.entity_category == ENTITY_CATEGORY_DIAGNOSTIC + assert entry.entity_category is EntityCategory.DIAGNOSTIC diff --git a/tests/components/wled/test_button.py b/tests/components/wled/test_button.py index d6eea403d97..c9c8412a5b9 100644 --- a/tests/components/wled/test_button.py +++ b/tests/components/wled/test_button.py @@ -13,12 +13,12 @@ from homeassistant.components.button import ( from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, - ENTITY_CATEGORY_CONFIG, STATE_UNAVAILABLE, STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity import EntityCategory from tests.common import MockConfigEntry @@ -37,7 +37,7 @@ async def test_button_restart( entry = entity_registry.async_get("button.wled_rgb_light_restart") assert entry assert entry.unique_id == "aabbccddeeff_restart" - assert entry.entity_category == ENTITY_CATEGORY_CONFIG + assert entry.entity_category is EntityCategory.CONFIG await hass.services.async_call( BUTTON_DOMAIN, @@ -111,7 +111,7 @@ async def test_button_update_stay_stable( entry = entity_registry.async_get("button.wled_rgb_light_update") assert entry assert entry.unique_id == "aabbccddeeff_update" - assert entry.entity_category == ENTITY_CATEGORY_CONFIG + assert entry.entity_category is EntityCategory.CONFIG state = hass.states.get("button.wled_rgb_light_update") assert state diff --git a/tests/components/wled/test_select.py b/tests/components/wled/test_select.py index 345c0c632fe..d798a723246 100644 --- a/tests/components/wled/test_select.py +++ b/tests/components/wled/test_select.py @@ -11,13 +11,13 @@ from homeassistant.components.wled.const import SCAN_INTERVAL from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_ICON, - ENTITY_CATEGORY_CONFIG, SERVICE_SELECT_OPTION, STATE_UNAVAILABLE, STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity import EntityCategory import homeassistant.util.dt as dt_util from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture @@ -90,7 +90,7 @@ async def test_color_palette_state( entry = entity_registry.async_get("select.wled_rgb_light_segment_1_color_palette") assert entry assert entry.unique_id == "aabbccddeeff_palette_1" - assert entry.entity_category == ENTITY_CATEGORY_CONFIG + assert entry.entity_category is EntityCategory.CONFIG async def test_color_palette_segment_change_state( diff --git a/tests/components/wled/test_sensor.py b/tests/components/wled/test_sensor.py index 9e43f573f7e..bf3ef3b060b 100644 --- a/tests/components/wled/test_sensor.py +++ b/tests/components/wled/test_sensor.py @@ -12,13 +12,13 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, DATA_BYTES, ELECTRIC_CURRENT_MILLIAMPERE, - ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity import EntityCategory from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry @@ -99,7 +99,7 @@ async def test_sensors( entry = registry.async_get("sensor.wled_rgb_light_estimated_current") assert entry assert entry.unique_id == "aabbccddeeff_estimated_current" - assert entry.entity_category == ENTITY_CATEGORY_DIAGNOSTIC + assert entry.entity_category is EntityCategory.DIAGNOSTIC state = hass.states.get("sensor.wled_rgb_light_uptime") assert state @@ -110,31 +110,31 @@ async def test_sensors( entry = registry.async_get("sensor.wled_rgb_light_uptime") assert entry assert entry.unique_id == "aabbccddeeff_uptime" - assert entry.entity_category == ENTITY_CATEGORY_DIAGNOSTIC + assert entry.entity_category is EntityCategory.DIAGNOSTIC state = hass.states.get("sensor.wled_rgb_light_free_memory") assert state assert state.attributes.get(ATTR_ICON) == "mdi:memory" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_BYTES assert state.state == "14600" - assert entry.entity_category == ENTITY_CATEGORY_DIAGNOSTIC + assert entry.entity_category is EntityCategory.DIAGNOSTIC entry = registry.async_get("sensor.wled_rgb_light_free_memory") assert entry assert entry.unique_id == "aabbccddeeff_free_heap" - assert entry.entity_category == ENTITY_CATEGORY_DIAGNOSTIC + assert entry.entity_category is EntityCategory.DIAGNOSTIC state = hass.states.get("sensor.wled_rgb_light_wifi_signal") assert state assert state.attributes.get(ATTR_ICON) == "mdi:wifi" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "76" - assert entry.entity_category == ENTITY_CATEGORY_DIAGNOSTIC + assert entry.entity_category is EntityCategory.DIAGNOSTIC entry = registry.async_get("sensor.wled_rgb_light_wifi_signal") assert entry assert entry.unique_id == "aabbccddeeff_wifi_signal" - assert entry.entity_category == ENTITY_CATEGORY_DIAGNOSTIC + assert entry.entity_category is EntityCategory.DIAGNOSTIC state = hass.states.get("sensor.wled_rgb_light_wifi_rssi") assert state @@ -148,7 +148,7 @@ async def test_sensors( entry = registry.async_get("sensor.wled_rgb_light_wifi_rssi") assert entry assert entry.unique_id == "aabbccddeeff_wifi_rssi" - assert entry.entity_category == ENTITY_CATEGORY_DIAGNOSTIC + assert entry.entity_category is EntityCategory.DIAGNOSTIC state = hass.states.get("sensor.wled_rgb_light_wifi_channel") assert state @@ -159,7 +159,7 @@ async def test_sensors( entry = registry.async_get("sensor.wled_rgb_light_wifi_channel") assert entry assert entry.unique_id == "aabbccddeeff_wifi_channel" - assert entry.entity_category == ENTITY_CATEGORY_DIAGNOSTIC + assert entry.entity_category is EntityCategory.DIAGNOSTIC state = hass.states.get("sensor.wled_rgb_light_wifi_bssid") assert state @@ -170,7 +170,7 @@ async def test_sensors( entry = registry.async_get("sensor.wled_rgb_light_wifi_bssid") assert entry assert entry.unique_id == "aabbccddeeff_wifi_bssid" - assert entry.entity_category == ENTITY_CATEGORY_DIAGNOSTIC + assert entry.entity_category is EntityCategory.DIAGNOSTIC @pytest.mark.parametrize( diff --git a/tests/components/wled/test_switch.py b/tests/components/wled/test_switch.py index c47d7012f6e..2bf494284f5 100644 --- a/tests/components/wled/test_switch.py +++ b/tests/components/wled/test_switch.py @@ -16,7 +16,6 @@ from homeassistant.components.wled.const import ( from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_ICON, - ENTITY_CATEGORY_CONFIG, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, @@ -25,6 +24,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity import EntityCategory import homeassistant.util.dt as dt_util from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture @@ -47,7 +47,7 @@ async def test_switch_state( entry = entity_registry.async_get("switch.wled_rgb_light_nightlight") assert entry assert entry.unique_id == "aabbccddeeff_nightlight" - assert entry.entity_category == ENTITY_CATEGORY_CONFIG + assert entry.entity_category is EntityCategory.CONFIG state = hass.states.get("switch.wled_rgb_light_sync_send") assert state @@ -58,7 +58,7 @@ async def test_switch_state( entry = entity_registry.async_get("switch.wled_rgb_light_sync_send") assert entry assert entry.unique_id == "aabbccddeeff_sync_send" - assert entry.entity_category == ENTITY_CATEGORY_CONFIG + assert entry.entity_category is EntityCategory.CONFIG state = hass.states.get("switch.wled_rgb_light_sync_receive") assert state @@ -69,7 +69,7 @@ async def test_switch_state( entry = entity_registry.async_get("switch.wled_rgb_light_sync_receive") assert entry assert entry.unique_id == "aabbccddeeff_sync_receive" - assert entry.entity_category == ENTITY_CATEGORY_CONFIG + assert entry.entity_category is EntityCategory.CONFIG state = hass.states.get("switch.wled_rgb_light_reverse") assert state @@ -79,7 +79,7 @@ async def test_switch_state( entry = entity_registry.async_get("switch.wled_rgb_light_reverse") assert entry assert entry.unique_id == "aabbccddeeff_reverse_0" - assert entry.entity_category == ENTITY_CATEGORY_CONFIG + assert entry.entity_category is EntityCategory.CONFIG async def test_switch_change_state( From 58bcf275f526d02bc1c45ec2dc7e6b72be22777a Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 18 Dec 2021 17:46:16 +0100 Subject: [PATCH 0709/2644] Add xiaomi miio gateway hw version (#62274) --- homeassistant/components/xiaomi_miio/__init__.py | 5 ++--- homeassistant/components/xiaomi_miio/gateway.py | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 009b068a521..ea35d3d785f 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -395,8 +395,6 @@ async def async_setup_gateway_entry( raise ConfigEntryNotReady() from error gateway_info = gateway.gateway_info - gateway_model = f"{gateway_info.model}-{gateway_info.hardware_version}" - device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, @@ -404,8 +402,9 @@ async def async_setup_gateway_entry( identifiers={(DOMAIN, gateway_id)}, manufacturer="Xiaomi", name=name, - model=gateway_model, + model=gateway_info.model, sw_version=gateway_info.firmware_version, + hw_version=gateway_info.hardware_version, ) def update_data(): diff --git a/homeassistant/components/xiaomi_miio/gateway.py b/homeassistant/components/xiaomi_miio/gateway.py index af6ef2e29a5..27f426ff9d6 100644 --- a/homeassistant/components/xiaomi_miio/gateway.py +++ b/homeassistant/components/xiaomi_miio/gateway.py @@ -160,6 +160,7 @@ class XiaomiGatewayDevice(CoordinatorEntity, Entity): name=self._sub_device.name, model=self._sub_device.model, sw_version=self._sub_device.firmware_version, + hw_version=self._sub_device.zigbee_model, ) @property From 99d1e015ad8ad62f4fab93dbb26f7f8f0e9efd1c Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sat, 18 Dec 2021 14:50:49 -0600 Subject: [PATCH 0710/2644] Update rokuecp to 0.8.5 (#62285) --- homeassistant/components/roku/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json index 7756d917e73..9a427afa27a 100644 --- a/homeassistant/components/roku/manifest.json +++ b/homeassistant/components/roku/manifest.json @@ -2,7 +2,7 @@ "domain": "roku", "name": "Roku", "documentation": "https://www.home-assistant.io/integrations/roku", - "requirements": ["rokuecp==0.8.4"], + "requirements": ["rokuecp==0.8.5"], "homekit": { "models": ["3810X", "4660X", "7820X", "C105X", "C135X"] }, diff --git a/requirements_all.txt b/requirements_all.txt index a17452d7489..bae5422a74a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2085,7 +2085,7 @@ rjpl==0.3.6 rocketchat-API==0.6.1 # homeassistant.components.roku -rokuecp==0.8.4 +rokuecp==0.8.5 # homeassistant.components.roomba roombapy==1.6.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5d6d6e65547..21a8b5d1562 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1248,7 +1248,7 @@ rflink==0.0.58 ring_doorbell==0.7.1 # homeassistant.components.roku -rokuecp==0.8.4 +rokuecp==0.8.5 # homeassistant.components.roomba roombapy==1.6.4 From b05149fc28bd563501a1370ad543ecb7032c6880 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 18 Dec 2021 16:17:54 -0600 Subject: [PATCH 0711/2644] Fix Non-thread-safe operation in zwave node_added (#62287) --- homeassistant/components/zwave/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index f21b75aaffc..cd15f632d0b 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -493,7 +493,7 @@ async def async_setup_entry( # noqa: C901 await platform.async_add_entities([entity]) if entity.unique_id: - hass.async_add_job(_add_node_to_component()) + hass.create_task(_add_node_to_component()) return @callback From 4dc70536b6b15bd4c919ab0b9475b2fa7e87378e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 18 Dec 2021 17:14:41 -0600 Subject: [PATCH 0712/2644] Fix Non-thread-safe operation in rflink binary_sensor (#62286) --- homeassistant/components/rflink/binary_sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/rflink/binary_sensor.py b/homeassistant/components/rflink/binary_sensor.py index 77a8a522f65..82457bd767e 100644 --- a/homeassistant/components/rflink/binary_sensor.py +++ b/homeassistant/components/rflink/binary_sensor.py @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_FORCE_UPDATE, CONF_NAME, ) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv import homeassistant.helpers.event as evt @@ -81,6 +82,7 @@ class RflinkBinarySensor(RflinkDevice, BinarySensorEntity): if self._state and self._off_delay is not None: + @callback def off_delay_listener(now): """Switch device off after a delay.""" self._delay_listener = None From ba79de56abab4a23c684ca93e9a706fa12564ef9 Mon Sep 17 00:00:00 2001 From: micha91 Date: Sun, 19 Dec 2021 00:18:42 +0100 Subject: [PATCH 0713/2644] Move Device Class definition to Home Assistant for MusicCast Select Entities (#61218) * Add a device class mapping to the consts to map from capability IDs to HA device classes. * Use python3.8 compliant typing * Fix return type * Use relative import for musiccast const --- homeassistant/components/yamaha_musiccast/const.py | 11 +++++++++++ homeassistant/components/yamaha_musiccast/select.py | 8 +++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yamaha_musiccast/const.py b/homeassistant/components/yamaha_musiccast/const.py index 5384cc56694..470b3729c08 100644 --- a/homeassistant/components/yamaha_musiccast/const.py +++ b/homeassistant/components/yamaha_musiccast/const.py @@ -56,3 +56,14 @@ ENTITY_CATEGORY_MAPPING = { EntityType.DIAGNOSTIC: ENTITY_CATEGORY_DIAGNOSTIC, EntityType.SYSTEM: ENTITY_CATEGORY_SYSTEM, } + +DEVICE_CLASS_MAPPING = { + "DIMMER": "yamaha_musiccast__dimmer", + "zone_SLEEP": "yamaha_musiccast__zone_sleep", + "zone_TONE_CONTROL_mode": "yamaha_musiccast__zone_tone_control_mode", + "zone_SURR_DECODER_TYPE": "yamaha_musiccast__zone_surr_decoder_type", + "zone_EQUALIZER_mode": "yamaha_musiccast__zone_equalizer_mode", + "zone_LINK_AUDIO_QUALITY": "yamaha_musiccast__zone_link_audio_quality", + "zone_LINK_CONTROL": "yamaha_musiccast__zone_link_control", + "zone_LINK_AUDIO_DELAY": "yamaha_musiccast__zone_link_audio_delay", +} diff --git a/homeassistant/components/yamaha_musiccast/select.py b/homeassistant/components/yamaha_musiccast/select.py index 68be67eac10..d64682c8eb4 100644 --- a/homeassistant/components/yamaha_musiccast/select.py +++ b/homeassistant/components/yamaha_musiccast/select.py @@ -1,4 +1,5 @@ """The select entities for musiccast.""" +from __future__ import annotations from aiomusiccast.capabilities import OptionSetter @@ -8,6 +9,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN, MusicCastCapabilityEntity, MusicCastDataUpdateCoordinator +from .const import DEVICE_CLASS_MAPPING async def async_setup_entry( @@ -45,9 +47,9 @@ class SelectableCapapility(MusicCastCapabilityEntity, SelectEntity): await self.capability.set(value) @property - def device_class(self) -> str: - """Return the ID of the capability, to identify the entity for translations.""" - return f"{DOMAIN}__{self.capability.id.lower()}" + def device_class(self) -> str | None: + """Return the device class, to identify the entity for translations.""" + return DEVICE_CLASS_MAPPING.get(self.capability.id) @property def options(self): From 98c398cc37bc0f85f0e8baa819d324d1897a1040 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 18 Dec 2021 16:26:37 -0700 Subject: [PATCH 0714/2644] Bump aioridwell to 2021.12.2 (#62284) --- homeassistant/components/ridwell/__init__.py | 2 +- homeassistant/components/ridwell/manifest.json | 2 +- homeassistant/components/ridwell/sensor.py | 6 +++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/ridwell/__init__.py b/homeassistant/components/ridwell/__init__.py index 85d456271df..03b1d2237e0 100644 --- a/homeassistant/components/ridwell/__init__.py +++ b/homeassistant/components/ridwell/__init__.py @@ -5,8 +5,8 @@ import asyncio from datetime import timedelta from aioridwell import async_get_client -from aioridwell.client import RidwellAccount, RidwellPickupEvent from aioridwell.errors import InvalidCredentialsError, RidwellError +from aioridwell.model import RidwellAccount, RidwellPickupEvent from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform diff --git a/homeassistant/components/ridwell/manifest.json b/homeassistant/components/ridwell/manifest.json index 8c9b2e71304..4aed69a05f3 100644 --- a/homeassistant/components/ridwell/manifest.json +++ b/homeassistant/components/ridwell/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ridwell", "requirements": [ - "aioridwell==0.2.0" + "aioridwell==2021.12.2" ], "codeowners": [ "@bachya" diff --git a/homeassistant/components/ridwell/sensor.py b/homeassistant/components/ridwell/sensor.py index 269a08587c7..ddbf62b2356 100644 --- a/homeassistant/components/ridwell/sensor.py +++ b/homeassistant/components/ridwell/sensor.py @@ -5,7 +5,7 @@ from collections.abc import Mapping from datetime import date, datetime from typing import Any -from aioridwell.client import RidwellAccount, RidwellPickupEvent +from aioridwell.model import RidwellAccount, RidwellPickupEvent from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry @@ -58,13 +58,13 @@ class RidwellSensor(CoordinatorEntity, SensorEntity): attrs: dict[str, Any] = { ATTR_PICKUP_TYPES: {}, - ATTR_PICKUP_STATE: event.state, + ATTR_PICKUP_STATE: event.state.value, } for pickup in event.pickups: if pickup.name not in attrs[ATTR_PICKUP_TYPES]: attrs[ATTR_PICKUP_TYPES][pickup.name] = { - ATTR_CATEGORY: pickup.category, + ATTR_CATEGORY: pickup.category.value, ATTR_QUANTITY: pickup.quantity, } else: diff --git a/requirements_all.txt b/requirements_all.txt index bae5422a74a..f646f6d1469 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -249,7 +249,7 @@ aiopylgtv==0.4.0 aiorecollect==1.0.8 # homeassistant.components.ridwell -aioridwell==0.2.0 +aioridwell==2021.12.2 # homeassistant.components.shelly aioshelly==1.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 21a8b5d1562..55c1e9376ca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -179,7 +179,7 @@ aiopylgtv==0.4.0 aiorecollect==1.0.8 # homeassistant.components.ridwell -aioridwell==0.2.0 +aioridwell==2021.12.2 # homeassistant.components.shelly aioshelly==1.0.5 From d3710c7ba663c57bb7d9a25ee41634e4c33fa908 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 19 Dec 2021 00:14:45 +0000 Subject: [PATCH 0715/2644] [ci skip] Translation update --- .../components/airly/translations/ca.json | 2 +- .../components/vicare/translations/af.json | 13 ++++++++++ .../components/vicare/translations/ca.json | 26 +++++++++++++++++++ .../components/vicare/translations/hu.json | 26 +++++++++++++++++++ .../components/vicare/translations/ja.json | 26 +++++++++++++++++++ .../components/vicare/translations/ru.json | 26 +++++++++++++++++++ 6 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/vicare/translations/af.json create mode 100644 homeassistant/components/vicare/translations/ca.json create mode 100644 homeassistant/components/vicare/translations/hu.json create mode 100644 homeassistant/components/vicare/translations/ja.json create mode 100644 homeassistant/components/vicare/translations/ru.json diff --git a/homeassistant/components/airly/translations/ca.json b/homeassistant/components/airly/translations/ca.json index e76cec94f4c..0d5177df33e 100644 --- a/homeassistant/components/airly/translations/ca.json +++ b/homeassistant/components/airly/translations/ca.json @@ -15,7 +15,7 @@ "longitude": "Longitud", "name": "Nom" }, - "description": "Configura una integraci\u00f3 de qualitat d'aire Airly. Per generar la clau API, v\u00e9s a https://developer.airly.eu/register", + "description": "Configura la integraci\u00f3 de qualitat de l'aire Airly. Per generar la clau API, v\u00e9s a https://developer.airly.eu/register", "title": "Airly" } } diff --git a/homeassistant/components/vicare/translations/af.json b/homeassistant/components/vicare/translations/af.json new file mode 100644 index 00000000000..92321a6be86 --- /dev/null +++ b/homeassistant/components/vicare/translations/af.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u5bc6\u7801", + "scan_interval": "\u626b\u63cf\u95f4\u9694\u65f6\u95f4(\u79d2)", + "username": "\u7535\u5b50\u90ae\u7bb1" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vicare/translations/ca.json b/homeassistant/components/vicare/translations/ca.json new file mode 100644 index 00000000000..a850cdf27fa --- /dev/null +++ b/homeassistant/components/vicare/translations/ca.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3.", + "unknown": "Error inesperat" + }, + "error": { + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "client_id": "Clau API", + "heating_type": "Tipus d'escalfador", + "name": "Nom", + "password": "Contrasenya", + "scan_interval": "Interval d'escaneig (segons)", + "username": "Correu electr\u00f2nic" + }, + "description": "Configura la integraci\u00f3 ViCare. Per generar la clau API, v\u00e9s a https://developer.viessmann.com", + "title": "{name}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vicare/translations/hu.json b/homeassistant/components/vicare/translations/hu.json new file mode 100644 index 00000000000..4dca080307d --- /dev/null +++ b/homeassistant/components/vicare/translations/hu.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "error": { + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "client_id": "API kulcs", + "heating_type": "F\u0171t\u00e9s t\u00edpusa", + "name": "N\u00e9v", + "password": "Jelsz\u00f3", + "scan_interval": "Beolvas\u00e1si id\u0151k\u00f6z (m\u00e1sodperc)", + "username": "E-mail" + }, + "description": "A ViCare integr\u00e1ci\u00f3 be\u00e1ll\u00edt\u00e1sa. Az API-kulcs gener\u00e1l\u00e1s\u00e1hoz keresse fel a https://developer.viessmann.com webhelyet.", + "title": "{name}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vicare/translations/ja.json b/homeassistant/components/vicare/translations/ja.json new file mode 100644 index 00000000000..f88edf8f359 --- /dev/null +++ b/homeassistant/components/vicare/translations/ja.json @@ -0,0 +1,26 @@ +{ + "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", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "error": { + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "client_id": "API\u30ad\u30fc", + "heating_type": "\u6696\u623f(\u52a0\u71b1)\u30bf\u30a4\u30d7", + "name": "\u540d\u524d", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "scan_interval": "\u30b9\u30ad\u30e3\u30f3\u30a4\u30f3\u30bf\u30fc\u30d0\u30eb(\u79d2)", + "username": "E\u30e1\u30fc\u30eb" + }, + "description": "ViCare\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://developer.viessmann.com \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044", + "title": "{name}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vicare/translations/ru.json b/homeassistant/components/vicare/translations/ru.json new file mode 100644 index 00000000000..9ce8c37f956 --- /dev/null +++ b/homeassistant/components/vicare/translations/ru.json @@ -0,0 +1,26 @@ +{ + "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.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "error": { + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "client_id": "\u041a\u043b\u044e\u0447 API", + "heating_type": "\u0422\u0438\u043f \u043e\u0442\u043e\u043f\u043b\u0435\u043d\u0438\u044f", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0440\u043e\u0441\u0430 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", + "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 ViCare. \u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u043a\u043b\u044e\u0447\u0430 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 https://developer.viessmann.com.", + "title": "{name}" + } + } + } +} \ No newline at end of file From 931d51949debe2179fa74294b82cddd127c164ec Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 19 Dec 2021 02:07:04 +0100 Subject: [PATCH 0716/2644] Use new DeviceClass enums in homekit (#61665) Co-authored-by: epenet --- homeassistant/components/homekit/__init__.py | 30 ++++++------- .../components/homekit/accessories.py | 42 +++++++++---------- homeassistant/components/homekit/const.py | 12 ------ .../components/homekit/type_covers.py | 2 +- .../components/homekit/type_humidifiers.py | 15 +++---- .../components/homekit/type_sensors.py | 40 +++++++++--------- homeassistant/components/homekit/util.py | 4 +- tests/components/homekit/test_type_cameras.py | 6 ++- tests/components/homekit/test_type_sensors.py | 2 +- 9 files changed, 70 insertions(+), 83 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index cfa734559fc..2bc7da6d24a 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -12,16 +12,14 @@ import voluptuous as vol from homeassistant.components import device_automation, network, zeroconf from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_BATTERY_CHARGING, - DEVICE_CLASS_MOTION, - DEVICE_CLASS_OCCUPANCY, DOMAIN as BINARY_SENSOR_DOMAIN, + BinarySensorDeviceClass, ) from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN from homeassistant.components.http import HomeAssistantView from homeassistant.components.humidifier import DOMAIN as HUMIDIFIER_DOMAIN from homeassistant.components.network.const import MDNS_TARGET_IP -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( ATTR_BATTERY_CHARGING, @@ -35,8 +33,6 @@ from homeassistant.const import ( CONF_IP_ADDRESS, CONF_NAME, CONF_PORT, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_HUMIDITY, EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, SERVICE_RELOAD, @@ -669,11 +665,11 @@ class HomeKit: ent_reg = entity_registry.async_get(self.hass) device_lookup = ent_reg.async_get_device_class_lookup( { - (BINARY_SENSOR_DOMAIN, DEVICE_CLASS_BATTERY_CHARGING), - (BINARY_SENSOR_DOMAIN, DEVICE_CLASS_MOTION), - (BINARY_SENSOR_DOMAIN, DEVICE_CLASS_OCCUPANCY), - (SENSOR_DOMAIN, DEVICE_CLASS_BATTERY), - (SENSOR_DOMAIN, DEVICE_CLASS_HUMIDITY), + (BINARY_SENSOR_DOMAIN, BinarySensorDeviceClass.BATTERY_CHARGING), + (BINARY_SENSOR_DOMAIN, BinarySensorDeviceClass.MOTION), + (BINARY_SENSOR_DOMAIN, BinarySensorDeviceClass.OCCUPANCY), + (SENSOR_DOMAIN, SensorDeviceClass.BATTERY), + (SENSOR_DOMAIN, SensorDeviceClass.HUMIDITY), } ) @@ -860,14 +856,14 @@ class HomeKit: or ent_reg_ent.device_id is None or ent_reg_ent.device_id not in device_lookup or (ent_reg_ent.device_class or ent_reg_ent.original_device_class) - in (DEVICE_CLASS_BATTERY_CHARGING, DEVICE_CLASS_BATTERY) + in (BinarySensorDeviceClass.BATTERY_CHARGING, SensorDeviceClass.BATTERY) ): return if ATTR_BATTERY_CHARGING not in state.attributes: battery_charging_binary_sensor_entity_id = device_lookup[ ent_reg_ent.device_id - ].get((BINARY_SENSOR_DOMAIN, DEVICE_CLASS_BATTERY_CHARGING)) + ].get((BINARY_SENSOR_DOMAIN, BinarySensorDeviceClass.BATTERY_CHARGING)) if battery_charging_binary_sensor_entity_id: self._config.setdefault(state.entity_id, {}).setdefault( CONF_LINKED_BATTERY_CHARGING_SENSOR, @@ -876,7 +872,7 @@ class HomeKit: if ATTR_BATTERY_LEVEL not in state.attributes: battery_sensor_entity_id = device_lookup[ent_reg_ent.device_id].get( - (SENSOR_DOMAIN, DEVICE_CLASS_BATTERY) + (SENSOR_DOMAIN, SensorDeviceClass.BATTERY) ) if battery_sensor_entity_id: self._config.setdefault(state.entity_id, {}).setdefault( @@ -885,7 +881,7 @@ class HomeKit: if state.entity_id.startswith(f"{CAMERA_DOMAIN}."): motion_binary_sensor_entity_id = device_lookup[ent_reg_ent.device_id].get( - (BINARY_SENSOR_DOMAIN, DEVICE_CLASS_MOTION) + (BINARY_SENSOR_DOMAIN, BinarySensorDeviceClass.MOTION) ) if motion_binary_sensor_entity_id: self._config.setdefault(state.entity_id, {}).setdefault( @@ -893,7 +889,7 @@ class HomeKit: motion_binary_sensor_entity_id, ) doorbell_binary_sensor_entity_id = device_lookup[ent_reg_ent.device_id].get( - (BINARY_SENSOR_DOMAIN, DEVICE_CLASS_OCCUPANCY) + (BINARY_SENSOR_DOMAIN, BinarySensorDeviceClass.OCCUPANCY) ) if doorbell_binary_sensor_entity_id: self._config.setdefault(state.entity_id, {}).setdefault( @@ -904,7 +900,7 @@ class HomeKit: if state.entity_id.startswith(f"{HUMIDIFIER_DOMAIN}."): current_humidity_sensor_entity_id = device_lookup[ ent_reg_ent.device_id - ].get((SENSOR_DOMAIN, DEVICE_CLASS_HUMIDITY)) + ].get((SENSOR_DOMAIN, SensorDeviceClass.HUMIDITY)) if current_humidity_sensor_entity_id: self._config.setdefault(state.entity_id, {}).setdefault( CONF_LINKED_HUMIDITY_SENSOR, diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index ca12daa33b3..6af4b4a8d89 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -7,13 +7,9 @@ from pyhap.const import CATEGORY_OTHER from pyhap.util import callback as pyhap_callback from homeassistant.components import cover -from homeassistant.components.cover import ( - DEVICE_CLASS_GARAGE, - DEVICE_CLASS_GATE, - DEVICE_CLASS_WINDOW, -) -from homeassistant.components.media_player import DEVICE_CLASS_TV +from homeassistant.components.media_player import MediaPlayerDeviceClass from homeassistant.components.remote import SUPPORT_ACTIVITY +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, @@ -27,11 +23,6 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, CONF_TYPE, - DEVICE_CLASS_CO, - DEVICE_CLASS_CO2, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_TEMPERATURE, LIGHT_LUX, PERCENTAGE, STATE_ON, @@ -58,7 +49,6 @@ from .const import ( CONF_LINKED_BATTERY_SENSOR, CONF_LOW_BATTERY_THRESHOLD, DEFAULT_LOW_BATTERY_THRESHOLD, - DEVICE_CLASS_PM25, DOMAIN, EVENT_HOMEKIT_CHANGED, HK_CHARGING, @@ -126,12 +116,17 @@ def get_accessory(hass, driver, state, aid, config): # noqa: C901 elif state.domain == "cover": device_class = state.attributes.get(ATTR_DEVICE_CLASS) - if device_class in (DEVICE_CLASS_GARAGE, DEVICE_CLASS_GATE) and features & ( - cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE + if ( + device_class + in ( + cover.CoverDeviceClass.GARAGE, + cover.CoverDeviceClass.GATE, + ) + and features & (cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE) ): a_type = "GarageDoorOpener" elif ( - device_class == DEVICE_CLASS_WINDOW + device_class == cover.CoverDeviceClass.WINDOW and features & cover.SUPPORT_SET_POSITION ): a_type = "Window" @@ -161,7 +156,7 @@ def get_accessory(hass, driver, state, aid, config): # noqa: C901 device_class = state.attributes.get(ATTR_DEVICE_CLASS) feature_list = config.get(CONF_FEATURE_LIST, []) - if device_class == DEVICE_CLASS_TV: + if device_class == MediaPlayerDeviceClass.TV: a_type = "TelevisionMediaPlayer" elif validate_media_player_features(state, feature_list): a_type = "MediaPlayer" @@ -170,20 +165,23 @@ def get_accessory(hass, driver, state, aid, config): # noqa: C901 device_class = state.attributes.get(ATTR_DEVICE_CLASS) unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - if device_class == DEVICE_CLASS_TEMPERATURE or unit in ( + if device_class == SensorDeviceClass.TEMPERATURE or unit in ( TEMP_CELSIUS, TEMP_FAHRENHEIT, ): a_type = "TemperatureSensor" - elif device_class == DEVICE_CLASS_HUMIDITY and unit == PERCENTAGE: + elif device_class == SensorDeviceClass.HUMIDITY and unit == PERCENTAGE: a_type = "HumiditySensor" - elif device_class == DEVICE_CLASS_PM25 or DEVICE_CLASS_PM25 in state.entity_id: + elif ( + device_class == SensorDeviceClass.PM25 + or SensorDeviceClass.PM25 in state.entity_id + ): a_type = "AirQualitySensor" - elif device_class == DEVICE_CLASS_CO: + elif device_class == SensorDeviceClass.CO: a_type = "CarbonMonoxideSensor" - elif device_class == DEVICE_CLASS_CO2 or "co2" in state.entity_id: + elif device_class == SensorDeviceClass.CO2 or "co2" in state.entity_id: a_type = "CarbonDioxideSensor" - elif device_class == DEVICE_CLASS_ILLUMINANCE or unit in ("lm", LIGHT_LUX): + elif device_class == SensorDeviceClass.ILLUMINANCE or unit in ("lm", LIGHT_LUX): a_type = "LightSensor" elif state.domain == "switch": diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 8494327bb68..1dd40d0fa31 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -234,18 +234,6 @@ PROP_MIN_STEP = "minStep" PROP_CELSIUS = {"minValue": -273, "maxValue": 999} PROP_VALID_VALUES = "ValidValues" -# #### Device Classes #### -DEVICE_CLASS_DOOR = "door" -DEVICE_CLASS_GARAGE_DOOR = "garage_door" -DEVICE_CLASS_GAS = "gas" -DEVICE_CLASS_MOISTURE = "moisture" -DEVICE_CLASS_MOTION = "motion" -DEVICE_CLASS_OCCUPANCY = "occupancy" -DEVICE_CLASS_OPENING = "opening" -DEVICE_CLASS_PM25 = "pm25" -DEVICE_CLASS_SMOKE = "smoke" -DEVICE_CLASS_WINDOW = "window" - # #### Thresholds #### THRESHOLD_CO = 25 THRESHOLD_CO2 = 1000 diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index b9153ef1372..51d0480bde3 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -319,7 +319,7 @@ class OpeningDevice(OpeningDeviceBase, HomeAccessory): @TYPES.register("Window") class Window(OpeningDevice): - """Generate a Window accessory for a cover entity with DEVICE_CLASS_WINDOW. + """Generate a Window accessory for a cover entity with WINDOW device class. The entity must support: set_cover_position. """ diff --git a/homeassistant/components/homekit/type_humidifiers.py b/homeassistant/components/homekit/type_humidifiers.py index 2f4866e395a..a468efd42b4 100644 --- a/homeassistant/components/homekit/type_humidifiers.py +++ b/homeassistant/components/homekit/type_humidifiers.py @@ -3,14 +3,13 @@ import logging from pyhap.const import CATEGORY_HUMIDIFIER +from homeassistant.components.humidifier import HumidifierDeviceClass from homeassistant.components.humidifier.const import ( ATTR_HUMIDITY, ATTR_MAX_HUMIDITY, ATTR_MIN_HUMIDITY, DEFAULT_MAX_HUMIDITY, DEFAULT_MIN_HUMIDITY, - DEVICE_CLASS_DEHUMIDIFIER, - DEVICE_CLASS_HUMIDIFIER, DOMAIN, SERVICE_SET_HUMIDITY, ) @@ -46,13 +45,13 @@ HC_HUMIDIFIER = 1 HC_DEHUMIDIFIER = 2 HC_HASS_TO_HOMEKIT_DEVICE_CLASS = { - DEVICE_CLASS_HUMIDIFIER: HC_HUMIDIFIER, - DEVICE_CLASS_DEHUMIDIFIER: HC_DEHUMIDIFIER, + HumidifierDeviceClass.HUMIDIFIER: HC_HUMIDIFIER, + HumidifierDeviceClass.DEHUMIDIFIER: HC_DEHUMIDIFIER, } HC_HASS_TO_HOMEKIT_DEVICE_CLASS_NAME = { - DEVICE_CLASS_HUMIDIFIER: "Humidifier", - DEVICE_CLASS_DEHUMIDIFIER: "Dehumidifier", + HumidifierDeviceClass.HUMIDIFIER: "Humidifier", + HumidifierDeviceClass.DEHUMIDIFIER: "Dehumidifier", } HC_DEVICE_CLASS_TO_TARGET_CHAR = { @@ -75,7 +74,9 @@ class HumidifierDehumidifier(HomeAccessory): super().__init__(*args, category=CATEGORY_HUMIDIFIER) self.chars = [] state = self.hass.states.get(self.entity_id) - device_class = state.attributes.get(ATTR_DEVICE_CLASS, DEVICE_CLASS_HUMIDIFIER) + device_class = state.attributes.get( + ATTR_DEVICE_CLASS, HumidifierDeviceClass.HUMIDIFIER + ) self._hk_device_class = HC_HASS_TO_HOMEKIT_DEVICE_CLASS[device_class] self._target_humidity_char_name = HC_DEVICE_CLASS_TO_TARGET_CHAR[ diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 148b705b23f..598d49155ec 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -7,6 +7,7 @@ from typing import NamedTuple from pyhap.const import CATEGORY_SENSOR +from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, @@ -36,15 +37,6 @@ from .const import ( CHAR_MOTION_DETECTED, CHAR_OCCUPANCY_DETECTED, CHAR_SMOKE_DETECTED, - DEVICE_CLASS_DOOR, - DEVICE_CLASS_GARAGE_DOOR, - DEVICE_CLASS_GAS, - DEVICE_CLASS_MOISTURE, - DEVICE_CLASS_MOTION, - DEVICE_CLASS_OCCUPANCY, - DEVICE_CLASS_OPENING, - DEVICE_CLASS_SMOKE, - DEVICE_CLASS_WINDOW, PROP_CELSIUS, SERV_AIR_QUALITY_SENSOR, SERV_CARBON_DIOXIDE_SENSOR, @@ -78,17 +70,27 @@ BINARY_SENSOR_SERVICE_MAP: dict[str, SI] = { SERV_CARBON_MONOXIDE_SENSOR, CHAR_CARBON_MONOXIDE_DETECTED, int ), DEVICE_CLASS_CO2: SI(SERV_CARBON_DIOXIDE_SENSOR, CHAR_CARBON_DIOXIDE_DETECTED, int), - DEVICE_CLASS_DOOR: SI(SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, int), - DEVICE_CLASS_GARAGE_DOOR: SI(SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, int), - DEVICE_CLASS_GAS: SI( + BinarySensorDeviceClass.DOOR: SI( + SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, int + ), + BinarySensorDeviceClass.GARAGE_DOOR: SI( + SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, int + ), + BinarySensorDeviceClass.GAS: SI( SERV_CARBON_MONOXIDE_SENSOR, CHAR_CARBON_MONOXIDE_DETECTED, int ), - DEVICE_CLASS_MOISTURE: SI(SERV_LEAK_SENSOR, CHAR_LEAK_DETECTED, int), - DEVICE_CLASS_MOTION: SI(SERV_MOTION_SENSOR, CHAR_MOTION_DETECTED, bool), - DEVICE_CLASS_OCCUPANCY: SI(SERV_OCCUPANCY_SENSOR, CHAR_OCCUPANCY_DETECTED, int), - DEVICE_CLASS_OPENING: SI(SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, int), - DEVICE_CLASS_SMOKE: SI(SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED, int), - DEVICE_CLASS_WINDOW: SI(SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, int), + BinarySensorDeviceClass.MOISTURE: SI(SERV_LEAK_SENSOR, CHAR_LEAK_DETECTED, int), + BinarySensorDeviceClass.MOTION: SI(SERV_MOTION_SENSOR, CHAR_MOTION_DETECTED, bool), + BinarySensorDeviceClass.OCCUPANCY: SI( + SERV_OCCUPANCY_SENSOR, CHAR_OCCUPANCY_DETECTED, int + ), + BinarySensorDeviceClass.OPENING: SI( + SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, int + ), + BinarySensorDeviceClass.SMOKE: SI(SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED, int), + BinarySensorDeviceClass.WINDOW: SI( + SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, int + ), } @@ -284,7 +286,7 @@ class BinarySensor(HomeAccessory): service_char = ( BINARY_SENSOR_SERVICE_MAP[device_class] if device_class in BINARY_SENSOR_SERVICE_MAP - else BINARY_SENSOR_SERVICE_MAP[DEVICE_CLASS_OCCUPANCY] + else BINARY_SENSOR_SERVICE_MAP[BinarySensorDeviceClass.OCCUPANCY] ) self.format = service_char.format diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 11b0f6cf925..894bfcf8985 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -14,8 +14,8 @@ from homeassistant.components import binary_sensor, media_player, sensor from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.components.media_player import ( - DEVICE_CLASS_TV, DOMAIN as MEDIA_PLAYER_DOMAIN, + MediaPlayerDeviceClass, ) from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN, SUPPORT_ACTIVITY from homeassistant.const import ( @@ -502,7 +502,7 @@ def state_needs_accessory_mode(state): return ( state.domain == MEDIA_PLAYER_DOMAIN - and state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TV + and state.attributes.get(ATTR_DEVICE_CLASS) == MediaPlayerDeviceClass.TV or state.domain == REMOTE_DOMAIN and state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) & SUPPORT_ACTIVITY ) diff --git a/tests/components/homekit/test_type_cameras.py b/tests/components/homekit/test_type_cameras.py index 05c809910cf..ba2dadc2a19 100644 --- a/tests/components/homekit/test_type_cameras.py +++ b/tests/components/homekit/test_type_cameras.py @@ -8,6 +8,10 @@ from pyhap.accessory_driver import AccessoryDriver import pytest from homeassistant.components import camera, ffmpeg +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_MOTION, + DEVICE_CLASS_OCCUPANCY, +) from homeassistant.components.camera.img_util import TurboJPEGSingleton from homeassistant.components.homekit.accessories import HomeBridge from homeassistant.components.homekit.const import ( @@ -20,8 +24,6 @@ from homeassistant.components.homekit.const import ( CONF_STREAM_SOURCE, CONF_SUPPORT_AUDIO, CONF_VIDEO_CODEC, - DEVICE_CLASS_MOTION, - DEVICE_CLASS_OCCUPANCY, SERV_DOORBELL, SERV_MOTION_SENSOR, SERV_STATELESS_PROGRAMMABLE_SWITCH, diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index 7ad48bfbce6..98e44de7575 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -1,7 +1,7 @@ """Test different accessory types: Sensors.""" +from homeassistant.components.binary_sensor import DEVICE_CLASS_MOTION from homeassistant.components.homekit import get_accessory from homeassistant.components.homekit.const import ( - DEVICE_CLASS_MOTION, PROP_CELSIUS, THRESHOLD_CO, THRESHOLD_CO2, From f280b03df8294fdcd08779441eb4be2c08966d8a Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Sun, 19 Dec 2021 16:41:32 +1100 Subject: [PATCH 0717/2644] Update async-upnp-client library to 0.23.1 (#62298) --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/components/yeelight/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index fb942717817..ecc3cd4256d 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -3,7 +3,7 @@ "name": "DLNA Digital Media Renderer", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", - "requirements": ["async-upnp-client==0.23.0"], + "requirements": ["async-upnp-client==0.23.1"], "dependencies": ["ssdp"], "ssdp": [ { diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index c11138e0816..e95c0e13887 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -2,7 +2,7 @@ "domain": "ssdp", "name": "Simple Service Discovery Protocol (SSDP)", "documentation": "https://www.home-assistant.io/integrations/ssdp", - "requirements": ["async-upnp-client==0.23.0"], + "requirements": ["async-upnp-client==0.23.1"], "dependencies": ["network"], "after_dependencies": ["zeroconf"], "codeowners": [], diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 87c40346fed..2644a91b20f 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -3,7 +3,7 @@ "name": "UPnP/IGD", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/upnp", - "requirements": ["async-upnp-client==0.23.0"], + "requirements": ["async-upnp-client==0.23.1"], "dependencies": ["network", "ssdp"], "codeowners": ["@StevenLooman","@ehendrix23"], "ssdp": [ diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 2e958c7a621..7cd1fe09dba 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -2,7 +2,7 @@ "domain": "yeelight", "name": "Yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight", - "requirements": ["yeelight==0.7.8", "async-upnp-client==0.23.0"], + "requirements": ["yeelight==0.7.8", "async-upnp-client==0.23.1"], "codeowners": ["@zewelor", "@shenxn", "@starkillerOG"], "config_flow": true, "dependencies": ["network"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9d1c3083706..216afd133e2 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodiscover==1.4.5 aiohttp==3.8.1 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.23.0 +async-upnp-client==0.23.1 async_timeout==4.0.0 atomicwrites==1.4.0 attrs==21.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index f646f6d1469..89863db2b96 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -342,7 +342,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.23.0 +async-upnp-client==0.23.1 # homeassistant.components.supla asyncpysupla==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 55c1e9376ca..ddb287b04b8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -242,7 +242,7 @@ arcam-fmj==0.12.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.23.0 +async-upnp-client==0.23.1 # homeassistant.components.aurora auroranoaa==0.0.2 From 6fd617a89ed2a33dc5660c2106ed4cacf23b4b03 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 18 Dec 2021 21:41:59 -0800 Subject: [PATCH 0718/2644] Bump ring to 0.7.2 (#62299) --- homeassistant/components/ring/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index 527fb143aff..3e745dc2d4b 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -2,7 +2,7 @@ "domain": "ring", "name": "Ring", "documentation": "https://www.home-assistant.io/integrations/ring", - "requirements": ["ring_doorbell==0.7.1"], + "requirements": ["ring_doorbell==0.7.2"], "dependencies": ["ffmpeg"], "codeowners": ["@balloob"], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 89863db2b96..fbd49ab7d0e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2073,7 +2073,7 @@ rfk101py==0.0.1 rflink==0.0.58 # homeassistant.components.ring -ring_doorbell==0.7.1 +ring_doorbell==0.7.2 # homeassistant.components.fleetgo ritassist==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ddb287b04b8..fa55a57bf77 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1245,7 +1245,7 @@ restrictedpython==5.2 rflink==0.0.58 # homeassistant.components.ring -ring_doorbell==0.7.1 +ring_doorbell==0.7.2 # homeassistant.components.roku rokuecp==0.8.5 From 8d6763eaad5d364b4ae24774caf1684d7bc36b1d Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Sat, 18 Dec 2021 21:43:31 -0800 Subject: [PATCH 0719/2644] Add wemo config_flow test to get 100% coverage (#62158) --- tests/components/wemo/test_config_flow.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 tests/components/wemo/test_config_flow.py diff --git a/tests/components/wemo/test_config_flow.py b/tests/components/wemo/test_config_flow.py new file mode 100644 index 00000000000..81040e44c9c --- /dev/null +++ b/tests/components/wemo/test_config_flow.py @@ -0,0 +1,22 @@ +"""Tests for Wemo config flow.""" + +from homeassistant import data_entry_flow +from homeassistant.components.wemo.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.core import HomeAssistant + +from tests.common import patch + + +async def test_not_discovered(hass: HomeAssistant) -> None: + """Test setting up with no devices discovered.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + + with patch("homeassistant.components.wemo.config_flow.pywemo") as mock_pywemo: + mock_pywemo.discover_devices.return_value = [] + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "no_devices_found" From a40549c1b999a520d9914fad456181a1c3f7f0d8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 18 Dec 2021 23:53:49 -0600 Subject: [PATCH 0720/2644] Fix hw_version not updating from an entity device_info (#62254) --- homeassistant/helpers/entity_platform.py | 1 + tests/helpers/test_entity_platform.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index d8cb8477f11..799b209f16e 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -474,6 +474,7 @@ class EntityPlatform: "name", "suggested_area", "sw_version", + "hw_version", "via_device", ): if key in device_info: diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 7626bedc13d..9aa0a849e5a 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -838,6 +838,7 @@ async def test_device_info_called(hass): "model": "test-model", "name": "test-name", "sw_version": "test-sw", + "hw_version": "test-hw", "suggested_area": "Heliport", "entry_type": dr.DeviceEntryType.SERVICE, "via_device": ("hue", "via-id"), @@ -869,6 +870,7 @@ async def test_device_info_called(hass): assert device.name == "test-name" assert device.suggested_area == "Heliport" assert device.sw_version == "test-sw" + assert device.hw_version == "test-hw" assert device.via_device_id == via.id From a4c101b021c14ee86647bdf0d37ceb458a156104 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 18 Dec 2021 23:55:57 -0600 Subject: [PATCH 0721/2644] Add network support to flux_led discovery (#61132) --- homeassistant/components/flux_led/__init__.py | 79 +++------------- .../components/flux_led/config_flow.py | 13 ++- homeassistant/components/flux_led/const.py | 1 - .../components/flux_led/discovery.py | 91 +++++++++++++++++++ .../components/flux_led/manifest.json | 1 + tests/components/flux_led/__init__.py | 4 +- tests/components/flux_led/conftest.py | 22 +++++ tests/components/flux_led/test_init.py | 61 +++++++++++-- 8 files changed, 185 insertions(+), 87 deletions(-) create mode 100644 homeassistant/components/flux_led/discovery.py diff --git a/homeassistant/components/flux_led/__init__.py b/homeassistant/components/flux_led/__init__.py index cfb39a9f8f1..a8836a61b23 100644 --- a/homeassistant/components/flux_led/__init__.py +++ b/homeassistant/components/flux_led/__init__.py @@ -1,15 +1,13 @@ """The Flux LED/MagicLight integration.""" from __future__ import annotations -import asyncio from datetime import timedelta import logging from typing import Any, Final from flux_led import DeviceType from flux_led.aio import AIOWifiLedBulb -from flux_led.aioscanner import AIOBulbScanner -from flux_led.const import ATTR_ID, ATTR_IPADDR, ATTR_MODEL, ATTR_MODEL_DESCRIPTION +from flux_led.const import ATTR_ID from flux_led.scanner import FluxLEDDiscovery from homeassistant import config_entries @@ -33,11 +31,16 @@ from .const import ( DISCOVER_SCAN_TIMEOUT, DOMAIN, FLUX_LED_DISCOVERY, - FLUX_LED_DISCOVERY_LOCK, FLUX_LED_EXCEPTIONS, SIGNAL_STATE_UPDATED, STARTUP_SCAN_TIMEOUT, ) +from .discovery import ( + async_discover_device, + async_discover_devices, + async_name_from_discovery, + async_trigger_discovery, +) _LOGGER = logging.getLogger(__name__) @@ -55,18 +58,6 @@ def async_wifi_bulb_for_host(host: str) -> AIOWifiLedBulb: return AIOWifiLedBulb(host) -@callback -def async_name_from_discovery(device: FluxLEDDiscovery) -> str: - """Convert a flux_led discovery to a human readable name.""" - mac_address = device[ATTR_ID] - if mac_address is None: - return device[ATTR_IPADDR] - short_mac = mac_address[-6:] - if device[ATTR_MODEL_DESCRIPTION]: - return f"{device[ATTR_MODEL_DESCRIPTION]} {short_mac}" - return f"{device[ATTR_MODEL]} {short_mac}" - - @callback def async_update_entry_from_discovery( hass: HomeAssistant, entry: config_entries.ConfigEntry, device: FluxLEDDiscovery @@ -83,52 +74,6 @@ def async_update_entry_from_discovery( ) -async def async_discover_devices( - hass: HomeAssistant, timeout: int, address: str | None = None -) -> list[FluxLEDDiscovery]: - """Discover flux led devices.""" - domain_data = hass.data.setdefault(DOMAIN, {}) - if FLUX_LED_DISCOVERY_LOCK not in domain_data: - domain_data[FLUX_LED_DISCOVERY_LOCK] = asyncio.Lock() - async with domain_data[FLUX_LED_DISCOVERY_LOCK]: - scanner = AIOBulbScanner() - try: - discovered = await scanner.async_scan(timeout=timeout, address=address) - except OSError as ex: - _LOGGER.debug("Scanning failed with error: %s", ex) - return [] - else: - return discovered - - -async def async_discover_device( - hass: HomeAssistant, host: str -) -> FluxLEDDiscovery | None: - """Direct discovery at a single ip instead of broadcast.""" - # If we are missing the unique_id we should be able to fetch it - # from the device by doing a directed discovery at the host only - for device in await async_discover_devices(hass, DISCOVER_SCAN_TIMEOUT, host): - if device[ATTR_IPADDR] == host: - return device - return None - - -@callback -def async_trigger_discovery( - hass: HomeAssistant, - discovered_devices: list[FluxLEDDiscovery], -) -> None: - """Trigger config flows for discovered devices.""" - for device in discovered_devices: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_DISCOVERY}, - data={**device}, - ) - ) - - async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the flux_led component.""" domain_data = hass.data.setdefault(DOMAIN, {}) @@ -173,11 +118,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady( str(ex) or f"Timed out trying to connect to {device.ipaddr}" ) from ex + coordinator = FluxLedUpdateCoordinator(hass, device) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms( - entry, PLATFORMS_BY_TYPE[device.device_type] - ) + platforms = PLATFORMS_BY_TYPE[device.device_type] + hass.config_entries.async_setup_platforms(entry, platforms) entry.async_on_unload(entry.add_update_listener(async_update_listener)) return True @@ -188,8 +133,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device: AIOWifiLedBulb = hass.data[DOMAIN][entry.entry_id].device platforms = PLATFORMS_BY_TYPE[device.device_type] if unload_ok := await hass.config_entries.async_unload_platforms(entry, platforms): - coordinator = hass.data[DOMAIN].pop(entry.entry_id) - await coordinator.device.async_stop() + del hass.data[DOMAIN][entry.entry_id] + await device.async_stop() return unload_ok diff --git a/homeassistant/components/flux_led/config_flow.py b/homeassistant/components/flux_led/config_flow.py index f4ef6a9b290..aeaa5a87271 100644 --- a/homeassistant/components/flux_led/config_flow.py +++ b/homeassistant/components/flux_led/config_flow.py @@ -16,13 +16,7 @@ from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import device_registry as dr from homeassistant.helpers.typing import DiscoveryInfoType -from . import ( - async_discover_device, - async_discover_devices, - async_name_from_discovery, - async_update_entry_from_discovery, - async_wifi_bulb_for_host, -) +from . import async_update_entry_from_discovery, async_wifi_bulb_for_host from .const import ( CONF_CUSTOM_EFFECT_COLORS, CONF_CUSTOM_EFFECT_SPEED_PCT, @@ -35,6 +29,11 @@ from .const import ( TRANSITION_JUMP, TRANSITION_STROBE, ) +from .discovery import ( + async_discover_device, + async_discover_devices, + async_name_from_discovery, +) CONF_DEVICE: Final = "device" diff --git a/homeassistant/components/flux_led/const.py b/homeassistant/components/flux_led/const.py index 88c50402a05..639bac7165e 100644 --- a/homeassistant/components/flux_led/const.py +++ b/homeassistant/components/flux_led/const.py @@ -39,7 +39,6 @@ DEFAULT_SCAN_INTERVAL: Final = 5 DEFAULT_EFFECT_SPEED: Final = 50 FLUX_LED_DISCOVERY: Final = "flux_led_discovery" -FLUX_LED_DISCOVERY_LOCK: Final = "flux_led_discovery_lock" FLUX_LED_EXCEPTIONS: Final = ( asyncio.TimeoutError, diff --git a/homeassistant/components/flux_led/discovery.py b/homeassistant/components/flux_led/discovery.py new file mode 100644 index 00000000000..71396623f95 --- /dev/null +++ b/homeassistant/components/flux_led/discovery.py @@ -0,0 +1,91 @@ +"""The Flux LED/MagicLight integration discovery.""" +from __future__ import annotations + +import asyncio +import logging + +from flux_led.aioscanner import AIOBulbScanner +from flux_led.const import ATTR_ID, ATTR_IPADDR, ATTR_MODEL, ATTR_MODEL_DESCRIPTION +from flux_led.scanner import FluxLEDDiscovery + +from homeassistant import config_entries +from homeassistant.components import network +from homeassistant.core import HomeAssistant, callback + +from .const import DISCOVER_SCAN_TIMEOUT, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +@callback +def async_name_from_discovery(device: FluxLEDDiscovery) -> str: + """Convert a flux_led discovery to a human readable name.""" + mac_address = device[ATTR_ID] + if mac_address is None: + return device[ATTR_IPADDR] + short_mac = mac_address[-6:] + if device[ATTR_MODEL_DESCRIPTION]: + return f"{device[ATTR_MODEL_DESCRIPTION]} {short_mac}" + return f"{device[ATTR_MODEL]} {short_mac}" + + +async def async_discover_devices( + hass: HomeAssistant, timeout: int, address: str | None = None +) -> list[FluxLEDDiscovery]: + """Discover flux led devices.""" + if address: + targets = [address] + else: + targets = [ + str(address) + for address in await network.async_get_ipv4_broadcast_addresses(hass) + ] + + scanner = AIOBulbScanner() + for idx, discovered in enumerate( + await asyncio.gather( + *[ + scanner.async_scan(timeout=timeout, address=address) + for address in targets + ], + return_exceptions=True, + ) + ): + if isinstance(discovered, Exception): + _LOGGER.debug("Scanning %s failed with error: %s", targets[idx], discovered) + continue + + if not address: + return scanner.getBulbInfo() + + return [ + device for device in scanner.getBulbInfo() if device[ATTR_IPADDR] == address + ] + + +async def async_discover_device( + hass: HomeAssistant, host: str +) -> FluxLEDDiscovery | None: + """Direct discovery at a single ip instead of broadcast.""" + # If we are missing the unique_id we should be able to fetch it + # from the device by doing a directed discovery at the host only + for device in await async_discover_devices(hass, DISCOVER_SCAN_TIMEOUT, host): + if device[ATTR_IPADDR] == host: + return device + return None + + +@callback +def async_trigger_discovery( + hass: HomeAssistant, + discovered_devices: list[FluxLEDDiscovery], +) -> None: + """Trigger config flows for discovered devices.""" + for device in discovered_devices: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DISCOVERY}, + data={**device}, + ) + ) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 34e642b4e1f..2be5d11c25f 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -2,6 +2,7 @@ "domain": "flux_led", "name": "Magic Home", "config_flow": true, + "dependencies": ["network"], "documentation": "https://www.home-assistant.io/integrations/flux_led", "requirements": ["flux_led==0.26.15"], "quality_scale": "platinum", diff --git a/tests/components/flux_led/__init__.py b/tests/components/flux_led/__init__.py index 6bfe02990a2..c37caec4956 100644 --- a/tests/components/flux_led/__init__.py +++ b/tests/components/flux_led/__init__.py @@ -168,10 +168,10 @@ def _patch_discovery(device=None, no_device=False): @contextmanager def _patcher(): with patch( - "homeassistant.components.flux_led.AIOBulbScanner.async_scan", + "homeassistant.components.flux_led.discovery.AIOBulbScanner.async_scan", new=_discovery, ), patch( - "homeassistant.components.flux_led.AIOBulbScanner.getBulbInfo", + "homeassistant.components.flux_led.discovery.AIOBulbScanner.getBulbInfo", return_value=[] if no_device else [device or FLUX_DISCOVERY], ): yield diff --git a/tests/components/flux_led/conftest.py b/tests/components/flux_led/conftest.py index abac297da2d..2a67c7b46f7 100644 --- a/tests/components/flux_led/conftest.py +++ b/tests/components/flux_led/conftest.py @@ -1,5 +1,7 @@ """Tests for the flux_led integration.""" +from unittest.mock import patch + import pytest from tests.common import mock_device_registry @@ -9,3 +11,23 @@ from tests.common import mock_device_registry def device_reg_fixture(hass): """Return an empty, loaded, registry.""" return mock_device_registry(hass) + + +@pytest.fixture +def mock_single_broadcast_address(): + """Mock network's async_async_get_ipv4_broadcast_addresses.""" + with patch( + "homeassistant.components.network.async_get_ipv4_broadcast_addresses", + return_value={"10.255.255.255"}, + ): + yield + + +@pytest.fixture +def mock_multiple_broadcast_addresses(): + """Mock network's async_async_get_ipv4_broadcast_addresses to return multiple addresses.""" + with patch( + "homeassistant.components.network.async_get_ipv4_broadcast_addresses", + return_value={"10.255.255.255", "192.168.0.255"}, + ): + yield diff --git a/tests/components/flux_led/test_init.py b/tests/components/flux_led/test_init.py index 23a238fa812..3d9be823b6b 100644 --- a/tests/components/flux_led/test_init.py +++ b/tests/components/flux_led/test_init.py @@ -3,6 +3,8 @@ from __future__ import annotations from unittest.mock import patch +from flux_led.aioscanner import AIOBulbScanner +from flux_led.scanner import FluxLEDDiscovery import pytest from homeassistant.components import flux_led @@ -26,23 +28,50 @@ from . import ( from tests.common import MockConfigEntry, async_fire_time_changed +@pytest.mark.usefixtures("mock_single_broadcast_address") async def test_configuring_flux_led_causes_discovery(hass: HomeAssistant) -> None: """Test that specifying empty config does discovery.""" with patch( - "homeassistant.components.flux_led.AIOBulbScanner.async_scan" + "homeassistant.components.flux_led.discovery.AIOBulbScanner.async_scan" + ) as scan, patch( + "homeassistant.components.flux_led.discovery.AIOBulbScanner.getBulbInfo" ) as discover: discover.return_value = [FLUX_DISCOVERY] await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - assert len(discover.mock_calls) == 1 + assert len(scan.mock_calls) == 1 hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() - assert len(discover.mock_calls) == 2 + assert len(scan.mock_calls) == 2 async_fire_time_changed(hass, utcnow() + flux_led.DISCOVERY_INTERVAL) await hass.async_block_till_done() - assert len(discover.mock_calls) == 3 + assert len(scan.mock_calls) == 3 + + +@pytest.mark.usefixtures("mock_multiple_broadcast_addresses") +async def test_configuring_flux_led_causes_discovery_multiple_addresses( + hass: HomeAssistant, +) -> None: + """Test that specifying empty config does discovery.""" + with patch( + "homeassistant.components.flux_led.discovery.AIOBulbScanner.async_scan" + ) as scan, patch( + "homeassistant.components.flux_led.discovery.AIOBulbScanner.getBulbInfo" + ) as discover: + discover.return_value = [FLUX_DISCOVERY] + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + assert len(scan.mock_calls) == 2 + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + assert len(scan.mock_calls) == 4 + + async_fire_time_changed(hass, utcnow() + flux_led.DISCOVERY_INTERVAL) + await hass.async_block_till_done() + assert len(scan.mock_calls) == 6 async def test_config_entry_reload(hass: HomeAssistant) -> None: @@ -78,20 +107,32 @@ async def test_config_entry_retry(hass: HomeAssistant) -> None: ], ) async def test_config_entry_fills_unique_id_with_directed_discovery( - hass: HomeAssistant, discovery: dict[str, str], title: str + hass: HomeAssistant, discovery: FluxLEDDiscovery, title: str ) -> None: """Test that the unique id is added if its missing via directed (not broadcast) discovery.""" config_entry = MockConfigEntry( - domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=None + domain=DOMAIN, data={CONF_NAME: "bogus", CONF_HOST: IP_ADDRESS}, unique_id=None ) config_entry.add_to_hass(hass) + assert config_entry.unique_id is None - async def _discovery(self, *args, address=None, **kwargs): - # Only return discovery results when doing directed discovery - return [discovery] if address == IP_ADDRESS else [] + class MockBulbScanner(AIOBulbScanner): + def __init__(self) -> None: + self._last_address: str | None = None + super().__init__() + + async def async_scan( + self, timeout: int = 10, address: str | None = None + ) -> list[FluxLEDDiscovery]: + self._last_address = address + return [discovery] if address == IP_ADDRESS else [] + + def getBulbInfo(self) -> FluxLEDDiscovery: + return [discovery] if self._last_address == IP_ADDRESS else [] with patch( - "homeassistant.components.flux_led.AIOBulbScanner.async_scan", new=_discovery + "homeassistant.components.flux_led.discovery.AIOBulbScanner", + return_value=MockBulbScanner(), ), _patch_wifibulb(): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() From afdc570d7047f2feb58032c60b81448e44306d86 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Sun, 19 Dec 2021 05:56:59 +0000 Subject: [PATCH 0722/2644] Use DeviceClass Enums in homekit_controller tests (#62219) --- .../homekit_controller/test_binary_sensor.py | 21 +++++++------------ .../homekit_controller/test_sensor.py | 15 +++++-------- 2 files changed, 12 insertions(+), 24 deletions(-) diff --git a/tests/components/homekit_controller/test_binary_sensor.py b/tests/components/homekit_controller/test_binary_sensor.py index e9ba4420176..e0b23775c4d 100644 --- a/tests/components/homekit_controller/test_binary_sensor.py +++ b/tests/components/homekit_controller/test_binary_sensor.py @@ -2,14 +2,7 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes -from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_GAS, - DEVICE_CLASS_MOISTURE, - DEVICE_CLASS_MOTION, - DEVICE_CLASS_OCCUPANCY, - DEVICE_CLASS_OPENING, - DEVICE_CLASS_SMOKE, -) +from homeassistant.components.binary_sensor import BinarySensorDeviceClass from tests.components.homekit_controller.common import setup_test_component @@ -41,7 +34,7 @@ async def test_motion_sensor_read_state(hass, utcnow): state = await helper.poll_and_get_state() assert state.state == "on" - assert state.attributes["device_class"] == DEVICE_CLASS_MOTION + assert state.attributes["device_class"] == BinarySensorDeviceClass.MOTION def create_contact_sensor_service(accessory): @@ -64,7 +57,7 @@ async def test_contact_sensor_read_state(hass, utcnow): state = await helper.poll_and_get_state() assert state.state == "on" - assert state.attributes["device_class"] == DEVICE_CLASS_OPENING + assert state.attributes["device_class"] == BinarySensorDeviceClass.OPENING def create_smoke_sensor_service(accessory): @@ -87,7 +80,7 @@ async def test_smoke_sensor_read_state(hass, utcnow): state = await helper.poll_and_get_state() assert state.state == "on" - assert state.attributes["device_class"] == DEVICE_CLASS_SMOKE + assert state.attributes["device_class"] == BinarySensorDeviceClass.SMOKE def create_carbon_monoxide_sensor_service(accessory): @@ -110,7 +103,7 @@ async def test_carbon_monoxide_sensor_read_state(hass, utcnow): state = await helper.poll_and_get_state() assert state.state == "on" - assert state.attributes["device_class"] == DEVICE_CLASS_GAS + assert state.attributes["device_class"] == BinarySensorDeviceClass.GAS def create_occupancy_sensor_service(accessory): @@ -133,7 +126,7 @@ async def test_occupancy_sensor_read_state(hass, utcnow): state = await helper.poll_and_get_state() assert state.state == "on" - assert state.attributes["device_class"] == DEVICE_CLASS_OCCUPANCY + assert state.attributes["device_class"] == BinarySensorDeviceClass.OCCUPANCY def create_leak_sensor_service(accessory): @@ -156,4 +149,4 @@ async def test_leak_sensor_read_state(hass, utcnow): state = await helper.poll_and_get_state() assert state.state == "on" - assert state.attributes["device_class"] == DEVICE_CLASS_MOISTURE + assert state.attributes["device_class"] == BinarySensorDeviceClass.MOISTURE diff --git a/tests/components/homekit_controller/test_sensor.py b/tests/components/homekit_controller/test_sensor.py index f96569551d8..426572457d0 100644 --- a/tests/components/homekit_controller/test_sensor.py +++ b/tests/components/homekit_controller/test_sensor.py @@ -3,12 +3,7 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes from aiohomekit.protocol.statuscodes import HapStatusCode -from homeassistant.const import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_TEMPERATURE, -) +from homeassistant.components.sensor import SensorDeviceClass from tests.components.homekit_controller.common import Helper, setup_test_component @@ -84,7 +79,7 @@ async def test_temperature_sensor_read_state(hass, utcnow): state = await helper.poll_and_get_state() assert state.state == "20" - assert state.attributes["device_class"] == DEVICE_CLASS_TEMPERATURE + assert state.attributes["device_class"] == SensorDeviceClass.TEMPERATURE async def test_temperature_sensor_not_added_twice(hass, utcnow): @@ -111,7 +106,7 @@ async def test_humidity_sensor_read_state(hass, utcnow): state = await helper.poll_and_get_state() assert state.state == "20" - assert state.attributes["device_class"] == DEVICE_CLASS_HUMIDITY + assert state.attributes["device_class"] == SensorDeviceClass.HUMIDITY async def test_light_level_sensor_read_state(hass, utcnow): @@ -128,7 +123,7 @@ async def test_light_level_sensor_read_state(hass, utcnow): state = await helper.poll_and_get_state() assert state.state == "20" - assert state.attributes["device_class"] == DEVICE_CLASS_ILLUMINANCE + assert state.attributes["device_class"] == SensorDeviceClass.ILLUMINANCE async def test_carbon_dioxide_level_sensor_read_state(hass, utcnow): @@ -162,7 +157,7 @@ async def test_battery_level_sensor(hass, utcnow): assert state.state == "20" assert state.attributes["icon"] == "mdi:battery-20" - assert state.attributes["device_class"] == DEVICE_CLASS_BATTERY + assert state.attributes["device_class"] == SensorDeviceClass.BATTERY async def test_battery_charging(hass, utcnow): From ebfe9aa384c7c453acfc74f8f13391cddb43d9fa Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 18 Dec 2021 23:06:17 -0700 Subject: [PATCH 0723/2644] Add a switch to opt-in to/opt-out of the next Ridwell pickup (#62293) * Add buttons to opt into/out of the next Ridwell pickup * Buttons finished * Coverage * better name * Move to switch * Clean up * Coverage * Use correct exception --- .coveragerc | 1 + homeassistant/components/ridwell/__init__.py | 59 +++++++++++++-- .../components/ridwell/config_flow.py | 2 +- homeassistant/components/ridwell/const.py | 2 + homeassistant/components/ridwell/sensor.py | 45 +++++++----- homeassistant/components/ridwell/switch.py | 71 +++++++++++++++++++ 6 files changed, 158 insertions(+), 22 deletions(-) create mode 100644 homeassistant/components/ridwell/switch.py diff --git a/.coveragerc b/.coveragerc index 78bc9e0ccb1..47a82d81861 100644 --- a/.coveragerc +++ b/.coveragerc @@ -887,6 +887,7 @@ omit = homeassistant/components/rest/switch.py homeassistant/components/ridwell/__init__.py homeassistant/components/ridwell/sensor.py + homeassistant/components/ridwell/switch.py homeassistant/components/ring/camera.py homeassistant/components/ripple/sensor.py homeassistant/components/rocketchat/notify.py diff --git a/homeassistant/components/ridwell/__init__.py b/homeassistant/components/ridwell/__init__.py index 03b1d2237e0..e3e1da4f21b 100644 --- a/homeassistant/components/ridwell/__init__.py +++ b/homeassistant/components/ridwell/__init__.py @@ -12,14 +12,25 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.helpers import aiohttp_client, entity_registry as er +from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) -from .const import DATA_ACCOUNT, DATA_COORDINATOR, DOMAIN, LOGGER +from .const import ( + DATA_ACCOUNT, + DATA_COORDINATOR, + DOMAIN, + LOGGER, + SENSOR_TYPE_NEXT_PICKUP, +) DEFAULT_UPDATE_INTERVAL = timedelta(hours=1) -PLATFORMS: list[Platform] = [Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.SWITCH] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -82,3 +93,43 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Migrate an old config entry.""" + version = entry.version + + LOGGER.debug("Migrating from version %s", version) + + # 1 -> 2: Update unique ID of existing, single sensor entity to be consistent with + # common format for platforms going forward: + if version == 1: + version = entry.version = 2 + + ent_reg = er.async_get(hass) + [entity_entry] = [ + e for e in ent_reg.entities.values() if e.config_entry_id == entry.entry_id + ] + new_unique_id = f"{entity_entry.unique_id}_{SENSOR_TYPE_NEXT_PICKUP}" + ent_reg.async_update_entity(entity_entry.entity_id, new_unique_id=new_unique_id) + + LOGGER.info("Migration to version %s successful", version) + + return True + + +class RidwellEntity(CoordinatorEntity): + """Define a base Ridwell entity.""" + + def __init__( + self, + coordinator: DataUpdateCoordinator, + account: RidwellAccount, + description: EntityDescription, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) + + self._account = account + self._attr_unique_id = f"{account.account_id}_{description.key}" + self.entity_description = description diff --git a/homeassistant/components/ridwell/config_flow.py b/homeassistant/components/ridwell/config_flow.py index 1ca5a9b5941..bcb881f3724 100644 --- a/homeassistant/components/ridwell/config_flow.py +++ b/homeassistant/components/ridwell/config_flow.py @@ -32,7 +32,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema( class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for WattTime.""" - VERSION = 1 + VERSION = 2 def __init__(self) -> None: """Initialize.""" diff --git a/homeassistant/components/ridwell/const.py b/homeassistant/components/ridwell/const.py index 8d280bf2cc0..bc1e78e2d93 100644 --- a/homeassistant/components/ridwell/const.py +++ b/homeassistant/components/ridwell/const.py @@ -7,3 +7,5 @@ LOGGER = logging.getLogger(__package__) DATA_ACCOUNT = "account" DATA_COORDINATOR = "coordinator" + +SENSOR_TYPE_NEXT_PICKUP = "next_pickup" diff --git a/homeassistant/components/ridwell/sensor.py b/homeassistant/components/ridwell/sensor.py index ddbf62b2356..44bab16691a 100644 --- a/homeassistant/components/ridwell/sensor.py +++ b/homeassistant/components/ridwell/sensor.py @@ -7,49 +7,60 @@ from typing import Any from aioridwell.model import RidwellAccount, RidwellPickupEvent -from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DATA_ACCOUNT, DATA_COORDINATOR, DOMAIN +from . import RidwellEntity +from .const import DATA_ACCOUNT, DATA_COORDINATOR, DOMAIN, SENSOR_TYPE_NEXT_PICKUP ATTR_CATEGORY = "category" ATTR_PICKUP_STATE = "pickup_state" ATTR_PICKUP_TYPES = "pickup_types" ATTR_QUANTITY = "quantity" +SENSOR_DESCRIPTION = SensorEntityDescription( + key=SENSOR_TYPE_NEXT_PICKUP, + name="Ridwell Pickup", + device_class=SensorDeviceClass.DATE, +) + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up WattTime sensors based on a config entry.""" + """Set up Ridwell sensors based on a config entry.""" accounts = hass.data[DOMAIN][entry.entry_id][DATA_ACCOUNT] coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] + async_add_entities( - [RidwellSensor(coordinator, account) for account in accounts.values()] + [ + RidwellSensor(coordinator, account, SENSOR_DESCRIPTION) + for account in accounts.values() + ] ) -class RidwellSensor(CoordinatorEntity, SensorEntity): +class RidwellSensor(RidwellEntity, SensorEntity): """Define a Ridwell pickup sensor.""" - _attr_device_class = SensorDeviceClass.DATE - def __init__( - self, coordinator: DataUpdateCoordinator, account: RidwellAccount + self, + coordinator: DataUpdateCoordinator, + account: RidwellAccount, + description: SensorEntityDescription, ) -> None: - """Initialize the sensor.""" - super().__init__(coordinator) + """Initialize.""" + super().__init__(coordinator, account, description) - self._account = account - self._attr_name = f"Ridwell Pickup ({account.address['street1']})" - self._attr_unique_id = account.account_id + self._attr_name = f"{description.name} ({account.address['street1']})" @property def extra_state_attributes(self) -> Mapping[str, Any]: diff --git a/homeassistant/components/ridwell/switch.py b/homeassistant/components/ridwell/switch.py new file mode 100644 index 00000000000..7bdea622507 --- /dev/null +++ b/homeassistant/components/ridwell/switch.py @@ -0,0 +1,71 @@ +"""Support for Ridwell buttons.""" +from __future__ import annotations + +from typing import Any + +from aioridwell.errors import RidwellError +from aioridwell.model import EventState, RidwellPickupEvent + +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import RidwellEntity +from .const import DATA_ACCOUNT, DATA_COORDINATOR, DOMAIN + +SWITCH_TYPE_OPT_IN = "opt_in" + +SWITCH_DESCRIPTION = SwitchEntityDescription( + key=SWITCH_TYPE_OPT_IN, + name="Opt-In to Next Pickup", + icon="mdi:calendar-check", +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Ridwell sensors based on a config entry.""" + accounts = hass.data[DOMAIN][entry.entry_id][DATA_ACCOUNT] + coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] + + async_add_entities( + [ + RidwellSwitch(coordinator, account, SWITCH_DESCRIPTION) + for account in accounts.values() + ] + ) + + +class RidwellSwitch(RidwellEntity, SwitchEntity): + """Define a Ridwell button.""" + + @property + def is_on(self) -> bool: + """Return True if entity is on.""" + event: RidwellPickupEvent = self.coordinator.data[self._account.account_id] + return event.state == EventState.SCHEDULED + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the switch off.""" + event: RidwellPickupEvent = self.coordinator.data[self._account.account_id] + + try: + await event.async_opt_out() + except RidwellError as err: + raise HomeAssistantError(f"Error while opting out: {err}") from err + + await self.coordinator.async_request_refresh() + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the switch on.""" + event: RidwellPickupEvent = self.coordinator.data[self._account.account_id] + + try: + await event.async_opt_in() + except RidwellError as err: + raise HomeAssistantError(f"Error while opting in: {err}") from err + + await self.coordinator.async_request_refresh() From 03477e0ae6a365c7d55ab520569c9f5291cd0083 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 19 Dec 2021 00:25:30 -0600 Subject: [PATCH 0724/2644] Split august motion and image capture binary sensors (#62154) --- .../components/august/binary_sensor.py | 18 +++++ homeassistant/components/august/camera.py | 3 +- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/august/test_binary_sensor.py | 66 ++++++++++++++++--- 6 files changed, 80 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index 2ec53b06bdb..b748c0994a1 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -58,6 +58,17 @@ def _retrieve_motion_state(data: AugustData, detail: DoorbellDetail) -> bool: return _activity_time_based_state(latest) +def _retrieve_image_capture_state(data: AugustData, detail: DoorbellDetail) -> bool: + latest = data.activity_stream.get_latest_device_activity( + detail.device_id, {ActivityType.DOORBELL_IMAGE_CAPTURE} + ) + + if latest is None: + return False + + return _activity_time_based_state(latest) + + def _retrieve_ding_state(data: AugustData, detail: DoorbellDetail) -> bool: latest = data.activity_stream.get_latest_device_activity( detail.device_id, {ActivityType.DOORBELL_DING} @@ -123,6 +134,13 @@ SENSOR_TYPES_DOORBELL: tuple[AugustBinarySensorEntityDescription, ...] = ( value_fn=_retrieve_motion_state, is_time_based=True, ), + AugustBinarySensorEntityDescription( + key="doorbell_image_capture", + name="Image Capture", + icon="mdi:file-image", + value_fn=_retrieve_image_capture_state, + is_time_based=True, + ), AugustBinarySensorEntityDescription( key="doorbell_online", name="Online", diff --git a/homeassistant/components/august/camera.py b/homeassistant/components/august/camera.py index 6f9ecf1b182..6c1f31c4b9c 100644 --- a/homeassistant/components/august/camera.py +++ b/homeassistant/components/august/camera.py @@ -63,7 +63,8 @@ class AugustCamera(AugustEntityMixin, Camera): def _update_from_data(self): """Get the latest state of the sensor.""" doorbell_activity = self._data.activity_stream.get_latest_device_activity( - self._device_id, {ActivityType.DOORBELL_MOTION} + self._device_id, + {ActivityType.DOORBELL_MOTION, ActivityType.DOORBELL_IMAGE_CAPTURE}, ) if doorbell_activity is not None: diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index fc365102926..f89be2915fb 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,7 +2,7 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": ["yalexs==1.1.13"], + "requirements": ["yalexs==1.1.15"], "codeowners": ["@bdraco"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index fbd49ab7d0e..f9d97f36b5b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2480,7 +2480,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.4 # homeassistant.components.august -yalexs==1.1.13 +yalexs==1.1.15 # homeassistant.components.yeelight yeelight==0.7.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fa55a57bf77..2008007ac04 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1479,7 +1479,7 @@ xmltodict==0.12.0 yalesmartalarmclient==0.3.4 # homeassistant.components.august -yalexs==1.1.13 +yalexs==1.1.15 # homeassistant.components.yeelight yeelight==0.7.8 diff --git a/tests/components/august/test_binary_sensor.py b/tests/components/august/test_binary_sensor.py index 26c824e5842..e2ff4a6771a 100644 --- a/tests/components/august/test_binary_sensor.py +++ b/tests/components/august/test_binary_sensor.py @@ -1,5 +1,6 @@ """The binary_sensor tests for the august platform.""" import datetime +import time from unittest.mock import Mock, patch from yalexs.pubnub_async import AugustPubNub @@ -26,6 +27,10 @@ from tests.components.august.mocks import ( ) +def _timetoken(): + return str(time.time_ns())[:-2] + + async def test_doorsense(hass): """Test creation of a lock with doorsense and bridge.""" lock_one = await _mock_lock_from_fixture( @@ -85,6 +90,10 @@ async def test_create_doorbell(hass): "binary_sensor.k98gidt45gul_name_motion" ) assert binary_sensor_k98gidt45gul_name_motion.state == STATE_OFF + binary_sensor_k98gidt45gul_name_image_capture = hass.states.get( + "binary_sensor.k98gidt45gul_name_image_capture" + ) + assert binary_sensor_k98gidt45gul_name_image_capture.state == STATE_OFF binary_sensor_k98gidt45gul_name_online = hass.states.get( "binary_sensor.k98gidt45gul_name_online" ) @@ -97,6 +106,10 @@ async def test_create_doorbell(hass): "binary_sensor.k98gidt45gul_name_motion" ) assert binary_sensor_k98gidt45gul_name_motion.state == STATE_OFF + binary_sensor_k98gidt45gul_name_image_capture = hass.states.get( + "binary_sensor.k98gidt45gul_name_image_capture" + ) + assert binary_sensor_k98gidt45gul_name_image_capture.state == STATE_OFF async def test_create_doorbell_offline(hass): @@ -171,7 +184,7 @@ async def test_doorbell_update_via_pubnub(hass): pubnub, Mock( channel=doorbell_one.pubsub_channel, - timetoken=dt_util.utcnow().timestamp() * 10000000, + timetoken=_timetoken(), message={ "status": "imagecapture", "data": { @@ -186,10 +199,46 @@ async def test_doorbell_update_via_pubnub(hass): await hass.async_block_till_done() + binary_sensor_k98gidt45gul_name_image_capture = hass.states.get( + "binary_sensor.k98gidt45gul_name_image_capture" + ) + assert binary_sensor_k98gidt45gul_name_image_capture.state == STATE_ON + + pubnub.message( + pubnub, + Mock( + channel=doorbell_one.pubsub_channel, + timetoken=_timetoken(), + message={ + "status": "doorbell_motion_detected", + "data": { + "event": "doorbell_motion_detected", + "image": { + "height": 640, + "width": 480, + "format": "jpg", + "created_at": "2021-03-16T02:36:26.886Z", + "bytes": 14061, + "secure_url": "https://dyu7azbnaoi74.cloudfront.net/images/1f8.jpeg", + "url": "https://dyu7azbnaoi74.cloudfront.net/images/1f8.jpeg", + "etag": "09e839331c4ea59eef28081f2caa0e90", + }, + "doorbellName": "Front Door", + "callID": None, + "origin": "mars-api", + "mutableContent": True, + }, + }, + ), + ) + + await hass.async_block_till_done() + binary_sensor_k98gidt45gul_name_motion = hass.states.get( "binary_sensor.k98gidt45gul_name_motion" ) assert binary_sensor_k98gidt45gul_name_motion.state == STATE_ON + binary_sensor_k98gidt45gul_name_ding = hass.states.get( "binary_sensor.k98gidt45gul_name_ding" ) @@ -204,16 +253,16 @@ async def test_doorbell_update_via_pubnub(hass): async_fire_time_changed(hass, new_time) await hass.async_block_till_done() - binary_sensor_k98gidt45gul_name_motion = hass.states.get( - "binary_sensor.k98gidt45gul_name_motion" + binary_sensor_k98gidt45gul_name_image_capture = hass.states.get( + "binary_sensor.k98gidt45gul_name_image_capture" ) - assert binary_sensor_k98gidt45gul_name_motion.state == STATE_OFF + assert binary_sensor_k98gidt45gul_name_image_capture.state == STATE_OFF pubnub.message( pubnub, Mock( channel=doorbell_one.pubsub_channel, - timetoken=dt_util.utcnow().timestamp() * 10000000, + timetoken=_timetoken(), message={ "status": "buttonpush", }, @@ -274,7 +323,7 @@ async def test_door_sense_update_via_pubnub(hass): pubnub, Mock( channel=lock_one.pubsub_channel, - timetoken=dt_util.utcnow().timestamp() * 10000000, + timetoken=_timetoken(), message={"status": "kAugLockState_Unlocking", "doorState": "closed"}, ), ) @@ -289,11 +338,10 @@ async def test_door_sense_update_via_pubnub(hass): pubnub, Mock( channel=lock_one.pubsub_channel, - timetoken=dt_util.utcnow().timestamp() * 10000000, + timetoken=_timetoken(), message={"status": "kAugLockState_Locking", "doorState": "open"}, ), ) - await hass.async_block_till_done() binary_sensor_online_with_doorsense_name = hass.states.get( "binary_sensor.online_with_doorsense_name_open" @@ -327,7 +375,7 @@ async def test_door_sense_update_via_pubnub(hass): pubnub, Mock( channel=lock_one.pubsub_channel, - timetoken=dt_util.utcnow().timestamp() * 10000000, + timetoken=_timetoken(), message={"status": "kAugLockState_Unlocking", "doorState": "open"}, ), ) From 7764c957ba688cbfc75352141f6a3b540b4b5a14 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 19 Dec 2021 00:26:19 -0600 Subject: [PATCH 0725/2644] Avoid setting nexia humidity to the same value since it causes the api to fail (#61843) --- homeassistant/components/nexia/climate.py | 24 +++++++++++++++++--- homeassistant/components/nexia/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py index 2c1fbf5a3f4..6212541f897 100644 --- a/homeassistant/components/nexia/climate.py +++ b/homeassistant/components/nexia/climate.py @@ -11,6 +11,7 @@ from nexia.const import ( SYSTEM_STATUS_IDLE, UNIT_FAHRENHEIT, ) +from nexia.util import find_humidity_setpoint import voluptuous as vol from homeassistant.components.climate import ClimateEntity @@ -58,6 +59,8 @@ from .coordinator import NexiaDataUpdateCoordinator from .entity import NexiaThermostatZoneEntity from .util import percent_conv +PARALLEL_UPDATES = 1 # keep data in sync with only one connection at a time + SERVICE_SET_AIRCLEANER_MODE = "set_aircleaner_mode" SERVICE_SET_HUMIDIFY_SETPOINT = "set_humidify_setpoint" SERVICE_SET_HVAC_RUN_MODE = "set_hvac_run_mode" @@ -231,9 +234,9 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): def set_humidity(self, humidity): """Dehumidify target.""" if self._thermostat.has_dehumidify_support(): - self._thermostat.set_dehumidify_setpoint(humidity / 100.0) + self.set_dehumidify_setpoint(humidity) else: - self._thermostat.set_humidify_setpoint(humidity / 100.0) + self.set_humidify_setpoint(humidity) self._signal_thermostat_update() @property @@ -453,7 +456,22 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): def set_humidify_setpoint(self, humidity): """Set the humidify setpoint.""" - self._thermostat.set_humidify_setpoint(humidity / 100.0) + target_humidity = find_humidity_setpoint(humidity / 100.0) + if self._thermostat.get_humidify_setpoint() == target_humidity: + # Trying to set the humidify setpoint to the + # same value will cause the api to timeout + return + self._thermostat.set_humidify_setpoint(target_humidity) + self._signal_thermostat_update() + + def set_dehumidify_setpoint(self, humidity): + """Set the dehumidify setpoint.""" + target_humidity = find_humidity_setpoint(humidity / 100.0) + if self._thermostat.get_dehumidify_setpoint() == target_humidity: + # Trying to set the dehumidify setpoint to the + # same value will cause the api to timeout + return + self._thermostat.set_dehumidify_setpoint(target_humidity) self._signal_thermostat_update() def _signal_thermostat_update(self): diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index 105cbdb62b7..624eee41db7 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -1,7 +1,7 @@ { "domain": "nexia", "name": "Nexia/American Standard/Trane", - "requirements": ["nexia==0.9.11"], + "requirements": ["nexia==0.9.12"], "codeowners": ["@bdraco"], "documentation": "https://www.home-assistant.io/integrations/nexia", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index f9d97f36b5b..f7319f1dfaf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1074,7 +1074,7 @@ nettigo-air-monitor==1.2.1 neurio==0.3.1 # homeassistant.components.nexia -nexia==0.9.11 +nexia==0.9.12 # homeassistant.components.nextcloud nextcloudmonitor==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2008007ac04..c71b96b35df 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -663,7 +663,7 @@ netmap==0.7.0.2 nettigo-air-monitor==1.2.1 # homeassistant.components.nexia -nexia==0.9.11 +nexia==0.9.12 # homeassistant.components.nfandroidtv notifications-android-tv==0.1.3 From 4572fec6809d652a651fa825b8d14251044be0f8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 19 Dec 2021 00:27:34 -0600 Subject: [PATCH 0726/2644] Bump flux_led to 0.27.8 to fix discovery of older devices (#62292) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 2be5d11c25f..51498f99590 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["network"], "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.26.15"], + "requirements": ["flux_led==0.27.8"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index f7319f1dfaf..45b538f86c1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -667,7 +667,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.26.15 +flux_led==0.27.8 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c71b96b35df..9b8813faf3c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -408,7 +408,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.26.15 +flux_led==0.27.8 # homeassistant.components.homekit fnvhash==0.1.0 From 90345b1cf27cabe16bbd79fda4299e81674a0c13 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sun, 19 Dec 2021 07:28:26 +0100 Subject: [PATCH 0727/2644] Fix logging for Shelly climate platform (#62264) Co-authored-by: Paulus Schoutsen --- homeassistant/components/shelly/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index d76b66ce181..4b535e584c7 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -69,7 +69,6 @@ async def async_setup_climate_entities( ) -> None: """Set up online climate devices.""" - _LOGGER.info("Setup online climate device %s", wrapper.name) device_block: Block | None = None sensor_block: Block | None = None @@ -82,6 +81,7 @@ async def async_setup_climate_entities( sensor_block = block if sensor_block and device_block: + _LOGGER.debug("Setup online climate device %s", wrapper.name) async_add_entities([BlockSleepingClimate(wrapper, sensor_block, device_block)]) @@ -92,7 +92,6 @@ async def async_restore_climate_entities( wrapper: BlockDeviceWrapper, ) -> None: """Restore sleeping climate devices.""" - _LOGGER.info("Setup sleeping climate device %s", wrapper.name) ent_reg = await entity_registry.async_get_registry(hass) entries = entity_registry.async_entries_for_config_entry( @@ -104,6 +103,7 @@ async def async_restore_climate_entities( if entry.domain != CLIMATE_DOMAIN: continue + _LOGGER.debug("Setup sleeping climate device %s", wrapper.name) _LOGGER.debug("Found entry %s [%s]", entry.original_name, entry.domain) async_add_entities([BlockSleepingClimate(wrapper, None, None, entry)]) From d7c5e41802e26cb96cd1180811ec3aabb5f7dd03 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 19 Dec 2021 00:30:44 -0600 Subject: [PATCH 0728/2644] Add hardware version to bond (#62256) --- homeassistant/components/bond/__init__.py | 1 + homeassistant/components/bond/entity.py | 3 +++ homeassistant/components/bond/utils.py | 5 +++++ tests/components/bond/test_init.py | 2 ++ 4 files changed, 11 insertions(+) diff --git a/homeassistant/components/bond/__init__.py b/homeassistant/components/bond/__init__.py index 95fa740f24c..bcc8e5d5be1 100644 --- a/homeassistant/components/bond/__init__.py +++ b/homeassistant/components/bond/__init__.py @@ -87,6 +87,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: name=hub_name, model=hub.target, sw_version=hub.fw_ver, + hw_version=hub.mcu_ver, suggested_area=hub.location, configuration_url=f"http://{host}", ) diff --git a/homeassistant/components/bond/entity.py b/homeassistant/components/bond/entity.py index 342e407ff48..1beca4895e2 100644 --- a/homeassistant/components/bond/entity.py +++ b/homeassistant/components/bond/entity.py @@ -11,6 +11,7 @@ from aiohttp import ClientError from bond_api import BPUPSubscriptions from homeassistant.const import ( + ATTR_HW_VERSION, ATTR_MODEL, ATTR_NAME, ATTR_SUGGESTED_AREA, @@ -78,6 +79,8 @@ class BondEntity(Entity): device_info[ATTR_MODEL] = self._hub.model if self._hub.fw_ver is not None: device_info[ATTR_SW_VERSION] = self._hub.fw_ver + if self._hub.mcu_ver is not None: + device_info[ATTR_HW_VERSION] = self._hub.mcu_ver else: model_data = [] if self._device.branding_profile: diff --git a/homeassistant/components/bond/utils.py b/homeassistant/components/bond/utils.py index b9dbe07ea50..785d7dfbd00 100644 --- a/homeassistant/components/bond/utils.py +++ b/homeassistant/components/bond/utils.py @@ -207,6 +207,11 @@ class BondHub: """Return this hub firmware version.""" return self._version.get("fw_ver") + @property + def mcu_ver(self) -> str | None: + """Return this hub hardware version.""" + return self._version.get("mcu_ver") + @property def devices(self) -> list[BondDevice]: """Return a list of all devices controlled by this hub.""" diff --git a/tests/components/bond/test_init.py b/tests/components/bond/test_init.py index db54ffdf716..88615d98122 100644 --- a/tests/components/bond/test_init.py +++ b/tests/components/bond/test_init.py @@ -84,6 +84,7 @@ async def test_async_setup_entry_sets_up_hub_and_supported_domains(hass: HomeAss "bondid": "test-bond-id", "target": "test-model", "fw_ver": "test-version", + "mcu_ver": "test-hw-version", } ), patch_setup_entry("cover") as mock_cover_async_setup_entry, patch_setup_entry( "fan" @@ -107,6 +108,7 @@ async def test_async_setup_entry_sets_up_hub_and_supported_domains(hass: HomeAss assert hub.manufacturer == "Olibra" assert hub.model == "test-model" assert hub.sw_version == "test-version" + assert hub.hw_version == "test-hw-version" assert hub.configuration_url == "http://some host" # verify supported domains are setup From a6b680cd3228942016c2c06b21732a4d86087c5d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 19 Dec 2021 00:59:16 -0600 Subject: [PATCH 0729/2644] Add support for enabling/disabling cloud access in flux_led (#61138) Co-authored-by: Paulus Schoutsen --- homeassistant/components/flux_led/__init__.py | 74 +++++++-------- .../components/flux_led/config_flow.py | 30 +++--- homeassistant/components/flux_led/const.py | 4 + .../components/flux_led/discovery.py | 93 ++++++++++++++++++- homeassistant/components/flux_led/entity.py | 54 +++++++++-- homeassistant/components/flux_led/light.py | 2 - homeassistant/components/flux_led/switch.py | 79 ++++++++++++++-- .../components/flux_led/translations/en.json | 3 +- tests/components/flux_led/__init__.py | 6 ++ tests/components/flux_led/test_config_flow.py | 62 +++++++++++-- tests/components/flux_led/test_init.py | 30 ++---- tests/components/flux_led/test_light.py | 63 ++++++++++++- tests/components/flux_led/test_switch.py | 39 +++++++- 13 files changed, 428 insertions(+), 111 deletions(-) diff --git a/homeassistant/components/flux_led/__init__.py b/homeassistant/components/flux_led/__init__.py index a8836a61b23..79ec3e8cf13 100644 --- a/homeassistant/components/flux_led/__init__.py +++ b/homeassistant/components/flux_led/__init__.py @@ -3,21 +3,14 @@ from __future__ import annotations from datetime import timedelta import logging -from typing import Any, Final +from typing import Any, Final, cast from flux_led import DeviceType from flux_led.aio import AIOWifiLedBulb from flux_led.const import ATTR_ID -from flux_led.scanner import FluxLEDDiscovery -from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - EVENT_HOMEASSISTANT_STARTED, - Platform, -) +from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STARTED, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr @@ -36,16 +29,18 @@ from .const import ( STARTUP_SCAN_TIMEOUT, ) from .discovery import ( + async_clear_discovery_cache, async_discover_device, async_discover_devices, - async_name_from_discovery, + async_get_discovery, async_trigger_discovery, + async_update_entry_from_discovery, ) _LOGGER = logging.getLogger(__name__) PLATFORMS_BY_TYPE: Final = { - DeviceType.Bulb: [Platform.LIGHT, Platform.NUMBER], + DeviceType.Bulb: [Platform.LIGHT, Platform.NUMBER, Platform.SWITCH], DeviceType.Switch: [Platform.SWITCH], } DISCOVERY_INTERVAL: Final = timedelta(minutes=15) @@ -58,22 +53,6 @@ def async_wifi_bulb_for_host(host: str) -> AIOWifiLedBulb: return AIOWifiLedBulb(host) -@callback -def async_update_entry_from_discovery( - hass: HomeAssistant, entry: config_entries.ConfigEntry, device: FluxLEDDiscovery -) -> None: - """Update a config entry from a flux_led discovery.""" - name = async_name_from_discovery(device) - mac_address = device[ATTR_ID] - assert mac_address is not None - hass.config_entries.async_update_entry( - entry, - data={**entry.data, CONF_NAME: name}, - title=name, - unique_id=dr.format_mac(mac_address), - ) - - async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the flux_led component.""" domain_data = hass.data.setdefault(DOMAIN, {}) @@ -92,18 +71,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -async def async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Update listener.""" - await hass.config_entries.async_reload(entry.entry_id) - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Flux LED/MagicLight from a config entry.""" host = entry.data[CONF_HOST] - if not entry.unique_id: - if discovery := await async_discover_device(hass, host): - async_update_entry_from_discovery(hass, entry, discovery) - device: AIOWifiLedBulb = async_wifi_bulb_for_host(host) signal = SIGNAL_STATE_UPDATED.format(device.ipaddr) @@ -119,11 +89,32 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: str(ex) or f"Timed out trying to connect to {device.ipaddr}" ) from ex - coordinator = FluxLedUpdateCoordinator(hass, device) + # UDP probe after successful connect only + directed_discovery = None + if discovery := async_get_discovery(hass, host): + directed_discovery = False + elif discovery := await async_discover_device(hass, host): + directed_discovery = True + + if discovery: + if entry.unique_id: + assert discovery[ATTR_ID] is not None + mac = dr.format_mac(cast(str, discovery[ATTR_ID])) + if mac != entry.unique_id: + # The device is offline and another flux_led device is now using the ip address + raise ConfigEntryNotReady( + f"Unexpected device found at {host}; Expected {entry.unique_id}, found {mac}" + ) + if directed_discovery: + # Only update the entry once we have verified the unique id + # is either missing or we have verified it matches + async_update_entry_from_discovery(hass, entry, discovery) + device.discovery = discovery + + coordinator = FluxLedUpdateCoordinator(hass, device, entry) hass.data[DOMAIN][entry.entry_id] = coordinator platforms = PLATFORMS_BY_TYPE[device.device_type] hass.config_entries.async_setup_platforms(entry, platforms) - entry.async_on_unload(entry.add_update_listener(async_update_listener)) return True @@ -133,6 +124,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device: AIOWifiLedBulb = hass.data[DOMAIN][entry.entry_id].device platforms = PLATFORMS_BY_TYPE[device.device_type] if unload_ok := await hass.config_entries.async_unload_platforms(entry, platforms): + # Make sure we probe the device again in case something has changed externally + async_clear_discovery_cache(hass, entry.data[CONF_HOST]) del hass.data[DOMAIN][entry.entry_id] await device.async_stop() return unload_ok @@ -142,12 +135,11 @@ class FluxLedUpdateCoordinator(DataUpdateCoordinator): """DataUpdateCoordinator to gather data for a specific flux_led device.""" def __init__( - self, - hass: HomeAssistant, - device: AIOWifiLedBulb, + self, hass: HomeAssistant, device: AIOWifiLedBulb, entry: ConfigEntry ) -> None: """Initialize DataUpdateCoordinator to gather data for specific device.""" self.device = device + self.entry = entry super().__init__( hass, _LOGGER, diff --git a/homeassistant/components/flux_led/config_flow.py b/homeassistant/components/flux_led/config_flow.py index aeaa5a87271..4f6e7844510 100644 --- a/homeassistant/components/flux_led/config_flow.py +++ b/homeassistant/components/flux_led/config_flow.py @@ -10,13 +10,13 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import dhcp -from homeassistant.const import CONF_HOST, CONF_MAC, CONF_MODE, CONF_NAME, CONF_PROTOCOL +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PROTOCOL from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import device_registry as dr from homeassistant.helpers.typing import DiscoveryInfoType -from . import async_update_entry_from_discovery, async_wifi_bulb_for_host +from . import async_wifi_bulb_for_host from .const import ( CONF_CUSTOM_EFFECT_COLORS, CONF_CUSTOM_EFFECT_SPEED_PCT, @@ -33,6 +33,8 @@ from .discovery import ( async_discover_device, async_discover_devices, async_name_from_discovery, + async_populate_data_from_discovery, + async_update_entry_from_discovery, ) CONF_DEVICE: Final = "device" @@ -73,7 +75,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): CONF_PROTOCOL: user_input.get(CONF_PROTOCOL), }, options={ - CONF_MODE: user_input[CONF_MODE], CONF_CUSTOM_EFFECT_COLORS: user_input[CONF_CUSTOM_EFFECT_COLORS], CONF_CUSTOM_EFFECT_SPEED_PCT: user_input[CONF_CUSTOM_EFFECT_SPEED_PCT], CONF_CUSTOM_EFFECT_TRANSITION: user_input[ @@ -86,7 +87,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle discovery via dhcp.""" self._discovered_device = FluxLEDDiscovery( ipaddr=discovery_info.ip, - model=discovery_info.hostname, + model=None, id=discovery_info.macaddress.replace(":", ""), model_num=None, version_num=None, @@ -115,11 +116,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): mac = dr.format_mac(mac_address) host = device[ATTR_IPADDR] await self.async_set_unique_id(mac) - self._abort_if_unique_id_configured(updates={CONF_HOST: host}) for entry in self._async_current_entries(include_ignore=False): - if entry.data[CONF_HOST] == host: - if not entry.unique_id: - async_update_entry_from_discovery(self.hass, entry, device) + if entry.unique_id == mac or entry.data[CONF_HOST] == host: + if async_update_entry_from_discovery(self.hass, entry, device): + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) return self.async_abort(reason="already_configured") self.context[CONF_HOST] = host for progress in self._async_in_progress(): @@ -164,12 +166,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Create a config entry from a device.""" self._async_abort_entries_match({CONF_HOST: device[ATTR_IPADDR]}) name = async_name_from_discovery(device) + data: dict[str, Any] = { + CONF_HOST: device[ATTR_IPADDR], + CONF_NAME: name, + } + async_populate_data_from_discovery(data, data, device) return self.async_create_entry( title=name, - data={ - CONF_HOST: device[ATTR_IPADDR], - CONF_NAME: name, - }, + data=data, ) async def async_step_user( @@ -259,7 +263,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): model=model, id=mac_address, model_num=bulb.model_num, - version_num=bulb.version_num, + version_num=None, # This is the minor version number firmware_date=None, model_info=None, model_description=bulb.model_data.description, diff --git a/homeassistant/components/flux_led/const.py b/homeassistant/components/flux_led/const.py index 639bac7165e..646dddb83a2 100644 --- a/homeassistant/components/flux_led/const.py +++ b/homeassistant/components/flux_led/const.py @@ -53,6 +53,10 @@ DISCOVER_SCAN_TIMEOUT: Final = 10 CONF_DEVICES: Final = "devices" CONF_CUSTOM_EFFECT: Final = "custom_effect" CONF_MODEL: Final = "model" +CONF_MINOR_VERSION: Final = "minor_version" +CONF_REMOTE_ACCESS_ENABLED: Final = "remote_access_enabled" +CONF_REMOTE_ACCESS_HOST: Final = "remote_access_host" +CONF_REMOTE_ACCESS_PORT: Final = "remote_access_port" MODE_AUTO: Final = "auto" MODE_RGB: Final = "rgb" diff --git a/homeassistant/components/flux_led/discovery.py b/homeassistant/components/flux_led/discovery.py index 71396623f95..d707af8ac9e 100644 --- a/homeassistant/components/flux_led/discovery.py +++ b/homeassistant/components/flux_led/discovery.py @@ -2,21 +2,54 @@ from __future__ import annotations import asyncio +from collections.abc import Mapping import logging +from typing import Any, Final from flux_led.aioscanner import AIOBulbScanner -from flux_led.const import ATTR_ID, ATTR_IPADDR, ATTR_MODEL, ATTR_MODEL_DESCRIPTION +from flux_led.const import ( + ATTR_ID, + ATTR_IPADDR, + ATTR_MODEL, + ATTR_MODEL_DESCRIPTION, + ATTR_REMOTE_ACCESS_ENABLED, + ATTR_REMOTE_ACCESS_HOST, + ATTR_REMOTE_ACCESS_PORT, + ATTR_VERSION_NUM, +) from flux_led.scanner import FluxLEDDiscovery from homeassistant import config_entries from homeassistant.components import network +from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import device_registry as dr +from homeassistant.util.network import is_ip_address -from .const import DISCOVER_SCAN_TIMEOUT, DOMAIN +from .const import ( + CONF_MINOR_VERSION, + CONF_MODEL, + CONF_REMOTE_ACCESS_ENABLED, + CONF_REMOTE_ACCESS_HOST, + CONF_REMOTE_ACCESS_PORT, + DISCOVER_SCAN_TIMEOUT, + DOMAIN, + FLUX_LED_DISCOVERY, +) _LOGGER = logging.getLogger(__name__) +CONF_TO_DISCOVERY: Final = { + CONF_HOST: ATTR_IPADDR, + CONF_REMOTE_ACCESS_ENABLED: ATTR_REMOTE_ACCESS_ENABLED, + CONF_REMOTE_ACCESS_HOST: ATTR_REMOTE_ACCESS_HOST, + CONF_REMOTE_ACCESS_PORT: ATTR_REMOTE_ACCESS_PORT, + CONF_MINOR_VERSION: ATTR_VERSION_NUM, + CONF_MODEL: ATTR_MODEL, +} + + @callback def async_name_from_discovery(device: FluxLEDDiscovery) -> str: """Convert a flux_led discovery to a human readable name.""" @@ -29,6 +62,62 @@ def async_name_from_discovery(device: FluxLEDDiscovery) -> str: return f"{device[ATTR_MODEL]} {short_mac}" +@callback +def async_populate_data_from_discovery( + current_data: Mapping[str, Any], + data_updates: dict[str, Any], + device: FluxLEDDiscovery, +) -> None: + """Copy discovery data into config entry data.""" + for conf_key, discovery_key in CONF_TO_DISCOVERY.items(): + if ( + device.get(discovery_key) is not None + and current_data.get(conf_key) != device[discovery_key] # type: ignore[misc] + ): + data_updates[conf_key] = device[discovery_key] # type: ignore[misc] + + +@callback +def async_update_entry_from_discovery( + hass: HomeAssistant, entry: config_entries.ConfigEntry, device: FluxLEDDiscovery +) -> bool: + """Update a config entry from a flux_led discovery.""" + data_updates: dict[str, Any] = {} + mac_address = device[ATTR_ID] + assert mac_address is not None + updates: dict[str, Any] = {} + if not entry.unique_id: + updates["unique_id"] = dr.format_mac(mac_address) + async_populate_data_from_discovery(entry.data, data_updates, device) + if not entry.data.get(CONF_NAME) or is_ip_address(entry.data[CONF_NAME]): + updates["title"] = data_updates[CONF_NAME] = async_name_from_discovery(device) + if data_updates: + updates["data"] = {**entry.data, **data_updates} + if updates: + return hass.config_entries.async_update_entry(entry, **updates) + return False + + +@callback +def async_get_discovery(hass: HomeAssistant, host: str) -> FluxLEDDiscovery | None: + """Check if a device was already discovered via a broadcast discovery.""" + discoveries: list[FluxLEDDiscovery] = hass.data[DOMAIN][FLUX_LED_DISCOVERY] + for discovery in discoveries: + if discovery[ATTR_IPADDR] == host: + return discovery + return None + + +@callback +def async_clear_discovery_cache(hass: HomeAssistant, host: str) -> None: + """Clear the host from the discovery cache.""" + domain_data = hass.data[DOMAIN] + discoveries: list[FluxLEDDiscovery] = domain_data[FLUX_LED_DISCOVERY] + domain_data[FLUX_LED_DISCOVERY] = [ + discovery for discovery in discoveries if discovery[ATTR_IPADDR] != host + ] + + async def async_discover_devices( hass: HomeAssistant, timeout: int, address: str | None = None ) -> list[FluxLEDDiscovery]: diff --git a/homeassistant/components/flux_led/entity.py b/homeassistant/components/flux_led/entity.py index 0e70e1f05f0..35ec8087dc4 100644 --- a/homeassistant/components/flux_led/entity.py +++ b/homeassistant/components/flux_led/entity.py @@ -6,18 +6,56 @@ from typing import Any from flux_led.aiodevice import AIOWifiLedBulb +from homeassistant import config_entries +from homeassistant.const import CONF_NAME from homeassistant.core import callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import FluxLedUpdateCoordinator -from .const import SIGNAL_STATE_UPDATED +from .const import CONF_MINOR_VERSION, CONF_MODEL, SIGNAL_STATE_UPDATED + + +def _async_device_info( + unique_id: str, device: AIOWifiLedBulb, entry: config_entries.ConfigEntry +) -> DeviceInfo: + version_num = device.version_num + if minor_version := entry.data.get(CONF_MINOR_VERSION): + sw_version = version_num + int(hex(minor_version)[2:]) / 100 + sw_version_str = f"{sw_version:0.3f}" + else: + sw_version_str = str(device.version_num) + return DeviceInfo( + connections={(dr.CONNECTION_NETWORK_MAC, unique_id)}, + manufacturer="Zengge", + model=device.model, + name=entry.data[CONF_NAME], + sw_version=sw_version_str, + hw_version=entry.data.get(CONF_MODEL), + ) + + +class FluxBaseEntity(Entity): + """Representation of a Flux entity without a coordinator.""" + + def __init__( + self, + device: AIOWifiLedBulb, + entry: config_entries.ConfigEntry, + ) -> None: + """Initialize the light.""" + self._device: AIOWifiLedBulb = device + self.entry = entry + if entry.unique_id: + self._attr_device_info = _async_device_info( + entry.unique_id, self._device, entry + ) class FluxEntity(CoordinatorEntity): - """Representation of a Flux entity.""" + """Representation of a Flux entity with a coordinator.""" coordinator: FluxLedUpdateCoordinator @@ -33,13 +71,9 @@ class FluxEntity(CoordinatorEntity): self._responding = True self._attr_name = name self._attr_unique_id = unique_id - if self.unique_id: - self._attr_device_info = DeviceInfo( - connections={(dr.CONNECTION_NETWORK_MAC, self.unique_id)}, - manufacturer="Magic Home (Zengge)", - model=self._device.model, - name=self.name, - sw_version=str(self._device.version_num), + if unique_id: + self._attr_device_info = _async_device_info( + unique_id, self._device, coordinator.entry ) @property diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index b138d41419d..ba454092d42 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -34,7 +34,6 @@ from homeassistant.const import ( CONF_DEVICES, CONF_HOST, CONF_MAC, - CONF_MODE, CONF_NAME, CONF_PROTOCOL, ) @@ -158,7 +157,6 @@ async def async_setup_platform( CONF_MAC: discovered_mac_by_host.get(host), CONF_NAME: device_config[CONF_NAME], CONF_PROTOCOL: device_config.get(CONF_PROTOCOL), - CONF_MODE: device_config.get(ATTR_MODE, MODE_AUTO), CONF_CUSTOM_EFFECT_COLORS: custom_effect_colors, CONF_CUSTOM_EFFECT_SPEED_PCT: custom_effects.get( CONF_SPEED_PCT, DEFAULT_EFFECT_SPEED diff --git a/homeassistant/components/flux_led/switch.py b/homeassistant/components/flux_led/switch.py index 01473bfc67c..8f499374b82 100644 --- a/homeassistant/components/flux_led/switch.py +++ b/homeassistant/components/flux_led/switch.py @@ -3,16 +3,26 @@ from __future__ import annotations from typing import Any +from flux_led import DeviceType +from flux_led.aio import AIOWifiLedBulb + from homeassistant import config_entries from homeassistant.components.switch import SwitchEntity from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import FluxLedUpdateCoordinator -from .const import DOMAIN -from .entity import FluxOnOffEntity +from .const import ( + CONF_REMOTE_ACCESS_ENABLED, + CONF_REMOTE_ACCESS_HOST, + CONF_REMOTE_ACCESS_PORT, + DOMAIN, +) +from .discovery import async_clear_discovery_cache +from .entity import FluxBaseEntity, FluxOnOffEntity async def async_setup_entry( @@ -22,15 +32,22 @@ async def async_setup_entry( ) -> None: """Set up the Flux lights.""" coordinator: FluxLedUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - async_add_entities( - [ + entities: list[FluxSwitch | FluxRemoteAccessSwitch] = [] + + if coordinator.device.device_type == DeviceType.Switch: + entities.append( FluxSwitch( coordinator, entry.unique_id, entry.data[CONF_NAME], ) - ] - ) + ) + + if entry.data.get(CONF_REMOTE_ACCESS_HOST): + entities.append(FluxRemoteAccessSwitch(coordinator.device, entry)) + + if entities: + async_add_entities(entities) class FluxSwitch(FluxOnOffEntity, CoordinatorEntity, SwitchEntity): @@ -40,3 +57,53 @@ class FluxSwitch(FluxOnOffEntity, CoordinatorEntity, SwitchEntity): """Turn the device on.""" if not self.is_on: await self._device.async_turn_on() + + +class FluxRemoteAccessSwitch(FluxBaseEntity, SwitchEntity): + """Representation of a Flux remote access switch.""" + + _attr_should_poll = False + _attr_entity_category = EntityCategory.CONFIG + + def __init__( + self, + device: AIOWifiLedBulb, + entry: config_entries.ConfigEntry, + ) -> None: + """Initialize the light.""" + super().__init__(device, entry) + self._attr_name = f"{entry.data[CONF_NAME]} Remote Access" + if entry.unique_id: + self._attr_unique_id = f"{entry.unique_id}_remote_access" + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the remote access on.""" + await self._device.async_enable_remote_access( + self.entry.data[CONF_REMOTE_ACCESS_HOST], + self.entry.data[CONF_REMOTE_ACCESS_PORT], + ) + await self._async_update_entry(True) + + async def _async_update_entry(self, new_state: bool) -> None: + """Update the entry with the new state on success.""" + async_clear_discovery_cache(self.hass, self._device.ipaddr) + self.hass.config_entries.async_update_entry( + self.entry, + data={**self.entry.data, CONF_REMOTE_ACCESS_ENABLED: new_state}, + ) + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the remote access off.""" + await self._device.async_disable_remote_access() + await self._async_update_entry(False) + + @property + def is_on(self) -> bool: + """Return true if remote access is enabled.""" + return bool(self.entry.data[CONF_REMOTE_ACCESS_ENABLED]) + + @property + def icon(self) -> str: + """Return icon based on state.""" + return "mdi:cloud-outline" if self.is_on else "mdi:cloud-off-outline" diff --git a/homeassistant/components/flux_led/translations/en.json b/homeassistant/components/flux_led/translations/en.json index 9a988408c30..7eda5cf0baf 100644 --- a/homeassistant/components/flux_led/translations/en.json +++ b/homeassistant/components/flux_led/translations/en.json @@ -27,8 +27,7 @@ "data": { "custom_effect_colors": "Custom Effect: List of 1 to 16 [R,G,B] colors. Example: [255,0,255],[60,128,0]", "custom_effect_speed_pct": "Custom Effect: Speed in percents for the effect that switch colors.", - "custom_effect_transition": "Custom Effect: Type of transition between the colors.", - "mode": "The chosen brightness mode." + "custom_effect_transition": "Custom Effect: Type of transition between the colors." } } } diff --git a/tests/components/flux_led/__init__.py b/tests/components/flux_led/__init__.py index c37caec4956..f86a06e41cc 100644 --- a/tests/components/flux_led/__init__.py +++ b/tests/components/flux_led/__init__.py @@ -57,6 +57,9 @@ FLUX_DISCOVERY = FluxLEDDiscovery( firmware_date=datetime.date(2021, 5, 5), model_info=MODEL, model_description=MODEL_DESCRIPTION, + remote_access_enabled=True, + remote_access_host="the.cloud", + remote_access_port=8816, ) @@ -80,6 +83,9 @@ def _mocked_bulb() -> AIOWifiLedBulb: bulb.async_turn_off = AsyncMock() bulb.async_turn_on = AsyncMock() bulb.async_set_levels = AsyncMock() + bulb.async_set_zones = AsyncMock() + bulb.async_disable_remote_access = AsyncMock() + bulb.async_enable_remote_access = AsyncMock() bulb.min_temp = 2700 bulb.max_temp = 6500 bulb.getRgb = MagicMock(return_value=[255, 0, 0]) diff --git a/tests/components/flux_led/test_config_flow.py b/tests/components/flux_led/test_config_flow.py index a546120ae41..3f2fc54b8d9 100644 --- a/tests/components/flux_led/test_config_flow.py +++ b/tests/components/flux_led/test_config_flow.py @@ -11,6 +11,11 @@ from homeassistant.components.flux_led.const import ( CONF_CUSTOM_EFFECT_COLORS, CONF_CUSTOM_EFFECT_SPEED_PCT, CONF_CUSTOM_EFFECT_TRANSITION, + CONF_MINOR_VERSION, + CONF_MODEL, + CONF_REMOTE_ACCESS_ENABLED, + CONF_REMOTE_ACCESS_HOST, + CONF_REMOTE_ACCESS_PORT, DOMAIN, MODE_RGB, TRANSITION_JUMP, @@ -20,7 +25,6 @@ from homeassistant.const import ( CONF_DEVICE, CONF_HOST, CONF_MAC, - CONF_MODE, CONF_NAME, CONF_PROTOCOL, ) @@ -34,6 +38,7 @@ from . import ( FLUX_DISCOVERY_PARTIAL, IP_ADDRESS, MAC_ADDRESS, + MODEL, MODULE, _patch_discovery, _patch_wifibulb, @@ -88,7 +93,16 @@ async def test_discovery(hass: HomeAssistant): assert result3["type"] == "create_entry" assert result3["title"] == DEFAULT_ENTRY_TITLE - assert result3["data"] == {CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE} + assert result3["data"] == { + CONF_MINOR_VERSION: 4, + CONF_HOST: IP_ADDRESS, + CONF_NAME: DEFAULT_ENTRY_TITLE, + CONF_MODEL: MODEL, + CONF_REMOTE_ACCESS_ENABLED: True, + CONF_REMOTE_ACCESS_HOST: "the.cloud", + CONF_REMOTE_ACCESS_PORT: 8816, + CONF_MINOR_VERSION: 0x04, + } mock_setup.assert_called_once() mock_setup_entry.assert_called_once() @@ -160,8 +174,14 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant): assert result3["type"] == "create_entry" assert result3["title"] == DEFAULT_ENTRY_TITLE assert result3["data"] == { + CONF_MINOR_VERSION: 4, CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE, + CONF_MODEL: MODEL, + CONF_REMOTE_ACCESS_ENABLED: True, + CONF_REMOTE_ACCESS_HOST: "the.cloud", + CONF_REMOTE_ACCESS_PORT: 8816, + CONF_MINOR_VERSION: 0x04, } await hass.async_block_till_done() @@ -204,7 +224,7 @@ async def test_import(hass: HomeAssistant): CONF_MAC: MAC_ADDRESS, CONF_NAME: "floor lamp", CONF_PROTOCOL: "ledenet", - CONF_MODE: MODE_RGB, + CONF_MODEL: MODE_RGB, CONF_CUSTOM_EFFECT_COLORS: "[255,0,0], [0,0,255]", CONF_CUSTOM_EFFECT_SPEED_PCT: 30, CONF_CUSTOM_EFFECT_TRANSITION: TRANSITION_STROBE, @@ -229,7 +249,6 @@ async def test_import(hass: HomeAssistant): CONF_PROTOCOL: "ledenet", } assert result["options"] == { - CONF_MODE: MODE_RGB, CONF_CUSTOM_EFFECT_COLORS: "[255,0,0], [0,0,255]", CONF_CUSTOM_EFFECT_SPEED_PCT: 30, CONF_CUSTOM_EFFECT_TRANSITION: TRANSITION_STROBE, @@ -278,7 +297,16 @@ async def test_manual_working_discovery(hass: HomeAssistant): await hass.async_block_till_done() assert result4["type"] == "create_entry" assert result4["title"] == DEFAULT_ENTRY_TITLE - assert result4["data"] == {CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE} + assert result4["data"] == { + CONF_MINOR_VERSION: 4, + CONF_HOST: IP_ADDRESS, + CONF_NAME: DEFAULT_ENTRY_TITLE, + CONF_MODEL: MODEL, + CONF_REMOTE_ACCESS_ENABLED: True, + CONF_REMOTE_ACCESS_HOST: "the.cloud", + CONF_REMOTE_ACCESS_PORT: 8816, + CONF_MINOR_VERSION: 0x04, + } # Duplicate result = await hass.config_entries.flow.async_init( @@ -376,7 +404,16 @@ async def test_discovered_by_discovery(hass): await hass.async_block_till_done() assert result2["type"] == "create_entry" - assert result2["data"] == {CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE} + assert result2["data"] == { + CONF_MINOR_VERSION: 4, + CONF_HOST: IP_ADDRESS, + CONF_NAME: DEFAULT_ENTRY_TITLE, + CONF_MODEL: MODEL, + CONF_REMOTE_ACCESS_ENABLED: True, + CONF_REMOTE_ACCESS_HOST: "the.cloud", + CONF_REMOTE_ACCESS_PORT: 8816, + CONF_MINOR_VERSION: 0x04, + } assert mock_async_setup.called assert mock_async_setup_entry.called @@ -402,7 +439,16 @@ async def test_discovered_by_dhcp_udp_responds(hass): await hass.async_block_till_done() assert result2["type"] == "create_entry" - assert result2["data"] == {CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE} + assert result2["data"] == { + CONF_MINOR_VERSION: 4, + CONF_HOST: IP_ADDRESS, + CONF_NAME: DEFAULT_ENTRY_TITLE, + CONF_MODEL: MODEL, + CONF_REMOTE_ACCESS_ENABLED: True, + CONF_REMOTE_ACCESS_HOST: "the.cloud", + CONF_REMOTE_ACCESS_PORT: 8816, + CONF_MINOR_VERSION: 0x04, + } assert mock_async_setup.called assert mock_async_setup_entry.called @@ -538,7 +584,7 @@ async def test_options(hass: HomeAssistant): domain=DOMAIN, data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, options={ - CONF_MODE: MODE_RGB, + CONF_MODEL: MODE_RGB, CONF_CUSTOM_EFFECT_COLORS: "[255,0,0], [0,0,255]", CONF_CUSTOM_EFFECT_SPEED_PCT: 30, CONF_CUSTOM_EFFECT_TRANSITION: TRANSITION_STROBE, diff --git a/tests/components/flux_led/test_init.py b/tests/components/flux_led/test_init.py index 3d9be823b6b..d3f5c38f1bc 100644 --- a/tests/components/flux_led/test_init.py +++ b/tests/components/flux_led/test_init.py @@ -3,8 +3,6 @@ from __future__ import annotations from unittest.mock import patch -from flux_led.aioscanner import AIOBulbScanner -from flux_led.scanner import FluxLEDDiscovery import pytest from homeassistant.components import flux_led @@ -107,32 +105,24 @@ async def test_config_entry_retry(hass: HomeAssistant) -> None: ], ) async def test_config_entry_fills_unique_id_with_directed_discovery( - hass: HomeAssistant, discovery: FluxLEDDiscovery, title: str + hass: HomeAssistant, discovery: dict[str, str], title: str ) -> None: """Test that the unique id is added if its missing via directed (not broadcast) discovery.""" config_entry = MockConfigEntry( - domain=DOMAIN, data={CONF_NAME: "bogus", CONF_HOST: IP_ADDRESS}, unique_id=None + domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=None ) config_entry.add_to_hass(hass) - assert config_entry.unique_id is None - class MockBulbScanner(AIOBulbScanner): - def __init__(self) -> None: - self._last_address: str | None = None - super().__init__() - - async def async_scan( - self, timeout: int = 10, address: str | None = None - ) -> list[FluxLEDDiscovery]: - self._last_address = address - return [discovery] if address == IP_ADDRESS else [] - - def getBulbInfo(self) -> FluxLEDDiscovery: - return [discovery] if self._last_address == IP_ADDRESS else [] + async def _discovery(self, *args, address=None, **kwargs): + # Only return discovery results when doing directed discovery + return [discovery] if address == IP_ADDRESS else [] with patch( - "homeassistant.components.flux_led.discovery.AIOBulbScanner", - return_value=MockBulbScanner(), + "homeassistant.components.flux_led.discovery.AIOBulbScanner.async_scan", + new=_discovery, + ), patch( + "homeassistant.components.flux_led.discovery.AIOBulbScanner.getBulbInfo", + return_value=[discovery], ), _patch_wifibulb(): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() diff --git a/tests/components/flux_led/test_light.py b/tests/components/flux_led/test_light.py index 92ea0fd8d39..25ceffe3022 100644 --- a/tests/components/flux_led/test_light.py +++ b/tests/components/flux_led/test_light.py @@ -1,6 +1,6 @@ """Tests for light platform.""" from datetime import timedelta -from unittest.mock import AsyncMock, Mock +from unittest.mock import AsyncMock, Mock, patch from flux_led.const import ( COLOR_MODE_ADDRESSABLE as FLUX_COLOR_MODE_ADDRESSABLE, @@ -21,6 +21,11 @@ from homeassistant.components.flux_led.const import ( CONF_CUSTOM_EFFECT_SPEED_PCT, CONF_CUSTOM_EFFECT_TRANSITION, CONF_DEVICES, + CONF_MINOR_VERSION, + CONF_MODEL, + CONF_REMOTE_ACCESS_ENABLED, + CONF_REMOTE_ACCESS_HOST, + CONF_REMOTE_ACCESS_PORT, CONF_SPEED_PCT, CONF_TRANSITION, DOMAIN, @@ -59,8 +64,10 @@ from homeassistant.util.dt import utcnow from . import ( DEFAULT_ENTRY_TITLE, + FLUX_DISCOVERY, IP_ADDRESS, MAC_ADDRESS, + MODEL, _mocked_bulb, _patch_discovery, _patch_wifibulb, @@ -1145,7 +1152,26 @@ async def test_migrate_from_yaml_with_custom_effect(hass: HomeAssistant) -> None } ], } - with _patch_discovery(), _patch_wifibulb(): + + last_address = None + + async def _discovery(self, *args, address=None, **kwargs): + # Only return discovery results when doing directed discovery + nonlocal last_address + last_address = address + return [FLUX_DISCOVERY] if address == IP_ADDRESS else [] + + def _mock_getBulbInfo(*args, **kwargs): + nonlocal last_address + return [FLUX_DISCOVERY] if last_address == IP_ADDRESS else [] + + with patch( + "homeassistant.components.flux_led.discovery.AIOBulbScanner.async_scan", + new=_discovery, + ), patch( + "homeassistant.components.flux_led.discovery.AIOBulbScanner.getBulbInfo", + new=_mock_getBulbInfo, + ), _patch_wifibulb(): await async_setup_component(hass, LIGHT_DOMAIN, config) await hass.async_block_till_done() await hass.async_block_till_done() @@ -1165,9 +1191,13 @@ async def test_migrate_from_yaml_with_custom_effect(hass: HomeAssistant) -> None CONF_HOST: IP_ADDRESS, CONF_NAME: "flux_lamppost", CONF_PROTOCOL: "ledenet", + CONF_MODEL: MODEL, + CONF_REMOTE_ACCESS_ENABLED: True, + CONF_REMOTE_ACCESS_HOST: "the.cloud", + CONF_REMOTE_ACCESS_PORT: 8816, + CONF_MINOR_VERSION: 0x04, } assert migrated_entry.options == { - CONF_MODE: "auto", CONF_CUSTOM_EFFECT_COLORS: "[(255, 0, 0), (255, 255, 0), (0, 255, 0)]", CONF_CUSTOM_EFFECT_SPEED_PCT: 30, CONF_CUSTOM_EFFECT_TRANSITION: "strobe", @@ -1189,7 +1219,26 @@ async def test_migrate_from_yaml_no_custom_effect(hass: HomeAssistant) -> None: } ], } - with _patch_discovery(), _patch_wifibulb(): + + last_address = None + + async def _discovery(self, *args, address=None, **kwargs): + # Only return discovery results when doing directed discovery + nonlocal last_address + last_address = address + return [FLUX_DISCOVERY] if address == IP_ADDRESS else [] + + def _mock_getBulbInfo(*args, **kwargs): + nonlocal last_address + return [FLUX_DISCOVERY] if last_address == IP_ADDRESS else [] + + with patch( + "homeassistant.components.flux_led.discovery.AIOBulbScanner.async_scan", + new=_discovery, + ), patch( + "homeassistant.components.flux_led.discovery.AIOBulbScanner.getBulbInfo", + new=_mock_getBulbInfo, + ), _patch_wifibulb(): await async_setup_component(hass, LIGHT_DOMAIN, config) await hass.async_block_till_done() await hass.async_block_till_done() @@ -1209,9 +1258,13 @@ async def test_migrate_from_yaml_no_custom_effect(hass: HomeAssistant) -> None: CONF_HOST: IP_ADDRESS, CONF_NAME: "flux_lamppost", CONF_PROTOCOL: "ledenet", + CONF_MODEL: MODEL, + CONF_REMOTE_ACCESS_ENABLED: True, + CONF_REMOTE_ACCESS_HOST: "the.cloud", + CONF_REMOTE_ACCESS_PORT: 8816, + CONF_MINOR_VERSION: 0x04, } assert migrated_entry.options == { - CONF_MODE: "auto", CONF_CUSTOM_EFFECT_COLORS: None, CONF_CUSTOM_EFFECT_SPEED_PCT: 50, CONF_CUSTOM_EFFECT_TRANSITION: "gradual", diff --git a/tests/components/flux_led/test_switch.py b/tests/components/flux_led/test_switch.py index b569d51e13a..fbbdc76727c 100644 --- a/tests/components/flux_led/test_switch.py +++ b/tests/components/flux_led/test_switch.py @@ -1,6 +1,6 @@ """Tests for switch platform.""" from homeassistant.components import flux_led -from homeassistant.components.flux_led.const import DOMAIN +from homeassistant.components.flux_led.const import CONF_REMOTE_ACCESS_ENABLED, DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, @@ -16,6 +16,7 @@ from . import ( DEFAULT_ENTRY_TITLE, IP_ADDRESS, MAC_ADDRESS, + _mocked_bulb, _mocked_switch, _patch_discovery, _patch_wifibulb, @@ -27,7 +28,7 @@ from tests.common import MockConfigEntry async def test_switch_on_off(hass: HomeAssistant) -> None: - """Test a switch light.""" + """Test a smart plug.""" config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, @@ -60,3 +61,37 @@ async def test_switch_on_off(hass: HomeAssistant) -> None: await async_mock_device_turn_on(hass, switch) assert hass.states.get(entity_id).state == STATE_ON + + +async def test_remote_access_on_off(hass: HomeAssistant) -> None: + """Test enable/disable remote access.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + with _patch_discovery(), _patch_wifibulb(bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "switch.bulb_rgbcw_ddeeff_remote_access" + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + await hass.services.async_call( + SWITCH_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.async_disable_remote_access.assert_called_once() + assert hass.states.get(entity_id).state == STATE_OFF + assert config_entry.data[CONF_REMOTE_ACCESS_ENABLED] is False + + await hass.services.async_call( + SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.async_enable_remote_access.assert_called_once() + + assert hass.states.get(entity_id).state == STATE_ON + assert config_entry.data[CONF_REMOTE_ACCESS_ENABLED] is True From 832184bacd8e991ecf0b688304f068449cc6de2f Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 18 Dec 2021 23:14:21 -0800 Subject: [PATCH 0730/2644] Speed up stream tests by 40-50% with shared data (#62300) --- tests/components/stream/common.py | 29 ++++-- tests/components/stream/conftest.py | 15 +++ tests/components/stream/test_hls.py | 23 ++--- tests/components/stream/test_recorder.py | 125 ++++++++++++----------- tests/components/stream/test_worker.py | 5 +- 5 files changed, 109 insertions(+), 88 deletions(-) diff --git a/tests/components/stream/common.py b/tests/components/stream/common.py index 0b25fd7e7c5..7fc25cb8478 100644 --- a/tests/components/stream/common.py +++ b/tests/components/stream/common.py @@ -1,6 +1,7 @@ """Collection of test helpers.""" from datetime import datetime from fractions import Fraction +import functools from functools import partial import io @@ -23,6 +24,11 @@ DefaultSegment = partial( AUDIO_SAMPLE_RATE = 8000 +def stream_teardown(): + """Perform test teardown.""" + frame_image_data.cache_clear() + + def generate_audio_frame(pcm_mulaw=False): """Generate a blank audio frame.""" if pcm_mulaw: @@ -37,6 +43,19 @@ def generate_audio_frame(pcm_mulaw=False): return audio_frame +@functools.lru_cache(maxsize=1024) +def frame_image_data(frame_i, total_frames): + """Generate image content for a frame of a video.""" + img = np.empty((480, 320, 3)) + img[:, :, 0] = 0.5 + 0.5 * np.sin(2 * np.pi * (0 / 3 + frame_i / total_frames)) + img[:, :, 1] = 0.5 + 0.5 * np.sin(2 * np.pi * (1 / 3 + frame_i / total_frames)) + img[:, :, 2] = 0.5 + 0.5 * np.sin(2 * np.pi * (2 / 3 + frame_i / total_frames)) + + img = np.round(255 * img).astype(np.uint8) + img = np.clip(img, 0, 255) + return img + + def generate_video(encoder, container_format, duration): """ Generate a test video. @@ -58,15 +77,7 @@ def generate_video(encoder, container_format, duration): stream.options.update({"g": str(fps), "keyint_min": str(fps)}) for frame_i in range(total_frames): - - img = np.empty((480, 320, 3)) - img[:, :, 0] = 0.5 + 0.5 * np.sin(2 * np.pi * (0 / 3 + frame_i / total_frames)) - img[:, :, 1] = 0.5 + 0.5 * np.sin(2 * np.pi * (1 / 3 + frame_i / total_frames)) - img[:, :, 2] = 0.5 + 0.5 * np.sin(2 * np.pi * (2 / 3 + frame_i / total_frames)) - - img = np.round(255 * img).astype(np.uint8) - img = np.clip(img, 0, 255) - + img = frame_image_data(frame_i, total_frames) frame = av.VideoFrame.from_ndarray(img, format="rgb24") for packet in stream.encode(frame): container.mux(packet) diff --git a/tests/components/stream/conftest.py b/tests/components/stream/conftest.py index 4b8f21a3a7e..c754903965a 100644 --- a/tests/components/stream/conftest.py +++ b/tests/components/stream/conftest.py @@ -25,6 +25,8 @@ import pytest from homeassistant.components.stream.core import Segment, StreamOutput from homeassistant.components.stream.worker import StreamState +from .common import generate_h264_video, stream_teardown + TEST_TIMEOUT = 7.0 # Lower than 9s home assistant timeout @@ -215,3 +217,16 @@ def hls_sync(): side_effect=sync.response, ): yield sync + + +@pytest.fixture(scope="package") +def h264_video(): + """Generate a video, shared across tests.""" + return generate_h264_video() + + +@pytest.fixture(scope="package", autouse=True) +def fixture_teardown(): + """Destroy package level test state.""" + yield + stream_teardown() diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index 5e0ee15f097..0f50f830a85 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -20,11 +20,7 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed -from tests.components.stream.common import ( - FAKE_TIME, - DefaultSegment as Segment, - generate_h264_video, -) +from tests.components.stream.common import FAKE_TIME, DefaultSegment as Segment STREAM_SOURCE = "some-stream-source" INIT_BYTES = b"init" @@ -118,7 +114,7 @@ def make_playlist( return "\n".join(response) -async def test_hls_stream(hass, hls_stream, stream_worker_sync): +async def test_hls_stream(hass, hls_stream, stream_worker_sync, h264_video): """ Test hls stream. @@ -130,8 +126,7 @@ async def test_hls_stream(hass, hls_stream, stream_worker_sync): stream_worker_sync.pause() # Setup demo HLS track - source = generate_h264_video() - stream = create_stream(hass, source, {}) + stream = create_stream(hass, h264_video, {}) # Request stream stream.add_provider(HLS_PROVIDER) @@ -169,15 +164,14 @@ async def test_hls_stream(hass, hls_stream, stream_worker_sync): assert fail_response.status == HTTPStatus.NOT_FOUND -async def test_stream_timeout(hass, hass_client, stream_worker_sync): +async def test_stream_timeout(hass, hass_client, stream_worker_sync, h264_video): """Test hls stream timeout.""" await async_setup_component(hass, "stream", {"stream": {}}) stream_worker_sync.pause() # Setup demo HLS track - source = generate_h264_video() - stream = create_stream(hass, source, {}) + stream = create_stream(hass, h264_video, {}) # Request stream stream.add_provider(HLS_PROVIDER) @@ -211,15 +205,16 @@ async def test_stream_timeout(hass, hass_client, stream_worker_sync): assert fail_response.status == HTTPStatus.NOT_FOUND -async def test_stream_timeout_after_stop(hass, hass_client, stream_worker_sync): +async def test_stream_timeout_after_stop( + hass, hass_client, stream_worker_sync, h264_video +): """Test hls stream timeout after the stream has been stopped already.""" await async_setup_component(hass, "stream", {"stream": {}}) stream_worker_sync.pause() # Setup demo HLS track - source = generate_h264_video() - stream = create_stream(hass, source, {}) + stream = create_stream(hass, h264_video, {}) # Request stream stream.add_provider(HLS_PROVIDER) diff --git a/tests/components/stream/test_recorder.py b/tests/components/stream/test_recorder.py index fc6b48d273b..50aa4df3f1c 100644 --- a/tests/components/stream/test_recorder.py +++ b/tests/components/stream/test_recorder.py @@ -16,17 +16,14 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from .common import DefaultSegment as Segment, generate_h264_video, remux_with_audio + from tests.common import async_fire_time_changed -from tests.components.stream.common import ( - DefaultSegment as Segment, - generate_h264_video, - remux_with_audio, -) MAX_ABORT_SEGMENTS = 20 # Abort test to avoid looping forever -async def test_record_stream(hass, hass_client, record_worker_sync): +async def test_record_stream(hass, hass_client, record_worker_sync, h264_video): """ Test record stream. @@ -37,8 +34,7 @@ async def test_record_stream(hass, hass_client, record_worker_sync): await async_setup_component(hass, "stream", {"stream": {}}) # Setup demo track - source = generate_h264_video() - stream = create_stream(hass, source, {}) + stream = create_stream(hass, h264_video, {}) with patch.object(hass.config, "is_allowed_path", return_value=True): await stream.async_record("/example/path") @@ -54,13 +50,12 @@ async def test_record_stream(hass, hass_client, record_worker_sync): async def test_record_lookback( - hass, hass_client, stream_worker_sync, record_worker_sync + hass, hass_client, stream_worker_sync, record_worker_sync, h264_video ): """Exercise record with loopback.""" await async_setup_component(hass, "stream", {"stream": {}}) - source = generate_h264_video() - stream = create_stream(hass, source, {}) + stream = create_stream(hass, h264_video, {}) # Start an HLS feed to enable lookback stream.add_provider(HLS_PROVIDER) @@ -74,7 +69,7 @@ async def test_record_lookback( stream.stop() -async def test_recorder_timeout(hass, hass_client, stream_worker_sync): +async def test_recorder_timeout(hass, hass_client, stream_worker_sync, h264_video): """ Test recorder timeout. @@ -87,9 +82,7 @@ async def test_recorder_timeout(hass, hass_client, stream_worker_sync): with patch("homeassistant.components.stream.IdleTimer.fire") as mock_timeout: # Setup demo track - source = generate_h264_video() - - stream = create_stream(hass, source, {}) + stream = create_stream(hass, h264_video, {}) with patch.object(hass.config, "is_allowed_path", return_value=True): await stream.async_record("/example/path") recorder = stream.add_provider(RECORDER_PROVIDER) @@ -109,13 +102,11 @@ async def test_recorder_timeout(hass, hass_client, stream_worker_sync): await hass.async_block_till_done() -async def test_record_path_not_allowed(hass, hass_client): +async def test_record_path_not_allowed(hass, hass_client, h264_video): """Test where the output path is not allowed by home assistant configuration.""" await async_setup_component(hass, "stream", {"stream": {}}) - # Setup demo track - source = generate_h264_video() - stream = create_stream(hass, source, {}) + stream = create_stream(hass, h264_video, {}) with patch.object( hass.config, "is_allowed_path", return_value=False ), pytest.raises(HomeAssistantError): @@ -136,15 +127,14 @@ def add_parts_to_segment(segment, source): ] -async def test_recorder_save(tmpdir): +async def test_recorder_save(tmpdir, h264_video): """Test recorder save.""" # Setup - source = generate_h264_video() filename = f"{tmpdir}/test.mp4" # Run segment = Segment(sequence=1) - add_parts_to_segment(segment, source) + add_parts_to_segment(segment, h264_video) segment.duration = 4 recorder_save_worker(filename, [segment]) @@ -152,18 +142,17 @@ async def test_recorder_save(tmpdir): assert os.path.exists(filename) -async def test_recorder_discontinuity(tmpdir): +async def test_recorder_discontinuity(tmpdir, h264_video): """Test recorder save across a discontinuity.""" # Setup - source = generate_h264_video() filename = f"{tmpdir}/test.mp4" # Run segment_1 = Segment(sequence=1, stream_id=0) - add_parts_to_segment(segment_1, source) + add_parts_to_segment(segment_1, h264_video) segment_1.duration = 4 segment_2 = Segment(sequence=2, stream_id=1) - add_parts_to_segment(segment_2, source) + add_parts_to_segment(segment_2, h264_video) segment_2.duration = 4 recorder_save_worker(filename, [segment_1, segment_2]) # Assert @@ -182,8 +171,29 @@ async def test_recorder_no_segments(tmpdir): assert not os.path.exists(filename) +@pytest.fixture(scope="module") +def h264_mov_video(): + """Generate a source video with no audio.""" + return generate_h264_video(container_format="mov") + + +@pytest.mark.parametrize( + "audio_codec,expected_audio_streams", + [ + ("aac", 1), # aac is a valid mp4 codec + ("pcm_mulaw", 0), # G.711 is not a valid mp4 codec + ("empty", 0), # audio stream with no packets + (None, 0), # no audio stream + ], +) async def test_record_stream_audio( - hass, hass_client, stream_worker_sync, record_worker_sync + hass, + hass_client, + stream_worker_sync, + record_worker_sync, + audio_codec, + expected_audio_streams, + h264_mov_video, ): """ Test treatment of different audio inputs. @@ -193,47 +203,38 @@ async def test_record_stream_audio( """ await async_setup_component(hass, "stream", {"stream": {}}) - # Generate source video with no audio - orig_source = generate_h264_video(container_format="mov") + # Remux source video with new audio + source = remux_with_audio(h264_mov_video, "mov", audio_codec) # mov can store PCM - for a_codec, expected_audio_streams in ( - ("aac", 1), # aac is a valid mp4 codec - ("pcm_mulaw", 0), # G.711 is not a valid mp4 codec - ("empty", 0), # audio stream with no packets - (None, 0), # no audio stream - ): - # Remux source video with new audio - source = remux_with_audio(orig_source, "mov", a_codec) # mov can store PCM + record_worker_sync.reset() + stream_worker_sync.pause() - record_worker_sync.reset() - stream_worker_sync.pause() + stream = create_stream(hass, source, {}) + with patch.object(hass.config, "is_allowed_path", return_value=True): + await stream.async_record("/example/path") + recorder = stream.add_provider(RECORDER_PROVIDER) - stream = create_stream(hass, source, {}) - with patch.object(hass.config, "is_allowed_path", return_value=True): - await stream.async_record("/example/path") - recorder = stream.add_provider(RECORDER_PROVIDER) + while True: + await recorder.recv() + if not (segment := recorder.last_segment): + break + last_segment = segment + stream_worker_sync.resume() - while True: - await recorder.recv() - if not (segment := recorder.last_segment): - break - last_segment = segment - stream_worker_sync.resume() + result = av.open( + BytesIO(last_segment.init + last_segment.get_data()), + "r", + format="mp4", + ) - result = av.open( - BytesIO(last_segment.init + last_segment.get_data()), - "r", - format="mp4", - ) + assert len(result.streams.audio) == expected_audio_streams + result.close() + stream.stop() + await hass.async_block_till_done() - assert len(result.streams.audio) == expected_audio_streams - result.close() - stream.stop() - await hass.async_block_till_done() - - # Verify that the save worker was invoked, then block until its - # thread completes and is shutdown completely to avoid thread leaks. - await record_worker_sync.join() + # Verify that the save worker was invoked, then block until its + # thread completes and is shutdown completely to avoid thread leaks. + await record_worker_sync.join() async def test_recorder_log(hass, caplog): diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index 3e9ea157934..48144219994 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -781,7 +781,7 @@ async def test_durations(hass, record_worker_sync): stream.stop() -async def test_has_keyframe(hass, record_worker_sync): +async def test_has_keyframe(hass, record_worker_sync, h264_video): """Test that the has_keyframe metadata matches the media.""" await async_setup_component( hass, @@ -797,8 +797,7 @@ async def test_has_keyframe(hass, record_worker_sync): }, ) - source = generate_h264_video() - stream = create_stream(hass, source, {}) + stream = create_stream(hass, h264_video, {}) # use record_worker_sync to grab output segments with patch.object(hass.config, "is_allowed_path", return_value=True): From e834382b9aca35a253f66ab148a19c0cca4ec899 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 19 Dec 2021 01:41:02 -0600 Subject: [PATCH 0731/2644] Add pico remote support to non-pro lutron caseta bridges (#61032) --- .../components/lutron_caseta/__init__.py | 188 ++++++++---------- .../components/lutron_caseta/const.py | 4 +- .../lutron_caseta/device_trigger.py | 176 +++++++++++++--- .../components/lutron_caseta/manifest.json | 2 +- requirements_all.txt | 5 +- requirements_test_all.txt | 5 +- 6 files changed, 234 insertions(+), 146 deletions(-) diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 3c74e378336..d91dc8f31e5 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -1,19 +1,19 @@ """Component for interacting with a Lutron Caseta system.""" +from __future__ import annotations + import asyncio import contextlib import logging import ssl -from aiolip import LIP -from aiolip.data import LIPMode -from aiolip.protocol import LIP_BUTTON_PRESS import async_timeout +from pylutron_caseta import BUTTON_STATUS_PRESSED from pylutron_caseta.smartbridge import Smartbridge import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_HOST, Platform -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv @@ -26,12 +26,12 @@ from .const import ( ATTR_AREA_NAME, ATTR_BUTTON_NUMBER, ATTR_DEVICE_NAME, + ATTR_LEAP_BUTTON_NUMBER, ATTR_SERIAL, ATTR_TYPE, BRIDGE_DEVICE, BRIDGE_DEVICE_ID, BRIDGE_LEAP, - BRIDGE_LIP, BRIDGE_TIMEOUT, BUTTON_DEVICES, CONF_CA_CERTS, @@ -41,6 +41,10 @@ from .const import ( LUTRON_CASETA_BUTTON_EVENT, MANUFACTURER, ) +from .device_trigger import ( + DEVICE_TYPE_SUBTYPE_MAP_TO_LIP, + LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP, +) _LOGGER = logging.getLogger(__name__) @@ -97,8 +101,11 @@ async def async_setup(hass, base_config): return True -async def async_setup_entry(hass, config_entry): +async def async_setup_entry( + hass: HomeAssistant, config_entry: config_entries.ConfigEntry +) -> bool: """Set up a bridge from a config entry.""" + entry_id = config_entry.entry_id host = config_entry.data[CONF_HOST] keyfile = hass.config.path(config_entry.data[CONF_KEYFILE]) certfile = hass.config.path(config_entry.data[CONF_CERTFILE]) @@ -130,85 +137,30 @@ async def async_setup_entry(hass, config_entry): devices = bridge.get_devices() bridge_device = devices[BRIDGE_DEVICE_ID] - _async_register_bridge_device(hass, config_entry.entry_id, bridge_device) + buttons = bridge.buttons + _async_register_bridge_device(hass, entry_id, bridge_device) + button_devices = _async_register_button_devices( + hass, entry_id, bridge_device, buttons + ) + _async_subscribe_pico_remote_events(hass, bridge, buttons) + # Store this bridge (keyed by entry_id) so it can be retrieved by the # platforms we're setting up. - hass.data[DOMAIN][config_entry.entry_id] = { + hass.data[DOMAIN][entry_id] = { BRIDGE_LEAP: bridge, BRIDGE_DEVICE: bridge_device, - BUTTON_DEVICES: {}, - BRIDGE_LIP: None, + BUTTON_DEVICES: button_devices, } - if bridge.lip_devices: - # If the bridge also supports LIP (Lutron Integration Protocol) - # we can fire events when pico buttons are pressed to allow - # pico remotes to control other devices. - await async_setup_lip(hass, config_entry, bridge.lip_devices) - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True -async def async_setup_lip(hass, config_entry, lip_devices): - """Connect to the bridge via Lutron Integration Protocol to watch for pico remotes.""" - host = config_entry.data[CONF_HOST] - config_entry_id = config_entry.entry_id - data = hass.data[DOMAIN][config_entry_id] - bridge_device = data[BRIDGE_DEVICE] - bridge = data[BRIDGE_LEAP] - lip = LIP() - try: - await lip.async_connect(host) - except asyncio.TimeoutError: - _LOGGER.warning( - "Failed to connect to via LIP at %s:23, Pico and Shade remotes will not be available; " - "Enable Telnet Support in the Lutron app under Settings >> Advanced >> Integration", - host, - ) - return - - _LOGGER.debug("Connected to Lutron Caseta bridge via LIP at %s:23", host) - button_devices_by_lip_id = _async_merge_lip_leap_data(lip_devices, bridge) - button_devices_by_dr_id = _async_register_button_devices( - hass, config_entry_id, bridge_device, button_devices_by_lip_id - ) - _async_subscribe_pico_remote_events(hass, lip, button_devices_by_lip_id) - data[BUTTON_DEVICES] = button_devices_by_dr_id - data[BRIDGE_LIP] = lip - - @callback -def _async_merge_lip_leap_data(lip_devices, bridge): - """Merge the leap data into the lip data.""" - sensor_devices = bridge.get_devices_by_domain("sensor") - - button_devices_by_id = { - id: device for id, device in lip_devices.items() if "Buttons" in device - } - sensor_devices_by_name = {device["name"]: device for device in sensor_devices} - - # Add the leap data into the lip data - # so we know the type, model, and serial - for device in button_devices_by_id.values(): - area = device.get("Area", {}).get("Name", "") - name = device["Name"] - leap_name = f"{area}_{name}" - device["leap_name"] = leap_name - leap_device_data = sensor_devices_by_name.get(leap_name) - if leap_device_data is None: - continue - for key in ("type", "model", "serial"): - if (val := leap_device_data.get(key)) is not None: - device[key] = val - - _LOGGER.debug("Button Devices: %s", button_devices_by_id) - return button_devices_by_id - - -@callback -def _async_register_bridge_device(hass, config_entry_id, bridge_device): +def _async_register_bridge_device( + hass: HomeAssistant, config_entry_id: str, bridge_device: dict +) -> None: """Register the bridge device in the device registry.""" device_registry = dr.async_get(hass) device_registry.async_get_or_create( @@ -217,24 +169,30 @@ def _async_register_bridge_device(hass, config_entry_id, bridge_device): config_entry_id=config_entry_id, identifiers={(DOMAIN, bridge_device["serial"])}, model=f"{bridge_device['model']} ({bridge_device['type']})", + configuration_url="https://device-login.lutron.com", ) @callback def _async_register_button_devices( - hass, config_entry_id, bridge_device, button_devices_by_id -): + hass: HomeAssistant, + config_entry_id: str, + bridge_device, + button_devices_by_id: dict[int, dict], +) -> dict[str, dr.DeviceEntry]: """Register button devices (Pico Remotes) in the device registry.""" device_registry = dr.async_get(hass) button_devices_by_dr_id = {} + seen = set() for device in button_devices_by_id.values(): - if "serial" not in device: + if "serial" not in device or device["serial"] in seen: continue + seen.add(device["serial"]) dr_device = device_registry.async_get_or_create( - name=device["leap_name"], - suggested_area=device["leap_name"].split("_")[0], + name=device["name"], + suggested_area=device["name"].split("_")[0], manufacturer=MANUFACTURER, config_entry_id=config_entry_id, identifiers={(DOMAIN, device["serial"])}, @@ -248,54 +206,74 @@ def _async_register_button_devices( @callback -def _async_subscribe_pico_remote_events(hass, lip, button_devices_by_id): +def _async_subscribe_pico_remote_events( + hass: HomeAssistant, + bridge_device: Smartbridge, + button_devices_by_id: dict[int, dict], +): """Subscribe to lutron events.""" @callback - def _async_lip_event(lip_message): - if lip_message.mode != LIPMode.DEVICE: - return - - device = button_devices_by_id.get(lip_message.integration_id) + def _async_button_event(button_id, event_type): + device = button_devices_by_id.get(button_id) if not device: return - if lip_message.value == LIP_BUTTON_PRESS: + if event_type == BUTTON_STATUS_PRESSED: action = ACTION_PRESS else: action = ACTION_RELEASE + type_ = device["type"] + name = device["name"] + button_number = device["button_number"] + # The original implementation used LIP instead of LEAP + # so we need to convert the button number to maintain compat + sub_type_to_lip_button = DEVICE_TYPE_SUBTYPE_MAP_TO_LIP[type_] + leap_button_to_sub_type = LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP[type_] + if (sub_type := leap_button_to_sub_type.get(button_number)) is None: + _LOGGER.error( + "Unknown LEAP button number %s is not in %s for %s (%s)", + button_number, + leap_button_to_sub_type, + name, + type_, + ) + return + lip_button_number = sub_type_to_lip_button[sub_type] + hass.bus.async_fire( LUTRON_CASETA_BUTTON_EVENT, { - ATTR_SERIAL: device.get("serial"), - ATTR_TYPE: device.get("type"), - ATTR_BUTTON_NUMBER: lip_message.action_number, - ATTR_DEVICE_NAME: device["Name"], - ATTR_AREA_NAME: device.get("Area", {}).get("Name"), + ATTR_SERIAL: device["serial"], + ATTR_TYPE: type_, + ATTR_BUTTON_NUMBER: lip_button_number, + ATTR_LEAP_BUTTON_NUMBER: button_number, + ATTR_DEVICE_NAME: name, + ATTR_AREA_NAME: name.split("_")[0], ATTR_ACTION: action, }, ) - lip.subscribe(_async_lip_event) - - asyncio.create_task(lip.async_run()) + for button_id in button_devices_by_id: + bridge_device.add_button_subscriber( + str(button_id), + lambda event_type, button_id=button_id: _async_button_event( + button_id, event_type + ), + ) -async def async_unload_entry(hass, config_entry): +async def async_unload_entry( + hass: HomeAssistant, entry: config_entries.ConfigEntry +) -> bool: """Unload the bridge bridge from a config entry.""" - data = hass.data[DOMAIN][config_entry.entry_id] - data[BRIDGE_LEAP].close() - if data[BRIDGE_LIP]: - await data[BRIDGE_LIP].async_stop() - - unload_ok = await hass.config_entries.async_unload_platforms( - config_entry, PLATFORMS - ) - if unload_ok: - hass.data[DOMAIN].pop(config_entry.entry_id) - + data = hass.data[DOMAIN][entry.entry_id] + smartbridge: Smartbridge = data[BRIDGE_LEAP] + await smartbridge.close() + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/lutron_caseta/const.py b/homeassistant/components/lutron_caseta/const.py index 89472f3366b..90303dc0023 100644 --- a/homeassistant/components/lutron_caseta/const.py +++ b/homeassistant/components/lutron_caseta/const.py @@ -11,7 +11,6 @@ ERROR_CANNOT_CONNECT = "cannot_connect" ABORT_REASON_CANNOT_CONNECT = "cannot_connect" BRIDGE_LEAP = "leap" -BRIDGE_LIP = "lip" BRIDGE_DEVICE = "bridge_device" BUTTON_DEVICES = "button_devices" LUTRON_CASETA_BUTTON_EVENT = "lutron_caseta_button_event" @@ -22,7 +21,8 @@ MANUFACTURER = "Lutron Electronics Co., Inc" ATTR_SERIAL = "serial" ATTR_TYPE = "type" -ATTR_BUTTON_NUMBER = "button_number" +ATTR_LEAP_BUTTON_NUMBER = "leap_button_number" +ATTR_BUTTON_NUMBER = "button_number" # LIP button number ATTR_DEVICE_NAME = "device_name" ATTR_AREA_NAME = "area_name" ATTR_ACTION = "action" diff --git a/homeassistant/components/lutron_caseta/device_trigger.py b/homeassistant/components/lutron_caseta/device_trigger.py index ce50923f2f5..4974a10d9ba 100644 --- a/homeassistant/components/lutron_caseta/device_trigger.py +++ b/homeassistant/components/lutron_caseta/device_trigger.py @@ -46,106 +46,180 @@ LUTRON_BUTTON_TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( ) -PICO_2_BUTTON_BUTTON_TYPES = { +PICO_2_BUTTON_BUTTON_TYPES_TO_LIP = { "on": 2, "off": 4, } +PICO_2_BUTTON_BUTTON_TYPES_TO_LEAP = { + "on": 0, + "off": 2, +} +LEAP_TO_PICO_2_BUTTON_BUTTON_TYPES = { + v: k for k, v in PICO_2_BUTTON_BUTTON_TYPES_TO_LEAP.items() +} PICO_2_BUTTON_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( { - vol.Required(CONF_SUBTYPE): vol.In(PICO_2_BUTTON_BUTTON_TYPES), + vol.Required(CONF_SUBTYPE): vol.In(PICO_2_BUTTON_BUTTON_TYPES_TO_LIP), } ) -PICO_2_BUTTON_RAISE_LOWER_BUTTON_TYPES = { +PICO_2_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LIP = { "on": 2, "off": 4, "raise": 5, "lower": 6, } +PICO_2_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LEAP = { + "on": 0, + "off": 2, + "raise": 3, + "lower": 4, +} +LEAP_TO_PICO_2_BUTTON_RAISE_LOWER_BUTTON_TYPES = { + v: k for k, v in PICO_2_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LEAP.items() +} PICO_2_BUTTON_RAISE_LOWER_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( { - vol.Required(CONF_SUBTYPE): vol.In(PICO_2_BUTTON_RAISE_LOWER_BUTTON_TYPES), + vol.Required(CONF_SUBTYPE): vol.In( + PICO_2_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LIP + ), } ) -PICO_3_BUTTON_BUTTON_TYPES = { +PICO_3_BUTTON_BUTTON_TYPES_TO_LIP = { "on": 2, "stop": 3, "off": 4, } +PICO_3_BUTTON_BUTTON_TYPES_TO_LEAP = { + "on": 0, + "stop": 1, + "off": 2, +} +LEAP_TO_PICO_3_BUTTON_BUTTON_TYPES = { + v: k for k, v in PICO_3_BUTTON_BUTTON_TYPES_TO_LEAP.items() +} PICO_3_BUTTON_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( { - vol.Required(CONF_SUBTYPE): vol.In(PICO_3_BUTTON_BUTTON_TYPES), + vol.Required(CONF_SUBTYPE): vol.In(PICO_3_BUTTON_BUTTON_TYPES_TO_LIP), } ) -PICO_3_BUTTON_RAISE_LOWER_BUTTON_TYPES = { +PICO_3_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LIP = { "on": 2, "stop": 3, "off": 4, "raise": 5, "lower": 6, } +PICO_3_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LEAP = { + "on": 0, + "stop": 1, + "off": 2, + "raise": 3, + "lower": 4, +} +LEAP_TO_PICO_3_BUTTON_RAISE_LOWER_BUTTON_TYPES = { + v: k for k, v in PICO_3_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LEAP.items() +} PICO_3_BUTTON_RAISE_LOWER_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( { - vol.Required(CONF_SUBTYPE): vol.In(PICO_3_BUTTON_RAISE_LOWER_BUTTON_TYPES), + vol.Required(CONF_SUBTYPE): vol.In( + PICO_3_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LIP + ), } ) -PICO_4_BUTTON_BUTTON_TYPES = { +PICO_4_BUTTON_BUTTON_TYPES_TO_LIP = { "button_1": 8, "button_2": 9, "button_3": 10, "button_4": 11, } +PICO_4_BUTTON_BUTTON_TYPES_TO_LEAP = { + "button_1": 1, + "button_2": 2, + "button_3": 3, + "button_4": 4, +} +LEAP_TO_PICO_4_BUTTON_BUTTON_TYPES = { + v: k for k, v in PICO_4_BUTTON_BUTTON_TYPES_TO_LEAP.items() +} PICO_4_BUTTON_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( { - vol.Required(CONF_SUBTYPE): vol.In(PICO_4_BUTTON_BUTTON_TYPES), + vol.Required(CONF_SUBTYPE): vol.In(PICO_4_BUTTON_BUTTON_TYPES_TO_LIP), } ) -PICO_4_BUTTON_ZONE_BUTTON_TYPES = { +PICO_4_BUTTON_ZONE_BUTTON_TYPES_TO_LIP = { "on": 8, "raise": 9, "lower": 10, "off": 11, } +PICO_4_BUTTON_ZONE_BUTTON_TYPES_TO_LEAP = { + "on": 1, + "raise": 2, + "lower": 3, + "off": 4, +} +LEAP_TO_PICO_4_BUTTON_ZONE_BUTTON_TYPES = { + v: k for k, v in PICO_4_BUTTON_ZONE_BUTTON_TYPES_TO_LEAP.items() +} PICO_4_BUTTON_ZONE_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( { - vol.Required(CONF_SUBTYPE): vol.In(PICO_4_BUTTON_ZONE_BUTTON_TYPES), + vol.Required(CONF_SUBTYPE): vol.In(PICO_4_BUTTON_ZONE_BUTTON_TYPES_TO_LIP), } ) -PICO_4_BUTTON_SCENE_BUTTON_TYPES = { +PICO_4_BUTTON_SCENE_BUTTON_TYPES_TO_LIP = { "button_1": 8, "button_2": 9, "button_3": 10, "off": 11, } +PICO_4_BUTTON_SCENE_BUTTON_TYPES_TO_LEAP = { + "button_1": 1, + "button_2": 2, + "button_3": 3, + "off": 4, +} +LEAP_TO_PICO_4_BUTTON_SCENE_BUTTON_TYPES = { + v: k for k, v in PICO_4_BUTTON_SCENE_BUTTON_TYPES_TO_LEAP.items() +} PICO_4_BUTTON_SCENE_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( { - vol.Required(CONF_SUBTYPE): vol.In(PICO_4_BUTTON_SCENE_BUTTON_TYPES), + vol.Required(CONF_SUBTYPE): vol.In(PICO_4_BUTTON_SCENE_BUTTON_TYPES_TO_LIP), } ) -PICO_4_BUTTON_2_GROUP_BUTTON_TYPES = { +PICO_4_BUTTON_2_GROUP_BUTTON_TYPES_TO_LIP = { "group_1_button_1": 8, "group_1_button_2": 9, "group_2_button_1": 10, "group_2_button_2": 11, } +PICO_4_BUTTON_2_GROUP_BUTTON_TYPES_TO_LEAP = { + "group_1_button_1": 1, + "group_1_button_2": 2, + "group_2_button_1": 3, + "group_2_button_2": 4, +} +LEAP_TO_PICO_4_BUTTON_2_GROUP_BUTTON_TYPES = { + v: k for k, v in PICO_4_BUTTON_2_GROUP_BUTTON_TYPES_TO_LEAP.items() +} PICO_4_BUTTON_2_GROUP_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( { - vol.Required(CONF_SUBTYPE): vol.In(PICO_4_BUTTON_2_GROUP_BUTTON_TYPES), + vol.Required(CONF_SUBTYPE): vol.In(PICO_4_BUTTON_2_GROUP_BUTTON_TYPES_TO_LIP), } ) -FOUR_GROUP_REMOTE_BUTTON_TYPES = { +FOUR_GROUP_REMOTE_BUTTON_TYPES_TO_LIP = { "open_all": 2, "stop_all": 3, "close_all": 4, @@ -172,9 +246,39 @@ FOUR_GROUP_REMOTE_BUTTON_TYPES = { "raise_4": 37, "lower_4": 38, } +FOUR_GROUP_REMOTE_BUTTON_TYPES_TO_LEAP = { + "open_all": 0, + "stop_all": 1, + "close_all": 2, + "raise_all": 3, + "lower_all": 4, + "open_1": 5, + "stop_1": 6, + "close_1": 7, + "raise_1": 8, + "lower_1": 9, + "open_2": 10, + "stop_2": 11, + "close_2": 12, + "raise_2": 13, + "lower_2": 14, + "open_3": 15, + "stop_3": 16, + "close_3": 17, + "raise_3": 18, + "lower_3": 19, + "open_4": 20, + "stop_4": 21, + "close_4": 22, + "raise_4": 23, + "lower_4": 24, +} +LEAP_TO_FOUR_GROUP_REMOTE_BUTTON_TYPES = { + v: k for k, v in FOUR_GROUP_REMOTE_BUTTON_TYPES_TO_LEAP.items() +} FOUR_GROUP_REMOTE_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( { - vol.Required(CONF_SUBTYPE): vol.In(FOUR_GROUP_REMOTE_BUTTON_TYPES), + vol.Required(CONF_SUBTYPE): vol.In(FOUR_GROUP_REMOTE_BUTTON_TYPES_TO_LIP), } ) @@ -190,16 +294,28 @@ DEVICE_TYPE_SCHEMA_MAP = { "FourGroupRemote": FOUR_GROUP_REMOTE_TRIGGER_SCHEMA, } -DEVICE_TYPE_SUBTYPE_MAP = { - "Pico2Button": PICO_2_BUTTON_BUTTON_TYPES, - "Pico2ButtonRaiseLower": PICO_2_BUTTON_RAISE_LOWER_BUTTON_TYPES, - "Pico3Button": PICO_3_BUTTON_BUTTON_TYPES, - "Pico3ButtonRaiseLower": PICO_3_BUTTON_RAISE_LOWER_BUTTON_TYPES, - "Pico4Button": PICO_4_BUTTON_BUTTON_TYPES, - "Pico4ButtonScene": PICO_4_BUTTON_SCENE_BUTTON_TYPES, - "Pico4ButtonZone": PICO_4_BUTTON_ZONE_BUTTON_TYPES, - "Pico4Button2Group": PICO_4_BUTTON_2_GROUP_BUTTON_TYPES, - "FourGroupRemote": FOUR_GROUP_REMOTE_BUTTON_TYPES, +DEVICE_TYPE_SUBTYPE_MAP_TO_LIP = { + "Pico2Button": PICO_2_BUTTON_BUTTON_TYPES_TO_LIP, + "Pico2ButtonRaiseLower": PICO_2_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LIP, + "Pico3Button": PICO_3_BUTTON_BUTTON_TYPES_TO_LIP, + "Pico3ButtonRaiseLower": PICO_3_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LIP, + "Pico4Button": PICO_4_BUTTON_BUTTON_TYPES_TO_LIP, + "Pico4ButtonScene": PICO_4_BUTTON_SCENE_BUTTON_TYPES_TO_LIP, + "Pico4ButtonZone": PICO_4_BUTTON_ZONE_BUTTON_TYPES_TO_LIP, + "Pico4Button2Group": PICO_4_BUTTON_2_GROUP_BUTTON_TYPES_TO_LIP, + "FourGroupRemote": FOUR_GROUP_REMOTE_BUTTON_TYPES_TO_LIP, +} + +LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP = { + "Pico2Button": LEAP_TO_PICO_2_BUTTON_BUTTON_TYPES, + "Pico2ButtonRaiseLower": LEAP_TO_PICO_2_BUTTON_RAISE_LOWER_BUTTON_TYPES, + "Pico3Button": LEAP_TO_PICO_3_BUTTON_BUTTON_TYPES, + "Pico3ButtonRaiseLower": LEAP_TO_PICO_3_BUTTON_RAISE_LOWER_BUTTON_TYPES, + "Pico4Button": LEAP_TO_PICO_4_BUTTON_BUTTON_TYPES, + "Pico4ButtonScene": LEAP_TO_PICO_4_BUTTON_SCENE_BUTTON_TYPES, + "Pico4ButtonZone": LEAP_TO_PICO_4_BUTTON_ZONE_BUTTON_TYPES, + "Pico4Button2Group": LEAP_TO_PICO_4_BUTTON_2_GROUP_BUTTON_TYPES, + "FourGroupRemote": LEAP_TO_FOUR_GROUP_REMOTE_BUTTON_TYPES, } TRIGGER_SCHEMA = vol.Any( @@ -238,7 +354,7 @@ async def async_get_triggers( if not (device := get_button_device_by_dr_id(hass, device_id)): raise InvalidDeviceAutomationConfig(f"Device not found: {device_id}") - valid_buttons = DEVICE_TYPE_SUBTYPE_MAP.get(device["type"], []) + valid_buttons = DEVICE_TYPE_SUBTYPE_MAP_TO_LIP.get(device["type"], []) for trigger in SUPPORTED_INPUTS_EVENTS_TYPES: for subtype in valid_buttons: @@ -273,7 +389,7 @@ async def async_attach_trigger( device_type = _device_model_to_type(device.model) _, serial = list(device.identifiers)[0] schema = DEVICE_TYPE_SCHEMA_MAP.get(device_type) - valid_buttons = DEVICE_TYPE_SUBTYPE_MAP.get(device_type) + valid_buttons = DEVICE_TYPE_SUBTYPE_MAP_TO_LIP.get(device_type) config = schema(config) event_config = { event_trigger.CONF_PLATFORM: CONF_EVENT, diff --git a/homeassistant/components/lutron_caseta/manifest.json b/homeassistant/components/lutron_caseta/manifest.json index b6f0785ffe7..b6d3eb51f7a 100644 --- a/homeassistant/components/lutron_caseta/manifest.json +++ b/homeassistant/components/lutron_caseta/manifest.json @@ -2,7 +2,7 @@ "domain": "lutron_caseta", "name": "Lutron Cas\u00e9ta", "documentation": "https://www.home-assistant.io/integrations/lutron_caseta", - "requirements": ["pylutron-caseta==0.11.0", "aiolip==1.1.6"], + "requirements": ["pylutron-caseta==0.13.0"], "config_flow": true, "zeroconf": ["_leap._tcp.local."], "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 45b538f86c1..cf103d060cf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -209,9 +209,6 @@ aiolifx==0.7.0 # homeassistant.components.lifx aiolifx_effects==0.2.2 -# homeassistant.components.lutron_caseta -aiolip==1.1.6 - # homeassistant.components.lookin aiolookin==0.1.0 @@ -1625,7 +1622,7 @@ pylitejet==0.3.0 pylitterbot==2021.12.0 # homeassistant.components.lutron_caseta -pylutron-caseta==0.11.0 +pylutron-caseta==0.13.0 # homeassistant.components.lutron pylutron==0.2.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9b8813faf3c..70245fcf559 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -142,9 +142,6 @@ aiohue==3.0.6 # homeassistant.components.apache_kafka aiokafka==0.6.0 -# homeassistant.components.lutron_caseta -aiolip==1.1.6 - # homeassistant.components.lookin aiolookin==0.1.0 @@ -995,7 +992,7 @@ pylitejet==0.3.0 pylitterbot==2021.12.0 # homeassistant.components.lutron_caseta -pylutron-caseta==0.11.0 +pylutron-caseta==0.13.0 # homeassistant.components.mailgun pymailgunner==1.4 From a63fa532752522ea04d060e8098d80f96da8691c Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 18 Dec 2021 23:53:40 -0800 Subject: [PATCH 0732/2644] Persist nest media events to disk backed storage (#61641) * Persist nest media events to disk backed storage Persist nest events in the media player to disk, targeting about ~500mb per camera device as a cap. Events are stored in config/nest/event_media/. Add a NestEventMediaStore is used for persistence. It has three main jobs: - Read/write the key/value data that holds event data (event type, time, device, etc) - Read/write media contents to disk - Pick the filename for the media event based on device and event deatils The nest event media manager library handles cache management and eviction, and by default uses an in memory cache. Home Assistant nest integration now provides the disk backed implementation, which is invoked by the nest library. The store reads the event metadata key/value dict on startup, and then writes it back with a short delay of 5 seconds to avoid unnecessary writes. Future work planned includes: - Possibly a small memory buffer for media objects themselves. This could make sense when adding thumbnails to the media player grid to avoid unnecessary fetches - Transcoding mp4 clips to animated image previews * Address style errors * Cleanup from CI test/pylint/etc. * Put media for each device into its own directory * Update comments for media store * Decrease # of events to lower disk requirements Target more like 1k events, to reduce disk needs. * Address PR feedback * Update homeassistant/components/nest/media_source.py Co-authored-by: Paulus Schoutsen * Ignore incorrect mypy in nest library * Fix pylint errors Co-authored-by: Paulus Schoutsen --- homeassistant/components/nest/__init__.py | 17 +- homeassistant/components/nest/media_source.py | 180 ++++++++- tests/components/nest/test_media_source.py | 367 +++++++++++++++++- 3 files changed, 540 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index eebbdcf026a..3cad979165a 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -51,7 +51,7 @@ from .const import ( ) from .events import EVENT_NAME_MAP, NEST_EVENT from .legacy import async_setup_legacy, async_setup_legacy_entry -from .media_source import get_media_source_devices +from .media_source import async_get_media_event_store, get_media_source_devices _LOGGER = logging.getLogger(__name__) @@ -87,10 +87,11 @@ PLATFORMS = [Platform.SENSOR, Platform.CAMERA, Platform.CLIMATE] WEB_AUTH_DOMAIN = DOMAIN INSTALLED_AUTH_DOMAIN = f"{DOMAIN}.installed" -# Fetch media for events with an in memory cache. The largest media items -# are mp4 clips at ~90kb each, so this totals a few MB per camera. -# Note: Media for events can only be published within 30 seconds of the event -EVENT_MEDIA_CACHE_SIZE = 64 +# Fetch media events with a disk backed cache, with a limit for each camera +# device. The largest media items are mp4 clips at ~120kb each, and we target +# ~125MB of storage per camera to try to balance a reasonable user experience +# for event history not not filling the disk. +EVENT_MEDIA_CACHE_SIZE = 1024 # number of events class WebAuth(config_entry_oauth2_flow.LocalOAuth2Implementation): @@ -169,6 +170,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ), ) + hass.http.register_view(NestEventMediaView(hass)) + return True @@ -215,6 +218,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Keep media for last N events in memory subscriber.cache_policy.event_cache_size = EVENT_MEDIA_CACHE_SIZE subscriber.cache_policy.fetch = True + # Use disk backed event media store + subscriber.cache_policy.store = await async_get_media_event_store(hass, subscriber) callback = SignalUpdateCallback(hass) subscriber.set_update_callback(callback.async_handle_event) @@ -248,8 +253,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_setup_platforms(entry, PLATFORMS) - hass.http.register_view(NestEventMediaView(hass)) - return True diff --git a/homeassistant/components/nest/media_source.py b/homeassistant/components/nest/media_source.py index 0f26331d7e5..66f6cec43d1 100644 --- a/homeassistant/components/nest/media_source.py +++ b/homeassistant/components/nest/media_source.py @@ -21,10 +21,13 @@ from __future__ import annotations from collections.abc import Mapping from dataclasses import dataclass import logging +import os from google_nest_sdm.camera_traits import CameraClipPreviewTrait, CameraEventImageTrait from google_nest_sdm.device import Device from google_nest_sdm.event import EventImageType, ImageEventBase +from google_nest_sdm.event_media import EventMediaStore +from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber from homeassistant.components.media_player.const import ( MEDIA_CLASS_DIRECTORY, @@ -42,12 +45,14 @@ from homeassistant.components.media_source.models import ( PlayMedia, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.storage import Store from homeassistant.helpers.template import DATE_STR_FORMAT -from homeassistant.util import dt as dt_util +from homeassistant.util import dt as dt_util, raise_if_invalid_filename from .const import DATA_SUBSCRIBER, DOMAIN from .device_info import NestDeviceInfo -from .events import MEDIA_SOURCE_EVENT_TITLE_MAP +from .events import EVENT_NAME_MAP, MEDIA_SOURCE_EVENT_TITLE_MAP _LOGGER = logging.getLogger(__name__) @@ -56,6 +61,175 @@ DEVICE_TITLE_FORMAT = "{device_name}: Recent Events" CLIP_TITLE_FORMAT = "{event_name} @ {event_time}" EVENT_MEDIA_API_URL_FORMAT = "/api/nest/event_media/{device_id}/{event_id}" +STORAGE_KEY = "nest.event_media" +STORAGE_VERSION = 1 +# Buffer writes every few minutes (plus guaranteed to be written at shutdown) +STORAGE_SAVE_DELAY_SECONDS = 120 +# Path under config directory +MEDIA_PATH = f"{DOMAIN}/event_media" + +# Size of small in-memory disk cache to avoid excessive disk reads +DISK_READ_LRU_MAX_SIZE = 32 + + +async def async_get_media_event_store( + hass: HomeAssistant, subscriber: GoogleNestSubscriber +) -> EventMediaStore: + """Create the disk backed EventMediaStore.""" + media_path = hass.config.path(MEDIA_PATH) + + def mkdir() -> None: + os.makedirs(media_path, exist_ok=True) + + await hass.async_add_executor_job(mkdir) + store = Store(hass, STORAGE_VERSION, STORAGE_KEY, private=True) + return NestEventMediaStore(hass, subscriber, store, media_path) + + +class NestEventMediaStore(EventMediaStore): + """Storage hook to locally persist nest media for events. + + This interface is meant to provide two storage features: + - media storage of events (jpgs, mp4s) + - metadata about events (e.g. motion, person), filename of the media, etc. + + The default implementation in nest is in memory, and this allows the data + to be backed by disk. + + The nest event media manager internal to the subscriber manages the lifetime + of individual objects stored here (e.g. purging when going over storage + limits). This store manages the addition/deletion once instructed. + """ + + def __init__( + self, + hass: HomeAssistant, + subscriber: GoogleNestSubscriber, + store: Store, + media_path: str, + ) -> None: + """Initialize NestEventMediaStore.""" + self._hass = hass + self._subscriber = subscriber + self._store = store + self._media_path = media_path + self._data: dict | None = None + self._devices: Mapping[str, str] | None = {} + + async def async_load(self) -> dict | None: + """Load data.""" + if self._data is None: + self._devices = await self._get_devices() + data = await self._store.async_load() + if data is None: + _LOGGER.debug("Loaded empty event store") + self._data = {} + elif isinstance(data, dict): + _LOGGER.debug("Loaded event store with %d records", len(data)) + self._data = data + else: + raise ValueError( + "Unexpected data in storage version={}, key={}".format( + STORAGE_VERSION, STORAGE_KEY + ) + ) + return self._data + + async def async_save(self, data: dict) -> None: # type: ignore[override] + """Save data.""" + self._data = data + + def provide_data() -> dict: + return data + + self._store.async_delay_save(provide_data, STORAGE_SAVE_DELAY_SECONDS) + + def get_media_key(self, device_id: str, event: ImageEventBase) -> str: + """Return the filename to use for a new event.""" + # Convert a nest device id to a home assistant device id + device_id_str = ( + self._devices.get(device_id, f"{device_id}-unknown_device") + if self._devices + else "unknown_device" + ) + event_id_str = event.event_session_id + try: + raise_if_invalid_filename(event_id_str) + except ValueError: + event_id_str = "" + time_str = str(int(event.timestamp.timestamp())) + event_type_str = EVENT_NAME_MAP.get(event.event_type, "event") + suffix = "jpg" if event.event_image_type == EventImageType.IMAGE else "mp4" + return f"{device_id_str}/{time_str}-{event_id_str}-{event_type_str}.{suffix}" + + def get_media_filename(self, media_key: str) -> str: + """Return the filename in storage for a media key.""" + return f"{self._media_path}/{media_key}" + + async def async_load_media(self, media_key: str) -> bytes | None: + """Load media content.""" + filename = self.get_media_filename(media_key) + + def load_media(filename: str) -> bytes | None: + if not os.path.exists(filename): + return None + _LOGGER.debug("Reading event media from disk store: %s", filename) + with open(filename, "rb") as media: + return media.read() + + try: + return await self._hass.async_add_executor_job(load_media, filename) + except OSError as err: + _LOGGER.error("Unable to read media file: %s %s", filename, err) + return None + + async def async_save_media(self, media_key: str, content: bytes) -> None: + """Write media content.""" + filename = self.get_media_filename(media_key) + + def save_media(filename: str, content: bytes) -> None: + os.makedirs(os.path.dirname(filename), exist_ok=True) + if os.path.exists(filename): + _LOGGER.debug( + "Event media already exists, not overwriting: %s", filename + ) + return + _LOGGER.debug("Saving event media to disk store: %s", filename) + with open(filename, "wb") as media: + media.write(content) + + try: + await self._hass.async_add_executor_job(save_media, filename, content) + except OSError as err: + _LOGGER.error("Unable to write media file: %s %s", filename, err) + + async def async_remove_media(self, media_key: str) -> None: + """Remove media content.""" + filename = self.get_media_filename(media_key) + + def remove_media(filename: str) -> None: + if not os.path.exists(filename): + return None + _LOGGER.debug("Removing event media from disk store: %s", filename) + os.remove(filename) + + try: + await self._hass.async_add_executor_job(remove_media, filename) + except OSError as err: + _LOGGER.error("Unable to remove media file: %s %s", filename, err) + + async def _get_devices(self) -> Mapping[str, str]: + """Return a mapping of nest device id to home assistant device id.""" + device_registry = dr.async_get(self._hass) + device_manager = await self._subscriber.async_get_device_manager() + devices = {} + for device in device_manager.devices.values(): + if device_entry := device_registry.async_get_device( + {(DOMAIN, device.name)} + ): + devices[device.name] = device_entry.id + return devices + async def async_get_media_source(hass: HomeAssistant) -> MediaSource: """Set up Nest media source.""" @@ -69,7 +243,7 @@ async def get_media_source_devices(hass: HomeAssistant) -> Mapping[str, Device]: return {} subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER] device_manager = await subscriber.async_get_device_manager() - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) devices = {} for device in device_manager.devices.values(): if not ( diff --git a/tests/components/nest/test_media_source.py b/tests/components/nest/test_media_source.py index 95f2afa8a06..84147fe9d94 100644 --- a/tests/components/nest/test_media_source.py +++ b/tests/components/nest/test_media_source.py @@ -6,6 +6,10 @@ as media in the media source. import datetime from http import HTTPStatus +import shutil +from typing import Generator +from unittest.mock import Mock, patch +import uuid import aiohttp from google_nest_sdm.device import Device @@ -19,9 +23,15 @@ from homeassistant.components.media_source.error import Unresolvable from homeassistant.config_entries import ConfigEntryState from homeassistant.helpers import device_registry as dr from homeassistant.helpers.template import DATE_STR_FORMAT +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from .common import async_setup_sdm_platform +from .common import ( + CONFIG, + FakeSubscriber, + async_setup_sdm_platform, + create_config_entry, +) DOMAIN = "nest" DEVICE_ID = "example/api/device/id" @@ -49,6 +59,7 @@ BATTERY_CAMERA_TRAITS = { "sdm.devices.traits.CameraPerson": {}, "sdm.devices.traits.CameraMotion": {}, } + PERSON_EVENT = "sdm.devices.events.CameraPerson.Person" MOTION_EVENT = "sdm.devices.events.CameraMotion.Motion" @@ -63,6 +74,17 @@ IMAGE_BYTES_FROM_EVENT = b"test url image bytes" IMAGE_AUTHORIZATION_HEADERS = {"Authorization": "Basic g.0.eventToken"} +@pytest.fixture(autouse=True) +def cleanup_media_storage(hass): + """Test cleanup, remove any media storage persisted during the test.""" + tmp_path = str(uuid.uuid4()) + m = Mock(spec=float) + m.return_value = tmp_path + with patch("homeassistant.components.nest.media_source.MEDIA_PATH", new_callable=m): + yield + shutil.rmtree(hass.config.path(tmp_path), ignore_errors=True) + + async def async_setup_devices(hass, auth, device_type, traits={}, events=[]): """Set up the platform and prerequisites.""" devices = { @@ -115,6 +137,22 @@ def create_event_message(event_data, timestamp, device_id=None): ) +def create_battery_event_data( + event_type, event_session_id=EVENT_SESSION_ID, event_id="n:2" +): + """Return event payload data for a battery camera event.""" + return { + event_type: { + "eventSessionId": event_session_id, + "eventId": event_id, + }, + "sdm.devices.events.CameraClipPreview.ClipPreview": { + "eventSessionId": event_session_id, + "previewUrl": "https://127.0.0.1/example", + }, + } + + async def test_no_eligible_devices(hass, auth): """Test a media source with no eligible camera devices.""" await async_setup_devices( @@ -335,7 +373,7 @@ async def test_event_order(hass, auth): event_timestamp_string = event_timestamp2.strftime(DATE_STR_FORMAT) assert browse.children[0].title == f"Motion @ {event_timestamp_string}" assert not browse.children[0].can_expand - assert not browse.can_play + assert not browse.children[0].can_play # Person event is next assert browse.children[1].domain == DOMAIN @@ -344,7 +382,7 @@ async def test_event_order(hass, auth): event_timestamp_string = event_timestamp1.strftime(DATE_STR_FORMAT) assert browse.children[1].title == f"Person @ {event_timestamp_string}" assert not browse.children[1].can_expand - assert not browse.can_play + assert not browse.children[1].can_play async def test_browse_invalid_device_id(hass, auth): @@ -436,16 +474,6 @@ async def test_resolve_invalid_event_id(hass, auth): async def test_camera_event_clip_preview(hass, auth, hass_client): """Test an event for a battery camera video clip.""" event_timestamp = dt_util.now() - event_data = { - "sdm.devices.events.CameraMotion.Motion": { - "eventSessionId": EVENT_SESSION_ID, - "eventId": "n:2", - }, - "sdm.devices.events.CameraClipPreview.ClipPreview": { - "eventSessionId": EVENT_SESSION_ID, - "previewUrl": "https://127.0.0.1/example", - }, - } await async_setup_devices( hass, auth, @@ -453,7 +481,7 @@ async def test_camera_event_clip_preview(hass, auth, hass_client): BATTERY_CAMERA_TRAITS, events=[ create_event_message( - event_data, + create_battery_event_data(MOTION_EVENT), timestamp=event_timestamp, ), ], @@ -692,3 +720,314 @@ async def test_multiple_devices(hass, auth, hass_client): hass, f"{const.URI_SCHEME}{DOMAIN}/{device2.id}" ) assert len(browse.children) == 3 + + +@pytest.fixture +def event_store() -> Generator[None, None, None]: + """Persist changes to event store immediately.""" + m = Mock(spec=float) + m.return_value = 0 + with patch( + "homeassistant.components.nest.media_source.STORAGE_SAVE_DELAY_SECONDS", + new_callable=m, + ): + yield + + +async def test_media_store_persistence(hass, auth, hass_client, event_store): + """Test the disk backed media store persistence.""" + nest_device = Device.MakeDevice( + { + "name": DEVICE_ID, + "type": CAMERA_DEVICE_TYPE, + "traits": BATTERY_CAMERA_TRAITS, + }, + auth=auth, + ) + + subscriber = FakeSubscriber() + device_manager = await subscriber.async_get_device_manager() + device_manager.add_device(nest_device) + # Fetch media for events when published + subscriber.cache_policy.fetch = True + + config_entry = create_config_entry(hass) + + with patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation" + ), patch("homeassistant.components.nest.PLATFORMS", [PLATFORM]), patch( + "homeassistant.components.nest.api.GoogleNestSubscriber", + return_value=subscriber, + ): + assert await async_setup_component(hass, DOMAIN, CONFIG) + await hass.async_block_till_done() + + device_registry = dr.async_get(hass) + device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) + assert device + assert device.name == DEVICE_NAME + + auth.responses = [ + aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT), + ] + event_timestamp = dt_util.now() + await subscriber.async_receive_event( + create_event_message( + create_battery_event_data(MOTION_EVENT), timestamp=event_timestamp + ) + ) + + # Browse to event + browse = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}" + ) + assert len(browse.children) == 1 + assert browse.children[0].domain == DOMAIN + assert browse.children[0].identifier == f"{device.id}/{EVENT_SESSION_ID}" + event_timestamp_string = event_timestamp.strftime(DATE_STR_FORMAT) + assert browse.children[0].title == f"Motion @ {event_timestamp_string}" + assert not browse.children[0].can_expand + assert browse.children[0].can_play + + media = await media_source.async_resolve_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{EVENT_SESSION_ID}" + ) + assert media.url == f"/api/nest/event_media/{device.id}/{EVENT_SESSION_ID}" + assert media.mime_type == "video/mp4" + + # Fetch event media + client = await hass_client() + response = await client.get(media.url) + assert response.status == HTTPStatus.OK, "Response not matched: %s" % response + contents = await response.read() + assert contents == IMAGE_BYTES_FROM_EVENT + + # Ensure event media store persists to disk + await hass.async_block_till_done() + + # Unload the integration. + assert config_entry.state == ConfigEntryState.LOADED + assert await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.NOT_LOADED + + # Now rebuild the entire integration and verify that all persisted storage + # can be re-loaded from disk. + subscriber = FakeSubscriber() + device_manager = await subscriber.async_get_device_manager() + device_manager.add_device(nest_device) + # Fetch media for events when published + subscriber.cache_policy.fetch = True + + with patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation" + ), patch("homeassistant.components.nest.PLATFORMS", [PLATFORM]), patch( + "homeassistant.components.nest.api.GoogleNestSubscriber", + return_value=subscriber, + ): + await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() + + device_registry = dr.async_get(hass) + device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) + assert device + assert device.name == DEVICE_NAME + + # Verify event metadata exists + browse = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}" + ) + assert len(browse.children) == 1 + assert browse.children[0].domain == DOMAIN + assert browse.children[0].identifier == f"{device.id}/{EVENT_SESSION_ID}" + event_timestamp_string = event_timestamp.strftime(DATE_STR_FORMAT) + assert browse.children[0].title == f"Motion @ {event_timestamp_string}" + assert not browse.children[0].can_expand + assert browse.children[0].can_play + + media = await media_source.async_resolve_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{EVENT_SESSION_ID}" + ) + assert media.url == f"/api/nest/event_media/{device.id}/{EVENT_SESSION_ID}" + assert media.mime_type == "video/mp4" + + # Verify media exists + response = await client.get(media.url) + assert response.status == HTTPStatus.OK, "Response not matched: %s" % response + contents = await response.read() + assert contents == IMAGE_BYTES_FROM_EVENT + + +async def test_media_store_filesystem_error(hass, auth, hass_client): + """Test a filesystem error read/writing event media.""" + event_timestamp = dt_util.now() + await async_setup_devices( + hass, + auth, + CAMERA_DEVICE_TYPE, + BATTERY_CAMERA_TRAITS, + events=[ + create_event_message( + create_battery_event_data(MOTION_EVENT), + timestamp=event_timestamp, + ), + ], + ) + + assert len(hass.states.async_all()) == 1 + camera = hass.states.get("camera.front") + assert camera is not None + + device_registry = dr.async_get(hass) + device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) + assert device + assert device.name == DEVICE_NAME + + auth.responses = [ + aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT), + ] + + # The client fetches the media from the server, but has a failure when + # persisting the media to disk. + client = await hass_client() + with patch("homeassistant.components.nest.media_source.open", side_effect=OSError): + response = await client.get( + f"/api/nest/event_media/{device.id}/{EVENT_SESSION_ID}" + ) + assert response.status == HTTPStatus.OK, "Response not matched: %s" % response + contents = await response.read() + assert contents == IMAGE_BYTES_FROM_EVENT + await hass.async_block_till_done() + + # Fetch the media again, and since the object does not exist in the cache it + # needs to be fetched again. The server returns an error to prove that it was + # not a cache read. A second attempt succeeds. + auth.responses = [ + aiohttp.web.Response(status=HTTPStatus.INTERNAL_SERVER_ERROR), + aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT), + ] + # First attempt, server fails when fetching + response = await client.get(f"/api/nest/event_media/{device.id}/{EVENT_SESSION_ID}") + assert response.status == HTTPStatus.INTERNAL_SERVER_ERROR, ( + "Response not matched: %s" % response + ) + + # Second attempt, server responds success + response = await client.get(f"/api/nest/event_media/{device.id}/{EVENT_SESSION_ID}") + assert response.status == HTTPStatus.OK, "Response not matched: %s" % response + contents = await response.read() + assert contents == IMAGE_BYTES_FROM_EVENT + + # Third attempt reads from the disk cache with no server fetch + response = await client.get(f"/api/nest/event_media/{device.id}/{EVENT_SESSION_ID}") + assert response.status == HTTPStatus.OK, "Response not matched: %s" % response + contents = await response.read() + assert contents == IMAGE_BYTES_FROM_EVENT + + auth.responses = [ + aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT), + ] + # Exercise a failure reading from the disk cache. Re-populate from server and write to disk ok + with patch("homeassistant.components.nest.media_source.open", side_effect=OSError): + response = await client.get( + f"/api/nest/event_media/{device.id}/{EVENT_SESSION_ID}" + ) + assert response.status == HTTPStatus.OK, "Response not matched: %s" % response + contents = await response.read() + assert contents == IMAGE_BYTES_FROM_EVENT + await hass.async_block_till_done() + + response = await client.get(f"/api/nest/event_media/{device.id}/{EVENT_SESSION_ID}") + assert response.status == HTTPStatus.OK, "Response not matched: %s" % response + contents = await response.read() + assert contents == IMAGE_BYTES_FROM_EVENT + + +async def test_camera_event_media_eviction(hass, auth, hass_client): + """Test media files getting evicted from the cache.""" + + # Set small cache size for testing eviction + m = Mock(spec=float) + m.return_value = 5 + with patch("homeassistant.components.nest.EVENT_MEDIA_CACHE_SIZE", new_callable=m): + subscriber = await async_setup_devices( + hass, + auth, + CAMERA_DEVICE_TYPE, + BATTERY_CAMERA_TRAITS, + ) + + # Media fetched as soon as it is published + subscriber.cache_policy.fetch = True + + device_registry = dr.async_get(hass) + device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) + assert device + assert device.name == DEVICE_NAME + + # Browse to the device + browse = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}" + ) + assert browse.domain == DOMAIN + assert browse.identifier == device.id + assert browse.title == "Front: Recent Events" + assert browse.can_expand + + # No events published yet + assert len(browse.children) == 0 + + event_timestamp = dt_util.now() + for i in range(0, 7): + auth.responses = [aiohttp.web.Response(body=f"image-bytes-{i}".encode())] + ts = event_timestamp + datetime.timedelta(seconds=i) + await subscriber.async_receive_event( + create_event_message( + create_battery_event_data( + MOTION_EVENT, event_session_id=f"event-session-{i}" + ), + timestamp=ts, + ) + ) + await hass.async_block_till_done() + + # Cache is limited to 5 events removing media as the cache is filled + browse = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}" + ) + assert len(browse.children) == 5 + + auth.responses = [ + aiohttp.web.Response(body=b"image-bytes-7"), + ] + ts = event_timestamp + datetime.timedelta(seconds=8) + # Simulate a failure case removing the media on cache eviction + with patch( + "homeassistant.components.nest.media_source.os.remove", side_effect=OSError + ) as mock_remove: + await subscriber.async_receive_event( + create_event_message( + create_battery_event_data( + MOTION_EVENT, event_session_id="event-session-7" + ), + timestamp=ts, + ) + ) + await hass.async_block_till_done() + assert mock_remove.called + + browse = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}" + ) + assert len(browse.children) == 5 + + # Verify all other content is still persisted correctly + client = await hass_client() + for i in range(3, 8): + response = await client.get( + f"/api/nest/event_media/{device.id}/event-session-{i}" + ) + assert response.status == HTTPStatus.OK, "Response not matched: %s" % response + contents = await response.read() + assert contents == f"image-bytes-{i}".encode() + await hass.async_block_till_done() From 615872a5d1ab4629977f662cfef1fe2b8a496f8a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 19 Dec 2021 02:09:21 -0600 Subject: [PATCH 0733/2644] Align zeroconf matching with ZeroconfServiceInfo (#62133) --- .../components/apple_tv/manifest.json | 2 +- homeassistant/components/axis/manifest.json | 6 +- .../components/doorbird/manifest.json | 2 +- homeassistant/components/nam/manifest.json | 4 +- .../components/samsungtv/manifest.json | 2 +- homeassistant/components/zeroconf/__init__.py | 57 +++++++++++-------- homeassistant/generated/zeroconf.py | 28 ++++++--- homeassistant/loader.py | 35 ++++++++++-- script/hassfest/manifest.py | 32 +++++++---- script/hassfest/zeroconf.py | 6 +- tests/components/zeroconf/test_init.py | 30 ++++++++-- tests/test_loader.py | 54 ++++++++++++++++++ 12 files changed, 195 insertions(+), 63 deletions(-) diff --git a/homeassistant/components/apple_tv/manifest.json b/homeassistant/components/apple_tv/manifest.json index a9a97b1660d..02fabc02565 100644 --- a/homeassistant/components/apple_tv/manifest.json +++ b/homeassistant/components/apple_tv/manifest.json @@ -7,7 +7,7 @@ "zeroconf": [ "_mediaremotetv._tcp.local.", "_touch-able._tcp.local.", - {"type":"_airplay._tcp.local.","model":"appletv*"} + {"type":"_airplay._tcp.local.","properties":{"model":"appletv*"}} ], "codeowners": ["@postlund"], "iot_class": "local_push" diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index 52e0c99044b..59e72341150 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -26,15 +26,15 @@ "zeroconf": [ { "type": "_axis-video._tcp.local.", - "macaddress": "00408C*" + "properties": {"macaddress": "00408c*"} }, { "type": "_axis-video._tcp.local.", - "macaddress": "ACCC8E*" + "properties": {"macaddress": "accc8e*"} }, { "type": "_axis-video._tcp.local.", - "macaddress": "B8A44F*" + "properties": {"macaddress": "b8a44f*"} } ], "after_dependencies": ["mqtt"], diff --git a/homeassistant/components/doorbird/manifest.json b/homeassistant/components/doorbird/manifest.json index 5dd9ecbd0db..b379dab7e98 100644 --- a/homeassistant/components/doorbird/manifest.json +++ b/homeassistant/components/doorbird/manifest.json @@ -7,7 +7,7 @@ "zeroconf": [ { "type": "_axis-video._tcp.local.", - "macaddress": "1CCAE3*" + "properties": {"macaddress": "1ccae3*"} } ], "codeowners": ["@oblogic7", "@bdraco"], diff --git a/homeassistant/components/nam/manifest.json b/homeassistant/components/nam/manifest.json index 1aab1cf4613..68d5fb50746 100644 --- a/homeassistant/components/nam/manifest.json +++ b/homeassistant/components/nam/manifest.json @@ -11,10 +11,10 @@ }, { "type": "_http._tcp.local.", - "manufacturer": "nettigo" + "properties": {"manufacturer": "nettigo"} } ], "config_flow": true, "quality_scale": "platinum", "iot_class": "local_polling" -} \ No newline at end of file +} diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index 36481b43756..9123a68b716 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -14,7 +14,7 @@ } ], "zeroconf": [ - {"type":"_airplay._tcp.local.","manufacturer":"samsung*"} + {"type":"_airplay._tcp.local.","properties":{"manufacturer":"samsung*"}} ], "dhcp": [ { diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index deeb42367cd..98d371d2d2f 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -48,17 +48,9 @@ HOMEKIT_TYPES = [ "_hap._udp.local.", ] -# Keys we support matching against in properties that are always matched in -# upper case. ex: ZeroconfServiceInfo.properties["macaddress"] -UPPER_MATCH_PROPS = {"macaddress"} -# Keys we support matching against in properties that are always matched in -# lower case. ex: ZeroconfServiceInfo.properties["model"] -LOWER_MATCH_PROPS = {"manufacturer", "model"} # Top level keys we support matching against in properties that are always matched in # lower case. ex: ZeroconfServiceInfo.name LOWER_MATCH_ATTRS = {"name"} -# Everything we support matching -ALL_MATCHERS = UPPER_MATCH_PROPS | LOWER_MATCH_PROPS | LOWER_MATCH_ATTRS CONF_DEFAULT_INTERFACE = "default_interface" CONF_IPV6 = "ipv6" @@ -75,6 +67,8 @@ MAX_PROPERTY_VALUE_LEN = 230 # Dns label max length MAX_NAME_LEN = 63 +ATTR_PROPERTIES: Final = "properties" + # Attributes for ZeroconfServiceInfo[ATTR_PROPERTIES] ATTR_PROPERTIES_ID: Final = "id" @@ -321,15 +315,28 @@ async def _async_register_hass_zc_service( await aio_zc.async_register_service(info, allow_name_change=True) -def _match_against_data(matcher: dict[str, str], match_data: dict[str, str]) -> bool: +def _match_against_data( + matcher: dict[str, str | dict[str, str]], match_data: dict[str, str] +) -> bool: """Check a matcher to ensure all values in match_data match.""" + for key in LOWER_MATCH_ATTRS: + if key not in matcher: + continue + if key not in match_data: + return False + match_val = matcher[key] + assert isinstance(match_val, str) + if not fnmatch.fnmatch(match_data[key], match_val): + return False + return True + + +def _match_against_props(matcher: dict[str, str], props: dict[str, str]) -> bool: + """Check a matcher to ensure all values in props.""" return not any( key - for key in ALL_MATCHERS - if key in matcher - and ( - key not in match_data or not fnmatch.fnmatch(match_data[key], matcher[key]) - ) + for key in matcher + if key not in props or not fnmatch.fnmatch(props[key].lower(), matcher[key]) ) @@ -340,7 +347,7 @@ class ZeroconfDiscovery: self, hass: HomeAssistant, zeroconf: HaZeroconf, - zeroconf_types: dict[str, list[dict[str, str]]], + zeroconf_types: dict[str, list[dict[str, str | dict[str, str]]]], homekit_models: dict[str, str], ipv6: bool, ) -> None: @@ -436,22 +443,24 @@ class ZeroconfDiscovery: for key in LOWER_MATCH_ATTRS: attr_value: str = getattr(info, key) match_data[key] = attr_value.lower() - for key in UPPER_MATCH_PROPS: - if key in props: - match_data[key] = props[key].upper() - for key in LOWER_MATCH_PROPS: - if key in props: - match_data[key] = props[key].lower() # Not all homekit types are currently used for discovery # so not all service type exist in zeroconf_types for matcher in self.zeroconf_types.get(service_type, []): - if len(matcher) > 1 and not _match_against_data(matcher, match_data): - continue + if len(matcher) > 1: + if not _match_against_data(matcher, match_data): + continue + if ATTR_PROPERTIES in matcher: + matcher_props = matcher[ATTR_PROPERTIES] + assert isinstance(matcher_props, dict) + if not _match_against_props(matcher_props, props): + continue + matcher_domain = matcher["domain"] + assert isinstance(matcher_domain, str) discovery_flow.async_create_flow( self.hass, - matcher["domain"], + matcher_domain, {"source": config_entries.SOURCE_ZEROCONF}, info, ) diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index c5c4e0c9a01..af828c077c5 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -14,11 +14,15 @@ ZEROCONF = { "_airplay._tcp.local.": [ { "domain": "apple_tv", - "model": "appletv*" + "properties": { + "model": "appletv*" + } }, { "domain": "samsungtv", - "manufacturer": "samsung*" + "properties": { + "manufacturer": "samsung*" + } } ], "_api._udp.local.": [ @@ -29,19 +33,27 @@ ZEROCONF = { "_axis-video._tcp.local.": [ { "domain": "axis", - "macaddress": "00408C*" + "properties": { + "macaddress": "00408c*" + } }, { "domain": "axis", - "macaddress": "ACCC8E*" + "properties": { + "macaddress": "accc8e*" + } }, { "domain": "axis", - "macaddress": "B8A44F*" + "properties": { + "macaddress": "b8a44f*" + } }, { "domain": "doorbird", - "macaddress": "1CCAE3*" + "properties": { + "macaddress": "1ccae3*" + } } ], "_bond._tcp.local.": [ @@ -123,7 +135,9 @@ ZEROCONF = { }, { "domain": "nam", - "manufacturer": "nettigo" + "properties": { + "manufacturer": "nettigo" + } }, { "domain": "rachio", diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 84d9cd2a72f..af3f90ab356 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -58,6 +58,8 @@ _UNDEF = object() # Internal; not helpers.typing.UNDEFINED due to circular depe MAX_LOAD_CONCURRENTLY = 4 +MOVED_ZEROCONF_PROPS = ("macaddress", "model", "manufacturer") + class Manifest(TypedDict, total=False): """ @@ -182,21 +184,42 @@ async def async_get_config_flows(hass: HomeAssistant) -> set[str]: return flows -async def async_get_zeroconf(hass: HomeAssistant) -> dict[str, list[dict[str, str]]]: +def async_process_zeroconf_match_dict(entry: dict[str, Any]) -> dict[str, Any]: + """Handle backwards compat with zeroconf matchers.""" + entry_without_type: dict[str, Any] = entry.copy() + del entry_without_type["type"] + # These properties keys used to be at the top level, we relocate + # them for backwards compat + for moved_prop in MOVED_ZEROCONF_PROPS: + if value := entry_without_type.pop(moved_prop, None): + _LOGGER.warning( + 'Matching the zeroconf property "%s" at top-level is deprecated and should be moved into a properties dict; Check the developer documentation', + moved_prop, + ) + if "properties" not in entry_without_type: + prop_dict: dict[str, str] = {} + entry_without_type["properties"] = prop_dict + else: + prop_dict = entry_without_type["properties"] + prop_dict[moved_prop] = value.lower() + return entry_without_type + + +async def async_get_zeroconf( + hass: HomeAssistant, +) -> dict[str, list[dict[str, str | dict[str, str]]]]: """Return cached list of zeroconf types.""" - zeroconf: dict[str, list[dict[str, str]]] = ZEROCONF.copy() + zeroconf: dict[str, list[dict[str, str | dict[str, str]]]] = ZEROCONF.copy() # type: ignore[assignment] integrations = await async_get_custom_components(hass) for integration in integrations.values(): if not integration.zeroconf: continue for entry in integration.zeroconf: - data = {"domain": integration.domain} + data: dict[str, str | dict[str, str]] = {"domain": integration.domain} if isinstance(entry, dict): typ = entry["type"] - entry_without_type = entry.copy() - del entry_without_type["type"] - data.update(entry_without_type) + data.update(async_process_zeroconf_match_dict(entry)) else: typ = entry diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index ecc00142e30..f2185c86fc8 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -12,6 +12,8 @@ from awesomeversion import ( import voluptuous as vol from voluptuous.humanize import humanize_error +from homeassistant.helpers import config_validation as cv + from .model import Config, Integration DOCUMENTATION_URL_SCHEMA = "https" @@ -180,16 +182,26 @@ MANIFEST_SCHEMA = vol.Schema( vol.Optional("zeroconf"): [ vol.Any( str, - vol.Schema( - { - vol.Required("type"): str, - vol.Optional("macaddress"): vol.All( - str, verify_uppercase, verify_wildcard - ), - vol.Optional("manufacturer"): vol.All(str, verify_lowercase), - vol.Optional("model"): vol.All(str, verify_lowercase), - vol.Optional("name"): vol.All(str, verify_lowercase), - } + vol.All( + cv.deprecated("macaddress"), + cv.deprecated("model"), + cv.deprecated("manufacturer"), + vol.Schema( + { + vol.Required("type"): str, + vol.Optional("macaddress"): vol.All( + str, verify_uppercase, verify_wildcard + ), + vol.Optional("manufacturer"): vol.All( + str, verify_lowercase + ), + vol.Optional("model"): vol.All(str, verify_lowercase), + vol.Optional("name"): vol.All(str, verify_lowercase), + vol.Optional("properties"): vol.Schema( + {str: verify_lowercase} + ), + } + ), ), ) ], diff --git a/script/hassfest/zeroconf.py b/script/hassfest/zeroconf.py index 4ce4896952e..446a6f32aeb 100644 --- a/script/hassfest/zeroconf.py +++ b/script/hassfest/zeroconf.py @@ -4,6 +4,8 @@ from __future__ import annotations from collections import OrderedDict, defaultdict import json +from homeassistant.loader import async_process_zeroconf_match_dict + from .model import Config, Integration BASE = """ @@ -42,9 +44,7 @@ def generate_and_validate(integrations: dict[str, Integration]): data = {"domain": domain} if isinstance(entry, dict): typ = entry["type"] - entry_without_type = entry.copy() - del entry_without_type["type"] - data.update(entry_without_type) + data.update(async_process_zeroconf_match_dict(entry)) else: typ = entry diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 16aca70d7fc..0482f38415b 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -295,7 +295,11 @@ async def test_zeroconf_match_macaddress(hass, mock_async_zeroconf): zc_gen.ZEROCONF, { "_http._tcp.local.": [ - {"domain": "shelly", "name": "shelly*", "macaddress": "FFAADD*"} + { + "domain": "shelly", + "name": "shelly*", + "properties": {"macaddress": "ffaadd*"}, + } ] }, clear=True, @@ -330,7 +334,11 @@ async def test_zeroconf_match_manufacturer(hass, mock_async_zeroconf): with patch.dict( zc_gen.ZEROCONF, - {"_airplay._tcp.local.": [{"domain": "samsungtv", "manufacturer": "samsung*"}]}, + { + "_airplay._tcp.local.": [ + {"domain": "samsungtv", "properties": {"manufacturer": "samsung*"}} + ] + }, clear=True, ), patch.object( hass.config_entries.flow, "async_init" @@ -363,7 +371,11 @@ async def test_zeroconf_match_model(hass, mock_async_zeroconf): with patch.dict( zc_gen.ZEROCONF, - {"_airplay._tcp.local.": [{"domain": "appletv", "model": "appletv*"}]}, + { + "_airplay._tcp.local.": [ + {"domain": "appletv", "properties": {"model": "appletv*"}} + ] + }, clear=True, ), patch.object( hass.config_entries.flow, "async_init" @@ -396,7 +408,11 @@ async def test_zeroconf_match_manufacturer_not_present(hass, mock_async_zeroconf with patch.dict( zc_gen.ZEROCONF, - {"_airplay._tcp.local.": [{"domain": "samsungtv", "manufacturer": "samsung*"}]}, + { + "_airplay._tcp.local.": [ + {"domain": "samsungtv", "properties": {"manufacturer": "samsung*"}} + ] + }, clear=True, ), patch.object( hass.config_entries.flow, "async_init" @@ -460,7 +476,11 @@ async def test_zeroconf_no_match_manufacturer(hass, mock_async_zeroconf): with patch.dict( zc_gen.ZEROCONF, - {"_airplay._tcp.local.": [{"domain": "samsungtv", "manufacturer": "samsung*"}]}, + { + "_airplay._tcp.local.": [ + {"domain": "samsungtv", "properties": {"manufacturer": "samsung*"}} + ] + }, clear=True, ), patch.object( hass.config_entries.flow, "async_init" diff --git a/tests/test_loader.py b/tests/test_loader.py index 2bffee75d1e..9c6dcba770a 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -329,6 +329,33 @@ def _get_test_integration_with_zeroconf_matcher(hass, name, config_flow): ) +def _get_test_integration_with_legacy_zeroconf_matcher(hass, name, config_flow): + """Return a generated test integration with a legacy zeroconf matcher.""" + return loader.Integration( + hass, + f"homeassistant.components.{name}", + None, + { + "name": name, + "domain": name, + "config_flow": config_flow, + "dependencies": [], + "requirements": [], + "zeroconf": [ + { + "type": f"_{name}._tcp.local.", + "macaddress": "AABBCC*", + "manufacturer": "legacy*", + "model": "legacy*", + "name": f"{name}*", + } + ], + "homekit": {"models": [name]}, + "ssdp": [{"manufacturer": name, "modelName": name}], + }, + ) + + def _get_test_integration_with_dhcp_matcher(hass, name, config_flow): """Return a generated test integration with a dhcp matcher.""" return loader.Integration( @@ -435,6 +462,33 @@ async def test_get_zeroconf(hass): ] +async def test_get_zeroconf_back_compat(hass): + """Verify that custom components with zeroconf are found and legacy matchers are converted.""" + test_1_integration = _get_test_integration(hass, "test_1", True) + test_2_integration = _get_test_integration_with_legacy_zeroconf_matcher( + hass, "test_2", True + ) + + with patch("homeassistant.loader.async_get_custom_components") as mock_get: + mock_get.return_value = { + "test_1": test_1_integration, + "test_2": test_2_integration, + } + zeroconf = await loader.async_get_zeroconf(hass) + assert zeroconf["_test_1._tcp.local."] == [{"domain": "test_1"}] + assert zeroconf["_test_2._tcp.local."] == [ + { + "domain": "test_2", + "name": "test_2*", + "properties": { + "macaddress": "aabbcc*", + "model": "legacy*", + "manufacturer": "legacy*", + }, + } + ] + + async def test_get_dhcp(hass): """Verify that custom components with dhcp are found.""" test_1_integration = _get_test_integration_with_dhcp_matcher(hass, "test_1", True) From 38cb477e7b2766d03a0ac0d50ae8b15a8a2cf5ad Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Sun, 19 Dec 2021 10:38:33 +0100 Subject: [PATCH 0734/2644] Cleanup tests for lcn events and device triggers (#61719) * Return PchkConnectionManager instance from init_integration * Removed ip and port from LCN host model identifer * Fix syntax error * Convert init_integration to a fixture * Rename device model for host * Instantiate MockPchkConnectionManager with arguments from tests * Invert logic for testing devices --- .../components/lcn/device_trigger.py | 5 +- homeassistant/components/lcn/helpers.py | 2 +- tests/components/lcn/conftest.py | 28 +++++--- tests/components/lcn/test_device_trigger.py | 68 ++++++------------- tests/components/lcn/test_events.py | 43 ++---------- tests/components/lcn/test_init.py | 47 +++++++------ 6 files changed, 78 insertions(+), 115 deletions(-) diff --git a/homeassistant/components/lcn/device_trigger.py b/homeassistant/components/lcn/device_trigger.py index b21c3b820af..33b474ab724 100644 --- a/homeassistant/components/lcn/device_trigger.py +++ b/homeassistant/components/lcn/device_trigger.py @@ -58,8 +58,11 @@ async def async_get_triggers( """List device triggers for LCN devices.""" device_registry = dr.async_get(hass) device = device_registry.async_get(device_id) + if device is None: + return [] - if device.model.startswith(("LCN host", "LCN group", "LCN resource")): # type: ignore[union-attr] + identifier = next(iter(device.identifiers)) + if (identifier[1].count("-") != 1) or device.model.startswith("LCN group"): # type: ignore[union-attr] return [] base_trigger = { diff --git a/homeassistant/components/lcn/helpers.py b/homeassistant/components/lcn/helpers.py index 74f135c4d1e..43ea62cdf6e 100644 --- a/homeassistant/components/lcn/helpers.py +++ b/homeassistant/components/lcn/helpers.py @@ -256,7 +256,7 @@ def register_lcn_host_device(hass: HomeAssistant, config_entry: ConfigEntry) -> identifiers={(DOMAIN, config_entry.entry_id)}, manufacturer="Issendorff", name=config_entry.title, - model=f"LCN host ({config_entry.data[CONF_IP_ADDRESS]}:{config_entry.data[CONF_PORT]})", + model="LCN-PCHK", ) diff --git a/tests/components/lcn/conftest.py b/tests/components/lcn/conftest.py index aebae09547a..cf52263e69d 100644 --- a/tests/components/lcn/conftest.py +++ b/tests/components/lcn/conftest.py @@ -41,13 +41,6 @@ class MockGroupConnection(GroupConnection): class MockPchkConnectionManager(PchkConnectionManager): """Fake connection handler.""" - return_value = None - - def __init__(self, *args, **kwargs): - """Initialize MockPchkCOnnectionManager.""" - super().__init__(*args, **kwargs) - self.__class__.return_value = self - async def async_connect(self, timeout=30): """Mock establishing a connection to PCHK.""" self.authentication_completed_future.set_result(True) @@ -59,9 +52,9 @@ class MockPchkConnectionManager(PchkConnectionManager): @patch.object(pypck.connection, "ModuleConnection", MockModuleConnection) @patch.object(pypck.connection, "GroupConnection", MockGroupConnection) - def get_address_conn(self, addr): + def get_address_conn(self, addr, request_serials=False): """Get LCN address connection.""" - return super().get_address_conn(addr, request_serials=False) + return super().get_address_conn(addr, request_serials) send_command = AsyncMock() @@ -102,11 +95,24 @@ def create_config_entry_myhome(): return create_config_entry("myhome") +@pytest.fixture(name="lcn_connection") async def init_integration(hass, entry): """Set up the LCN integration in Home Assistant.""" + lcn_connection = None + + def lcn_connection_factory(*args, **kwargs): + nonlocal lcn_connection + lcn_connection = MockPchkConnectionManager(*args, **kwargs) + return lcn_connection + entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + with patch( + "pypck.connection.PchkConnectionManager", + side_effect=lcn_connection_factory, + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + yield lcn_connection async def setup_component(hass): diff --git a/tests/components/lcn/test_device_trigger.py b/tests/components/lcn/test_device_trigger.py index 00138594a5c..4030a9b26da 100644 --- a/tests/components/lcn/test_device_trigger.py +++ b/tests/components/lcn/test_device_trigger.py @@ -1,6 +1,4 @@ """Tests for LCN device triggers.""" -from unittest.mock import patch - from pypck.inputs import ModSendKeysHost, ModStatusAccessControl from pypck.lcn_addr import LcnAddr from pypck.lcn_defs import AccessControlPeriphery, KeyAction, SendKeyCommand @@ -13,15 +11,13 @@ from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.setup import async_setup_component -from .conftest import MockPchkConnectionManager, get_device, init_integration +from .conftest import get_device from tests.common import assert_lists_same, async_get_device_automations -@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) -async def test_get_triggers_module_device(hass, entry): +async def test_get_triggers_module_device(hass, entry, lcn_connection): """Test we get the expected triggers from a LCN module device.""" - await init_integration(hass, entry) device = get_device(hass, entry, (0, 7, False)) expected_triggers = [ @@ -55,25 +51,25 @@ async def test_get_triggers_module_device(hass, entry): assert_lists_same(triggers, expected_triggers) -@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) -async def test_get_triggers_non_module_device(hass, entry): +async def test_get_triggers_non_module_device(hass, entry, lcn_connection): """Test we get the expected triggers from a LCN non-module device.""" not_included_types = ("transmitter", "transponder", "fingerprint", "send_keys") - await init_integration(hass, entry) device_registry = dr.async_get(hass) - for device_id in device_registry.devices: - device = device_registry.async_get(device_id) - if device.model.startswith(("LCN host", "LCN group", "LCN resource")): - triggers = await async_get_device_automations(hass, "trigger", device_id) - for trigger in triggers: - assert trigger[CONF_TYPE] not in not_included_types + host_device = device_registry.async_get_device({(DOMAIN, entry.entry_id)}) + group_device = get_device(hass, entry, (0, 5, True)) + resource_device = device_registry.async_get_device( + {(DOMAIN, f"{entry.entry_id}-m000007-output1")} + ) + + for device in (host_device, group_device, resource_device): + triggers = await async_get_device_automations(hass, "trigger", device.id) + for trigger in triggers: + assert trigger[CONF_TYPE] not in not_included_types -@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) -async def test_if_fires_on_transponder_event(hass, calls, entry): +async def test_if_fires_on_transponder_event(hass, calls, entry, lcn_connection): """Test for transponder event triggers firing.""" - await init_integration(hass, entry) address = (0, 7, False) device = get_device(hass, entry, address) @@ -107,7 +103,6 @@ async def test_if_fires_on_transponder_event(hass, calls, entry): code="aabbcc", ) - lcn_connection = MockPchkConnectionManager.return_value await lcn_connection.async_process_input(inp) await hass.async_block_till_done() @@ -118,10 +113,8 @@ async def test_if_fires_on_transponder_event(hass, calls, entry): } -@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) -async def test_if_fires_on_fingerprint_event(hass, calls, entry): +async def test_if_fires_on_fingerprint_event(hass, calls, entry, lcn_connection): """Test for fingerprint event triggers firing.""" - await init_integration(hass, entry) address = (0, 7, False) device = get_device(hass, entry, address) @@ -155,7 +148,6 @@ async def test_if_fires_on_fingerprint_event(hass, calls, entry): code="aabbcc", ) - lcn_connection = MockPchkConnectionManager.return_value await lcn_connection.async_process_input(inp) await hass.async_block_till_done() @@ -166,10 +158,8 @@ async def test_if_fires_on_fingerprint_event(hass, calls, entry): } -@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) -async def test_if_fires_on_transmitter_event(hass, calls, entry): +async def test_if_fires_on_transmitter_event(hass, calls, entry, lcn_connection): """Test for transmitter event triggers firing.""" - await init_integration(hass, entry) address = (0, 7, False) device = get_device(hass, entry, address) @@ -209,7 +199,6 @@ async def test_if_fires_on_transmitter_event(hass, calls, entry): action=KeyAction.HIT, ) - lcn_connection = MockPchkConnectionManager.return_value await lcn_connection.async_process_input(inp) await hass.async_block_till_done() @@ -223,10 +212,8 @@ async def test_if_fires_on_transmitter_event(hass, calls, entry): } -@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) -async def test_if_fires_on_send_keys_event(hass, calls, entry): +async def test_if_fires_on_send_keys_event(hass, calls, entry, lcn_connection): """Test for send_keys event triggers firing.""" - await init_integration(hass, entry) address = (0, 7, False) device = get_device(hass, entry, address) @@ -261,7 +248,6 @@ async def test_if_fires_on_send_keys_event(hass, calls, entry): keys=[True, False, False, False, False, False, False, False], ) - lcn_connection = MockPchkConnectionManager.return_value await lcn_connection.async_process_input(inp) await hass.async_block_till_done() @@ -273,10 +259,8 @@ async def test_if_fires_on_send_keys_event(hass, calls, entry): } -@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) -async def test_get_transponder_trigger_capabilities(hass, entry): +async def test_get_transponder_trigger_capabilities(hass, entry, lcn_connection): """Test we get the expected capabilities from a transponder device trigger.""" - await init_integration(hass, entry) address = (0, 7, False) device = get_device(hass, entry, address) @@ -296,10 +280,8 @@ async def test_get_transponder_trigger_capabilities(hass, entry): ) == [{"name": "code", "optional": True, "type": "string", "lower": True}] -@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) -async def test_get_fingerprint_trigger_capabilities(hass, entry): +async def test_get_fingerprint_trigger_capabilities(hass, entry, lcn_connection): """Test we get the expected capabilities from a fingerprint device trigger.""" - await init_integration(hass, entry) address = (0, 7, False) device = get_device(hass, entry, address) @@ -319,10 +301,8 @@ async def test_get_fingerprint_trigger_capabilities(hass, entry): ) == [{"name": "code", "optional": True, "type": "string", "lower": True}] -@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) -async def test_get_transmitter_trigger_capabilities(hass, entry): +async def test_get_transmitter_trigger_capabilities(hass, entry, lcn_connection): """Test we get the expected capabilities from a transmitter device trigger.""" - await init_integration(hass, entry) address = (0, 7, False) device = get_device(hass, entry, address) @@ -352,10 +332,8 @@ async def test_get_transmitter_trigger_capabilities(hass, entry): ] -@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) -async def test_get_send_keys_trigger_capabilities(hass, entry): +async def test_get_send_keys_trigger_capabilities(hass, entry, lcn_connection): """Test we get the expected capabilities from a send_keys device trigger.""" - await init_integration(hass, entry) address = (0, 7, False) device = get_device(hass, entry, address) @@ -390,10 +368,8 @@ async def test_get_send_keys_trigger_capabilities(hass, entry): ] -@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) -async def test_unknown_trigger_capabilities(hass, entry): +async def test_unknown_trigger_capabilities(hass, entry, lcn_connection): """Test we get empty capabilities if trigger is unknown.""" - await init_integration(hass, entry) address = (0, 7, False) device = get_device(hass, entry, address) diff --git a/tests/components/lcn/test_events.py b/tests/components/lcn/test_events.py index f977d586dad..38a685ad663 100644 --- a/tests/components/lcn/test_events.py +++ b/tests/components/lcn/test_events.py @@ -1,20 +1,13 @@ """Tests for LCN events.""" -from unittest.mock import patch - from pypck.inputs import Input, ModSendKeysHost, ModStatusAccessControl from pypck.lcn_addr import LcnAddr from pypck.lcn_defs import AccessControlPeriphery, KeyAction, SendKeyCommand -from .conftest import MockPchkConnectionManager, init_integration - from tests.common import async_capture_events -@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) -async def test_fire_transponder_event(hass, entry): +async def test_fire_transponder_event(hass, lcn_connection): """Test the transponder event is fired.""" - await init_integration(hass, entry) - events = async_capture_events(hass, "lcn_transponder") inp = ModStatusAccessControl( @@ -23,7 +16,6 @@ async def test_fire_transponder_event(hass, entry): code="aabbcc", ) - lcn_connection = MockPchkConnectionManager.return_value await lcn_connection.async_process_input(inp) await hass.async_block_till_done() @@ -32,11 +24,8 @@ async def test_fire_transponder_event(hass, entry): assert events[0].data["code"] == "aabbcc" -@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) -async def test_fire_fingerprint_event(hass, entry): +async def test_fire_fingerprint_event(hass, lcn_connection): """Test the fingerprint event is fired.""" - await init_integration(hass, entry) - events = async_capture_events(hass, "lcn_fingerprint") inp = ModStatusAccessControl( @@ -45,7 +34,6 @@ async def test_fire_fingerprint_event(hass, entry): code="aabbcc", ) - lcn_connection = MockPchkConnectionManager.return_value await lcn_connection.async_process_input(inp) await hass.async_block_till_done() @@ -54,11 +42,8 @@ async def test_fire_fingerprint_event(hass, entry): assert events[0].data["code"] == "aabbcc" -@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) -async def test_fire_transmitter_event(hass, entry): +async def test_fire_transmitter_event(hass, lcn_connection): """Test the transmitter event is fired.""" - await init_integration(hass, entry) - events = async_capture_events(hass, "lcn_transmitter") inp = ModStatusAccessControl( @@ -70,7 +55,6 @@ async def test_fire_transmitter_event(hass, entry): action=KeyAction.HIT, ) - lcn_connection = MockPchkConnectionManager.return_value await lcn_connection.async_process_input(inp) await hass.async_block_till_done() @@ -82,11 +66,8 @@ async def test_fire_transmitter_event(hass, entry): assert events[0].data["action"] == "hit" -@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) -async def test_fire_sendkeys_event(hass, entry): +async def test_fire_sendkeys_event(hass, lcn_connection): """Test the send_keys event is fired.""" - await init_integration(hass, entry) - events = async_capture_events(hass, "lcn_send_keys") inp = ModSendKeysHost( @@ -95,7 +76,6 @@ async def test_fire_sendkeys_event(hass, entry): keys=[True, True, False, False, False, False, False, False], ) - lcn_connection = MockPchkConnectionManager.return_value await lcn_connection.async_process_input(inp) await hass.async_block_till_done() @@ -114,13 +94,9 @@ async def test_fire_sendkeys_event(hass, entry): assert events[3].data["action"] == "make" -@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) -async def test_dont_fire_on_non_module_input(hass, entry): +async def test_dont_fire_on_non_module_input(hass, lcn_connection): """Test for no event is fired if a non-module input is received.""" - await init_integration(hass, entry) - inp = Input() - lcn_connection = MockPchkConnectionManager.return_value for event_name in ( "lcn_transponder", @@ -134,20 +110,15 @@ async def test_dont_fire_on_non_module_input(hass, entry): assert len(events) == 0 -@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) -async def test_dont_fire_on_unknown_module(hass, entry): +async def test_dont_fire_on_unknown_module(hass, lcn_connection): """Test for no event is fired if an input from an unknown module is received.""" - await init_integration(hass, entry) - inp = ModStatusAccessControl( LcnAddr(0, 10, False), # unknown module periphery=AccessControlPeriphery.FINGERPRINT, code="aabbcc", ) - lcn_connection = MockPchkConnectionManager.return_value - - events = async_capture_events(hass, "lcn_transmitter") + events = async_capture_events(hass, "lcn_fingerprint") await lcn_connection.async_process_input(inp) await hass.async_block_till_done() assert len(events) == 0 diff --git a/tests/components/lcn/test_init.py b/tests/components/lcn/test_init.py index 40f655dd695..844bf5fa9b8 100644 --- a/tests/components/lcn/test_init.py +++ b/tests/components/lcn/test_init.py @@ -12,14 +12,11 @@ from homeassistant.components.lcn.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.helpers import device_registry as dr, entity_registry as er -from .conftest import MockPchkConnectionManager, init_integration, setup_component +from .conftest import MockPchkConnectionManager, setup_component -@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) -async def test_async_setup_entry(hass, entry): +async def test_async_setup_entry(hass, entry, lcn_connection): """Test a successful setup entry and unload of entry.""" - await init_integration(hass, entry) - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert entry.state == ConfigEntryState.LOADED @@ -30,13 +27,14 @@ async def test_async_setup_entry(hass, entry): assert not hass.data.get(DOMAIN) -@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) async def test_async_setup_multiple_entries(hass, entry, entry2): """Test a successful setup and unload of multiple entries.""" - for config_entry in (entry, entry2): - await init_integration(hass, config_entry) - assert config_entry.state == ConfigEntryState.LOADED - await hass.async_block_till_done() + with patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager): + for config_entry in (entry, entry2): + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 2 @@ -49,7 +47,6 @@ async def test_async_setup_multiple_entries(hass, entry, entry2): assert not hass.data.get(DOMAIN) -@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) async def test_async_setup_entry_update(hass, entry): """Test a successful setup entry if entry with same id already exists.""" # setup first entry @@ -74,9 +71,10 @@ async def test_async_setup_entry_update(hass, entry): assert dummy_device in device_registry.devices.values() # setup new entry with same data via import step (should cleanup dummy device) - await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=entry.data - ) + with patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager): + await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=entry.data + ) assert dummy_device not in device_registry.devices.values() assert dummy_entity not in entity_registry.entities.values() @@ -87,7 +85,10 @@ async def test_async_setup_entry_raises_authentication_error(hass, entry): with patch.object( PchkConnectionManager, "async_connect", side_effect=PchkAuthenticationError ): - await init_integration(hass, entry) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.SETUP_ERROR @@ -96,22 +97,28 @@ async def test_async_setup_entry_raises_license_error(hass, entry): with patch.object( PchkConnectionManager, "async_connect", side_effect=PchkLicenseError ): - await init_integration(hass, entry) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.SETUP_ERROR async def test_async_setup_entry_raises_timeout_error(hass, entry): """Test that an authentication error is handled properly.""" with patch.object(PchkConnectionManager, "async_connect", side_effect=TimeoutError): - await init_integration(hass, entry) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.SETUP_ERROR -@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) async def test_async_setup_from_configuration_yaml(hass): """Test a successful setup using data from configuration.yaml.""" - - with patch("homeassistant.components.lcn.async_setup_entry") as async_setup_entry: + with patch( + "pypck.connection.PchkConnectionManager", MockPchkConnectionManager + ), patch("homeassistant.components.lcn.async_setup_entry") as async_setup_entry: await setup_component(hass) assert async_setup_entry.await_count == 2 From 37bed6460743058deff3b44898b78aa560f85365 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sun, 19 Dec 2021 11:37:14 +0100 Subject: [PATCH 0735/2644] Silently retry Fronius inverter endpoint 2 times (#61826) --- .../components/fronius/coordinator.py | 24 +++++++++--- tests/components/fronius/test_coordinator.py | 38 ++++++++++++++----- 2 files changed, 47 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/fronius/coordinator.py b/homeassistant/components/fronius/coordinator.py index e89f828f47d..f5ea498e381 100644 --- a/homeassistant/components/fronius/coordinator.py +++ b/homeassistant/components/fronius/coordinator.py @@ -5,7 +5,7 @@ from abc import ABC, abstractmethod from datetime import timedelta from typing import TYPE_CHECKING, Any, Dict, TypeVar -from pyfronius import FroniusError +from pyfronius import BadStatusError, FroniusError from homeassistant.components.sensor import SensorEntityDescription from homeassistant.core import callback @@ -43,6 +43,8 @@ class FroniusCoordinatorBase( error_interval: timedelta valid_descriptions: list[SensorEntityDescription] + MAX_FAILED_UPDATES = 3 + def __init__(self, *args: Any, solar_net: FroniusSolarNet, **kwargs: Any) -> None: """Set up the FroniusCoordinatorBase class.""" self._failed_update_count = 0 @@ -62,7 +64,7 @@ class FroniusCoordinatorBase( data = await self._update_method() except FroniusError as err: self._failed_update_count += 1 - if self._failed_update_count == 3: + if self._failed_update_count == self.MAX_FAILED_UPDATES: self.update_interval = self.error_interval raise UpdateFailed(err) from err @@ -116,6 +118,8 @@ class FroniusInverterUpdateCoordinator(FroniusCoordinatorBase): error_interval = timedelta(minutes=10) valid_descriptions = INVERTER_ENTITY_DESCRIPTIONS + SILENT_RETRIES = 3 + def __init__( self, *args: Any, inverter_info: FroniusDeviceInfo, **kwargs: Any ) -> None: @@ -125,9 +129,19 @@ class FroniusInverterUpdateCoordinator(FroniusCoordinatorBase): async def _update_method(self) -> dict[SolarNetId, Any]: """Return data per solar net id from pyfronius.""" - data = await self.solar_net.fronius.current_inverter_data( - self.inverter_info.solar_net_id - ) + # almost 1% of `current_inverter_data` requests on Symo devices result in + # `BadStatusError Code: 8 - LNRequestTimeout` due to flaky internal + # communication between the logger and the inverter. + for silent_retry in range(self.SILENT_RETRIES): + try: + data = await self.solar_net.fronius.current_inverter_data( + self.inverter_info.solar_net_id + ) + except BadStatusError as err: + if silent_retry == (self.SILENT_RETRIES - 1): + raise err + continue + break # wrap a single devices data in a dict with solar_net_id key for # FroniusCoordinatorBase _async_update_data and add_entities_for_seen_keys return {self.inverter_info.solar_net_id: data} diff --git a/tests/components/fronius/test_coordinator.py b/tests/components/fronius/test_coordinator.py index b729c4d97ac..a2368975128 100644 --- a/tests/components/fronius/test_coordinator.py +++ b/tests/components/fronius/test_coordinator.py @@ -1,7 +1,7 @@ """Test the Fronius update coordinators.""" from unittest.mock import patch -from pyfronius import FroniusError +from pyfronius import BadStatusError, FroniusError from homeassistant.components.fronius.coordinator import ( FroniusInverterUpdateCoordinator, @@ -18,27 +18,32 @@ async def test_adaptive_update_interval(hass, aioclient_mock): with patch("pyfronius.Fronius.current_inverter_data") as mock_inverter_data: mock_responses(aioclient_mock) await setup_fronius_integration(hass) - assert mock_inverter_data.call_count == 1 + mock_inverter_data.assert_called_once() + mock_inverter_data.reset_mock() async_fire_time_changed( hass, dt.utcnow() + FroniusInverterUpdateCoordinator.default_interval ) await hass.async_block_till_done() - assert mock_inverter_data.call_count == 2 + mock_inverter_data.assert_called_once() + mock_inverter_data.reset_mock() - mock_inverter_data.side_effect = FroniusError - # first 3 requests at default interval - 4th has different interval - for _ in range(4): + mock_inverter_data.side_effect = FroniusError() + # first 3 bad requests at default interval - 4th has different interval + for _ in range(3): async_fire_time_changed( hass, dt.utcnow() + FroniusInverterUpdateCoordinator.default_interval ) await hass.async_block_till_done() - assert mock_inverter_data.call_count == 5 + assert mock_inverter_data.call_count == 3 + mock_inverter_data.reset_mock() + async_fire_time_changed( hass, dt.utcnow() + FroniusInverterUpdateCoordinator.error_interval ) await hass.async_block_till_done() - assert mock_inverter_data.call_count == 6 + assert mock_inverter_data.call_count == 1 + mock_inverter_data.reset_mock() mock_inverter_data.side_effect = None # next successful request resets to default interval @@ -46,10 +51,23 @@ async def test_adaptive_update_interval(hass, aioclient_mock): hass, dt.utcnow() + FroniusInverterUpdateCoordinator.error_interval ) await hass.async_block_till_done() - assert mock_inverter_data.call_count == 7 + mock_inverter_data.assert_called_once() + mock_inverter_data.reset_mock() async_fire_time_changed( hass, dt.utcnow() + FroniusInverterUpdateCoordinator.default_interval ) await hass.async_block_till_done() - assert mock_inverter_data.call_count == 8 + mock_inverter_data.assert_called_once() + mock_inverter_data.reset_mock() + + # BadStatusError on inverter endpoints have special handling + mock_inverter_data.side_effect = BadStatusError("mock_endpoint", 8) + # first 3 requests at default interval - 4th has different interval + for _ in range(3): + async_fire_time_changed( + hass, dt.utcnow() + FroniusInverterUpdateCoordinator.default_interval + ) + await hass.async_block_till_done() + # BadStatusError does 3 silent retries for inverter endpoint * 3 request intervals = 9 + assert mock_inverter_data.call_count == 9 From 1cbcb9e2fd151ffb13aba736fd49875b371461ca Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sun, 19 Dec 2021 11:49:21 +0100 Subject: [PATCH 0736/2644] Don't add Fronius entities with unknown state (#62282) --- .../components/fronius/coordinator.py | 2 + homeassistant/components/fronius/sensor.py | 4 +- tests/components/fronius/test_sensor.py | 76 +++++-------------- 3 files changed, 21 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/fronius/coordinator.py b/homeassistant/components/fronius/coordinator.py index f5ea498e381..7e0e1a59731 100644 --- a/homeassistant/components/fronius/coordinator.py +++ b/homeassistant/components/fronius/coordinator.py @@ -100,6 +100,8 @@ class FroniusCoordinatorBase( for key in self.unregistered_keys[solar_net_id].intersection( device_data ): + if device_data[key]["value"] is None: + continue new_entities.append(entity_constructor(self, key, solar_net_id)) self.unregistered_keys[solar_net_id].remove(key) if new_entities: diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index 31d3d77fd17..67d86c1cc48 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -148,7 +148,7 @@ INVERTER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ ), SensorEntityDescription( key="current_ac", - name="AC Current", + name="AC current", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, @@ -163,7 +163,7 @@ INVERTER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ ), SensorEntityDescription( key="current_dc_2", - name="DC Current 2", + name="DC current 2", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, diff --git a/tests/components/fronius/test_sensor.py b/tests/components/fronius/test_sensor.py index 2e48faf606a..0f3e8f28a56 100644 --- a/tests/components/fronius/test_sensor.py +++ b/tests/components/fronius/test_sensor.py @@ -6,7 +6,6 @@ from homeassistant.components.fronius.coordinator import ( FroniusPowerFlowUpdateCoordinator, ) from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.const import STATE_UNKNOWN from homeassistant.helpers import device_registry as dr from homeassistant.util import dt @@ -26,11 +25,11 @@ async def test_symo_inverter(hass, aioclient_mock): mock_responses(aioclient_mock, night=True) config_entry = await setup_fronius_integration(hass) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 23 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 20 await enable_all_entities( hass, config_entry.entry_id, FroniusInverterUpdateCoordinator.default_interval ) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 55 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 52 assert_state("sensor.current_dc_fronius_inverter_1_http_fronius", 0) assert_state("sensor.energy_day_fronius_inverter_1_http_fronius", 10828) assert_state("sensor.energy_total_fronius_inverter_1_http_fronius", 44186900) @@ -43,11 +42,11 @@ async def test_symo_inverter(hass, aioclient_mock): hass, dt.utcnow() + FroniusInverterUpdateCoordinator.default_interval ) await hass.async_block_till_done() - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 57 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 56 await enable_all_entities( hass, config_entry.entry_id, FroniusInverterUpdateCoordinator.default_interval ) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 59 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 58 # 4 additional AC entities assert_state("sensor.current_dc_fronius_inverter_1_http_fronius", 2.19) assert_state("sensor.energy_day_fronius_inverter_1_http_fronius", 1113) @@ -81,13 +80,7 @@ async def test_symo_logger(hass, aioclient_mock): mock_responses(aioclient_mock) await setup_fronius_integration(hass) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 25 - - # ignored constant entities: - # hardware_platform, hardware_version, product_type - # software_version, time_zone, time_zone_location - # time_stamp, unique_identifier, utc_offset - # + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 24 # states are rounded to 4 decimals assert_state( "sensor.cash_factor_fronius_logger_info_0_http_fronius", @@ -114,14 +107,11 @@ async def test_symo_meter(hass, aioclient_mock): mock_responses(aioclient_mock) config_entry = await setup_fronius_integration(hass) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 25 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 24 await enable_all_entities( hass, config_entry.entry_id, FroniusMeterUpdateCoordinator.default_interval ) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 59 - # ignored entities: - # manufacturer, model, serial, enable, timestamp, visible, meter_location - # + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 58 # states are rounded to 4 decimals assert_state("sensor.current_ac_phase_1_fronius_meter_0_http_fronius", 7.755) assert_state("sensor.current_ac_phase_2_fronius_meter_0_http_fronius", 6.68) @@ -179,13 +169,11 @@ async def test_symo_power_flow(hass, aioclient_mock): mock_responses(aioclient_mock, night=True) config_entry = await setup_fronius_integration(hass) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 23 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 20 await enable_all_entities( hass, config_entry.entry_id, FroniusInverterUpdateCoordinator.default_interval ) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 55 - # ignored: location, mode, timestamp - # + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 52 # states are rounded to 4 decimals assert_state( "sensor.energy_day_fronius_power_flow_0_http_fronius", @@ -199,10 +187,6 @@ async def test_symo_power_flow(hass, aioclient_mock): "sensor.energy_year_fronius_power_flow_0_http_fronius", 25507686, ) - assert_state( - "sensor.power_battery_fronius_power_flow_0_http_fronius", - STATE_UNKNOWN, - ) assert_state( "sensor.power_grid_fronius_power_flow_0_http_fronius", 975.31, @@ -211,18 +195,10 @@ async def test_symo_power_flow(hass, aioclient_mock): "sensor.power_load_fronius_power_flow_0_http_fronius", -975.31, ) - assert_state( - "sensor.power_photovoltaics_fronius_power_flow_0_http_fronius", - STATE_UNKNOWN, - ) assert_state( "sensor.relative_autonomy_fronius_power_flow_0_http_fronius", 0, ) - assert_state( - "sensor.relative_self_consumption_fronius_power_flow_0_http_fronius", - STATE_UNKNOWN, - ) # Second test at daytime when inverter is producing mock_responses(aioclient_mock, night=False) @@ -230,8 +206,8 @@ async def test_symo_power_flow(hass, aioclient_mock): hass, dt.utcnow() + FroniusPowerFlowUpdateCoordinator.default_interval ) await hass.async_block_till_done() - # still 55 because power_flow update interval is shorter than others - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 55 + # 54 because power_flow `rel_SelfConsumption` and `P_PV` is not `null` anymore + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 54 assert_state( "sensor.energy_day_fronius_power_flow_0_http_fronius", 1101.7001, @@ -244,10 +220,6 @@ async def test_symo_power_flow(hass, aioclient_mock): "sensor.energy_year_fronius_power_flow_0_http_fronius", 25508788, ) - assert_state( - "sensor.power_battery_fronius_power_flow_0_http_fronius", - STATE_UNKNOWN, - ) assert_state( "sensor.power_grid_fronius_power_flow_0_http_fronius", 1703.74, @@ -281,17 +253,15 @@ async def test_gen24(hass, aioclient_mock): mock_responses(aioclient_mock, fixture_set="gen24") config_entry = await setup_fronius_integration(hass, is_logger=False) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 25 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 22 await enable_all_entities( hass, config_entry.entry_id, FroniusMeterUpdateCoordinator.default_interval ) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 57 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 52 # inverter 1 - assert_state("sensor.energy_year_fronius_inverter_1_http_fronius", STATE_UNKNOWN) assert_state("sensor.current_ac_fronius_inverter_1_http_fronius", 0.1589) assert_state("sensor.current_dc_2_fronius_inverter_1_http_fronius", 0.0754) assert_state("sensor.status_code_fronius_inverter_1_http_fronius", 7) - assert_state("sensor.energy_day_fronius_inverter_1_http_fronius", STATE_UNKNOWN) assert_state("sensor.current_dc_fronius_inverter_1_http_fronius", 0.0783) assert_state("sensor.voltage_dc_2_fronius_inverter_1_http_fronius", 403.4312) assert_state("sensor.power_ac_fronius_inverter_1_http_fronius", 37.3204) @@ -356,11 +326,6 @@ async def test_gen24(hass, aioclient_mock): assert_state("sensor.power_load_fronius_power_flow_0_http_fronius", -695.6827) assert_state("sensor.meter_mode_fronius_power_flow_0_http_fronius", "meter") assert_state("sensor.relative_autonomy_fronius_power_flow_0_http_fronius", 5.3592) - assert_state( - "sensor.power_battery_fronius_power_flow_0_http_fronius", STATE_UNKNOWN - ) - assert_state("sensor.energy_year_fronius_power_flow_0_http_fronius", STATE_UNKNOWN) - assert_state("sensor.energy_day_fronius_power_flow_0_http_fronius", STATE_UNKNOWN) assert_state("sensor.energy_total_fronius_power_flow_0_http_fronius", 1530193.42) @@ -377,19 +342,17 @@ async def test_gen24_storage(hass, aioclient_mock): hass, is_logger=False, unique_id="12345678" ) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 36 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 34 await enable_all_entities( hass, config_entry.entry_id, FroniusMeterUpdateCoordinator.default_interval ) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 68 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 64 # inverter 1 assert_state("sensor.current_dc_fronius_inverter_1_http_fronius", 0.3952) assert_state("sensor.voltage_dc_2_fronius_inverter_1_http_fronius", 318.8103) assert_state("sensor.current_dc_2_fronius_inverter_1_http_fronius", 0.3564) - assert_state("sensor.energy_year_fronius_inverter_1_http_fronius", STATE_UNKNOWN) assert_state("sensor.current_ac_fronius_inverter_1_http_fronius", 1.1087) assert_state("sensor.power_ac_fronius_inverter_1_http_fronius", 250.9093) - assert_state("sensor.energy_day_fronius_inverter_1_http_fronius", STATE_UNKNOWN) assert_state("sensor.error_code_fronius_inverter_1_http_fronius", 0) assert_state("sensor.status_code_fronius_inverter_1_http_fronius", 7) assert_state("sensor.energy_total_fronius_inverter_1_http_fronius", 7512794.0117) @@ -463,8 +426,6 @@ async def test_gen24_storage(hass, aioclient_mock): ) assert_state("sensor.relative_autonomy_fronius_power_flow_0_http_fronius", 7.4984) assert_state("sensor.meter_mode_fronius_power_flow_0_http_fronius", "bidirectional") - assert_state("sensor.energy_year_fronius_power_flow_0_http_fronius", STATE_UNKNOWN) - assert_state("sensor.energy_day_fronius_power_flow_0_http_fronius", STATE_UNKNOWN) assert_state("sensor.energy_total_fronius_power_flow_0_http_fronius", 7512664.4042) # storage assert_state("sensor.current_dc_fronius_storage_0_http_fronius", 0.0) @@ -519,11 +480,11 @@ async def test_primo_s0(hass, aioclient_mock): mock_responses(aioclient_mock, fixture_set="primo_s0", inverter_ids=[1, 2]) config_entry = await setup_fronius_integration(hass, is_logger=True) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 30 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 29 await enable_all_entities( hass, config_entry.entry_id, FroniusMeterUpdateCoordinator.default_interval ) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 41 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 40 # logger assert_state("sensor.cash_factor_fronius_logger_info_0_http_fronius", 1) assert_state("sensor.co2_factor_fronius_logger_info_0_http_fronius", 0.53) @@ -561,9 +522,6 @@ async def test_primo_s0(hass, aioclient_mock): assert_state("sensor.power_real_fronius_meter_0_http_fronius", -2216.7487) # power_flow assert_state("sensor.power_load_fronius_power_flow_0_http_fronius", -2218.9349) - assert_state( - "sensor.power_battery_fronius_power_flow_0_http_fronius", STATE_UNKNOWN - ) assert_state("sensor.meter_mode_fronius_power_flow_0_http_fronius", "vague-meter") assert_state("sensor.power_photovoltaics_fronius_power_flow_0_http_fronius", 1834) assert_state("sensor.power_grid_fronius_power_flow_0_http_fronius", 384.9349) From b559d8845ef8e4c2ac70998a5ca230dbbb173086 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 19 Dec 2021 06:28:09 -0500 Subject: [PATCH 0737/2644] Use enums in zwave_js (#62130) * Use enums in zwave_js * oops --- .../components/zwave_js/binary_sensor.py | 84 ++++++++--------- homeassistant/components/zwave_js/cover.py | 13 ++- homeassistant/components/zwave_js/sensor.py | 90 ++++++++----------- 3 files changed, 78 insertions(+), 109 deletions(-) diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py index 5d91e9b8d93..88ab7221600 100644 --- a/homeassistant/components/zwave_js/binary_sensor.py +++ b/homeassistant/components/zwave_js/binary_sensor.py @@ -12,27 +12,15 @@ from zwave_js_server.const.command_class.notification import ( ) from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_DOOR, - DEVICE_CLASS_GAS, - DEVICE_CLASS_HEAT, - DEVICE_CLASS_LOCK, - DEVICE_CLASS_MOISTURE, - DEVICE_CLASS_MOTION, - DEVICE_CLASS_PLUG, - DEVICE_CLASS_PROBLEM, - DEVICE_CLASS_SAFETY, - DEVICE_CLASS_SMOKE, - DEVICE_CLASS_SOUND, - DEVICE_CLASS_TAMPER, DOMAIN as BINARY_SENSOR_DOMAIN, + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC 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 .const import DATA_CLIENT, DOMAIN @@ -91,101 +79,101 @@ NOTIFICATION_SENSOR_MAPPINGS: tuple[NotificationZWaveJSEntityDescription, ...] = # NotificationType 1: Smoke Alarm - State Id's 1 and 2 - Smoke detected key=NOTIFICATION_SMOKE_ALARM, states=("1", "2"), - device_class=DEVICE_CLASS_SMOKE, + device_class=BinarySensorDeviceClass.SMOKE, ), NotificationZWaveJSEntityDescription( # NotificationType 1: Smoke Alarm - All other State Id's key=NOTIFICATION_SMOKE_ALARM, - device_class=DEVICE_CLASS_PROBLEM, + device_class=BinarySensorDeviceClass.PROBLEM, ), NotificationZWaveJSEntityDescription( # NotificationType 2: Carbon Monoxide - State Id's 1 and 2 key=NOTIFICATION_CARBON_MONOOXIDE, states=("1", "2"), - device_class=DEVICE_CLASS_GAS, + device_class=BinarySensorDeviceClass.GAS, ), NotificationZWaveJSEntityDescription( # NotificationType 2: Carbon Monoxide - All other State Id's key=NOTIFICATION_CARBON_MONOOXIDE, - device_class=DEVICE_CLASS_PROBLEM, + device_class=BinarySensorDeviceClass.PROBLEM, ), NotificationZWaveJSEntityDescription( # NotificationType 3: Carbon Dioxide - State Id's 1 and 2 key=NOTIFICATION_CARBON_DIOXIDE, states=("1", "2"), - device_class=DEVICE_CLASS_GAS, + device_class=BinarySensorDeviceClass.GAS, ), NotificationZWaveJSEntityDescription( # NotificationType 3: Carbon Dioxide - All other State Id's key=NOTIFICATION_CARBON_DIOXIDE, - device_class=DEVICE_CLASS_PROBLEM, + device_class=BinarySensorDeviceClass.PROBLEM, ), NotificationZWaveJSEntityDescription( # NotificationType 4: Heat - State Id's 1, 2, 5, 6 (heat/underheat) key=NOTIFICATION_HEAT, states=("1", "2", "5", "6"), - device_class=DEVICE_CLASS_HEAT, + device_class=BinarySensorDeviceClass.HEAT, ), NotificationZWaveJSEntityDescription( # NotificationType 4: Heat - All other State Id's key=NOTIFICATION_HEAT, - device_class=DEVICE_CLASS_PROBLEM, + device_class=BinarySensorDeviceClass.PROBLEM, ), NotificationZWaveJSEntityDescription( # NotificationType 5: Water - State Id's 1, 2, 3, 4 key=NOTIFICATION_WATER, states=("1", "2", "3", "4"), - device_class=DEVICE_CLASS_MOISTURE, + device_class=BinarySensorDeviceClass.MOISTURE, ), NotificationZWaveJSEntityDescription( # NotificationType 5: Water - All other State Id's key=NOTIFICATION_WATER, - device_class=DEVICE_CLASS_PROBLEM, + device_class=BinarySensorDeviceClass.PROBLEM, ), NotificationZWaveJSEntityDescription( # NotificationType 6: Access Control - State Id's 1, 2, 3, 4 (Lock) key=NOTIFICATION_ACCESS_CONTROL, states=("1", "2", "3", "4"), - device_class=DEVICE_CLASS_LOCK, + device_class=BinarySensorDeviceClass.LOCK, ), NotificationZWaveJSEntityDescription( # NotificationType 6: Access Control - State Id's 11 (Lock jammed) key=NOTIFICATION_ACCESS_CONTROL, states=("11",), - device_class=DEVICE_CLASS_PROBLEM, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, ), NotificationZWaveJSEntityDescription( # NotificationType 6: Access Control - State Id 22 (door/window open) key=NOTIFICATION_ACCESS_CONTROL, off_state="23", states=("22", "23"), - device_class=DEVICE_CLASS_DOOR, + device_class=BinarySensorDeviceClass.DOOR, ), NotificationZWaveJSEntityDescription( # NotificationType 7: Home Security - State Id's 1, 2 (intrusion) key=NOTIFICATION_HOME_SECURITY, states=("1", "2"), - device_class=DEVICE_CLASS_SAFETY, + device_class=BinarySensorDeviceClass.SAFETY, ), NotificationZWaveJSEntityDescription( # NotificationType 7: Home Security - State Id's 3, 4, 9 (tampering) key=NOTIFICATION_HOME_SECURITY, states=("3", "4", "9"), - device_class=DEVICE_CLASS_TAMPER, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.TAMPER, + entity_category=EntityCategory.DIAGNOSTIC, ), NotificationZWaveJSEntityDescription( # NotificationType 7: Home Security - State Id's 5, 6 (glass breakage) key=NOTIFICATION_HOME_SECURITY, states=("5", "6"), - device_class=DEVICE_CLASS_SAFETY, + device_class=BinarySensorDeviceClass.SAFETY, ), NotificationZWaveJSEntityDescription( # NotificationType 7: Home Security - State Id's 7, 8 (motion) key=NOTIFICATION_HOME_SECURITY, states=("7", "8"), - device_class=DEVICE_CLASS_MOTION, + device_class=BinarySensorDeviceClass.MOTION, ), NotificationZWaveJSEntityDescription( # NotificationType 8: Power Management - @@ -193,55 +181,55 @@ NOTIFICATION_SENSOR_MAPPINGS: tuple[NotificationZWaveJSEntityDescription, ...] = key=NOTIFICATION_POWER_MANAGEMENT, off_state="2", states=("2", "3"), - device_class=DEVICE_CLASS_PLUG, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.PLUG, + entity_category=EntityCategory.DIAGNOSTIC, ), NotificationZWaveJSEntityDescription( # NotificationType 8: Power Management - # State Id's 6, 7, 8, 9 (power status) key=NOTIFICATION_POWER_MANAGEMENT, states=("6", "7", "8", "9"), - device_class=DEVICE_CLASS_SAFETY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.SAFETY, + entity_category=EntityCategory.DIAGNOSTIC, ), NotificationZWaveJSEntityDescription( # NotificationType 8: Power Management - # State Id's 10, 11, 17 (Battery maintenance status) key=NOTIFICATION_POWER_MANAGEMENT, states=("10", "11", "17"), - device_class=DEVICE_CLASS_BATTERY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, ), NotificationZWaveJSEntityDescription( # NotificationType 9: System - State Id's 1, 2, 3, 4, 6, 7 key=NOTIFICATION_SYSTEM, states=("1", "2", "3", "4", "6", "7"), - device_class=DEVICE_CLASS_PROBLEM, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, ), NotificationZWaveJSEntityDescription( # NotificationType 10: Emergency - State Id's 1, 2, 3 key=NOTIFICATION_EMERGENCY, states=("1", "2", "3"), - device_class=DEVICE_CLASS_PROBLEM, + device_class=BinarySensorDeviceClass.PROBLEM, ), NotificationZWaveJSEntityDescription( # NotificationType 14: Siren key=NOTIFICATION_SIREN, states=("1",), - device_class=DEVICE_CLASS_SOUND, + device_class=BinarySensorDeviceClass.SOUND, ), NotificationZWaveJSEntityDescription( # NotificationType 18: Gas key=NOTIFICATION_GAS, states=("1", "2", "3", "4"), - device_class=DEVICE_CLASS_GAS, + device_class=BinarySensorDeviceClass.GAS, ), NotificationZWaveJSEntityDescription( # NotificationType 18: Gas key=NOTIFICATION_GAS, states=("6",), - device_class=DEVICE_CLASS_PROBLEM, + device_class=BinarySensorDeviceClass.PROBLEM, ), ) @@ -251,7 +239,7 @@ PROPERTY_SENSOR_MAPPINGS: dict[str, PropertyZWaveJSEntityDescription] = { DOOR_STATUS_PROPERTY: PropertyZWaveJSEntityDescription( key=DOOR_STATUS_PROPERTY, on_states=("open",), - device_class=DEVICE_CLASS_DOOR, + device_class=BinarySensorDeviceClass.DOOR, ), } @@ -260,8 +248,8 @@ PROPERTY_SENSOR_MAPPINGS: dict[str, PropertyZWaveJSEntityDescription] = { BOOLEAN_SENSOR_MAPPINGS: dict[str, BinarySensorEntityDescription] = { CommandClass.BATTERY: BinarySensorEntityDescription( key=str(CommandClass.BATTERY), - device_class=DEVICE_CLASS_BATTERY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, ), } diff --git a/homeassistant/components/zwave_js/cover.py b/homeassistant/components/zwave_js/cover.py index c94c54e1948..baae6af54ce 100644 --- a/homeassistant/components/zwave_js/cover.py +++ b/homeassistant/components/zwave_js/cover.py @@ -20,10 +20,6 @@ from zwave_js_server.model.value import Value as ZwaveValue from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, - DEVICE_CLASS_BLIND, - DEVICE_CLASS_GARAGE, - DEVICE_CLASS_SHUTTER, - DEVICE_CLASS_WINDOW, DOMAIN as COVER_DOMAIN, SUPPORT_CLOSE, SUPPORT_CLOSE_TILT, @@ -32,6 +28,7 @@ from homeassistant.components.cover import ( SUPPORT_SET_POSITION, SUPPORT_SET_TILT_POSITION, SUPPORT_STOP, + CoverDeviceClass, CoverEntity, ) from homeassistant.config_entries import ConfigEntry @@ -119,11 +116,11 @@ class ZWaveCover(ZWaveBaseEntity, CoverEntity): super().__init__(config_entry, client, info) # Entity class attributes - self._attr_device_class = DEVICE_CLASS_WINDOW + self._attr_device_class = CoverDeviceClass.WINDOW if self.info.platform_hint in ("window_shutter", "window_shutter_tilt"): - self._attr_device_class = DEVICE_CLASS_SHUTTER + self._attr_device_class = CoverDeviceClass.SHUTTER if self.info.platform_hint == "window_blind": - self._attr_device_class = DEVICE_CLASS_BLIND + self._attr_device_class = CoverDeviceClass.BLIND @property def is_closed(self) -> bool | None: @@ -235,7 +232,7 @@ class ZwaveMotorizedBarrier(ZWaveBaseEntity, CoverEntity): """Representation of a Z-Wave motorized barrier device.""" _attr_supported_features = SUPPORT_OPEN | SUPPORT_CLOSE - _attr_device_class = DEVICE_CLASS_GARAGE + _attr_device_class = CoverDeviceClass.GARAGE def __init__( self, diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 86901b94d3d..1fb7b933972 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -17,33 +17,17 @@ from zwave_js_server.model.value import ConfigurationValue from zwave_js_server.util.command_class.meter import get_meter_type from homeassistant.components.sensor import ( - DEVICE_CLASS_ENERGY, DOMAIN as SENSOR_DOMAIN, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_CO, - DEVICE_CLASS_CO2, - DEVICE_CLASS_CURRENT, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_POWER, - DEVICE_CLASS_POWER_FACTOR, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_SIGNAL_STRENGTH, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_VOLTAGE, - ENTITY_CATEGORY_DIAGNOSTIC, -) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( @@ -90,91 +74,91 @@ STATUS_ICON: dict[NodeStatus, str] = { ENTITY_DESCRIPTION_KEY_MAP: dict[str, SensorEntityDescription] = { ENTITY_DESC_KEY_BATTERY: SensorEntityDescription( ENTITY_DESC_KEY_BATTERY, - device_class=DEVICE_CLASS_BATTERY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, ), ENTITY_DESC_KEY_CURRENT: SensorEntityDescription( ENTITY_DESC_KEY_CURRENT, - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, ), ENTITY_DESC_KEY_VOLTAGE: SensorEntityDescription( ENTITY_DESC_KEY_VOLTAGE, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, ), ENTITY_DESC_KEY_ENERGY_MEASUREMENT: SensorEntityDescription( ENTITY_DESC_KEY_ENERGY_MEASUREMENT, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.MEASUREMENT, ), ENTITY_DESC_KEY_ENERGY_TOTAL_INCREASING: SensorEntityDescription( ENTITY_DESC_KEY_ENERGY_TOTAL_INCREASING, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), ENTITY_DESC_KEY_POWER: SensorEntityDescription( ENTITY_DESC_KEY_POWER, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), ENTITY_DESC_KEY_POWER_FACTOR: SensorEntityDescription( ENTITY_DESC_KEY_POWER_FACTOR, - device_class=DEVICE_CLASS_POWER_FACTOR, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER_FACTOR, + state_class=SensorStateClass.MEASUREMENT, ), ENTITY_DESC_KEY_CO: SensorEntityDescription( ENTITY_DESC_KEY_CO, - device_class=DEVICE_CLASS_CO, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CO, + state_class=SensorStateClass.MEASUREMENT, ), ENTITY_DESC_KEY_CO2: SensorEntityDescription( ENTITY_DESC_KEY_CO2, - device_class=DEVICE_CLASS_CO2, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CO2, + state_class=SensorStateClass.MEASUREMENT, ), ENTITY_DESC_KEY_HUMIDITY: SensorEntityDescription( ENTITY_DESC_KEY_HUMIDITY, - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), ENTITY_DESC_KEY_ILLUMINANCE: SensorEntityDescription( ENTITY_DESC_KEY_ILLUMINANCE, - device_class=DEVICE_CLASS_ILLUMINANCE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, ), ENTITY_DESC_KEY_PRESSURE: SensorEntityDescription( ENTITY_DESC_KEY_PRESSURE, - device_class=DEVICE_CLASS_PRESSURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, ), ENTITY_DESC_KEY_SIGNAL_STRENGTH: SensorEntityDescription( ENTITY_DESC_KEY_SIGNAL_STRENGTH, - device_class=DEVICE_CLASS_SIGNAL_STRENGTH, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), ENTITY_DESC_KEY_TEMPERATURE: SensorEntityDescription( ENTITY_DESC_KEY_TEMPERATURE, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), ENTITY_DESC_KEY_TARGET_TEMPERATURE: SensorEntityDescription( ENTITY_DESC_KEY_TARGET_TEMPERATURE, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, state_class=None, ), ENTITY_DESC_KEY_MEASUREMENT: SensorEntityDescription( ENTITY_DESC_KEY_MEASUREMENT, device_class=None, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), ENTITY_DESC_KEY_TOTAL_INCREASING: SensorEntityDescription( ENTITY_DESC_KEY_TOTAL_INCREASING, device_class=None, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, ), } @@ -465,7 +449,7 @@ class ZWaveNodeStatusSensor(SensorEntity): """Representation of a node status sensor.""" _attr_should_poll = False - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_entity_category = EntityCategory.DIAGNOSTIC def __init__( self, config_entry: ConfigEntry, client: ZwaveClient, node: ZwaveNode From 7fe895e554a7411373758291d5a374227a924e01 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 19 Dec 2021 12:42:52 +0100 Subject: [PATCH 0738/2644] Add unique ID to config entry in Luftdaten (#62176) --- .../components/luftdaten/__init__.py | 26 +--- .../components/luftdaten/config_flow.py | 29 +--- tests/components/luftdaten/conftest.py | 32 +++++ .../components/luftdaten/test_config_flow.py | 132 +++++++++++------- 4 files changed, 122 insertions(+), 97 deletions(-) create mode 100644 tests/components/luftdaten/conftest.py diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py index e449507b194..a2d7631552d 100644 --- a/homeassistant/components/luftdaten/__init__.py +++ b/homeassistant/components/luftdaten/__init__.py @@ -7,7 +7,6 @@ from luftdaten import Luftdaten from luftdaten.exceptions import LuftdatenError from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription -from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONF_MONITORED_CONDITIONS, @@ -18,13 +17,11 @@ from homeassistant.const import ( TEMP_CELSIUS, Platform, ) -from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval -from .config_flow import duplicate_stations from .const import CONF_SENSOR_ID, DEFAULT_SCAN_INTERVAL, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -83,13 +80,6 @@ SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) -@callback -def _async_fixup_sensor_id(hass, config_entry, sensor_id): - hass.config_entries.async_update_entry( - config_entry, data={**config_entry.data, CONF_SENSOR_ID: int(sensor_id)} - ) - - async def async_setup_entry(hass, config_entry): """Set up Luftdaten as config entry.""" hass.data.setdefault( @@ -100,19 +90,11 @@ async def async_setup_entry(hass, config_entry): }, ) - if not isinstance(config_entry.data[CONF_SENSOR_ID], int): - _async_fixup_sensor_id(hass, config_entry, config_entry.data[CONF_SENSOR_ID]) - - if ( - config_entry.data[CONF_SENSOR_ID] in duplicate_stations(hass) - and config_entry.source == SOURCE_IMPORT - ): - _LOGGER.warning( - "Removing duplicate sensors for station %s", - config_entry.data[CONF_SENSOR_ID], + # For backwards compat, set unique ID + if config_entry.unique_id is None: + hass.config_entries.async_update_entry( + config_entry, unique_id=config_entry.data[CONF_SENSOR_ID] ) - hass.async_create_task(hass.config_entries.async_remove(config_entry.entry_id)) - return False try: luftdaten = LuftDatenData( diff --git a/homeassistant/components/luftdaten/config_flow.py b/homeassistant/components/luftdaten/config_flow.py index b7aae271815..febc42e28fd 100644 --- a/homeassistant/components/luftdaten/config_flow.py +++ b/homeassistant/components/luftdaten/config_flow.py @@ -18,25 +18,6 @@ import homeassistant.helpers.config_validation as cv from .const import CONF_SENSOR_ID, DEFAULT_SCAN_INTERVAL, DOMAIN -@callback -def configured_sensors(hass): - """Return a set of configured Luftdaten sensors.""" - return { - entry.data[CONF_SENSOR_ID] - for entry in hass.config_entries.async_entries(DOMAIN) - } - - -@callback -def duplicate_stations(hass): - """Return a set of duplicate configured Luftdaten stations.""" - stations = [ - int(entry.data[CONF_SENSOR_ID]) - for entry in hass.config_entries.async_entries(DOMAIN) - ] - return {x for x in stations if stations.count(x) > 1} - - class LuftDatenFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Luftdaten config flow.""" @@ -59,10 +40,8 @@ class LuftDatenFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if not user_input: return self._show_form() - sensor_id = user_input[CONF_SENSOR_ID] - - if sensor_id in configured_sensors(self.hass): - return self._show_form({CONF_SENSOR_ID: "already_configured"}) + await self.async_set_unique_id(str(user_input[CONF_SENSOR_ID])) + self._abort_if_unique_id_configured() luftdaten = Luftdaten(user_input[CONF_SENSOR_ID]) try: @@ -86,4 +65,6 @@ class LuftDatenFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): scan_interval = user_input.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) user_input.update({CONF_SCAN_INTERVAL: scan_interval.total_seconds()}) - return self.async_create_entry(title=str(sensor_id), data=user_input) + return self.async_create_entry( + title=str(user_input[CONF_SENSOR_ID]), data=user_input + ) diff --git a/tests/components/luftdaten/conftest.py b/tests/components/luftdaten/conftest.py new file mode 100644 index 00000000000..28cade2c34d --- /dev/null +++ b/tests/components/luftdaten/conftest.py @@ -0,0 +1,32 @@ +"""Fixtures for Luftdaten tests.""" +from __future__ import annotations + +from collections.abc import Generator +from unittest.mock import patch + +import pytest + +from homeassistant.components.luftdaten import DOMAIN +from homeassistant.components.luftdaten.const import CONF_SENSOR_ID + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="12345", + domain=DOMAIN, + data={CONF_SENSOR_ID: 123456}, + unique_id="12345", + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[None, None, None]: + """Mock setting up a config entry.""" + with patch( + "homeassistant.components.luftdaten.async_setup_entry", return_value=True + ): + yield diff --git a/tests/components/luftdaten/test_config_flow.py b/tests/components/luftdaten/test_config_flow.py index ac475a8ab38..9b9aa139e02 100644 --- a/tests/components/luftdaten/test_config_flow.py +++ b/tests/components/luftdaten/test_config_flow.py @@ -1,84 +1,114 @@ """Define tests for the Luftdaten config flow.""" -from datetime import timedelta -from unittest.mock import patch +from unittest.mock import MagicMock, patch -from homeassistant import data_entry_flow -from homeassistant.components.luftdaten import DOMAIN, config_flow +from luftdaten.exceptions import LuftdatenConnectionError + +from homeassistant.components.luftdaten import DOMAIN from homeassistant.components.luftdaten.const import CONF_SENSOR_ID +from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_SCAN_INTERVAL, CONF_SHOW_ON_MAP +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) from tests.common import MockConfigEntry -async def test_duplicate_error(hass): +async def test_duplicate_error( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: """Test that errors are shown when duplicates are added.""" - conf = {CONF_SENSOR_ID: "12345abcde"} + mock_config_entry.add_to_hass(hass) - MockConfigEntry(domain=DOMAIN, data=conf).add_to_hass(hass) - flow = config_flow.LuftDatenFlowHandler() - flow.hass = hass + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) - result = await flow.async_step_user(user_input=conf) - assert result["errors"] == {CONF_SENSOR_ID: "already_configured"} + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == SOURCE_USER + assert "flow_id" in result + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_SENSOR_ID: 12345}, + ) + + assert result2.get("type") == RESULT_TYPE_ABORT + assert result2.get("reason") == "already_configured" -async def test_communication_error(hass): +async def test_communication_error(hass: HomeAssistant) -> None: """Test that no sensor is added while unable to communicate with API.""" - conf = {CONF_SENSOR_ID: "12345abcde"} + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) - flow = config_flow.LuftDatenFlowHandler() - flow.hass = hass + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == SOURCE_USER + assert "flow_id" in result - with patch("luftdaten.Luftdaten.get_data", return_value=None): - result = await flow.async_step_user(user_input=conf) - assert result["errors"] == {CONF_SENSOR_ID: "invalid_sensor"} + with patch("luftdaten.Luftdaten.get_data", side_effect=LuftdatenConnectionError): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_SENSOR_ID: 12345}, + ) + + assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("step_id") == SOURCE_USER + assert result2.get("errors") == {CONF_SENSOR_ID: "cannot_connect"} -async def test_invalid_sensor(hass): +async def test_invalid_sensor(hass: HomeAssistant) -> None: """Test that an invalid sensor throws an error.""" - conf = {CONF_SENSOR_ID: "12345abcde"} + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) - flow = config_flow.LuftDatenFlowHandler() - flow.hass = hass + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == SOURCE_USER + assert "flow_id" in result with patch("luftdaten.Luftdaten.get_data", return_value=False), patch( "luftdaten.Luftdaten.validate_sensor", return_value=False ): - result = await flow.async_step_user(user_input=conf) - assert result["errors"] == {CONF_SENSOR_ID: "invalid_sensor"} + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_SENSOR_ID: 12345}, + ) + + assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("step_id") == SOURCE_USER + assert result2.get("errors") == {CONF_SENSOR_ID: "invalid_sensor"} -async def test_show_form(hass): - """Test that the form is served with no input.""" - flow = config_flow.LuftDatenFlowHandler() - flow.hass = hass - - result = await flow.async_step_user(user_input=None) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" - - -async def test_step_user(hass): +async def test_step_user(hass: HomeAssistant, mock_setup_entry: MagicMock) -> None: """Test that the user step works.""" - conf = { - CONF_SENSOR_ID: "12345abcde", - CONF_SHOW_ON_MAP: False, - CONF_SCAN_INTERVAL: timedelta(minutes=5), - } + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) - flow = config_flow.LuftDatenFlowHandler() - flow.hass = hass + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == SOURCE_USER + assert "flow_id" in result with patch("luftdaten.Luftdaten.get_data", return_value=True), patch( "luftdaten.Luftdaten.validate_sensor", return_value=True ): - result = await flow.async_step_user(user_input=conf) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_SENSOR_ID: 12345, + CONF_SHOW_ON_MAP: False, + }, + ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "12345abcde" - assert result["data"] == { - CONF_SENSOR_ID: "12345abcde", - CONF_SHOW_ON_MAP: False, - CONF_SCAN_INTERVAL: 300, - } + assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("title") == "12345" + assert result2.get("data") == { + CONF_SENSOR_ID: 12345, + CONF_SHOW_ON_MAP: False, + CONF_SCAN_INTERVAL: 600.0, + } From b01078199a9b7e098707043f321acde0d6472628 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Sun, 19 Dec 2021 12:01:54 +0000 Subject: [PATCH 0739/2644] Use DeviceClass Enums in filter tests (#62138) --- tests/components/filter/test_sensor.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/components/filter/test_sensor.py b/tests/components/filter/test_sensor.py index ec831b79670..c2fc8cbdd06 100644 --- a/tests/components/filter/test_sensor.py +++ b/tests/components/filter/test_sensor.py @@ -16,8 +16,8 @@ from homeassistant.components.filter.sensor import ( ) from homeassistant.components.sensor import ( ATTR_STATE_CLASS, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, + SensorStateClass, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -274,15 +274,15 @@ async def test_setup(hass): 1, { "icon": "mdi:test", - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, + ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, }, ) await hass.async_block_till_done() state = hass.states.get("sensor.test") assert state.attributes["icon"] == "mdi:test" - assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_TEMPERATURE - assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_TOTAL_INCREASING + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE + assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.TOTAL_INCREASING assert state.state == "1.0" From d52caf77d5f1ab9f3a87bb09292a13924bfc0b5a Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 19 Dec 2021 07:02:29 -0500 Subject: [PATCH 0740/2644] Finish using enums in srp_energy (#62192) --- homeassistant/components/srp_energy/sensor.py | 14 +++++++------- tests/components/srp_energy/test_sensor.py | 12 ++++-------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/srp_energy/sensor.py b/homeassistant/components/srp_energy/sensor.py index 747b25faa50..f1579ea45e8 100644 --- a/homeassistant/components/srp_energy/sensor.py +++ b/homeassistant/components/srp_energy/sensor.py @@ -5,12 +5,12 @@ import logging import async_timeout from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout -from homeassistant.components.sensor import STATE_CLASS_TOTAL_INCREASING, SensorEntity -from homeassistant.const import ( - ATTR_ATTRIBUTION, - DEVICE_CLASS_ENERGY, - ENERGY_KILO_WATT_HOUR, +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorStateClass, ) +from homeassistant.const import ATTR_ATTRIBUTION, ENERGY_KILO_WATT_HOUR from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( @@ -144,12 +144,12 @@ class SrpEntity(SensorEntity): @property def device_class(self): """Return the device class.""" - return DEVICE_CLASS_ENERGY + return SensorDeviceClass.ENERGY @property def state_class(self): """Return the state class.""" - return STATE_CLASS_TOTAL_INCREASING + return SensorStateClass.TOTAL_INCREASING async def async_added_to_hass(self): """When entity is added to hass.""" diff --git a/tests/components/srp_energy/test_sensor.py b/tests/components/srp_energy/test_sensor.py index 5a4585cc8e5..cb19ae8720a 100644 --- a/tests/components/srp_energy/test_sensor.py +++ b/tests/components/srp_energy/test_sensor.py @@ -1,7 +1,7 @@ """Tests for the srp_energy sensor platform.""" from unittest.mock import MagicMock -from homeassistant.components.sensor import STATE_CLASS_TOTAL_INCREASING +from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.components.srp_energy.const import ( ATTRIBUTION, DEFAULT_NAME, @@ -11,11 +11,7 @@ from homeassistant.components.srp_energy.const import ( SRP_ENERGY_DOMAIN, ) from homeassistant.components.srp_energy.sensor import SrpEntity, async_setup_entry -from homeassistant.const import ( - ATTR_ATTRIBUTION, - DEVICE_CLASS_ENERGY, - ENERGY_KILO_WATT_HOUR, -) +from homeassistant.const import ATTR_ATTRIBUTION, ENERGY_KILO_WATT_HOUR async def test_async_setup_entry(hass): @@ -99,8 +95,8 @@ async def test_srp_entity(hass): assert srp_entity.should_poll is False assert srp_entity.extra_state_attributes[ATTR_ATTRIBUTION] == ATTRIBUTION assert srp_entity.available is not None - assert srp_entity.device_class == DEVICE_CLASS_ENERGY - assert srp_entity.state_class == STATE_CLASS_TOTAL_INCREASING + assert srp_entity.device_class is SensorDeviceClass.ENERGY + assert srp_entity.state_class is SensorStateClass.TOTAL_INCREASING await srp_entity.async_added_to_hass() assert srp_entity.state is not None From 779ce6216ca3877317a72197bd4aa3abea080460 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Sun, 19 Dec 2021 13:03:06 +0100 Subject: [PATCH 0741/2644] Change unload for P1 Monitor (#62213) --- homeassistant/components/p1_monitor/__init__.py | 3 +-- tests/components/p1_monitor/test_init.py | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/p1_monitor/__init__.py b/homeassistant/components/p1_monitor/__init__.py index d87c521332e..44a3c855c8c 100644 --- a/homeassistant/components/p1_monitor/__init__.py +++ b/homeassistant/components/p1_monitor/__init__.py @@ -45,8 +45,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload P1 Monitor config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - coordinator = hass.data[DOMAIN].pop(entry.entry_id) - await coordinator.p1monitor.close() + del hass.data[DOMAIN][entry.entry_id] return unload_ok diff --git a/tests/components/p1_monitor/test_init.py b/tests/components/p1_monitor/test_init.py index bddaff137e6..1cf9cf21966 100644 --- a/tests/components/p1_monitor/test_init.py +++ b/tests/components/p1_monitor/test_init.py @@ -24,6 +24,7 @@ async def test_load_unload_config_entry( await hass.async_block_till_done() assert not hass.data.get(DOMAIN) + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED @patch( From b0cfc76add00fac4c3c2246e768fc99116edbde4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 19 Dec 2021 13:04:52 +0100 Subject: [PATCH 0742/2644] Use new enums in smarthab (#62208) Co-authored-by: epenet --- homeassistant/components/smarthab/cover.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/smarthab/cover.py b/homeassistant/components/smarthab/cover.py index 64e693941b5..6a53090d3f8 100644 --- a/homeassistant/components/smarthab/cover.py +++ b/homeassistant/components/smarthab/cover.py @@ -7,10 +7,10 @@ from requests.exceptions import Timeout from homeassistant.components.cover import ( ATTR_POSITION, - DEVICE_CLASS_WINDOW, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION, + CoverDeviceClass, CoverEntity, ) @@ -37,7 +37,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class SmartHabCover(CoverEntity): """Representation a cover.""" - _attr_device_class = DEVICE_CLASS_WINDOW + _attr_device_class = CoverDeviceClass.WINDOW def __init__(self, cover): """Initialize a SmartHabCover.""" From 7d3dfeea64323ce89926cc650423c8b1e1c8270b Mon Sep 17 00:00:00 2001 From: Gage Benne Date: Sun, 19 Dec 2021 07:05:51 -0500 Subject: [PATCH 0743/2644] Bump pydexcom to 0.2.2 (#62207) --- homeassistant/components/dexcom/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/dexcom/manifest.json b/homeassistant/components/dexcom/manifest.json index 1bf15776cad..6133a67bcf1 100644 --- a/homeassistant/components/dexcom/manifest.json +++ b/homeassistant/components/dexcom/manifest.json @@ -3,7 +3,7 @@ "name": "Dexcom", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dexcom", - "requirements": ["pydexcom==0.2.1"], + "requirements": ["pydexcom==0.2.2"], "codeowners": ["@gagebenne"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index cf103d060cf..f075983c77b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1444,7 +1444,7 @@ pydeconz==85 pydelijn==0.6.1 # homeassistant.components.dexcom -pydexcom==0.2.1 +pydexcom==0.2.2 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 70245fcf559..a1a79963303 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -877,7 +877,7 @@ pydaikin==2.6.0 pydeconz==85 # homeassistant.components.dexcom -pydexcom==0.2.1 +pydexcom==0.2.2 # homeassistant.components.zwave pydispatcher==2.0.5 From 6a489bb45a83f7cb8c8bfe8b5e8b6e8c11909ae3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 19 Dec 2021 13:06:36 +0100 Subject: [PATCH 0744/2644] Cleanup attr** usage in uptime (#62212) Co-authored-by: epenet --- homeassistant/components/uptime/sensor.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/uptime/sensor.py b/homeassistant/components/uptime/sensor.py index 90d2a9e34ec..a622835a0da 100644 --- a/homeassistant/components/uptime/sensor.py +++ b/homeassistant/components/uptime/sensor.py @@ -45,9 +45,10 @@ async def async_setup_platform( class UptimeSensor(SensorEntity): """Representation of an uptime sensor.""" + _attr_device_class = SensorDeviceClass.TIMESTAMP + _attr_should_poll = False + def __init__(self, name: str) -> None: """Initialize the uptime sensor.""" - self._attr_name: str = name - self._attr_device_class = SensorDeviceClass.TIMESTAMP - self._attr_should_poll: bool = False + self._attr_name = name self._attr_native_value = dt_util.utcnow() From 1d1c91a4eb6dea626873d9fc91246040ab4f7034 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 19 Dec 2021 13:08:59 +0100 Subject: [PATCH 0745/2644] Use _attr_** in slide (#62206) Co-authored-by: epenet --- homeassistant/components/slide/cover.py | 35 +++++-------------------- 1 file changed, 7 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/slide/cover.py b/homeassistant/components/slide/cover.py index 470cde8ac94..ab8d00717ff 100644 --- a/homeassistant/components/slide/cover.py +++ b/homeassistant/components/slide/cover.py @@ -4,10 +4,10 @@ import logging from homeassistant.components.cover import ( ATTR_POSITION, - DEVICE_CLASS_CURTAIN, STATE_CLOSED, STATE_CLOSING, STATE_OPENING, + CoverDeviceClass, CoverEntity, ) from homeassistant.const import ATTR_ID @@ -35,30 +35,19 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class SlideCover(CoverEntity): """Representation of a Slide cover.""" + _attr_assumed_state = True + _attr_device_class = CoverDeviceClass.CURTAIN + def __init__(self, api, slide): """Initialize the cover.""" self._api = api self._slide = slide self._id = slide["id"] - self._unique_id = slide["mac"] - self._name = slide["name"] + self._attr_extra_state_attributes = {ATTR_ID: self._id} + self._attr_unique_id = slide["mac"] + self._attr_name = slide["name"] self._invert = slide["invert"] - @property - def unique_id(self): - """Return the device unique id.""" - return self._unique_id - - @property - def name(self): - """Return the device name.""" - return self._name - - @property - def extra_state_attributes(self): - """Return device specific state attributes.""" - return {ATTR_ID: self._id} - @property def is_opening(self): """Return if the cover is opening or not.""" @@ -81,16 +70,6 @@ class SlideCover(CoverEntity): """Return False if state is not available.""" return self._slide["online"] - @property - def assumed_state(self): - """Let HA know the integration is assumed state.""" - return True - - @property - def device_class(self): - """Return the device class of the cover.""" - return DEVICE_CLASS_CURTAIN - @property def current_cover_position(self): """Return the current position of cover shutter.""" From 19fc15c3ac2fc50832c9eb60b12834c8857d9286 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 19 Dec 2021 13:12:02 +0100 Subject: [PATCH 0746/2644] Use new enums in saj (#62205) Co-authored-by: epenet --- homeassistant/components/saj/sensor.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py index 8e59899de27..07f8aff3f9e 100644 --- a/homeassistant/components/saj/sensor.py +++ b/homeassistant/components/saj/sensor.py @@ -9,9 +9,9 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, + SensorStateClass, ) from homeassistant.const import ( CONF_HOST, @@ -19,9 +19,6 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_TYPE, CONF_USERNAME, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_TEMPERATURE, ENERGY_KILO_WATT_HOUR, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, @@ -178,9 +175,9 @@ class SAJsensor(SensorEntity): self._state = self._sensor.value if pysaj_sensor.name in ("current_power", "temperature"): - self._attr_state_class = STATE_CLASS_MEASUREMENT + self._attr_state_class = SensorStateClass.MEASUREMENT if pysaj_sensor.name == "total_yield": - self._attr_state_class = STATE_CLASS_TOTAL_INCREASING + self._attr_state_class = SensorStateClass.TOTAL_INCREASING @property def name(self): @@ -204,14 +201,14 @@ class SAJsensor(SensorEntity): def device_class(self): """Return the device class the sensor belongs to.""" if self.unit_of_measurement == POWER_WATT: - return DEVICE_CLASS_POWER + return SensorDeviceClass.POWER if self.unit_of_measurement == ENERGY_KILO_WATT_HOUR: - return DEVICE_CLASS_ENERGY + return SensorDeviceClass.ENERGY if ( self.unit_of_measurement == TEMP_CELSIUS or self._sensor.unit == TEMP_FAHRENHEIT ): - return DEVICE_CLASS_TEMPERATURE + return SensorDeviceClass.TEMPERATURE @property def should_poll(self) -> bool: From 340ffc96dc8493aedff7a5ee34bf026e463d2039 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 19 Dec 2021 13:15:09 +0100 Subject: [PATCH 0747/2644] Use new enums in sleepiq (#62203) Co-authored-by: epenet --- homeassistant/components/sleepiq/binary_sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sleepiq/binary_sensor.py b/homeassistant/components/sleepiq/binary_sensor.py index cfbd6f576be..9a9aeb6374d 100644 --- a/homeassistant/components/sleepiq/binary_sensor.py +++ b/homeassistant/components/sleepiq/binary_sensor.py @@ -1,6 +1,6 @@ """Support for SleepIQ sensors.""" from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_OCCUPANCY, + BinarySensorDeviceClass, BinarySensorEntity, ) @@ -40,9 +40,9 @@ class IsInBedBinarySensor(SleepIQSensor, BinarySensorEntity): return self._state is True @property - def device_class(self): + def device_class(self) -> BinarySensorDeviceClass: """Return the class of this sensor.""" - return DEVICE_CLASS_OCCUPANCY + return BinarySensorDeviceClass.OCCUPANCY def update(self): """Get the latest data from SleepIQ and updates the states.""" From 868a1c222c6e7cfbbfdab90f181a451260ccd5b8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 19 Dec 2021 13:23:04 +0100 Subject: [PATCH 0748/2644] Use _attr_attribution in poolsense (#62180) Co-authored-by: epenet --- homeassistant/components/poolsense/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/poolsense/__init__.py b/homeassistant/components/poolsense/__init__.py index 31362e41668..72bfee387eb 100644 --- a/homeassistant/components/poolsense/__init__.py +++ b/homeassistant/components/poolsense/__init__.py @@ -7,7 +7,7 @@ from poolsense import PoolSense from poolsense.exceptions import PoolSenseError from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION, CONF_EMAIL, CONF_PASSWORD, Platform +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client from homeassistant.helpers.entity import EntityDescription @@ -19,7 +19,7 @@ from homeassistant.helpers.update_coordinator import ( from .const import ATTRIBUTION, DOMAIN -PLATFORMS = [Platform.SENSOR, Platform.BINARY_SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] _LOGGER = logging.getLogger(__name__) @@ -62,7 +62,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class PoolSenseEntity(CoordinatorEntity): """Implements a common class elements representing the PoolSense component.""" - _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + _attr_attribution = ATTRIBUTION def __init__(self, coordinator, email, description: EntityDescription): """Initialize poolsense sensor.""" From 8bca984d61b38ae286b34d2f0ca97e87c30249aa Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 19 Dec 2021 13:25:51 +0100 Subject: [PATCH 0749/2644] Use _attr_** in openweathermap (#62179) Co-authored-by: epenet --- .../components/openweathermap/sensor.py | 8 +--- .../components/openweathermap/weather.py | 43 ++++--------------- 2 files changed, 10 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/openweathermap/sensor.py b/homeassistant/components/openweathermap/sensor.py index 4a34069e036..20ee621b9dc 100644 --- a/homeassistant/components/openweathermap/sensor.py +++ b/homeassistant/components/openweathermap/sensor.py @@ -9,7 +9,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo @@ -71,7 +70,7 @@ class AbstractOpenWeatherMapSensor(SensorEntity): """Abstract class for an OpenWeatherMap sensor.""" _attr_should_poll = False - _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + _attr_attribution = ATTRIBUTION def __init__( self, @@ -94,11 +93,6 @@ class AbstractOpenWeatherMapSensor(SensorEntity): name=DEFAULT_NAME, ) - @property - def attribution(self) -> str: - """Return the attribution.""" - return ATTRIBUTION - @property def available(self) -> bool: """Return True if entity is available.""" diff --git a/homeassistant/components/openweathermap/weather.py b/homeassistant/components/openweathermap/weather.py index aafa1a9c808..d4ab99bc30b 100644 --- a/homeassistant/components/openweathermap/weather.py +++ b/homeassistant/components/openweathermap/weather.py @@ -47,6 +47,10 @@ async def async_setup_entry( class OpenWeatherMapWeather(WeatherEntity): """Implementation of an OpenWeatherMap sensor.""" + _attr_attribution = ATTRIBUTION + _attr_should_poll = False + _attr_temperature_unit = TEMP_CELSIUS + def __init__( self, name: str, @@ -54,39 +58,15 @@ class OpenWeatherMapWeather(WeatherEntity): weather_coordinator: WeatherUpdateCoordinator, ) -> None: """Initialize the sensor.""" - self._name = name - self._unique_id = unique_id - self._weather_coordinator = weather_coordinator - - @property - def name(self) -> str: - """Return the name of the sensor.""" - return self._name - - @property - def unique_id(self) -> str: - """Return a unique_id for this entity.""" - return self._unique_id - - @property - def device_info(self) -> DeviceInfo: - """Return the device info.""" - return DeviceInfo( + self._attr_name = name + self._attr_unique_id = unique_id + self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, self._unique_id)}, + identifiers={(DOMAIN, unique_id)}, manufacturer=MANUFACTURER, name=DEFAULT_NAME, ) - - @property - def should_poll(self) -> bool: - """Return the polling requirement of the entity.""" - return False - - @property - def attribution(self) -> str: - """Return the attribution.""" - return ATTRIBUTION + self._weather_coordinator = weather_coordinator @property def condition(self) -> str | None: @@ -98,11 +78,6 @@ class OpenWeatherMapWeather(WeatherEntity): """Return the temperature.""" return self._weather_coordinator.data[ATTR_API_TEMPERATURE] - @property - def temperature_unit(self) -> str: - """Return the unit of measurement.""" - return TEMP_CELSIUS - @property def pressure(self) -> float | None: """Return the pressure.""" From abb36ff45f46a5ca20af6393b127063f888c0a66 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 19 Dec 2021 13:27:23 +0100 Subject: [PATCH 0750/2644] Use _attr_attribution in sense (#62181) Co-authored-by: epenet --- homeassistant/components/sense/sensor.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sense/sensor.py b/homeassistant/components/sense/sensor.py index b5fdb4398c0..9a2c968741e 100644 --- a/homeassistant/components/sense/sensor.py +++ b/homeassistant/components/sense/sensor.py @@ -5,7 +5,6 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.const import ( - ATTR_ATTRIBUTION, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, PERCENTAGE, @@ -152,7 +151,7 @@ class SenseActiveSensor(SensorEntity): _attr_icon = ICON _attr_native_unit_of_measurement = POWER_WATT - _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + _attr_attribution = ATTRIBUTION _attr_should_poll = False _attr_available = False _attr_state_class = SensorStateClass.MEASUREMENT @@ -205,7 +204,7 @@ class SenseVoltageSensor(SensorEntity): """Implementation of a Sense energy voltage sensor.""" _attr_native_unit_of_measurement = ELECTRIC_POTENTIAL_VOLT - _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + _attr_attribution = ATTRIBUTION _attr_icon = ICON _attr_should_poll = False _attr_available = False @@ -251,7 +250,7 @@ class SenseTrendsSensor(CoordinatorEntity, SensorEntity): _attr_device_class = SensorDeviceClass.ENERGY _attr_state_class = SensorStateClass.TOTAL _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR - _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + _attr_attribution = ATTRIBUTION _attr_icon = ICON _attr_should_poll = False @@ -299,7 +298,7 @@ class SenseEnergyDevice(SensorEntity): _attr_available = False _attr_state_class = SensorStateClass.MEASUREMENT _attr_native_unit_of_measurement = POWER_WATT - _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + _attr_attribution = ATTRIBUTION _attr_device_class = SensorDeviceClass.POWER _attr_should_poll = False From 867cbeedb98c666cda686d213ba8dc6eb9778599 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 19 Dec 2021 07:30:16 -0500 Subject: [PATCH 0751/2644] Use enums in zwave (#62131) Co-authored-by: Franck Nijhof --- homeassistant/components/zwave/binary_sensor.py | 2 +- homeassistant/components/zwave/cover.py | 5 +++-- homeassistant/components/zwave/sensor.py | 8 ++++---- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/zwave/binary_sensor.py b/homeassistant/components/zwave/binary_sensor.py index 094279c4e7a..e85daffec3c 100644 --- a/homeassistant/components/zwave/binary_sensor.py +++ b/homeassistant/components/zwave/binary_sensor.py @@ -59,7 +59,7 @@ class ZWaveBinarySensor(BinarySensorEntity, ZWaveDeviceEntity): @property def device_class(self): - """Return the class of this sensor, from DEVICE_CLASSES.""" + """Return the class of this sensor, from BinarySensorDeviceClass.""" return self._sensor_type diff --git a/homeassistant/components/zwave/cover.py b/homeassistant/components/zwave/cover.py index 688ee666676..fc88db56ae7 100644 --- a/homeassistant/components/zwave/cover.py +++ b/homeassistant/components/zwave/cover.py @@ -6,6 +6,7 @@ from homeassistant.components.cover import ( DOMAIN, SUPPORT_CLOSE, SUPPORT_OPEN, + CoverDeviceClass, CoverEntity, ) from homeassistant.core import callback @@ -156,8 +157,8 @@ class ZwaveGarageDoorBase(ZWaveDeviceEntity, CoverEntity): @property def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return "garage" + """Return the class of this device, from CoverDeviceClass.""" + return CoverDeviceClass.GARAGE @property def supported_features(self): diff --git a/homeassistant/components/zwave/sensor.py b/homeassistant/components/zwave/sensor.py index 75046c2f9d8..894bac8a292 100644 --- a/homeassistant/components/zwave/sensor.py +++ b/homeassistant/components/zwave/sensor.py @@ -1,6 +1,6 @@ """Support for Z-Wave sensors.""" -from homeassistant.components.sensor import DEVICE_CLASS_BATTERY, DOMAIN, SensorEntity -from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.components.sensor import DOMAIN, SensorDeviceClass, SensorEntity +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -83,7 +83,7 @@ class ZWaveMultilevelSensor(ZWaveSensor): def device_class(self): """Return the class of this device.""" if self._units in ["C", "F"]: - return DEVICE_CLASS_TEMPERATURE + return SensorDeviceClass.TEMPERATURE return None @property @@ -115,4 +115,4 @@ class ZWaveBatterySensor(ZWaveSensor): @property def device_class(self): """Return the class of this device.""" - return DEVICE_CLASS_BATTERY + return SensorDeviceClass.BATTERY From 4e2195baa17e946198f5cd4888538be15f1f14b8 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 19 Dec 2021 07:40:39 -0500 Subject: [PATCH 0752/2644] Remove deprecated yaml config from environment canada (#61839) --- .../components/environment_canada/__init__.py | 27 ------------ .../components/environment_canada/camera.py | 34 +------------- .../environment_canada/config_flow.py | 4 -- .../components/environment_canada/sensor.py | 38 ++-------------- .../components/environment_canada/weather.py | 24 +--------- .../environment_canada/test_config_flow.py | 44 ++----------------- 6 files changed, 10 insertions(+), 161 deletions(-) diff --git a/homeassistant/components/environment_canada/__init__.py b/homeassistant/components/environment_canada/__init__.py index 90227ec997d..9b5ce11861c 100644 --- a/homeassistant/components/environment_canada/__init__.py +++ b/homeassistant/components/environment_canada/__init__.py @@ -5,7 +5,6 @@ import xml.etree.ElementTree as et from env_canada import ECRadar, ECWeather, ec_exc -from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, Platform from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -63,32 +62,6 @@ async def async_unload_entry(hass, config_entry): return unload_ok -def trigger_import(hass, config): - """Trigger a import of YAML config into a config_entry.""" - _LOGGER.warning( - "Environment Canada YAML configuration is deprecated; your YAML configuration " - "has been imported into the UI and can be safely removed" - ) - if not config.get(CONF_LANGUAGE): - config[CONF_LANGUAGE] = "English" - - data = {} - for key in ( - CONF_STATION, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_LANGUAGE, - ): - if config.get(key): - data[key] = config[key] - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=data - ) - ) - - class ECDataUpdateCoordinator(DataUpdateCoordinator): """Class to manage fetching EC data.""" diff --git a/homeassistant/components/environment_canada/camera.py b/homeassistant/components/environment_canada/camera.py index 86f6299585c..5de1086f98c 100644 --- a/homeassistant/components/environment_canada/camera.py +++ b/homeassistant/components/environment_canada/camera.py @@ -1,40 +1,10 @@ """Support for the Environment Canada radar imagery.""" from __future__ import annotations -import voluptuous as vol - -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME -import homeassistant.helpers.config_validation as cv +from homeassistant.components.camera import Camera from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import trigger_import -from .const import ATTR_OBSERVATION_TIME, CONF_STATION, DOMAIN - -CONF_LOOP = "loop" -CONF_PRECIP_TYPE = "precip_type" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_LOOP, default=True): cv.boolean, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_STATION): cv.matches_regex(r"^C[A-Z]{4}$|^[A-Z]{3}$"), - vol.Inclusive(CONF_LATITUDE, "latlon"): cv.latitude, - vol.Inclusive(CONF_LONGITUDE, "latlon"): cv.longitude, - vol.Optional(CONF_PRECIP_TYPE): vol.In(["RAIN", "SNOW"]), - } -) - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Environment Canada camera.""" - lat = config.get(CONF_LATITUDE, hass.config.latitude) - lon = config.get(CONF_LONGITUDE, hass.config.longitude) - - config[CONF_LATITUDE] = lat - config[CONF_LONGITUDE] = lon - - trigger_import(hass, config) +from .const import ATTR_OBSERVATION_TIME, DOMAIN async def async_setup_entry(hass, config_entry, async_add_entities): diff --git a/homeassistant/components/environment_canada/config_flow.py b/homeassistant/components/environment_canada/config_flow.py index 683f312d9d5..07b6eac0da0 100644 --- a/homeassistant/components/environment_canada/config_flow.py +++ b/homeassistant/components/environment_canada/config_flow.py @@ -94,7 +94,3 @@ class EnvironmentCanadaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="user", data_schema=data_schema, errors=errors ) - - async def async_step_import(self, import_data): - """Import entry from configuration.yaml.""" - return await self.async_step_user(import_data) diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py index e71411913c9..630d78469ff 100644 --- a/homeassistant/components/environment_canada/sensor.py +++ b/homeassistant/components/environment_canada/sensor.py @@ -4,28 +4,11 @@ import re import voluptuous as vol -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, - SensorDeviceClass, - SensorEntity, -) -from homeassistant.const import ( - ATTR_LOCATION, - CONF_LATITUDE, - CONF_LONGITUDE, - TEMP_CELSIUS, -) -import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.const import ATTR_LOCATION, TEMP_CELSIUS from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import trigger_import -from .const import ( - ATTR_OBSERVATION_TIME, - ATTR_STATION, - CONF_LANGUAGE, - CONF_STATION, - DOMAIN, -) +from .const import ATTR_OBSERVATION_TIME, ATTR_STATION, DOMAIN ATTR_TIME = "alert time" @@ -41,21 +24,6 @@ def validate_station(station): return station -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_LANGUAGE, default="english"): vol.In(["english", "french"]), - vol.Optional(CONF_STATION): validate_station, - vol.Inclusive(CONF_LATITUDE, "latlon"): cv.latitude, - vol.Inclusive(CONF_LONGITUDE, "latlon"): cv.longitude, - } -) - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Environment Canada sensor.""" - trigger_import(hass, config) - - async def async_setup_entry(hass, config_entry, async_add_entities): """Add a weather entity from a config_entry.""" coordinator = hass.data[DOMAIN][config_entry.entry_id]["weather_coordinator"] diff --git a/homeassistant/components/environment_canada/weather.py b/homeassistant/components/environment_canada/weather.py index 5231e95e2bc..3bd57163abc 100644 --- a/homeassistant/components/environment_canada/weather.py +++ b/homeassistant/components/environment_canada/weather.py @@ -24,18 +24,13 @@ from homeassistant.components.weather import ( ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, - PLATFORM_SCHEMA, WeatherEntity, ) -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS -import homeassistant.helpers.config_validation as cv +from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt -from . import trigger_import -from .const import CONF_STATION, DOMAIN - -CONF_FORECAST = "forecast" +from .const import DOMAIN def validate_station(station): @@ -47,16 +42,6 @@ def validate_station(station): return station -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_STATION): validate_station, - vol.Inclusive(CONF_LATITUDE, "latlon"): cv.latitude, - vol.Inclusive(CONF_LONGITUDE, "latlon"): cv.longitude, - vol.Optional(CONF_FORECAST, default="daily"): vol.In(["daily", "hourly"]), - } -) - # Icon codes from http://dd.weatheroffice.ec.gc.ca/citypage_weather/ # docs/current_conditions_icon_code_descriptions_e.csv ICON_CONDITION_MAP = { @@ -75,11 +60,6 @@ ICON_CONDITION_MAP = { } -async def async_setup_platform(hass, config, async_add_entries, discovery_info=None): - """Set up the Environment Canada weather.""" - trigger_import(hass, config) - - async def async_setup_entry(hass, config_entry, async_add_entities): """Add a weather entity from a config_entry.""" coordinator = hass.data[DOMAIN][config_entry.entry_id]["weather_coordinator"] diff --git a/tests/components/environment_canada/test_config_flow.py b/tests/components/environment_canada/test_config_flow.py index 2614778f9b4..de3ff516eae 100644 --- a/tests/components/environment_canada/test_config_flow.py +++ b/tests/components/environment_canada/test_config_flow.py @@ -11,7 +11,6 @@ from homeassistant.components.environment_canada.const import ( CONF_STATION, DOMAIN, ) -from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from tests.common import MockConfigEntry @@ -123,25 +122,8 @@ async def test_exception_handling(hass, error): assert result["errors"] == {"base": base_error} -async def test_import_station_not_specified(hass): - """Test that the import step works.""" - with mocked_ec(), patch( - "homeassistant.components.environment_canada.async_setup_entry", - return_value=True, - ): - fake_config = dict(FAKE_CONFIG) - del fake_config[CONF_STATION] - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=fake_config - ) - await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["data"] == FAKE_CONFIG - assert result["title"] == FAKE_TITLE - - -async def test_import_lat_lon_not_specified(hass): - """Test that the import step works.""" +async def test_lat_lon_not_specified(hass): + """Test that the import step works when coordinates are not specified.""" with mocked_ec(), patch( "homeassistant.components.environment_canada.async_setup_entry", return_value=True, @@ -150,29 +132,9 @@ async def test_import_lat_lon_not_specified(hass): del fake_config[CONF_LATITUDE] del fake_config[CONF_LONGITUDE] result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=fake_config + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=fake_config ) await hass.async_block_till_done() assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"] == FAKE_CONFIG assert result["title"] == FAKE_TITLE - - -async def test_async_step_import(hass): - """Test that the import step works.""" - with mocked_ec(), patch( - "homeassistant.components.environment_canada.async_setup_entry", - return_value=True, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=FAKE_CONFIG - ) - await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["data"] == FAKE_CONFIG - assert result["title"] == FAKE_TITLE - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=FAKE_CONFIG - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT From 355f2f25d88e31f7f90077af586a4c9ef09cc886 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Sun, 19 Dec 2021 14:55:53 +0100 Subject: [PATCH 0753/2644] Support additional Apple TV device types (#61104) --- .../components/apple_tv/manifest.json | 9 +++- homeassistant/generated/zeroconf.py | 42 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/apple_tv/manifest.json b/homeassistant/components/apple_tv/manifest.json index 02fabc02565..9bc63773522 100644 --- a/homeassistant/components/apple_tv/manifest.json +++ b/homeassistant/components/apple_tv/manifest.json @@ -7,7 +7,14 @@ "zeroconf": [ "_mediaremotetv._tcp.local.", "_touch-able._tcp.local.", - {"type":"_airplay._tcp.local.","properties":{"model":"appletv*"}} + "_appletv-v2._tcp.local.", + "_hscp._tcp.local.", + {"type":"_airplay._tcp.local.", "properties": {"model":"appletv*"}}, + {"type":"_airplay._tcp.local.", "properties": {"model":"audioaccessory*"}}, + {"type":"_airplay._tcp.local.", "properties": {"am":"airport*"}}, + {"type":"_raop._tcp.local.", "properties": {"am":"appletv*"}}, + {"type":"_raop._tcp.local.", "properties": {"am":"audioaccessory*"}}, + {"type":"_raop._tcp.local.", "properties": {"am":"airport*"}} ], "codeowners": ["@postlund"], "iot_class": "local_push" diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index af828c077c5..8825b1639e9 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -18,6 +18,18 @@ ZEROCONF = { "model": "appletv*" } }, + { + "domain": "apple_tv", + "properties": { + "model": "audioaccessory*" + } + }, + { + "domain": "apple_tv", + "properties": { + "am": "airport*" + } + }, { "domain": "samsungtv", "properties": { @@ -30,6 +42,11 @@ ZEROCONF = { "domain": "guardian" } ], + "_appletv-v2._tcp.local.": [ + { + "domain": "apple_tv" + } + ], "_axis-video._tcp.local.": [ { "domain": "axis", @@ -124,6 +141,11 @@ ZEROCONF = { "domain": "homekit" } ], + "_hscp._tcp.local.": [ + { + "domain": "apple_tv" + } + ], "_http._tcp.local.": [ { "domain": "bosch_shc", @@ -241,6 +263,26 @@ ZEROCONF = { "name": "brother*" } ], + "_raop._tcp.local.": [ + { + "domain": "apple_tv", + "properties": { + "am": "appletv*" + } + }, + { + "domain": "apple_tv", + "properties": { + "am": "audioaccessory*" + } + }, + { + "domain": "apple_tv", + "properties": { + "am": "airport*" + } + } + ], "_sonos._tcp.local.": [ { "domain": "sonos" From d325de751098656ca98a44c929ff9721f1f6284c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 19 Dec 2021 09:44:53 -0600 Subject: [PATCH 0754/2644] Add additional guarding to color_rgb_to_rgbww (#62220) --- homeassistant/util/color.py | 4 +++- tests/util/test_color.py | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index e8f802e3aa5..ccb4980492f 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -450,7 +450,9 @@ def color_rgb_to_rgbww( w_r, w_g, w_b = color_temperature_to_rgb(color_temp_kelvin) # Find the ratio of the midpoint white in the input rgb channels - white_level = min(r / w_r, g / w_g, b / w_b if w_b else 0) + white_level = min( + r / w_r if w_r else 0, g / w_g if w_g else 0, b / w_b if w_b else 0 + ) # Subtract the white portion from the rgb channels. rgb = (r - w_r * white_level, g - w_g * white_level, b - w_b * white_level) diff --git a/tests/util/test_color.py b/tests/util/test_color.py index 9c4b781dc65..31778781676 100644 --- a/tests/util/test_color.py +++ b/tests/util/test_color.py @@ -401,6 +401,8 @@ def test_color_rgb_to_rgbww(): assert color_util.color_rgb_to_rgbww(64, 64, 64, 154, 370) == (0, 14, 25, 64, 64) assert color_util.color_rgb_to_rgbww(32, 64, 16, 154, 370) == (9, 64, 0, 38, 38) assert color_util.color_rgb_to_rgbww(0, 0, 0, 154, 370) == (0, 0, 0, 0, 0) + assert color_util.color_rgb_to_rgbww(0, 0, 0, 0, 100) == (0, 0, 0, 0, 0) + assert color_util.color_rgb_to_rgbww(255, 255, 255, 1, 5) == (103, 69, 0, 255, 255) def test_color_temperature_to_rgbww(): From 9128693e714fca8416bb2173b9503524c26259d7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 19 Dec 2021 10:48:42 -0600 Subject: [PATCH 0755/2644] Add reboot button to Magic Home/flux_led (#62323) --- homeassistant/components/flux_led/__init__.py | 9 +++- homeassistant/components/flux_led/button.py | 47 +++++++++++++++++++ tests/components/flux_led/__init__.py | 1 + tests/components/flux_led/test_button.py | 41 ++++++++++++++++ 4 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/flux_led/button.py create mode 100644 tests/components/flux_led/test_button.py diff --git a/homeassistant/components/flux_led/__init__.py b/homeassistant/components/flux_led/__init__.py index 79ec3e8cf13..48115f41f5f 100644 --- a/homeassistant/components/flux_led/__init__.py +++ b/homeassistant/components/flux_led/__init__.py @@ -40,8 +40,13 @@ from .discovery import ( _LOGGER = logging.getLogger(__name__) PLATFORMS_BY_TYPE: Final = { - DeviceType.Bulb: [Platform.LIGHT, Platform.NUMBER, Platform.SWITCH], - DeviceType.Switch: [Platform.SWITCH], + DeviceType.Bulb: [ + Platform.BUTTON, + Platform.LIGHT, + Platform.NUMBER, + Platform.SWITCH, + ], + DeviceType.Switch: [Platform.BUTTON, Platform.SWITCH], } DISCOVERY_INTERVAL: Final = timedelta(minutes=15) REQUEST_REFRESH_DELAY: Final = 1.5 diff --git a/homeassistant/components/flux_led/button.py b/homeassistant/components/flux_led/button.py new file mode 100644 index 00000000000..cc1f4f891bd --- /dev/null +++ b/homeassistant/components/flux_led/button.py @@ -0,0 +1,47 @@ +"""Support for Magic home button.""" +from __future__ import annotations + +from flux_led.aio import AIOWifiLedBulb + +from homeassistant import config_entries +from homeassistant.components.button import ButtonEntity +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import FluxLedUpdateCoordinator +from .const import DOMAIN +from .entity import FluxBaseEntity + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Magic Home button based on a config entry.""" + coordinator: FluxLedUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities([FluxRestartButton(coordinator.device, entry)]) + + +class FluxRestartButton(FluxBaseEntity, ButtonEntity): + """Representation of a Flux restart button.""" + + _attr_should_poll = False + _attr_entity_category = EntityCategory.CONFIG + + def __init__( + self, + device: AIOWifiLedBulb, + entry: config_entries.ConfigEntry, + ) -> None: + """Initialize the reboot button.""" + super().__init__(device, entry) + self._attr_name = f"{entry.data[CONF_NAME]} Restart" + if entry.unique_id: + self._attr_unique_id = f"{entry.unique_id}_restart" + + async def async_press(self) -> None: + """Send out a restart command.""" + await self._device.async_reboot() diff --git a/tests/components/flux_led/__init__.py b/tests/components/flux_led/__init__.py index f86a06e41cc..5efeba1da94 100644 --- a/tests/components/flux_led/__init__.py +++ b/tests/components/flux_led/__init__.py @@ -124,6 +124,7 @@ def _mocked_switch() -> AIOWifiLedBulb: switch.device_type = DeviceType.Switch switch.requires_turn_on = True + switch.async_reboot = AsyncMock() switch.async_setup = AsyncMock(side_effect=_save_setup_callback) switch.async_stop = AsyncMock() switch.async_update = AsyncMock() diff --git a/tests/components/flux_led/test_button.py b/tests/components/flux_led/test_button.py new file mode 100644 index 00000000000..1117373fcd6 --- /dev/null +++ b/tests/components/flux_led/test_button.py @@ -0,0 +1,41 @@ +"""Tests for button platform.""" +from homeassistant.components import flux_led +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN +from homeassistant.components.flux_led.const import DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from . import ( + DEFAULT_ENTRY_TITLE, + IP_ADDRESS, + MAC_ADDRESS, + _mocked_switch, + _patch_discovery, + _patch_wifibulb, +) + +from tests.common import MockConfigEntry + + +async def test_switch_reboot(hass: HomeAssistant) -> None: + """Test a smart plug can be rebooted.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + switch = _mocked_switch() + with _patch_discovery(), _patch_wifibulb(device=switch): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "button.bulb_rgbcw_ddeeff_restart" + + assert hass.states.get(entity_id) + + await hass.services.async_call( + BUTTON_DOMAIN, "press", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + switch.async_reboot.assert_called_once() From 2f4c29cf1fff6e99c38250de1e760e3824ba3520 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 19 Dec 2021 10:52:48 -0600 Subject: [PATCH 0756/2644] Move Magic Home/flux_led coordinator to its own module (#62324) --- homeassistant/components/flux_led/__init__.py | 32 +----------- .../components/flux_led/coordinator.py | 49 +++++++++++++++++++ homeassistant/components/flux_led/entity.py | 2 +- homeassistant/components/flux_led/light.py | 2 +- homeassistant/components/flux_led/number.py | 2 +- homeassistant/components/flux_led/switch.py | 2 +- 6 files changed, 54 insertions(+), 35 deletions(-) create mode 100644 homeassistant/components/flux_led/coordinator.py diff --git a/homeassistant/components/flux_led/__init__.py b/homeassistant/components/flux_led/__init__.py index 48115f41f5f..25f8b2554ea 100644 --- a/homeassistant/components/flux_led/__init__.py +++ b/homeassistant/components/flux_led/__init__.py @@ -14,11 +14,9 @@ from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STARTED, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( DISCOVER_SCAN_TIMEOUT, @@ -28,6 +26,7 @@ from .const import ( SIGNAL_STATE_UPDATED, STARTUP_SCAN_TIMEOUT, ) +from .coordinator import FluxLedUpdateCoordinator from .discovery import ( async_clear_discovery_cache, async_discover_device, @@ -134,32 +133,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: del hass.data[DOMAIN][entry.entry_id] await device.async_stop() return unload_ok - - -class FluxLedUpdateCoordinator(DataUpdateCoordinator): - """DataUpdateCoordinator to gather data for a specific flux_led device.""" - - def __init__( - self, hass: HomeAssistant, device: AIOWifiLedBulb, entry: ConfigEntry - ) -> None: - """Initialize DataUpdateCoordinator to gather data for specific device.""" - self.device = device - self.entry = entry - super().__init__( - hass, - _LOGGER, - name=self.device.ipaddr, - update_interval=timedelta(seconds=10), - # We don't want an immediate refresh since the device - # takes a moment to reflect the state change - request_refresh_debouncer=Debouncer( - hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=False - ), - ) - - async def _async_update_data(self) -> None: - """Fetch all device and sensor data from api.""" - try: - await self.device.async_update() - except FLUX_LED_EXCEPTIONS as ex: - raise UpdateFailed(ex) from ex diff --git a/homeassistant/components/flux_led/coordinator.py b/homeassistant/components/flux_led/coordinator.py new file mode 100644 index 00000000000..dc2f9ca0fce --- /dev/null +++ b/homeassistant/components/flux_led/coordinator.py @@ -0,0 +1,49 @@ +"""The Flux LED/MagicLight integration coordinator.""" +from __future__ import annotations + +from datetime import timedelta +import logging +from typing import Final + +from flux_led.aio import AIOWifiLedBulb + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import FLUX_LED_EXCEPTIONS + +_LOGGER = logging.getLogger(__name__) + + +REQUEST_REFRESH_DELAY: Final = 1.5 + + +class FluxLedUpdateCoordinator(DataUpdateCoordinator): + """DataUpdateCoordinator to gather data for a specific flux_led device.""" + + def __init__( + self, hass: HomeAssistant, device: AIOWifiLedBulb, entry: ConfigEntry + ) -> None: + """Initialize DataUpdateCoordinator to gather data for specific device.""" + self.device = device + self.entry = entry + super().__init__( + hass, + _LOGGER, + name=self.device.ipaddr, + update_interval=timedelta(seconds=10), + # We don't want an immediate refresh since the device + # takes a moment to reflect the state change + request_refresh_debouncer=Debouncer( + hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=False + ), + ) + + async def _async_update_data(self) -> None: + """Fetch all device and sensor data from api.""" + try: + await self.device.async_update() + except FLUX_LED_EXCEPTIONS as ex: + raise UpdateFailed(ex) from ex diff --git a/homeassistant/components/flux_led/entity.py b/homeassistant/components/flux_led/entity.py index 35ec8087dc4..1589bd0fb3a 100644 --- a/homeassistant/components/flux_led/entity.py +++ b/homeassistant/components/flux_led/entity.py @@ -14,8 +14,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import FluxLedUpdateCoordinator from .const import CONF_MINOR_VERSION, CONF_MODEL, SIGNAL_STATE_UPDATED +from .coordinator import FluxLedUpdateCoordinator def _async_device_info( diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index ba454092d42..a3811d0c4e3 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -48,7 +48,6 @@ from homeassistant.util.color import ( color_temperature_mired_to_kelvin, ) -from . import FluxLedUpdateCoordinator from .const import ( CONF_AUTOMATIC_ADD, CONF_COLORS, @@ -69,6 +68,7 @@ from .const import ( TRANSITION_JUMP, TRANSITION_STROBE, ) +from .coordinator import FluxLedUpdateCoordinator from .entity import FluxOnOffEntity from .util import _effect_brightness, _flux_color_mode_to_hass, _hass_color_modes diff --git a/homeassistant/components/flux_led/number.py b/homeassistant/components/flux_led/number.py index 28007181e5c..b19e6b0e048 100644 --- a/homeassistant/components/flux_led/number.py +++ b/homeassistant/components/flux_led/number.py @@ -11,8 +11,8 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import FluxLedUpdateCoordinator from .const import DOMAIN, EFFECT_SPEED_SUPPORT_MODES +from .coordinator import FluxLedUpdateCoordinator from .entity import FluxEntity from .util import _effect_brightness, _hass_color_modes diff --git a/homeassistant/components/flux_led/switch.py b/homeassistant/components/flux_led/switch.py index 8f499374b82..8dc5079d7ec 100644 --- a/homeassistant/components/flux_led/switch.py +++ b/homeassistant/components/flux_led/switch.py @@ -14,13 +14,13 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import FluxLedUpdateCoordinator from .const import ( CONF_REMOTE_ACCESS_ENABLED, CONF_REMOTE_ACCESS_HOST, CONF_REMOTE_ACCESS_PORT, DOMAIN, ) +from .coordinator import FluxLedUpdateCoordinator from .discovery import async_clear_discovery_cache from .entity import FluxBaseEntity, FluxOnOffEntity From 5d5b6bef55f871c0a073d3b4452ca6a7a2d54b75 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 19 Dec 2021 11:55:27 -0500 Subject: [PATCH 0757/2644] Remove deprecated yaml config from opengarage (#61961) --- .../components/opengarage/config_flow.py | 13 +--- homeassistant/components/opengarage/cover.py | 52 +-------------- .../components/opengarage/test_config_flow.py | 66 ------------------- 3 files changed, 3 insertions(+), 128 deletions(-) diff --git a/homeassistant/components/opengarage/config_flow.py b/homeassistant/components/opengarage/config_flow.py index 6ddc186cb9c..c1b0ff7105e 100644 --- a/homeassistant/components/opengarage/config_flow.py +++ b/homeassistant/components/opengarage/config_flow.py @@ -9,7 +9,7 @@ import opengarage import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SSL, CONF_VERIFY_SSL +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError @@ -58,17 +58,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_import(self, import_info): - """Set the config entry up from yaml.""" - - user_input = { - CONF_DEVICE_KEY: import_info[CONF_DEVICE_KEY], - CONF_HOST: f"{'https' if import_info.get(CONF_SSL, False) else 'http'}://{import_info[CONF_HOST]}", - CONF_PORT: import_info.get(CONF_PORT, DEFAULT_PORT), - CONF_VERIFY_SSL: import_info.get(CONF_VERIFY_SSL, False), - } - return await self.async_step_user(user_input) - async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: diff --git a/homeassistant/components/opengarage/cover.py b/homeassistant/components/opengarage/cover.py index 204ed093a9d..22a1d24c90b 100644 --- a/homeassistant/components/opengarage/cover.py +++ b/homeassistant/components/opengarage/cover.py @@ -1,70 +1,22 @@ """Platform for the opengarage.io cover component.""" import logging -import voluptuous as vol - -from homeassistant import config_entries from homeassistant.components.cover import ( - PLATFORM_SCHEMA, SUPPORT_CLOSE, SUPPORT_OPEN, CoverDeviceClass, CoverEntity, ) -from homeassistant.const import ( - CONF_COVERS, - CONF_HOST, - CONF_NAME, - CONF_PORT, - CONF_SSL, - CONF_VERIFY_SSL, - STATE_CLOSED, - STATE_CLOSING, - STATE_OPEN, - STATE_OPENING, -) +from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv -from .const import CONF_DEVICE_KEY, DEFAULT_PORT, DOMAIN +from .const import DOMAIN from .entity import OpenGarageEntity _LOGGER = logging.getLogger(__name__) STATES_MAP = {0: STATE_CLOSED, 1: STATE_OPEN} -COVER_SCHEMA = vol.Schema( - { - vol.Required(CONF_DEVICE_KEY): cv.string, - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_SSL, default=False): cv.boolean, - vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, - } -) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Required(CONF_COVERS): cv.schema_with_slug_keys(COVER_SCHEMA)} -) - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the OpenGarage covers.""" - _LOGGER.warning( - "Open Garage YAML configuration is deprecated, " - "it has been imported into the UI automatically and can be safely removed" - ) - devices = config.get(CONF_COVERS) - for device_config in devices.values(): - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=device_config, - ) - ) - async def async_setup_entry(hass, entry, async_add_entities): """Set up the OpenGarage covers.""" diff --git a/tests/components/opengarage/test_config_flow.py b/tests/components/opengarage/test_config_flow.py index d421fb6a129..5406c40b3aa 100644 --- a/tests/components/opengarage/test_config_flow.py +++ b/tests/components/opengarage/test_config_flow.py @@ -134,69 +134,3 @@ async def test_flow_entry_already_exists(hass: HomeAssistant) -> None: assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" - - -async def test_step_import(hass: HomeAssistant) -> None: - """Test when import configuring from yaml.""" - with patch( - "opengarage.OpenGarage.update_state", - return_value={"name": "Name of the device", "mac": "unique"}, - ), patch( - "homeassistant.components.opengarage.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - "host": "1.1.1.1", - "device_key": "AfsasdnfkjDD", - "port": 1234, - "verify_ssl": False, - "ssl": False, - }, - ) - await hass.async_block_till_done() - - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "Name of the device" - assert result["data"] == { - "host": "http://1.1.1.1", - "device_key": "AfsasdnfkjDD", - "port": 1234, - "verify_ssl": False, - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_step_import_ssl(hass: HomeAssistant) -> None: - """Test when import configuring from yaml.""" - with patch( - "opengarage.OpenGarage.update_state", - return_value={"name": "Name of the device", "mac": "unique"}, - ), patch( - "homeassistant.components.opengarage.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - "host": "1.1.1.1", - "device_key": "AfsasdnfkjDD", - "port": 1234, - "verify_ssl": False, - "ssl": True, - }, - ) - await hass.async_block_till_done() - - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "Name of the device" - assert result["data"] == { - "host": "https://1.1.1.1", - "device_key": "AfsasdnfkjDD", - "port": 1234, - "verify_ssl": False, - } - assert len(mock_setup_entry.mock_calls) == 1 From c3a963e12a2ec1418d27c098c1481cfdc1a21286 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 19 Dec 2021 10:57:33 -0600 Subject: [PATCH 0758/2644] Add switch to enable/disable music mode in Magic Home/flux_led (#62320) --- homeassistant/components/flux_led/switch.py | 58 +++++++++++++++++---- tests/components/flux_led/__init__.py | 1 + tests/components/flux_led/test_switch.py | 45 ++++++++++++++++ 3 files changed, 95 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/flux_led/switch.py b/homeassistant/components/flux_led/switch.py index 8dc5079d7ec..37f5f057353 100644 --- a/homeassistant/components/flux_led/switch.py +++ b/homeassistant/components/flux_led/switch.py @@ -5,6 +5,7 @@ from typing import Any from flux_led import DeviceType from flux_led.aio import AIOWifiLedBulb +from flux_led.const import MODE_MUSIC from homeassistant import config_entries from homeassistant.components.switch import SwitchEntity @@ -22,7 +23,7 @@ from .const import ( ) from .coordinator import FluxLedUpdateCoordinator from .discovery import async_clear_discovery_cache -from .entity import FluxBaseEntity, FluxOnOffEntity +from .entity import FluxBaseEntity, FluxEntity, FluxOnOffEntity async def async_setup_entry( @@ -32,20 +33,19 @@ async def async_setup_entry( ) -> None: """Set up the Flux lights.""" coordinator: FluxLedUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - entities: list[FluxSwitch | FluxRemoteAccessSwitch] = [] + entities: list[FluxSwitch | FluxRemoteAccessSwitch | FluxMusicSwitch] = [] + unique_id = entry.unique_id + name = entry.data[CONF_NAME] if coordinator.device.device_type == DeviceType.Switch: - entities.append( - FluxSwitch( - coordinator, - entry.unique_id, - entry.data[CONF_NAME], - ) - ) + entities.append(FluxSwitch(coordinator, unique_id, name)) if entry.data.get(CONF_REMOTE_ACCESS_HOST): entities.append(FluxRemoteAccessSwitch(coordinator.device, entry)) + if coordinator.device.microphone: + entities.append(FluxMusicSwitch(coordinator, unique_id, name)) + if entities: async_add_entities(entities) @@ -107,3 +107,43 @@ class FluxRemoteAccessSwitch(FluxBaseEntity, SwitchEntity): def icon(self) -> str: """Return icon based on state.""" return "mdi:cloud-outline" if self.is_on else "mdi:cloud-off-outline" + + +class FluxMusicSwitch(FluxEntity, SwitchEntity): + """Representation of a Flux music switch.""" + + def __init__( + self, + coordinator: FluxLedUpdateCoordinator, + unique_id: str | None, + name: str, + ) -> None: + """Initialize the flux music switch.""" + super().__init__(coordinator, unique_id, name) + self._attr_name = f"{name} Music" + if unique_id: + self._attr_unique_id = f"{unique_id}_music" + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the microphone on.""" + if self._device.requires_turn_on and not self._device.is_on: + await self._device.async_turn_on() + await self._device.async_set_music_mode() + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the microphone off.""" + await self._device.async_set_levels(*self._device.rgb, brightness=255) + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + @property + def is_on(self) -> bool: + """Return true if microphone is is on.""" + return self._device.is_on and self._device.effect == MODE_MUSIC + + @property + def icon(self) -> str: + """Return icon based on state.""" + return "mdi:microphone" if self.is_on else "mdi:microphone-off" diff --git a/tests/components/flux_led/__init__.py b/tests/components/flux_led/__init__.py index 5efeba1da94..d23c4281481 100644 --- a/tests/components/flux_led/__init__.py +++ b/tests/components/flux_led/__init__.py @@ -73,6 +73,7 @@ def _mocked_bulb() -> AIOWifiLedBulb: bulb.requires_turn_on = True bulb.async_setup = AsyncMock(side_effect=_save_setup_callback) bulb.effect_list = ["some_effect"] + bulb.async_set_music_mode = AsyncMock() bulb.async_set_custom_pattern = AsyncMock() bulb.async_set_preset_pattern = AsyncMock() bulb.async_set_effect = AsyncMock() diff --git a/tests/components/flux_led/test_switch.py b/tests/components/flux_led/test_switch.py index fbbdc76727c..ce2855a53ed 100644 --- a/tests/components/flux_led/test_switch.py +++ b/tests/components/flux_led/test_switch.py @@ -1,4 +1,6 @@ """Tests for switch platform.""" +from flux_led.const import MODE_MUSIC + from homeassistant.components import flux_led from homeassistant.components.flux_led.const import CONF_REMOTE_ACCESS_ENABLED, DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN @@ -95,3 +97,46 @@ async def test_remote_access_on_off(hass: HomeAssistant) -> None: assert hass.states.get(entity_id).state == STATE_ON assert config_entry.data[CONF_REMOTE_ACCESS_ENABLED] is True + + +async def test_music_mode_switch(hass: HomeAssistant) -> None: + """Test music mode switch.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + bulb.raw_state = bulb.raw_state._replace(model_num=0xA3) # has music mode + bulb.microphone = True + with _patch_discovery(), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "switch.bulb_rgbcw_ddeeff_music" + + assert hass.states.get(entity_id).state == STATE_OFF + + bulb.effect = MODE_MUSIC + bulb.is_on = False + await hass.services.async_call( + SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.async_set_music_mode.assert_called_once() + assert hass.states.get(entity_id).state == STATE_OFF + + bulb.async_set_music_mode.reset_mock() + bulb.is_on = True + await hass.services.async_call( + SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.async_set_music_mode.assert_called_once() + assert hass.states.get(entity_id).state == STATE_ON + + bulb.effect = None + await hass.services.async_call( + SWITCH_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.async_set_levels.assert_called_once() + assert hass.states.get(entity_id).state == STATE_OFF From b869b680fb72672fdb77adf3445fe1dacfe6a895 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 19 Dec 2021 12:00:49 -0500 Subject: [PATCH 0759/2644] Remove deprecated yaml config from aurora abb (#62317) --- .../aurora_abb_powerone/config_flow.py | 12 - .../components/aurora_abb_powerone/sensor.py | 36 +-- .../aurora_abb_powerone/test_config_flow.py | 252 ------------------ 3 files changed, 2 insertions(+), 298 deletions(-) diff --git a/homeassistant/components/aurora_abb_powerone/config_flow.py b/homeassistant/components/aurora_abb_powerone/config_flow.py index 3d292857266..f2e0aab5b07 100644 --- a/homeassistant/components/aurora_abb_powerone/config_flow.py +++ b/homeassistant/components/aurora_abb_powerone/config_flow.py @@ -80,18 +80,6 @@ class AuroraABBConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._com_ports_list = None self._default_com_port = None - async def async_step_import(self, config: dict[str, Any]) -> FlowResult: - """Import a configuration from config.yaml.""" - if self.hass.config_entries.async_entries(DOMAIN): - return self.async_abort(reason="already_configured") - - conf = {} - conf[CONF_PORT] = config["device"] - conf[CONF_ADDRESS] = config["address"] - # config["name"] from yaml is ignored. - - return self.async_create_entry(title=DEFAULT_INTEGRATION_TITLE, data=conf) - async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: diff --git a/homeassistant/components/aurora_abb_powerone/sensor.py b/homeassistant/components/aurora_abb_powerone/sensor.py index 6c4f783ecf9..b96674caadb 100644 --- a/homeassistant/components/aurora_abb_powerone/sensor.py +++ b/homeassistant/components/aurora_abb_powerone/sensor.py @@ -6,29 +6,18 @@ import logging from typing import Any from aurorapy.client import AuroraError, AuroraSerialClient -import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import ( - CONF_ADDRESS, - CONF_DEVICE, - CONF_NAME, - ENERGY_KILO_WATT_HOUR, - POWER_WATT, - TEMP_CELSIUS, -) -import homeassistant.helpers.config_validation as cv +from homeassistant.const import ENERGY_KILO_WATT_HOUR, POWER_WATT, TEMP_CELSIUS from homeassistant.helpers.entity import EntityCategory from .aurora_device import AuroraEntity -from .const import DEFAULT_ADDRESS, DOMAIN +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -57,27 +46,6 @@ SENSOR_TYPES = [ ), ] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_DEVICE): cv.string, - vol.Optional(CONF_ADDRESS, default=DEFAULT_ADDRESS): cv.positive_int, - vol.Optional(CONF_NAME, default="Solar PV"): cv.string, - } -) - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up based on configuration.yaml (DEPRECATED).""" - _LOGGER.warning( - "Loading aurora_abb_powerone via platform config is deprecated; The configuration" - " has been migrated to a config entry and can be safely removed from configuration.yaml" - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config - ) - ) - async def async_setup_entry(hass, config_entry, async_add_entities) -> None: """Set up aurora_abb_powerone sensor based on a config entry.""" diff --git a/tests/components/aurora_abb_powerone/test_config_flow.py b/tests/components/aurora_abb_powerone/test_config_flow.py index e9a6a100f4a..e53dcf5ab06 100644 --- a/tests/components/aurora_abb_powerone/test_config_flow.py +++ b/tests/components/aurora_abb_powerone/test_config_flow.py @@ -1,6 +1,4 @@ """Test the Aurora ABB PowerOne Solar PV config flow.""" -from datetime import timedelta -import logging from logging import INFO from unittest.mock import patch @@ -14,24 +12,11 @@ from homeassistant.components.aurora_abb_powerone.const import ( ATTR_SERIAL_NUMBER, DOMAIN, ) -from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_ADDRESS, CONF_PORT -from homeassistant.util.dt import utcnow - -from tests.common import MockConfigEntry, async_fire_time_changed TEST_DATA = {"device": "/dev/ttyUSB7", "address": 3, "name": "MyAuroraPV"} -def _simulated_returns(index, global_measure=None): - returns = { - 3: 45.678, # power - 21: 9.876, # temperature - 5: 12345, # energy - } - return returns[index] - - async def test_form(hass): """Test we get the form.""" await setup.async_setup_component(hass, "persistent_notification", {}) @@ -164,240 +149,3 @@ async def test_form_invalid_com_ports(hass): ) assert result2["errors"] == {"base": "cannot_connect"} assert len(mock_clientclose.mock_calls) == 1 - - -async def test_import_invalid_com_ports(hass, caplog): - """Test we display correct info when the comport is invalid..""" - - caplog.set_level(logging.ERROR) - with patch( - "aurorapy.client.AuroraSerialClient.connect", - side_effect=OSError(19, "...no such device..."), - return_value=None, - ): - await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TEST_DATA - ) - configs = hass.config_entries.async_entries(DOMAIN) - assert len(configs) == 1 - entry = configs[0] - assert entry.state == ConfigEntryState.SETUP_ERROR - assert "Failed to connect to inverter: " in caplog.text - - -async def test_import_com_port_wont_open(hass): - """Test we display correct info when comport won't open.""" - - with patch( - "aurorapy.client.AuroraSerialClient.connect", - side_effect=AuroraError("..could not open port..."), - ): - await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TEST_DATA - ) - configs = hass.config_entries.async_entries(DOMAIN) - assert len(configs) == 1 - entry = configs[0] - assert entry.state == ConfigEntryState.SETUP_ERROR - - -async def test_import_other_oserror(hass): - """Test we display correct info when comport won't open.""" - - with patch( - "aurorapy.client.AuroraSerialClient.connect", - side_effect=OSError(18, "...another error..."), - ): - await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TEST_DATA - ) - configs = hass.config_entries.async_entries(DOMAIN) - assert len(configs) == 1 - entry = configs[0] - assert entry.state == ConfigEntryState.SETUP_ERROR - - -# Tests below can be deleted after deprecation period is finished. -async def test_import_day(hass): - """Test .yaml import when the inverter is able to communicate.""" - - with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None,), patch( - "aurorapy.client.AuroraSerialClient.serial_number", - return_value="9876543", - ), patch( - "aurorapy.client.AuroraSerialClient.version", - return_value="9.8.7.6", - ), patch( - "aurorapy.client.AuroraSerialClient.pn", - return_value="A.B.C", - ), patch( - "aurorapy.client.AuroraSerialClient.firmware", - return_value="1.234", - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TEST_DATA - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["data"][CONF_PORT] == "/dev/ttyUSB7" - assert result["data"][CONF_ADDRESS] == 3 - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_import_night(hass): - """Test .yaml import when the inverter is inaccessible (e.g. darkness).""" - - # First time round, no response. - with patch( - "aurorapy.client.AuroraSerialClient.connect", - side_effect=AuroraError("No response after"), - ) as mock_connect: - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TEST_DATA - ) - - configs = hass.config_entries.async_entries(DOMAIN) - assert len(configs) == 1 - entry = configs[0] - assert not entry.unique_id - assert entry.state == ConfigEntryState.SETUP_RETRY - - assert len(mock_connect.mock_calls) == 1 - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["data"][CONF_PORT] == "/dev/ttyUSB7" - assert result["data"][CONF_ADDRESS] == 3 - - # Second time round, talking this time. - with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None,), patch( - "aurorapy.client.AuroraSerialClient.serial_number", - return_value="9876543", - ), patch( - "aurorapy.client.AuroraSerialClient.version", - return_value="9.8.7.6", - ), patch( - "aurorapy.client.AuroraSerialClient.pn", - return_value="A.B.C", - ), patch( - "aurorapy.client.AuroraSerialClient.firmware", - return_value="1.234", - ), patch( - "aurorapy.client.AuroraSerialClient.measure", - side_effect=_simulated_returns, - ): - # Wait >5seconds for the config to auto retry. - async_fire_time_changed(hass, utcnow() + timedelta(seconds=6)) - await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED - assert entry.unique_id - - assert len(mock_connect.mock_calls) == 1 - power = hass.states.get("sensor.power_output") - assert power - assert power.state == "45.7" - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - - -async def test_import_night_then_user(hass): - """Attempt yaml import and fail (dark), but user sets up manually before auto retry.""" - - # First time round, no response. - with patch( - "aurorapy.client.AuroraSerialClient.connect", - side_effect=AuroraError("No response after"), - ) as mock_connect: - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TEST_DATA - ) - - configs = hass.config_entries.async_entries(DOMAIN) - assert len(configs) == 1 - entry = configs[0] - assert not entry.unique_id - assert entry.state == ConfigEntryState.SETUP_RETRY - - assert len(mock_connect.mock_calls) == 1 - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["data"][CONF_PORT] == "/dev/ttyUSB7" - assert result["data"][CONF_ADDRESS] == 3 - - # Failed once, now simulate the user initiating config flow with valid settings. - fakecomports = [] - fakecomports.append(list_ports_common.ListPortInfo("/dev/ttyUSB7")) - with patch( - "serial.tools.list_ports.comports", - return_value=fakecomports, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == "form" - assert result["errors"] == {} - - with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None,), patch( - "aurorapy.client.AuroraSerialClient.serial_number", - return_value="9876543", - ), patch( - "aurorapy.client.AuroraSerialClient.version", - return_value="9.8.7.6", - ), patch( - "aurorapy.client.AuroraSerialClient.pn", - return_value="A.B.C", - ), patch( - "aurorapy.client.AuroraSerialClient.firmware", - return_value="1.234", - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_PORT: "/dev/ttyUSB7", CONF_ADDRESS: 7}, - ) - - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert len(hass.config_entries.async_entries(DOMAIN)) == 2 - - # Now retry yaml - it should fail with duplicate - with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None,), patch( - "aurorapy.client.AuroraSerialClient.serial_number", - return_value="9876543", - ), patch( - "aurorapy.client.AuroraSerialClient.version", - return_value="9.8.7.6", - ), patch( - "aurorapy.client.AuroraSerialClient.pn", - return_value="A.B.C", - ), patch( - "aurorapy.client.AuroraSerialClient.firmware", - return_value="1.234", - ): - # Wait >5seconds for the config to auto retry. - async_fire_time_changed(hass, utcnow() + timedelta(seconds=6)) - await hass.async_block_till_done() - assert entry.state == ConfigEntryState.NOT_LOADED - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - - -async def test_import_already_existing(hass): - """Test configuration.yaml import when already configured.""" - TESTDATA = {"device": "/dev/ttyUSB7", "address": 7, "name": "MyAuroraPV"} - - entry = MockConfigEntry( - domain=DOMAIN, - title="MyAuroraPV", - unique_id="0123456", - data={ - CONF_PORT: "/dev/ttyUSB7", - CONF_ADDRESS: 7, - ATTR_FIRMWARE: "1.234", - ATTR_MODEL: "9.8.7.6 (A.B.C)", - ATTR_SERIAL_NUMBER: "9876543", - "title": "PhotoVoltaic Inverters", - }, - ) - entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TESTDATA - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" From 1bd904b5b5f50420ab650aad27912833927979bc Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 19 Dec 2021 12:01:33 -0500 Subject: [PATCH 0760/2644] Use enums for rest tests (#62197) Co-authored-by: Franck Nijhof --- tests/components/rest/test_binary_sensor.py | 22 +++++----- tests/components/rest/test_sensor.py | 48 +++++++++++---------- tests/components/rest/test_switch.py | 4 +- 3 files changed, 38 insertions(+), 36 deletions(-) diff --git a/tests/components/rest/test_binary_sensor.py b/tests/components/rest/test_binary_sensor.py index 6daffcb2a5e..72fc46c56be 100644 --- a/tests/components/rest/test_binary_sensor.py +++ b/tests/components/rest/test_binary_sensor.py @@ -8,7 +8,7 @@ import httpx import respx from homeassistant import config as hass_config -import homeassistant.components.binary_sensor as binary_sensor +from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDeviceClass from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, @@ -26,7 +26,7 @@ from tests.common import get_fixture_path async def test_setup_missing_basic_config(hass): """Test setup with configuration missing required entries.""" assert await async_setup_component( - hass, binary_sensor.DOMAIN, {"binary_sensor": {"platform": "rest"}} + hass, DOMAIN, {"binary_sensor": {"platform": "rest"}} ) await hass.async_block_till_done() assert len(hass.states.async_all("binary_sensor")) == 0 @@ -36,7 +36,7 @@ async def test_setup_missing_config(hass): """Test setup with configuration missing required entries.""" assert await async_setup_component( hass, - binary_sensor.DOMAIN, + DOMAIN, { "binary_sensor": { "platform": "rest", @@ -58,7 +58,7 @@ async def test_setup_failed_connect(hass, caplog): ) assert await async_setup_component( hass, - binary_sensor.DOMAIN, + DOMAIN, { "binary_sensor": { "platform": "rest", @@ -78,7 +78,7 @@ async def test_setup_timeout(hass): respx.get("http://localhost").mock(side_effect=asyncio.TimeoutError()) assert await async_setup_component( hass, - binary_sensor.DOMAIN, + DOMAIN, { "binary_sensor": { "platform": "rest", @@ -97,7 +97,7 @@ async def test_setup_minimum(hass): respx.get("http://localhost") % HTTPStatus.OK assert await async_setup_component( hass, - binary_sensor.DOMAIN, + DOMAIN, { "binary_sensor": { "platform": "rest", @@ -116,7 +116,7 @@ async def test_setup_minimum_resource_template(hass): respx.get("http://localhost") % HTTPStatus.OK assert await async_setup_component( hass, - binary_sensor.DOMAIN, + DOMAIN, { "binary_sensor": { "platform": "rest", @@ -134,7 +134,7 @@ async def test_setup_duplicate_resource_template(hass): respx.get("http://localhost") % HTTPStatus.OK assert await async_setup_component( hass, - binary_sensor.DOMAIN, + DOMAIN, { "binary_sensor": { "platform": "rest", @@ -167,7 +167,7 @@ async def test_setup_get(hass): "username": "my username", "password": "my password", "headers": {"Accept": CONTENT_TYPE_JSON}, - "device_class": binary_sensor.DEVICE_CLASS_PLUG, + "device_class": BinarySensorDeviceClass.PLUG, } }, ) @@ -177,7 +177,7 @@ async def test_setup_get(hass): state = hass.states.get("binary_sensor.foo") assert state.state == STATE_OFF - assert state.attributes[ATTR_DEVICE_CLASS] == binary_sensor.DEVICE_CLASS_PLUG + assert state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.PLUG @respx.mock @@ -418,7 +418,7 @@ async def test_setup_query_params(hass): respx.get("http://localhost", params={"search": "something"}) % HTTPStatus.OK assert await async_setup_component( hass, - binary_sensor.DOMAIN, + DOMAIN, { "binary_sensor": { "platform": "rest", diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index fb826eefd78..86ce816f932 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -8,15 +8,18 @@ import respx from homeassistant import config as hass_config from homeassistant.components.homeassistant import SERVICE_UPDATE_ENTITY -import homeassistant.components.sensor as sensor +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + DOMAIN, + SensorDeviceClass, + SensorStateClass, +) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, CONTENT_TYPE_JSON, DATA_MEGABYTES, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_TIMESTAMP, SERVICE_RELOAD, STATE_UNKNOWN, TEMP_CELSIUS, @@ -28,9 +31,7 @@ from tests.common import get_fixture_path async def test_setup_missing_config(hass): """Test setup with configuration missing required entries.""" - assert await async_setup_component( - hass, sensor.DOMAIN, {"sensor": {"platform": "rest"}} - ) + assert await async_setup_component(hass, DOMAIN, {"sensor": {"platform": "rest"}}) await hass.async_block_till_done() assert len(hass.states.async_all("sensor")) == 0 @@ -39,7 +40,7 @@ async def test_setup_missing_schema(hass): """Test setup with resource missing schema.""" assert await async_setup_component( hass, - sensor.DOMAIN, + DOMAIN, {"sensor": {"platform": "rest", "resource": "localhost", "method": "GET"}}, ) await hass.async_block_till_done() @@ -54,7 +55,7 @@ async def test_setup_failed_connect(hass, caplog): ) assert await async_setup_component( hass, - sensor.DOMAIN, + DOMAIN, { "sensor": { "platform": "rest", @@ -74,7 +75,7 @@ async def test_setup_timeout(hass): respx.get("http://localhost").mock(side_effect=asyncio.TimeoutError()) assert await async_setup_component( hass, - sensor.DOMAIN, + DOMAIN, {"sensor": {"platform": "rest", "resource": "localhost", "method": "GET"}}, ) await hass.async_block_till_done() @@ -87,7 +88,7 @@ async def test_setup_minimum(hass): respx.get("http://localhost") % HTTPStatus.OK assert await async_setup_component( hass, - sensor.DOMAIN, + DOMAIN, { "sensor": { "platform": "rest", @@ -109,7 +110,7 @@ async def test_manual_update(hass): ) assert await async_setup_component( hass, - sensor.DOMAIN, + DOMAIN, { "sensor": { "name": "mysensor", @@ -142,7 +143,7 @@ async def test_setup_minimum_resource_template(hass): respx.get("http://localhost") % HTTPStatus.OK assert await async_setup_component( hass, - sensor.DOMAIN, + DOMAIN, { "sensor": { "platform": "rest", @@ -160,7 +161,7 @@ async def test_setup_duplicate_resource_template(hass): respx.get("http://localhost") % HTTPStatus.OK assert await async_setup_component( hass, - sensor.DOMAIN, + DOMAIN, { "sensor": { "platform": "rest", @@ -194,8 +195,8 @@ async def test_setup_get(hass): "username": "my username", "password": "my password", "headers": {"Accept": CONTENT_TYPE_JSON}, - "device_class": DEVICE_CLASS_TEMPERATURE, - "state_class": sensor.STATE_CLASS_MEASUREMENT, + "device_class": SensorDeviceClass.TEMPERATURE, + "state_class": SensorStateClass.MEASUREMENT, } }, ) @@ -215,8 +216,8 @@ async def test_setup_get(hass): state = hass.states.get("sensor.foo") assert state.state == "" assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS - assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_TEMPERATURE - assert state.attributes[sensor.ATTR_STATE_CLASS] == sensor.STATE_CLASS_MEASUREMENT + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE + assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.MEASUREMENT @respx.mock @@ -234,8 +235,8 @@ async def test_setup_timestamp(hass, caplog): "resource": "http://localhost", "method": "GET", "value_template": "{{ value_json.key }}", - "device_class": DEVICE_CLASS_TIMESTAMP, - "state_class": sensor.STATE_CLASS_MEASUREMENT, + "device_class": SensorDeviceClass.TIMESTAMP, + "state_class": SensorStateClass.MEASUREMENT, } }, ) @@ -246,7 +247,8 @@ async def test_setup_timestamp(hass, caplog): state = hass.states.get("sensor.rest_sensor") assert state.state == "2021-11-11T11:39:00+00:00" - assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_TIMESTAMP + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TIMESTAMP + assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.MEASUREMENT assert "sensor.rest_sensor rendered invalid timestamp" not in caplog.text assert "sensor.rest_sensor rendered timestamp without timezone" not in caplog.text @@ -262,7 +264,7 @@ async def test_setup_timestamp(hass, caplog): ) state = hass.states.get("sensor.rest_sensor") assert state.state == "unknown" - assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_TIMESTAMP + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TIMESTAMP assert "sensor.rest_sensor rendered invalid timestamp" in caplog.text # Bad response: No timezone @@ -277,7 +279,7 @@ async def test_setup_timestamp(hass, caplog): ) state = hass.states.get("sensor.rest_sensor") assert state.state == "unknown" - assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_TIMESTAMP + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TIMESTAMP assert "sensor.rest_sensor rendered timestamp without timezone" in caplog.text @@ -411,7 +413,7 @@ async def test_setup_query_params(hass): respx.get("http://localhost", params={"search": "something"}) % HTTPStatus.OK assert await async_setup_component( hass, - sensor.DOMAIN, + DOMAIN, { "sensor": { "platform": "rest", diff --git a/tests/components/rest/test_switch.py b/tests/components/rest/test_switch.py index 1b724052b1e..d9a5019f2cb 100644 --- a/tests/components/rest/test_switch.py +++ b/tests/components/rest/test_switch.py @@ -6,7 +6,7 @@ import aiohttp from homeassistant.components.rest import DOMAIN import homeassistant.components.rest.switch as rest -from homeassistant.components.switch import DEVICE_CLASS_SWITCH, DOMAIN as SWITCH_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SwitchDeviceClass from homeassistant.const import ( CONF_HEADERS, CONF_NAME, @@ -23,7 +23,7 @@ from tests.common import assert_setup_component """Tests for setting up the REST switch platform.""" NAME = "foo" -DEVICE_CLASS = DEVICE_CLASS_SWITCH +DEVICE_CLASS = SwitchDeviceClass.SWITCH METHOD = "post" RESOURCE = "http://localhost/" STATE_RESOURCE = RESOURCE From 1ec8619687820784ee4661173495aeaa24d58e1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 19 Dec 2021 19:02:52 +0200 Subject: [PATCH 0761/2644] Make core config source an enum (#61966) Co-authored-by: Franck Nijhof --- homeassistant/config.py | 9 +++++++-- homeassistant/core.py | 27 +++++++++++++++++++-------- tests/test_config.py | 12 ++++++------ tests/test_core.py | 4 ++-- 4 files changed, 34 insertions(+), 18 deletions(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index 540336eeca3..a2c613ebe69 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -52,7 +52,12 @@ from homeassistant.const import ( TEMP_CELSIUS, __version__, ) -from homeassistant.core import DOMAIN as CONF_CORE, SOURCE_YAML, HomeAssistant, callback +from homeassistant.core import ( + DOMAIN as CONF_CORE, + ConfigSource, + HomeAssistant, + callback, +) from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, extract_domain_configs import homeassistant.helpers.config_validation as cv @@ -542,7 +547,7 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: dict) -> Non CONF_CURRENCY, ) ): - hac.config_source = SOURCE_YAML + hac.config_source = ConfigSource.YAML for key, attr in ( (CONF_LATITUDE, "latitude"), diff --git a/homeassistant/core.py b/homeassistant/core.py index cb2a132307d..51317d17d48 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -26,6 +26,7 @@ import voluptuous as vol import yarl from homeassistant import async_timeout_backcompat, block_async_io, loader, util +from homeassistant.backports.enum import StrEnum from homeassistant.const import ( ATTR_DOMAIN, ATTR_FRIENDLY_NAME, @@ -103,10 +104,20 @@ BLOCK_LOG_TIMEOUT = 60 # How long we wait for the result of a service call SERVICE_CALL_LIMIT = 10 # seconds -# Source of core configuration -SOURCE_DISCOVERED = "discovered" -SOURCE_STORAGE = "storage" -SOURCE_YAML = "yaml" + +class ConfigSource(StrEnum): + """Source of core configuration.""" + + DEFAULT = "default" + DISCOVERED = "discovered" + STORAGE = "storage" + YAML = "yaml" + + +# SOURCE_* are deprecated as of Home Assistant 2022.2, use ConfigSource instead +SOURCE_DISCOVERED = ConfigSource.DISCOVERED.value +SOURCE_STORAGE = ConfigSource.STORAGE.value +SOURCE_YAML = ConfigSource.YAML.value # How long to wait until things that run on startup have to finish. TIMEOUT_EVENT_START = 15 @@ -1557,7 +1568,7 @@ class Config: self.external_url: str | None = None self.currency: str = "EUR" - self.config_source: str = "default" + self.config_source: ConfigSource = ConfigSource.DEFAULT # If True, pip install is skipped for requirements on startup self.skip_pip: bool = False @@ -1676,7 +1687,7 @@ class Config: def _update( self, *, - source: str, + source: ConfigSource, latitude: float | None = None, longitude: float | None = None, elevation: int | None = None, @@ -1714,7 +1725,7 @@ class Config: async def async_update(self, **kwargs: Any) -> None: """Update the configuration from a dictionary.""" - self._update(source=SOURCE_STORAGE, **kwargs) + self._update(source=ConfigSource.STORAGE, **kwargs) await self.async_store() self.hass.bus.async_fire(EVENT_CORE_CONFIG_UPDATE, kwargs) @@ -1742,7 +1753,7 @@ class Config: _LOGGER.warning("Invalid internal_url set. It's not allowed to have a path") self._update( - source=SOURCE_STORAGE, + source=ConfigSource.STORAGE, latitude=data.get("latitude"), longitude=data.get("longitude"), elevation=data.get("elevation"), diff --git a/tests/test_config.py b/tests/test_config.py index 9f2cc56b1b7..cd77b80e3ae 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -27,7 +27,7 @@ from homeassistant.const import ( CONF_UNIT_SYSTEM_METRIC, __version__, ) -from homeassistant.core import SOURCE_STORAGE, HomeAssistantError +from homeassistant.core import ConfigSource, HomeAssistantError from homeassistant.helpers import config_validation as cv import homeassistant.helpers.check_config as check_config from homeassistant.helpers.entity import Entity @@ -395,7 +395,7 @@ async def test_loading_configuration_from_storage(hass, hass_storage): assert hass.config.currency == "EUR" assert len(hass.config.allowlist_external_dirs) == 3 assert "/etc" in hass.config.allowlist_external_dirs - assert hass.config.config_source == SOURCE_STORAGE + assert hass.config.config_source is ConfigSource.STORAGE async def test_loading_configuration_from_storage_with_yaml_only(hass, hass_storage): @@ -425,7 +425,7 @@ async def test_loading_configuration_from_storage_with_yaml_only(hass, hass_stor assert len(hass.config.allowlist_external_dirs) == 3 assert "/etc" in hass.config.allowlist_external_dirs assert hass.config.media_dirs == {"mymedia": "/usr"} - assert hass.config.config_source == SOURCE_STORAGE + assert hass.config.config_source is ConfigSource.STORAGE async def test_updating_configuration(hass, hass_storage): @@ -486,7 +486,7 @@ async def test_override_stored_configuration(hass, hass_storage): assert hass.config.time_zone == "Europe/Copenhagen" assert len(hass.config.allowlist_external_dirs) == 3 assert "/etc" in hass.config.allowlist_external_dirs - assert hass.config.config_source == config_util.SOURCE_YAML + assert hass.config.config_source is ConfigSource.YAML async def test_loading_configuration(hass): @@ -521,7 +521,7 @@ async def test_loading_configuration(hass): assert "/etc" in hass.config.allowlist_external_dirs assert "/usr" in hass.config.allowlist_external_dirs assert hass.config.media_dirs == {"mymedia": "/usr"} - assert hass.config.config_source == config_util.SOURCE_YAML + assert hass.config.config_source is ConfigSource.YAML assert hass.config.legacy_templates is True assert hass.config.currency == "EUR" @@ -550,7 +550,7 @@ async def test_loading_configuration_temperature_unit(hass): assert hass.config.time_zone == "America/New_York" assert hass.config.external_url == "https://www.example.com" assert hass.config.internal_url == "http://example.local" - assert hass.config.config_source == config_util.SOURCE_YAML + assert hass.config.config_source is ConfigSource.YAML assert hass.config.currency == "EUR" diff --git a/tests/test_core.py b/tests/test_core.py index 641a5e0dfda..c2d99967a4b 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -902,7 +902,7 @@ def test_config_defaults(): assert config.time_zone == "UTC" assert config.internal_url is None assert config.external_url is None - assert config.config_source == "default" + assert config.config_source is ha.ConfigSource.DEFAULT assert config.skip_pip is False assert config.components == set() assert config.api is None @@ -948,7 +948,7 @@ def test_config_as_dict(): "allowlist_external_dirs": set(), "allowlist_external_urls": set(), "version": __version__, - "config_source": "default", + "config_source": ha.ConfigSource.DEFAULT, "safe_mode": False, "state": "RUNNING", "external_url": None, From 647febd7d84360de583af648d74bd242a0229172 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 19 Dec 2021 09:09:59 -0800 Subject: [PATCH 0762/2644] Mark camera unavailable when keepalive stream fails (#62294) * Mark camera unavailable when keepalive stream fails Add a listener in stream that notifies camera when the stream state has changed, and use that to inform the camera `available` property. Update the property to be set only from the main loop where it is read to reduce thread safety races. Issue #54659 * Fix pylint import related errors * Address lint naming errors * Apply suggestions from code review Co-authored-by: Paulus Schoutsen Co-authored-by: Paulus Schoutsen --- homeassistant/components/camera/__init__.py | 8 +++ homeassistant/components/stream/__init__.py | 21 ++++++-- tests/components/camera/test_init.py | 59 ++++++++++++++++++++- tests/components/stream/test_hls.py | 28 ++++++++-- 4 files changed, 107 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index fd05d3c2095..5d170eece4c 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -447,6 +447,13 @@ class Camera(Entity): return None return STREAM_TYPE_HLS + @property + def available(self) -> bool: + """Return True if entity is available.""" + if self.stream and not self.stream.available: + return self.stream.available + return super().available + async def create_stream(self) -> Stream | None: """Create a Stream for stream_source.""" # There is at most one stream (a decode worker) per camera @@ -456,6 +463,7 @@ class Camera(Entity): if not source: return None self.stream = create_stream(self.hass, source, options=self.stream_options) + self.stream.set_update_callback(self.async_write_ha_state) return self.stream async def stream_source(self) -> str | None: diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 070dd062e42..2d2e4058460 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -16,7 +16,7 @@ to always keep workers active. """ from __future__ import annotations -from collections.abc import Mapping +from collections.abc import Callable, Mapping import logging import re import secrets @@ -204,7 +204,8 @@ class Stream: self._thread_quit = threading.Event() self._outputs: dict[str, StreamOutput] = {} self._fast_restart_once = False - self._available = True + self._available: bool = True + self._update_callback: Callable[[], None] | None = None def endpoint_url(self, fmt: str) -> str: """Start the stream and returns a url for the output format.""" @@ -260,6 +261,17 @@ class Stream: """Return False if the stream is started and known to be unavailable.""" return self._available + def set_update_callback(self, update_callback: Callable[[], None]) -> None: + """Set callback to run when state changes.""" + self._update_callback = update_callback + + @callback + def _async_update_state(self, available: bool) -> None: + """Set state and Run callback to notify state has been updated.""" + self._available = available + if self._update_callback: + self._update_callback() + def start(self) -> None: """Start a stream.""" if self._thread is None or not self._thread.is_alive(): @@ -292,8 +304,7 @@ class Stream: wait_timeout = 0 while not self._thread_quit.wait(timeout=wait_timeout): start_time = time.time() - - self._available = True + self.hass.add_job(self._async_update_state, True) try: stream_worker( self.source, @@ -303,7 +314,6 @@ class Stream: ) except StreamWorkerError as err: _LOGGER.error("Error from stream worker: %s", str(err)) - self._available = False stream_state.discontinuity() if not self.keepalive or self._thread_quit.is_set(): @@ -314,6 +324,7 @@ class Stream: continue break + self.hass.add_job(self._async_update_state, False) # To avoid excessive restarts, wait before restarting # As the required recovery time may be different for different setups, start # with trying a short wait_timeout and increase it on each reconnection attempt. diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 3f8f62449ba..13ec69bcd4e 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -16,7 +16,11 @@ from homeassistant.components.camera.const import ( from homeassistant.components.camera.prefs import CameraEntityPreferences from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.config import async_process_ha_core_config -from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START +from homeassistant.const import ( + ATTR_ENTITY_ID, + EVENT_HOMEASSISTANT_START, + STATE_UNAVAILABLE, +) from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component @@ -633,3 +637,56 @@ async def test_websocket_web_rtc_offer_invalid_stream_type( assert response["type"] == TYPE_RESULT assert not response["success"] assert response["error"]["code"] == "web_rtc_offer_failed" + + +async def test_state_streaming(hass, hass_ws_client, mock_camera): + """Camera state.""" + demo_camera = hass.states.get("camera.demo_camera") + assert demo_camera is not None + assert demo_camera.state == camera.STATE_STREAMING + + +async def test_stream_unavailable(hass, hass_ws_client, mock_camera, mock_stream): + """Camera state.""" + await async_setup_component(hass, "camera", {}) + + with patch( + "homeassistant.components.camera.Stream.endpoint_url", + return_value="http://home.assistant/playlist.m3u8", + ), patch( + "homeassistant.components.demo.camera.DemoCamera.stream_source", + return_value="http://example.com", + ), patch( + "homeassistant.components.camera.Stream.set_update_callback", + ) as mock_update_callback: + # Request playlist through WebSocket. We just want to create the stream + # but don't care about the result. + client = await hass_ws_client(hass) + await client.send_json( + {"id": 10, "type": "camera/stream", "entity_id": "camera.demo_camera"} + ) + await client.receive_json() + assert mock_update_callback.called + + # Simluate the stream going unavailable + callback = mock_update_callback.call_args.args[0] + with patch( + "homeassistant.components.camera.Stream.available", new_callable=lambda: False + ): + callback() + await hass.async_block_till_done() + + demo_camera = hass.states.get("camera.demo_camera") + assert demo_camera is not None + assert demo_camera.state == STATE_UNAVAILABLE + + # Simulate stream becomes available + with patch( + "homeassistant.components.camera.Stream.available", new_callable=lambda: True + ): + callback() + await hass.async_block_till_done() + + demo_camera = hass.states.get("camera.demo_camera") + assert demo_camera is not None + assert demo_camera.state == camera.STATE_STREAMING diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index 0f50f830a85..67c7415f509 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -173,6 +173,14 @@ async def test_stream_timeout(hass, hass_client, stream_worker_sync, h264_video) # Setup demo HLS track stream = create_stream(hass, h264_video, {}) + available_states = [] + + def update_callback() -> None: + nonlocal available_states + available_states.append(stream.available) + + stream.set_update_callback(update_callback) + # Request stream stream.add_provider(HLS_PROVIDER) stream.start() @@ -204,6 +212,9 @@ async def test_stream_timeout(hass, hass_client, stream_worker_sync, h264_video) fail_response = await http_client.get(parsed_url.path) assert fail_response.status == HTTPStatus.NOT_FOUND + # Streams only marked as failure when keepalive is true + assert available_states == [True] + async def test_stream_timeout_after_stop( hass, hass_client, stream_worker_sync, h264_video @@ -237,13 +248,21 @@ async def test_stream_keepalive(hass): # Setup demo HLS track source = "test_stream_keepalive_source" stream = create_stream(hass, source, {}) - assert stream.available track = stream.add_provider(HLS_PROVIDER) track.num_segments = 2 + available_states = [] + + def update_callback() -> None: + nonlocal available_states + available_states.append(stream.available) + + stream.set_update_callback(update_callback) + cur_time = 0 def time_side_effect(): + print("stream.available=%s", stream.available) nonlocal cur_time if cur_time >= 80: stream.keepalive = False # Thread should exit and be joinable. @@ -263,11 +282,14 @@ async def test_stream_keepalive(hass): stream._thread.join() stream._thread = None assert av_open.call_count == 2 - assert not stream.available + await hass.async_block_till_done() # Stop stream, if it hasn't quit already stream.stop() - assert not stream.available + + # Stream marked initially available, then marked as failed, then marked available + # before the final failure that exits the stream. + assert available_states == [True, False, True] async def test_hls_playlist_view_no_output(hass, hls_stream): From 78d028a01381528af00fd7ab980a486462375d7a Mon Sep 17 00:00:00 2001 From: Thomas Schamm Date: Sun, 19 Dec 2021 18:24:37 +0100 Subject: [PATCH 0763/2644] Bumped boschshcpy 0.2.19 to 0.2.27 (#62326) --- homeassistant/components/bosch_shc/manifest.json | 8 +++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/bosch_shc/manifest.json b/homeassistant/components/bosch_shc/manifest.json index d4a8498fba3..43d92906592 100644 --- a/homeassistant/components/bosch_shc/manifest.json +++ b/homeassistant/components/bosch_shc/manifest.json @@ -3,11 +3,9 @@ "name": "Bosch SHC", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/bosch_shc", - "requirements": ["boschshcpy==0.2.19"], - "zeroconf": [ - {"type": "_http._tcp.local.", "name": "bosch shc*"} - ], + "requirements": ["boschshcpy==0.2.27"], + "zeroconf": [{ "type": "_http._tcp.local.", "name": "bosch shc*" }], "iot_class": "local_push", "codeowners": ["@tschamm"], "after_dependencies": ["zeroconf"] -} \ No newline at end of file +} diff --git a/requirements_all.txt b/requirements_all.txt index f075983c77b..4ce995f10b9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -424,7 +424,7 @@ blockchain==1.4.4 bond-api==0.1.15 # homeassistant.components.bosch_shc -boschshcpy==0.2.19 +boschshcpy==0.2.27 # homeassistant.components.amazon_polly # homeassistant.components.route53 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a1a79963303..af3182af5c6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -272,7 +272,7 @@ blinkpy==0.18.0 bond-api==0.1.15 # homeassistant.components.bosch_shc -boschshcpy==0.2.19 +boschshcpy==0.2.27 # homeassistant.components.braviatv bravia-tv==1.0.11 From dfc93f6ab8e5158b7131eb766e8e50f396148e36 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 19 Dec 2021 12:44:26 -0500 Subject: [PATCH 0764/2644] Remove deprecated yaml config from Efergy (#61520) --- .../components/efergy/config_flow.py | 13 +---- homeassistant/components/efergy/const.py | 1 - homeassistant/components/efergy/sensor.py | 53 ++----------------- tests/components/efergy/__init__.py | 1 - tests/components/efergy/test_config_flow.py | 35 +----------- 5 files changed, 6 insertions(+), 97 deletions(-) diff --git a/homeassistant/components/efergy/config_flow.py b/homeassistant/components/efergy/config_flow.py index f386b539768..8283bfce62d 100644 --- a/homeassistant/components/efergy/config_flow.py +++ b/homeassistant/components/efergy/config_flow.py @@ -11,9 +11,8 @@ from homeassistant import config_entries from homeassistant.const import CONF_API_KEY from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import ConfigType -from .const import CONF_APPTOKEN, DEFAULT_NAME, DOMAIN +from .const import DEFAULT_NAME, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -56,16 +55,6 @@ class EfergyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_import(self, import_config: ConfigType) -> FlowResult: - """Import a config entry from configuration.yaml.""" - for entry in self._async_current_entries(): - if entry.data[CONF_API_KEY] == import_config[CONF_APPTOKEN]: - _part = import_config[CONF_APPTOKEN][0:4] - _msg = f"Efergy yaml config with partial key {_part} has been imported. Please remove it" - _LOGGER.warning(_msg) - return self.async_abort(reason="already_configured") - return await self.async_step_user({CONF_API_KEY: import_config[CONF_APPTOKEN]}) - async def async_step_reauth(self, config: dict[str, Any]) -> FlowResult: """Handle a reauthorization flow request.""" return await self.async_step_user() diff --git a/homeassistant/components/efergy/const.py b/homeassistant/components/efergy/const.py index b141c3ebdb8..f5e9af6a4c8 100644 --- a/homeassistant/components/efergy/const.py +++ b/homeassistant/components/efergy/const.py @@ -3,7 +3,6 @@ from datetime import timedelta ATTRIBUTION = "Data provided by Efergy" -CONF_APPTOKEN = "app_token" CONF_CURRENT_VALUES = "current_values" DATA_KEY_API = "api" diff --git a/homeassistant/components/efergy/sensor.py b/homeassistant/components/efergy/sensor.py index b448c6db6ec..21d4002bcdb 100644 --- a/homeassistant/components/efergy/sensor.py +++ b/homeassistant/components/efergy/sensor.py @@ -5,31 +5,20 @@ import logging from re import sub from pyefergy import Efergy, exceptions -import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - CONF_CURRENCY, - CONF_MONITORED_VARIABLES, - CONF_TYPE, - ENERGY_KILO_WATT_HOUR, - POWER_WATT, -) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENERGY_KILO_WATT_HOUR, POWER_WATT from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_platform -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import EfergyEntity -from .const import CONF_APPTOKEN, CONF_CURRENT_VALUES, DATA_KEY_API, DOMAIN +from .const import CONF_CURRENT_VALUES, DATA_KEY_API, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -113,42 +102,6 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), ) -TYPES_SCHEMA = vol.In( - ["current_values", "instant_readings", "amount", "budget", "cost"] -) - - -SENSORS_SCHEMA = vol.Schema( - { - vol.Required(CONF_TYPE): TYPES_SCHEMA, - vol.Optional(CONF_CURRENCY, default=""): cv.string, - vol.Optional("period", default="year"): cv.string, - } -) - -# Deprecated in Home Assistant 2021.11 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_APPTOKEN): cv.string, - vol.Optional("utc_offset", default="0"): cv.string, - vol.Required(CONF_MONITORED_VARIABLES): [SENSORS_SCHEMA], - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the Efergy sensor from yaml.""" - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config - ) - ) - async def async_setup_entry( hass: HomeAssistant, diff --git a/tests/components/efergy/__init__.py b/tests/components/efergy/__init__.py index c4f099df822..4c26e25e5f4 100644 --- a/tests/components/efergy/__init__.py +++ b/tests/components/efergy/__init__.py @@ -17,7 +17,6 @@ MULTI_SENSOR_TOKEN = "9r6QGF7dpZfO3fqPTBl1fyRmjV1cGoLT" CONF_DATA = {CONF_API_KEY: TOKEN} HID = "12345678901234567890123456789012" -IMPORT_DATA = {"platform": "efergy", "app_token": TOKEN} BASE_URL = "https://engage.efergy.com/mobile_proxy/" diff --git a/tests/components/efergy/test_config_flow.py b/tests/components/efergy/test_config_flow.py index d49b89984e1..89f3b266b7c 100644 --- a/tests/components/efergy/test_config_flow.py +++ b/tests/components/efergy/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch from pyefergy import exceptions from homeassistant.components.efergy.const import DEFAULT_NAME, DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_SOURCE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import ( @@ -13,14 +13,7 @@ from homeassistant.data_entry_flow import ( RESULT_TYPE_FORM, ) -from . import ( - CONF_DATA, - HID, - IMPORT_DATA, - _patch_efergy, - _patch_efergy_status, - create_entry, -) +from . import CONF_DATA, HID, _patch_efergy, _patch_efergy_status, create_entry def _patch_setup(): @@ -83,30 +76,6 @@ async def test_flow_user_unknown(hass: HomeAssistant): assert result["errors"]["base"] == "unknown" -async def test_flow_import(hass: HomeAssistant): - """Test import step.""" - with _patch_efergy(), _patch_setup(): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_IMPORT}, data=IMPORT_DATA - ) - - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["title"] == DEFAULT_NAME - assert result["data"] == CONF_DATA - assert result["result"].unique_id == HID - - -async def test_flow_import_already_configured(hass: HomeAssistant): - """Test import step already configured.""" - create_entry(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_IMPORT}, data=IMPORT_DATA - ) - - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - async def test_flow_reauth(hass: HomeAssistant): """Test reauth step.""" entry = create_entry(hass) From 2bfcc5777dcd3b1d1fcacb975b5083e73a029a80 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 19 Dec 2021 12:12:51 -0700 Subject: [PATCH 0765/2644] Use migration helper in Ridwell (#62327) --- homeassistant/components/ridwell/__init__.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/ridwell/__init__.py b/homeassistant/components/ridwell/__init__.py index e3e1da4f21b..5f3933a09c5 100644 --- a/homeassistant/components/ridwell/__init__.py +++ b/homeassistant/components/ridwell/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio from datetime import timedelta +from typing import Any from aioridwell import async_get_client from aioridwell.errors import InvalidCredentialsError, RidwellError @@ -10,7 +11,7 @@ from aioridwell.model import RidwellAccount, RidwellPickupEvent from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, entity_registry as er from homeassistant.helpers.entity import EntityDescription @@ -106,12 +107,14 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if version == 1: version = entry.version = 2 - ent_reg = er.async_get(hass) - [entity_entry] = [ - e for e in ent_reg.entities.values() if e.config_entry_id == entry.entry_id - ] - new_unique_id = f"{entity_entry.unique_id}_{SENSOR_TYPE_NEXT_PICKUP}" - ent_reg.async_update_entity(entity_entry.entity_id, new_unique_id=new_unique_id) + @callback + def migrate_unique_id(entity_entry: er.RegistryEntry) -> dict[str, Any]: + """Migrate the unique ID to a new format.""" + return { + "new_unique_id": f"{entity_entry.unique_id}_{SENSOR_TYPE_NEXT_PICKUP}" + } + + await er.async_migrate_entries(hass, entry.entry_id, migrate_unique_id) LOGGER.info("Migration to version %s successful", version) From f9e38cd08b506d39ec31e0affd0e2fbdbf092a1b Mon Sep 17 00:00:00 2001 From: Thijs Walcarius Date: Sun, 19 Dec 2021 20:14:56 +0100 Subject: [PATCH 0766/2644] Fix missing brightness for Velbus entities (#62314) * Fix #62169: missing brightness for Velbus-entities * Use default implementation of supported_features Co-authored-by: Thijs Walcarius --- homeassistant/components/velbus/light.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/velbus/light.py b/homeassistant/components/velbus/light.py index bd903c76790..f1a90651716 100644 --- a/homeassistant/components/velbus/light.py +++ b/homeassistant/components/velbus/light.py @@ -49,7 +49,7 @@ class VelbusLight(VelbusEntity, LightEntity): """Representation of a Velbus light.""" _channel: VelbusDimmer - _attr_supported_feature = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION + _attr_supported_features = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION @property def is_on(self) -> bool: @@ -96,7 +96,7 @@ class VelbusButtonLight(VelbusEntity, LightEntity): _channel: VelbusButton _attr_entity_registry_enabled_default = False - _attr_supported_feature = SUPPORT_FLASH + _attr_supported_features = SUPPORT_FLASH def __init__(self, channel: VelbusChannel) -> None: """Initialize the button light (led).""" From b63c766ec0246865aabac275b9aaaf030a00bd47 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 19 Dec 2021 13:17:19 -0600 Subject: [PATCH 0767/2644] Add homekit discovery to ecobee (#62334) --- homeassistant/components/ecobee/manifest.json | 3 +++ homeassistant/generated/zeroconf.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/homeassistant/components/ecobee/manifest.json b/homeassistant/components/ecobee/manifest.json index bf6d0b922dd..49f56956eea 100644 --- a/homeassistant/components/ecobee/manifest.json +++ b/homeassistant/components/ecobee/manifest.json @@ -9,5 +9,8 @@ "codeowners": [ "@marthoc" ], + "homekit": { + "models": ["EB-*", "ecobee*"] + }, "iot_class": "cloud_polling" } \ No newline at end of file diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 8825b1639e9..e7af0cd960d 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -344,6 +344,7 @@ HOMEKIT = { "BSB002": "hue", "C105X": "roku", "C135X": "roku", + "EB-*": "ecobee", "Healty Home Coach": "netatmo", "Iota": "abode", "LIFX": "lifx", @@ -361,6 +362,7 @@ HOMEKIT = { "Welcome": "netatmo", "Wemo": "wemo", "YL*": "yeelight", + "ecobee*": "ecobee", "iSmartGate": "gogogate2", "iZone": "izone", "tado": "tado" From 1b17c295d65b88ff995005f6207a9e0287b80ee4 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 19 Dec 2021 12:17:31 -0700 Subject: [PATCH 0768/2644] Use migration helper in RainMachine (#62328) --- homeassistant/components/rainmachine/__init__.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 0e0dd726f75..5ffdc7951db 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -344,10 +344,9 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if version == 1: version = entry.version = 2 - ent_reg = er.async_get(hass) - for entity_entry in [ - e for e in ent_reg.entities.values() if e.config_entry_id == entry.entry_id - ]: + @callback + def migrate_unique_id(entity_entry: er.RegistryEntry) -> dict[str, Any]: + """Migrate the unique ID to a new format.""" unique_id_pieces = entity_entry.unique_id.split("_") old_mac = unique_id_pieces[0] new_mac = ":".join(old_mac[i : i + 2] for i in range(0, len(old_mac), 2)) @@ -356,9 +355,9 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if entity_entry.entity_id.startswith("switch"): unique_id_pieces[1] = unique_id_pieces[1][11:].lower() - ent_reg.async_update_entity( - entity_entry.entity_id, new_unique_id="_".join(unique_id_pieces) - ) + return {"new_unique_id": "_".join(unique_id_pieces)} + + await er.async_migrate_entries(hass, entry.entry_id, migrate_unique_id) LOGGER.info("Migration to version %s successful", version) From 9017d35e73e9a94b553392b538290b8e2345fbfb Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Sun, 19 Dec 2021 20:22:41 +0100 Subject: [PATCH 0769/2644] Fix velbus climate current temp (#62329) --- homeassistant/components/velbus/climate.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/velbus/climate.py b/homeassistant/components/velbus/climate.py index c11698b1358..5dce4033074 100644 --- a/homeassistant/components/velbus/climate.py +++ b/homeassistant/components/velbus/climate.py @@ -61,6 +61,11 @@ class VelbusClimate(VelbusEntity, ClimateEntity): None, ) + @property + def current_temperature(self) -> int | None: + """Return the current temperature.""" + return self._channel.get_state() + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperatures.""" if (temp := kwargs.get(ATTR_TEMPERATURE)) is None: From 368e16f189c853fb11082b40b1585d8c1113f919 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 19 Dec 2021 11:45:04 -0800 Subject: [PATCH 0770/2644] Simplify nest test patch using new keyword (#62336) --- tests/components/nest/test_media_source.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/tests/components/nest/test_media_source.py b/tests/components/nest/test_media_source.py index 84147fe9d94..e2c810cc873 100644 --- a/tests/components/nest/test_media_source.py +++ b/tests/components/nest/test_media_source.py @@ -8,7 +8,7 @@ import datetime from http import HTTPStatus import shutil from typing import Generator -from unittest.mock import Mock, patch +from unittest.mock import patch import uuid import aiohttp @@ -78,9 +78,7 @@ IMAGE_AUTHORIZATION_HEADERS = {"Authorization": "Basic g.0.eventToken"} def cleanup_media_storage(hass): """Test cleanup, remove any media storage persisted during the test.""" tmp_path = str(uuid.uuid4()) - m = Mock(spec=float) - m.return_value = tmp_path - with patch("homeassistant.components.nest.media_source.MEDIA_PATH", new_callable=m): + with patch("homeassistant.components.nest.media_source.MEDIA_PATH", new=tmp_path): yield shutil.rmtree(hass.config.path(tmp_path), ignore_errors=True) @@ -725,11 +723,9 @@ async def test_multiple_devices(hass, auth, hass_client): @pytest.fixture def event_store() -> Generator[None, None, None]: """Persist changes to event store immediately.""" - m = Mock(spec=float) - m.return_value = 0 with patch( "homeassistant.components.nest.media_source.STORAGE_SAVE_DELAY_SECONDS", - new_callable=m, + new=0, ): yield @@ -947,9 +943,7 @@ async def test_camera_event_media_eviction(hass, auth, hass_client): """Test media files getting evicted from the cache.""" # Set small cache size for testing eviction - m = Mock(spec=float) - m.return_value = 5 - with patch("homeassistant.components.nest.EVENT_MEDIA_CACHE_SIZE", new_callable=m): + with patch("homeassistant.components.nest.EVENT_MEDIA_CACHE_SIZE", new=5): subscriber = await async_setup_devices( hass, auth, From cb26862d7a42b849a87e065f3a2a1c9fd4533ea6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 19 Dec 2021 14:13:23 -0600 Subject: [PATCH 0771/2644] Add zeroconf discovery to ecobee for non-homekit models (#62335) --- homeassistant/components/ecobee/manifest.json | 4 ++++ homeassistant/generated/zeroconf.py | 14 ++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/homeassistant/components/ecobee/manifest.json b/homeassistant/components/ecobee/manifest.json index 49f56956eea..a22ec48da90 100644 --- a/homeassistant/components/ecobee/manifest.json +++ b/homeassistant/components/ecobee/manifest.json @@ -12,5 +12,9 @@ "homekit": { "models": ["EB-*", "ecobee*"] }, + "zeroconf": [ + {"type":"_sideplay._tcp.local.", "properties": {"mdl":"eb-*"}}, + {"type":"_sideplay._tcp.local.", "properties": {"mdl":"ecobee*"}} + ], "iot_class": "cloud_polling" } \ No newline at end of file diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index e7af0cd960d..d5b8839bd77 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -283,6 +283,20 @@ ZEROCONF = { } } ], + "_sideplay._tcp.local.": [ + { + "domain": "ecobee", + "properties": { + "mdl": "eb-*" + } + }, + { + "domain": "ecobee", + "properties": { + "mdl": "ecobee*" + } + } + ], "_sonos._tcp.local.": [ { "domain": "sonos" From 8eb33ede432a4defc832db9ec7f95e279b860ee9 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 19 Dec 2021 13:52:21 -0700 Subject: [PATCH 0772/2644] Fix bug in which SimpliSafe websocket won't reconnect on error (#62241) --- .../components/simplisafe/__init__.py | 48 +++++++++++++++---- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index e80716de9f8..537e199fc04 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -12,6 +12,7 @@ from simplipy.errors import ( EndpointUnavailableError, InvalidCredentialsError, SimplipyError, + WebsocketError, ) from simplipy.system import SystemNotification from simplipy.system.v3 import ( @@ -473,6 +474,7 @@ class SimpliSafe: self._api = api self._hass = hass self._system_notifications: dict[int, set[SystemNotification]] = {} + self._websocket_reconnect_task: asyncio.Task | None = None self.entry = entry self.initial_event_to_use: dict[int, dict[str, Any]] = {} self.systems: dict[int, SystemType] = {} @@ -517,11 +519,29 @@ class SimpliSafe: self._system_notifications[system.system_id] = latest_notifications - async def _async_websocket_on_connect(self) -> None: + async def _async_start_websocket_loop(self) -> None: """Define a callback for connecting to the websocket.""" if TYPE_CHECKING: assert self._api.websocket - await self._api.websocket.async_listen() + + should_reconnect = True + + try: + await self._api.websocket.async_reconnect() + await self._api.websocket.async_listen() + except asyncio.CancelledError: + LOGGER.debug("Request to cancel websocket loop received") + raise + except WebsocketError as err: + LOGGER.error("Failed to connect to websocket: %s", err) + except Exception as err: # pylint: disable=broad-except + LOGGER.error("Unknown exception while connecting to websocket: %s", err) + + if should_reconnect: + LOGGER.info("Disconnected from websocket; reconnecting") + self._websocket_reconnect_task = self._hass.async_create_task( + self._async_start_websocket_loop() + ) @callback def _async_websocket_on_event(self, event: WebsocketEvent) -> None: @@ -561,17 +581,25 @@ class SimpliSafe: assert self._api.refresh_token assert self._api.websocket - self._api.websocket.add_connect_callback(self._async_websocket_on_connect) self._api.websocket.add_event_callback(self._async_websocket_on_event) - asyncio.create_task(self._api.websocket.async_connect()) + self._websocket_reconnect_task = asyncio.create_task( + self._async_start_websocket_loop() + ) async def async_websocket_disconnect_listener(_: Event) -> None: """Define an event handler to disconnect from the websocket.""" if TYPE_CHECKING: assert self._api.websocket - if self._api.websocket.connected: - await self._api.websocket.async_disconnect() + if self._websocket_reconnect_task: + self._websocket_reconnect_task.cancel() + try: + await self._websocket_reconnect_task + except asyncio.CancelledError: + LOGGER.debug("Websocket reconnection task successfully canceled") + self._websocket_reconnect_task = None + + await self._api.websocket.async_disconnect() self.entry.async_on_unload( self._hass.bus.async_listen_once( @@ -621,10 +649,10 @@ class SimpliSafe: if TYPE_CHECKING: assert self._api.websocket - if self._api.websocket.connected: - # If a websocket connection is open, reconnect it to use the - # new access token: - asyncio.create_task(self._api.websocket.async_reconnect()) + # Open a new websocket connection with the fresh token: + self._websocket_reconnect_task = self._hass.async_create_task( + self._async_start_websocket_loop() + ) self.entry.async_on_unload( self._api.add_refresh_token_callback(async_handle_refresh_token) From 443003795bd4b3342c6a62162a104f554f15ee59 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 19 Dec 2021 14:14:08 -0700 Subject: [PATCH 0773/2644] Replace Guardian logged errors with HomeAssistantError in service handlers (#62342) --- homeassistant/components/guardian/__init__.py | 5 ++++- homeassistant/components/guardian/switch.py | 9 ++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index f91c667bfbd..55af1619da5 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -21,6 +21,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -203,7 +204,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async with client: await func(call, client) except GuardianError as err: - LOGGER.error("Error while executing %s: %s", func.__name__, err) + raise HomeAssistantError( + f"Error while executing {func.__name__}: {err}" + ) from err return wrapper diff --git a/homeassistant/components/guardian/switch.py b/homeassistant/components/guardian/switch.py index 17e28c1901a..9a4f70fd3d2 100644 --- a/homeassistant/components/guardian/switch.py +++ b/homeassistant/components/guardian/switch.py @@ -9,11 +9,12 @@ from aioguardian.errors import GuardianError from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import ValveControllerEntity -from .const import API_VALVE_STATUS, DATA_CLIENT, DATA_COORDINATOR, DOMAIN, LOGGER +from .const import API_VALVE_STATUS, DATA_CLIENT, DATA_COORDINATOR, DOMAIN ATTR_AVG_CURRENT = "average_current" ATTR_INST_CURRENT = "instantaneous_current" @@ -97,8 +98,7 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): async with self._client: await self._client.valve.close() except GuardianError as err: - LOGGER.error("Error while closing the valve: %s", err) - return + raise HomeAssistantError(f"Error while closing the valve: {err}") from err self._attr_is_on = False self.async_write_ha_state() @@ -109,8 +109,7 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): async with self._client: await self._client.valve.open() except GuardianError as err: - LOGGER.error("Error while opening the valve: %s", err) - return + raise HomeAssistantError(f"Error while opening the valve: {err}") from err self._attr_is_on = True self.async_write_ha_state() From 1baba2a807d454def81c5c727e78cd75b73a3acc Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 19 Dec 2021 16:24:04 -0500 Subject: [PATCH 0774/2644] Remove deprecated yaml config from flux_led (#61844) Co-authored-by: J. Nick Koston --- homeassistant/components/flux_led/button.py | 2 +- .../components/flux_led/config_flow.py | 30 +--- homeassistant/components/flux_led/const.py | 11 -- homeassistant/components/flux_led/light.py | 83 +-------- tests/components/flux_led/test_config_flow.py | 60 +------ tests/components/flux_led/test_init.py | 13 +- tests/components/flux_led/test_light.py | 157 +----------------- 7 files changed, 17 insertions(+), 339 deletions(-) diff --git a/homeassistant/components/flux_led/button.py b/homeassistant/components/flux_led/button.py index cc1f4f891bd..c0600e0db6a 100644 --- a/homeassistant/components/flux_led/button.py +++ b/homeassistant/components/flux_led/button.py @@ -10,8 +10,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import FluxLedUpdateCoordinator from .const import DOMAIN +from .coordinator import FluxLedUpdateCoordinator from .entity import FluxBaseEntity diff --git a/homeassistant/components/flux_led/config_flow.py b/homeassistant/components/flux_led/config_flow.py index 4f6e7844510..942ba840212 100644 --- a/homeassistant/components/flux_led/config_flow.py +++ b/homeassistant/components/flux_led/config_flow.py @@ -1,7 +1,6 @@ """Config flow for Flux LED/MagicLight.""" from __future__ import annotations -import logging from typing import Any, Final, cast from flux_led.const import ATTR_ID, ATTR_IPADDR, ATTR_MODEL, ATTR_MODEL_DESCRIPTION @@ -10,7 +9,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import dhcp -from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PROTOCOL +from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import device_registry as dr @@ -40,9 +39,6 @@ from .discovery import ( CONF_DEVICE: Final = "device" -_LOGGER = logging.getLogger(__name__) - - class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Magic Home Integration.""" @@ -59,30 +55,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow for the Flux LED component.""" return OptionsFlow(config_entry) - async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: - """Handle configuration via YAML import.""" - _LOGGER.debug("Importing configuration from YAML for flux_led") - host = user_input[CONF_HOST] - self._async_abort_entries_match({CONF_HOST: host}) - if mac := user_input[CONF_MAC]: - await self.async_set_unique_id(dr.format_mac(mac), raise_on_progress=False) - self._abort_if_unique_id_configured(updates={CONF_HOST: host}) - return self.async_create_entry( - title=user_input[CONF_NAME], - data={ - CONF_HOST: host, - CONF_NAME: user_input[CONF_NAME], - CONF_PROTOCOL: user_input.get(CONF_PROTOCOL), - }, - options={ - CONF_CUSTOM_EFFECT_COLORS: user_input[CONF_CUSTOM_EFFECT_COLORS], - CONF_CUSTOM_EFFECT_SPEED_PCT: user_input[CONF_CUSTOM_EFFECT_SPEED_PCT], - CONF_CUSTOM_EFFECT_TRANSITION: user_input[ - CONF_CUSTOM_EFFECT_TRANSITION - ], - }, - ) - async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle discovery via dhcp.""" self._discovered_device = FluxLEDDiscovery( diff --git a/homeassistant/components/flux_led/const.py b/homeassistant/components/flux_led/const.py index 646dddb83a2..be100e4141c 100644 --- a/homeassistant/components/flux_led/const.py +++ b/homeassistant/components/flux_led/const.py @@ -33,7 +33,6 @@ API: Final = "flux_api" SIGNAL_STATE_UPDATED = "flux_led_{}_state_updated" -CONF_AUTOMATIC_ADD: Final = "automatic_add" DEFAULT_NETWORK_SCAN_INTERVAL: Final = 120 DEFAULT_SCAN_INTERVAL: Final = 5 DEFAULT_EFFECT_SPEED: Final = 50 @@ -50,22 +49,12 @@ FLUX_LED_EXCEPTIONS: Final = ( STARTUP_SCAN_TIMEOUT: Final = 5 DISCOVER_SCAN_TIMEOUT: Final = 10 -CONF_DEVICES: Final = "devices" -CONF_CUSTOM_EFFECT: Final = "custom_effect" CONF_MODEL: Final = "model" CONF_MINOR_VERSION: Final = "minor_version" CONF_REMOTE_ACCESS_ENABLED: Final = "remote_access_enabled" CONF_REMOTE_ACCESS_HOST: Final = "remote_access_host" CONF_REMOTE_ACCESS_PORT: Final = "remote_access_port" -MODE_AUTO: Final = "auto" -MODE_RGB: Final = "rgb" -MODE_RGBW: Final = "rgbw" - -# This mode enables white value to be controlled by brightness. -# RGB value is ignored when this mode is specified. -MODE_WHITE: Final = "w" - TRANSITION_GRADUAL: Final = "gradual" TRANSITION_JUMP: Final = "jump" TRANSITION_STROBE: Final = "strobe" diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index a3811d0c4e3..58d98bf2462 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -5,7 +5,6 @@ import ast import logging from typing import Any, Final -from flux_led.const import ATTR_ID, ATTR_IPADDR from flux_led.utils import ( color_temp_to_white_levels, rgbcw_brightness, @@ -24,24 +23,15 @@ from homeassistant.components.light import ( ATTR_RGBWW_COLOR, ATTR_WHITE, COLOR_MODE_RGBWW, - PLATFORM_SCHEMA, SUPPORT_EFFECT, SUPPORT_TRANSITION, LightEntity, ) -from homeassistant.const import ( - ATTR_MODE, - CONF_DEVICES, - CONF_HOST, - CONF_MAC, - CONF_NAME, - CONF_PROTOCOL, -) +from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.color import ( color_temperature_kelvin_to_mired, @@ -49,9 +39,7 @@ from homeassistant.util.color import ( ) from .const import ( - CONF_AUTOMATIC_ADD, CONF_COLORS, - CONF_CUSTOM_EFFECT, CONF_CUSTOM_EFFECT_COLORS, CONF_CUSTOM_EFFECT_SPEED_PCT, CONF_CUSTOM_EFFECT_TRANSITION, @@ -59,11 +47,6 @@ from .const import ( CONF_TRANSITION, DEFAULT_EFFECT_SPEED, DOMAIN, - FLUX_LED_DISCOVERY, - MODE_AUTO, - MODE_RGB, - MODE_RGBW, - MODE_WHITE, TRANSITION_GRADUAL, TRANSITION_JUMP, TRANSITION_STROBE, @@ -105,70 +88,6 @@ CUSTOM_EFFECT_DICT: Final = { ), } -CUSTOM_EFFECT_SCHEMA: Final = vol.Schema(CUSTOM_EFFECT_DICT) - -DEVICE_SCHEMA: Final = vol.Schema( - { - vol.Optional(CONF_NAME): cv.string, - vol.Optional(ATTR_MODE, default=MODE_AUTO): vol.All( - cv.string, vol.In([MODE_AUTO, MODE_RGBW, MODE_RGB, MODE_WHITE]) - ), - vol.Optional(CONF_PROTOCOL): vol.All(cv.string, vol.In(["ledenet"])), - vol.Optional(CONF_CUSTOM_EFFECT): CUSTOM_EFFECT_SCHEMA, - } -) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}, - vol.Optional(CONF_AUTOMATIC_ADD, default=False): cv.boolean, - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> bool: - """Set up the flux led platform.""" - domain_data = hass.data[DOMAIN] - discovered_mac_by_host = { - device[ATTR_IPADDR]: device[ATTR_ID] - for device in domain_data[FLUX_LED_DISCOVERY] - } - for host, device_config in config.get(CONF_DEVICES, {}).items(): - _LOGGER.warning( - "Configuring flux_led via yaml is deprecated; the configuration for" - " %s has been migrated to a config entry and can be safely removed", - host, - ) - custom_effects = device_config.get(CONF_CUSTOM_EFFECT, {}) - custom_effect_colors = None - if CONF_COLORS in custom_effects: - custom_effect_colors = str(custom_effects[CONF_COLORS]) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_HOST: host, - CONF_MAC: discovered_mac_by_host.get(host), - CONF_NAME: device_config[CONF_NAME], - CONF_PROTOCOL: device_config.get(CONF_PROTOCOL), - CONF_CUSTOM_EFFECT_COLORS: custom_effect_colors, - CONF_CUSTOM_EFFECT_SPEED_PCT: custom_effects.get( - CONF_SPEED_PCT, DEFAULT_EFFECT_SPEED - ), - CONF_CUSTOM_EFFECT_TRANSITION: custom_effects.get( - CONF_TRANSITION, TRANSITION_GRADUAL - ), - }, - ) - ) - return True - async def async_setup_entry( hass: HomeAssistant, diff --git a/tests/components/flux_led/test_config_flow.py b/tests/components/flux_led/test_config_flow.py index 3f2fc54b8d9..32e1707f7eb 100644 --- a/tests/components/flux_led/test_config_flow.py +++ b/tests/components/flux_led/test_config_flow.py @@ -17,17 +17,10 @@ from homeassistant.components.flux_led.const import ( CONF_REMOTE_ACCESS_HOST, CONF_REMOTE_ACCESS_PORT, DOMAIN, - MODE_RGB, TRANSITION_JUMP, TRANSITION_STROBE, ) -from homeassistant.const import ( - CONF_DEVICE, - CONF_HOST, - CONF_MAC, - CONF_NAME, - CONF_PROTOCOL, -) +from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM @@ -217,56 +210,6 @@ async def test_discovery_no_device(hass: HomeAssistant): assert result2["reason"] == "no_devices_found" -async def test_import(hass: HomeAssistant): - """Test import from yaml.""" - config = { - CONF_HOST: IP_ADDRESS, - CONF_MAC: MAC_ADDRESS, - CONF_NAME: "floor lamp", - CONF_PROTOCOL: "ledenet", - CONF_MODEL: MODE_RGB, - CONF_CUSTOM_EFFECT_COLORS: "[255,0,0], [0,0,255]", - CONF_CUSTOM_EFFECT_SPEED_PCT: 30, - CONF_CUSTOM_EFFECT_TRANSITION: TRANSITION_STROBE, - } - - # Success - with _patch_discovery(), _patch_wifibulb(), patch( - f"{MODULE}.async_setup", return_value=True - ) as mock_setup, patch( - f"{MODULE}.async_setup_entry", return_value=True - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config - ) - await hass.async_block_till_done() - - assert result["type"] == "create_entry" - assert result["title"] == "floor lamp" - assert result["data"] == { - CONF_HOST: IP_ADDRESS, - CONF_NAME: "floor lamp", - CONF_PROTOCOL: "ledenet", - } - assert result["options"] == { - CONF_CUSTOM_EFFECT_COLORS: "[255,0,0], [0,0,255]", - CONF_CUSTOM_EFFECT_SPEED_PCT: 30, - CONF_CUSTOM_EFFECT_TRANSITION: TRANSITION_STROBE, - } - mock_setup.assert_called_once() - mock_setup_entry.assert_called_once() - - # Duplicate - with _patch_discovery(), _patch_wifibulb(): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config - ) - await hass.async_block_till_done() - - assert result["type"] == "abort" - assert result["reason"] == "already_configured" - - async def test_manual_working_discovery(hass: HomeAssistant): """Test manually setup.""" result = await hass.config_entries.flow.async_init( @@ -584,7 +527,6 @@ async def test_options(hass: HomeAssistant): domain=DOMAIN, data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, options={ - CONF_MODEL: MODE_RGB, CONF_CUSTOM_EFFECT_COLORS: "[255,0,0], [0,0,255]", CONF_CUSTOM_EFFECT_SPEED_PCT: 30, CONF_CUSTOM_EFFECT_TRANSITION: TRANSITION_STROBE, diff --git a/tests/components/flux_led/test_init.py b/tests/components/flux_led/test_init.py index d3f5c38f1bc..1b4864de788 100644 --- a/tests/components/flux_led/test_init.py +++ b/tests/components/flux_led/test_init.py @@ -101,7 +101,7 @@ async def test_config_entry_retry(hass: HomeAssistant) -> None: "discovery,title", [ (FLUX_DISCOVERY, DEFAULT_ENTRY_TITLE), - (FLUX_DISCOVERY_PARTIAL, "AZ120444 ddeeff"), + (FLUX_DISCOVERY_PARTIAL, DEFAULT_ENTRY_TITLE), ], ) async def test_config_entry_fills_unique_id_with_directed_discovery( @@ -112,17 +112,24 @@ async def test_config_entry_fills_unique_id_with_directed_discovery( domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=None ) config_entry.add_to_hass(hass) + last_address = None async def _discovery(self, *args, address=None, **kwargs): # Only return discovery results when doing directed discovery - return [discovery] if address == IP_ADDRESS else [] + nonlocal last_address + last_address = address + return [FLUX_DISCOVERY] if address == IP_ADDRESS else [] + + def _mock_getBulbInfo(*args, **kwargs): + nonlocal last_address + return [FLUX_DISCOVERY] if last_address == IP_ADDRESS else [] with patch( "homeassistant.components.flux_led.discovery.AIOBulbScanner.async_scan", new=_discovery, ), patch( "homeassistant.components.flux_led.discovery.AIOBulbScanner.getBulbInfo", - return_value=[discovery], + new=_mock_getBulbInfo, ), _patch_wifibulb(): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() diff --git a/tests/components/flux_led/test_light.py b/tests/components/flux_led/test_light.py index 25ceffe3022..554a4eeedf8 100644 --- a/tests/components/flux_led/test_light.py +++ b/tests/components/flux_led/test_light.py @@ -1,6 +1,6 @@ """Tests for light platform.""" from datetime import timedelta -from unittest.mock import AsyncMock, Mock, patch +from unittest.mock import AsyncMock, Mock from flux_led.const import ( COLOR_MODE_ADDRESSABLE as FLUX_COLOR_MODE_ADDRESSABLE, @@ -16,20 +16,12 @@ import pytest from homeassistant.components import flux_led from homeassistant.components.flux_led.const import ( CONF_COLORS, - CONF_CUSTOM_EFFECT, CONF_CUSTOM_EFFECT_COLORS, CONF_CUSTOM_EFFECT_SPEED_PCT, CONF_CUSTOM_EFFECT_TRANSITION, - CONF_DEVICES, - CONF_MINOR_VERSION, - CONF_MODEL, - CONF_REMOTE_ACCESS_ENABLED, - CONF_REMOTE_ACCESS_HOST, - CONF_REMOTE_ACCESS_PORT, CONF_SPEED_PCT, CONF_TRANSITION, DOMAIN, - MODE_AUTO, TRANSITION_JUMP, ) from homeassistant.components.light import ( @@ -51,8 +43,6 @@ from homeassistant.const import ( CONF_HOST, CONF_MODE, CONF_NAME, - CONF_PLATFORM, - CONF_PROTOCOL, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, @@ -64,10 +54,8 @@ from homeassistant.util.dt import utcnow from . import ( DEFAULT_ENTRY_TITLE, - FLUX_DISCOVERY, IP_ADDRESS, MAC_ADDRESS, - MODEL, _mocked_bulb, _patch_discovery, _patch_wifibulb, @@ -971,7 +959,7 @@ async def test_rgb_light_custom_effects(hass: HomeAssistant) -> None: data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, unique_id=MAC_ADDRESS, options={ - CONF_MODE: MODE_AUTO, + CONF_MODE: "auto", CONF_CUSTOM_EFFECT_COLORS: "[0,0,255], [255,0,0]", CONF_CUSTOM_EFFECT_SPEED_PCT: 88, CONF_CUSTOM_EFFECT_TRANSITION: TRANSITION_JUMP, @@ -1047,7 +1035,7 @@ async def test_rgb_light_custom_effects_invalid_colors( ) -> None: """Test an rgb light with a invalid effect.""" options = { - CONF_MODE: MODE_AUTO, + CONF_MODE: "auto", CONF_CUSTOM_EFFECT_SPEED_PCT: 88, CONF_CUSTOM_EFFECT_TRANSITION: TRANSITION_JUMP, } @@ -1132,145 +1120,6 @@ async def test_rgb_light_custom_effect_via_service( bulb.async_set_custom_pattern.reset_mock() -async def test_migrate_from_yaml_with_custom_effect(hass: HomeAssistant) -> None: - """Test migrate from yaml.""" - config = { - LIGHT_DOMAIN: [ - { - CONF_PLATFORM: DOMAIN, - CONF_DEVICES: { - IP_ADDRESS: { - CONF_NAME: "flux_lamppost", - CONF_PROTOCOL: "ledenet", - CONF_CUSTOM_EFFECT: { - CONF_SPEED_PCT: 30, - CONF_TRANSITION: "strobe", - CONF_COLORS: [[255, 0, 0], [255, 255, 0], [0, 255, 0]], - }, - } - }, - } - ], - } - - last_address = None - - async def _discovery(self, *args, address=None, **kwargs): - # Only return discovery results when doing directed discovery - nonlocal last_address - last_address = address - return [FLUX_DISCOVERY] if address == IP_ADDRESS else [] - - def _mock_getBulbInfo(*args, **kwargs): - nonlocal last_address - return [FLUX_DISCOVERY] if last_address == IP_ADDRESS else [] - - with patch( - "homeassistant.components.flux_led.discovery.AIOBulbScanner.async_scan", - new=_discovery, - ), patch( - "homeassistant.components.flux_led.discovery.AIOBulbScanner.getBulbInfo", - new=_mock_getBulbInfo, - ), _patch_wifibulb(): - await async_setup_component(hass, LIGHT_DOMAIN, config) - await hass.async_block_till_done() - await hass.async_block_till_done() - await hass.async_block_till_done() - - entries = hass.config_entries.async_entries(DOMAIN) - assert entries - - migrated_entry = None - for entry in entries: - if entry.unique_id == MAC_ADDRESS: - migrated_entry = entry - break - - assert migrated_entry is not None - assert migrated_entry.data == { - CONF_HOST: IP_ADDRESS, - CONF_NAME: "flux_lamppost", - CONF_PROTOCOL: "ledenet", - CONF_MODEL: MODEL, - CONF_REMOTE_ACCESS_ENABLED: True, - CONF_REMOTE_ACCESS_HOST: "the.cloud", - CONF_REMOTE_ACCESS_PORT: 8816, - CONF_MINOR_VERSION: 0x04, - } - assert migrated_entry.options == { - CONF_CUSTOM_EFFECT_COLORS: "[(255, 0, 0), (255, 255, 0), (0, 255, 0)]", - CONF_CUSTOM_EFFECT_SPEED_PCT: 30, - CONF_CUSTOM_EFFECT_TRANSITION: "strobe", - } - - -async def test_migrate_from_yaml_no_custom_effect(hass: HomeAssistant) -> None: - """Test migrate from yaml.""" - config = { - LIGHT_DOMAIN: [ - { - CONF_PLATFORM: DOMAIN, - CONF_DEVICES: { - IP_ADDRESS: { - CONF_NAME: "flux_lamppost", - CONF_PROTOCOL: "ledenet", - } - }, - } - ], - } - - last_address = None - - async def _discovery(self, *args, address=None, **kwargs): - # Only return discovery results when doing directed discovery - nonlocal last_address - last_address = address - return [FLUX_DISCOVERY] if address == IP_ADDRESS else [] - - def _mock_getBulbInfo(*args, **kwargs): - nonlocal last_address - return [FLUX_DISCOVERY] if last_address == IP_ADDRESS else [] - - with patch( - "homeassistant.components.flux_led.discovery.AIOBulbScanner.async_scan", - new=_discovery, - ), patch( - "homeassistant.components.flux_led.discovery.AIOBulbScanner.getBulbInfo", - new=_mock_getBulbInfo, - ), _patch_wifibulb(): - await async_setup_component(hass, LIGHT_DOMAIN, config) - await hass.async_block_till_done() - await hass.async_block_till_done() - await hass.async_block_till_done() - - entries = hass.config_entries.async_entries(DOMAIN) - assert entries - - migrated_entry = None - for entry in entries: - if entry.unique_id == MAC_ADDRESS: - migrated_entry = entry - break - - assert migrated_entry is not None - assert migrated_entry.data == { - CONF_HOST: IP_ADDRESS, - CONF_NAME: "flux_lamppost", - CONF_PROTOCOL: "ledenet", - CONF_MODEL: MODEL, - CONF_REMOTE_ACCESS_ENABLED: True, - CONF_REMOTE_ACCESS_HOST: "the.cloud", - CONF_REMOTE_ACCESS_PORT: 8816, - CONF_MINOR_VERSION: 0x04, - } - assert migrated_entry.options == { - CONF_CUSTOM_EFFECT_COLORS: None, - CONF_CUSTOM_EFFECT_SPEED_PCT: 50, - CONF_CUSTOM_EFFECT_TRANSITION: "gradual", - } - - async def test_addressable_light(hass: HomeAssistant) -> None: """Test an addressable light.""" config_entry = MockConfigEntry( From dbb4c1b5f0615225a964322eeb78d38814f29223 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 19 Dec 2021 14:58:34 -0700 Subject: [PATCH 0775/2644] Replace RainMachine logged errors with HomeAssistantError in service handlers (#62350) --- .../components/rainmachine/switch.py | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index ddda36c13e8..d96b145300e 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -30,7 +30,6 @@ from .const import ( DATA_ZONES, DEFAULT_ZONE_RUN, DOMAIN, - LOGGER, ) ATTR_AREA = "area" @@ -215,22 +214,14 @@ class RainMachineBaseSwitch(RainMachineEntity, SwitchEntity): try: resp = await api_coro except RequestError as err: - LOGGER.error( - 'Error while executing %s on "%s": %s', - api_coro.__name__, - self.name, - err, - ) - return + raise HomeAssistantError( + f'Error while executing {api_coro.__name__} on "{self.name}": {err}', + ) from err if resp["statusCode"] != 0: - LOGGER.error( - 'Error while executing %s on "%s": %s', - api_coro.__name__, - self.name, - resp["message"], + raise HomeAssistantError( + f'Error while executing {api_coro.__name__} on "{self.name}": {resp["message"]}', ) - return # Because of how inextricably linked programs and zones are, anytime one is # toggled, we make sure to update the data of both coordinators: From fed18fe1425971963822294247985888f6efdd85 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 19 Dec 2021 15:02:51 -0700 Subject: [PATCH 0776/2644] Replace OpenUV logged errors with `HomeAssistantError` in service handlers (#62349) Co-authored-by: Martin Hjelmare --- homeassistant/components/openuv/__init__.py | 22 ++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index a60414e2872..7a9ef1785b7 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -18,7 +18,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant, ServiceCall, callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers import aiohttp_client from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -147,7 +147,7 @@ class OpenUV: """Initialize.""" self._entry = entry self.client = client - self.data: dict[str, Any] = {} + self.data: dict[str, Any] = {DATA_PROTECTION_WINDOW: {}, DATA_UV: {}} async def async_update_protection_data(self) -> None: """Update binary sensor (protection window) data.""" @@ -155,20 +155,24 @@ class OpenUV: high = self._entry.options.get(CONF_TO_WINDOW, DEFAULT_TO_WINDOW) try: - resp = await self.client.uv_protection_window(low=low, high=high) - self.data[DATA_PROTECTION_WINDOW] = resp["result"] + data = await self.client.uv_protection_window(low=low, high=high) except OpenUvError as err: - LOGGER.error("Error during protection data update: %s", err) - self.data[DATA_PROTECTION_WINDOW] = {} + raise HomeAssistantError( + f"Error during protection data update: {err}" + ) from err + + self.data[DATA_PROTECTION_WINDOW] = data["result"] async def async_update_uv_index_data(self) -> None: """Update sensor (uv index, etc) data.""" try: data = await self.client.uv_index() - self.data[DATA_UV] = data except OpenUvError as err: - LOGGER.error("Error during uv index data update: %s", err) - self.data[DATA_UV] = {} + raise HomeAssistantError( + f"Error during UV index data update: {err}" + ) from err + + self.data[DATA_UV] = data async def async_update(self) -> None: """Update sensor/binary sensor data.""" From 667a632e063b1de7320aee4ac7deed1c1505dcfe Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 19 Dec 2021 17:03:18 -0500 Subject: [PATCH 0777/2644] Remove deprecated yaml config from enphase_envoy (#62348) From b77fc2e8cbc4a89f7a46f401ea1a7fd5a60be876 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 19 Dec 2021 15:05:13 -0700 Subject: [PATCH 0778/2644] Ensure existing SimpliSafe websocket tasks are cancelled appropriately (#62347) --- .../components/simplisafe/__init__.py | 33 +++++++++++-------- .../components/simplisafe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 537e199fc04..025c3a3ae49 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -520,14 +520,14 @@ class SimpliSafe: self._system_notifications[system.system_id] = latest_notifications async def _async_start_websocket_loop(self) -> None: - """Define a callback for connecting to the websocket.""" + """Start a websocket reconnection loop.""" if TYPE_CHECKING: assert self._api.websocket should_reconnect = True try: - await self._api.websocket.async_reconnect() + await self._api.websocket.async_connect() await self._api.websocket.async_listen() except asyncio.CancelledError: LOGGER.debug("Request to cancel websocket loop received") @@ -539,10 +539,25 @@ class SimpliSafe: if should_reconnect: LOGGER.info("Disconnected from websocket; reconnecting") + await self._async_cancel_websocket_loop() self._websocket_reconnect_task = self._hass.async_create_task( self._async_start_websocket_loop() ) + async def _async_cancel_websocket_loop(self) -> None: + """Stop any existing websocket reconnection loop.""" + if self._websocket_reconnect_task: + self._websocket_reconnect_task.cancel() + try: + await self._websocket_reconnect_task + except asyncio.CancelledError: + LOGGER.debug("Websocket reconnection task successfully canceled") + self._websocket_reconnect_task = None + + if TYPE_CHECKING: + assert self._api.websocket + await self._api.websocket.async_disconnect() + @callback def _async_websocket_on_event(self, event: WebsocketEvent) -> None: """Define a callback for receiving a websocket event.""" @@ -591,15 +606,7 @@ class SimpliSafe: if TYPE_CHECKING: assert self._api.websocket - if self._websocket_reconnect_task: - self._websocket_reconnect_task.cancel() - try: - await self._websocket_reconnect_task - except asyncio.CancelledError: - LOGGER.debug("Websocket reconnection task successfully canceled") - self._websocket_reconnect_task = None - - await self._api.websocket.async_disconnect() + await self._async_cancel_websocket_loop() self.entry.async_on_unload( self._hass.bus.async_listen_once( @@ -641,8 +648,7 @@ class SimpliSafe: data={**self.entry.data, CONF_TOKEN: token}, ) - @callback - def async_handle_refresh_token(token: str) -> None: + async def async_handle_refresh_token(token: str) -> None: """Handle a new refresh token.""" async_save_refresh_token(token) @@ -650,6 +656,7 @@ class SimpliSafe: assert self._api.websocket # Open a new websocket connection with the fresh token: + await self._async_cancel_websocket_loop() self._websocket_reconnect_task = self._hass.async_create_task( self._async_start_websocket_loop() ) diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 0b6cb385be6..8e494af013a 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,7 +3,7 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==2021.12.1"], + "requirements": ["simplisafe-python==2021.12.2"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 4ce995f10b9..3cde5400641 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2158,7 +2158,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==2021.12.1 +simplisafe-python==2021.12.2 # homeassistant.components.sisyphus sisyphus-control==3.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index af3182af5c6..cfb9f5f8c76 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1285,7 +1285,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==2021.12.1 +simplisafe-python==2021.12.2 # homeassistant.components.slack slackclient==2.5.0 From 3c913d4e88d062945515a0aba2ab292a4504f341 Mon Sep 17 00:00:00 2001 From: AJ Schmidt Date: Sun, 19 Dec 2021 17:12:33 -0500 Subject: [PATCH 0779/2644] Re-add `binary_sensor` attribute for AlarmDecoder that was inadvertently removed (#62351) --- homeassistant/components/alarmdecoder/binary_sensor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/alarmdecoder/binary_sensor.py b/homeassistant/components/alarmdecoder/binary_sensor.py index 430a4f73262..b5a2a4498c3 100644 --- a/homeassistant/components/alarmdecoder/binary_sensor.py +++ b/homeassistant/components/alarmdecoder/binary_sensor.py @@ -81,6 +81,9 @@ class AlarmDecoderBinarySensor(BinarySensorEntity): self._relay_addr = relay_addr self._relay_chan = relay_chan self._attr_device_class = zone_type + self._attr_extra_state_attributes = { + CONF_ZONE_NUMBER: self._zone_number, + } async def async_added_to_hass(self): """Register callbacks.""" From 79199605706a199f0109b9ae7c9c27cdfbe55059 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 19 Dec 2021 18:42:30 -0500 Subject: [PATCH 0780/2644] Remove deprecated yaml config from broadlink (#62341) --- homeassistant/components/broadlink/remote.py | 21 ++-------------- homeassistant/components/broadlink/sensor.py | 26 -------------------- homeassistant/components/broadlink/switch.py | 26 +++----------------- 3 files changed, 5 insertions(+), 68 deletions(-) diff --git a/homeassistant/components/broadlink/remote.py b/homeassistant/components/broadlink/remote.py index 5a939b68bb4..ad597d80d20 100644 --- a/homeassistant/components/broadlink/remote.py +++ b/homeassistant/components/broadlink/remote.py @@ -24,7 +24,6 @@ from homeassistant.components.remote import ( ATTR_NUM_REPEATS, DEFAULT_DELAY_SECS, DOMAIN as RM_DOMAIN, - PLATFORM_SCHEMA, SERVICE_DELETE_COMMAND, SERVICE_LEARN_COMMAND, SERVICE_SEND_COMMAND, @@ -32,7 +31,7 @@ from homeassistant.components.remote import ( SUPPORT_LEARN_COMMAND, RemoteEntity, ) -from homeassistant.const import CONF_HOST, STATE_OFF +from homeassistant.const import STATE_OFF from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity @@ -41,7 +40,7 @@ from homeassistant.util import dt from .const import DOMAIN from .entity import BroadlinkEntity -from .helpers import data_packet, import_device +from .helpers import data_packet _LOGGER = logging.getLogger(__name__) @@ -85,22 +84,6 @@ SERVICE_DELETE_SCHEMA = COMMAND_SCHEMA.extend( {vol.Required(ATTR_DEVICE): vol.All(cv.string, vol.Length(min=1))} ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Required(CONF_HOST): cv.string}, extra=vol.ALLOW_EXTRA -) - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Import the device and discontinue platform. - - This is for backward compatibility. - Do not use this method. - """ - import_device(hass, config[CONF_HOST]) - _LOGGER.warning( - "The remote platform is deprecated, please remove it from your configuration" - ) - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up a Broadlink remote.""" diff --git a/homeassistant/components/broadlink/sensor.py b/homeassistant/components/broadlink/sensor.py index 6eb0314fd65..eb2c51d86c3 100644 --- a/homeassistant/components/broadlink/sensor.py +++ b/homeassistant/components/broadlink/sensor.py @@ -1,19 +1,13 @@ """Support for Broadlink sensors.""" from __future__ import annotations -import logging - -import voluptuous as vol - from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, ) from homeassistant.const import ( - CONF_HOST, ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, @@ -21,13 +15,9 @@ from homeassistant.const import ( POWER_WATT, TEMP_CELSIUS, ) -from homeassistant.helpers import config_validation as cv from .const import DOMAIN from .entity import BroadlinkEntity -from .helpers import import_device - -_LOGGER = logging.getLogger(__name__) SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( @@ -94,22 +84,6 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Required(CONF_HOST): cv.string}, extra=vol.ALLOW_EXTRA -) - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Import the device and discontinue platform. - - This is for backward compatibility. - Do not use this method. - """ - import_device(hass, config[CONF_HOST]) - _LOGGER.warning( - "The sensor platform is deprecated, please remove it from your configuration" - ) - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Broadlink sensor.""" diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index a2a988c8531..36f3b4db759 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -13,7 +13,6 @@ from homeassistant.components.switch import ( from homeassistant.const import ( CONF_COMMAND_OFF, CONF_COMMAND_ON, - CONF_FRIENDLY_NAME, CONF_HOST, CONF_MAC, CONF_NAME, @@ -42,14 +41,6 @@ SWITCH_SCHEMA = vol.Schema( } ) -OLD_SWITCH_SCHEMA = vol.Schema( - { - vol.Optional(CONF_COMMAND_OFF): data_packet, - vol.Optional(CONF_COMMAND_ON): data_packet, - vol.Optional(CONF_FRIENDLY_NAME): cv.string, - } -) - PLATFORM_SCHEMA = vol.All( cv.deprecated(CONF_HOST), cv.deprecated(CONF_SLOTS), @@ -59,9 +50,9 @@ PLATFORM_SCHEMA = vol.All( { vol.Required(CONF_MAC): mac_address, vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_SWITCHES, default=[]): vol.Any( - cv.schema_with_slug_keys(OLD_SWITCH_SCHEMA), - vol.All(cv.ensure_list, [SWITCH_SCHEMA]), + vol.Optional(CONF_SWITCHES, default=[]): vol.All( + cv.ensure_list, + [SWITCH_SCHEMA], ), } ), @@ -78,17 +69,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= host = config.get(CONF_HOST) switches = config.get(CONF_SWITCHES) - if not isinstance(switches, list): - switches = [ - {CONF_NAME: switch.pop(CONF_FRIENDLY_NAME, name), **switch} - for name, switch in switches.items() - ] - - _LOGGER.warning( - "Your configuration for the switch platform is deprecated. " - "Please refer to the Broadlink documentation to catch up" - ) - if switches: platform_data = hass.data[DOMAIN].platforms.setdefault(Platform.SWITCH, {}) platform_data.setdefault(mac_addr, []).extend(switches) From 131859737044ece2d41b91c17ce12457d624c978 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Sun, 19 Dec 2021 16:09:30 -0800 Subject: [PATCH 0781/2644] Fix typing for wemo (#62157) --- .strict-typing | 1 + homeassistant/components/wemo/__init__.py | 63 +++++++++---------- .../components/wemo/binary_sensor.py | 16 +++-- homeassistant/components/wemo/config_flow.py | 3 +- .../components/wemo/device_trigger.py | 21 ++++++- homeassistant/components/wemo/entity.py | 10 +-- homeassistant/components/wemo/fan.py | 36 +++++++---- homeassistant/components/wemo/light.py | 62 +++++++++++------- homeassistant/components/wemo/sensor.py | 29 +++++---- homeassistant/components/wemo/switch.py | 63 ++++++++++++------- homeassistant/components/wemo/wemo_device.py | 3 +- mypy.ini | 14 ++++- script/hassfest/mypy_config.py | 1 - tests/components/wemo/test_light_bridge.py | 7 ++- 14 files changed, 207 insertions(+), 122 deletions(-) diff --git a/.strict-typing b/.strict-typing index f663ec6a6b9..3eae62f4b4a 100644 --- a/.strict-typing +++ b/.strict-typing @@ -151,6 +151,7 @@ homeassistant.components.water_heater.* homeassistant.components.watttime.* homeassistant.components.weather.* homeassistant.components.websocket_api.* +homeassistant.components.wemo.* homeassistant.components.zodiac.* homeassistant.components.zeroconf.* homeassistant.components.zone.* diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index 27d3a0cbf25..24cf7d66a0a 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -1,7 +1,9 @@ """Support for WeMo device discovery.""" from __future__ import annotations +from collections.abc import Sequence import logging +from typing import Any, Optional, Tuple import pywemo import voluptuous as vol @@ -14,10 +16,11 @@ from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DISCOVERY, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.typing import ConfigType from homeassistant.util.async_ import gather_with_concurrency from .const import DOMAIN @@ -44,21 +47,20 @@ WEMO_MODEL_DISPATCH = { _LOGGER = logging.getLogger(__name__) +HostPortTuple = Tuple[str, Optional[int]] -def coerce_host_port(value): + +def coerce_host_port(value: str) -> HostPortTuple: """Validate that provided value is either just host or host:port. Returns (host, None) or (host, port) respectively. """ - host, _, port = value.partition(":") + host, _, port_str = value.partition(":") if not host: raise vol.Invalid("host cannot be empty") - if port: - port = cv.port(port) - else: - port = None + port = cv.port(port_str) if port_str else None return host, port @@ -82,7 +84,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up for WeMo devices.""" hass.data[DOMAIN] = { "config": config.get(DOMAIN, {}), @@ -112,11 +114,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: discovery_responder = pywemo.ssdp.DiscoveryResponder(registry.port) await hass.async_add_executor_job(discovery_responder.start) - static_conf = config.get(CONF_STATIC, []) + static_conf: Sequence[HostPortTuple] = config.get(CONF_STATIC, []) wemo_dispatcher = WemoDispatcher(entry) wemo_discovery = WemoDiscovery(hass, wemo_dispatcher, static_conf) - async def async_stop_wemo(event): + async def async_stop_wemo(event: Event) -> None: """Shutdown Wemo subscriptions and subscription thread on exit.""" _LOGGER.debug("Shutting down WeMo event subscriptions") await hass.async_add_executor_job(registry.stop) @@ -142,8 +144,8 @@ class WemoDispatcher: def __init__(self, config_entry: ConfigEntry) -> None: """Initialize the WemoDispatcher.""" self._config_entry = config_entry - self._added_serial_numbers = set() - self._loaded_components = set() + self._added_serial_numbers: set[str] = set() + self._loaded_components: set[str] = set() async def async_add_unique_device( self, hass: HomeAssistant, wemo: pywemo.WeMoDevice @@ -191,16 +193,16 @@ class WemoDiscovery: self, hass: HomeAssistant, wemo_dispatcher: WemoDispatcher, - static_config: list[tuple[[str, str | None]]], + static_config: Sequence[HostPortTuple], ) -> None: """Initialize the WemoDiscovery.""" self._hass = hass self._wemo_dispatcher = wemo_dispatcher - self._stop = None + self._stop: CALLBACK_TYPE | None = None self._scan_delay = 0 self._static_config = static_config - async def async_discover_and_schedule(self, *_) -> None: + async def async_discover_and_schedule(self, *_: tuple[Any]) -> None: """Periodically scan the network looking for WeMo devices.""" _LOGGER.debug("Scanning network for WeMo devices") try: @@ -229,26 +231,23 @@ class WemoDiscovery: self._stop() self._stop = None - async def discover_statics(self): + async def discover_statics(self) -> None: """Initialize or Re-Initialize connections to statically configured devices.""" - if self._static_config: - _LOGGER.debug("Adding statically configured WeMo devices") - for device in await gather_with_concurrency( - MAX_CONCURRENCY, - *( - self._hass.async_add_executor_job( - validate_static_config, host, port - ) - for host, port in self._static_config - ), - ): - if device: - await self._wemo_dispatcher.async_add_unique_device( - self._hass, device - ) + if not self._static_config: + return + _LOGGER.debug("Adding statically configured WeMo devices") + for device in await gather_with_concurrency( + MAX_CONCURRENCY, + *( + self._hass.async_add_executor_job(validate_static_config, host, port) + for host, port in self._static_config + ), + ): + if device: + await self._wemo_dispatcher.async_add_unique_device(self._hass, device) -def validate_static_config(host, port): +def validate_static_config(host: str, port: int | None) -> pywemo.WeMoDevice | None: """Handle a static config.""" url = pywemo.setup_url_for_address(host, port) diff --git a/homeassistant/components/wemo/binary_sensor.py b/homeassistant/components/wemo/binary_sensor.py index 1341d5526a3..766ca61c560 100644 --- a/homeassistant/components/wemo/binary_sensor.py +++ b/homeassistant/components/wemo/binary_sensor.py @@ -4,16 +4,24 @@ import asyncio from pywemo import Insight, Maker from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN as WEMO_DOMAIN from .entity import WemoEntity +from .wemo_device import DeviceCoordinator -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up WeMo binary sensors.""" - async def _discovered_wemo(coordinator): + async def _discovered_wemo(coordinator: DeviceCoordinator) -> None: """Handle a discovered Wemo device.""" if isinstance(coordinator.wemo, Insight): async_add_entities([InsightBinarySensor(coordinator)]) @@ -38,7 +46,7 @@ class WemoBinarySensor(WemoEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Return true if the state is on. Standby is on.""" - return self.wemo.get_state() + return bool(self.wemo.get_state()) class MakerBinarySensor(WemoEntity, BinarySensorEntity): @@ -49,7 +57,7 @@ class MakerBinarySensor(WemoEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Return true if the Maker's sensor is pulled low.""" - return self.wemo.has_sensor and self.wemo.sensor_state == 0 + return bool(self.wemo.has_sensor) and self.wemo.sensor_state == 0 class InsightBinarySensor(WemoBinarySensor): diff --git a/homeassistant/components/wemo/config_flow.py b/homeassistant/components/wemo/config_flow.py index b778779ea3c..6783d870cdc 100644 --- a/homeassistant/components/wemo/config_flow.py +++ b/homeassistant/components/wemo/config_flow.py @@ -2,12 +2,13 @@ import pywemo +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_flow from . import DOMAIN -async def _async_has_devices(hass): +async def _async_has_devices(hass: HomeAssistant) -> bool: """Return if there are devices that can be discovered.""" return bool(await hass.async_add_executor_job(pywemo.discover_devices)) diff --git a/homeassistant/components/wemo/device_trigger.py b/homeassistant/components/wemo/device_trigger.py index da9a157e1a4..1cdc29fa995 100644 --- a/homeassistant/components/wemo/device_trigger.py +++ b/homeassistant/components/wemo/device_trigger.py @@ -1,10 +1,20 @@ """Triggers for WeMo devices.""" +from __future__ import annotations + +from typing import Any + from pywemo.subscribe import EVENT_TYPE_LONG_PRESS import voluptuous as vol +from homeassistant.components.automation import ( + AutomationActionType, + AutomationTriggerInfo, +) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.homeassistant.triggers import event as event_trigger from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE +from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.typing import ConfigType from .const import DOMAIN as WEMO_DOMAIN, WEMO_SUBSCRIPTION_EVENT from .wemo_device import async_get_coordinator @@ -18,7 +28,9 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( ) -async def async_get_triggers(hass, device_id): +async def async_get_triggers( + hass: HomeAssistant, device_id: str +) -> list[dict[str, Any]]: """Return a list of triggers.""" wemo_trigger = { @@ -44,7 +56,12 @@ async def async_get_triggers(hass, device_id): return triggers -async def async_attach_trigger(hass, config, action, automation_info): +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: AutomationTriggerInfo, +) -> CALLBACK_TYPE: """Attach a trigger.""" event_config = event_trigger.TRIGGER_SCHEMA( { diff --git a/homeassistant/components/wemo/entity.py b/homeassistant/components/wemo/entity.py index 2811d371f6b..824fcacae7c 100644 --- a/homeassistant/components/wemo/entity.py +++ b/homeassistant/components/wemo/entity.py @@ -33,7 +33,7 @@ class WemoEntity(CoordinatorEntity): self._available = True @property - def name_suffix(self): + def name_suffix(self) -> str | None: """Suffix to append to the WeMo device name.""" return self._name_suffix @@ -42,7 +42,7 @@ class WemoEntity(CoordinatorEntity): """Return the name of the device if any.""" if suffix := self.name_suffix: return f"{self.wemo.name} {suffix}" - return self.wemo.name + return str(self.wemo.name) @property def available(self) -> bool: @@ -50,10 +50,10 @@ class WemoEntity(CoordinatorEntity): return super().available and self._available @property - def unique_id_suffix(self): + def unique_id_suffix(self) -> str | None: """Suffix to append to the WeMo device's unique ID.""" if self._unique_id_suffix is None and self.name_suffix is not None: - return self._name_suffix.lower() + return self.name_suffix.lower() return self._unique_id_suffix @property @@ -61,7 +61,7 @@ class WemoEntity(CoordinatorEntity): """Return the id of this WeMo device.""" if suffix := self.unique_id_suffix: return f"{self.wemo.serialnumber}_{suffix}" - return self.wemo.serialnumber + return str(self.wemo.serialnumber) @property def device_info(self) -> DeviceInfo: diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index 00f9b77aa61..003dfa7e633 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -1,14 +1,19 @@ """Support for WeMo humidifier.""" +from __future__ import annotations + import asyncio from datetime import timedelta import math +from typing import Any import voluptuous as vol from homeassistant.components.fan import SUPPORT_SET_SPEED, FanEntity -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_platform 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, @@ -21,6 +26,7 @@ from .const import ( SERVICE_SET_HUMIDITY, ) from .entity import WemoEntity +from .wemo_device import DeviceCoordinator SCAN_INTERVAL = timedelta(seconds=10) PARALLEL_UPDATES = 0 @@ -63,10 +69,14 @@ SET_HUMIDITY_SCHEMA = { } -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up WeMo binary sensors.""" - async def _discovered_wemo(coordinator): + async def _discovered_wemo(coordinator: DeviceCoordinator) -> None: """Handle a discovered Wemo device.""" async_add_entities([WemoHumidifier(coordinator)]) @@ -95,7 +105,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class WemoHumidifier(WemoEntity, FanEntity): """Representation of a WeMo humidifier.""" - def __init__(self, coordinator): + def __init__(self, coordinator: DeviceCoordinator) -> None: """Initialize the WeMo switch.""" super().__init__(coordinator) if self.wemo.fan_mode != WEMO_FAN_OFF: @@ -104,12 +114,12 @@ class WemoHumidifier(WemoEntity, FanEntity): self._last_fan_on_mode = WEMO_FAN_MEDIUM @property - def icon(self): + def icon(self) -> str: """Return the icon of device based on its type.""" return "mdi:water-percent" @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return device specific state attributes.""" return { ATTR_CURRENT_HUMIDITY: self.wemo.current_humidity_percent, @@ -145,26 +155,26 @@ class WemoHumidifier(WemoEntity, FanEntity): @property def is_on(self) -> bool: """Return true if the state is on.""" - return self.wemo.get_state() + return bool(self.wemo.get_state()) def turn_on( self, - speed: str = None, - percentage: int = None, - preset_mode: str = None, - **kwargs, + speed: str | None = None, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, ) -> None: """Turn the fan on.""" self.set_percentage(percentage) - def turn_off(self, **kwargs) -> None: + def turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" with self._wemo_exception_handler("turn off"): self.wemo.set_state(WEMO_FAN_OFF) self.schedule_update_ha_state() - def set_percentage(self, percentage: int) -> None: + def set_percentage(self, percentage: int | None) -> None: """Set the fan_mode of the Humidifier.""" if percentage is None: named_speed = self._last_fan_on_mode diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py index c46a4e78440..eb19355f2b3 100644 --- a/homeassistant/components/wemo/light.py +++ b/homeassistant/components/wemo/light.py @@ -1,5 +1,8 @@ """Support for Belkin WeMo lights.""" +from __future__ import annotations + import asyncio +from typing import Any from pywemo.ouimeaux_device import bridge @@ -14,10 +17,12 @@ from homeassistant.components.light import ( SUPPORT_TRANSITION, LightEntity, ) -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.color as color_util from .const import DOMAIN as WEMO_DOMAIN @@ -32,10 +37,14 @@ SUPPORT_WEMO = ( WEMO_OFF = 0 -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up WeMo lights.""" - async def _discovered_wemo(coordinator: DeviceCoordinator): + async def _discovered_wemo(coordinator: DeviceCoordinator) -> None: """Handle a discovered Wemo device.""" if isinstance(coordinator.wemo, bridge.Bridge): async_setup_bridge(hass, config_entry, async_add_entities, coordinator) @@ -53,12 +62,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback -def async_setup_bridge(hass, config_entry, async_add_entities, coordinator): +def async_setup_bridge( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + coordinator: DeviceCoordinator, +) -> None: """Set up a WeMo link.""" known_light_ids = set() @callback - def async_update_lights(): + def async_update_lights() -> None: """Check to see if the bridge has any new lights.""" new_lights = [] @@ -87,7 +101,7 @@ class WemoLight(WemoEntity, LightEntity): @property def name(self) -> str: """Return the name of the device if any.""" - return self.light.name + return str(self.light.name) @property def available(self) -> bool: @@ -95,9 +109,9 @@ class WemoLight(WemoEntity, LightEntity): return super().available and self.light.state.get("available") @property - def unique_id(self): + def unique_id(self) -> str: """Return the ID of this light.""" - return self.light.uniqueID + return str(self.light.uniqueID) @property def device_info(self) -> DeviceInfo: @@ -111,33 +125,35 @@ class WemoLight(WemoEntity, LightEntity): ) @property - def brightness(self): + def brightness(self) -> int: """Return the brightness of this light between 0..255.""" - return self.light.state.get("level", 255) + return int(self.light.state.get("level", 255)) @property - def hs_color(self): + def hs_color(self) -> tuple[float, float] | None: """Return the hs color values of this light.""" if xy_color := self.light.state.get("color_xy"): return color_util.color_xy_to_hs(*xy_color) return None @property - def color_temp(self): + def color_temp(self) -> int | None: """Return the color temperature of this light in mireds.""" - return self.light.state.get("temperature_mireds") + if (temp := self.light.state.get("temperature_mireds")) is not None: + return int(temp) + return None @property - def is_on(self): + def is_on(self) -> bool: """Return true if device is on.""" - return self.light.state.get("onoff") != WEMO_OFF + return bool(self.light.state.get("onoff") != WEMO_OFF) @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" return SUPPORT_WEMO - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" xy_color = None @@ -168,7 +184,7 @@ class WemoLight(WemoEntity, LightEntity): self.schedule_update_ha_state() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" transition_time = int(kwargs.get(ATTR_TRANSITION, 0)) @@ -182,12 +198,12 @@ class WemoDimmer(WemoEntity, LightEntity): """Representation of a WeMo dimmer.""" @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" return SUPPORT_BRIGHTNESS @property - def brightness(self): + def brightness(self) -> int: """Return the brightness of this light between 1 and 100.""" wemo_brightness = int(self.wemo.get_brightness()) return int((wemo_brightness * 255) / 100) @@ -195,9 +211,9 @@ class WemoDimmer(WemoEntity, LightEntity): @property def is_on(self) -> bool: """Return true if the state is on.""" - return self.wemo.get_state() + return bool(self.wemo.get_state()) - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the dimmer on.""" # Wemo dimmer switches use a range of [0, 100] to control # brightness. Level 255 might mean to set it to previous value @@ -212,7 +228,7 @@ class WemoDimmer(WemoEntity, LightEntity): self.schedule_update_ha_state() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the dimmer off.""" with self._wemo_exception_handler("turn off"): self.wemo.off() diff --git a/homeassistant/components/wemo/sensor.py b/homeassistant/components/wemo/sensor.py index 49e6b187515..c46e8928a03 100644 --- a/homeassistant/components/wemo/sensor.py +++ b/homeassistant/components/wemo/sensor.py @@ -7,8 +7,11 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ENERGY_KILO_WATT_HOUR, POWER_WATT +from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.util import convert @@ -17,10 +20,14 @@ from .entity import WemoEntity from .wemo_device import DeviceCoordinator -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up WeMo sensors.""" - async def _discovered_wemo(coordinator: DeviceCoordinator): + async def _discovered_wemo(coordinator: DeviceCoordinator) -> None: """Handle a discovered Wemo device.""" async_add_entities( [InsightCurrentPower(coordinator), InsightTodayEnergy(coordinator)] @@ -42,7 +49,7 @@ class InsightSensor(WemoEntity, SensorEntity): @property def name_suffix(self) -> str: """Return the name of the entity if any.""" - return self.entity_description.name + return str(self.entity_description.name) @property def unique_id_suffix(self) -> str: @@ -50,7 +57,7 @@ class InsightSensor(WemoEntity, SensorEntity): return self.entity_description.key @property - def available(self) -> str: + def available(self) -> bool: """Return true if sensor is available.""" return ( self.entity_description.key in self.wemo.insight_params @@ -72,12 +79,11 @@ class InsightCurrentPower(InsightSensor): @property def native_value(self) -> StateType: """Return the current power consumption.""" - return ( - convert( - self.wemo.insight_params.get(self.entity_description.key), float, 0.0 - ) - / 1000.0 + milliwatts = convert( + self.wemo.insight_params.get(self.entity_description.key), float, 0.0 ) + assert isinstance(milliwatts, float) + return milliwatts / 1000.0 class InsightTodayEnergy(InsightSensor): @@ -94,7 +100,8 @@ class InsightTodayEnergy(InsightSensor): @property def native_value(self) -> StateType: """Return the current energy use today.""" - miliwatts = convert( + milliwatt_seconds = convert( self.wemo.insight_params.get(self.entity_description.key), float, 0.0 ) - return round(miliwatts / (1000.0 * 1000.0 * 60), 2) + assert isinstance(milliwatt_seconds, float) + return round(milliwatt_seconds / (1000.0 * 1000.0 * 60), 2) diff --git a/homeassistant/components/wemo/switch.py b/homeassistant/components/wemo/switch.py index d1240d034b5..39a6219b98b 100644 --- a/homeassistant/components/wemo/switch.py +++ b/homeassistant/components/wemo/switch.py @@ -1,16 +1,23 @@ """Support for WeMo switches.""" +from __future__ import annotations + import asyncio from datetime import datetime, timedelta +from typing import Any from pywemo import CoffeeMaker, Insight, Maker from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON, STATE_STANDBY, STATE_UNKNOWN +from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import convert from .const import DOMAIN as WEMO_DOMAIN from .entity import WemoEntity +from .wemo_device import DeviceCoordinator SCAN_INTERVAL = timedelta(seconds=10) PARALLEL_UPDATES = 0 @@ -29,10 +36,14 @@ WEMO_OFF = 0 WEMO_STANDBY = 8 -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up WeMo switches.""" - async def _discovered_wemo(coordinator): + async def _discovered_wemo(coordinator: DeviceCoordinator) -> None: """Handle a discovered Wemo device.""" async_add_entities([WemoSwitch(coordinator)]) @@ -50,9 +61,9 @@ class WemoSwitch(WemoEntity, SwitchEntity): """Representation of a WeMo switch.""" @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the device.""" - attr = {} + attr: dict[str, Any] = {} if isinstance(self.wemo, Maker): # Is the maker sensor on or off. if self.wemo.maker_params["hassensor"]: @@ -81,10 +92,11 @@ class WemoSwitch(WemoEntity, SwitchEntity): attr["on_total_time"] = WemoSwitch.as_uptime( self.wemo.insight_params.get("ontotal", 0) ) - attr["power_threshold_w"] = ( - convert(self.wemo.insight_params.get("powerthreshold"), float, 0.0) - / 1000.0 + threshold = convert( + self.wemo.insight_params.get("powerthreshold"), float, 0.0 ) + assert isinstance(threshold, float) + attr["power_threshold_w"] = threshold / 1000.0 if isinstance(self.wemo, CoffeeMaker): attr[ATTR_COFFEMAKER_MODE] = self.wemo.mode @@ -92,7 +104,7 @@ class WemoSwitch(WemoEntity, SwitchEntity): return attr @staticmethod - def as_uptime(_seconds): + def as_uptime(_seconds: int) -> str: """Format seconds into uptime string in the format: 00d 00h 00m 00s.""" uptime = datetime(1, 1, 1) + timedelta(seconds=_seconds) return "{:0>2d}d {:0>2d}h {:0>2d}m {:0>2d}s".format( @@ -100,26 +112,28 @@ class WemoSwitch(WemoEntity, SwitchEntity): ) @property - def current_power_w(self): + def current_power_w(self) -> float | None: """Return the current power usage in W.""" - if isinstance(self.wemo, Insight): - return ( - convert(self.wemo.insight_params.get("currentpower"), float, 0.0) - / 1000.0 - ) + if not isinstance(self.wemo, Insight): + return None + milliwatts = convert(self.wemo.insight_params.get("currentpower"), float, 0.0) + assert isinstance(milliwatts, float) + return milliwatts / 1000.0 @property - def today_energy_kwh(self): + def today_energy_kwh(self) -> float | None: """Return the today total energy usage in kWh.""" - if isinstance(self.wemo, Insight): - miliwatts = convert(self.wemo.insight_params.get("todaymw"), float, 0.0) - return round(miliwatts / (1000.0 * 1000.0 * 60), 2) + if not isinstance(self.wemo, Insight): + return None + milliwatt_seconds = convert(self.wemo.insight_params.get("todaymw"), float, 0.0) + assert isinstance(milliwatt_seconds, float) + return round(milliwatt_seconds / (1000.0 * 1000.0 * 60), 2) @property - def detail_state(self): + def detail_state(self) -> str: """Return the state of the device.""" if isinstance(self.wemo, CoffeeMaker): - return self.wemo.mode_string + return str(self.wemo.mode_string) if isinstance(self.wemo, Insight): standby_state = int(self.wemo.insight_params.get("state", 0)) if standby_state == WEMO_ON: @@ -129,9 +143,10 @@ class WemoSwitch(WemoEntity, SwitchEntity): if standby_state == WEMO_STANDBY: return STATE_STANDBY return STATE_UNKNOWN + assert False # Unreachable code statement. @property - def icon(self): + def icon(self) -> str | None: """Return the icon of device based on its type.""" if isinstance(self.wemo, CoffeeMaker): return "mdi:coffee" @@ -140,16 +155,16 @@ class WemoSwitch(WemoEntity, SwitchEntity): @property def is_on(self) -> bool: """Return true if the state is on. Standby is on.""" - return self.wemo.get_state() + return bool(self.wemo.get_state()) - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" with self._wemo_exception_handler("turn on"): self.wemo.on() self.schedule_update_ha_state() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" with self._wemo_exception_handler("turn off"): self.wemo.off() diff --git a/homeassistant/components/wemo/wemo_device.py b/homeassistant/components/wemo/wemo_device.py index a507338a4cd..b7138cb0a94 100644 --- a/homeassistant/components/wemo/wemo_device.py +++ b/homeassistant/components/wemo/wemo_device.py @@ -165,4 +165,5 @@ async def async_register_device( @callback def async_get_coordinator(hass: HomeAssistant, device_id: str) -> DeviceCoordinator: """Return DeviceCoordinator for device_id.""" - return hass.data[DOMAIN]["devices"][device_id] + coordinator: DeviceCoordinator = hass.data[DOMAIN]["devices"][device_id] + return coordinator diff --git a/mypy.ini b/mypy.ini index 475840c3b4d..b4c6c802d04 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1672,6 +1672,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.wemo.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.zodiac.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -2066,9 +2077,6 @@ ignore_errors = true [mypy-homeassistant.components.vizio.*] ignore_errors = true -[mypy-homeassistant.components.wemo.*] -ignore_errors = true - [mypy-homeassistant.components.withings.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 6fc6c0e3993..220837fd10b 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -127,7 +127,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.vera.*", "homeassistant.components.verisure.*", "homeassistant.components.vizio.*", - "homeassistant.components.wemo.*", "homeassistant.components.withings.*", "homeassistant.components.xbox.*", "homeassistant.components.xiaomi_aqara.*", diff --git a/tests/components/wemo/test_light_bridge.py b/tests/components/wemo/test_light_bridge.py index b00cfe30ef7..9f622a22c1d 100644 --- a/tests/components/wemo/test_light_bridge.py +++ b/tests/components/wemo/test_light_bridge.py @@ -8,7 +8,7 @@ from homeassistant.components.homeassistant import ( DOMAIN as HA_DOMAIN, SERVICE_UPDATE_ENTITY, ) -from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.light import ATTR_COLOR_TEMP, DOMAIN as LIGHT_DOMAIN from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component @@ -88,13 +88,16 @@ async def test_light_update_entity( # On state. pywemo_bridge_light.state["onoff"] = 1 + pywemo_bridge_light.state["temperature_mireds"] = 432 await hass.services.async_call( HA_DOMAIN, SERVICE_UPDATE_ENTITY, {ATTR_ENTITY_ID: [wemo_entity.entity_id]}, blocking=True, ) - assert hass.states.get(wemo_entity.entity_id).state == STATE_ON + state = hass.states.get(wemo_entity.entity_id) + assert state.attributes.get(ATTR_COLOR_TEMP) == 432 + assert state.state == STATE_ON # Off state. pywemo_bridge_light.state["onoff"] = 0 From 1f066a7b6fd75a9e8f0308049e3042f145f5d362 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 20 Dec 2021 00:15:28 +0000 Subject: [PATCH 0782/2644] [ci skip] Translation update --- .../azure_event_hub/translations/he.json | 12 ++++++++++ .../components/flux_led/translations/en.json | 3 ++- .../components/vicare/translations/he.json | 23 +++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/azure_event_hub/translations/he.json create mode 100644 homeassistant/components/vicare/translations/he.json diff --git a/homeassistant/components/azure_event_hub/translations/he.json b/homeassistant/components/azure_event_hub/translations/he.json new file mode 100644 index 00000000000..cf4b00f2264 --- /dev/null +++ b/homeassistant/components/azure_event_hub/translations/he.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8", + "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": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flux_led/translations/en.json b/homeassistant/components/flux_led/translations/en.json index 7eda5cf0baf..9a988408c30 100644 --- a/homeassistant/components/flux_led/translations/en.json +++ b/homeassistant/components/flux_led/translations/en.json @@ -27,7 +27,8 @@ "data": { "custom_effect_colors": "Custom Effect: List of 1 to 16 [R,G,B] colors. Example: [255,0,255],[60,128,0]", "custom_effect_speed_pct": "Custom Effect: Speed in percents for the effect that switch colors.", - "custom_effect_transition": "Custom Effect: Type of transition between the colors." + "custom_effect_transition": "Custom Effect: Type of transition between the colors.", + "mode": "The chosen brightness mode." } } } diff --git a/homeassistant/components/vicare/translations/he.json b/homeassistant/components/vicare/translations/he.json new file mode 100644 index 00000000000..db5a392f540 --- /dev/null +++ b/homeassistant/components/vicare/translations/he.json @@ -0,0 +1,23 @@ +{ + "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.", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "error": { + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "client_id": "\u05de\u05e4\u05ea\u05d7 API", + "name": "\u05e9\u05dd", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05d3\u05d5\u05d0\"\u05dc" + }, + "title": "{name}" + } + } + } +} \ No newline at end of file From 6ae7b928ea96c23a3f5a3d5f3f563ef381255a57 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 19 Dec 2021 19:42:37 -0800 Subject: [PATCH 0783/2644] Add a camera specific logger to help diagnose stream errors (#61647) * Add a camera specific logger to help diagnose stream errors Add a camera specific logger to help users associate stream errors with a particular camera. Issue #54659 * Apply code review feedback * Update package name based on manual testing --- homeassistant/components/camera/__init__.py | 7 +++- homeassistant/components/stream/__init__.py | 42 +++++++++++++++------ tests/components/stream/test_worker.py | 6 +-- 3 files changed, 40 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 5d170eece4c..286c7957169 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -462,7 +462,12 @@ class Camera(Entity): source = await self.stream_source() if not source: return None - self.stream = create_stream(self.hass, source, options=self.stream_options) + self.stream = create_stream( + self.hass, + source, + options=self.stream_options, + stream_label=self.entity_id, + ) self.stream.set_update_callback(self.async_write_ha_state) return self.stream diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 2d2e4058460..0556bc2c7a9 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -69,12 +69,17 @@ def redact_credentials(data: str) -> str: def create_stream( - hass: HomeAssistant, stream_source: str, options: dict[str, str] + hass: HomeAssistant, + stream_source: str, + options: dict[str, str], + stream_label: str | None = None, ) -> Stream: """Create a stream with the specified identfier based on the source url. - The stream_source is typically an rtsp url and options are passed into - pyav / ffmpeg as options. + The stream_source is typically an rtsp url (though any url accepted by ffmpeg is fine) and + options are passed into pyav / ffmpeg as options. + + The stream_label is a string used as an additional message in logging. """ if DOMAIN not in hass.config.components: raise HomeAssistantError("Stream integration is not set up.") @@ -87,7 +92,7 @@ def create_stream( **options, } - stream = Stream(hass, stream_source, options=options) + stream = Stream(hass, stream_source, options=options, stream_label=stream_label) hass.data[DOMAIN][ATTR_STREAMS].append(stream) return stream @@ -192,12 +197,17 @@ class Stream: """Represents a single stream.""" def __init__( - self, hass: HomeAssistant, source: str, options: dict[str, str] + self, + hass: HomeAssistant, + source: str, + options: dict[str, str], + stream_label: str | None = None, ) -> None: """Initialize a stream.""" self.hass = hass self.source = source self.options = options + self._stream_label = stream_label self.keepalive = False self.access_token: str | None = None self._thread: threading.Thread | None = None @@ -206,6 +216,11 @@ class Stream: self._fast_restart_once = False self._available: bool = True self._update_callback: Callable[[], None] | None = None + self._logger = ( + logging.getLogger(f"{__package__}.stream.{stream_label}") + if stream_label + else _LOGGER + ) def endpoint_url(self, fmt: str) -> str: """Start the stream and returns a url for the output format.""" @@ -285,11 +300,13 @@ class Stream: target=self._run_worker, ) self._thread.start() - _LOGGER.info("Started stream: %s", redact_credentials(str(self.source))) + self._logger.info( + "Started stream: %s", redact_credentials(str(self.source)) + ) def update_source(self, new_source: str) -> None: """Restart the stream with a new stream source.""" - _LOGGER.debug("Updating stream source %s", new_source) + self._logger.debug("Updating stream source %s", new_source) self.source = new_source self._fast_restart_once = True self._thread_quit.set() @@ -313,7 +330,8 @@ class Stream: self._thread_quit, ) except StreamWorkerError as err: - _LOGGER.error("Error from stream worker: %s", str(err)) + self._logger.error("Error from stream worker: %s", str(err)) + self._available = False stream_state.discontinuity() if not self.keepalive or self._thread_quit.is_set(): @@ -332,7 +350,7 @@ class Stream: if time.time() - start_time > STREAM_RESTART_RESET_TIME: wait_timeout = 0 wait_timeout += STREAM_RESTART_INCREMENT - _LOGGER.debug( + self._logger.debug( "Restarting stream worker in %d seconds: %s", wait_timeout, self.source, @@ -363,7 +381,9 @@ class Stream: self._thread_quit.set() self._thread.join() self._thread = None - _LOGGER.info("Stopped stream: %s", redact_credentials(str(self.source))) + self._logger.info( + "Stopped stream: %s", redact_credentials(str(self.source)) + ) async def async_record( self, video_path: str, duration: int = 30, lookback: int = 5 @@ -390,7 +410,7 @@ class Stream: recorder.video_path = video_path self.start() - _LOGGER.debug("Started a stream recording of %s seconds", duration) + self._logger.debug("Started a stream recording of %s seconds", duration) # Take advantage of lookback hls: HlsStreamOutput = cast(HlsStreamOutput, self.outputs().get(HLS_PROVIDER)) diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index 48144219994..f05b2ece829 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -724,7 +724,7 @@ async def test_durations(hass, record_worker_sync): ) source = generate_h264_video(duration=SEGMENT_DURATION + 1) - stream = create_stream(hass, source, {}) + stream = create_stream(hass, source, {}, stream_label="camera") # use record_worker_sync to grab output segments with patch.object(hass.config, "is_allowed_path", return_value=True): @@ -797,7 +797,7 @@ async def test_has_keyframe(hass, record_worker_sync, h264_video): }, ) - stream = create_stream(hass, h264_video, {}) + stream = create_stream(hass, h264_video, {}, stream_label="camera") # use record_worker_sync to grab output segments with patch.object(hass.config, "is_allowed_path", return_value=True): @@ -836,7 +836,7 @@ async def test_h265_video_is_hvc1(hass, record_worker_sync): ) source = generate_h265_video() - stream = create_stream(hass, source, {}) + stream = create_stream(hass, source, {}, stream_label="camera") # use record_worker_sync to grab output segments with patch.object(hass.config, "is_allowed_path", return_value=True): From d64e7b1dc42d09e37a42f3df85168456561054db Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 19 Dec 2021 22:33:36 -0600 Subject: [PATCH 0784/2644] Support Plex resuming and playback offset (#61468) --- homeassistant/components/plex/media_player.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 6a48a427519..42d8371004e 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -482,6 +482,8 @@ class PlexMediaPlayer(MediaPlayerEntity): if isinstance(src, int): src = {"plex_key": src} + offset = 0 + if playqueue_id := src.pop("playqueue_id", None): try: playqueue = self.plex_server.get_playqueue(playqueue_id) @@ -491,16 +493,21 @@ class PlexMediaPlayer(MediaPlayerEntity): ) from err else: shuffle = src.pop("shuffle", 0) + offset = src.pop("offset", 0) * 1000 + resume = src.pop("resume", False) media = self.plex_server.lookup_media(media_type, **src) if media is None: raise HomeAssistantError(f"Media could not be found: {media_id}") + if resume and not offset: + offset = media.viewOffset + _LOGGER.debug("Attempting to play %s on %s", media, self.name) playqueue = self.plex_server.create_playqueue(media, shuffle=shuffle) try: - self.device.playMedia(playqueue) + self.device.playMedia(playqueue, offset=offset) except requests.exceptions.ConnectTimeout as exc: raise HomeAssistantError( f"Request failed when playing on {self.name}" From f2e4613db5c1f829e7108e7a4d9c13688f9ab10b Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 20 Dec 2021 06:00:55 +0100 Subject: [PATCH 0785/2644] Bump pyatmo to 6.2.1 (#62291) --- homeassistant/components/netatmo/climate.py | 11 +++++------ homeassistant/components/netatmo/manifest.json | 2 +- homeassistant/components/netatmo/select.py | 12 ++++-------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 12 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 1ead9d7cbdb..c8b5e01e5db 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -136,14 +136,13 @@ async def async_setup_entry( for home_id in climate_topology.home_ids: signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}" - try: - await data_handler.register_data_class( - CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id - ) - except KeyError: + await data_handler.register_data_class( + CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id + ) + + if (climate_state := data_handler.data[signal_name]) is None: continue - climate_state = data_handler.data[signal_name] climate_topology.register_handler(home_id, climate_state.process_topology) for room in climate_state.homes[home_id].rooms.values(): diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 501d5142bcc..4ee7aeb0fde 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -3,7 +3,7 @@ "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", "requirements": [ - "pyatmo==6.2.0" + "pyatmo==6.2.1" ], "after_dependencies": [ "cloud", diff --git a/homeassistant/components/netatmo/select.py b/homeassistant/components/netatmo/select.py index 98576497f3e..4d69c9ab853 100644 --- a/homeassistant/components/netatmo/select.py +++ b/homeassistant/components/netatmo/select.py @@ -49,17 +49,13 @@ async def async_setup_entry( for home_id in climate_topology.home_ids: signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}" - try: - await data_handler.register_data_class( - CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id - ) - except KeyError: - continue - await data_handler.register_data_class( CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id ) - climate_state = data_handler.data.get(signal_name) + + if (climate_state := data_handler.data[signal_name]) is None: + continue + climate_topology.register_handler(home_id, climate_state.process_topology) hass.data[DOMAIN][DATA_SCHEDULES][home_id] = climate_state.homes[ diff --git a/requirements_all.txt b/requirements_all.txt index 3cde5400641..dd0692ef59b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1372,7 +1372,7 @@ pyarlo==0.2.4 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==6.2.0 +pyatmo==6.2.1 # homeassistant.components.atome pyatome==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cfb9f5f8c76..0b594fb08c9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -841,7 +841,7 @@ pyarlo==0.2.4 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==6.2.0 +pyatmo==6.2.1 # homeassistant.components.apple_tv pyatv==0.9.8 From e8096e7f51b87c8ceb771f7400c1393d297da95a Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Sun, 19 Dec 2021 21:02:05 -0800 Subject: [PATCH 0786/2644] Bump pywemo==0.7.0 (#62360) --- homeassistant/components/wemo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/wemo/conftest.py | 2 +- tests/components/wemo/test_device_trigger.py | 10 +++++----- tests/components/wemo/test_wemo_device.py | 4 ++-- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json index 59eae24c714..d0643ed51a9 100644 --- a/homeassistant/components/wemo/manifest.json +++ b/homeassistant/components/wemo/manifest.json @@ -3,7 +3,7 @@ "name": "Belkin WeMo", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wemo", - "requirements": ["pywemo==0.6.7"], + "requirements": ["pywemo==0.7.0"], "ssdp": [ { "manufacturer": "Belkin International Inc." diff --git a/requirements_all.txt b/requirements_all.txt index dd0692ef59b..f25f09fdd31 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2019,7 +2019,7 @@ pyvolumio==0.1.3 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==0.6.7 +pywemo==0.7.0 # homeassistant.components.wilight pywilight==0.0.70 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0b594fb08c9..ba6e4e42632 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1218,7 +1218,7 @@ pyvolumio==0.1.3 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==0.6.7 +pywemo==0.7.0 # homeassistant.components.wilight pywilight==0.0.70 diff --git a/tests/components/wemo/conftest.py b/tests/components/wemo/conftest.py index 08abd140dac..13ec0cb2337 100644 --- a/tests/components/wemo/conftest.py +++ b/tests/components/wemo/conftest.py @@ -57,7 +57,7 @@ def pywemo_device_fixture(pywemo_registry, pywemo_model): device.port = MOCK_PORT device.name = MOCK_NAME device.serialnumber = MOCK_SERIAL_NUMBER - device.model_name = pywemo_model + device.model_name = pywemo_model.replace("LongPress", "") device.get_state.return_value = 0 # Default to Off device.supports_long_press.return_value = cls.supports_long_press() diff --git a/tests/components/wemo/test_device_trigger.py b/tests/components/wemo/test_device_trigger.py index 76016469b72..0ad7d95dd7a 100644 --- a/tests/components/wemo/test_device_trigger.py +++ b/tests/components/wemo/test_device_trigger.py @@ -3,7 +3,6 @@ import pytest from pywemo.subscribe import EVENT_TYPE_LONG_PRESS from homeassistant.components.automation import DOMAIN as AUTOMATION_DOMAIN -from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.wemo.const import DOMAIN, WEMO_SUBSCRIPTION_EVENT from homeassistant.const import ( CONF_DEVICE_ID, @@ -11,6 +10,7 @@ from homeassistant.const import ( CONF_ENTITY_ID, CONF_PLATFORM, CONF_TYPE, + Platform, ) from homeassistant.setup import async_setup_component @@ -26,8 +26,8 @@ DATA_MESSAGE = {"message": "service-called"} @pytest.fixture def pywemo_model(): - """Pywemo Dimmer models use the light platform (WemoDimmer class).""" - return "Dimmer" + """Pywemo LightSwitch models use the switch platform.""" + return "LightSwitchLongPress" async def setup_automation(hass, device_id, trigger_type): @@ -67,14 +67,14 @@ async def test_get_triggers(hass, wemo_entity): }, { CONF_DEVICE_ID: wemo_entity.device_id, - CONF_DOMAIN: LIGHT_DOMAIN, + CONF_DOMAIN: Platform.SWITCH, CONF_ENTITY_ID: wemo_entity.entity_id, CONF_PLATFORM: "device", CONF_TYPE: "turned_off", }, { CONF_DEVICE_ID: wemo_entity.device_id, - CONF_DOMAIN: LIGHT_DOMAIN, + CONF_DOMAIN: Platform.SWITCH, CONF_ENTITY_ID: wemo_entity.entity_id, CONF_PLATFORM: "device", CONF_TYPE: "turned_on", diff --git a/tests/components/wemo/test_wemo_device.py b/tests/components/wemo/test_wemo_device.py index e756e816a47..9ef9e6b5685 100644 --- a/tests/components/wemo/test_wemo_device.py +++ b/tests/components/wemo/test_wemo_device.py @@ -26,8 +26,8 @@ asyncio.set_event_loop_policy(runner.HassEventLoopPolicy(True)) @pytest.fixture def pywemo_model(): - """Pywemo Dimmer models use the light platform (WemoDimmer class).""" - return "Dimmer" + """Pywemo LightSwitch models use the switch platform.""" + return "LightSwitchLongPress" async def test_async_register_device_longpress_fails(hass, pywemo_device): From 70947b14a3d66c8cc2f9bda4d1237f9bf0c3b546 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 19 Dec 2021 23:04:45 -0600 Subject: [PATCH 0787/2644] Improve SSDP callback performance (#62359) --- homeassistant/components/ssdp/__init__.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index ea4740be77a..46cdb362a71 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -472,22 +472,24 @@ class Scanner: ) location = ssdp_device.location - info_desc = await self._async_get_description_dict(location) or {} + info_desc = None combined_headers = ssdp_device.combined_headers(dst) - info_with_desc = CaseInsensitiveDict(combined_headers, **info_desc) - callbacks = self._async_get_matching_callbacks(combined_headers) matching_domains: set[str] = set() # If there are no changes from a search, do not trigger a config flow if source != SsdpSource.SEARCH_ALIVE: + info_desc = await self._async_get_description_dict(location) or {} + assert isinstance(combined_headers, CaseInsensitiveDict) matching_domains = self.integration_matchers.async_matching_domains( - info_with_desc + CaseInsensitiveDict({**combined_headers.as_dict(), **info_desc}) ) if not callbacks and not matching_domains: return + if info_desc is None: + info_desc = await self._async_get_description_dict(location) or {} discovery_info = discovery_info_from_headers_and_description( combined_headers, info_desc ) @@ -565,7 +567,10 @@ def discovery_info_from_headers_and_description( """Convert headers and description to discovery_info.""" ssdp_usn = combined_headers["usn"] ssdp_st = combined_headers.get("st") - upnp_info = {**info_desc} + if isinstance(info_desc, CaseInsensitiveDict): + upnp_info = {**info_desc.as_dict()} + else: + upnp_info = {**info_desc} # Increase compatibility: depending on the message type, # either the ST (Search Target, from M-SEARCH messages) From f50dc10276e7f2cc053d6e377895cb814a34dbcc Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Sun, 19 Dec 2021 21:11:40 -0800 Subject: [PATCH 0788/2644] Use the Platform enum in wemo (#62153) --- homeassistant/components/wemo/__init__.py | 31 +++++++++------------- tests/components/wemo/test_fan.py | 5 ++-- tests/components/wemo/test_light_bridge.py | 6 ++--- tests/components/wemo/test_light_dimmer.py | 5 ++-- tests/components/wemo/test_switch.py | 5 ++-- 5 files changed, 22 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index 24cf7d66a0a..8d75b9bddae 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -9,13 +9,8 @@ import pywemo import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.fan import DOMAIN as FAN_DOMAIN -from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_DISCOVERY, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import CONF_DISCOVERY, EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -32,17 +27,17 @@ MAX_CONCURRENCY = 3 # Mapping from Wemo model_name to domain. WEMO_MODEL_DISPATCH = { - "Bridge": [LIGHT_DOMAIN], - "CoffeeMaker": [SWITCH_DOMAIN], - "Dimmer": [LIGHT_DOMAIN], - "Humidifier": [FAN_DOMAIN], - "Insight": [BINARY_SENSOR_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN], - "LightSwitch": [SWITCH_DOMAIN], - "Maker": [BINARY_SENSOR_DOMAIN, SWITCH_DOMAIN], - "Motion": [BINARY_SENSOR_DOMAIN], - "OutdoorPlug": [SWITCH_DOMAIN], - "Sensor": [BINARY_SENSOR_DOMAIN], - "Socket": [SWITCH_DOMAIN], + "Bridge": [Platform.LIGHT], + "CoffeeMaker": [Platform.SWITCH], + "Dimmer": [Platform.LIGHT], + "Humidifier": [Platform.FAN], + "Insight": [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH], + "LightSwitch": [Platform.SWITCH], + "Maker": [Platform.BINARY_SENSOR, Platform.SWITCH], + "Motion": [Platform.BINARY_SENSOR], + "OutdoorPlug": [Platform.SWITCH], + "Sensor": [Platform.BINARY_SENSOR], + "Socket": [Platform.SWITCH], } _LOGGER = logging.getLogger(__name__) @@ -155,7 +150,7 @@ class WemoDispatcher: return coordinator = await async_register_device(hass, self._config_entry, wemo) - for component in WEMO_MODEL_DISPATCH.get(wemo.model_name, [SWITCH_DOMAIN]): + for component in WEMO_MODEL_DISPATCH.get(wemo.model_name, [Platform.SWITCH]): # Three cases: # - First time we see component, we need to load it and initialize the backlog # - Component is being loaded, add to backlog diff --git a/tests/components/wemo/test_fan.py b/tests/components/wemo/test_fan.py index dc450311e6a..a00d5523678 100644 --- a/tests/components/wemo/test_fan.py +++ b/tests/components/wemo/test_fan.py @@ -3,14 +3,13 @@ import pytest from pywemo.exceptions import ActionException -from homeassistant.components.fan import DOMAIN as FAN_DOMAIN from homeassistant.components.homeassistant import ( DOMAIN as HA_DOMAIN, SERVICE_UPDATE_ENTITY, ) from homeassistant.components.wemo import fan from homeassistant.components.wemo.const import DOMAIN -from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, Platform from homeassistant.setup import async_setup_component from . import entity_test_helpers @@ -85,7 +84,7 @@ async def test_available_after_update( pywemo_device.set_state.side_effect = ActionException pywemo_device.get_state.return_value = 1 await entity_test_helpers.test_avaliable_after_update( - hass, pywemo_registry, pywemo_device, wemo_entity, FAN_DOMAIN + hass, pywemo_registry, pywemo_device, wemo_entity, Platform.FAN ) diff --git a/tests/components/wemo/test_light_bridge.py b/tests/components/wemo/test_light_bridge.py index 9f622a22c1d..1d316d93c8f 100644 --- a/tests/components/wemo/test_light_bridge.py +++ b/tests/components/wemo/test_light_bridge.py @@ -8,8 +8,8 @@ from homeassistant.components.homeassistant import ( DOMAIN as HA_DOMAIN, SERVICE_UPDATE_ENTITY, ) -from homeassistant.components.light import ATTR_COLOR_TEMP, DOMAIN as LIGHT_DOMAIN -from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.components.light import ATTR_COLOR_TEMP +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, Platform from homeassistant.setup import async_setup_component from . import entity_test_helpers @@ -76,7 +76,7 @@ async def test_available_after_update( pywemo_bridge_light.turn_on.side_effect = pywemo.exceptions.ActionException pywemo_bridge_light.state["onoff"] = 1 await entity_test_helpers.test_avaliable_after_update( - hass, pywemo_registry, pywemo_device, wemo_entity, LIGHT_DOMAIN + hass, pywemo_registry, pywemo_device, wemo_entity, Platform.LIGHT ) diff --git a/tests/components/wemo/test_light_dimmer.py b/tests/components/wemo/test_light_dimmer.py index 830eb6dbdf4..0460994aa86 100644 --- a/tests/components/wemo/test_light_dimmer.py +++ b/tests/components/wemo/test_light_dimmer.py @@ -7,8 +7,7 @@ from homeassistant.components.homeassistant import ( DOMAIN as HA_DOMAIN, SERVICE_UPDATE_ENTITY, ) -from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN -from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, Platform from homeassistant.setup import async_setup_component from . import entity_test_helpers @@ -41,7 +40,7 @@ async def test_available_after_update( pywemo_device.on.side_effect = ActionException pywemo_device.get_state.return_value = 1 await entity_test_helpers.test_avaliable_after_update( - hass, pywemo_registry, pywemo_device, wemo_entity, LIGHT_DOMAIN + hass, pywemo_registry, pywemo_device, wemo_entity, Platform.LIGHT ) diff --git a/tests/components/wemo/test_switch.py b/tests/components/wemo/test_switch.py index 1023498c792..963c662b124 100644 --- a/tests/components/wemo/test_switch.py +++ b/tests/components/wemo/test_switch.py @@ -7,8 +7,7 @@ from homeassistant.components.homeassistant import ( DOMAIN as HA_DOMAIN, SERVICE_UPDATE_ENTITY, ) -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, Platform from homeassistant.setup import async_setup_component from . import entity_test_helpers @@ -83,5 +82,5 @@ async def test_available_after_update( pywemo_device.on.side_effect = ActionException pywemo_device.get_state.return_value = 1 await entity_test_helpers.test_avaliable_after_update( - hass, pywemo_registry, pywemo_device, wemo_entity, SWITCH_DOMAIN + hass, pywemo_registry, pywemo_device, wemo_entity, Platform.SWITCH ) From 2d049e9b4a0faf288f3e44e42197e19d8169be0f Mon Sep 17 00:00:00 2001 From: Penny Wood Date: Mon, 20 Dec 2021 14:25:01 +0800 Subject: [PATCH 0789/2644] Update version of iZone library to add some bug fixes (#61548) --- homeassistant/components/izone/climate.py | 5 ----- homeassistant/components/izone/manifest.json | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py index b21089d8baf..2498a671219 100644 --- a/homeassistant/components/izone/climate.py +++ b/homeassistant/components/izone/climate.py @@ -525,11 +525,6 @@ class ZoneDevice(ClimateEntity): """Return True if entity is available.""" return self._controller.available - @property - def assumed_state(self) -> bool: - """Return True if unable to access real state of the entity.""" - return self._controller.assumed_state - @property def unique_id(self): """Return the ID of the controller device.""" diff --git a/homeassistant/components/izone/manifest.json b/homeassistant/components/izone/manifest.json index 82927fef795..9cdf30ad42b 100644 --- a/homeassistant/components/izone/manifest.json +++ b/homeassistant/components/izone/manifest.json @@ -2,11 +2,11 @@ "domain": "izone", "name": "iZone", "documentation": "https://www.home-assistant.io/integrations/izone", - "requirements": ["python-izone==1.1.8"], + "requirements": ["python-izone==1.2.3"], "codeowners": ["@Swamp-Ig"], "config_flow": true, "homekit": { "models": ["iZone"] }, - "iot_class": "local_push" + "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index f25f09fdd31..1307dd09d74 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1901,7 +1901,7 @@ python-gitlab==1.6.0 python-hpilo==4.3 # homeassistant.components.izone -python-izone==1.1.8 +python-izone==1.2.3 # homeassistant.components.joaoapps_join python-join-api==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ba6e4e42632..802e763f46c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1148,7 +1148,7 @@ python-ecobee-api==0.2.14 python-forecastio==1.4.0 # homeassistant.components.izone -python-izone==1.1.8 +python-izone==1.2.3 # homeassistant.components.juicenet python-juicenet==1.0.2 From 02ad5f37794343529fde0ce6767cb03db1444b2a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 19 Dec 2021 22:28:15 -0800 Subject: [PATCH 0790/2644] Bump voluptuous_serialize to 2.5.0 (#62363) --- homeassistant/package_constraints.txt | 2 +- requirements.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 216afd133e2..b80c8503656 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -29,7 +29,7 @@ pyyaml==6.0 requests==2.26.0 scapy==2.4.5 sqlalchemy==1.4.27 -voluptuous-serialize==2.4.0 +voluptuous-serialize==2.5.0 voluptuous==0.12.2 yarl==1.6.3 zeroconf==0.37.0 diff --git a/requirements.txt b/requirements.txt index 5832d0ea2d0..4c6af849ce8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,5 +21,5 @@ python-slugify==4.0.1 pyyaml==6.0 requests==2.26.0 voluptuous==0.12.2 -voluptuous-serialize==2.4.0 +voluptuous-serialize==2.5.0 yarl==1.6.3 diff --git a/setup.py b/setup.py index ee163bc79f4..270f5c58f58 100755 --- a/setup.py +++ b/setup.py @@ -53,7 +53,7 @@ REQUIRES = [ "pyyaml==6.0", "requests==2.26.0", "voluptuous==0.12.2", - "voluptuous-serialize==2.4.0", + "voluptuous-serialize==2.5.0", "yarl==1.6.3", ] From a5c39e6fe41d34d0c15be4fc9e7c7839227df10c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 20 Dec 2021 00:00:49 -0800 Subject: [PATCH 0791/2644] Improve evil genius labs error handling (#62365) --- .../evil_genius_labs/config_flow.py | 10 ++++++-- .../components/evil_genius_labs/strings.json | 1 + homeassistant/strings.json | 3 ++- .../evil_genius_labs/test_config_flow.py | 25 ++++++++++++++++++- 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/evil_genius_labs/config_flow.py b/homeassistant/components/evil_genius_labs/config_flow.py index f4f7b464904..744e0194ded 100644 --- a/homeassistant/components/evil_genius_labs/config_flow.py +++ b/homeassistant/components/evil_genius_labs/config_flow.py @@ -1,10 +1,12 @@ """Config flow for Evil Genius Labs integration.""" from __future__ import annotations +import asyncio import logging from typing import Any import aiohttp +import async_timeout import pyevilgenius import voluptuous as vol @@ -29,9 +31,11 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, ) try: - data = await hub.get_data() - info = await hub.get_info() + async with async_timeout.timeout(10): + data = await hub.get_data() + info = await hub.get_info() except aiohttp.ClientError as err: + _LOGGER.debug("Unable to connect: %s", err) raise CannotConnect from err return {"title": data["name"]["value"], "unique_id": info["wiFiChipId"]} @@ -60,6 +64,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: info = await validate_input(self.hass, user_input) + except asyncio.TimeoutError: + errors["base"] = "timeout" except CannotConnect: errors["base"] = "cannot_connect" except Exception: # pylint: disable=broad-except diff --git a/homeassistant/components/evil_genius_labs/strings.json b/homeassistant/components/evil_genius_labs/strings.json index 16c5de158a9..790e9a69c7f 100644 --- a/homeassistant/components/evil_genius_labs/strings.json +++ b/homeassistant/components/evil_genius_labs/strings.json @@ -9,6 +9,7 @@ }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "timeout": "[%key:common::config_flow::error::timeout_connect%]", "unknown": "[%key:common::config_flow::error::unknown%]" } } diff --git a/homeassistant/strings.json b/homeassistant/strings.json index 31693c5bba1..39c041bb464 100644 --- a/homeassistant/strings.json +++ b/homeassistant/strings.json @@ -56,7 +56,8 @@ "invalid_api_key": "Invalid API key", "invalid_auth": "Invalid authentication", "invalid_host": "Invalid hostname or IP address", - "unknown": "Unexpected error" + "unknown": "Unexpected error", + "timeout_connect": "Timeout establishing connection" }, "abort": { "single_instance_allowed": "Already configured. Only a single configuration possible.", diff --git a/tests/components/evil_genius_labs/test_config_flow.py b/tests/components/evil_genius_labs/test_config_flow.py index 55e207ba7e0..6a5d4ea816a 100644 --- a/tests/components/evil_genius_labs/test_config_flow.py +++ b/tests/components/evil_genius_labs/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Evil Genius Labs config flow.""" +import asyncio from unittest.mock import patch import aiohttp @@ -43,7 +44,7 @@ async def test_form(hass: HomeAssistant, data_fixture, info_fixture) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_cannot_connect(hass: HomeAssistant) -> None: +async def test_form_cannot_connect(hass: HomeAssistant, caplog) -> None: """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -62,6 +63,28 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: assert result2["type"] == RESULT_TYPE_FORM assert result2["errors"] == {"base": "cannot_connect"} + assert "Unable to connect" in caplog.text + + +async def test_form_timeout(hass: HomeAssistant) -> None: + """Test we handle timeout error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "pyevilgenius.EvilGeniusDevice.get_data", + side_effect=asyncio.TimeoutError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "timeout"} async def test_form_unknown(hass: HomeAssistant) -> None: From 31c0440b2584b43e8ee44738cf63f4d4622f121b Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 20 Dec 2021 03:46:33 -0500 Subject: [PATCH 0792/2644] Use enums in smartthings (#62198) --- .../components/smartthings/binary_sensor.py | 30 ++-- homeassistant/components/smartthings/cover.py | 10 +- .../components/smartthings/sensor.py | 138 ++++++++---------- 3 files changed, 79 insertions(+), 99 deletions(-) diff --git a/homeassistant/components/smartthings/binary_sensor.py b/homeassistant/components/smartthings/binary_sensor.py index 79b4176d9ea..f5ea0f823f1 100644 --- a/homeassistant/components/smartthings/binary_sensor.py +++ b/homeassistant/components/smartthings/binary_sensor.py @@ -6,16 +6,10 @@ from collections.abc import Sequence from pysmartthings import Attribute, Capability from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_MOISTURE, - DEVICE_CLASS_MOTION, - DEVICE_CLASS_MOVING, - DEVICE_CLASS_OPENING, - DEVICE_CLASS_PRESENCE, - DEVICE_CLASS_PROBLEM, - DEVICE_CLASS_SOUND, + BinarySensorDeviceClass, BinarySensorEntity, ) -from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC +from homeassistant.helpers.entity import EntityCategory from . import SmartThingsEntity from .const import DATA_BROKERS, DOMAIN @@ -32,18 +26,18 @@ CAPABILITY_TO_ATTRIB = { Capability.water_sensor: Attribute.water, } ATTRIB_TO_CLASS = { - Attribute.acceleration: DEVICE_CLASS_MOVING, - Attribute.contact: DEVICE_CLASS_OPENING, - Attribute.filter_status: DEVICE_CLASS_PROBLEM, - Attribute.motion: DEVICE_CLASS_MOTION, - Attribute.presence: DEVICE_CLASS_PRESENCE, - Attribute.sound: DEVICE_CLASS_SOUND, - Attribute.tamper: DEVICE_CLASS_PROBLEM, - Attribute.valve: DEVICE_CLASS_OPENING, - Attribute.water: DEVICE_CLASS_MOISTURE, + Attribute.acceleration: BinarySensorDeviceClass.MOVING, + Attribute.contact: BinarySensorDeviceClass.OPENING, + Attribute.filter_status: BinarySensorDeviceClass.PROBLEM, + Attribute.motion: BinarySensorDeviceClass.MOTION, + Attribute.presence: BinarySensorDeviceClass.PRESENCE, + Attribute.sound: BinarySensorDeviceClass.SOUND, + Attribute.tamper: BinarySensorDeviceClass.PROBLEM, + Attribute.valve: BinarySensorDeviceClass.OPENING, + Attribute.water: BinarySensorDeviceClass.MOISTURE, } ATTRIB_TO_ENTTIY_CATEGORY = { - Attribute.tamper: ENTITY_CATEGORY_DIAGNOSTIC, + Attribute.tamper: EntityCategory.DIAGNOSTIC, } diff --git a/homeassistant/components/smartthings/cover.py b/homeassistant/components/smartthings/cover.py index 66715edfe60..269c1476836 100644 --- a/homeassistant/components/smartthings/cover.py +++ b/homeassistant/components/smartthings/cover.py @@ -7,9 +7,6 @@ from pysmartthings import Attribute, Capability from homeassistant.components.cover import ( ATTR_POSITION, - DEVICE_CLASS_DOOR, - DEVICE_CLASS_GARAGE, - DEVICE_CLASS_SHADE, DOMAIN as COVER_DOMAIN, STATE_CLOSED, STATE_CLOSING, @@ -18,6 +15,7 @@ from homeassistant.components.cover import ( SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION, + CoverDeviceClass, CoverEntity, ) from homeassistant.const import ATTR_BATTERY_LEVEL @@ -103,13 +101,13 @@ class SmartThingsCover(SmartThingsEntity, CoverEntity): """Update the attrs of the cover.""" value = None if Capability.door_control in self._device.capabilities: - self._device_class = DEVICE_CLASS_DOOR + self._device_class = CoverDeviceClass.DOOR value = self._device.status.door elif Capability.window_shade in self._device.capabilities: - self._device_class = DEVICE_CLASS_SHADE + self._device_class = CoverDeviceClass.SHADE value = self._device.status.window_shade elif Capability.garage_door_control in self._device.capabilities: - self._device_class = DEVICE_CLASS_GARAGE + self._device_class = CoverDeviceClass.GARAGE value = self._device.status.door self._state = VALUE_TO_STATE.get(value) diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index fa749e07dfb..4ec03588600 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -8,28 +8,15 @@ from pysmartthings import Attribute, Capability from pysmartthings.device import DeviceEntity from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, + SensorStateClass, ) from homeassistant.const import ( AREA_SQUARE_METERS, CONCENTRATION_PARTS_PER_MILLION, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_CO, - DEVICE_CLASS_CO2, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_POWER, - DEVICE_CLASS_SIGNAL_STRENGTH, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_TIMESTAMP, - DEVICE_CLASS_VOLTAGE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, - ENTITY_CATEGORY_CONFIG, - ENTITY_CATEGORY_DIAGNOSTIC, LIGHT_LUX, MASS_KILOGRAMS, PERCENTAGE, @@ -38,6 +25,7 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, VOLUME_CUBIC_METERS, ) +from homeassistant.helpers.entity import EntityCategory from homeassistant.util import dt as dt_util from . import SmartThingsEntity @@ -55,7 +43,7 @@ CAPABILITY_TO_SENSORS = { None, None, None, - ENTITY_CATEGORY_CONFIG, + EntityCategory.CONFIG, ) ], Capability.air_conditioner_mode: [ @@ -65,7 +53,7 @@ CAPABILITY_TO_SENSORS = { None, None, None, - ENTITY_CATEGORY_CONFIG, + EntityCategory.CONFIG, ) ], Capability.air_quality_sensor: [ @@ -74,7 +62,7 @@ CAPABILITY_TO_SENSORS = { "Air Quality", "CAQI", None, - STATE_CLASS_MEASUREMENT, + SensorStateClass.MEASUREMENT, None, ) ], @@ -87,9 +75,9 @@ CAPABILITY_TO_SENSORS = { Attribute.battery, "Battery", PERCENTAGE, - DEVICE_CLASS_BATTERY, + SensorDeviceClass.BATTERY, None, - ENTITY_CATEGORY_DIAGNOSTIC, + EntityCategory.DIAGNOSTIC, ) ], Capability.body_mass_index_measurement: [ @@ -98,7 +86,7 @@ CAPABILITY_TO_SENSORS = { "Body Mass Index", f"{MASS_KILOGRAMS}/{AREA_SQUARE_METERS}", None, - STATE_CLASS_MEASUREMENT, + SensorStateClass.MEASUREMENT, None, ) ], @@ -108,7 +96,7 @@ CAPABILITY_TO_SENSORS = { "Body Weight", MASS_KILOGRAMS, None, - STATE_CLASS_MEASUREMENT, + SensorStateClass.MEASUREMENT, None, ) ], @@ -117,8 +105,8 @@ CAPABILITY_TO_SENSORS = { Attribute.carbon_dioxide, "Carbon Dioxide Measurement", CONCENTRATION_PARTS_PER_MILLION, - DEVICE_CLASS_CO2, - STATE_CLASS_MEASUREMENT, + SensorDeviceClass.CO2, + SensorStateClass.MEASUREMENT, None, ) ], @@ -137,8 +125,8 @@ CAPABILITY_TO_SENSORS = { Attribute.carbon_monoxide_level, "Carbon Monoxide Measurement", CONCENTRATION_PARTS_PER_MILLION, - DEVICE_CLASS_CO, - STATE_CLASS_MEASUREMENT, + SensorDeviceClass.CO, + SensorStateClass.MEASUREMENT, None, ) ], @@ -158,7 +146,7 @@ CAPABILITY_TO_SENSORS = { Attribute.completion_time, "Dishwasher Completion Time", None, - DEVICE_CLASS_TIMESTAMP, + SensorDeviceClass.TIMESTAMP, None, None, ), @@ -170,7 +158,7 @@ CAPABILITY_TO_SENSORS = { None, None, None, - ENTITY_CATEGORY_CONFIG, + EntityCategory.CONFIG, ) ], Capability.dryer_operating_state: [ @@ -180,7 +168,7 @@ CAPABILITY_TO_SENSORS = { Attribute.completion_time, "Dryer Completion Time", None, - DEVICE_CLASS_TIMESTAMP, + SensorDeviceClass.TIMESTAMP, None, None, ), @@ -191,7 +179,7 @@ CAPABILITY_TO_SENSORS = { "Fine Dust Level", None, None, - STATE_CLASS_MEASUREMENT, + SensorStateClass.MEASUREMENT, None, ), Map( @@ -199,7 +187,7 @@ CAPABILITY_TO_SENSORS = { "Dust Level", None, None, - STATE_CLASS_MEASUREMENT, + SensorStateClass.MEASUREMENT, None, ), ], @@ -208,8 +196,8 @@ CAPABILITY_TO_SENSORS = { Attribute.energy, "Energy Meter", ENERGY_KILO_WATT_HOUR, - DEVICE_CLASS_ENERGY, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass.ENERGY, + SensorStateClass.TOTAL_INCREASING, None, ) ], @@ -219,7 +207,7 @@ CAPABILITY_TO_SENSORS = { "Equivalent Carbon Dioxide Measurement", CONCENTRATION_PARTS_PER_MILLION, None, - STATE_CLASS_MEASUREMENT, + SensorStateClass.MEASUREMENT, None, ) ], @@ -229,7 +217,7 @@ CAPABILITY_TO_SENSORS = { "Formaldehyde Measurement", CONCENTRATION_PARTS_PER_MILLION, None, - STATE_CLASS_MEASUREMENT, + SensorStateClass.MEASUREMENT, None, ) ], @@ -239,7 +227,7 @@ CAPABILITY_TO_SENSORS = { "Gas Meter", ENERGY_KILO_WATT_HOUR, None, - STATE_CLASS_MEASUREMENT, + SensorStateClass.MEASUREMENT, None, ), Map( @@ -249,7 +237,7 @@ CAPABILITY_TO_SENSORS = { Attribute.gas_meter_time, "Gas Meter Time", None, - DEVICE_CLASS_TIMESTAMP, + SensorDeviceClass.TIMESTAMP, None, None, ), @@ -258,7 +246,7 @@ CAPABILITY_TO_SENSORS = { "Gas Meter Volume", VOLUME_CUBIC_METERS, None, - STATE_CLASS_MEASUREMENT, + SensorStateClass.MEASUREMENT, None, ), ], @@ -267,8 +255,8 @@ CAPABILITY_TO_SENSORS = { Attribute.illuminance, "Illuminance", LIGHT_LUX, - DEVICE_CLASS_ILLUMINANCE, - STATE_CLASS_MEASUREMENT, + SensorDeviceClass.ILLUMINANCE, + SensorStateClass.MEASUREMENT, None, ) ], @@ -278,7 +266,7 @@ CAPABILITY_TO_SENSORS = { "Infrared Level", PERCENTAGE, None, - STATE_CLASS_MEASUREMENT, + SensorStateClass.MEASUREMENT, None, ) ], @@ -313,7 +301,7 @@ CAPABILITY_TO_SENSORS = { None, None, None, - ENTITY_CATEGORY_CONFIG, + EntityCategory.CONFIG, ) ], Capability.oven_operating_state: [ @@ -330,8 +318,8 @@ CAPABILITY_TO_SENSORS = { Attribute.power, "Power Meter", POWER_WATT, - DEVICE_CLASS_POWER, - STATE_CLASS_MEASUREMENT, + SensorDeviceClass.POWER, + SensorStateClass.MEASUREMENT, None, ) ], @@ -342,7 +330,7 @@ CAPABILITY_TO_SENSORS = { None, None, None, - ENTITY_CATEGORY_DIAGNOSTIC, + EntityCategory.DIAGNOSTIC, ) ], Capability.refrigeration_setpoint: [ @@ -350,7 +338,7 @@ CAPABILITY_TO_SENSORS = { Attribute.refrigeration_setpoint, "Refrigeration Setpoint", None, - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, None, None, ) @@ -360,8 +348,8 @@ CAPABILITY_TO_SENSORS = { Attribute.humidity, "Relative Humidity Measurement", PERCENTAGE, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + SensorDeviceClass.HUMIDITY, + SensorStateClass.MEASUREMENT, None, ) ], @@ -372,7 +360,7 @@ CAPABILITY_TO_SENSORS = { None, None, None, - ENTITY_CATEGORY_CONFIG, + EntityCategory.CONFIG, ) ], Capability.robot_cleaner_movement: [ @@ -392,7 +380,7 @@ CAPABILITY_TO_SENSORS = { None, None, None, - ENTITY_CATEGORY_CONFIG, + EntityCategory.CONFIG, ) ], Capability.signal_strength: [ @@ -401,16 +389,16 @@ CAPABILITY_TO_SENSORS = { "LQI Signal Strength", None, None, - STATE_CLASS_MEASUREMENT, - ENTITY_CATEGORY_DIAGNOSTIC, + SensorStateClass.MEASUREMENT, + EntityCategory.DIAGNOSTIC, ), Map( Attribute.rssi, "RSSI Signal Strength", None, - DEVICE_CLASS_SIGNAL_STRENGTH, - STATE_CLASS_MEASUREMENT, - ENTITY_CATEGORY_DIAGNOSTIC, + SensorDeviceClass.SIGNAL_STRENGTH, + SensorStateClass.MEASUREMENT, + EntityCategory.DIAGNOSTIC, ), ], Capability.smoke_detector: [ @@ -421,8 +409,8 @@ CAPABILITY_TO_SENSORS = { Attribute.temperature, "Temperature Measurement", None, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + SensorDeviceClass.TEMPERATURE, + SensorStateClass.MEASUREMENT, None, ) ], @@ -431,7 +419,7 @@ CAPABILITY_TO_SENSORS = { Attribute.cooling_setpoint, "Thermostat Cooling Setpoint", None, - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, None, None, ) @@ -443,7 +431,7 @@ CAPABILITY_TO_SENSORS = { None, None, None, - ENTITY_CATEGORY_CONFIG, + EntityCategory.CONFIG, ) ], Capability.thermostat_heating_setpoint: [ @@ -451,9 +439,9 @@ CAPABILITY_TO_SENSORS = { Attribute.heating_setpoint, "Thermostat Heating Setpoint", None, - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, None, - ENTITY_CATEGORY_CONFIG, + EntityCategory.CONFIG, ) ], Capability.thermostat_mode: [ @@ -463,7 +451,7 @@ CAPABILITY_TO_SENSORS = { None, None, None, - ENTITY_CATEGORY_CONFIG, + EntityCategory.CONFIG, ) ], Capability.thermostat_operating_state: [ @@ -481,9 +469,9 @@ CAPABILITY_TO_SENSORS = { Attribute.thermostat_setpoint, "Thermostat Setpoint", None, - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass.TEMPERATURE, None, - ENTITY_CATEGORY_CONFIG, + EntityCategory.CONFIG, ) ], Capability.three_axis: [], @@ -497,7 +485,7 @@ CAPABILITY_TO_SENSORS = { "Tvoc Measurement", CONCENTRATION_PARTS_PER_MILLION, None, - STATE_CLASS_MEASUREMENT, + SensorStateClass.MEASUREMENT, None, ) ], @@ -507,7 +495,7 @@ CAPABILITY_TO_SENSORS = { "Ultraviolet Index", None, None, - STATE_CLASS_MEASUREMENT, + SensorStateClass.MEASUREMENT, None, ) ], @@ -516,8 +504,8 @@ CAPABILITY_TO_SENSORS = { Attribute.voltage, "Voltage Measurement", ELECTRIC_POTENTIAL_VOLT, - DEVICE_CLASS_VOLTAGE, - STATE_CLASS_MEASUREMENT, + SensorDeviceClass.VOLTAGE, + SensorStateClass.MEASUREMENT, None, ) ], @@ -528,7 +516,7 @@ CAPABILITY_TO_SENSORS = { None, None, None, - ENTITY_CATEGORY_CONFIG, + EntityCategory.CONFIG, ) ], Capability.washer_operating_state: [ @@ -538,7 +526,7 @@ CAPABILITY_TO_SENSORS = { Attribute.completion_time, "Washer Completion Time", None, - DEVICE_CLASS_TIMESTAMP, + SensorDeviceClass.TIMESTAMP, None, None, ), @@ -659,7 +647,7 @@ class SmartThingsSensor(SmartThingsEntity, SensorEntity): """Return the state of the sensor.""" value = self._device.status.attributes[self._attribute].value - if self._device_class != DEVICE_CLASS_TIMESTAMP: + if self._device_class != SensorDeviceClass.TIMESTAMP: return value return dt_util.parse_datetime(value) @@ -715,9 +703,9 @@ class SmartThingsPowerConsumptionSensor(SmartThingsEntity, SensorEntity): """Init the class.""" super().__init__(device) self.report_name = report_name - self._attr_state_class = STATE_CLASS_MEASUREMENT + self._attr_state_class = SensorStateClass.MEASUREMENT if self.report_name != "power": - self._attr_state_class = STATE_CLASS_TOTAL_INCREASING + self._attr_state_class = SensorStateClass.TOTAL_INCREASING @property def name(self) -> str: @@ -743,8 +731,8 @@ class SmartThingsPowerConsumptionSensor(SmartThingsEntity, SensorEntity): def device_class(self): """Return the device class of the sensor.""" if self.report_name == "power": - return DEVICE_CLASS_POWER - return DEVICE_CLASS_ENERGY + return SensorDeviceClass.POWER + return SensorDeviceClass.ENERGY @property def native_unit_of_measurement(self): From 030a2c4de2b5ec2609cdbfa49867cceb32ff038c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 20 Dec 2021 12:31:16 +0100 Subject: [PATCH 0793/2644] Invalidate CI cache by bumping caching version (#62383) --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6e3ed202559..8f9822d2650 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -10,7 +10,7 @@ on: pull_request: ~ env: - CACHE_VERSION: 4 + CACHE_VERSION: 5 DEFAULT_PYTHON: 3.8 PRE_COMMIT_CACHE: ~/.cache/pre-commit SQLALCHEMY_WARN_20: 1 From d471e7e1110d111a2f02ea077341e44719defc3d Mon Sep 17 00:00:00 2001 From: Thomas Dietrich Date: Mon, 20 Dec 2021 13:04:30 +0100 Subject: [PATCH 0794/2644] Fix typo in Kostal Plenticore integration entity (#62380) --- homeassistant/components/kostal_plenticore/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/kostal_plenticore/const.py b/homeassistant/components/kostal_plenticore/const.py index d4e1fa47a8b..594ed9bcbfb 100644 --- a/homeassistant/components/kostal_plenticore/const.py +++ b/homeassistant/components/kostal_plenticore/const.py @@ -723,7 +723,7 @@ SWITCH_SETTINGS_DATA = [ SwitchData( "devices:local", "Battery:Strategy", - "Battery Strategy:", + "Battery Strategy", "1", "1", "Automatic", From 3d75befe0a986d3905aaee5032964f9f9523c94d Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 20 Dec 2021 05:35:45 -0700 Subject: [PATCH 0795/2644] Replace SimpliSafe logged errors with `HomeAssistantError` in service handlers (#62352) --- .../components/simplisafe/__init__.py | 13 ++++++++---- .../simplisafe/alarm_control_panel.py | 20 +++++++++---------- homeassistant/components/simplisafe/lock.py | 11 ++++++---- 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 025c3a3ae49..b188b7309d8 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -55,7 +55,11 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import CoreState, Event, HomeAssistant, ServiceCall, callback -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryNotReady, + HomeAssistantError, +) from homeassistant.helpers import ( aiohttp_client, config_validation as cv, @@ -368,7 +372,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await func(call, system) except SimplipyError as err: - LOGGER.error("Error while executing %s: %s", func.__name__, err) + raise HomeAssistantError( + f'Error while executing "{call.service}": {err}' + ) from err return wrapper @@ -397,8 +403,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) -> None: """Set one or more system parameters.""" if not isinstance(system, SystemV3): - LOGGER.error("Can only set system properties on V3 systems") - return + raise HomeAssistantError("Can only set system properties on V3 systems") await system.async_set_properties( {prop: value for prop, value in call.data.items() if prop != ATTR_DEVICE_ID} diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index ac3d4721ccb..43bcaee2059 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -41,6 +41,7 @@ from homeassistant.const import ( STATE_ALARM_TRIGGERED, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import SimpliSafe, SimpliSafeEntity @@ -173,8 +174,9 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): try: await self._system.async_set_off() except SimplipyError as err: - LOGGER.error('Error while disarming "%s": %s', self._system.system_id, err) - return + raise HomeAssistantError( + f'Error while disarming "{self._system.system_id}": {err}' + ) from err self._attr_state = STATE_ALARM_DISARMED self.async_write_ha_state() @@ -187,10 +189,9 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): try: await self._system.async_set_home() except SimplipyError as err: - LOGGER.error( - 'Error while arming "%s" (home): %s', self._system.system_id, err - ) - return + raise HomeAssistantError( + f'Error while arming (home) "{self._system.system_id}": {err}' + ) from err self._attr_state = STATE_ALARM_ARMED_HOME self.async_write_ha_state() @@ -203,10 +204,9 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): try: await self._system.async_set_away() except SimplipyError as err: - LOGGER.error( - 'Error while arming "%s" (away): %s', self._system.system_id, err - ) - return + raise HomeAssistantError( + f'Error while arming (away) "{self._system.system_id}": {err}' + ) from err self._attr_state = STATE_ALARM_ARMING self.async_write_ha_state() diff --git a/homeassistant/components/simplisafe/lock.py b/homeassistant/components/simplisafe/lock.py index 435b60af44b..14816cdd579 100644 --- a/homeassistant/components/simplisafe/lock.py +++ b/homeassistant/components/simplisafe/lock.py @@ -11,6 +11,7 @@ from simplipy.websocket import EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED, Websocket from homeassistant.components.lock import LockEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import SimpliSafe, SimpliSafeEntity @@ -64,8 +65,9 @@ class SimpliSafeLock(SimpliSafeEntity, LockEntity): try: await self._device.async_lock() except SimplipyError as err: - LOGGER.error('Error while locking "%s": %s', self._device.name, err) - return + raise HomeAssistantError( + f'Error while locking "{self._device.name}": {err}' + ) from err self._attr_is_locked = True self.async_write_ha_state() @@ -75,8 +77,9 @@ class SimpliSafeLock(SimpliSafeEntity, LockEntity): try: await self._device.async_unlock() except SimplipyError as err: - LOGGER.error('Error while unlocking "%s": %s', self._device.name, err) - return + raise HomeAssistantError( + f'Error while unlocking "{self._device.name}": {err}' + ) from err self._attr_is_locked = False self.async_write_ha_state() From abc7dcf6bf45f5266a350ee78b5d63f0054ebb77 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 20 Dec 2021 06:45:34 -0600 Subject: [PATCH 0796/2644] Add zones support to flux_led (#61072) --- homeassistant/components/flux_led/const.py | 1 + homeassistant/components/flux_led/light.py | 39 +++++++++++++++++- .../components/flux_led/services.yaml | 41 +++++++++++++++++++ homeassistant/components/flux_led/util.py | 11 ++++- tests/components/flux_led/test_light.py | 32 +++++++++++++++ 5 files changed, 122 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/flux_led/const.py b/homeassistant/components/flux_led/const.py index be100e4141c..430c5a0e38a 100644 --- a/homeassistant/components/flux_led/const.py +++ b/homeassistant/components/flux_led/const.py @@ -62,6 +62,7 @@ TRANSITION_STROBE: Final = "strobe" CONF_COLORS: Final = "colors" CONF_SPEED_PCT: Final = "speed_pct" CONF_TRANSITION: Final = "transition" +CONF_EFFECT: Final = "effect" EFFECT_SPEED_SUPPORT_MODES: Final = {COLOR_MODE_RGB, COLOR_MODE_RGBW, COLOR_MODE_RGBWW} diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index 58d98bf2462..6bde2ec3b31 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -5,6 +5,7 @@ import ast import logging from typing import Any, Final +from flux_led.const import MultiColorEffects from flux_led.utils import ( color_temp_to_white_levels, rgbcw_brightness, @@ -43,6 +44,7 @@ from .const import ( CONF_CUSTOM_EFFECT_COLORS, CONF_CUSTOM_EFFECT_SPEED_PCT, CONF_CUSTOM_EFFECT_TRANSITION, + CONF_EFFECT, CONF_SPEED_PCT, CONF_TRANSITION, DEFAULT_EFFECT_SPEED, @@ -53,7 +55,12 @@ from .const import ( ) from .coordinator import FluxLedUpdateCoordinator from .entity import FluxOnOffEntity -from .util import _effect_brightness, _flux_color_mode_to_hass, _hass_color_modes +from .util import ( + _effect_brightness, + _flux_color_mode_to_hass, + _hass_color_modes, + _str_to_multi_color_effect, +) _LOGGER = logging.getLogger(__name__) @@ -73,6 +80,7 @@ COLOR_TEMP_WARM_VS_COLD_WHITE_CUT_OFF: Final = 285 EFFECT_CUSTOM: Final = "custom" SERVICE_CUSTOM_EFFECT: Final = "set_custom_effect" +SERVICE_SET_ZONES: Final = "set_zones" CUSTOM_EFFECT_DICT: Final = { vol.Required(CONF_COLORS): vol.All( @@ -88,6 +96,20 @@ CUSTOM_EFFECT_DICT: Final = { ), } +SET_ZONES_DICT: Final = { + vol.Required(CONF_COLORS): vol.All( + cv.ensure_list, + vol.Length(min=1, max=2048), + [vol.All(vol.Coerce(tuple), vol.ExactSequence((cv.byte, cv.byte, cv.byte)))], + ), + vol.Optional(CONF_SPEED_PCT, default=50): vol.All( + vol.Range(min=0, max=100), vol.Coerce(int) + ), + vol.Optional(CONF_EFFECT, default=MultiColorEffects.STATIC.name.lower()): vol.All( + cv.string, vol.In([effect.name.lower() for effect in MultiColorEffects]) + ), +} + async def async_setup_entry( hass: HomeAssistant, @@ -103,6 +125,11 @@ async def async_setup_entry( CUSTOM_EFFECT_DICT, "async_set_custom_effect", ) + platform.async_register_entity_service( + SERVICE_SET_ZONES, + SET_ZONES_DICT, + "async_set_zones", + ) options = entry.options try: @@ -293,3 +320,13 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity): speed_pct, transition, ) + + async def async_set_zones( + self, colors: list[tuple[int, int, int]], speed_pct: int, effect: str + ) -> None: + """Set a colors for zones.""" + await self._device.async_set_zones( + colors, + speed_pct, + _str_to_multi_color_effect(effect), + ) diff --git a/homeassistant/components/flux_led/services.yaml b/homeassistant/components/flux_led/services.yaml index f1dae55560b..ff1e0679807 100644 --- a/homeassistant/components/flux_led/services.yaml +++ b/homeassistant/components/flux_led/services.yaml @@ -36,3 +36,44 @@ set_custom_effect: - "gradual" - "jump" - "strobe" +set_zones: + description: Set strip zones for Addressable v3 controllers (0xA3). + target: + entity: + integration: flux_led + domain: light + fields: + colors: + description: List of colors for each zone (RGB). The length of each zone is the number of pixels per segment divided by the number of colors. (Max 2048 Colors) + example: | + - [255,0,0] + - [0,255,0] + - [0,0,255] + - [255,255,255] + required: true + selector: + object: + speed_pct: + description: Effect speed for the custom effect (0-100) + example: 80 + default: 50 + required: false + selector: + number: + min: 1 + step: 1 + max: 100 + unit_of_measurement: "%" + effect: + description: Effect + example: 'running_water' + default: 'static' + required: false + selector: + select: + options: + - "static" + - "running_water" + - "strobe" + - "jump" + - "breathing" diff --git a/homeassistant/components/flux_led/util.py b/homeassistant/components/flux_led/util.py index 818b5c45506..8e1e387cafb 100644 --- a/homeassistant/components/flux_led/util.py +++ b/homeassistant/components/flux_led/util.py @@ -2,7 +2,7 @@ from __future__ import annotations from flux_led.aio import AIOWifiLedBulb -from flux_led.const import COLOR_MODE_DIM as FLUX_COLOR_MODE_DIM +from flux_led.const import COLOR_MODE_DIM as FLUX_COLOR_MODE_DIM, MultiColorEffects from homeassistant.components.light import ( COLOR_MODE_BRIGHTNESS, @@ -34,3 +34,12 @@ def _flux_color_mode_to_hass( def _effect_brightness(brightness: int) -> int: """Convert hass brightness to effect brightness.""" return round(brightness / 255 * 100) + + +def _str_to_multi_color_effect(effect_str: str) -> MultiColorEffects: + """Convert an multicolor effect string to MultiColorEffects.""" + for effect in MultiColorEffects: + if effect.name.lower() == effect_str: + return effect + # unreachable due to schema validation + assert False # pragma: no cover diff --git a/tests/components/flux_led/test_light.py b/tests/components/flux_led/test_light.py index 554a4eeedf8..cdf11a19a0b 100644 --- a/tests/components/flux_led/test_light.py +++ b/tests/components/flux_led/test_light.py @@ -10,6 +10,7 @@ from flux_led.const import ( COLOR_MODE_RGBW as FLUX_COLOR_MODE_RGBW, COLOR_MODE_RGBWW as FLUX_COLOR_MODE_RGBWW, COLOR_MODES_RGB_W as FLUX_COLOR_MODES_RGB_W, + MultiColorEffects, ) import pytest @@ -19,6 +20,7 @@ from homeassistant.components.flux_led.const import ( CONF_CUSTOM_EFFECT_COLORS, CONF_CUSTOM_EFFECT_SPEED_PCT, CONF_CUSTOM_EFFECT_TRANSITION, + CONF_EFFECT, CONF_SPEED_PCT, CONF_TRANSITION, DOMAIN, @@ -1119,6 +1121,36 @@ async def test_rgb_light_custom_effect_via_service( ) bulb.async_set_custom_pattern.reset_mock() + await hass.services.async_call( + DOMAIN, + "set_zones", + { + ATTR_ENTITY_ID: entity_id, + CONF_COLORS: [[0, 0, 255], [255, 0, 0]], + CONF_EFFECT: "running_water", + }, + blocking=True, + ) + bulb.async_set_zones.assert_called_with( + [(0, 0, 255), (255, 0, 0)], 50, MultiColorEffects.RUNNING_WATER + ) + bulb.async_set_zones.reset_mock() + + await hass.services.async_call( + DOMAIN, + "set_zones", + { + ATTR_ENTITY_ID: entity_id, + CONF_COLORS: [[0, 0, 255], [255, 0, 0]], + CONF_SPEED_PCT: 30, + }, + blocking=True, + ) + bulb.async_set_zones.assert_called_with( + [(0, 0, 255), (255, 0, 0)], 30, MultiColorEffects.STATIC + ) + bulb.async_set_zones.reset_mock() + async def test_addressable_light(hass: HomeAssistant) -> None: """Test an addressable light.""" From cabcb52fb3295c4f5cc9c330eb16f00993d85c49 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 20 Dec 2021 07:49:15 -0500 Subject: [PATCH 0797/2644] Remove deprecated yaml config from co2signal (#62343) --- .../components/co2signal/config_flow.py | 63 +---------- homeassistant/components/co2signal/const.py | 5 - homeassistant/components/co2signal/sensor.py | 29 +---- .../components/co2signal/test_config_flow.py | 104 ------------------ 4 files changed, 6 insertions(+), 195 deletions(-) diff --git a/homeassistant/components/co2signal/config_flow.py b/homeassistant/components/co2signal/config_flow.py index fb4e48c66e8..036282cb3e8 100644 --- a/homeassistant/components/co2signal/config_flow.py +++ b/homeassistant/components/co2signal/config_flow.py @@ -6,11 +6,11 @@ from typing import Any import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_TOKEN +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv -from . import APIRatelimitExceeded, CO2Error, InvalidAuth, get_data +from . import APIRatelimitExceeded, InvalidAuth, get_data from .const import CONF_COUNTRY_CODE, DOMAIN from .util import get_extra_name @@ -19,71 +19,12 @@ TYPE_SPECIFY_COORDINATES = "Specify coordinates" TYPE_SPECIFY_COUNTRY = "Specify country code" -def _get_entry_type(config: dict) -> str: - """Get entry type from the configuration.""" - if CONF_LATITUDE in config: - return TYPE_SPECIFY_COORDINATES - - if CONF_COUNTRY_CODE in config: - return TYPE_SPECIFY_COUNTRY - - return TYPE_USE_HOME - - class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Co2signal.""" VERSION = 1 _data: dict | None - async def async_step_import(self, import_info): - """Set the config entry up from yaml.""" - data = {CONF_API_KEY: import_info[CONF_TOKEN]} - - if CONF_COUNTRY_CODE in import_info: - data[CONF_COUNTRY_CODE] = import_info[CONF_COUNTRY_CODE] - new_entry_type = TYPE_SPECIFY_COUNTRY - elif ( - CONF_LATITUDE in import_info - and import_info[CONF_LATITUDE] != self.hass.config.latitude - and import_info[CONF_LONGITUDE] != self.hass.config.longitude - ): - data[CONF_LATITUDE] = import_info[CONF_LATITUDE] - data[CONF_LONGITUDE] = import_info[CONF_LONGITUDE] - new_entry_type = TYPE_SPECIFY_COORDINATES - else: - new_entry_type = TYPE_USE_HOME - - for entry in self._async_current_entries(include_ignore=False): - - if (cur_entry_type := _get_entry_type(entry.data)) != new_entry_type: - continue - - if cur_entry_type == TYPE_USE_HOME and new_entry_type == TYPE_USE_HOME: - return self.async_abort(reason="already_configured") - - if ( - cur_entry_type == TYPE_SPECIFY_COUNTRY - and data[CONF_COUNTRY_CODE] == entry.data[CONF_COUNTRY_CODE] - ): - return self.async_abort(reason="already_configured") - - if ( - cur_entry_type == TYPE_SPECIFY_COORDINATES - and data[CONF_LATITUDE] == entry.data[CONF_LATITUDE] - and data[CONF_LONGITUDE] == entry.data[CONF_LONGITUDE] - ): - return self.async_abort(reason="already_configured") - - try: - await self.hass.async_add_executor_job(get_data, self.hass, data) - except CO2Error: - return self.async_abort(reason="unknown") - - return self.async_create_entry( - title=get_extra_name(data) or "CO2 Signal", data=data - ) - async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: diff --git a/homeassistant/components/co2signal/const.py b/homeassistant/components/co2signal/const.py index 1db0ccc20fd..a1264acc9ff 100644 --- a/homeassistant/components/co2signal/const.py +++ b/homeassistant/components/co2signal/const.py @@ -4,8 +4,3 @@ DOMAIN = "co2signal" CONF_COUNTRY_CODE = "country_code" ATTRIBUTION = "Data provided by CO2signal" -MSG_LOCATION = ( - "Please use either coordinates or the country code. " - "For the coordinates, " - "you need to use both latitude and longitude." -) diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index 6b176c2cf5f..09f56e1083f 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -5,40 +5,19 @@ from dataclasses import dataclass from datetime import timedelta from typing import cast -import voluptuous as vol - from homeassistant import config_entries -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, - SensorEntity, - SensorStateClass, -) -from homeassistant.const import ( - ATTR_ATTRIBUTION, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_TOKEN, - PERCENTAGE, -) -from homeassistant.helpers import config_validation as cv, update_coordinator +from homeassistant.components.sensor import SensorEntity, SensorStateClass +from homeassistant.const import ATTR_ATTRIBUTION, PERCENTAGE +from homeassistant.helpers import update_coordinator from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.typing import StateType from . import CO2SignalCoordinator, CO2SignalResponse -from .const import ATTRIBUTION, CONF_COUNTRY_CODE, DOMAIN, MSG_LOCATION +from .const import ATTRIBUTION, DOMAIN SCAN_INTERVAL = timedelta(minutes=3) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_TOKEN): cv.string, - vol.Inclusive(CONF_LATITUDE, "coords", msg=MSG_LOCATION): cv.latitude, - vol.Inclusive(CONF_LONGITUDE, "coords", msg=MSG_LOCATION): cv.longitude, - vol.Optional(CONF_COUNTRY_CODE): cv.string, - } -) - @dataclass class CO2SensorEntityDescription: diff --git a/tests/components/co2signal/test_config_flow.py b/tests/components/co2signal/test_config_flow.py index b2530decb12..7961e413135 100644 --- a/tests/components/co2signal/test_config_flow.py +++ b/tests/components/co2signal/test_config_flow.py @@ -7,12 +7,9 @@ from homeassistant import config_entries from homeassistant.components.co2signal import DOMAIN, config_flow from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM -from homeassistant.setup import async_setup_component from . import VALID_PAYLOAD -from tests.common import MockConfigEntry - async def test_form_home(hass: HomeAssistant) -> None: """Test we get the form.""" @@ -196,104 +193,3 @@ async def test_form_error_unexpected_data(hass: HomeAssistant) -> None: assert result2["type"] == RESULT_TYPE_FORM assert result2["errors"] == {"base": "unknown"} - - -async def test_import(hass: HomeAssistant) -> None: - """Test we import correctly.""" - - with patch( - "CO2Signal.get_latest", - return_value=VALID_PAYLOAD, - ): - assert await async_setup_component( - hass, "sensor", {"sensor": {"platform": "co2signal", "token": "1234"}} - ) - await hass.async_block_till_done() - - assert len(hass.config_entries.async_entries("co2signal")) == 1 - - state = hass.states.get("sensor.co2_intensity") - assert state is not None - assert state.state == "45.99" - assert state.name == "CO2 intensity" - assert state.attributes["unit_of_measurement"] == "gCO2eq/kWh" - assert state.attributes["country_code"] == "FR" - - state = hass.states.get("sensor.grid_fossil_fuel_percentage") - assert state is not None - assert state.state == "5.46" - assert state.name == "Grid fossil fuel percentage" - assert state.attributes["unit_of_measurement"] == "%" - assert state.attributes["country_code"] == "FR" - - -async def test_import_abort_existing_home(hass: HomeAssistant) -> None: - """Test we abort if home entry found.""" - - MockConfigEntry(domain="co2signal", data={"api_key": "abcd"}).add_to_hass(hass) - - with patch( - "CO2Signal.get_latest", - return_value=VALID_PAYLOAD, - ): - assert await async_setup_component( - hass, "sensor", {"sensor": {"platform": "co2signal", "token": "1234"}} - ) - await hass.async_block_till_done() - - assert len(hass.config_entries.async_entries("co2signal")) == 1 - - -async def test_import_abort_existing_country(hass: HomeAssistant) -> None: - """Test we abort if existing country found.""" - - MockConfigEntry( - domain="co2signal", data={"api_key": "abcd", "country_code": "nl"} - ).add_to_hass(hass) - - with patch( - "CO2Signal.get_latest", - return_value=VALID_PAYLOAD, - ): - assert await async_setup_component( - hass, - "sensor", - { - "sensor": { - "platform": "co2signal", - "token": "1234", - "country_code": "nl", - } - }, - ) - await hass.async_block_till_done() - - assert len(hass.config_entries.async_entries("co2signal")) == 1 - - -async def test_import_abort_existing_coordinates(hass: HomeAssistant) -> None: - """Test we abort if existing coordinates found.""" - - MockConfigEntry( - domain="co2signal", data={"api_key": "abcd", "latitude": 1, "longitude": 2} - ).add_to_hass(hass) - - with patch( - "CO2Signal.get_latest", - return_value=VALID_PAYLOAD, - ): - assert await async_setup_component( - hass, - "sensor", - { - "sensor": { - "platform": "co2signal", - "token": "1234", - "latitude": 1, - "longitude": 2, - } - }, - ) - await hass.async_block_till_done() - - assert len(hass.config_entries.async_entries("co2signal")) == 1 From b5de2c38b378696420313c2fc79843b717e77ebe Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Dec 2021 13:54:10 +0100 Subject: [PATCH 0798/2644] Use new enums in sms (#62372) Co-authored-by: epenet --- homeassistant/components/sms/sensor.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sms/sensor.py b/homeassistant/components/sms/sensor.py index f5152ac7462..c51d1ef8773 100644 --- a/homeassistant/components/sms/sensor.py +++ b/homeassistant/components/sms/sensor.py @@ -3,8 +3,12 @@ import logging import gammu # pylint: disable=import-error -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription -from homeassistant.const import DEVICE_CLASS_SIGNAL_STRENGTH, SIGNAL_STRENGTH_DECIBELS +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) +from homeassistant.const import SIGNAL_STRENGTH_DECIBELS from homeassistant.helpers.entity import DeviceInfo from .const import DOMAIN, SMS_GATEWAY @@ -25,7 +29,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): SensorEntityDescription( key="signal", name=f"gsm_signal_imei_{imei}", - device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, entity_registry_enabled_default=False, ), From e689afc0b38626302b40ac07460a003c2f0618b0 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 20 Dec 2021 07:56:45 -0500 Subject: [PATCH 0799/2644] Remove deprecated yaml config from dlna_dmr (#62344) --- .../components/dlna_dmr/config_flow.py | 87 +------ .../components/dlna_dmr/media_player.py | 48 +--- tests/components/dlna_dmr/test_config_flow.py | 222 +----------------- .../components/dlna_dmr/test_media_player.py | 35 +-- 4 files changed, 4 insertions(+), 388 deletions(-) diff --git a/homeassistant/components/dlna_dmr/config_flow.py b/homeassistant/components/dlna_dmr/config_flow.py index 69437d99e3d..d3c1b6ca845 100644 --- a/homeassistant/components/dlna_dmr/config_flow.py +++ b/homeassistant/components/dlna_dmr/config_flow.py @@ -14,13 +14,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import ssdp -from homeassistant.const import ( - CONF_DEVICE_ID, - CONF_HOST, - CONF_NAME, - CONF_TYPE, - CONF_URL, -) +from homeassistant.const import CONF_DEVICE_ID, CONF_HOST, CONF_TYPE, CONF_URL from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import IntegrationError @@ -125,85 +119,6 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id="manual", data_schema=data_schema, errors=errors ) - async def async_step_import(self, import_data: FlowInput = None) -> FlowResult: - """Import a new DLNA DMR device from a config entry. - - This flow is triggered by `async_setup_platform`. If the device has not - been migrated, and can be connected to, automatically import it. If it - cannot be connected to, prompt the user to turn it on. If it has already - been migrated, do nothing. - """ - LOGGER.debug("async_step_import: import_data: %s", import_data) - - if not import_data or CONF_URL not in import_data: - LOGGER.debug("Entry not imported: incomplete_config") - return self.async_abort(reason="incomplete_config") - - self._location = import_data[CONF_URL] - self._async_abort_entries_match({CONF_URL: self._location}) - - # Use the location as this config flow's unique ID until UDN is known - await self.async_set_unique_id(self._location) - - # Set options from the import_data, except listen_ip which is no longer used - self._options[CONF_LISTEN_PORT] = import_data.get(CONF_LISTEN_PORT) - self._options[CONF_CALLBACK_URL_OVERRIDE] = import_data.get( - CONF_CALLBACK_URL_OVERRIDE - ) - - # Override device name if it's set in the YAML - self._name = import_data.get(CONF_NAME) - - discoveries = await self._async_get_discoveries() - - # Find the device in the list of unconfigured devices - for discovery in discoveries: - if discovery.ssdp_location == self._location: - # Device found via SSDP, it shouldn't need polling - self._options[CONF_POLL_AVAILABILITY] = False - # Discovery info has everything required to create config entry - await self._async_set_info_from_discovery(discovery) - LOGGER.debug( - "Entry %s found via SSDP, with UDN %s", - self._location, - self._udn, - ) - return self._create_entry() - - # This device will need to be polled - self._options[CONF_POLL_AVAILABILITY] = True - - # Device was not found via SSDP, connect directly for configuration - try: - await self._async_connect() - except ConnectError as err: - # This will require user action - LOGGER.debug("Entry %s not imported yet: %s", self._location, err.args[0]) - return await self.async_step_import_turn_on() - - LOGGER.debug("Entry %s ready for import", self._location) - return self._create_entry() - - async def async_step_import_turn_on( - self, user_input: FlowInput = None - ) -> FlowResult: - """Request the user to turn on the device so that import can finish.""" - LOGGER.debug("async_step_import_turn_on: %s", user_input) - - self.context["title_placeholders"] = {"name": self._name or self._location} - - errors = {} - if user_input is not None: - try: - await self._async_connect() - except ConnectError as err: - errors["base"] = err.args[0] - else: - return self._create_entry() - - self._set_confirm_only() - return self.async_show_form(step_id="import_turn_on", errors=errors) - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle a flow initialized by SSDP discovery.""" LOGGER.debug("async_step_ssdp: discovery_info %s", pformat(discovery_info)) diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index 2b529609d9e..1b64f592460 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -13,11 +13,10 @@ from async_upnp_client.const import NotificationSubType from async_upnp_client.exceptions import UpnpError, UpnpResponseError from async_upnp_client.profiles.dlna import DmrDevice, PlayMode, TransportState from async_upnp_client.utils import async_get_local_ip -import voluptuous as vol from homeassistant import config_entries from homeassistant.components import ssdp -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity +from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( ATTR_MEDIA_EXTRA, REPEAT_MODE_ALL, @@ -38,7 +37,6 @@ from homeassistant.components.media_player.const import ( ) from homeassistant.const import ( CONF_DEVICE_ID, - CONF_NAME, CONF_TYPE, CONF_URL, STATE_IDLE, @@ -49,9 +47,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry, entity_registry -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( CONF_CALLBACK_URL_OVERRIDE, @@ -69,25 +65,6 @@ from .data import EventListenAddr, get_domain_data PARALLEL_UPDATES = 0 -# Configuration via YAML is deprecated in favour of config flow -CONF_LISTEN_IP = "listen_ip" -PLATFORM_SCHEMA = vol.All( - cv.deprecated(CONF_URL), - cv.deprecated(CONF_LISTEN_IP), - cv.deprecated(CONF_LISTEN_PORT), - cv.deprecated(CONF_NAME), - cv.deprecated(CONF_CALLBACK_URL_OVERRIDE), - PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_URL): cv.string, - vol.Optional(CONF_LISTEN_IP): cv.string, - vol.Optional(CONF_LISTEN_PORT): cv.port, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_CALLBACK_URL_OVERRIDE): cv.url, - } - ), -) - Func = TypeVar("Func", bound=Callable[..., Any]) @@ -111,29 +88,6 @@ def catch_request_errors(func: Func) -> Func: return cast(Func, wrapper) -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up DLNA media_player platform.""" - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=config, - ) - ) - - _LOGGER.warning( - "Configuring dlna_dmr via yaml is deprecated; the configuration for" - " %s will be migrated to a config entry and can be safely removed when" - "migration is complete", - config.get(CONF_URL), - ) - - async def async_setup_entry( hass: HomeAssistant, entry: config_entries.ConfigEntry, diff --git a/tests/components/dlna_dmr/test_config_flow.py b/tests/components/dlna_dmr/test_config_flow.py index c497d45a4f2..fb0c416c086 100644 --- a/tests/components/dlna_dmr/test_config_flow.py +++ b/tests/components/dlna_dmr/test_config_flow.py @@ -15,14 +15,7 @@ from homeassistant.components.dlna_dmr.const import ( CONF_POLL_AVAILABILITY, DOMAIN as DLNA_DOMAIN, ) -from homeassistant.const import ( - CONF_DEVICE_ID, - CONF_HOST, - CONF_NAME, - CONF_PLATFORM, - CONF_TYPE, - CONF_URL, -) +from homeassistant.const import CONF_DEVICE_ID, CONF_HOST, CONF_TYPE, CONF_URL from homeassistant.core import HomeAssistant from .conftest import ( @@ -43,13 +36,6 @@ pytestmark = [ WRONG_DEVICE_TYPE = "urn:schemas-upnp-org:device:InternetGatewayDevice:1" -IMPORTED_DEVICE_NAME = "Imported DMR device" - -MOCK_CONFIG_IMPORT_DATA = { - CONF_PLATFORM: DLNA_DOMAIN, - CONF_URL: MOCK_DEVICE_LOCATION, -} - MOCK_ROOT_DEVICE_UDN = "ROOT_DEVICE" MOCK_DISCOVERY = ssdp.SsdpServiceInfo( @@ -276,212 +262,6 @@ async def test_user_flow_wrong_st(hass: HomeAssistant, domain_data_mock: Mock) - assert result["step_id"] == "manual" -async def test_import_flow_invalid(hass: HomeAssistant, domain_data_mock: Mock) -> None: - """Test import flow of invalid YAML config.""" - # Missing CONF_URL - result = await hass.config_entries.flow.async_init( - DLNA_DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={CONF_PLATFORM: DLNA_DOMAIN}, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "incomplete_config" - - -async def test_import_flow_ssdp_discovered( - hass: HomeAssistant, ssdp_scanner_mock: Mock -) -> None: - """Test import of YAML config with a device also found via SSDP.""" - ssdp_scanner_mock.async_get_discovery_info_by_st.side_effect = [ - [MOCK_DISCOVERY], - [], - [], - ] - result = await hass.config_entries.flow.async_init( - DLNA_DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=MOCK_CONFIG_IMPORT_DATA, - ) - await hass.async_block_till_done() - - assert ssdp_scanner_mock.async_get_discovery_info_by_st.call_count >= 1 - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == MOCK_DEVICE_NAME - assert result["data"] == { - CONF_URL: MOCK_DEVICE_LOCATION, - CONF_DEVICE_ID: MOCK_DEVICE_UDN, - CONF_TYPE: MOCK_DEVICE_TYPE, - } - assert result["options"] == { - CONF_LISTEN_PORT: None, - CONF_CALLBACK_URL_OVERRIDE: None, - CONF_POLL_AVAILABILITY: False, - } - - # The config entry should not be duplicated when dlna_dmr is restarted - ssdp_scanner_mock.async_get_discovery_info_by_st.side_effect = [ - [MOCK_DISCOVERY], - [], - [], - ] - result = await hass.config_entries.flow.async_init( - DLNA_DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=MOCK_CONFIG_IMPORT_DATA, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - # Wait for platform to be fully setup - await hass.async_block_till_done() - - -async def test_import_flow_direct_connect( - hass: HomeAssistant, ssdp_scanner_mock: Mock -) -> None: - """Test import of YAML config with a device *not found* via SSDP.""" - ssdp_scanner_mock.async_get_discovery_info_by_st.return_value = [] - - result = await hass.config_entries.flow.async_init( - DLNA_DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=MOCK_CONFIG_IMPORT_DATA, - ) - await hass.async_block_till_done() - - assert ssdp_scanner_mock.async_get_discovery_info_by_st.call_count >= 1 - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == MOCK_DEVICE_NAME - assert result["data"] == { - CONF_URL: MOCK_DEVICE_LOCATION, - CONF_DEVICE_ID: MOCK_DEVICE_UDN, - CONF_TYPE: MOCK_DEVICE_TYPE, - } - assert result["options"] == { - CONF_LISTEN_PORT: None, - CONF_CALLBACK_URL_OVERRIDE: None, - CONF_POLL_AVAILABILITY: True, - } - - # The config entry should not be duplicated when dlna_dmr is restarted - result = await hass.config_entries.flow.async_init( - DLNA_DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=MOCK_CONFIG_IMPORT_DATA, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - -async def test_import_flow_offline( - hass: HomeAssistant, domain_data_mock: Mock, ssdp_scanner_mock: Mock -) -> None: - """Test import flow of offline device.""" - # Device is not yet contactable - domain_data_mock.upnp_factory.async_create_device.side_effect = UpnpError - - result = await hass.config_entries.flow.async_init( - DLNA_DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_PLATFORM: DLNA_DOMAIN, - CONF_URL: MOCK_DEVICE_LOCATION, - CONF_NAME: IMPORTED_DEVICE_NAME, - CONF_CALLBACK_URL_OVERRIDE: "http://override/callback", - CONF_LISTEN_PORT: 2222, - }, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {} - assert result["step_id"] == "import_turn_on" - - import_flow_id = result["flow_id"] - - # User clicks submit, same form is displayed with an error - result = await hass.config_entries.flow.async_configure( - import_flow_id, user_input={} - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "cannot_connect"} - assert result["step_id"] == "import_turn_on" - - # Device is discovered via SSDP, new flow should not be initialized - ssdp_scanner_mock.async_get_discovery_info_by_st.side_effect = [ - [MOCK_DISCOVERY], - [], - [], - ] - domain_data_mock.upnp_factory.async_create_device.side_effect = None - - result = await hass.config_entries.flow.async_init( - DLNA_DOMAIN, - context={"source": config_entries.SOURCE_SSDP}, - data=MOCK_DISCOVERY, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_in_progress" - - # User clicks submit, config entry should be created - result = await hass.config_entries.flow.async_configure( - import_flow_id, user_input={} - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == IMPORTED_DEVICE_NAME - assert result["data"] == { - CONF_URL: MOCK_DEVICE_LOCATION, - CONF_DEVICE_ID: MOCK_DEVICE_UDN, - CONF_TYPE: MOCK_DEVICE_TYPE, - } - # Options should be retained - assert result["options"] == { - CONF_LISTEN_PORT: 2222, - CONF_CALLBACK_URL_OVERRIDE: "http://override/callback", - CONF_POLL_AVAILABILITY: True, - } - - # Wait for platform to be fully setup - await hass.async_block_till_done() - - -async def test_import_flow_options( - hass: HomeAssistant, ssdp_scanner_mock: Mock -) -> None: - """Test import of YAML config with options set.""" - ssdp_scanner_mock.async_get_discovery_info_by_st.return_value = [] - - result = await hass.config_entries.flow.async_init( - DLNA_DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_PLATFORM: DLNA_DOMAIN, - CONF_URL: MOCK_DEVICE_LOCATION, - CONF_NAME: IMPORTED_DEVICE_NAME, - CONF_LISTEN_PORT: 2222, - CONF_CALLBACK_URL_OVERRIDE: "http://override/callback", - }, - ) - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == IMPORTED_DEVICE_NAME - assert result["data"] == { - CONF_URL: MOCK_DEVICE_LOCATION, - CONF_DEVICE_ID: MOCK_DEVICE_UDN, - CONF_TYPE: MOCK_DEVICE_TYPE, - } - assert result["options"] == { - CONF_LISTEN_PORT: 2222, - CONF_CALLBACK_URL_OVERRIDE: "http://override/callback", - CONF_POLL_AVAILABILITY: True, - } - - # Wait for platform to be fully setup - await hass.async_block_till_done() - - async def test_ssdp_flow_success(hass: HomeAssistant) -> None: """Test that SSDP discovery with an available device works.""" result = await hass.config_entries.flow.async_init( diff --git a/tests/components/dlna_dmr/test_media_player.py b/tests/components/dlna_dmr/test_media_player.py index b8c10b47b60..2d59c31c666 100644 --- a/tests/components/dlna_dmr/test_media_player.py +++ b/tests/components/dlna_dmr/test_media_player.py @@ -29,7 +29,7 @@ from homeassistant.components.dlna_dmr.const import ( from homeassistant.components.dlna_dmr.data import EventListenAddr from homeassistant.components.media_player import ATTR_TO_PROPERTY, const as mp_const from homeassistant.components.media_player.const import DOMAIN as MP_DOMAIN -from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM, CONF_URL +from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import async_get as async_get_dr from homeassistant.helpers.entity_component import async_update_entity @@ -37,7 +37,6 @@ from homeassistant.helpers.entity_registry import ( async_entries_for_config_entry, async_get as async_get_er, ) -from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_setup_component from .conftest import ( @@ -183,38 +182,6 @@ async def mock_disconnected_entity_id( assert dmr_device_mock.on_event is None -async def test_setup_platform_import_flow_started( - hass: HomeAssistant, domain_data_mock: Mock -) -> None: - """Test import flow of YAML config is started if there's config data.""" - # Cause connection attempts to fail - domain_data_mock.upnp_factory.async_create_device.side_effect = UpnpConnectionError - - # Run the setup - mock_config: ConfigType = { - MP_DOMAIN: [ - { - CONF_PLATFORM: DLNA_DOMAIN, - CONF_URL: MOCK_DEVICE_LOCATION, - CONF_LISTEN_PORT: 1234, - } - ] - } - - await async_setup_component(hass, MP_DOMAIN, mock_config) - await hass.async_block_till_done() - - # Check config_flow has started - flows = hass.config_entries.flow.async_progress(include_uninitialized=True) - assert len(flows) == 1 - - # It should be paused, waiting for the user to turn on the device - flow = flows[0] - assert flow["handler"] == "dlna_dmr" - assert flow["step_id"] == "import_turn_on" - assert flow["context"].get("unique_id") == MOCK_DEVICE_LOCATION - - async def test_setup_entry_no_options( hass: HomeAssistant, domain_data_mock: Mock, From c18c58f56063df2ba230d049580c9eb57ada2dcd Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Dec 2021 14:17:23 +0100 Subject: [PATCH 0800/2644] Use attr** in linode (#61882) Co-authored-by: epenet --- .../components/linode/binary_sensor.py | 53 +++++++------------ homeassistant/components/linode/switch.py | 25 ++------- 2 files changed, 23 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/linode/binary_sensor.py b/homeassistant/components/linode/binary_sensor.py index 9e482283042..46e9609aef8 100644 --- a/homeassistant/components/linode/binary_sensor.py +++ b/homeassistant/components/linode/binary_sensor.py @@ -4,8 +4,8 @@ import logging import voluptuous as vol from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_MOVING, PLATFORM_SCHEMA, + BinarySensorDeviceClass, BinarySensorEntity, ) import homeassistant.helpers.config_validation as cv @@ -49,49 +49,34 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class LinodeBinarySensor(BinarySensorEntity): """Representation of a Linode droplet sensor.""" - _attr_device_class = DEVICE_CLASS_MOVING + _attr_device_class = BinarySensorDeviceClass.MOVING def __init__(self, li, node_id): # pylint: disable=invalid-name """Initialize a new Linode sensor.""" self._linode = li self._node_id = node_id - self._state = None - self.data = None - self._attrs = {} - self._name = None - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def is_on(self): - """Return true if the binary sensor is on.""" - return self._state - - @property - def extra_state_attributes(self): - """Return the state attributes of the Linode Node.""" - return self._attrs + self._attr_extra_state_attributes = {} + self._attr_name = None def update(self): """Update state of sensor.""" + data = None self._linode.update() if self._linode.data is not None: for node in self._linode.data: if node.id == self._node_id: - self.data = node - if self.data is not None: - self._state = self.data.status == "running" - self._attrs = { - ATTR_CREATED: self.data.created, - ATTR_NODE_ID: self.data.id, - ATTR_NODE_NAME: self.data.label, - ATTR_IPV4_ADDRESS: self.data.ipv4, - ATTR_IPV6_ADDRESS: self.data.ipv6, - ATTR_MEMORY: self.data.specs.memory, - ATTR_REGION: self.data.region.country, - ATTR_VCPUS: self.data.specs.vcpus, + data = node + + if data is not None: + self._attr_is_on = data.status == "running" + self._attr_extra_state_attributes = { + ATTR_CREATED: data.created, + ATTR_NODE_ID: data.id, + ATTR_NODE_NAME: data.label, + ATTR_IPV4_ADDRESS: data.ipv4, + ATTR_IPV6_ADDRESS: data.ipv6, + ATTR_MEMORY: data.specs.memory, + ATTR_REGION: data.region.country, + ATTR_VCPUS: data.specs.vcpus, } - self._name = self.data.label + self._attr_name = data.label diff --git a/homeassistant/components/linode/switch.py b/homeassistant/components/linode/switch.py index cd2efe9c4da..4a45b3fd793 100644 --- a/homeassistant/components/linode/switch.py +++ b/homeassistant/components/linode/switch.py @@ -51,24 +51,7 @@ class LinodeSwitch(SwitchEntity): self._linode = li self._node_id = node_id self.data = None - self._state = None - self._attrs = {} - self._name = None - - @property - def name(self): - """Return the name of the switch.""" - return self._name - - @property - def is_on(self): - """Return true if switch is on.""" - return self._state - - @property - def extra_state_attributes(self): - """Return the state attributes of the Linode Node.""" - return self._attrs + self._attr_extra_state_attributes = {} def turn_on(self, **kwargs): """Boot-up the Node.""" @@ -88,8 +71,8 @@ class LinodeSwitch(SwitchEntity): if node.id == self._node_id: self.data = node if self.data is not None: - self._state = self.data.status == "running" - self._attrs = { + self._attr_is_on = self.data.status == "running" + self._attr_extra_state_attributes = { ATTR_CREATED: self.data.created, ATTR_NODE_ID: self.data.id, ATTR_NODE_NAME: self.data.label, @@ -99,4 +82,4 @@ class LinodeSwitch(SwitchEntity): ATTR_REGION: self.data.region.country, ATTR_VCPUS: self.data.specs.vcpus, } - self._name = self.data.label + self._attr_name = self.data.label From c7a3a0da9f65cd5b6510499383bd03f6ad2c6375 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Dec 2021 14:18:48 +0100 Subject: [PATCH 0801/2644] Use new enums in spc (#62384) Co-authored-by: epenet --- homeassistant/components/spc/binary_sensor.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/spc/binary_sensor.py b/homeassistant/components/spc/binary_sensor.py index 48023d09a99..60126138d01 100644 --- a/homeassistant/components/spc/binary_sensor.py +++ b/homeassistant/components/spc/binary_sensor.py @@ -2,9 +2,7 @@ from pyspcwebgw.const import ZoneInput, ZoneType from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_MOTION, - DEVICE_CLASS_OPENING, - DEVICE_CLASS_SMOKE, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.core import callback @@ -15,9 +13,9 @@ from . import DATA_API, SIGNAL_UPDATE_SENSOR def _get_device_class(zone_type): return { - ZoneType.ALARM: DEVICE_CLASS_MOTION, - ZoneType.ENTRY_EXIT: DEVICE_CLASS_OPENING, - ZoneType.FIRE: DEVICE_CLASS_SMOKE, + ZoneType.ALARM: BinarySensorDeviceClass.MOTION, + ZoneType.ENTRY_EXIT: BinarySensorDeviceClass.OPENING, + ZoneType.FIRE: BinarySensorDeviceClass.SMOKE, ZoneType.TECHNICAL: "power", }.get(zone_type) From d147038cc517b5a0d94e040a7b2c16ee59d127ea Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Dec 2021 14:19:09 +0100 Subject: [PATCH 0802/2644] Use new enums in soma (#62377) Co-authored-by: epenet --- homeassistant/components/soma/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/soma/sensor.py b/homeassistant/components/soma/sensor.py index 948e8d1e1e1..3d8a60df844 100644 --- a/homeassistant/components/soma/sensor.py +++ b/homeassistant/components/soma/sensor.py @@ -4,8 +4,8 @@ import logging from requests import RequestException -from homeassistant.components.sensor import SensorEntity -from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.const import PERCENTAGE from homeassistant.util import Throttle from . import DEVICES, SomaEntity @@ -29,7 +29,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class SomaSensor(SomaEntity, SensorEntity): """Representation of a Soma cover device.""" - _attr_device_class = DEVICE_CLASS_BATTERY + _attr_device_class = SensorDeviceClass.BATTERY _attr_native_unit_of_measurement = PERCENTAGE @property From 43099f6eb0bfb963296d59307d1b27fac2acccf7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Dec 2021 14:19:40 +0100 Subject: [PATCH 0803/2644] Use _attr_attribution in synology_dsm (#62382) Co-authored-by: epenet --- homeassistant/components/synology_dsm/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 8212bd67eda..e1a9076c947 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -29,7 +29,6 @@ from synology_dsm.exceptions import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_HOST, CONF_MAC, CONF_PASSWORD, @@ -528,7 +527,7 @@ class SynologyDSMBaseEntity(CoordinatorEntity): entity_description: SynologyDSMEntityDescription unique_id: str - _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + _attr_attribution = ATTRIBUTION def __init__( self, From c96606df6896bd72bdb1611af11289e87386da41 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 20 Dec 2021 14:28:02 +0100 Subject: [PATCH 0804/2644] Don't use the homeassistant media app when casting media (#62385) --- homeassistant/components/cast/media_player.py | 2 +- tests/components/cast/test_media_player.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 61922a4cd8b..b687f96948b 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -526,7 +526,7 @@ class CastDevice(MediaPlayerEntity): controller.play_media(media) else: app_data = {"media_id": media_id, "media_type": media_type, **extra} - quick_play(self._chromecast, "homeassistant_media", app_data) + quick_play(self._chromecast, "default_media_receiver", app_data) def _media_status(self): """ diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 85562f39761..dae9981ae67 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -791,7 +791,7 @@ async def test_entity_play_media(hass: HomeAssistant, quick_play_mock): chromecast.media_controller.play_media.assert_not_called() quick_play_mock.assert_called_once_with( chromecast, - "homeassistant_media", + "default_media_receiver", { "media_id": "best.mp3", "media_type": "audio", @@ -907,7 +907,7 @@ async def test_entity_play_media_sign_URL(hass: HomeAssistant, quick_play_mock): # Play_media await common.async_play_media(hass, "audio", "/best.mp3", entity_id) quick_play_mock.assert_called_once_with( - chromecast, "homeassistant_media", {"media_id": ANY, "media_type": "audio"} + chromecast, "default_media_receiver", {"media_id": ANY, "media_type": "audio"} ) assert quick_play_mock.call_args[0][2]["media_id"].startswith( "http://example.com:8123/best.mp3?authSig=" @@ -1311,7 +1311,7 @@ async def test_group_media_control(hass, mz_mock, quick_play_mock): assert not chromecast.media_controller.play_media.called quick_play_mock.assert_called_once_with( chromecast, - "homeassistant_media", + "default_media_receiver", {"media_id": "best.mp3", "media_type": "music"}, ) From 2f0b73c4ada90b41df9def77222550bf355c36a7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Dec 2021 14:33:08 +0100 Subject: [PATCH 0805/2644] Use new enums in solax (#62376) Co-authored-by: epenet --- homeassistant/components/solax/sensor.py | 40 +++++++++--------------- 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/solax/sensor.py b/homeassistant/components/solax/sensor.py index 7854142c32b..616bc99821c 100644 --- a/homeassistant/components/solax/sensor.py +++ b/homeassistant/components/solax/sensor.py @@ -8,21 +8,11 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, + SensorStateClass, ) -from homeassistant.const import ( - CONF_IP_ADDRESS, - CONF_PORT, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_CURRENT, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_VOLTAGE, - TEMP_CELSIUS, -) +from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, TEMP_CELSIUS from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval @@ -51,24 +41,24 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= for sensor, (idx, unit) in api.inverter.sensor_map().items(): device_class = state_class = None if unit == "C": - device_class = DEVICE_CLASS_TEMPERATURE - state_class = STATE_CLASS_MEASUREMENT + device_class = SensorDeviceClass.TEMPERATURE + state_class = SensorStateClass.MEASUREMENT unit = TEMP_CELSIUS elif unit == "kWh": - device_class = DEVICE_CLASS_ENERGY - state_class = STATE_CLASS_TOTAL_INCREASING + device_class = SensorDeviceClass.ENERGY + state_class = SensorStateClass.TOTAL_INCREASING elif unit == "V": - device_class = DEVICE_CLASS_VOLTAGE - state_class = STATE_CLASS_MEASUREMENT + device_class = SensorDeviceClass.VOLTAGE + state_class = SensorStateClass.MEASUREMENT elif unit == "A": - device_class = DEVICE_CLASS_CURRENT - state_class = STATE_CLASS_MEASUREMENT + device_class = SensorDeviceClass.CURRENT + state_class = SensorStateClass.MEASUREMENT elif unit == "W": - device_class = DEVICE_CLASS_POWER - state_class = STATE_CLASS_MEASUREMENT + device_class = SensorDeviceClass.POWER + state_class = SensorStateClass.MEASUREMENT elif unit == "%": - device_class = DEVICE_CLASS_BATTERY - state_class = STATE_CLASS_MEASUREMENT + device_class = SensorDeviceClass.BATTERY + state_class = SensorStateClass.MEASUREMENT uid = f"{serial}-{idx}" devices.append(Inverter(uid, serial, sensor, unit, state_class, device_class)) endpoint.sensors = devices From 28af0b4092d325e1b5ae200cc138184b85a330e6 Mon Sep 17 00:00:00 2001 From: Thomas Dietrich Date: Mon, 20 Dec 2021 14:53:51 +0100 Subject: [PATCH 0806/2644] Statistics component typing (#60997) * Implement optional manually defined uniqueid * Fix test case via mocked environment * Add typing to statistics component * Fix minor inconsistency * Fix linter issues * Execute hassfest * Fix stricter mypy warnings * Fix maxsplit warning * Make binary value range explicit check * Add basic typing to statistics tests * Add empty config testcase * Minor improvements * Improve after comments * Remove unnecessary test case * Fix changed type * Remove dict.get default --- .strict-typing | 1 + homeassistant/components/statistics/sensor.py | 265 +++++++++--------- mypy.ini | 11 + tests/components/statistics/test_sensor.py | 106 ++++--- 4 files changed, 214 insertions(+), 169 deletions(-) diff --git a/.strict-typing b/.strict-typing index 3eae62f4b4a..2558506c1c2 100644 --- a/.strict-typing +++ b/.strict-typing @@ -121,6 +121,7 @@ homeassistant.components.slack.* homeassistant.components.sonos.media_player homeassistant.components.ssdp.* homeassistant.components.stookalert.* +homeassistant.components.statistics.* homeassistant.components.stream.* homeassistant.components.sun.* homeassistant.components.surepetcare.* diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index e2608c888aa..3ec1c6fc512 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -1,11 +1,17 @@ """Support for statistics for sensor values.""" +from __future__ import annotations + from collections import deque +from collections.abc import Callable import contextlib +from datetime import datetime, timedelta import logging import statistics +from typing import Any, Literal, cast import voluptuous as vol +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.recorder.models import States from homeassistant.components.recorder.util import execute, session_scope from homeassistant.components.sensor import ( @@ -21,14 +27,23 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import callback +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + HomeAssistant, + State, + callback, + split_entity_id, +) from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( async_track_point_in_utc_time, async_track_state_change_event, ) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.start import async_at_start +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType from homeassistant.util import dt as dt_util from . import DOMAIN, PLATFORMS @@ -100,13 +115,13 @@ DEFAULT_QUANTILE_METHOD = "exclusive" ICON = "mdi:calculator" -def valid_binary_characteristic_configuration(config): +def valid_binary_characteristic_configuration(config: dict[str, Any]) -> dict[str, Any]: """Validate that the characteristic selected is valid for the source sensor type, throw if it isn't.""" - if config.get(CONF_ENTITY_ID).split(".")[0] == "binary_sensor": + if split_entity_id(str(config.get(CONF_ENTITY_ID)))[0] == BINARY_SENSOR_DOMAIN: if config.get(CONF_STATE_CHARACTERISTIC) not in STATS_BINARY_SUPPORT: raise ValueError( "The configured characteristic '" - + config.get(CONF_STATE_CHARACTERISTIC) + + str(config.get(CONF_STATE_CHARACTERISTIC)) + "' is not supported for a binary source sensor." ) return config @@ -162,28 +177,32 @@ PLATFORM_SCHEMA = vol.All( ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Set up the Statistics sensor.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) async_add_entities( - [ + new_entities=[ StatisticsSensor( - source_entity_id=config.get(CONF_ENTITY_ID), - name=config.get(CONF_NAME), + source_entity_id=config[CONF_ENTITY_ID], + name=config[CONF_NAME], unique_id=config.get(CONF_UNIQUE_ID), - state_characteristic=config.get(CONF_STATE_CHARACTERISTIC), - samples_max_buffer_size=config.get(CONF_SAMPLES_MAX_BUFFER_SIZE), + state_characteristic=config[CONF_STATE_CHARACTERISTIC], + samples_max_buffer_size=config[CONF_SAMPLES_MAX_BUFFER_SIZE], samples_max_age=config.get(CONF_MAX_AGE), - precision=config.get(CONF_PRECISION), - quantile_intervals=config.get(CONF_QUANTILE_INTERVALS), - quantile_method=config.get(CONF_QUANTILE_METHOD), + precision=config[CONF_PRECISION], + quantile_intervals=config[CONF_QUANTILE_INTERVALS], + quantile_method=config[CONF_QUANTILE_METHOD], ) ], - True, + update_before_add=True, ) - return True class StatisticsSensor(SensorEntity): @@ -191,41 +210,46 @@ class StatisticsSensor(SensorEntity): def __init__( self, - source_entity_id, - name, - unique_id, - state_characteristic, - samples_max_buffer_size, - samples_max_age, - precision, - quantile_intervals, - quantile_method, - ): + source_entity_id: str, + name: str, + unique_id: str | None, + state_characteristic: str, + samples_max_buffer_size: int, + samples_max_age: timedelta | None, + precision: int, + quantile_intervals: int, + quantile_method: str, + ) -> None: """Initialize the Statistics sensor.""" - self._source_entity_id = source_entity_id - self.is_binary = self._source_entity_id.split(".")[0] == "binary_sensor" - self._name = name - self._unique_id = unique_id - self._state_characteristic = state_characteristic + self._attr_icon: str = ICON + self._attr_name: str = name + self._attr_should_poll: bool = False + self._attr_unique_id: str | None = unique_id + self._source_entity_id: str = source_entity_id + self.is_binary: bool = ( + split_entity_id(self._source_entity_id)[0] == BINARY_SENSOR_DOMAIN + ) + self._state_characteristic: str = state_characteristic if self._state_characteristic == STAT_DEFAULT: self._state_characteristic = STAT_COUNT if self.is_binary else STAT_MEAN _LOGGER.warning(DEPRECATION_WARNING, self._state_characteristic, name) - self._samples_max_buffer_size = samples_max_buffer_size - self._samples_max_age = samples_max_age - self._precision = precision - self._quantile_intervals = quantile_intervals - self._quantile_method = quantile_method - self._value = None - self._unit_of_measurement = None - self._available = False - self.states = deque(maxlen=self._samples_max_buffer_size) - self.ages = deque(maxlen=self._samples_max_buffer_size) - self.attributes = { + self._samples_max_buffer_size: int = samples_max_buffer_size + self._samples_max_age: timedelta | None = samples_max_age + self._precision: int = precision + self._quantile_intervals: int = quantile_intervals + self._quantile_method: str = quantile_method + self._value: StateType | datetime = None + self._unit_of_measurement: str | None = None + self._available: bool = False + self.states: deque[float | bool] = deque(maxlen=self._samples_max_buffer_size) + self.ages: deque[datetime] = deque(maxlen=self._samples_max_buffer_size) + self.attributes: dict[str, StateType] = { STAT_AGE_COVERAGE_RATIO: None, STAT_BUFFER_USAGE_RATIO: None, STAT_SOURCE_VALUE_VALID: None, } + self._state_characteristic_fn: Callable[[], StateType | datetime] if self.is_binary: self._state_characteristic_fn = getattr( self, f"_stat_binary_{self._state_characteristic}" @@ -235,20 +259,20 @@ class StatisticsSensor(SensorEntity): self, f"_stat_{self._state_characteristic}" ) - self._update_listener = None + self._update_listener: CALLBACK_TYPE | None = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" @callback - def async_stats_sensor_state_listener(event): + def async_stats_sensor_state_listener(event: Event) -> None: """Handle the sensor state changes.""" if (new_state := event.data.get("new_state")) is None: return self._add_state_to_queue(new_state) self.async_schedule_update_ha_state(True) - async def async_stats_sensor_startup(_): + async def async_stats_sensor_startup(_: HomeAssistant) -> None: """Add listener and get recorded state.""" _LOGGER.debug("Startup for %s", self.entity_id) @@ -265,7 +289,7 @@ class StatisticsSensor(SensorEntity): async_at_start(self.hass, async_stats_sensor_startup) - def _add_state_to_queue(self, new_state): + def _add_state_to_queue(self, new_state: State) -> None: """Add the state to the queue.""" self._available = new_state.state != STATE_UNAVAILABLE if new_state.state == STATE_UNAVAILABLE: @@ -277,7 +301,8 @@ class StatisticsSensor(SensorEntity): try: if self.is_binary: - self.states.append(new_state.state) + assert new_state.state in ("on", "off") + self.states.append(new_state.state == "on") else: self.states.append(float(new_state.state)) self.ages.append(new_state.last_updated) @@ -293,8 +318,9 @@ class StatisticsSensor(SensorEntity): self._unit_of_measurement = self._derive_unit_of_measurement(new_state) - def _derive_unit_of_measurement(self, new_state): - base_unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + def _derive_unit_of_measurement(self, new_state: State) -> str | None: + base_unit: str | None = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + unit: str | None if self.is_binary and self._state_characteristic in ( STAT_AVERAGE_STEP, STAT_AVERAGE_TIMELESS, @@ -336,66 +362,46 @@ class StatisticsSensor(SensorEntity): return unit @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def unique_id(self): - """Return the unique id of the sensor.""" - return self._unique_id - - @property - def state_class(self): + def state_class(self) -> Literal[SensorStateClass.MEASUREMENT] | None: """Return the state class of this entity.""" if self._state_characteristic in STATS_NOT_A_NUMBER: return None return SensorStateClass.MEASUREMENT @property - def native_value(self): + def native_value(self) -> StateType | datetime: """Return the state of the sensor.""" return self._value @property - def native_unit_of_measurement(self): + def native_unit_of_measurement(self) -> str | None: """Return the unit the value is expressed in.""" return self._unit_of_measurement @property - def available(self): + def available(self) -> bool: """Return the availability of the sensor linked to the source sensor.""" return self._available @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, StateType] | None: """Return the state attributes of the sensor.""" return { key: value for key, value in self.attributes.items() if value is not None } - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return ICON - - def _purge_old(self): - """Remove states which are older than self._samples_max_age.""" + def _purge_old_states(self, max_age: timedelta) -> None: + """Remove states which are older than a given age.""" now = dt_util.utcnow() _LOGGER.debug( "%s: purging records older then %s(%s)", self.entity_id, - dt_util.as_local(now - self._samples_max_age), + dt_util.as_local(now - max_age), self._samples_max_age, ) - while self.ages and (now - self.ages[0]) > self._samples_max_age: + while self.ages and (now - self.ages[0]) > max_age: _LOGGER.debug( "%s: purging record with datetime %s(%s)", self.entity_id, @@ -405,7 +411,7 @@ class StatisticsSensor(SensorEntity): self.ages.popleft() self.states.popleft() - def _next_to_purge_timestamp(self): + def _next_to_purge_timestamp(self) -> datetime | None: """Find the timestamp when the next purge would occur.""" if self.ages and self._samples_max_age: # Take the oldest entry from the ages list and add the configured max_age. @@ -414,11 +420,11 @@ class StatisticsSensor(SensorEntity): return self.ages[0] + self._samples_max_age return None - async def async_update(self): + async def async_update(self) -> None: """Get the latest data and updates the states.""" _LOGGER.debug("%s: updating statistics", self.entity_id) if self._samples_max_age is not None: - self._purge_old() + self._purge_old_states(self._samples_max_age) self._update_attributes() self._update_value() @@ -434,7 +440,7 @@ class StatisticsSensor(SensorEntity): self._update_listener = None @callback - def _scheduled_update(now): + def _scheduled_update(now: datetime) -> None: """Timer callback for sensor update.""" _LOGGER.debug("%s: executing scheduled update", self.entity_id) self.async_schedule_update_ha_state(True) @@ -444,7 +450,7 @@ class StatisticsSensor(SensorEntity): self.hass, _scheduled_update, next_to_purge_timestamp ) - async def _initialize_from_database(self): + async def _initialize_from_database(self) -> None: """Initialize the list of states from the database. The query will get the list of states in DESCENDING order so that we @@ -478,14 +484,15 @@ class StatisticsSensor(SensorEntity): ) states = execute(query, to_native=True, validate_entity_ids=False) - for state in reversed(states): - self._add_state_to_queue(state) + if states: + for state in reversed(states): + self._add_state_to_queue(state) self.async_schedule_update_ha_state(True) _LOGGER.debug("%s: initializing from database completed", self.entity_id) - def _update_attributes(self): + def _update_attributes(self) -> None: """Calculate and update the various attributes.""" self.attributes[STAT_BUFFER_USAGE_RATIO] = round( len(self.states) / self._samples_max_buffer_size, 2 @@ -500,7 +507,7 @@ class StatisticsSensor(SensorEntity): else: self.attributes[STAT_AGE_COVERAGE_RATIO] = None - def _update_value(self): + def _update_value(self) -> None: """Front to call the right statistical characteristics functions. One of the _stat_*() functions is represented by self._state_characteristic_fn(). @@ -510,16 +517,16 @@ class StatisticsSensor(SensorEntity): if self._state_characteristic not in STATS_NOT_A_NUMBER: with contextlib.suppress(TypeError): - value = round(value, self._precision) + value = round(cast(float, value), self._precision) if self._precision == 0: value = int(value) self._value = value # Statistics for numeric sensor - def _stat_average_linear(self): + def _stat_average_linear(self) -> StateType: if len(self.states) >= 2: - area = 0 + area: float = 0 for i in range(1, len(self.states)): area += ( 0.5 @@ -530,9 +537,9 @@ class StatisticsSensor(SensorEntity): return area / age_range_seconds return None - def _stat_average_step(self): + def _stat_average_step(self) -> StateType: if len(self.states) >= 2: - area = 0 + area: float = 0 for i in range(1, len(self.states)): area += ( self.states[i - 1] @@ -542,65 +549,65 @@ class StatisticsSensor(SensorEntity): return area / age_range_seconds return None - def _stat_average_timeless(self): + def _stat_average_timeless(self) -> StateType: return self._stat_mean() - def _stat_change(self): + def _stat_change(self) -> StateType: if len(self.states) > 0: return self.states[-1] - self.states[0] return None - def _stat_change_sample(self): + def _stat_change_sample(self) -> StateType: if len(self.states) > 1: return (self.states[-1] - self.states[0]) / (len(self.states) - 1) return None - def _stat_change_second(self): + def _stat_change_second(self) -> StateType: if len(self.states) > 1: age_range_seconds = (self.ages[-1] - self.ages[0]).total_seconds() if age_range_seconds > 0: return (self.states[-1] - self.states[0]) / age_range_seconds return None - def _stat_count(self): + def _stat_count(self) -> StateType: return len(self.states) - def _stat_datetime_newest(self): + def _stat_datetime_newest(self) -> datetime | None: if len(self.states) > 0: return self.ages[-1] return None - def _stat_datetime_oldest(self): + def _stat_datetime_oldest(self) -> datetime | None: if len(self.states) > 0: return self.ages[0] return None - def _stat_distance_95_percent_of_values(self): + def _stat_distance_95_percent_of_values(self) -> StateType: if len(self.states) >= 2: - return 2 * 1.96 * self._stat_standard_deviation() + return 2 * 1.96 * cast(float, self._stat_standard_deviation()) return None - def _stat_distance_99_percent_of_values(self): + def _stat_distance_99_percent_of_values(self) -> StateType: if len(self.states) >= 2: - return 2 * 2.58 * self._stat_standard_deviation() + return 2 * 2.58 * cast(float, self._stat_standard_deviation()) return None - def _stat_distance_absolute(self): + def _stat_distance_absolute(self) -> StateType: if len(self.states) > 0: return max(self.states) - min(self.states) return None - def _stat_mean(self): + def _stat_mean(self) -> StateType: if len(self.states) > 0: return statistics.mean(self.states) return None - def _stat_median(self): + def _stat_median(self) -> StateType: if len(self.states) > 0: return statistics.median(self.states) return None - def _stat_noisiness(self): + def _stat_noisiness(self) -> StateType: if len(self.states) >= 2: diff_sum = sum( abs(j - i) for i, j in zip(list(self.states), list(self.states)[1:]) @@ -608,62 +615,64 @@ class StatisticsSensor(SensorEntity): return diff_sum / (len(self.states) - 1) return None - def _stat_quantiles(self): + def _stat_quantiles(self) -> StateType: if len(self.states) > self._quantile_intervals: - return [ - round(quantile, self._precision) - for quantile in statistics.quantiles( - self.states, - n=self._quantile_intervals, - method=self._quantile_method, - ) - ] + return str( + [ + round(quantile, self._precision) + for quantile in statistics.quantiles( + self.states, + n=self._quantile_intervals, + method=self._quantile_method, + ) + ] + ) return None - def _stat_standard_deviation(self): + def _stat_standard_deviation(self) -> StateType: if len(self.states) >= 2: return statistics.stdev(self.states) return None - def _stat_total(self): + def _stat_total(self) -> StateType: if len(self.states) > 0: return sum(self.states) return None - def _stat_value_max(self): + def _stat_value_max(self) -> StateType: if len(self.states) > 0: return max(self.states) return None - def _stat_value_min(self): + def _stat_value_min(self) -> StateType: if len(self.states) > 0: return min(self.states) return None - def _stat_variance(self): + def _stat_variance(self) -> StateType: if len(self.states) >= 2: return statistics.variance(self.states) return None # Statistics for binary sensor - def _stat_binary_average_step(self): + def _stat_binary_average_step(self) -> StateType: if len(self.states) >= 2: - on_seconds = 0 + on_seconds: float = 0 for i in range(1, len(self.states)): - if self.states[i - 1] == "on": + if self.states[i - 1] is True: on_seconds += (self.ages[i] - self.ages[i - 1]).total_seconds() age_range_seconds = (self.ages[-1] - self.ages[0]).total_seconds() return 100 / age_range_seconds * on_seconds return None - def _stat_binary_average_timeless(self): + def _stat_binary_average_timeless(self) -> StateType: return self._stat_binary_mean() - def _stat_binary_count(self): + def _stat_binary_count(self) -> StateType: return len(self.states) - def _stat_binary_mean(self): + def _stat_binary_mean(self) -> StateType: if len(self.states) > 0: - return 100.0 / len(self.states) * self.states.count("on") + return 100.0 / len(self.states) * self.states.count(True) return None diff --git a/mypy.ini b/mypy.ini index b4c6c802d04..d092972f2b0 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1342,6 +1342,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.statistics.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.stream.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index 51ab31e6ed5..05420a859c6 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -14,6 +14,7 @@ from homeassistant.const import ( STATE_UNKNOWN, TEMP_CELSIUS, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -29,7 +30,7 @@ VALUES_BINARY = ["on", "off", "on", "off", "on", "off", "on", "off", "on"] VALUES_NUMERIC = [17, 20, 15.2, 5, 3.8, 9.2, 6.7, 14, 6] -async def test_unique_id(hass): +async def test_unique_id(hass: HomeAssistant): """Test configuration defined unique_id.""" assert await async_setup_component( hass, @@ -54,7 +55,7 @@ async def test_unique_id(hass): assert entity_id == "sensor.test" -async def test_sensor_defaults_numeric(hass): +async def test_sensor_defaults_numeric(hass: HomeAssistant): """Test the general behavior of the sensor, with numeric source sensor.""" assert await async_setup_component( hass, @@ -74,12 +75,13 @@ async def test_sensor_defaults_numeric(hass): for value in VALUES_NUMERIC: hass.states.async_set( "sensor.test_monitored", - value, + str(value), {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, ) await hass.async_block_till_done() state = hass.states.get("sensor.test") + assert state is not None assert state.state == str(round(sum(VALUES_NUMERIC) / len(VALUES_NUMERIC), 2)) assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT @@ -96,16 +98,18 @@ async def test_sensor_defaults_numeric(hass): ) await hass.async_block_till_done() new_state = hass.states.get("sensor.test") + assert new_state is not None assert new_state.state == STATE_UNAVAILABLE assert new_state.attributes.get("source_value_valid") is None hass.states.async_set( "sensor.test_monitored", - 0, + "0", {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, ) await hass.async_block_till_done() new_state = hass.states.get("sensor.test") new_mean = round(sum(VALUES_NUMERIC) / (len(VALUES_NUMERIC) + 1), 2) + assert new_state is not None assert new_state.state == str(new_mean) assert new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS assert new_state.attributes.get("buffer_usage_ratio") == round(10 / 20, 2) @@ -116,6 +120,7 @@ async def test_sensor_defaults_numeric(hass): hass.states.async_set("sensor.test_monitored", "beer", {}) await hass.async_block_till_done() new_state = hass.states.get("sensor.test") + assert new_state is not None assert new_state.state == str(new_mean) assert new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS assert new_state.attributes.get("source_value_valid") is False @@ -125,6 +130,7 @@ async def test_sensor_defaults_numeric(hass): hass.states.async_set("sensor.test_monitored", STATE_UNKNOWN, {}) await hass.async_block_till_done() new_state = hass.states.get("sensor.test") + assert new_state is not None assert new_state.state == str(new_mean) assert new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS assert new_state.attributes.get("source_value_valid") is False @@ -134,12 +140,13 @@ async def test_sensor_defaults_numeric(hass): hass.states.async_remove("sensor.test_monitored") await hass.async_block_till_done() new_state = hass.states.get("sensor.test") + assert new_state is not None assert new_state.state == str(new_mean) assert new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS assert new_state.attributes.get("source_value_valid") is False -async def test_sensor_defaults_binary(hass): +async def test_sensor_defaults_binary(hass: HomeAssistant): """Test the general behavior of the sensor, with binary source sensor.""" assert await async_setup_component( hass, @@ -165,6 +172,7 @@ async def test_sensor_defaults_binary(hass): await hass.async_block_till_done() state = hass.states.get("sensor.test") + assert state is not None assert state.state == str(len(VALUES_BINARY)) assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT @@ -173,7 +181,7 @@ async def test_sensor_defaults_binary(hass): assert "age_coverage_ratio" not in state.attributes -async def test_sensor_source_with_force_update(hass): +async def test_sensor_source_with_force_update(hass: HomeAssistant): """Test the behavior of the sensor when the source sensor force-updates with same value.""" repeating_values = [18, 0, 0, 0, 0, 0, 0, 0, 9] assert await async_setup_component( @@ -201,12 +209,12 @@ async def test_sensor_source_with_force_update(hass): for value in repeating_values: hass.states.async_set( "sensor.test_monitored_normal", - value, + str(value), {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, ) hass.states.async_set( "sensor.test_monitored_force", - value, + str(value), {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, force_update=True, ) @@ -214,13 +222,14 @@ async def test_sensor_source_with_force_update(hass): state_normal = hass.states.get("sensor.test_normal") state_force = hass.states.get("sensor.test_force") + assert state_normal and state_force assert state_normal.state == str(round(sum(repeating_values) / 3, 2)) assert state_force.state == str(round(sum(repeating_values) / 9, 2)) assert state_normal.attributes.get("buffer_usage_ratio") == round(3 / 20, 2) assert state_force.attributes.get("buffer_usage_ratio") == round(9 / 20, 2) -async def test_sampling_size_non_default(hass): +async def test_sampling_size_non_default(hass: HomeAssistant): """Test rotation.""" assert await async_setup_component( hass, @@ -242,18 +251,19 @@ async def test_sampling_size_non_default(hass): for value in VALUES_NUMERIC: hass.states.async_set( "sensor.test_monitored", - value, + str(value), {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, ) await hass.async_block_till_done() state = hass.states.get("sensor.test") new_mean = round(sum(VALUES_NUMERIC[-5:]) / len(VALUES_NUMERIC[-5:]), 2) + assert state is not None assert state.state == str(new_mean) assert state.attributes.get("buffer_usage_ratio") == round(5 / 5, 2) -async def test_sampling_size_1(hass): +async def test_sampling_size_1(hass: HomeAssistant): """Test validity of stats requiring only one sample.""" assert await async_setup_component( hass, @@ -275,18 +285,19 @@ async def test_sampling_size_1(hass): for value in VALUES_NUMERIC[-3:]: # just the last 3 will do hass.states.async_set( "sensor.test_monitored", - value, + str(value), {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, ) await hass.async_block_till_done() state = hass.states.get("sensor.test") new_mean = float(VALUES_NUMERIC[-1]) + assert state is not None assert state.state == str(new_mean) assert state.attributes.get("buffer_usage_ratio") == round(1 / 1, 2) -async def test_age_limit_expiry(hass): +async def test_age_limit_expiry(hass: HomeAssistant): """Test that values are removed after certain age.""" now = dt_util.utcnow() mock_data = { @@ -321,7 +332,7 @@ async def test_age_limit_expiry(hass): async_fire_time_changed(hass, mock_data["return_time"]) hass.states.async_set( "sensor.test_monitored", - value, + str(value), {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, ) await hass.async_block_till_done() @@ -330,6 +341,7 @@ async def test_age_limit_expiry(hass): state = hass.states.get("sensor.test") new_mean = round(sum(VALUES_NUMERIC[-5:]) / len(VALUES_NUMERIC[-5:]), 2) + assert state is not None assert state.state == str(new_mean) assert state.attributes.get("buffer_usage_ratio") == round(5 / 20, 2) assert state.attributes.get("age_coverage_ratio") == 1.0 @@ -342,6 +354,7 @@ async def test_age_limit_expiry(hass): state = hass.states.get("sensor.test") new_mean = round(sum(VALUES_NUMERIC[-2:]) / len(VALUES_NUMERIC[-2:]), 2) + assert state is not None assert state.state == str(new_mean) assert state.attributes.get("buffer_usage_ratio") == round(2 / 20, 2) assert state.attributes.get("age_coverage_ratio") == 1 / 4 @@ -354,6 +367,7 @@ async def test_age_limit_expiry(hass): state = hass.states.get("sensor.test") new_mean = float(VALUES_NUMERIC[-1]) + assert state is not None assert state.state == str(new_mean) assert state.attributes.get("buffer_usage_ratio") == round(1 / 20, 2) assert state.attributes.get("age_coverage_ratio") == 0 @@ -365,12 +379,13 @@ async def test_age_limit_expiry(hass): await hass.async_block_till_done() state = hass.states.get("sensor.test") + assert state is not None assert state.state == STATE_UNKNOWN assert state.attributes.get("buffer_usage_ratio") == round(0 / 20, 2) assert state.attributes.get("age_coverage_ratio") is None -async def test_precision(hass): +async def test_precision(hass: HomeAssistant): """Test correct result with precision set.""" assert await async_setup_component( hass, @@ -399,19 +414,21 @@ async def test_precision(hass): for value in VALUES_NUMERIC: hass.states.async_set( "sensor.test_monitored", - value, + str(value), {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, ) await hass.async_block_till_done() mean = sum(VALUES_NUMERIC) / len(VALUES_NUMERIC) state = hass.states.get("sensor.test_precision_0") + assert state is not None assert state.state == str(int(round(mean, 0))) state = hass.states.get("sensor.test_precision_3") + assert state is not None assert state.state == str(round(mean, 3)) -async def test_state_class(hass): +async def test_state_class(hass: HomeAssistant): """Test state class, which depends on the characteristic configured.""" assert await async_setup_component( hass, @@ -438,18 +455,20 @@ async def test_state_class(hass): for value in VALUES_NUMERIC: hass.states.async_set( "sensor.test_monitored", - value, + str(value), {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, ) await hass.async_block_till_done() state = hass.states.get("sensor.test_normal") + assert state is not None assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT state = hass.states.get("sensor.test_nan") + assert state is not None assert state.attributes.get(ATTR_STATE_CLASS) is None -async def test_unitless_source_sensor(hass): +async def test_unitless_source_sensor(hass: HomeAssistant): """Statistics for a unitless source sensor should never have a unit.""" assert await async_setup_component( hass, @@ -490,31 +509,31 @@ async def test_unitless_source_sensor(hass): ) await hass.async_block_till_done() - for value in VALUES_NUMERIC: + for value_numeric in VALUES_NUMERIC: hass.states.async_set( "sensor.test_monitored_unitless", - value, + str(value_numeric), ) - for value in VALUES_BINARY: + for value_binary in VALUES_BINARY: hass.states.async_set( "binary_sensor.test_monitored_unitless", - value, + str(value_binary), ) await hass.async_block_till_done() state = hass.states.get("sensor.test_unitless_1") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + assert state and state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None state = hass.states.get("sensor.test_unitless_2") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + assert state and state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None state = hass.states.get("sensor.test_unitless_3") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + assert state and state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None state = hass.states.get("sensor.test_unitless_4") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + assert state and state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None state = hass.states.get("sensor.test_unitless_5") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "%" + assert state and state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "%" -async def test_state_characteristics(hass): +async def test_state_characteristics(hass: HomeAssistant): """Test configured state characteristic for value and unit.""" now = dt_util.utcnow() start_datetime = datetime(now.year + 1, 8, 2, 12, 23, 42, tzinfo=dt_util.UTC) @@ -772,12 +791,12 @@ async def test_state_characteristics(hass): async_fire_time_changed(hass, mock_data["return_time"]) hass.states.async_set( "sensor.test_monitored", - VALUES_NUMERIC[i], + str(VALUES_NUMERIC[i]), {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, ) hass.states.async_set( "binary_sensor.test_monitored", - VALUES_BINARY[i], + str(VALUES_BINARY[i]), {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, ) await hass.async_block_till_done() @@ -786,6 +805,7 @@ async def test_state_characteristics(hass): state = hass.states.get( f"sensor.test_{characteristic['source_sensor_domain']}_{characteristic['name']}" ) + assert state is not None assert state.state == str(characteristic["value_9"]), ( f"value mismatch for characteristic " f"'{characteristic['source_sensor_domain']}/{characteristic['name']}' " @@ -806,6 +826,7 @@ async def test_state_characteristics(hass): state = hass.states.get( f"sensor.test_{characteristic['source_sensor_domain']}_{characteristic['name']}" ) + assert state is not None assert state.state == str(characteristic["value_1"]), ( f"value mismatch for characteristic " f"'{characteristic['source_sensor_domain']}/{characteristic['name']}' " @@ -823,6 +844,7 @@ async def test_state_characteristics(hass): state = hass.states.get( f"sensor.test_{characteristic['source_sensor_domain']}_{characteristic['name']}" ) + assert state is not None assert state.state == str(characteristic["value_0"]), ( f"value mismatch for characteristic " f"'{characteristic['source_sensor_domain']}/{characteristic['name']}' " @@ -831,7 +853,7 @@ async def test_state_characteristics(hass): ) -async def test_invalid_state_characteristic(hass): +async def test_invalid_state_characteristic(hass: HomeAssistant): """Test the detection of wrong state_characteristics selected.""" assert await async_setup_component( hass, @@ -857,7 +879,7 @@ async def test_invalid_state_characteristic(hass): hass.states.async_set( "sensor.test_monitored", - VALUES_NUMERIC[0], + str(VALUES_NUMERIC[0]), {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, ) await hass.async_block_till_done() @@ -868,7 +890,7 @@ async def test_invalid_state_characteristic(hass): assert state is None -async def test_initialize_from_database(hass): +async def test_initialize_from_database(hass: HomeAssistant): """Test initializing the statistics from the recorder database.""" # enable and pre-fill the recorder await async_init_recorder_component(hass) @@ -878,7 +900,7 @@ async def test_initialize_from_database(hass): for value in VALUES_NUMERIC: hass.states.async_set( "sensor.test_monitored", - value, + str(value), {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, ) await hass.async_block_till_done() @@ -903,11 +925,12 @@ async def test_initialize_from_database(hass): await hass.async_block_till_done() state = hass.states.get("sensor.test") + assert state is not None assert state.state == str(round(sum(VALUES_NUMERIC) / len(VALUES_NUMERIC), 2)) assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS -async def test_initialize_from_database_with_maxage(hass): +async def test_initialize_from_database_with_maxage(hass: HomeAssistant): """Test initializing the statistics from the database.""" now = dt_util.utcnow() mock_data = { @@ -919,7 +942,7 @@ async def test_initialize_from_database_with_maxage(hass): # Testing correct retrieval from recorder, thus we do not # want purging to occur within the class itself. - def mock_purge(self): + def mock_purge(self, *args): return # enable and pre-fill the recorder @@ -929,11 +952,11 @@ async def test_initialize_from_database_with_maxage(hass): with patch( "homeassistant.components.statistics.sensor.dt_util.utcnow", new=mock_now - ), patch.object(StatisticsSensor, "_purge_old", mock_purge): + ), patch.object(StatisticsSensor, "_purge_old_states", mock_purge): for value in VALUES_NUMERIC: hass.states.async_set( "sensor.test_monitored", - value, + str(value), {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, ) await hass.async_block_till_done() @@ -959,6 +982,7 @@ async def test_initialize_from_database_with_maxage(hass): await hass.async_block_till_done() state = hass.states.get("sensor.test") + assert state is not None assert state.attributes.get("age_coverage_ratio") == round(2 / 3, 2) # The max_age timestamp should be 1 hour before what we have right # now in mock_data['return_time']. @@ -967,7 +991,7 @@ async def test_initialize_from_database_with_maxage(hass): ) + timedelta(hours=1) -async def test_reload(hass): +async def test_reload(hass: HomeAssistant): """Verify we can reload statistics sensors.""" await async_init_recorder_component(hass) @@ -988,7 +1012,7 @@ async def test_reload(hass): ) await hass.async_block_till_done() - hass.states.async_set("sensor.test_monitored", 12345) + hass.states.async_set("sensor.test_monitored", "0") await hass.async_block_till_done() assert len(hass.states.async_all()) == 2 From 0cc1a7b9bda324ae87f5365c34b2f59496ed1a97 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Dec 2021 15:58:25 +0100 Subject: [PATCH 0807/2644] Use new enums in solaredge_local (#62374) Co-authored-by: epenet --- homeassistant/components/solaredge_local/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/solaredge_local/sensor.py b/homeassistant/components/solaredge_local/sensor.py index d3607ecd29c..b49341ab72a 100644 --- a/homeassistant/components/solaredge_local/sensor.py +++ b/homeassistant/components/solaredge_local/sensor.py @@ -14,13 +14,13 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, + SensorDeviceClass, SensorEntity, SensorEntityDescription, ) from homeassistant.const import ( CONF_IP_ADDRESS, CONF_NAME, - DEVICE_CLASS_TEMPERATURE, ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_WATT_HOUR, @@ -129,7 +129,7 @@ SENSOR_TYPES: tuple[SolarEdgeLocalSensorEntityDescription, ...] = ( name="Average Optimizer Temperature", native_unit_of_measurement=TEMP_CELSIUS, icon="mdi:solar-panel", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), SolarEdgeLocalSensorEntityDescription( key="optimizervoltage", @@ -144,7 +144,7 @@ SENSOR_TYPE_INVERTER_TEMPERATURE = SolarEdgeLocalSensorEntityDescription( name="Inverter Temperature", native_unit_of_measurement=TEMP_CELSIUS, extra_attribute="operating_mode", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ) SENSOR_TYPES_ENERGY_IMPORT: tuple[SolarEdgeLocalSensorEntityDescription, ...] = ( From ff062bd052a0f96eff5b207f5adc0956b7c7df27 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 20 Dec 2021 16:07:13 +0100 Subject: [PATCH 0808/2644] Cleanup stale setup from CO2Signal (#62395) --- homeassistant/components/co2signal/sensor.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index 09f56e1083f..c6582ac4c8c 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -5,7 +5,6 @@ from dataclasses import dataclass from datetime import timedelta from typing import cast -from homeassistant import config_entries from homeassistant.components.sensor import SensorEntity, SensorStateClass from homeassistant.const import ATTR_ATTRIBUTION, PERCENTAGE from homeassistant.helpers import update_coordinator @@ -45,15 +44,6 @@ SENSORS = ( ) -async def async_setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the CO2signal sensor.""" - await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=config, - ) - - async def async_setup_entry(hass, entry, async_add_entities): """Set up the CO2signal sensor.""" coordinator: CO2SignalCoordinator = hass.data[DOMAIN][entry.entry_id] From fc6c0b1d4a37eff116e5c0cced6ec701094bddfc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 20 Dec 2021 16:18:58 +0100 Subject: [PATCH 0809/2644] Add input_button (#62008) * Add input_button * Update homeassistant/components/input_button/__init__.py Co-authored-by: Erik Montnemery * Improve test coverage * Add reload test: not affecting state Co-authored-by: Erik Montnemery --- .core_files.yaml | 1 + .strict-typing | 1 + CODEOWNERS | 2 + .../components/default_config/manifest.json | 1 + homeassistant/components/demo/__init__.py | 16 + .../components/input_button/__init__.py | 171 +++++++++ .../components/input_button/manifest.json | 7 + .../components/input_button/services.yaml | 6 + mypy.ini | 11 + script/hassfest/dependencies.py | 1 + script/hassfest/manifest.py | 1 + tests/components/input_button/__init__.py | 1 + tests/components/input_button/test_init.py | 357 ++++++++++++++++++ 13 files changed, 576 insertions(+) create mode 100644 homeassistant/components/input_button/__init__.py create mode 100644 homeassistant/components/input_button/manifest.json create mode 100644 homeassistant/components/input_button/services.yaml create mode 100644 tests/components/input_button/__init__.py create mode 100644 tests/components/input_button/test_init.py diff --git a/.core_files.yaml b/.core_files.yaml index 27daff11f35..9eb52b635f3 100644 --- a/.core_files.yaml +++ b/.core_files.yaml @@ -65,6 +65,7 @@ components: &components - homeassistant/components/homeassistant/** - homeassistant/components/image/* - homeassistant/components/input_boolean/* + - homeassistant/components/input_button/* - homeassistant/components/input_datetime/* - homeassistant/components/input_number/* - homeassistant/components/input_select/* diff --git a/.strict-typing b/.strict-typing index 2558506c1c2..f9baa9ac01a 100644 --- a/.strict-typing +++ b/.strict-typing @@ -65,6 +65,7 @@ homeassistant.components.http.* homeassistant.components.huawei_lte.* homeassistant.components.hyperion.* homeassistant.components.image_processing.* +homeassistant.components.input_button.* homeassistant.components.input_select.* homeassistant.components.integration.* homeassistant.components.iqvia.* diff --git a/CODEOWNERS b/CODEOWNERS index abc61c3fa99..f65f04a3b75 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -425,6 +425,8 @@ homeassistant/components/influxdb/* @fabaff @mdegat01 tests/components/influxdb/* @fabaff @mdegat01 homeassistant/components/input_boolean/* @home-assistant/core tests/components/input_boolean/* @home-assistant/core +homeassistant/components/input_button/* @home-assistant/core +tests/components/input_button/* @home-assistant/core homeassistant/components/input_datetime/* @home-assistant/core tests/components/input_datetime/* @home-assistant/core homeassistant/components/input_number/* @home-assistant/core diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index 274e53c2f38..88f86034aea 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -11,6 +11,7 @@ "frontend", "history", "input_boolean", + "input_button", "input_datetime", "input_number", "input_select", diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index e4ec3bd671c..3a1fbff2f87 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -119,6 +119,22 @@ async def async_setup(hass, config): ) ) + # Set up input button + tasks.append( + bootstrap.async_setup_component( + hass, + "input_button", + { + "input_button": { + "bell": { + "icon": "mdi:bell-ring-outline", + "name": "Ring bell", + } + } + }, + ) + ) + # Set up input number tasks.append( bootstrap.async_setup_component( diff --git a/homeassistant/components/input_button/__init__.py b/homeassistant/components/input_button/__init__.py new file mode 100644 index 00000000000..c92f073971a --- /dev/null +++ b/homeassistant/components/input_button/__init__.py @@ -0,0 +1,171 @@ +"""Support to keep track of user controlled buttons which can be used in automations.""" +from __future__ import annotations + +import logging +from typing import cast + +import voluptuous as vol + +from homeassistant.components.button import SERVICE_PRESS, ButtonEntity +from homeassistant.const import ( + ATTR_EDITABLE, + CONF_ICON, + CONF_ID, + CONF_NAME, + SERVICE_RELOAD, +) +from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.helpers import collection +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.restore_state import RestoreEntity +import homeassistant.helpers.service +from homeassistant.helpers.storage import Store +from homeassistant.helpers.typing import ConfigType + +DOMAIN = "input_button" + +_LOGGER = logging.getLogger(__name__) + +CREATE_FIELDS = { + vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), + vol.Optional(CONF_ICON): cv.icon, +} + +UPDATE_FIELDS = { + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_ICON): cv.icon, +} + +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: cv.schema_with_slug_keys(vol.Any(UPDATE_FIELDS, None))}, + extra=vol.ALLOW_EXTRA, +) + +RELOAD_SERVICE_SCHEMA = vol.Schema({}) +STORAGE_KEY = DOMAIN +STORAGE_VERSION = 1 + + +class InputButtonStorageCollection(collection.StorageCollection): + """Input button collection stored in storage.""" + + CREATE_SCHEMA = vol.Schema(CREATE_FIELDS) + UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS) + + async def _process_create_data(self, data: dict) -> vol.Schema: + """Validate the config is valid.""" + return self.CREATE_SCHEMA(data) + + @callback + def _get_suggested_id(self, info: dict) -> str: + """Suggest an ID based on the config.""" + return cast(str, info[CONF_NAME]) + + async def _update_data(self, data: dict, update_data: dict) -> dict: + """Return a new updated data object.""" + update_data = self.UPDATE_SCHEMA(update_data) + return {**data, **update_data} + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up an input button.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + id_manager = collection.IDManager() + + yaml_collection = collection.YamlCollection( + logging.getLogger(f"{__name__}.yaml_collection"), id_manager + ) + collection.sync_entity_lifecycle( + hass, DOMAIN, DOMAIN, component, yaml_collection, InputButton.from_yaml + ) + + storage_collection = InputButtonStorageCollection( + Store(hass, STORAGE_VERSION, STORAGE_KEY), + logging.getLogger(f"{__name__}.storage_collection"), + id_manager, + ) + collection.sync_entity_lifecycle( + hass, DOMAIN, DOMAIN, component, storage_collection, InputButton + ) + + await yaml_collection.async_load( + [{CONF_ID: id_, **(conf or {})} for id_, conf in config.get(DOMAIN, {}).items()] + ) + await storage_collection.async_load() + + collection.StorageCollectionWebsocket( + storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS + ).async_setup(hass) + + async def reload_service_handler(service_call: ServiceCall) -> None: + """Remove all input buttons and load new ones from config.""" + conf = await component.async_prepare_reload(skip_reset=True) + if conf is None: + return + await yaml_collection.async_load( + [ + {CONF_ID: id_, **(conf or {})} + for id_, conf in conf.get(DOMAIN, {}).items() + ] + ) + + homeassistant.helpers.service.async_register_admin_service( + hass, + DOMAIN, + SERVICE_RELOAD, + reload_service_handler, + schema=RELOAD_SERVICE_SCHEMA, + ) + + component.async_register_entity_service(SERVICE_PRESS, {}, "_async_press_action") + + return True + + +class InputButton(ButtonEntity, RestoreEntity): + """Representation of a button.""" + + _attr_should_poll = False + + def __init__(self, config: ConfigType) -> None: + """Initialize a button.""" + self._config = config + self.editable = True + self._attr_unique_id = config[CONF_ID] + + @classmethod + def from_yaml(cls, config: ConfigType) -> ButtonEntity: + """Return entity instance initialized from yaml storage.""" + button = cls(config) + button.entity_id = f"{DOMAIN}.{config[CONF_ID]}" + button.editable = False + return button + + @property + def name(self) -> str | None: + """Return name of the button.""" + return self._config.get(CONF_NAME) + + @property + def icon(self) -> str | None: + """Return the icon to be used for this entity.""" + return self._config.get(CONF_ICON) + + @property + def extra_state_attributes(self) -> dict[str, bool]: + """Return the state attributes of the entity.""" + return {ATTR_EDITABLE: self.editable} + + async def async_press(self) -> None: + """Press the button. + + Left emtpty intentionally. + The input button itself doesn't trigger anything. + """ + return None + + async def async_update_config(self, config: ConfigType) -> None: + """Handle when the config is updated.""" + self._config = config + self.async_write_ha_state() diff --git a/homeassistant/components/input_button/manifest.json b/homeassistant/components/input_button/manifest.json new file mode 100644 index 00000000000..76133500d36 --- /dev/null +++ b/homeassistant/components/input_button/manifest.json @@ -0,0 +1,7 @@ +{ + "domain": "input_button", + "name": "Input Button", + "documentation": "https://www.home-assistant.io/integrations/input_button", + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" +} diff --git a/homeassistant/components/input_button/services.yaml b/homeassistant/components/input_button/services.yaml new file mode 100644 index 00000000000..899ead91cb5 --- /dev/null +++ b/homeassistant/components/input_button/services.yaml @@ -0,0 +1,6 @@ +press: + name: Press + description: Press the input button entity. + target: + entity: + domain: input_button diff --git a/mypy.ini b/mypy.ini index d092972f2b0..417e3d39d1c 100644 --- a/mypy.ini +++ b/mypy.ini @@ -726,6 +726,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.input_button.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.input_select.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py index c52a926e4e1..9769e2c3614 100644 --- a/script/hassfest/dependencies.py +++ b/script/hassfest/dependencies.py @@ -102,6 +102,7 @@ ALLOWED_USED_COMPONENTS = { "hassio", "homeassistant", "input_boolean", + "input_button", "input_datetime", "input_number", "input_select", diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index f2185c86fc8..a0c078e325b 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -64,6 +64,7 @@ NO_IOT_CLASS = [ "image_processing", "image", "input_boolean", + "input_button", "input_datetime", "input_number", "input_select", diff --git a/tests/components/input_button/__init__.py b/tests/components/input_button/__init__.py new file mode 100644 index 00000000000..f5fd0e4fa97 --- /dev/null +++ b/tests/components/input_button/__init__.py @@ -0,0 +1 @@ +"""Tests for the input_test component.""" diff --git a/tests/components/input_button/test_init.py b/tests/components/input_button/test_init.py new file mode 100644 index 00000000000..33342455147 --- /dev/null +++ b/tests/components/input_button/test_init.py @@ -0,0 +1,357 @@ +"""The tests for the input_test component.""" +import logging +from unittest.mock import patch + +import pytest + +from homeassistant.components.input_button import DOMAIN, SERVICE_PRESS +from homeassistant.const import ( + ATTR_EDITABLE, + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + ATTR_ICON, + ATTR_NAME, + SERVICE_RELOAD, + STATE_UNKNOWN, +) +from homeassistant.core import Context, CoreState, State +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.event import async_track_state_change +from homeassistant.setup import async_setup_component + +from tests.common import mock_component, mock_restore_cache + +_LOGGER = logging.getLogger(__name__) + + +@pytest.fixture +def storage_setup(hass, hass_storage): + """Storage setup.""" + + async def _storage(items=None, config=None): + if items is None: + hass_storage[DOMAIN] = { + "key": DOMAIN, + "version": 1, + "data": {"items": [{"id": "from_storage", "name": "from storage"}]}, + } + else: + hass_storage[DOMAIN] = items + if config is None: + config = {DOMAIN: {}} + return await async_setup_component(hass, DOMAIN, config) + + return _storage + + +async def test_config(hass): + """Test config.""" + invalid_configs = [None, 1, {}, {"name with space": None}] + + for cfg in invalid_configs: + assert not await async_setup_component(hass, DOMAIN, {DOMAIN: cfg}) + + +async def test_config_options(hass): + """Test configuration options.""" + count_start = len(hass.states.async_entity_ids()) + + _LOGGER.debug("ENTITIES @ start: %s", hass.states.async_entity_ids()) + + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + "test_1": None, + "test_2": {"name": "Hello World", "icon": "mdi:work"}, + } + }, + ) + + _LOGGER.debug("ENTITIES: %s", hass.states.async_entity_ids()) + + assert count_start + 2 == len(hass.states.async_entity_ids()) + + state_1 = hass.states.get("input_button.test_1") + state_2 = hass.states.get("input_button.test_2") + + assert state_1 is not None + assert state_2 is not None + + assert state_1.state == STATE_UNKNOWN + assert ATTR_ICON not in state_1.attributes + assert ATTR_FRIENDLY_NAME not in state_1.attributes + + assert state_2.state == STATE_UNKNOWN + assert state_2.attributes.get(ATTR_FRIENDLY_NAME) == "Hello World" + assert state_2.attributes.get(ATTR_ICON) == "mdi:work" + + +async def test_restore_state(hass): + """Ensure states are restored on startup.""" + mock_restore_cache( + hass, + (State("input_button.b1", "2021-01-01T23:59:59+00:00"),), + ) + + hass.state = CoreState.starting + mock_component(hass, "recorder") + + await async_setup_component(hass, DOMAIN, {DOMAIN: {"b1": None, "b2": None}}) + + state = hass.states.get("input_button.b1") + assert state + assert state.state == "2021-01-01T23:59:59+00:00" + + state = hass.states.get("input_button.b2") + assert state + assert state.state == STATE_UNKNOWN + + +async def test_input_button_context(hass, hass_admin_user): + """Test that input_button context works.""" + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"update": {}}}) + + state = hass.states.get("input_button.update") + assert state is not None + + await hass.services.async_call( + DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: state.entity_id}, + True, + Context(user_id=hass_admin_user.id), + ) + + state2 = hass.states.get("input_button.update") + assert state2 is not None + assert state.state != state2.state + assert state2.context.user_id == hass_admin_user.id + + +async def test_reload(hass, hass_admin_user): + """Test reload service.""" + count_start = len(hass.states.async_entity_ids()) + ent_reg = er.async_get(hass) + + _LOGGER.debug("ENTITIES @ start: %s", hass.states.async_entity_ids()) + + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + "test_1": None, + "test_2": {"name": "Hello World", "icon": "mdi:work"}, + } + }, + ) + + _LOGGER.debug("ENTITIES: %s", hass.states.async_entity_ids()) + + assert count_start + 2 == len(hass.states.async_entity_ids()) + + state_1 = hass.states.get("input_button.test_1") + state_2 = hass.states.get("input_button.test_2") + state_3 = hass.states.get("input_button.test_3") + + assert state_1 is not None + assert state_2 is not None + assert state_3 is None + + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_1") is not None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_2") is not None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_3") is None + + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value={ + DOMAIN: { + "test_2": { + "name": "Hello World reloaded", + "icon": "mdi:work_reloaded", + }, + "test_3": None, + } + }, + ): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + + assert count_start + 2 == len(hass.states.async_entity_ids()) + + state_1 = hass.states.get("input_button.test_1") + state_2 = hass.states.get("input_button.test_2") + state_3 = hass.states.get("input_button.test_3") + + assert state_1 is None + assert state_2 is not None + assert state_3 is not None + + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_1") is None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_2") is not None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_3") is not None + + +async def test_reload_not_changing_state(hass, storage_setup): + """Test reload not changing state.""" + assert await storage_setup() + state_changes = [] + + def state_changed_listener(entity_id, from_s, to_s): + state_changes.append(to_s) + + state = hass.states.get(f"{DOMAIN}.from_storage") + assert state is not None + + async_track_state_change(hass, [f"{DOMAIN}.from_storage"], state_changed_listener) + + # Pressing button changes state + await hass.services.async_call( + DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: state.entity_id}, + True, + ) + await hass.async_block_till_done() + assert len(state_changes) == 1 + + # Reloading does not + with patch( + "homeassistant.config.load_yaml_config_file", autospec=True, return_value={} + ): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + ) + + await hass.async_block_till_done() + + state = hass.states.get(f"{DOMAIN}.from_storage") + assert state is not None + assert len(state_changes) == 1 + + +async def test_load_from_storage(hass, storage_setup): + """Test set up from storage.""" + assert await storage_setup() + state = hass.states.get(f"{DOMAIN}.from_storage") + assert state.state == STATE_UNKNOWN + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "from storage" + assert state.attributes.get(ATTR_EDITABLE) + + +async def test_editable_state_attribute(hass, storage_setup): + """Test editable attribute.""" + assert await storage_setup(config={DOMAIN: {"from_yaml": None}}) + + state = hass.states.get(f"{DOMAIN}.from_storage") + assert state.state == STATE_UNKNOWN + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "from storage" + assert state.attributes.get(ATTR_EDITABLE) + + state = hass.states.get(f"{DOMAIN}.from_yaml") + assert state.state == STATE_UNKNOWN + assert not state.attributes.get(ATTR_EDITABLE) + + +async def test_ws_list(hass, hass_ws_client, storage_setup): + """Test listing via WS.""" + assert await storage_setup(config={DOMAIN: {"from_yaml": None}}) + + client = await hass_ws_client(hass) + + await client.send_json({"id": 6, "type": f"{DOMAIN}/list"}) + resp = await client.receive_json() + assert resp["success"] + + storage_ent = "from_storage" + yaml_ent = "from_yaml" + result = {item["id"]: item for item in resp["result"]} + + assert len(result) == 1 + assert storage_ent in result + assert yaml_ent not in result + assert result[storage_ent][ATTR_NAME] == "from storage" + + +async def test_ws_create_update(hass, hass_ws_client, storage_setup): + """Test creating and updating via WS.""" + assert await storage_setup(config={DOMAIN: {}}) + + client = await hass_ws_client(hass) + + await client.send_json({"id": 7, "type": f"{DOMAIN}/create", "name": "new"}) + resp = await client.receive_json() + assert resp["success"] + + state = hass.states.get(f"{DOMAIN}.new") + assert state is not None + assert state.state == STATE_UNKNOWN + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "new" + + ent_reg = er.async_get(hass) + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "new") is not None + + await client.send_json( + {"id": 8, "type": f"{DOMAIN}/update", f"{DOMAIN}_id": "new", "name": "newer"} + ) + resp = await client.receive_json() + assert resp["success"] + + state = hass.states.get(f"{DOMAIN}.new") + assert state is not None + assert state.state == STATE_UNKNOWN + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "newer" + + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "new") is not None + + +async def test_ws_delete(hass, hass_ws_client, storage_setup): + """Test WS delete cleans up entity registry.""" + assert await storage_setup() + + input_id = "from_storage" + input_entity_id = f"{DOMAIN}.{input_id}" + ent_reg = er.async_get(hass) + + state = hass.states.get(input_entity_id) + assert state is not None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is not None + + client = await hass_ws_client(hass) + + await client.send_json( + {"id": 6, "type": f"{DOMAIN}/delete", f"{DOMAIN}_id": f"{input_id}"} + ) + resp = await client.receive_json() + assert resp["success"] + + state = hass.states.get(input_entity_id) + assert state is None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is None + + +async def test_setup_no_config(hass, hass_admin_user): + """Test component setup with no config.""" + count_start = len(hass.states.async_entity_ids()) + assert await async_setup_component(hass, DOMAIN, {}) + + with patch( + "homeassistant.config.load_yaml_config_file", autospec=True, return_value={} + ): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + + assert count_start == len(hass.states.async_entity_ids()) From f14f0f93cbcf560b10d32f6a4967ce2c67884e67 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 20 Dec 2021 16:22:09 +0100 Subject: [PATCH 0810/2644] Invalidate CI cache when bumping dependencies (#62394) Co-authored-by: Martin Hjelmare --- .github/workflows/ci.yaml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8f9822d2650..cf04ceee518 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -155,10 +155,15 @@ jobs: key: >- ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ steps.generate-python-key.outputs.key }} - restore-keys: | - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_test.txt') }}- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements.txt') }}- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}- + # Temporary disabling the restore of environments when bumping + # a dependency. It seems that we are experiencing issues with + # restoring environments in GitHub Actions, although unclear why. + # First attempt: https://github.com/home-assistant/core/pull/62383 + # + # restore-keys: | + # ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_test.txt') }}- + # ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements.txt') }}- + # ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}- - name: Create Python virtual environment if: steps.cache-venv.outputs.cache-hit != 'true' run: | From e2a95181ada448b2b79be22f44e20f3013aafabd Mon Sep 17 00:00:00 2001 From: Nate Clark Date: Mon, 20 Dec 2021 10:27:06 -0500 Subject: [PATCH 0811/2644] Update CODEOWNERS and correct iot class for Konnected (#62391) --- CODEOWNERS | 4 ++-- homeassistant/components/konnected/manifest.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index f65f04a3b75..65ab446fb97 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -478,8 +478,8 @@ homeassistant/components/knx/* @Julius2342 @farmio @marvin-w tests/components/knx/* @Julius2342 @farmio @marvin-w homeassistant/components/kodi/* @OnFreund @cgtobi tests/components/kodi/* @OnFreund @cgtobi -homeassistant/components/konnected/* @heythisisnate @kit-klein -tests/components/konnected/* @heythisisnate @kit-klein +homeassistant/components/konnected/* @heythisisnate +tests/components/konnected/* @heythisisnate homeassistant/components/kostal_plenticore/* @stegm tests/components/kostal_plenticore/* @stegm homeassistant/components/kraken/* @eifinger diff --git a/homeassistant/components/konnected/manifest.json b/homeassistant/components/konnected/manifest.json index 4838e1ab1e4..c4ba720bc6a 100644 --- a/homeassistant/components/konnected/manifest.json +++ b/homeassistant/components/konnected/manifest.json @@ -10,6 +10,6 @@ } ], "dependencies": ["http"], - "codeowners": ["@heythisisnate", "@kit-klein"], - "iot_class": "local_polling" + "codeowners": ["@heythisisnate"], + "iot_class": "local_push" } From e5cf9b78bdd95df5bdce42e6d47815dde258aaff Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Dec 2021 16:53:21 +0100 Subject: [PATCH 0812/2644] Use new enums in synology_dsm (#62398) Co-authored-by: epenet --- .../components/synology_dsm/const.py | 86 +++++++++---------- 1 file changed, 41 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index 55f45fddabd..5b7c8648416 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -11,28 +11,24 @@ from synology_dsm.api.storage.storage import SynoStorage from synology_dsm.api.surveillance_station import SynoSurveillanceStation from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_SAFETY, - DEVICE_CLASS_UPDATE, + BinarySensorDeviceClass, BinarySensorEntityDescription, ) from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, + SensorDeviceClass, SensorEntityDescription, + SensorStateClass, ) from homeassistant.components.switch import SwitchEntityDescription from homeassistant.const import ( DATA_MEGABYTES, DATA_RATE_KILOBYTES_PER_SECOND, DATA_TERABYTES, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_TIMESTAMP, - ENTITY_CATEGORY_CONFIG, - ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, TEMP_CELSIUS, Platform, ) -from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.entity import EntityCategory, EntityDescription DOMAIN = "synology_dsm" PLATFORMS = [Platform.BINARY_SENSOR, Platform.CAMERA, Platform.SENSOR, Platform.SWITCH] @@ -110,8 +106,8 @@ UPGRADE_BINARY_SENSORS: tuple[SynologyDSMBinarySensorEntityDescription, ...] = ( api_key=SynoCoreUpgrade.API_KEY, key="update_available", name="Update Available", - device_class=DEVICE_CLASS_UPDATE, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.UPDATE, + entity_category=EntityCategory.DIAGNOSTIC, ), ) @@ -120,7 +116,7 @@ SECURITY_BINARY_SENSORS: tuple[SynologyDSMBinarySensorEntityDescription, ...] = api_key=SynoCoreSecurity.API_KEY, key="status", name="Security Status", - device_class=DEVICE_CLASS_SAFETY, + device_class=BinarySensorDeviceClass.SAFETY, ), ) @@ -129,15 +125,15 @@ STORAGE_DISK_BINARY_SENSORS: tuple[SynologyDSMBinarySensorEntityDescription, ... api_key=SynoStorage.API_KEY, key="disk_exceed_bad_sector_thr", name="Exceeded Max Bad Sectors", - device_class=DEVICE_CLASS_SAFETY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.SAFETY, + entity_category=EntityCategory.DIAGNOSTIC, ), SynologyDSMBinarySensorEntityDescription( api_key=SynoStorage.API_KEY, key="disk_below_remain_life_thr", name="Below Min Remaining Life", - device_class=DEVICE_CLASS_SAFETY, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=BinarySensorDeviceClass.SAFETY, + entity_category=EntityCategory.DIAGNOSTIC, ), ) @@ -150,7 +146,7 @@ UTILISATION_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, icon="mdi:chip", entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SynologyDSMSensorEntityDescription( api_key=SynoCoreUtilization.API_KEY, @@ -158,7 +154,7 @@ UTILISATION_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( name="CPU Utilization (User)", native_unit_of_measurement=PERCENTAGE, icon="mdi:chip", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SynologyDSMSensorEntityDescription( api_key=SynoCoreUtilization.API_KEY, @@ -167,7 +163,7 @@ UTILISATION_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, icon="mdi:chip", entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SynologyDSMSensorEntityDescription( api_key=SynoCoreUtilization.API_KEY, @@ -175,7 +171,7 @@ UTILISATION_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( name="CPU Utilization (Total)", native_unit_of_measurement=PERCENTAGE, icon="mdi:chip", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SynologyDSMSensorEntityDescription( api_key=SynoCoreUtilization.API_KEY, @@ -205,7 +201,7 @@ UTILISATION_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( name="Memory Usage (Real)", native_unit_of_measurement=PERCENTAGE, icon="mdi:memory", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SynologyDSMSensorEntityDescription( api_key=SynoCoreUtilization.API_KEY, @@ -214,7 +210,7 @@ UTILISATION_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:memory", entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SynologyDSMSensorEntityDescription( api_key=SynoCoreUtilization.API_KEY, @@ -223,7 +219,7 @@ UTILISATION_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:memory", entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SynologyDSMSensorEntityDescription( api_key=SynoCoreUtilization.API_KEY, @@ -231,7 +227,7 @@ UTILISATION_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( name="Memory Available (Swap)", native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:memory", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SynologyDSMSensorEntityDescription( api_key=SynoCoreUtilization.API_KEY, @@ -239,7 +235,7 @@ UTILISATION_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( name="Memory Available (Real)", native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:memory", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SynologyDSMSensorEntityDescription( api_key=SynoCoreUtilization.API_KEY, @@ -247,7 +243,7 @@ UTILISATION_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( name="Memory Total (Swap)", native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:memory", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SynologyDSMSensorEntityDescription( api_key=SynoCoreUtilization.API_KEY, @@ -255,7 +251,7 @@ UTILISATION_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( name="Memory Total (Real)", native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:memory", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SynologyDSMSensorEntityDescription( api_key=SynoCoreUtilization.API_KEY, @@ -263,7 +259,7 @@ UTILISATION_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( name="Upload Throughput", native_unit_of_measurement=DATA_RATE_KILOBYTES_PER_SECOND, icon="mdi:upload", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SynologyDSMSensorEntityDescription( api_key=SynoCoreUtilization.API_KEY, @@ -271,7 +267,7 @@ UTILISATION_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( name="Download Throughput", native_unit_of_measurement=DATA_RATE_KILOBYTES_PER_SECOND, icon="mdi:download", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), ) STORAGE_VOL_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( @@ -288,7 +284,7 @@ STORAGE_VOL_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( native_unit_of_measurement=DATA_TERABYTES, icon="mdi:chart-pie", entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SynologyDSMSensorEntityDescription( api_key=SynoStorage.API_KEY, @@ -296,7 +292,7 @@ STORAGE_VOL_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( name="Used Space", native_unit_of_measurement=DATA_TERABYTES, icon="mdi:chart-pie", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SynologyDSMSensorEntityDescription( api_key=SynoStorage.API_KEY, @@ -310,17 +306,17 @@ STORAGE_VOL_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( key="volume_disk_temp_avg", name="Average Disk Temp", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=SensorDeviceClass.TEMPERATURE, + entity_category=EntityCategory.DIAGNOSTIC, ), SynologyDSMSensorEntityDescription( api_key=SynoStorage.API_KEY, key="volume_disk_temp_max", name="Maximum Disk Temp", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, entity_registry_enabled_default=False, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), ) STORAGE_DISK_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( @@ -330,23 +326,23 @@ STORAGE_DISK_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( name="Status (Smart)", icon="mdi:checkbox-marked-circle-outline", entity_registry_enabled_default=False, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), SynologyDSMSensorEntityDescription( api_key=SynoStorage.API_KEY, key="disk_status", name="Status", icon="mdi:checkbox-marked-circle-outline", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), SynologyDSMSensorEntityDescription( api_key=SynoStorage.API_KEY, key="disk_temp", name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), ) @@ -356,17 +352,17 @@ INFORMATION_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( key="temperature", name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), SynologyDSMSensorEntityDescription( api_key=SynoDSMInformation.API_KEY, key="uptime", name="Last Boot", - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, entity_registry_enabled_default=False, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), ) @@ -377,6 +373,6 @@ SURVEILLANCE_SWITCH: tuple[SynologyDSMSwitchEntityDescription, ...] = ( key="home_mode", name="Home Mode", icon="mdi:home-account", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=EntityCategory.CONFIG, ), ) From 75000c317b86a674bcedbef350226f3be866bda4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Dec 2021 16:53:40 +0100 Subject: [PATCH 0813/2644] Use new enums in switcher_kis (#62400) Co-authored-by: epenet --- homeassistant/components/switcher_kis/sensor.py | 13 ++++++------- homeassistant/components/switcher_kis/switch.py | 10 +++------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/switcher_kis/sensor.py b/homeassistant/components/switcher_kis/sensor.py index d694bfee380..6b6bf1bec4a 100644 --- a/homeassistant/components/switcher_kis/sensor.py +++ b/homeassistant/components/switcher_kis/sensor.py @@ -6,10 +6,9 @@ from dataclasses import dataclass from aioswitcher.device import DeviceCategory from homeassistant.components.sensor import ( - DEVICE_CLASS_CURRENT, - DEVICE_CLASS_POWER, - STATE_CLASS_MEASUREMENT, + SensorDeviceClass, SensorEntity, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ELECTRIC_CURRENT_AMPERE, POWER_WATT @@ -40,14 +39,14 @@ POWER_SENSORS = { "power_consumption": AttributeDescription( name="Power Consumption", unit=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), "electric_current": AttributeDescription( name="Electric Current", unit=ELECTRIC_CURRENT_AMPERE, - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, ), } diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py index 620a742f4e3..6df841b1e4e 100644 --- a/homeassistant/components/switcher_kis/switch.py +++ b/homeassistant/components/switcher_kis/switch.py @@ -10,11 +10,7 @@ from aioswitcher.api import Command, SwitcherApi, SwitcherBaseResponse from aioswitcher.device import DeviceCategory, DeviceState import voluptuous as vol -from homeassistant.components.switch import ( - DEVICE_CLASS_OUTLET, - DEVICE_CLASS_SWITCH, - SwitchEntity, -) +from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import ( @@ -169,13 +165,13 @@ class SwitcherBaseSwitchEntity(CoordinatorEntity, SwitchEntity): class SwitcherPowerPlugSwitchEntity(SwitcherBaseSwitchEntity): """Representation of a Switcher power plug switch entity.""" - _attr_device_class = DEVICE_CLASS_OUTLET + _attr_device_class = SwitchDeviceClass.OUTLET class SwitcherWaterHeaterSwitchEntity(SwitcherBaseSwitchEntity): """Representation of a Switcher water heater switch entity.""" - _attr_device_class = DEVICE_CLASS_SWITCH + _attr_device_class = SwitchDeviceClass.SWITCH async def async_set_auto_off_service(self, auto_off: timedelta) -> None: """Use for handling setting device auto-off service calls.""" From 23baf6e02a319fc9edf654578f620e8b189b6e70 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Dec 2021 16:53:50 +0100 Subject: [PATCH 0814/2644] Use new enums in syncthru (#62399) Co-authored-by: epenet --- homeassistant/components/syncthru/binary_sensor.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/syncthru/binary_sensor.py b/homeassistant/components/syncthru/binary_sensor.py index 18780e9225e..e3c8317df3d 100644 --- a/homeassistant/components/syncthru/binary_sensor.py +++ b/homeassistant/components/syncthru/binary_sensor.py @@ -4,8 +4,7 @@ from __future__ import annotations from pysyncthru import SyncThru, SyncthruState from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_CONNECTIVITY, - DEVICE_CLASS_PROBLEM, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.const import CONF_NAME @@ -77,7 +76,7 @@ class SyncThruBinarySensor(CoordinatorEntity, BinarySensorEntity): class SyncThruOnlineSensor(SyncThruBinarySensor): """Implementation of a sensor that checks whether is turned on/online.""" - _attr_device_class = DEVICE_CLASS_CONNECTIVITY + _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY def __init__(self, syncthru, name): """Initialize the sensor.""" @@ -93,7 +92,7 @@ class SyncThruOnlineSensor(SyncThruBinarySensor): class SyncThruProblemSensor(SyncThruBinarySensor): """Implementation of a sensor that checks whether the printer works correctly.""" - _attr_device_class = DEVICE_CLASS_PROBLEM + _attr_device_class = BinarySensorDeviceClass.PROBLEM def __init__(self, syncthru, name): """Initialize the sensor.""" From 071e29bf1d8f4f9a6c788be44552770b818b3bea Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Dec 2021 17:15:30 +0100 Subject: [PATCH 0815/2644] Use new enums in systemmonitor (#62401) Co-authored-by: epenet --- .../components/systemmonitor/sensor.py | 52 +++++++++---------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index 8bbdae820e3..5c6294107f8 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -16,10 +16,10 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.const import ( CONF_RESOURCES, @@ -28,8 +28,6 @@ from homeassistant.const import ( DATA_GIBIBYTES, DATA_MEBIBYTES, DATA_RATE_MEGABYTES_PER_SECOND, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_TIMESTAMP, EVENT_HOMEASSISTANT_STOP, PERCENTAGE, STATE_OFF, @@ -80,21 +78,21 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = { name="Disk free", native_unit_of_measurement=DATA_GIBIBYTES, icon="mdi:harddisk", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), "disk_use": SysMonitorSensorEntityDescription( key="disk_use", name="Disk use", native_unit_of_measurement=DATA_GIBIBYTES, icon="mdi:harddisk", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), "disk_use_percent": SysMonitorSensorEntityDescription( key="disk_use_percent", name="Disk use (percent)", native_unit_of_measurement=PERCENTAGE, icon="mdi:harddisk", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), "ipv4_address": SysMonitorSensorEntityDescription( key="ipv4_address", @@ -111,53 +109,53 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = { "last_boot": SysMonitorSensorEntityDescription( key="last_boot", name="Last boot", - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, ), "load_15m": SysMonitorSensorEntityDescription( key="load_15m", name="Load (15m)", icon=CPU_ICON, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), "load_1m": SysMonitorSensorEntityDescription( key="load_1m", name="Load (1m)", icon=CPU_ICON, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), "load_5m": SysMonitorSensorEntityDescription( key="load_5m", name="Load (5m)", icon=CPU_ICON, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), "memory_free": SysMonitorSensorEntityDescription( key="memory_free", name="Memory free", native_unit_of_measurement=DATA_MEBIBYTES, icon="mdi:memory", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), "memory_use": SysMonitorSensorEntityDescription( key="memory_use", name="Memory use", native_unit_of_measurement=DATA_MEBIBYTES, icon="mdi:memory", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), "memory_use_percent": SysMonitorSensorEntityDescription( key="memory_use_percent", name="Memory use (percent)", native_unit_of_measurement=PERCENTAGE, icon="mdi:memory", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), "network_in": SysMonitorSensorEntityDescription( key="network_in", name="Network in", native_unit_of_measurement=DATA_MEBIBYTES, icon="mdi:server-network", - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, mandatory_arg=True, ), "network_out": SysMonitorSensorEntityDescription( @@ -165,21 +163,21 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = { name="Network out", native_unit_of_measurement=DATA_MEBIBYTES, icon="mdi:server-network", - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, mandatory_arg=True, ), "packets_in": SysMonitorSensorEntityDescription( key="packets_in", name="Packets in", icon="mdi:server-network", - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, mandatory_arg=True, ), "packets_out": SysMonitorSensorEntityDescription( key="packets_out", name="Packets out", icon="mdi:server-network", - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, mandatory_arg=True, ), "throughput_network_in": SysMonitorSensorEntityDescription( @@ -187,7 +185,7 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = { name="Network throughput in", native_unit_of_measurement=DATA_RATE_MEGABYTES_PER_SECOND, icon="mdi:server-network", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, mandatory_arg=True, ), "throughput_network_out": SysMonitorSensorEntityDescription( @@ -195,14 +193,14 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = { name="Network throughput out", native_unit_of_measurement=DATA_RATE_MEGABYTES_PER_SECOND, icon="mdi:server-network", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, mandatory_arg=True, ), "process": SysMonitorSensorEntityDescription( key="process", name="Process", icon=CPU_ICON, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, mandatory_arg=True, ), "processor_use": SysMonitorSensorEntityDescription( @@ -210,35 +208,35 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = { name="Processor use", native_unit_of_measurement=PERCENTAGE, icon=CPU_ICON, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), "processor_temperature": SysMonitorSensorEntityDescription( key="processor_temperature", name="Processor temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), "swap_free": SysMonitorSensorEntityDescription( key="swap_free", name="Swap free", native_unit_of_measurement=DATA_MEBIBYTES, icon="mdi:harddisk", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), "swap_use": SysMonitorSensorEntityDescription( key="swap_use", name="Swap use", native_unit_of_measurement=DATA_MEBIBYTES, icon="mdi:harddisk", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), "swap_use_percent": SysMonitorSensorEntityDescription( key="swap_use_percent", name="Swap use (percent)", native_unit_of_measurement=PERCENTAGE, icon="mdi:harddisk", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), } From 8bd03d520b33b625c7ff40f892e064d0db3485c6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Dec 2021 17:15:44 +0100 Subject: [PATCH 0816/2644] Use new enums in system_bridge (#62402) Co-authored-by: epenet --- .../components/system_bridge/binary_sensor.py | 7 +- .../components/system_bridge/sensor.py | 84 +++++++++---------- 2 files changed, 43 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/system_bridge/binary_sensor.py b/homeassistant/components/system_bridge/binary_sensor.py index 7681a428cca..b95a577c89a 100644 --- a/homeassistant/components/system_bridge/binary_sensor.py +++ b/homeassistant/components/system_bridge/binary_sensor.py @@ -7,8 +7,7 @@ from dataclasses import dataclass from systembridge import Bridge from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_BATTERY_CHARGING, - DEVICE_CLASS_UPDATE, + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -31,7 +30,7 @@ BASE_BINARY_SENSOR_TYPES: tuple[SystemBridgeBinarySensorEntityDescription, ...] SystemBridgeBinarySensorEntityDescription( key="version_available", name="New Version Available", - device_class=DEVICE_CLASS_UPDATE, + device_class=BinarySensorDeviceClass.UPDATE, value=lambda bridge: bridge.information.updates.available, ), ) @@ -40,7 +39,7 @@ BATTERY_BINARY_SENSOR_TYPES: tuple[SystemBridgeBinarySensorEntityDescription, .. SystemBridgeBinarySensorEntityDescription( key="battery_is_charging", name="Battery Is Charging", - device_class=DEVICE_CLASS_BATTERY_CHARGING, + device_class=BinarySensorDeviceClass.BATTERY_CHARGING, value=lambda bridge: bridge.battery.isCharging, ), ) diff --git a/homeassistant/components/system_bridge/sensor.py b/homeassistant/components/system_bridge/sensor.py index 3787feecf13..84cf9b7f599 100644 --- a/homeassistant/components/system_bridge/sensor.py +++ b/homeassistant/components/system_bridge/sensor.py @@ -9,18 +9,14 @@ from typing import Final, cast from systembridge import Bridge from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DATA_GIGABYTES, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_TIMESTAMP, - DEVICE_CLASS_VOLTAGE, ELECTRIC_POTENTIAL_VOLT, FREQUENCY_GIGAHERTZ, FREQUENCY_HERTZ, @@ -64,7 +60,7 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = ( SystemBridgeSensorEntityDescription( key="cpu_speed", name="CPU Speed", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=FREQUENCY_GIGAHERTZ, icon="mdi:speedometer", value=lambda bridge: bridge.cpu.currentSpeed.avg, @@ -73,8 +69,8 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = ( key="cpu_temperature", name="CPU Temperature", entity_registry_enabled_default=False, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, value=lambda bridge: bridge.cpu.temperature.main, ), @@ -82,29 +78,29 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = ( key="cpu_voltage", name="CPU Voltage", entity_registry_enabled_default=False, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, value=lambda bridge: bridge.cpu.cpu.voltage, ), SystemBridgeSensorEntityDescription( key="displays_connected", name="Displays Connected", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, icon="mdi:monitor", value=lambda bridge: len(bridge.display.displays), ), SystemBridgeSensorEntityDescription( key="kernel", name="Kernel", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, icon="mdi:devices", value=lambda bridge: bridge.os.kernel, ), SystemBridgeSensorEntityDescription( key="memory_free", name="Memory Free", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=DATA_GIGABYTES, icon="mdi:memory", value=lambda bridge: round(bridge.memory.free / 1000 ** 3, 2), @@ -112,7 +108,7 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = ( SystemBridgeSensorEntityDescription( key="memory_used_percentage", name="Memory Used %", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, icon="mdi:memory", value=lambda bridge: round((bridge.memory.used / bridge.memory.total) * 100, 2), @@ -121,7 +117,7 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = ( key="memory_used", name="Memory Used", entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=DATA_GIGABYTES, icon="mdi:memory", value=lambda bridge: round(bridge.memory.used / 1000 ** 3, 2), @@ -129,14 +125,14 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = ( SystemBridgeSensorEntityDescription( key="os", name="Operating System", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, icon="mdi:devices", value=lambda bridge: f"{bridge.os.distro} {bridge.os.release}", ), SystemBridgeSensorEntityDescription( key="processes_load", name="Load", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, icon="mdi:percent", value=lambda bridge: round(bridge.processes.load.currentLoad, 2), @@ -145,7 +141,7 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = ( key="processes_load_idle", name="Idle Load", entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, icon="mdi:percent", value=lambda bridge: round(bridge.processes.load.currentLoadIdle, 2), @@ -154,7 +150,7 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = ( key="processes_load_system", name="System Load", entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, icon="mdi:percent", value=lambda bridge: round(bridge.processes.load.currentLoadSystem, 2), @@ -163,7 +159,7 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = ( key="processes_load_user", name="User Load", entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, icon="mdi:percent", value=lambda bridge: round(bridge.processes.load.currentLoadUser, 2), @@ -186,16 +182,16 @@ BATTERY_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = ( SystemBridgeSensorEntityDescription( key="battery", name="Battery", - device_class=DEVICE_CLASS_BATTERY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, value=lambda bridge: bridge.battery.percent, ), SystemBridgeSensorEntityDescription( key="battery_time_remaining", name="Battery Time Remaining", - device_class=DEVICE_CLASS_TIMESTAMP, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TIMESTAMP, + state_class=SensorStateClass.MEASUREMENT, value=lambda bridge: str( datetime.now() + timedelta(minutes=bridge.battery.timeRemaining) ), @@ -221,7 +217,7 @@ async def async_setup_entry( SystemBridgeSensorEntityDescription( key=f"filesystem_{uid}", name=f"{key} Space Used", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, icon="mdi:harddisk", value=lambda bridge, i=key: round( @@ -244,7 +240,7 @@ async def async_setup_entry( SystemBridgeSensorEntityDescription( key=f"display_{name}_resolution_x", name=f"Display {name} Resolution X", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PIXELS, icon="mdi:monitor", value=lambda bridge, i=index: bridge.display.displays[ @@ -257,7 +253,7 @@ async def async_setup_entry( SystemBridgeSensorEntityDescription( key=f"display_{name}_resolution_y", name=f"Display {name} Resolution Y", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PIXELS, icon="mdi:monitor", value=lambda bridge, i=index: bridge.display.displays[ @@ -270,7 +266,7 @@ async def async_setup_entry( SystemBridgeSensorEntityDescription( key=f"display_{name}_refresh_rate", name=f"Display {name} Refresh Rate", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=FREQUENCY_HERTZ, icon="mdi:monitor", value=lambda bridge, i=index: bridge.display.displays[ @@ -296,7 +292,7 @@ async def async_setup_entry( key=f"gpu_{index}_core_clock_speed", name=f"{name} Clock Speed", entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=FREQUENCY_MEGAHERTZ, icon="mdi:speedometer", value=lambda bridge, i=index: bridge.graphics.controllers[ @@ -310,7 +306,7 @@ async def async_setup_entry( key=f"gpu_{index}_memory_clock_speed", name=f"{name} Memory Clock Speed", entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=FREQUENCY_MEGAHERTZ, icon="mdi:speedometer", value=lambda bridge, i=index: bridge.graphics.controllers[ @@ -323,7 +319,7 @@ async def async_setup_entry( SystemBridgeSensorEntityDescription( key=f"gpu_{index}_memory_free", name=f"{name} Memory Free", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=DATA_GIGABYTES, icon="mdi:memory", value=lambda bridge, i=index: round( @@ -336,7 +332,7 @@ async def async_setup_entry( SystemBridgeSensorEntityDescription( key=f"gpu_{index}_memory_used_percentage", name=f"{name} Memory Used %", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, icon="mdi:memory", value=lambda bridge, i=index: round( @@ -355,7 +351,7 @@ async def async_setup_entry( key=f"gpu_{index}_memory_used", name=f"{name} Memory Used", entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=DATA_GIGABYTES, icon="mdi:memory", value=lambda bridge, i=index: round( @@ -369,7 +365,7 @@ async def async_setup_entry( key=f"gpu_{index}_fan_speed", name=f"{name} Fan Speed", entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, icon="mdi:fan", value=lambda bridge, i=index: bridge.graphics.controllers[ @@ -383,8 +379,8 @@ async def async_setup_entry( key=f"gpu_{index}_power_usage", name=f"{name} Power Usage", entity_registry_enabled_default=False, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_WATT, value=lambda bridge, i=index: bridge.graphics.controllers[ i @@ -397,8 +393,8 @@ async def async_setup_entry( key=f"gpu_{index}_temperature", name=f"{name} Temperature", entity_registry_enabled_default=False, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, value=lambda bridge, i=index: bridge.graphics.controllers[ i @@ -410,7 +406,7 @@ async def async_setup_entry( SystemBridgeSensorEntityDescription( key=f"gpu_{index}_usage_percentage", name=f"{name} Usage %", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, icon="mdi:percent", value=lambda bridge, i=index: bridge.graphics.controllers[ @@ -429,7 +425,7 @@ async def async_setup_entry( key=f"processes_load_cpu_{index}", name=f"Load CPU {index}", entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, icon="mdi:percent", value=lambda bridge, index=index: round( @@ -443,7 +439,7 @@ async def async_setup_entry( key=f"processes_load_cpu_{index}_idle", name=f"Idle Load CPU {index}", entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, icon="mdi:percent", value=lambda bridge, index=index: round( @@ -457,7 +453,7 @@ async def async_setup_entry( key=f"processes_load_cpu_{index}_system", name=f"System Load CPU {index}", entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, icon="mdi:percent", value=lambda bridge, index=index: round( @@ -471,7 +467,7 @@ async def async_setup_entry( key=f"processes_load_cpu_{index}_user", name=f"User Load CPU {index}", entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, icon="mdi:percent", value=lambda bridge, index=index: round( From 5251c1b934c5a01f762e842e5873777e0900cdf3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Dec 2021 17:16:20 +0100 Subject: [PATCH 0817/2644] Use new enums in speedtestdotnet (#62405) Co-authored-by: epenet --- homeassistant/components/speedtestdotnet/const.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/speedtestdotnet/const.py b/homeassistant/components/speedtestdotnet/const.py index caed1f408ba..735c656134b 100644 --- a/homeassistant/components/speedtestdotnet/const.py +++ b/homeassistant/components/speedtestdotnet/const.py @@ -5,10 +5,7 @@ from collections.abc import Callable from dataclasses import dataclass from typing import Final -from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - SensorEntityDescription, -) +from homeassistant.components.sensor import SensorEntityDescription, SensorStateClass from homeassistant.const import ( DATA_RATE_MEGABITS_PER_SECOND, TIME_MILLISECONDS, @@ -32,20 +29,20 @@ SENSOR_TYPES: Final[tuple[SpeedtestSensorEntityDescription, ...]] = ( key="ping", name="Ping", native_unit_of_measurement=TIME_MILLISECONDS, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SpeedtestSensorEntityDescription( key="download", name="Download", native_unit_of_measurement=DATA_RATE_MEGABITS_PER_SECOND, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, value=lambda value: round(value / 10 ** 6, 2), ), SpeedtestSensorEntityDescription( key="upload", name="Upload", native_unit_of_measurement=DATA_RATE_MEGABITS_PER_SECOND, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, value=lambda value: round(value / 10 ** 6, 2), ), ) From bea1fbb4aa4e19d8a599ffb11ed06934244fe9f2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Dec 2021 17:38:31 +0100 Subject: [PATCH 0818/2644] Use new enums in supla (#62406) Co-authored-by: epenet --- homeassistant/components/supla/cover.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/supla/cover.py b/homeassistant/components/supla/cover.py index c76b28e9de0..0c6715e3728 100644 --- a/homeassistant/components/supla/cover.py +++ b/homeassistant/components/supla/cover.py @@ -2,11 +2,7 @@ import logging from pprint import pformat -from homeassistant.components.cover import ( - ATTR_POSITION, - DEVICE_CLASS_GARAGE, - CoverEntity, -) +from homeassistant.components.cover import ATTR_POSITION, CoverDeviceClass, CoverEntity from . import DOMAIN, SUPLA_COORDINATORS, SUPLA_SERVERS, SuplaChannel @@ -115,4 +111,4 @@ class SuplaGateDoor(SuplaChannel, CoverEntity): @property def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" - return DEVICE_CLASS_GARAGE + return CoverDeviceClass.GARAGE From ae3162bb6ee85af80dcfee04de8bb689133fa9e5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Dec 2021 17:55:37 +0100 Subject: [PATCH 0819/2644] Use new enums in starline (#62407) Co-authored-by: epenet --- .../components/starline/binary_sensor.py | 15 ++++++--------- homeassistant/components/starline/sensor.py | 6 +++--- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/starline/binary_sensor.py b/homeassistant/components/starline/binary_sensor.py index e2e3d7ea4fa..c5a96322d43 100644 --- a/homeassistant/components/starline/binary_sensor.py +++ b/homeassistant/components/starline/binary_sensor.py @@ -4,10 +4,7 @@ from __future__ import annotations from dataclasses import dataclass from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_DOOR, - DEVICE_CLASS_LOCK, - DEVICE_CLASS_POWER, - DEVICE_CLASS_PROBLEM, + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -35,27 +32,27 @@ BINARY_SENSOR_TYPES: tuple[StarlineBinarySensorEntityDescription, ...] = ( StarlineBinarySensorEntityDescription( key="hbrake", name_="Hand Brake", - device_class=DEVICE_CLASS_POWER, + device_class=BinarySensorDeviceClass.POWER, ), StarlineBinarySensorEntityDescription( key="hood", name_="Hood", - device_class=DEVICE_CLASS_DOOR, + device_class=BinarySensorDeviceClass.DOOR, ), StarlineBinarySensorEntityDescription( key="trunk", name_="Trunk", - device_class=DEVICE_CLASS_DOOR, + device_class=BinarySensorDeviceClass.DOOR, ), StarlineBinarySensorEntityDescription( key="alarm", name_="Alarm", - device_class=DEVICE_CLASS_PROBLEM, + device_class=BinarySensorDeviceClass.PROBLEM, ), StarlineBinarySensorEntityDescription( key="door", name_="Doors", - device_class=DEVICE_CLASS_LOCK, + device_class=BinarySensorDeviceClass.LOCK, ), ) diff --git a/homeassistant/components/starline/sensor.py b/homeassistant/components/starline/sensor.py index 9ce3aa3bc08..472faa44195 100644 --- a/homeassistant/components/starline/sensor.py +++ b/homeassistant/components/starline/sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from dataclasses import dataclass from homeassistant.components.sensor import ( - DEVICE_CLASS_TEMPERATURE, + SensorDeviceClass, SensorEntity, SensorEntityDescription, ) @@ -50,13 +50,13 @@ SENSOR_TYPES: tuple[StarlineSensorEntityDescription, ...] = ( StarlineSensorEntityDescription( key="ctemp", name_="Interior Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, ), StarlineSensorEntityDescription( key="etemp", name_="Engine Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, ), StarlineSensorEntityDescription( From 7da7a8434eac86c3e4da852dd966351521d89741 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Dec 2021 18:03:08 +0100 Subject: [PATCH 0820/2644] Use new enums in switchbot (#62404) Co-authored-by: epenet --- .../components/switchbot/binary_sensor.py | 5 +++-- homeassistant/components/switchbot/cover.py | 4 ++-- homeassistant/components/switchbot/sensor.py | 15 ++++++++------- homeassistant/components/switchbot/switch.py | 4 ++-- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/switchbot/binary_sensor.py b/homeassistant/components/switchbot/binary_sensor.py index 899066ff502..c8b3aec4573 100644 --- a/homeassistant/components/switchbot/binary_sensor.py +++ b/homeassistant/components/switchbot/binary_sensor.py @@ -6,8 +6,9 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_MAC, CONF_NAME, ENTITY_CATEGORY_DIAGNOSTIC +from homeassistant.const import CONF_MAC, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DATA_COORDINATOR, DOMAIN @@ -19,7 +20,7 @@ PARALLEL_UPDATES = 1 BINARY_SENSOR_TYPES: dict[str, BinarySensorEntityDescription] = { "calibration": BinarySensorEntityDescription( key="calibration", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, ), } diff --git a/homeassistant/components/switchbot/cover.py b/homeassistant/components/switchbot/cover.py index 5582b8b4ed6..9814534ce6d 100644 --- a/homeassistant/components/switchbot/cover.py +++ b/homeassistant/components/switchbot/cover.py @@ -9,11 +9,11 @@ from switchbot import SwitchbotCurtain # pylint: disable=import-error from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, ATTR_POSITION, - DEVICE_CLASS_CURTAIN, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION, SUPPORT_STOP, + CoverDeviceClass, CoverEntity, ) from homeassistant.config_entries import ConfigEntry @@ -60,7 +60,7 @@ class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity): """Representation of a Switchbot.""" coordinator: SwitchbotDataUpdateCoordinator - _attr_device_class = DEVICE_CLASS_CURTAIN + _attr_device_class = CoverDeviceClass.CURTAIN _attr_supported_features = ( SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP | SUPPORT_SET_POSITION ) diff --git a/homeassistant/components/switchbot/sensor.py b/homeassistant/components/switchbot/sensor.py index 3d1eb9f4858..c3afff27654 100644 --- a/homeassistant/components/switchbot/sensor.py +++ b/homeassistant/components/switchbot/sensor.py @@ -1,14 +1,15 @@ """Support for SwitchBot sensors.""" from __future__ import annotations -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_MAC, CONF_NAME, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_SIGNAL_STRENGTH, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ) @@ -26,20 +27,20 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { "rssi": SensorEntityDescription( key="rssi", native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, ), "battery": SensorEntityDescription( key="battery", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_BATTERY, + device_class=SensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, ), "lightLevel": SensorEntityDescription( key="lightLevel", native_unit_of_measurement="Level", - device_class=DEVICE_CLASS_ILLUMINANCE, + device_class=SensorDeviceClass.ILLUMINANCE, ), } diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index a09255d0392..76353559756 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -8,8 +8,8 @@ from switchbot import Switchbot # pylint: disable=import-error import voluptuous as vol from homeassistant.components.switch import ( - DEVICE_CLASS_SWITCH, PLATFORM_SCHEMA, + SwitchDeviceClass, SwitchEntity, ) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -99,7 +99,7 @@ class SwitchBotBotEntity(SwitchbotEntity, SwitchEntity, RestoreEntity): """Representation of a Switchbot.""" coordinator: SwitchbotDataUpdateCoordinator - _attr_device_class = DEVICE_CLASS_SWITCH + _attr_device_class = SwitchDeviceClass.SWITCH def __init__( self, From de88d4306899c01281523bfc7aca6c0d2182e13f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Dec 2021 18:03:52 +0100 Subject: [PATCH 0821/2644] Use new enums in subaru (#62403) Co-authored-by: epenet --- homeassistant/components/subaru/sensor.py | 27 +++++++++++------------ 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/subaru/sensor.py b/homeassistant/components/subaru/sensor.py index 7aeab66b929..0dc11d619fe 100644 --- a/homeassistant/components/subaru/sensor.py +++ b/homeassistant/components/subaru/sensor.py @@ -1,13 +1,12 @@ """Support for Subaru sensors.""" import subarulink.const as sc -from homeassistant.components.sensor import DEVICE_CLASSES, SensorEntity +from homeassistant.components.sensor import ( + DEVICE_CLASSES, + SensorDeviceClass, + SensorEntity, +) from homeassistant.const import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_TIMESTAMP, - DEVICE_CLASS_VOLTAGE, ELECTRIC_POTENTIAL_VOLT, LENGTH_KILOMETERS, LENGTH_MILES, @@ -79,37 +78,37 @@ API_GEN_2_SENSORS = [ }, { SENSOR_TYPE: "Tire Pressure FL", - SENSOR_CLASS: DEVICE_CLASS_PRESSURE, + SENSOR_CLASS: SensorDeviceClass.PRESSURE, SENSOR_FIELD: sc.TIRE_PRESSURE_FL, SENSOR_UNITS: PRESSURE_HPA, }, { SENSOR_TYPE: "Tire Pressure FR", - SENSOR_CLASS: DEVICE_CLASS_PRESSURE, + SENSOR_CLASS: SensorDeviceClass.PRESSURE, SENSOR_FIELD: sc.TIRE_PRESSURE_FR, SENSOR_UNITS: PRESSURE_HPA, }, { SENSOR_TYPE: "Tire Pressure RL", - SENSOR_CLASS: DEVICE_CLASS_PRESSURE, + SENSOR_CLASS: SensorDeviceClass.PRESSURE, SENSOR_FIELD: sc.TIRE_PRESSURE_RL, SENSOR_UNITS: PRESSURE_HPA, }, { SENSOR_TYPE: "Tire Pressure RR", - SENSOR_CLASS: DEVICE_CLASS_PRESSURE, + SENSOR_CLASS: SensorDeviceClass.PRESSURE, SENSOR_FIELD: sc.TIRE_PRESSURE_RR, SENSOR_UNITS: PRESSURE_HPA, }, { SENSOR_TYPE: "External Temp", - SENSOR_CLASS: DEVICE_CLASS_TEMPERATURE, + SENSOR_CLASS: SensorDeviceClass.TEMPERATURE, SENSOR_FIELD: sc.EXTERNAL_TEMP, SENSOR_UNITS: TEMP_CELSIUS, }, { SENSOR_TYPE: "12V Battery Voltage", - SENSOR_CLASS: DEVICE_CLASS_VOLTAGE, + SENSOR_CLASS: SensorDeviceClass.VOLTAGE, SENSOR_FIELD: sc.BATTERY_VOLTAGE, SENSOR_UNITS: ELECTRIC_POTENTIAL_VOLT, }, @@ -125,13 +124,13 @@ EV_SENSORS = [ }, { SENSOR_TYPE: "EV Battery Level", - SENSOR_CLASS: DEVICE_CLASS_BATTERY, + SENSOR_CLASS: SensorDeviceClass.BATTERY, SENSOR_FIELD: sc.EV_STATE_OF_CHARGE_PERCENT, SENSOR_UNITS: PERCENTAGE, }, { SENSOR_TYPE: "EV Time to Full Charge", - SENSOR_CLASS: DEVICE_CLASS_TIMESTAMP, + SENSOR_CLASS: SensorDeviceClass.TIMESTAMP, SENSOR_FIELD: sc.EV_TIME_TO_FULLY_CHARGED, SENSOR_UNITS: TIME_MINUTES, }, From 36a57b00d96e2ffed57572aaa3de074305e03244 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Dec 2021 18:05:04 +0100 Subject: [PATCH 0822/2644] Use attr** in solax (#62397) Co-authored-by: epenet --- homeassistant/components/solax/sensor.py | 32 ++++++------------------ 1 file changed, 7 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/solax/sensor.py b/homeassistant/components/solax/sensor.py index 616bc99821c..a26aa533d29 100644 --- a/homeassistant/components/solax/sensor.py +++ b/homeassistant/components/solax/sensor.py @@ -98,6 +98,8 @@ class RealTimeDataEndpoint: class Inverter(SensorEntity): """Class for a sensor.""" + _attr_should_poll = False + def __init__( self, uid, @@ -108,35 +110,15 @@ class Inverter(SensorEntity): device_class=None, ): """Initialize an inverter sensor.""" - self.uid = uid - self.serial = serial - self.key = key - self.value = None - self.unit = unit + self._attr_unique_id = uid + self._attr_name = f"Solax {serial} {key}" + self._attr_native_unit_of_measurement = unit self._attr_state_class = state_class self._attr_device_class = device_class + self.key = key + self.value = None @property def native_value(self): """State of this inverter attribute.""" return self.value - - @property - def unique_id(self): - """Return unique id.""" - return self.uid - - @property - def name(self): - """Name of this inverter attribute.""" - return f"Solax {self.serial} {self.key}" - - @property - def native_unit_of_measurement(self): - """Return the unit of measurement.""" - return self.unit - - @property - def should_poll(self): - """No polling needed.""" - return False From bae82d76b5054452beacd39d504d742a8e361323 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Dec 2021 18:06:33 +0100 Subject: [PATCH 0823/2644] Use new enums in spider (#62396) Co-authored-by: epenet --- homeassistant/components/spider/sensor.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/spider/sensor.py b/homeassistant/components/spider/sensor.py index c390e060194..eb7259b0412 100644 --- a/homeassistant/components/spider/sensor.py +++ b/homeassistant/components/spider/sensor.py @@ -1,15 +1,10 @@ """Support for Spider Powerplugs (energy & power).""" from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, + SensorStateClass, ) -from homeassistant.const import ( - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - ENERGY_KILO_WATT_HOUR, - POWER_WATT, -) +from homeassistant.const import ENERGY_KILO_WATT_HOUR, POWER_WATT from homeassistant.helpers.entity import DeviceInfo from .const import DOMAIN @@ -31,8 +26,8 @@ class SpiderPowerPlugEnergy(SensorEntity): """Representation of a Spider Power Plug (energy).""" _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR - _attr_device_class = DEVICE_CLASS_ENERGY - _attr_state_class = STATE_CLASS_TOTAL_INCREASING + _attr_device_class = SensorDeviceClass.ENERGY + _attr_state_class = SensorStateClass.TOTAL_INCREASING def __init__(self, api, power_plug) -> None: """Initialize the Spider Power Plug.""" @@ -72,8 +67,8 @@ class SpiderPowerPlugEnergy(SensorEntity): class SpiderPowerPlugPower(SensorEntity): """Representation of a Spider Power Plug (power).""" - _attr_device_class = DEVICE_CLASS_POWER - _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_device_class = SensorDeviceClass.POWER + _attr_state_class = SensorStateClass.MEASUREMENT _attr_native_unit_of_measurement = POWER_WATT def __init__(self, api, power_plug) -> None: From ac0f655fd65b57499e63ed7483ae60ed1a1d7638 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Mon, 20 Dec 2021 17:09:28 +0000 Subject: [PATCH 0824/2644] Use DeviceClass Enums in devolo_home_network tests (#62117) --- tests/components/devolo_home_network/test_sensor.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/components/devolo_home_network/test_sensor.py b/tests/components/devolo_home_network/test_sensor.py index 6ce4c36cdb5..a7b1be5f07d 100644 --- a/tests/components/devolo_home_network/test_sensor.py +++ b/tests/components/devolo_home_network/test_sensor.py @@ -8,10 +8,11 @@ from homeassistant.components.devolo_home_network.const import ( LONG_UPDATE_INTERVAL, SHORT_UPDATE_INTERVAL, ) -from homeassistant.components.sensor import DOMAIN, STATE_CLASS_MEASUREMENT -from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC, STATE_UNAVAILABLE +from homeassistant.components.sensor import DOMAIN, SensorStateClass +from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry +from homeassistant.helpers.entity import EntityCategory from homeassistant.util import dt from . import configure_integration @@ -47,7 +48,7 @@ async def test_update_connected_wifi_clients(hass: HomeAssistant): state = hass.states.get(state_key) assert state is not None assert state.state == "1" - assert state.attributes["state_class"] == STATE_CLASS_MEASUREMENT + assert state.attributes["state_class"] == SensorStateClass.MEASUREMENT # Emulate device failure with patch( @@ -89,7 +90,7 @@ async def test_update_neighboring_wifi_networks(hass: HomeAssistant): assert state.state == "1" er = entity_registry.async_get(hass) - assert er.async_get(state_key).entity_category == ENTITY_CATEGORY_DIAGNOSTIC + assert er.async_get(state_key).entity_category is EntityCategory.DIAGNOSTIC # Emulate device failure with patch( @@ -131,7 +132,7 @@ async def test_update_connected_plc_devices(hass: HomeAssistant): assert state.state == "1" er = entity_registry.async_get(hass) - assert er.async_get(state_key).entity_category == ENTITY_CATEGORY_DIAGNOSTIC + assert er.async_get(state_key).entity_category is EntityCategory.DIAGNOSTIC # Emulate device failure with patch( From c931044d4662b76124f8e7289566b61ff5875fdb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 20 Dec 2021 18:09:36 +0100 Subject: [PATCH 0825/2644] Invalidate CI cache when bumping dependencies, part 2 (#62412) --- .github/workflows/ci.yaml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index cf04ceee518..23bfe6adbaf 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -522,10 +522,15 @@ jobs: key: >- ${{ runner.os }}-${{ matrix.python-version }}-${{ steps.generate-python-key.outputs.key }} - restore-keys: | - ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('requirements_all.txt') }}- - ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements_test.txt') }}- - ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}- + # Temporary disabling the restore of environments when bumping + # a dependency. It seems that we are experiencing issues with + # restoring environments in GitHub Actions, although unclear why. + # First attempt: https://github.com/home-assistant/core/pull/62383 + # + # restore-keys: | + # ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('requirements_all.txt') }}- + # ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements_test.txt') }}- + # ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}- - name: Create full Python ${{ matrix.python-version }} virtual environment if: steps.cache-venv.outputs.cache-hit != 'true' run: | From 527d3a9e5bcc5bad4b1b3ba709172b4a3adacff8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Dec 2021 18:16:53 +0100 Subject: [PATCH 0826/2644] Use new enums in solaredge (#62373) Co-authored-by: epenet --- homeassistant/components/solaredge/const.py | 50 ++++++++------------ homeassistant/components/solaredge/sensor.py | 7 ++- 2 files changed, 23 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/solaredge/const.py b/homeassistant/components/solaredge/const.py index 6e353ffe339..916a01de9e9 100644 --- a/homeassistant/components/solaredge/const.py +++ b/homeassistant/components/solaredge/const.py @@ -2,18 +2,8 @@ from datetime import timedelta import logging -from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL, - STATE_CLASS_TOTAL_INCREASING, -) -from homeassistant.const import ( - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - ENERGY_WATT_HOUR, - PERCENTAGE, - POWER_WATT, -) +from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass +from homeassistant.const import ENERGY_WATT_HOUR, PERCENTAGE, POWER_WATT from .models import SolarEdgeSensorEntityDescription @@ -43,9 +33,9 @@ SENSOR_TYPES = [ json_key="lifeTimeData", name="Lifetime energy", icon="mdi:solar-power", - state_class=STATE_CLASS_TOTAL, + state_class=SensorStateClass.TOTAL, native_unit_of_measurement=ENERGY_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, ), SolarEdgeSensorEntityDescription( key="energy_this_year", @@ -54,7 +44,7 @@ SENSOR_TYPES = [ entity_registry_enabled_default=False, icon="mdi:solar-power", native_unit_of_measurement=ENERGY_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, ), SolarEdgeSensorEntityDescription( key="energy_this_month", @@ -63,7 +53,7 @@ SENSOR_TYPES = [ entity_registry_enabled_default=False, icon="mdi:solar-power", native_unit_of_measurement=ENERGY_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, ), SolarEdgeSensorEntityDescription( key="energy_today", @@ -72,16 +62,16 @@ SENSOR_TYPES = [ entity_registry_enabled_default=False, icon="mdi:solar-power", native_unit_of_measurement=ENERGY_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, ), SolarEdgeSensorEntityDescription( key="current_power", json_key="currentPower", name="Current Power", icon="mdi:solar-power", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, + device_class=SensorDeviceClass.POWER, ), SolarEdgeSensorEntityDescription( key="site_details", @@ -151,52 +141,52 @@ SENSOR_TYPES = [ json_key="Purchased", name="Imported Energy", entity_registry_enabled_default=False, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=ENERGY_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, ), SolarEdgeSensorEntityDescription( key="production_energy", json_key="Production", name="Production Energy", entity_registry_enabled_default=False, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=ENERGY_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, ), SolarEdgeSensorEntityDescription( key="consumption_energy", json_key="Consumption", name="Consumption Energy", entity_registry_enabled_default=False, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=ENERGY_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, ), SolarEdgeSensorEntityDescription( key="selfconsumption_energy", json_key="SelfConsumption", name="SelfConsumption Energy", entity_registry_enabled_default=False, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=ENERGY_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, ), SolarEdgeSensorEntityDescription( key="feedin_energy", json_key="FeedIn", name="Exported Energy", entity_registry_enabled_default=False, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=ENERGY_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, + device_class=SensorDeviceClass.ENERGY, ), SolarEdgeSensorEntityDescription( key="storage_level", json_key="STORAGE", name="Storage Level", entity_registry_enabled_default=False, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, ), ] diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index a151a50a9c8..a769a043442 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -5,9 +5,8 @@ from typing import Any from solaredge import Solaredge -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import DEVICE_CLASS_BATTERY, DEVICE_CLASS_POWER from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -203,7 +202,7 @@ class SolarEdgeEnergyDetailsSensor(SolarEdgeSensorEntity): class SolarEdgePowerFlowSensor(SolarEdgeSensorEntity): """Representation of an SolarEdge Monitoring API power flow sensor.""" - _attr_device_class = DEVICE_CLASS_POWER + _attr_device_class = SensorDeviceClass.POWER def __init__( self, @@ -230,7 +229,7 @@ class SolarEdgePowerFlowSensor(SolarEdgeSensorEntity): class SolarEdgeStorageLevelSensor(SolarEdgeSensorEntity): """Representation of an SolarEdge Monitoring API storage level sensor.""" - _attr_device_class = DEVICE_CLASS_BATTERY + _attr_device_class = SensorDeviceClass.BATTERY @property def native_value(self) -> str | None: From ff7a0dabb2018dbd7714b9ef300f6d3fc139ea80 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Dec 2021 18:28:52 +0100 Subject: [PATCH 0827/2644] Use new enums in somfy (#62378) Co-authored-by: epenet --- homeassistant/components/somfy/cover.py | 7 +++---- homeassistant/components/somfy/sensor.py | 6 +++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/somfy/cover.py b/homeassistant/components/somfy/cover.py index 8ed06b3bcd7..ac89f9b609e 100644 --- a/homeassistant/components/somfy/cover.py +++ b/homeassistant/components/somfy/cover.py @@ -6,8 +6,6 @@ from pymfy.api.devices.category import Category from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, - DEVICE_CLASS_BLIND, - DEVICE_CLASS_SHUTTER, SUPPORT_CLOSE, SUPPORT_CLOSE_TILT, SUPPORT_OPEN, @@ -16,6 +14,7 @@ from homeassistant.components.cover import ( SUPPORT_SET_TILT_POSITION, SUPPORT_STOP, SUPPORT_STOP_TILT, + CoverDeviceClass, CoverEntity, ) from homeassistant.const import CONF_OPTIMISTIC, STATE_CLOSED, STATE_OPEN @@ -123,9 +122,9 @@ class SomfyCover(SomfyEntity, RestoreEntity, CoverEntity): def device_class(self): """Return the device class.""" if self.categories & BLIND_DEVICE_CATEGORIES: - return DEVICE_CLASS_BLIND + return CoverDeviceClass.BLIND if self.categories & SHUTTER_DEVICE_CATEGORIES: - return DEVICE_CLASS_SHUTTER + return CoverDeviceClass.SHUTTER return None @property diff --git a/homeassistant/components/somfy/sensor.py b/homeassistant/components/somfy/sensor.py index 9a0602cb592..c39565e6e3b 100644 --- a/homeassistant/components/somfy/sensor.py +++ b/homeassistant/components/somfy/sensor.py @@ -3,8 +3,8 @@ from pymfy.api.devices.category import Category from pymfy.api.devices.thermostat import Thermostat -from homeassistant.components.sensor import SensorEntity -from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.const import PERCENTAGE from .const import COORDINATOR, DOMAIN from .entity import SomfyEntity @@ -29,7 +29,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class SomfyThermostatBatterySensor(SomfyEntity, SensorEntity): """Representation of a Somfy thermostat battery.""" - _attr_device_class = DEVICE_CLASS_BATTERY + _attr_device_class = SensorDeviceClass.BATTERY _attr_native_unit_of_measurement = PERCENTAGE def __init__(self, coordinator, device_id): From afc42ff835ca407a3699686062c68b9621f63dac Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Mon, 20 Dec 2021 18:31:59 +0100 Subject: [PATCH 0828/2644] Bump brunt to 1.1.0 (#62386) --- homeassistant/components/brunt/__init__.py | 2 +- homeassistant/components/brunt/cover.py | 21 +++++++++----------- homeassistant/components/brunt/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 13 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/brunt/__init__.py b/homeassistant/components/brunt/__init__.py index 37c9fd73632..988a96ce08e 100644 --- a/homeassistant/components/brunt/__init__.py +++ b/homeassistant/components/brunt/__init__.py @@ -45,7 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: async with async_timeout.timeout(10): things = await bapi.async_get_things(force=True) - return {thing.SERIAL: thing for thing in things} + return {thing.serial: thing for thing in things} except ServerDisconnectedError as err: raise UpdateFailed(f"Error communicating with API: {err}") from err except ClientResponseError as err: diff --git a/homeassistant/components/brunt/cover.py b/homeassistant/components/brunt/cover.py index faa4653d3be..08b7eade612 100644 --- a/homeassistant/components/brunt/cover.py +++ b/homeassistant/components/brunt/cover.py @@ -100,7 +100,7 @@ class BruntDevice(CoordinatorEntity, CoverEntity): self._remove_update_listener = None - self._attr_name = self._thing.NAME + self._attr_name = self._thing.name self._attr_device_class = CoverDeviceClass.SHADE self._attr_supported_features = COVER_FEATURES self._attr_attribution = ATTRIBUTION @@ -109,8 +109,8 @@ class BruntDevice(CoordinatorEntity, CoverEntity): name=self._attr_name, via_device=(DOMAIN, self._entry_id), manufacturer="Brunt", - sw_version=self._thing.FW_VERSION, - model=self._thing.MODEL, + sw_version=self._thing.fw_version, + model=self._thing.model, ) async def async_added_to_hass(self) -> None: @@ -127,8 +127,7 @@ class BruntDevice(CoordinatorEntity, CoverEntity): None is unknown, 0 is closed, 100 is fully open. """ - pos = self.coordinator.data[self.unique_id].currentPosition - return int(pos) if pos is not None else None + return self.coordinator.data[self.unique_id].current_position @property def request_cover_position(self) -> int | None: @@ -139,8 +138,7 @@ class BruntDevice(CoordinatorEntity, CoverEntity): to Brunt, at times there is a diff of 1 to current None is unknown, 0 is closed, 100 is fully open. """ - pos = self.coordinator.data[self.unique_id].requestPosition - return int(pos) if pos is not None else None + return self.coordinator.data[self.unique_id].request_position @property def move_state(self) -> int | None: @@ -149,8 +147,7 @@ class BruntDevice(CoordinatorEntity, CoverEntity): None is unknown, 0 when stopped, 1 when opening, 2 when closing """ - mov = self.coordinator.data[self.unique_id].moveState - return int(mov) if mov is not None else None + return self.coordinator.data[self.unique_id].move_state @property def is_opening(self) -> bool: @@ -190,11 +187,11 @@ class BruntDevice(CoordinatorEntity, CoverEntity): """Set the cover to the new position and wait for the update to be reflected.""" try: await self._bapi.async_change_request_position( - position, thingUri=self._thing.thingUri + position, thing_uri=self._thing.thing_uri ) except ClientResponseError as exc: raise HomeAssistantError( - f"Unable to reposition {self._thing.NAME}" + f"Unable to reposition {self._thing.name}" ) from exc self.coordinator.update_interval = FAST_INTERVAL await self.coordinator.async_request_refresh() @@ -204,7 +201,7 @@ class BruntDevice(CoordinatorEntity, CoverEntity): """Update the update interval after each refresh.""" if ( self.request_cover_position - == self._bapi.last_requested_positions[self._thing.thingUri] + == self._bapi.last_requested_positions[self._thing.thing_uri] and self.move_state == 0 ): self.coordinator.update_interval = REGULAR_INTERVAL diff --git a/homeassistant/components/brunt/manifest.json b/homeassistant/components/brunt/manifest.json index 7b9307e8ef2..f970419b787 100644 --- a/homeassistant/components/brunt/manifest.json +++ b/homeassistant/components/brunt/manifest.json @@ -3,7 +3,7 @@ "name": "Brunt Blind Engine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/brunt", - "requirements": ["brunt==1.0.2"], + "requirements": ["brunt==1.1.0"], "codeowners": ["@eavanvalkenburg"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 1307dd09d74..61d329b7b0e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -443,7 +443,7 @@ brother==1.1.0 brottsplatskartan==0.0.1 # homeassistant.components.brunt -brunt==1.0.2 +brunt==1.1.0 # homeassistant.components.bsblan bsblan==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 802e763f46c..acd093e8873 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -284,7 +284,7 @@ broadlink==0.18.0 brother==1.1.0 # homeassistant.components.brunt -brunt==1.0.2 +brunt==1.1.0 # homeassistant.components.bsblan bsblan==0.4.0 From 07e1e174ac113d8c085e2aef16224281d91cc339 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Dec 2021 18:33:58 +0100 Subject: [PATCH 0829/2644] Use attr** in somfy-mylink (#62381) Co-authored-by: epenet --- .../components/somfy_mylink/cover.py | 89 +++++-------------- 1 file changed, 22 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/somfy_mylink/cover.py b/homeassistant/components/somfy_mylink/cover.py index b4eb847a5e0..480eeda9fd1 100644 --- a/homeassistant/components/somfy_mylink/cover.py +++ b/homeassistant/components/somfy_mylink/cover.py @@ -1,12 +1,7 @@ """Cover Platform for the Somfy MyLink component.""" import logging -from homeassistant.components.cover import ( - DEVICE_CLASS_BLIND, - DEVICE_CLASS_SHUTTER, - DEVICE_CLASS_WINDOW, - CoverEntity, -) +from homeassistant.components.cover import CoverDeviceClass, CoverEntity from homeassistant.const import STATE_CLOSED, STATE_OPEN from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.restore_state import RestoreEntity @@ -21,7 +16,10 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -MYLINK_COVER_TYPE_TO_DEVICE_CLASS = {0: DEVICE_CLASS_BLIND, 1: DEVICE_CLASS_SHUTTER} +MYLINK_COVER_TYPE_TO_DEVICE_CLASS = { + 0: CoverDeviceClass.BLIND, + 1: CoverDeviceClass.SHUTTER, +} async def async_setup_entry(hass, config_entry, async_add_entities): @@ -38,7 +36,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): "target_id": cover["targetID"], "name": cover["name"], "device_class": MYLINK_COVER_TYPE_TO_DEVICE_CLASS.get( - cover.get("type"), DEVICE_CLASS_WINDOW + cover.get("type"), CoverDeviceClass.WINDOW ), "reverse": reversed_target_ids.get(cover["targetID"], False), } @@ -57,76 +55,33 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class SomfyShade(RestoreEntity, CoverEntity): """Object for controlling a Somfy cover.""" + _attr_should_poll = False + _attr_assumed_state = True + def __init__( self, somfy_mylink, target_id, name="SomfyShade", reverse=False, - device_class=DEVICE_CLASS_WINDOW, + device_class=CoverDeviceClass.WINDOW, ): """Initialize the cover.""" self.somfy_mylink = somfy_mylink self._target_id = target_id - self._name = name + self._attr_unique_id = target_id + self._attr_name = name self._reverse = reverse - self._closed = None - self._is_opening = None - self._is_closing = None - self._device_class = device_class - - @property - def should_poll(self): - """No polling since assumed state.""" - return False - - @property - def unique_id(self): - """Return the unique ID of this cover.""" - return self._target_id - - @property - def name(self): - """Return the name of the cover.""" - return self._name - - @property - def assumed_state(self): - """Let HA know the integration is assumed state.""" - return True - - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return self._device_class - - @property - def is_opening(self): - """Return if the cover is opening.""" - return self._is_opening - - @property - def is_closing(self): - """Return if the cover is closing.""" - return self._is_closing - - @property - def is_closed(self) -> bool: - """Return if the cover is closed.""" - return self._closed - - @property - def device_info(self) -> DeviceInfo: - """Return the device_info of the device.""" - return DeviceInfo( + self._attr_device_class = device_class + self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self._target_id)}, manufacturer=MANUFACTURER, - name=self._name, + name=name, ) async def async_close_cover(self, **kwargs): """Close the cover.""" - self._is_closing = True + self._attr_is_closing = True self.async_write_ha_state() try: # Blocks until the close command is sent @@ -134,14 +89,14 @@ class SomfyShade(RestoreEntity, CoverEntity): await self.somfy_mylink.move_down(self._target_id) else: await self.somfy_mylink.move_up(self._target_id) - self._closed = True + self._attr_is_closed = True finally: - self._is_closing = None + self._attr_is_closing = None self.async_write_ha_state() async def async_open_cover(self, **kwargs): """Open the cover.""" - self._is_opening = True + self._attr_is_opening = True self.async_write_ha_state() try: # Blocks until the open command is sent @@ -149,9 +104,9 @@ class SomfyShade(RestoreEntity, CoverEntity): await self.somfy_mylink.move_up(self._target_id) else: await self.somfy_mylink.move_down(self._target_id) - self._closed = False + self._attr_is_closed = False finally: - self._is_opening = None + self._attr_is_opening = None self.async_write_ha_state() async def async_stop_cover(self, **kwargs): @@ -168,4 +123,4 @@ class SomfyShade(RestoreEntity, CoverEntity): STATE_OPEN, STATE_CLOSED, ): - self._closed = last_state.state == STATE_CLOSED + self._attr_is_closed = last_state.state == STATE_CLOSED From ba818c0a95908ee554171f08ba1ebb987bf31d09 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 20 Dec 2021 18:37:11 +0100 Subject: [PATCH 0830/2644] Bump pychromecast to 10.2.2 (#62390) --- homeassistant/components/cast/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index bee18948a33..b084540bebb 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", - "requirements": ["pychromecast==10.2.1"], + "requirements": ["pychromecast==10.2.2"], "after_dependencies": [ "cloud", "http", diff --git a/requirements_all.txt b/requirements_all.txt index 61d329b7b0e..82020f0aef7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1405,7 +1405,7 @@ pycfdns==1.2.2 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==10.2.1 +pychromecast==10.2.2 # homeassistant.components.pocketcasts pycketcasts==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index acd093e8873..e59227ae781 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -859,7 +859,7 @@ pybotvac==0.0.22 pycfdns==1.2.2 # homeassistant.components.cast -pychromecast==10.2.1 +pychromecast==10.2.2 # homeassistant.components.climacell pyclimacell==0.18.2 From c04e1818093bd650bb013e8a63c04eaad1b36dcb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Dec 2021 18:42:36 +0100 Subject: [PATCH 0831/2644] Use attr** in smarty (#62371) Co-authored-by: epenet --- .../components/smarty/binary_sensor.py | 49 +++++------- homeassistant/components/smarty/sensor.py | 75 ++++++------------- 2 files changed, 41 insertions(+), 83 deletions(-) diff --git a/homeassistant/components/smarty/binary_sensor.py b/homeassistant/components/smarty/binary_sensor.py index 965102f07f7..90944c04d8b 100644 --- a/homeassistant/components/smarty/binary_sensor.py +++ b/homeassistant/components/smarty/binary_sensor.py @@ -3,7 +3,7 @@ import logging from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_PROBLEM, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.core import callback @@ -31,33 +31,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class SmartyBinarySensor(BinarySensorEntity): """Representation of a Smarty Binary Sensor.""" + _attr_should_poll = False + def __init__(self, name, device_class, smarty): """Initialize the entity.""" - self._name = name - self._state = None - self._sensor_type = device_class + self._attr_name = name + self._attr_device_class = device_class self._smarty = smarty - @property - def device_class(self): - """Return the class of the sensor.""" - return self._sensor_type - - @property - def should_poll(self) -> bool: - """Do not poll.""" - return False - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def is_on(self): - """Return true if the binary sensor is on.""" - return self._state - async def async_added_to_hass(self): """Call to update.""" async_dispatcher_connect(self.hass, SIGNAL_UPDATE_SMARTY, self._update_callback) @@ -77,8 +58,8 @@ class BoostSensor(SmartyBinarySensor): def update(self) -> None: """Update state.""" - _LOGGER.debug("Updating sensor %s", self._name) - self._state = self._smarty.boost + _LOGGER.debug("Updating sensor %s", self._attr_name) + self._attr_is_on = self._smarty.boost class AlarmSensor(SmartyBinarySensor): @@ -87,13 +68,15 @@ class AlarmSensor(SmartyBinarySensor): def __init__(self, name, smarty): """Alarm Sensor Init.""" super().__init__( - name=f"{name} Alarm", device_class=DEVICE_CLASS_PROBLEM, smarty=smarty + name=f"{name} Alarm", + device_class=BinarySensorDeviceClass.PROBLEM, + smarty=smarty, ) def update(self) -> None: """Update state.""" - _LOGGER.debug("Updating sensor %s", self._name) - self._state = self._smarty.alarm + _LOGGER.debug("Updating sensor %s", self._attr_name) + self._attr_is_on = self._smarty.alarm class WarningSensor(SmartyBinarySensor): @@ -102,10 +85,12 @@ class WarningSensor(SmartyBinarySensor): def __init__(self, name, smarty): """Warning Sensor Init.""" super().__init__( - name=f"{name} Warning", device_class=DEVICE_CLASS_PROBLEM, smarty=smarty + name=f"{name} Warning", + device_class=BinarySensorDeviceClass.PROBLEM, + smarty=smarty, ) def update(self) -> None: """Update state.""" - _LOGGER.debug("Updating sensor %s", self._name) - self._state = self._smarty.warning + _LOGGER.debug("Updating sensor %s", self._attr_name) + self._attr_is_on = self._smarty.warning diff --git a/homeassistant/components/smarty/sensor.py b/homeassistant/components/smarty/sensor.py index 44a8392991a..9266698ed58 100644 --- a/homeassistant/components/smarty/sensor.py +++ b/homeassistant/components/smarty/sensor.py @@ -4,12 +4,8 @@ from __future__ import annotations import datetime as dt import logging -from homeassistant.components.sensor import SensorEntity -from homeassistant.const import ( - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_TIMESTAMP, - TEMP_CELSIUS, -) +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.const import TEMP_CELSIUS from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.dt as dt_util @@ -39,41 +35,18 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class SmartySensor(SensorEntity): """Representation of a Smarty Sensor.""" + _attr_should_poll = False + def __init__( self, name: str, device_class: str, smarty, unit_of_measurement: str = "" ): """Initialize the entity.""" - self._name = name - self._state: dt.datetime | None = None - self._sensor_type = device_class - self._unit_of_measurement = unit_of_measurement + self._attr_name = name + self._attr_native_value = None + self._attr_device_class = device_class + self._attr_native_unit_of_measurement = unit_of_measurement self._smarty = smarty - @property - def should_poll(self) -> bool: - """Do not poll.""" - return False - - @property - def device_class(self): - """Return the device class of the sensor.""" - return self._sensor_type - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state - - @property - def native_unit_of_measurement(self): - """Return the unit this state is expressed in.""" - return self._unit_of_measurement - async def async_added_to_hass(self): """Call to update.""" async_dispatcher_connect(self.hass, SIGNAL_UPDATE_SMARTY, self._update_callback) @@ -91,15 +64,15 @@ class SupplyAirTemperatureSensor(SmartySensor): """Supply Air Temperature Init.""" super().__init__( name=f"{name} Supply Air Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, unit_of_measurement=TEMP_CELSIUS, smarty=smarty, ) def update(self) -> None: """Update state.""" - _LOGGER.debug("Updating sensor %s", self._name) - self._state = self._smarty.supply_air_temperature + _LOGGER.debug("Updating sensor %s", self._attr_name) + self._attr_native_value = self._smarty.supply_air_temperature class ExtractAirTemperatureSensor(SmartySensor): @@ -109,15 +82,15 @@ class ExtractAirTemperatureSensor(SmartySensor): """Supply Air Temperature Init.""" super().__init__( name=f"{name} Extract Air Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, unit_of_measurement=TEMP_CELSIUS, smarty=smarty, ) def update(self) -> None: """Update state.""" - _LOGGER.debug("Updating sensor %s", self._name) - self._state = self._smarty.extract_air_temperature + _LOGGER.debug("Updating sensor %s", self._attr_name) + self._attr_native_value = self._smarty.extract_air_temperature class OutdoorAirTemperatureSensor(SmartySensor): @@ -127,15 +100,15 @@ class OutdoorAirTemperatureSensor(SmartySensor): """Outdoor Air Temperature Init.""" super().__init__( name=f"{name} Outdoor Air Temperature", - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, unit_of_measurement=TEMP_CELSIUS, smarty=smarty, ) def update(self) -> None: """Update state.""" - _LOGGER.debug("Updating sensor %s", self._name) - self._state = self._smarty.outdoor_air_temperature + _LOGGER.debug("Updating sensor %s", self._attr_name) + self._attr_native_value = self._smarty.outdoor_air_temperature class SupplyFanSpeedSensor(SmartySensor): @@ -152,8 +125,8 @@ class SupplyFanSpeedSensor(SmartySensor): def update(self) -> None: """Update state.""" - _LOGGER.debug("Updating sensor %s", self._name) - self._state = self._smarty.supply_fan_speed + _LOGGER.debug("Updating sensor %s", self._attr_name) + self._attr_native_value = self._smarty.supply_fan_speed class ExtractFanSpeedSensor(SmartySensor): @@ -170,8 +143,8 @@ class ExtractFanSpeedSensor(SmartySensor): def update(self) -> None: """Update state.""" - _LOGGER.debug("Updating sensor %s", self._name) - self._state = self._smarty.extract_fan_speed + _LOGGER.debug("Updating sensor %s", self._attr_name) + self._attr_native_value = self._smarty.extract_fan_speed class FilterDaysLeftSensor(SmartySensor): @@ -181,7 +154,7 @@ class FilterDaysLeftSensor(SmartySensor): """Filter Days Left Init.""" super().__init__( name=f"{name} Filter Days Left", - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, unit_of_measurement=None, smarty=smarty, ) @@ -189,8 +162,8 @@ class FilterDaysLeftSensor(SmartySensor): def update(self) -> None: """Update state.""" - _LOGGER.debug("Updating sensor %s", self._name) + _LOGGER.debug("Updating sensor %s", self._attr_name) days_left = self._smarty.filter_timer if days_left is not None and days_left != self._days_left: - self._state = dt_util.now() + dt.timedelta(days=days_left) + self._attr_native_value = dt_util.now() + dt.timedelta(days=days_left) self._days_left = days_left From 2ddd45afd55dfb7fe2cb4c578664673ed93f7974 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 20 Dec 2021 18:49:16 +0100 Subject: [PATCH 0832/2644] Update frontend to 20211220.0 (#62389) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 4ac95c38afc..6ae1709e418 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20211215.0" + "home-assistant-frontend==20211220.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b80c8503656..dcd9d257474 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ ciso8601==2.2.0 cryptography==35.0.0 emoji==1.5.0 hass-nabucasa==0.50.0 -home-assistant-frontend==20211215.0 +home-assistant-frontend==20211220.0 httpx==0.21.0 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index 82020f0aef7..571ec704b8a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -825,7 +825,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211215.0 +home-assistant-frontend==20211220.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e59227ae781..9c385e7f1d3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -521,7 +521,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211215.0 +home-assistant-frontend==20211220.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 334c6c5c02242b53cca9e12a1afbbc66993b81b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 20 Dec 2021 20:16:30 +0200 Subject: [PATCH 0833/2644] Make device automation type an enum (#62354) --- .../components/device_automation/__init__.py | 88 +++++++++++++------ .../components/device_automation/trigger.py | 10 ++- homeassistant/components/homekit/__init__.py | 4 +- .../components/homekit/config_flow.py | 4 +- homeassistant/helpers/condition.py | 5 +- homeassistant/helpers/script.py | 6 +- .../device_action/tests/test_device_action.py | 5 +- .../tests/test_device_condition.py | 5 +- .../tests/test_device_trigger.py | 5 +- tests/common.py | 4 +- .../components/device_automation/test_init.py | 23 ++++- .../mobile_app/test_device_action.py | 4 +- 12 files changed, 120 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 74582f0f77b..9d80ce169a9 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio from collections.abc import Iterable, Mapping +from enum import Enum from functools import wraps import logging from types import ModuleType @@ -19,6 +20,7 @@ from homeassistant.helpers import ( device_registry as dr, entity_registry as er, ) +from homeassistant.helpers.frame import report from homeassistant.loader import IntegrationNotFound, bind_hass from homeassistant.requirements import async_get_integration_with_requirements @@ -45,32 +47,49 @@ class DeviceAutomationDetails(NamedTuple): get_capabilities_func: str -TYPES = { - "trigger": DeviceAutomationDetails( +class DeviceAutomationType(Enum): + """Device automation type.""" + + TRIGGER = DeviceAutomationDetails( "device_trigger", "async_get_triggers", "async_get_trigger_capabilities", - ), - "condition": DeviceAutomationDetails( + ) + CONDITION = DeviceAutomationDetails( "device_condition", "async_get_conditions", "async_get_condition_capabilities", - ), - "action": DeviceAutomationDetails( + ) + ACTION = DeviceAutomationDetails( "device_action", "async_get_actions", "async_get_action_capabilities", - ), + ) + + +# TYPES is deprecated as of Home Assistant 2022.2, use DeviceAutomationType instead +TYPES = { + "trigger": DeviceAutomationType.TRIGGER.value, + "condition": DeviceAutomationType.CONDITION.value, + "action": DeviceAutomationType.ACTION.value, } @bind_hass async def async_get_device_automations( hass: HomeAssistant, - automation_type: str, + automation_type: DeviceAutomationType | str, device_ids: Iterable[str] | None = None, ) -> Mapping[str, Any]: """Return all the device automations for a type optionally limited to specific device ids.""" + if isinstance(automation_type, str): + report( + "uses str for async_get_device_automations automation_type. This is " + "deprecated and will stop working in Home Assistant 2022.4, it should be " + "updated to use DeviceAutomationType instead", + error_if_core=False, + ) + automation_type = DeviceAutomationType[automation_type.upper()] return await _async_get_device_automations(hass, automation_type, device_ids) @@ -98,13 +117,21 @@ async def async_setup(hass, config): async def async_get_device_automation_platform( - hass: HomeAssistant, domain: str, automation_type: str + hass: HomeAssistant, domain: str, automation_type: DeviceAutomationType | str ) -> ModuleType: """Load device automation platform for integration. Throws InvalidDeviceAutomationConfig if the integration is not found or does not support device automation. """ - platform_name = TYPES[automation_type].section + if isinstance(automation_type, str): + report( + "uses str for async_get_device_automation_platform automation_type. This " + "is deprecated and will stop working in Home Assistant 2022.4, it should " + "be updated to use DeviceAutomationType instead", + error_if_core=False, + ) + automation_type = DeviceAutomationType[automation_type.upper()] + platform_name = automation_type.value.section try: integration = await async_get_integration_with_requirements(hass, domain) platform = integration.get_platform(platform_name) @@ -114,7 +141,8 @@ async def async_get_device_automation_platform( ) from err except ImportError as err: raise InvalidDeviceAutomationConfig( - f"Integration '{domain}' does not support device automation {automation_type}s" + f"Integration '{domain}' does not support device automation " + f"{automation_type.name.lower()}s" ) from err return platform @@ -131,7 +159,7 @@ async def _async_get_device_automations_from_domain( except InvalidDeviceAutomationConfig: return {} - function_name = TYPES[automation_type].get_automations_func + function_name = automation_type.value.get_automations_func return await asyncio.gather( *( @@ -143,7 +171,9 @@ async def _async_get_device_automations_from_domain( async def _async_get_device_automations( - hass: HomeAssistant, automation_type: str, device_ids: Iterable[str] | None + hass: HomeAssistant, + automation_type: DeviceAutomationType, + device_ids: Iterable[str] | None, ) -> Mapping[str, list[dict[str, Any]]]: """List device automations.""" device_registry = dr.async_get(hass) @@ -188,7 +218,7 @@ async def _async_get_device_automations( if isinstance(device_results, Exception): logging.getLogger(__name__).error( "Unexpected error fetching device %ss", - automation_type, + automation_type.name.lower(), exc_info=device_results, ) continue @@ -207,7 +237,9 @@ async def _async_get_device_automation_capabilities(hass, automation_type, autom except InvalidDeviceAutomationConfig: return {} - function_name = TYPES[automation_type].get_capabilities_func + if isinstance(automation_type, str): # until tests pass DeviceAutomationType + automation_type = DeviceAutomationType[automation_type.upper()] + function_name = automation_type.value.get_capabilities_func if not hasattr(platform, function_name): # The device automation has no capabilities @@ -256,9 +288,11 @@ def handle_device_errors(func): async def websocket_device_automation_list_actions(hass, connection, msg): """Handle request for device actions.""" device_id = msg["device_id"] - actions = (await _async_get_device_automations(hass, "action", [device_id])).get( - device_id - ) + actions = ( + await _async_get_device_automations( + hass, DeviceAutomationType.ACTION, [device_id] + ) + ).get(device_id) connection.send_result(msg["id"], actions) @@ -274,7 +308,9 @@ async def websocket_device_automation_list_conditions(hass, connection, msg): """Handle request for device conditions.""" device_id = msg["device_id"] conditions = ( - await _async_get_device_automations(hass, "condition", [device_id]) + await _async_get_device_automations( + hass, DeviceAutomationType.CONDITION, [device_id] + ) ).get(device_id) connection.send_result(msg["id"], conditions) @@ -290,9 +326,11 @@ async def websocket_device_automation_list_conditions(hass, connection, msg): async def websocket_device_automation_list_triggers(hass, connection, msg): """Handle request for device triggers.""" device_id = msg["device_id"] - triggers = (await _async_get_device_automations(hass, "trigger", [device_id])).get( - device_id - ) + triggers = ( + await _async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, [device_id] + ) + ).get(device_id) connection.send_result(msg["id"], triggers) @@ -308,7 +346,7 @@ async def websocket_device_automation_get_action_capabilities(hass, connection, """Handle request for device action capabilities.""" action = msg["action"] capabilities = await _async_get_device_automation_capabilities( - hass, "action", action + hass, DeviceAutomationType.ACTION, action ) connection.send_result(msg["id"], capabilities) @@ -327,7 +365,7 @@ async def websocket_device_automation_get_condition_capabilities(hass, connectio """Handle request for device condition capabilities.""" condition = msg["condition"] capabilities = await _async_get_device_automation_capabilities( - hass, "condition", condition + hass, DeviceAutomationType.CONDITION, condition ) connection.send_result(msg["id"], capabilities) @@ -346,6 +384,6 @@ async def websocket_device_automation_get_trigger_capabilities(hass, connection, """Handle request for device trigger capabilities.""" trigger = msg["trigger"] capabilities = await _async_get_device_automation_capabilities( - hass, "trigger", trigger + hass, DeviceAutomationType.TRIGGER, trigger ) connection.send_result(msg["id"], capabilities) diff --git a/homeassistant/components/device_automation/trigger.py b/homeassistant/components/device_automation/trigger.py index 62bd8d1c808..008a7603dba 100644 --- a/homeassistant/components/device_automation/trigger.py +++ b/homeassistant/components/device_automation/trigger.py @@ -3,7 +3,11 @@ import voluptuous as vol from homeassistant.const import CONF_DOMAIN -from . import DEVICE_TRIGGER_BASE_SCHEMA, async_get_device_automation_platform +from . import ( + DEVICE_TRIGGER_BASE_SCHEMA, + DeviceAutomationType, + async_get_device_automation_platform, +) from .exceptions import InvalidDeviceAutomationConfig # mypy: allow-untyped-defs, no-check-untyped-defs @@ -14,7 +18,7 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) async def async_validate_trigger_config(hass, config): """Validate config.""" platform = await async_get_device_automation_platform( - hass, config[CONF_DOMAIN], "trigger" + hass, config[CONF_DOMAIN], DeviceAutomationType.TRIGGER ) if not hasattr(platform, "async_validate_trigger_config"): return platform.TRIGGER_SCHEMA(config) @@ -28,6 +32,6 @@ async def async_validate_trigger_config(hass, config): async def async_attach_trigger(hass, config, action, automation_info): """Listen for trigger.""" platform = await async_get_device_automation_platform( - hass, config[CONF_DOMAIN], "trigger" + hass, config[CONF_DOMAIN], DeviceAutomationType.TRIGGER ) return await platform.async_attach_trigger(hass, config, action, automation_info) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 2bc7da6d24a..503a76418a9 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -819,7 +819,9 @@ class HomeKit: valid_device_ids.append(device_id) for device_id, device_triggers in ( await device_automation.async_get_device_automations( - self.hass, "trigger", valid_device_ids + self.hass, + device_automation.DeviceAutomationType.TRIGGER, + valid_device_ids, ) ).items(): self.add_bridge_triggers_accessory( diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index 0d8bf967c5b..f47ecdf5dbb 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -512,7 +512,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow): async def _async_get_supported_devices(hass): """Return all supported devices.""" - results = await device_automation.async_get_device_automations(hass, "trigger") + results = await device_automation.async_get_device_automations( + hass, device_automation.DeviceAutomationType.TRIGGER + ) dev_reg = device_registry.async_get(hass) unsorted = { device_id: dev_reg.async_get(device_id).name or device_id diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 030e5dacfd5..d8d98f05ccd 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -14,6 +14,7 @@ from typing import Any, Callable, cast from homeassistant.components import zone as zone_cmp from homeassistant.components.device_automation import ( + DeviceAutomationType, async_get_device_automation_platform, ) from homeassistant.components.sensor import DEVICE_CLASS_TIMESTAMP @@ -881,7 +882,7 @@ async def async_device_from_config( ) -> ConditionCheckerType: """Test a device condition.""" platform = await async_get_device_automation_platform( - hass, config[CONF_DOMAIN], "condition" + hass, config[CONF_DOMAIN], DeviceAutomationType.CONDITION ) return trace_condition_function( cast( @@ -952,7 +953,7 @@ async def async_validate_condition_config( config = cv.DEVICE_CONDITION_SCHEMA(config) assert not isinstance(config, Template) platform = await async_get_device_automation_platform( - hass, config[CONF_DOMAIN], "condition" + hass, config[CONF_DOMAIN], DeviceAutomationType.CONDITION ) if hasattr(platform, "async_validate_condition_config"): return await platform.async_validate_condition_config(hass, config) # type: ignore diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 20a1dbb8aec..3e4432a40eb 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -254,7 +254,7 @@ async def async_validate_action_config( elif action_type == cv.SCRIPT_ACTION_DEVICE_AUTOMATION: platform = await device_automation.async_get_device_automation_platform( - hass, config[CONF_DOMAIN], "action" + hass, config[CONF_DOMAIN], device_automation.DeviceAutomationType.ACTION ) if hasattr(platform, "async_validate_action_config"): config = await platform.async_validate_action_config(hass, config) # type: ignore @@ -590,7 +590,9 @@ class _ScriptRun: """Perform the device automation specified in the action.""" self._step_log("device automation") platform = await device_automation.async_get_device_automation_platform( - self._hass, self._action[CONF_DOMAIN], "action" + self._hass, + self._action[CONF_DOMAIN], + device_automation.DeviceAutomationType.ACTION, ) await platform.async_call_action_from_config( self._hass, self._action, self._variables, self._context diff --git a/script/scaffold/templates/device_action/tests/test_device_action.py b/script/scaffold/templates/device_action/tests/test_device_action.py index 424fa0a9afd..f300ae55cf7 100644 --- a/script/scaffold/templates/device_action/tests/test_device_action.py +++ b/script/scaffold/templates/device_action/tests/test_device_action.py @@ -3,6 +3,7 @@ import pytest from homeassistant.components import automation from homeassistant.components.NEW_DOMAIN import DOMAIN +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry, entity_registry from homeassistant.setup import async_setup_component @@ -56,7 +57,9 @@ async def test_get_actions( "entity_id": "NEW_DOMAIN.test_5678", }, ] - actions = await async_get_device_automations(hass, "action", device_entry.id) + actions = await async_get_device_automations( + hass, DeviceAutomationType.ACTION, device_entry.id + ) assert_lists_same(actions, expected_actions) diff --git a/script/scaffold/templates/device_condition/tests/test_device_condition.py b/script/scaffold/templates/device_condition/tests/test_device_condition.py index 9a283fa1f5b..539a60ded97 100644 --- a/script/scaffold/templates/device_condition/tests/test_device_condition.py +++ b/script/scaffold/templates/device_condition/tests/test_device_condition.py @@ -5,6 +5,7 @@ import pytest from homeassistant.components import automation from homeassistant.components.NEW_DOMAIN import DOMAIN +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import device_registry, entity_registry @@ -67,7 +68,9 @@ async def test_get_conditions( "entity_id": f"{DOMAIN}.test_5678", }, ] - conditions = await async_get_device_automations(hass, "condition", device_entry.id) + conditions = await async_get_device_automations( + hass, DeviceAutomationType.CONDITION, device_entry.id + ) assert_lists_same(conditions, expected_conditions) diff --git a/script/scaffold/templates/device_trigger/tests/test_device_trigger.py b/script/scaffold/templates/device_trigger/tests/test_device_trigger.py index 55343abadb1..59ba9654566 100644 --- a/script/scaffold/templates/device_trigger/tests/test_device_trigger.py +++ b/script/scaffold/templates/device_trigger/tests/test_device_trigger.py @@ -3,6 +3,7 @@ import pytest from homeassistant.components import automation from homeassistant.components.NEW_DOMAIN import DOMAIN +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component @@ -60,7 +61,9 @@ async def test_get_triggers(hass, device_reg, entity_reg): "entity_id": f"{DOMAIN}.test_5678", }, ] - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert_lists_same(triggers, expected_triggers) diff --git a/tests/common.py b/tests/common.py index 9d4a9cfe366..327427eda6e 100644 --- a/tests/common.py +++ b/tests/common.py @@ -69,7 +69,9 @@ CLIENT_REDIRECT_URI = "https://example.com/app/callback" async def async_get_device_automations( - hass: HomeAssistant, automation_type: str, device_id: str + hass: HomeAssistant, + automation_type: device_automation.DeviceAutomationType | str, + device_id: str, ) -> Any: """Get a device automation for a single device id.""" automations = await device_automation.async_get_device_automations( diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index 563611b99ad..fe656663ca8 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -391,6 +391,13 @@ async def test_async_get_device_automations_single_device_trigger( connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) entity_reg.async_get_or_create("light", "test", "5678", device_id=device_entry.id) + result = await device_automation.async_get_device_automations( + hass, device_automation.DeviceAutomationType.TRIGGER, [device_entry.id] + ) + assert device_entry.id in result + assert len(result[device_entry.id]) == 2 + + # Test deprecated str automation_type works, to be removed in 2022.4 result = await device_automation.async_get_device_automations( hass, "trigger", [device_entry.id] ) @@ -410,7 +417,9 @@ async def test_async_get_device_automations_all_devices_trigger( connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) entity_reg.async_get_or_create("light", "test", "5678", device_id=device_entry.id) - result = await device_automation.async_get_device_automations(hass, "trigger") + result = await device_automation.async_get_device_automations( + hass, device_automation.DeviceAutomationType.TRIGGER + ) assert device_entry.id in result assert len(result[device_entry.id]) == 2 @@ -427,7 +436,9 @@ async def test_async_get_device_automations_all_devices_condition( connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) entity_reg.async_get_or_create("light", "test", "5678", device_id=device_entry.id) - result = await device_automation.async_get_device_automations(hass, "condition") + result = await device_automation.async_get_device_automations( + hass, device_automation.DeviceAutomationType.CONDITION + ) assert device_entry.id in result assert len(result[device_entry.id]) == 2 @@ -444,7 +455,9 @@ async def test_async_get_device_automations_all_devices_action( connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) entity_reg.async_get_or_create("light", "test", "5678", device_id=device_entry.id) - result = await device_automation.async_get_device_automations(hass, "action") + result = await device_automation.async_get_device_automations( + hass, device_automation.DeviceAutomationType.ACTION + ) assert device_entry.id in result assert len(result[device_entry.id]) == 3 @@ -465,7 +478,9 @@ async def test_async_get_device_automations_all_devices_action_exception_throw( "homeassistant.components.light.device_trigger.async_get_triggers", side_effect=KeyError, ): - result = await device_automation.async_get_device_automations(hass, "trigger") + result = await device_automation.async_get_device_automations( + hass, device_automation.DeviceAutomationType.TRIGGER + ) assert device_entry.id in result assert len(result[device_entry.id]) == 0 assert "KeyError" in caplog.text diff --git a/tests/components/mobile_app/test_device_action.py b/tests/components/mobile_app/test_device_action.py index e5b15412e4d..f7846ecc377 100644 --- a/tests/components/mobile_app/test_device_action.py +++ b/tests/components/mobile_app/test_device_action.py @@ -16,7 +16,9 @@ async def test_get_actions(hass, push_registration): ] capabilitites = await device_automation._async_get_device_automation_capabilities( - hass, "action", {"domain": DOMAIN, "device_id": device_id, "type": "notify"} + hass, + device_automation.DeviceAutomationType.ACTION, + {"domain": DOMAIN, "device_id": device_id, "type": "notify"}, ) assert "extra_fields" in capabilitites From 7bee0e6423767004bbbea0871721b32537d78c4c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Dec 2021 19:18:55 +0100 Subject: [PATCH 0834/2644] Update "Code Coverage" task to use numprocesses (#61449) Co-authored-by: epenet --- .vscode/tasks.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 1aea23fc781..77d31339a69 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -64,7 +64,7 @@ "label": "Code Coverage", "detail": "Generate code coverage report for a given integration.", "type": "shell", - "command": "pytest ./tests/components/${input:integrationName}/ --cov=homeassistant.components.${input:integrationName} --cov-report term-missing --durations-min=1 --durations=0", + "command": "pytest ./tests/components/${input:integrationName}/ --cov=homeassistant.components.${input:integrationName} --cov-report term-missing --durations-min=1 --durations=0 --numprocesses=auto", "group": { "kind": "test", "isDefault": true From 6a81821399d011d24ee6aeb6c682a98743db8e1e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Dec 2021 19:20:40 +0100 Subject: [PATCH 0835/2644] Refactor kaiterra to use SensorEntityDescription (#61865) Co-authored-by: epenet --- homeassistant/components/kaiterra/sensor.py | 62 +++++++++------------ 1 file changed, 25 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/kaiterra/sensor.py b/homeassistant/components/kaiterra/sensor.py index fbaa730ab9f..096ca414d50 100644 --- a/homeassistant/components/kaiterra/sensor.py +++ b/homeassistant/components/kaiterra/sensor.py @@ -1,20 +1,25 @@ """Support for Kaiterra Temperature ahn Humidity Sensors.""" -from homeassistant.components.sensor import SensorEntity -from homeassistant.const import ( - CONF_DEVICE_ID, - CONF_NAME, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_TEMPERATURE, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, ) +from homeassistant.const import CONF_DEVICE_ID, CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import DISPATCHER_KAITERRA, DOMAIN SENSORS = [ - {"name": "Temperature", "prop": "rtemp", "device_class": DEVICE_CLASS_TEMPERATURE}, - {"name": "Humidity", "prop": "rhumid", "device_class": DEVICE_CLASS_HUMIDITY}, + SensorEntityDescription( + name="Temperature", + key="rtemp", + device_class=SensorDeviceClass.TEMPERATURE, + ), + SensorEntityDescription( + name="Humidity", + key="rhumid", + device_class=SensorDeviceClass.HUMIDITY, + ), ] @@ -28,57 +33,40 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= device_id = discovery_info[CONF_DEVICE_ID] async_add_entities( - [KaiterraSensor(api, name, device_id, sensor) for sensor in SENSORS] + [KaiterraSensor(api, name, device_id, description) for description in SENSORS] ) class KaiterraSensor(SensorEntity): """Implementation of a Kaittera sensor.""" - def __init__(self, api, name, device_id, sensor): + _attr_should_poll = False + + def __init__(self, api, name, device_id, description: SensorEntityDescription): """Initialize the sensor.""" self._api = api - self._name = f'{name} {sensor["name"]}' self._device_id = device_id - self._kind = sensor["name"].lower() - self._property = sensor["prop"] - self._device_class = sensor["device_class"] + self.entity_description = description + self._attr_name = f"{name} {description.name}" + self._attr_unique_id = f"{device_id}_{description.name.lower()}" @property def _sensor(self): """Return the sensor data.""" - return self._api.data.get(self._device_id, {}).get(self._property, {}) - - @property - def should_poll(self): - """Return that the sensor should not be polled.""" - return False + return self._api.data.get(self._device_id, {}).get( + self.entity_description.key, {} + ) @property def available(self): """Return the availability of the sensor.""" return self._api.data.get(self._device_id) is not None - @property - def device_class(self): - """Return the device class.""" - return self._device_class - - @property - def name(self): - """Return the name.""" - return self._name - @property def native_value(self): """Return the state.""" return self._sensor.get("value") - @property - def unique_id(self): - """Return the sensor's unique id.""" - return f"{self._device_id}_{self._kind}" - @property def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" From 6cf9f1a0cc3af31a8e43a55024fabb7245d8add5 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 20 Dec 2021 19:22:04 +0100 Subject: [PATCH 0836/2644] Update xknx to 0.18.14 (#62411) Co-authored-by: Franck Nijhof --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/knx/test_config_flow.py | 10 +++++++++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index b793c667353..21ac4ce9ea4 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/knx", "requirements": [ - "xknx==0.18.13" + "xknx==0.18.14" ], "codeowners": [ "@Julius2342", diff --git a/requirements_all.txt b/requirements_all.txt index 571ec704b8a..1a1cbe487f1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2460,7 +2460,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.knx -xknx==0.18.13 +xknx==0.18.14 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9c385e7f1d3..f0f1a4cd54e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1462,7 +1462,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.knx -xknx==0.18.13 +xknx==0.18.14 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/tests/components/knx/test_config_flow.py b/tests/components/knx/test_config_flow.py index 65289c2b173..4f3e1734b69 100644 --- a/tests/components/knx/test_config_flow.py +++ b/tests/components/knx/test_config_flow.py @@ -28,7 +28,15 @@ from tests.common import MockConfigEntry def _gateway_descriptor(ip: str, port: int) -> GatewayDescriptor: """Get mock gw descriptor.""" - return GatewayDescriptor("Test", ip, port, "eth0", "127.0.0.1", True) + return GatewayDescriptor( + "Test", + ip, + port, + "eth0", + "127.0.0.1", + supports_routing=True, + supports_tunnelling=True, + ) async def test_user_single_instance(hass): From 168fefad887a8ef164658b16c8736c6f570965c0 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Mon, 20 Dec 2021 18:53:44 +0000 Subject: [PATCH 0837/2644] Use DeviceClass Enums in homekit (#62218) --- .../homekit/test_get_accessories.py | 7 ++-- tests/components/homekit/test_homekit.py | 36 +++++++++---------- tests/components/homekit/test_type_cameras.py | 23 ++++++------ .../homekit/test_type_humidifiers.py | 8 ++--- .../homekit/test_type_media_players.py | 18 +++++----- tests/components/homekit/test_type_sensors.py | 15 ++++---- 6 files changed, 54 insertions(+), 53 deletions(-) diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 8cc416adabd..0f843c387ab 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -18,6 +18,7 @@ from homeassistant.components.homekit.const import ( TYPE_VALVE, ) import homeassistant.components.media_player.const as media_player_c +from homeassistant.components.sensor import SensorDeviceClass import homeassistant.components.vacuum as vacuum from homeassistant.const import ( ATTR_CODE, @@ -26,8 +27,6 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, CONF_TYPE, - DEVICE_CLASS_CO, - DEVICE_CLASS_CO2, LIGHT_LUX, PERCENTAGE, STATE_UNKNOWN, @@ -219,14 +218,14 @@ def test_type_media_player(type_name, entity_id, state, attrs, config): "CarbonMonoxideSensor", "sensor.co", "2", - {ATTR_DEVICE_CLASS: DEVICE_CLASS_CO}, + {ATTR_DEVICE_CLASS: SensorDeviceClass.CO}, ), ("CarbonDioxideSensor", "sensor.airmeter_co2", "500", {}), ( "CarbonDioxideSensor", "sensor.co2", "500", - {ATTR_DEVICE_CLASS: DEVICE_CLASS_CO2}, + {ATTR_DEVICE_CLASS: SensorDeviceClass.CO2}, ), ( "HumiditySensor", diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 0b1d2cc8535..2d504ad517a 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -11,10 +11,7 @@ import pytest from homeassistant import config as hass_config from homeassistant.components import homekit as homekit_base, zeroconf -from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_BATTERY_CHARGING, - DEVICE_CLASS_MOTION, -) +from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.homekit import ( MAX_DEVICES, STATUS_READY, @@ -37,6 +34,7 @@ from homeassistant.components.homekit.const import ( ) from homeassistant.components.homekit.type_triggers import DeviceTriggerAccessory from homeassistant.components.homekit.util import get_persist_fullpath_for_entry_id +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -45,8 +43,6 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, CONF_PORT, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_HUMIDITY, EVENT_HOMEASSISTANT_STARTED, PERCENTAGE, SERVICE_RELOAD, @@ -1118,14 +1114,14 @@ async def test_homekit_finds_linked_batteries( "powerwall", "battery_charging", device_id=device_entry.id, - original_device_class=DEVICE_CLASS_BATTERY_CHARGING, + original_device_class=BinarySensorDeviceClass.BATTERY_CHARGING, ) battery_sensor = entity_reg.async_get_or_create( "sensor", "powerwall", "battery", device_id=device_entry.id, - original_device_class=DEVICE_CLASS_BATTERY, + original_device_class=SensorDeviceClass.BATTERY, ) light = entity_reg.async_get_or_create( "light", "powerwall", "demo", device_id=device_entry.id @@ -1134,10 +1130,10 @@ async def test_homekit_finds_linked_batteries( hass.states.async_set( binary_charging_sensor.entity_id, STATE_ON, - {ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY_CHARGING}, + {ATTR_DEVICE_CLASS: BinarySensorDeviceClass.BATTERY_CHARGING}, ) hass.states.async_set( - battery_sensor.entity_id, 30, {ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY} + battery_sensor.entity_id, 30, {ATTR_DEVICE_CLASS: SensorDeviceClass.BATTERY} ) hass.states.async_set(light.entity_id, STATE_ON) @@ -1187,14 +1183,14 @@ async def test_homekit_async_get_integration_fails( "invalid_integration_does_not_exist", "battery_charging", device_id=device_entry.id, - original_device_class=DEVICE_CLASS_BATTERY_CHARGING, + original_device_class=BinarySensorDeviceClass.BATTERY_CHARGING, ) battery_sensor = entity_reg.async_get_or_create( "sensor", "invalid_integration_does_not_exist", "battery", device_id=device_entry.id, - original_device_class=DEVICE_CLASS_BATTERY, + original_device_class=SensorDeviceClass.BATTERY, ) light = entity_reg.async_get_or_create( "light", "invalid_integration_does_not_exist", "demo", device_id=device_entry.id @@ -1203,10 +1199,10 @@ async def test_homekit_async_get_integration_fails( hass.states.async_set( binary_charging_sensor.entity_id, STATE_ON, - {ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY_CHARGING}, + {ATTR_DEVICE_CLASS: BinarySensorDeviceClass.BATTERY_CHARGING}, ) hass.states.async_set( - battery_sensor.entity_id, 30, {ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY} + battery_sensor.entity_id, 30, {ATTR_DEVICE_CLASS: SensorDeviceClass.BATTERY} ) hass.states.async_set(light.entity_id, STATE_ON) @@ -1334,14 +1330,14 @@ async def test_homekit_ignored_missing_devices( "powerwall", "battery_charging", device_id=device_entry.id, - original_device_class=DEVICE_CLASS_BATTERY_CHARGING, + original_device_class=BinarySensorDeviceClass.BATTERY_CHARGING, ) entity_reg.async_get_or_create( "sensor", "powerwall", "battery", device_id=device_entry.id, - original_device_class=DEVICE_CLASS_BATTERY, + original_device_class=SensorDeviceClass.BATTERY, ) light = entity_reg.async_get_or_create( "light", "powerwall", "demo", device_id=device_entry.id @@ -1404,7 +1400,7 @@ async def test_homekit_finds_linked_motion_sensors( "camera", "motion_sensor", device_id=device_entry.id, - original_device_class=DEVICE_CLASS_MOTION, + original_device_class=BinarySensorDeviceClass.MOTION, ) camera = entity_reg.async_get_or_create( "camera", "camera", "demo", device_id=device_entry.id @@ -1413,7 +1409,7 @@ async def test_homekit_finds_linked_motion_sensors( hass.states.async_set( binary_motion_sensor.entity_id, STATE_ON, - {ATTR_DEVICE_CLASS: DEVICE_CLASS_MOTION}, + {ATTR_DEVICE_CLASS: BinarySensorDeviceClass.MOTION}, ) hass.states.async_set(camera.entity_id, STATE_ON) @@ -1466,7 +1462,7 @@ async def test_homekit_finds_linked_humidity_sensors( "humidifier", "humidity_sensor", device_id=device_entry.id, - original_device_class=DEVICE_CLASS_HUMIDITY, + original_device_class=SensorDeviceClass.HUMIDITY, ) humidifier = entity_reg.async_get_or_create( "humidifier", "humidifier", "demo", device_id=device_entry.id @@ -1476,7 +1472,7 @@ async def test_homekit_finds_linked_humidity_sensors( humidity_sensor.entity_id, "42", { - ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_DEVICE_CLASS: SensorDeviceClass.HUMIDITY, ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, ) diff --git a/tests/components/homekit/test_type_cameras.py b/tests/components/homekit/test_type_cameras.py index ba2dadc2a19..9a8b284b97e 100644 --- a/tests/components/homekit/test_type_cameras.py +++ b/tests/components/homekit/test_type_cameras.py @@ -8,10 +8,7 @@ from pyhap.accessory_driver import AccessoryDriver import pytest from homeassistant.components import camera, ffmpeg -from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_MOTION, - DEVICE_CLASS_OCCUPANCY, -) +from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.camera.img_util import TurboJPEGSingleton from homeassistant.components.homekit.accessories import HomeBridge from homeassistant.components.homekit.const import ( @@ -610,7 +607,7 @@ async def test_camera_with_linked_motion_sensor(hass, run_driver, events): motion_entity_id = "binary_sensor.motion" hass.states.async_set( - motion_entity_id, STATE_ON, {ATTR_DEVICE_CLASS: DEVICE_CLASS_MOTION} + motion_entity_id, STATE_ON, {ATTR_DEVICE_CLASS: BinarySensorDeviceClass.MOTION} ) await hass.async_block_till_done() entity_id = "camera.demo_camera" @@ -647,14 +644,14 @@ async def test_camera_with_linked_motion_sensor(hass, run_driver, events): assert char.value is True hass.states.async_set( - motion_entity_id, STATE_OFF, {ATTR_DEVICE_CLASS: DEVICE_CLASS_MOTION} + motion_entity_id, STATE_OFF, {ATTR_DEVICE_CLASS: BinarySensorDeviceClass.MOTION} ) await hass.async_block_till_done() assert char.value is False char.set_value(True) hass.states.async_set( - motion_entity_id, STATE_ON, {ATTR_DEVICE_CLASS: DEVICE_CLASS_MOTION} + motion_entity_id, STATE_ON, {ATTR_DEVICE_CLASS: BinarySensorDeviceClass.MOTION} ) await hass.async_block_till_done() assert char.value is True @@ -708,7 +705,9 @@ async def test_camera_with_linked_doorbell_sensor(hass, run_driver, events): doorbell_entity_id = "binary_sensor.doorbell" hass.states.async_set( - doorbell_entity_id, STATE_ON, {ATTR_DEVICE_CLASS: DEVICE_CLASS_OCCUPANCY} + doorbell_entity_id, + STATE_ON, + {ATTR_DEVICE_CLASS: BinarySensorDeviceClass.OCCUPANCY}, ) await hass.async_block_till_done() entity_id = "camera.demo_camera" @@ -752,7 +751,9 @@ async def test_camera_with_linked_doorbell_sensor(hass, run_driver, events): assert char2.value is None hass.states.async_set( - doorbell_entity_id, STATE_OFF, {ATTR_DEVICE_CLASS: DEVICE_CLASS_OCCUPANCY} + doorbell_entity_id, + STATE_OFF, + {ATTR_DEVICE_CLASS: BinarySensorDeviceClass.OCCUPANCY}, ) await hass.async_block_till_done() assert char.value is None @@ -761,7 +762,9 @@ async def test_camera_with_linked_doorbell_sensor(hass, run_driver, events): char.set_value(True) char2.set_value(True) hass.states.async_set( - doorbell_entity_id, STATE_ON, {ATTR_DEVICE_CLASS: DEVICE_CLASS_OCCUPANCY} + doorbell_entity_id, + STATE_ON, + {ATTR_DEVICE_CLASS: BinarySensorDeviceClass.OCCUPANCY}, ) await hass.async_block_till_done() assert char.value is None diff --git a/tests/components/homekit/test_type_humidifiers.py b/tests/components/homekit/test_type_humidifiers.py index 1a301e340b3..49b3ee72784 100644 --- a/tests/components/homekit/test_type_humidifiers.py +++ b/tests/components/homekit/test_type_humidifiers.py @@ -27,11 +27,11 @@ from homeassistant.components.humidifier.const import ( DOMAIN, SERVICE_SET_HUMIDITY, ) +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, - DEVICE_CLASS_HUMIDITY, PERCENTAGE, SERVICE_TURN_OFF, SERVICE_TURN_ON, @@ -313,7 +313,7 @@ async def test_humidifier_with_linked_humidity_sensor(hass, hk_driver): humidity_sensor_entity_id, "42.0", { - ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_DEVICE_CLASS: SensorDeviceClass.HUMIDITY, ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, ) @@ -341,7 +341,7 @@ async def test_humidifier_with_linked_humidity_sensor(hass, hk_driver): humidity_sensor_entity_id, "43.0", { - ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_DEVICE_CLASS: SensorDeviceClass.HUMIDITY, ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, ) @@ -353,7 +353,7 @@ async def test_humidifier_with_linked_humidity_sensor(hass, hk_driver): humidity_sensor_entity_id, STATE_UNAVAILABLE, { - ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_DEVICE_CLASS: SensorDeviceClass.HUMIDITY, ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, ) diff --git a/tests/components/homekit/test_type_media_players.py b/tests/components/homekit/test_type_media_players.py index 6b24c731fab..7446b0d4c3a 100644 --- a/tests/components/homekit/test_type_media_players.py +++ b/tests/components/homekit/test_type_media_players.py @@ -18,7 +18,7 @@ from homeassistant.components.homekit.type_media_players import ( MediaPlayer, TelevisionMediaPlayer, ) -from homeassistant.components.media_player import DEVICE_CLASS_TV +from homeassistant.components.media_player import MediaPlayerDeviceClass from homeassistant.components.media_player.const import ( ATTR_INPUT_SOURCE, ATTR_INPUT_SOURCE_LIST, @@ -181,7 +181,7 @@ async def test_media_player_television(hass, hk_driver, events, caplog): entity_id, None, { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TV, + ATTR_DEVICE_CLASS: MediaPlayerDeviceClass.TV, ATTR_SUPPORTED_FEATURES: 3469, ATTR_MEDIA_VOLUME_MUTED: False, ATTR_INPUT_SOURCE_LIST: ["HDMI 1", "HDMI 2", "HDMI 3", "HDMI 4"], @@ -358,7 +358,7 @@ async def test_media_player_television_basic(hass, hk_driver, events, caplog): hass.states.async_set( entity_id, None, - {ATTR_DEVICE_CLASS: DEVICE_CLASS_TV, ATTR_SUPPORTED_FEATURES: 384}, + {ATTR_DEVICE_CLASS: MediaPlayerDeviceClass.TV, ATTR_SUPPORTED_FEATURES: 384}, ) await hass.async_block_till_done() acc = TelevisionMediaPlayer(hass, hk_driver, "MediaPlayer", entity_id, 2, None) @@ -394,7 +394,7 @@ async def test_media_player_television_supports_source_select_no_sources( hass.states.async_set( entity_id, None, - {ATTR_DEVICE_CLASS: DEVICE_CLASS_TV, ATTR_SUPPORTED_FEATURES: 3469}, + {ATTR_DEVICE_CLASS: MediaPlayerDeviceClass.TV, ATTR_SUPPORTED_FEATURES: 3469}, ) await hass.async_block_till_done() acc = TelevisionMediaPlayer(hass, hk_driver, "MediaPlayer", entity_id, 2, None) @@ -415,7 +415,7 @@ async def test_tv_restore(hass, hk_driver, events): "generic", "1234", suggested_object_id="simple", - original_device_class=DEVICE_CLASS_TV, + original_device_class=MediaPlayerDeviceClass.TV, ) registry.async_get_or_create( "media_player", @@ -426,7 +426,7 @@ async def test_tv_restore(hass, hk_driver, events): ATTR_INPUT_SOURCE_LIST: ["HDMI 1", "HDMI 2", "HDMI 3", "HDMI 4"], }, supported_features=3469, - original_device_class=DEVICE_CLASS_TV, + original_device_class=MediaPlayerDeviceClass.TV, ) hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) @@ -465,7 +465,7 @@ async def test_media_player_television_max_sources(hass, hk_driver, events, capl entity_id, None, { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TV, + ATTR_DEVICE_CLASS: MediaPlayerDeviceClass.TV, ATTR_SUPPORTED_FEATURES: 3469, ATTR_MEDIA_VOLUME_MUTED: False, ATTR_INPUT_SOURCE: "HDMI 3", @@ -489,7 +489,7 @@ async def test_media_player_television_max_sources(hass, hk_driver, events, capl entity_id, None, { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TV, + ATTR_DEVICE_CLASS: MediaPlayerDeviceClass.TV, ATTR_SUPPORTED_FEATURES: 3469, ATTR_MEDIA_VOLUME_MUTED: False, ATTR_INPUT_SOURCE: "HDMI 90", @@ -503,7 +503,7 @@ async def test_media_player_television_max_sources(hass, hk_driver, events, capl entity_id, None, { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TV, + ATTR_DEVICE_CLASS: MediaPlayerDeviceClass.TV, ATTR_SUPPORTED_FEATURES: 3469, ATTR_MEDIA_VOLUME_MUTED: False, ATTR_INPUT_SOURCE: "HDMI 91", diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index 98e44de7575..0b51e44660c 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -1,5 +1,6 @@ """Test different accessory types: Sensors.""" -from homeassistant.components.binary_sensor import DEVICE_CLASS_MOTION + +from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.homekit import get_accessory from homeassistant.components.homekit.const import ( PROP_CELSIUS, @@ -269,7 +270,7 @@ async def test_motion_uses_bool(hass, hk_driver): entity_id = "binary_sensor.motion" hass.states.async_set( - entity_id, STATE_UNKNOWN, {ATTR_DEVICE_CLASS: DEVICE_CLASS_MOTION} + entity_id, STATE_UNKNOWN, {ATTR_DEVICE_CLASS: BinarySensorDeviceClass.MOTION} ) await hass.async_block_till_done() @@ -282,24 +283,26 @@ async def test_motion_uses_bool(hass, hk_driver): assert acc.char_detected.value is False - hass.states.async_set(entity_id, STATE_ON, {ATTR_DEVICE_CLASS: DEVICE_CLASS_MOTION}) + hass.states.async_set( + entity_id, STATE_ON, {ATTR_DEVICE_CLASS: BinarySensorDeviceClass.MOTION} + ) await hass.async_block_till_done() assert acc.char_detected.value is True hass.states.async_set( - entity_id, STATE_OFF, {ATTR_DEVICE_CLASS: DEVICE_CLASS_MOTION} + entity_id, STATE_OFF, {ATTR_DEVICE_CLASS: BinarySensorDeviceClass.MOTION} ) await hass.async_block_till_done() assert acc.char_detected.value is False hass.states.async_set( - entity_id, STATE_HOME, {ATTR_DEVICE_CLASS: DEVICE_CLASS_MOTION} + entity_id, STATE_HOME, {ATTR_DEVICE_CLASS: BinarySensorDeviceClass.MOTION} ) await hass.async_block_till_done() assert acc.char_detected.value is True hass.states.async_set( - entity_id, STATE_NOT_HOME, {ATTR_DEVICE_CLASS: DEVICE_CLASS_MOTION} + entity_id, STATE_NOT_HOME, {ATTR_DEVICE_CLASS: BinarySensorDeviceClass.MOTION} ) await hass.async_block_till_done() assert acc.char_detected.value is False From 2dfd4c49dae1ec49bae12964ad79e5f7ff068cbd Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Mon, 20 Dec 2021 10:56:56 -0800 Subject: [PATCH 0838/2644] Fix Non-thread-safe operation in wemo tests (#62418) --- tests/components/wemo/entity_test_helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/wemo/entity_test_helpers.py b/tests/components/wemo/entity_test_helpers.py index 6836f87a4a0..c3d671d1fca 100644 --- a/tests/components/wemo/entity_test_helpers.py +++ b/tests/components/wemo/entity_test_helpers.py @@ -53,7 +53,7 @@ async def _async_multiple_call_helper(hass, pywemo_device, call1, call2): return nonlocal call_count call_count += 1 - hass.add_job(waiting.set) + hass.loop.call_soon_threadsafe(waiting.set) event.wait() # Danger! Do not use a Mock side_effect here. The test will deadlock. When From 5a41251d45d26a12db9029aef5db0ee7ad5126d4 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Mon, 20 Dec 2021 20:08:35 +0100 Subject: [PATCH 0839/2644] Add config_flow to AndroidTV integration (#54444) Co-authored-by: Robert Hillis --- .coveragerc | 1 + CODEOWNERS | 4 +- .../components/androidtv/__init__.py | 191 ++++++ .../components/androidtv/config_flow.py | 378 ++++++++++++ homeassistant/components/androidtv/const.py | 34 + .../components/androidtv/manifest.json | 3 +- .../components/androidtv/media_player.py | 492 +++++++-------- .../components/androidtv/services.yaml | 33 +- .../components/androidtv/strings.json | 66 ++ .../components/androidtv/translations/en.json | 66 ++ homeassistant/generated/config_flows.py | 1 + tests/components/androidtv/patchers.py | 8 +- .../components/androidtv/test_config_flow.py | 581 ++++++++++++++++++ .../components/androidtv/test_media_player.py | 527 ++++++++-------- 14 files changed, 1850 insertions(+), 535 deletions(-) create mode 100644 homeassistant/components/androidtv/config_flow.py create mode 100644 homeassistant/components/androidtv/const.py create mode 100644 homeassistant/components/androidtv/strings.json create mode 100644 homeassistant/components/androidtv/translations/en.json create mode 100644 tests/components/androidtv/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 47a82d81861..0c48b0ebb25 100644 --- a/.coveragerc +++ b/.coveragerc @@ -55,6 +55,7 @@ omit = homeassistant/components/amcrest/* homeassistant/components/ampio/* homeassistant/components/android_ip_webcam/* + homeassistant/components/androidtv/__init__.py homeassistant/components/anel_pwrctrl/switch.py homeassistant/components/anthemav/media_player.py homeassistant/components/apcupsd/* diff --git a/CODEOWNERS b/CODEOWNERS index 65ab446fb97..bb926fa8b5b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -63,8 +63,8 @@ tests/components/ambient_station/* @bachya homeassistant/components/amcrest/* @flacjacket homeassistant/components/analytics/* @home-assistant/core @ludeeus tests/components/analytics/* @home-assistant/core @ludeeus -homeassistant/components/androidtv/* @JeffLIrion -tests/components/androidtv/* @JeffLIrion +homeassistant/components/androidtv/* @JeffLIrion @ollo69 +tests/components/androidtv/* @JeffLIrion @ollo69 homeassistant/components/apache_kafka/* @bachya tests/components/apache_kafka/* @bachya homeassistant/components/api/* @home-assistant/core diff --git a/homeassistant/components/androidtv/__init__.py b/homeassistant/components/androidtv/__init__.py index 14832aef315..d64329526b8 100644 --- a/homeassistant/components/androidtv/__init__.py +++ b/homeassistant/components/androidtv/__init__.py @@ -1 +1,192 @@ """Support for functionality to interact with Android TV/Fire TV devices.""" +import logging +import os + +from adb_shell.auth.keygen import keygen +from androidtv.adb_manager.adb_manager_sync import ADBPythonSync +from androidtv.setup_async import setup + +from homeassistant.components.media_player import DOMAIN as MP_DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import ( + CONF_DEVICE_CLASS, + CONF_HOST, + CONF_PORT, + EVENT_HOMEASSISTANT_STOP, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.storage import STORAGE_DIR + +from .const import ( + ANDROID_DEV, + ANDROID_DEV_OPT, + CONF_ADB_SERVER_IP, + CONF_ADB_SERVER_PORT, + CONF_ADBKEY, + CONF_STATE_DETECTION_RULES, + DEFAULT_ADB_SERVER_PORT, + DEVICE_ANDROIDTV, + DEVICE_FIRETV, + DOMAIN, + PROP_SERIALNO, + SIGNAL_CONFIG_ENTITY, +) + +PLATFORMS = [Platform.MEDIA_PLAYER] +RELOAD_OPTIONS = [CONF_STATE_DETECTION_RULES] + +_LOGGER = logging.getLogger(__name__) + + +def _setup_androidtv(hass, config): + """Generate an ADB key (if needed) and load it.""" + adbkey = config.get(CONF_ADBKEY, hass.config.path(STORAGE_DIR, "androidtv_adbkey")) + if CONF_ADB_SERVER_IP not in config: + # Use "adb_shell" (Python ADB implementation) + if not os.path.isfile(adbkey): + # Generate ADB key files + keygen(adbkey) + + # Load the ADB key + signer = ADBPythonSync.load_adbkey(adbkey) + adb_log = f"using Python ADB implementation with adbkey='{adbkey}'" + + else: + # Use "pure-python-adb" (communicate with ADB server) + signer = None + adb_log = f"using ADB server at {config[CONF_ADB_SERVER_IP]}:{config[CONF_ADB_SERVER_PORT]}" + + return adbkey, signer, adb_log + + +async def async_connect_androidtv( + hass, config, *, state_detection_rules=None, timeout=30.0 +): + """Connect to Android device.""" + address = f"{config[CONF_HOST]}:{config[CONF_PORT]}" + + adbkey, signer, adb_log = await hass.async_add_executor_job( + _setup_androidtv, hass, config + ) + + aftv = await setup( + config[CONF_HOST], + config[CONF_PORT], + adbkey, + config.get(CONF_ADB_SERVER_IP), + config.get(CONF_ADB_SERVER_PORT, DEFAULT_ADB_SERVER_PORT), + state_detection_rules, + config[CONF_DEVICE_CLASS], + timeout, + signer, + ) + + if not aftv.available: + # Determine the name that will be used for the device in the log + if config[CONF_DEVICE_CLASS] == DEVICE_ANDROIDTV: + device_name = "Android TV device" + elif config[CONF_DEVICE_CLASS] == DEVICE_FIRETV: + device_name = "Fire TV device" + else: + device_name = "Android TV / Fire TV device" + + error_message = f"Could not connect to {device_name} at {address} {adb_log}" + return None, error_message + + return aftv, None + + +def _migrate_aftv_entity(hass, aftv, entry_unique_id): + """Migrate a entity to new unique id.""" + entity_reg = er.async_get(hass) + + entity_unique_id = entry_unique_id + if entity_reg.async_get_entity_id(MP_DOMAIN, DOMAIN, entity_unique_id): + # entity already exist, nothing to do + return + + old_unique_id = aftv.device_properties.get(PROP_SERIALNO) + if not old_unique_id: + # serial no not found, exit + return + + migr_entity = entity_reg.async_get_entity_id(MP_DOMAIN, DOMAIN, old_unique_id) + if not migr_entity: + # old entity not found, exit + return + + try: + entity_reg.async_update_entity(migr_entity, new_unique_id=entity_unique_id) + except ValueError as exp: + _LOGGER.warning("Migration of old entity failed: %s", exp) + + +async def async_setup(hass, config): + """Set up the Android TV integration.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Android TV platform.""" + + state_det_rules = entry.options.get(CONF_STATE_DETECTION_RULES) + aftv, error_message = await async_connect_androidtv( + hass, entry.data, state_detection_rules=state_det_rules + ) + if not aftv: + raise ConfigEntryNotReady(error_message) + + # migrate existing entity to new unique ID + if entry.source == SOURCE_IMPORT: + _migrate_aftv_entity(hass, aftv, entry.unique_id) + + async def async_close_connection(event): + """Close Android TV connection on HA Stop.""" + await aftv.adb_close() + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_close_connection) + ) + entry.async_on_unload(entry.add_update_listener(update_listener)) + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { + ANDROID_DEV: aftv, + ANDROID_DEV_OPT: entry.options.copy(), + } + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + aftv = hass.data[DOMAIN][entry.entry_id][ANDROID_DEV] + await aftv.adb_close() + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +async def update_listener(hass: HomeAssistant, entry: ConfigEntry): + """Update when config_entry options update.""" + reload_opt = False + old_options = hass.data[DOMAIN][entry.entry_id][ANDROID_DEV_OPT] + for opt_key, opt_val in entry.options.items(): + if opt_key in RELOAD_OPTIONS: + old_val = old_options.get(opt_key) + if old_val is None or old_val != opt_val: + reload_opt = True + break + + if reload_opt: + await hass.config_entries.async_reload(entry.entry_id) + return + + hass.data[DOMAIN][entry.entry_id][ANDROID_DEV_OPT] = entry.options.copy() + async_dispatcher_send(hass, f"{SIGNAL_CONFIG_ENTITY}_{entry.entry_id}") diff --git a/homeassistant/components/androidtv/config_flow.py b/homeassistant/components/androidtv/config_flow.py new file mode 100644 index 00000000000..c346378fbc2 --- /dev/null +++ b/homeassistant/components/androidtv/config_flow.py @@ -0,0 +1,378 @@ +"""Config flow to configure the Android TV integration.""" +import json +import logging +import os +import socket + +from androidtv import state_detection_rules_validator +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_DEVICE_CLASS, CONF_HOST, CONF_NAME, CONF_PORT +from homeassistant.core import callback +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.device_registry import format_mac + +from . import async_connect_androidtv +from .const import ( + CONF_ADB_SERVER_IP, + CONF_ADB_SERVER_PORT, + CONF_ADBKEY, + CONF_APPS, + CONF_EXCLUDE_UNNAMED_APPS, + CONF_GET_SOURCES, + CONF_MIGRATION_OPTIONS, + CONF_SCREENCAP, + CONF_STATE_DETECTION_RULES, + CONF_TURN_OFF_COMMAND, + CONF_TURN_ON_COMMAND, + DEFAULT_ADB_SERVER_PORT, + DEFAULT_DEVICE_CLASS, + DEFAULT_EXCLUDE_UNNAMED_APPS, + DEFAULT_GET_SOURCES, + DEFAULT_PORT, + DEFAULT_SCREENCAP, + DEVICE_CLASSES, + DOMAIN, + PROP_ETHMAC, + PROP_WIFIMAC, +) + +APPS_NEW_ID = "NewApp" +CONF_APP_DELETE = "app_delete" +CONF_APP_ID = "app_id" +CONF_APP_NAME = "app_name" + +RULES_NEW_ID = "NewRule" +CONF_RULE_DELETE = "rule_delete" +CONF_RULE_ID = "rule_id" +CONF_RULE_VALUES = "rule_values" + +RESULT_CONN_ERROR = "cannot_connect" +RESULT_UNKNOWN = "unknown" + +_LOGGER = logging.getLogger(__name__) + + +def _is_file(value): + """Validate that the value is an existing file.""" + file_in = os.path.expanduser(str(value)) + return os.path.isfile(file_in) and os.access(file_in, os.R_OK) + + +def _get_ip(host): + """Get the ip address from the host name.""" + try: + return socket.gethostbyname(host) + except socket.gaierror: + return None + + +class AndroidTVFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow.""" + + VERSION = 1 + + def __init__(self): + """Initialize AndroidTV config flow.""" + self._import_options = None + + @callback + def _show_setup_form(self, user_input=None, error=None): + """Show the setup form to the user.""" + user_input = user_input or {} + data_schema = vol.Schema( + { + vol.Required(CONF_HOST, default=user_input.get(CONF_HOST, "")): str, + vol.Required(CONF_DEVICE_CLASS, default=DEFAULT_DEVICE_CLASS): vol.In( + DEVICE_CLASSES + ), + vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port, + }, + ) + + if self.show_advanced_options: + data_schema = data_schema.extend( + { + vol.Optional(CONF_ADBKEY): str, + vol.Optional(CONF_ADB_SERVER_IP): str, + vol.Required( + CONF_ADB_SERVER_PORT, default=DEFAULT_ADB_SERVER_PORT + ): cv.port, + } + ) + + return self.async_show_form( + step_id="user", + data_schema=data_schema, + errors={"base": error}, + ) + + async def _async_check_connection(self, user_input): + """Attempt to connect the Android TV.""" + + try: + aftv, error_message = await async_connect_androidtv(self.hass, user_input) + except Exception: # pylint: disable=broad-except + _LOGGER.exception( + "Unknown error connecting with Android TV at %s", user_input[CONF_HOST] + ) + return RESULT_UNKNOWN, None + + if not aftv: + _LOGGER.warning(error_message) + return RESULT_CONN_ERROR, None + + dev_prop = aftv.device_properties + unique_id = format_mac( + dev_prop.get(PROP_ETHMAC) or dev_prop.get(PROP_WIFIMAC, "") + ) + await aftv.adb_close() + return None, unique_id + + async def async_step_user(self, user_input=None): + """Handle a flow initiated by the user.""" + error = None + + if user_input is not None: + host = user_input[CONF_HOST] + adb_key = user_input.get(CONF_ADBKEY) + adb_server = user_input.get(CONF_ADB_SERVER_IP) + + if adb_key and adb_server: + return self._show_setup_form(user_input, "key_and_server") + + if adb_key: + isfile = await self.hass.async_add_executor_job(_is_file, adb_key) + if not isfile: + return self._show_setup_form(user_input, "adbkey_not_file") + + ip_address = await self.hass.async_add_executor_job(_get_ip, host) + if not ip_address: + return self._show_setup_form(user_input, "invalid_host") + + self._async_abort_entries_match({CONF_HOST: host}) + if ip_address != host: + self._async_abort_entries_match({CONF_HOST: ip_address}) + + error, unique_id = await self._async_check_connection(user_input) + if error is None: + if not unique_id: + return self.async_abort(reason="invalid_unique_id") + + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=user_input.get(CONF_NAME) or host, + data=user_input, + options=self._import_options, + ) + + user_input = user_input or {} + return self._show_setup_form(user_input, error) + + async def async_step_import(self, import_config=None): + """Import a config entry.""" + for entry in self._async_current_entries(): + if entry.data[CONF_HOST] == import_config[CONF_HOST]: + _LOGGER.warning( + "Host [%s] already configured. This yaml configuration has already been imported. Please remove it", + import_config[CONF_HOST], + ) + return self.async_abort(reason="already_configured") + self._import_options = import_config.pop(CONF_MIGRATION_OPTIONS, None) + return await self.async_step_user(import_config) + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle an option flow for Android TV.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize options flow.""" + self.config_entry = config_entry + + apps = config_entry.options.get(CONF_APPS, {}) + det_rules = config_entry.options.get(CONF_STATE_DETECTION_RULES, {}) + self._apps = apps.copy() + self._state_det_rules = det_rules.copy() + self._conf_app_id = None + self._conf_rule_id = None + + @callback + def _save_config(self, data): + """Save the updated options.""" + new_data = { + k: v + for k, v in data.items() + if k not in [CONF_APPS, CONF_STATE_DETECTION_RULES] + } + if self._apps: + new_data[CONF_APPS] = self._apps + if self._state_det_rules: + new_data[CONF_STATE_DETECTION_RULES] = self._state_det_rules + + return self.async_create_entry(title="", data=new_data) + + async def async_step_init(self, user_input=None): + """Handle options flow.""" + if user_input is not None: + if sel_app := user_input.get(CONF_APPS): + return await self.async_step_apps(None, sel_app) + if sel_rule := user_input.get(CONF_STATE_DETECTION_RULES): + return await self.async_step_rules(None, sel_rule) + return self._save_config(user_input) + + return self._async_init_form() + + @callback + def _async_init_form(self): + """Return initial configuration form.""" + + apps_list = {k: f"{v} ({k})" if v else k for k, v in self._apps.items()} + apps = {APPS_NEW_ID: "Add new", **apps_list} + rules = [RULES_NEW_ID] + list(self._state_det_rules) + options = self.config_entry.options + + data_schema = vol.Schema( + { + vol.Optional(CONF_APPS): vol.In(apps), + vol.Optional( + CONF_GET_SOURCES, + default=options.get(CONF_GET_SOURCES, DEFAULT_GET_SOURCES), + ): bool, + vol.Optional( + CONF_EXCLUDE_UNNAMED_APPS, + default=options.get( + CONF_EXCLUDE_UNNAMED_APPS, DEFAULT_EXCLUDE_UNNAMED_APPS + ), + ): bool, + vol.Optional( + CONF_SCREENCAP, + default=options.get(CONF_SCREENCAP, DEFAULT_SCREENCAP), + ): bool, + vol.Optional( + CONF_TURN_OFF_COMMAND, + description={ + "suggested_value": options.get(CONF_TURN_OFF_COMMAND, "") + }, + ): str, + vol.Optional( + CONF_TURN_ON_COMMAND, + description={ + "suggested_value": options.get(CONF_TURN_ON_COMMAND, "") + }, + ): str, + vol.Optional(CONF_STATE_DETECTION_RULES): vol.In(rules), + } + ) + + return self.async_show_form(step_id="init", data_schema=data_schema) + + async def async_step_apps(self, user_input=None, app_id=None): + """Handle options flow for apps list.""" + if app_id is not None: + self._conf_app_id = app_id if app_id != APPS_NEW_ID else None + return self._async_apps_form(app_id) + + if user_input is not None: + app_id = user_input.get(CONF_APP_ID, self._conf_app_id) + if app_id: + if user_input.get(CONF_APP_DELETE, False): + self._apps.pop(app_id) + else: + self._apps[app_id] = user_input.get(CONF_APP_NAME, "") + + return await self.async_step_init() + + @callback + def _async_apps_form(self, app_id): + """Return configuration form for apps.""" + data_schema = { + vol.Optional( + CONF_APP_NAME, + description={"suggested_value": self._apps.get(app_id, "")}, + ): str, + } + if app_id == APPS_NEW_ID: + data_schema[vol.Optional(CONF_APP_ID)] = str + else: + data_schema[vol.Optional(CONF_APP_DELETE, default=False)] = bool + + return self.async_show_form( + step_id="apps", + data_schema=vol.Schema(data_schema), + description_placeholders={ + "app_id": f"`{app_id}`" if app_id != APPS_NEW_ID else "", + }, + ) + + async def async_step_rules(self, user_input=None, rule_id=None): + """Handle options flow for detection rules.""" + if rule_id is not None: + self._conf_rule_id = rule_id if rule_id != RULES_NEW_ID else None + return self._async_rules_form(rule_id) + + if user_input is not None: + rule_id = user_input.get(CONF_RULE_ID, self._conf_rule_id) + if rule_id: + if user_input.get(CONF_RULE_DELETE, False): + self._state_det_rules.pop(rule_id) + elif str_det_rule := user_input.get(CONF_RULE_VALUES): + state_det_rule = _validate_state_det_rules(str_det_rule) + if state_det_rule is None: + return self._async_rules_form( + rule_id=self._conf_rule_id or RULES_NEW_ID, + default_id=rule_id, + errors={"base": "invalid_det_rules"}, + ) + self._state_det_rules[rule_id] = state_det_rule + + return await self.async_step_init() + + @callback + def _async_rules_form(self, rule_id, default_id="", errors=None): + """Return configuration form for detection rules.""" + state_det_rule = self._state_det_rules.get(rule_id) + str_det_rule = json.dumps(state_det_rule) if state_det_rule else "" + + data_schema = {} + if rule_id == RULES_NEW_ID: + data_schema[vol.Optional(CONF_RULE_ID, default=default_id)] = str + data_schema[vol.Optional(CONF_RULE_VALUES, default=str_det_rule)] = str + if rule_id != RULES_NEW_ID: + data_schema[vol.Optional(CONF_RULE_DELETE, default=False)] = bool + + return self.async_show_form( + step_id="rules", + data_schema=vol.Schema(data_schema), + description_placeholders={ + "rule_id": f"`{rule_id}`" if rule_id != RULES_NEW_ID else "", + }, + errors=errors, + ) + + +def _validate_state_det_rules(state_det_rules): + """Validate a string that contain state detection rules and return a dict.""" + try: + json_rules = json.loads(state_det_rules) + except ValueError: + _LOGGER.warning("Error loading state detection rules") + return None + + if not isinstance(json_rules, list): + json_rules = [json_rules] + + try: + state_detection_rules_validator(json_rules, ValueError) + except ValueError as exc: + _LOGGER.warning("Invalid state detection rules: %s", exc) + return None + return json_rules diff --git a/homeassistant/components/androidtv/const.py b/homeassistant/components/androidtv/const.py new file mode 100644 index 00000000000..f6f0c07286f --- /dev/null +++ b/homeassistant/components/androidtv/const.py @@ -0,0 +1,34 @@ +"""Android TV component constants.""" +DOMAIN = "androidtv" + +ANDROID_DEV = DOMAIN +ANDROID_DEV_OPT = "androidtv_opt" + +CONF_ADB_SERVER_IP = "adb_server_ip" +CONF_ADB_SERVER_PORT = "adb_server_port" +CONF_ADBKEY = "adbkey" +CONF_APPS = "apps" +CONF_EXCLUDE_UNNAMED_APPS = "exclude_unnamed_apps" +CONF_GET_SOURCES = "get_sources" +CONF_MIGRATION_OPTIONS = "migration_options" +CONF_SCREENCAP = "screencap" +CONF_STATE_DETECTION_RULES = "state_detection_rules" +CONF_TURN_OFF_COMMAND = "turn_off_command" +CONF_TURN_ON_COMMAND = "turn_on_command" + +DEFAULT_ADB_SERVER_PORT = 5037 +DEFAULT_DEVICE_CLASS = "auto" +DEFAULT_EXCLUDE_UNNAMED_APPS = False +DEFAULT_GET_SOURCES = True +DEFAULT_PORT = 5555 +DEFAULT_SCREENCAP = True + +DEVICE_ANDROIDTV = "androidtv" +DEVICE_FIRETV = "firetv" +DEVICE_CLASSES = [DEFAULT_DEVICE_CLASS, DEVICE_ANDROIDTV, DEVICE_FIRETV] + +PROP_ETHMAC = "ethmac" +PROP_SERIALNO = "serialno" +PROP_WIFIMAC = "wifimac" + +SIGNAL_CONFIG_ENTITY = "androidtv_config" diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index 00be4fa50c4..f50876e6629 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -7,6 +7,7 @@ "androidtv[async]==0.0.60", "pure-python-adb[async]==0.3.0.dev0" ], - "codeowners": ["@JeffLIrion"], + "codeowners": ["@JeffLIrion", "@ollo69"], + "config_flow": true, "iot_class": "local_polling" } diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 89deeec25b8..099c4fddba2 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -1,10 +1,10 @@ """Support for functionality to interact with Android TV / Fire TV devices.""" +from __future__ import annotations + from datetime import datetime import functools import logging -import os -from adb_shell.auth.keygen import keygen from adb_shell.exceptions import ( AdbTimeoutError, InvalidChecksumError, @@ -13,10 +13,8 @@ from adb_shell.exceptions import ( TcpTimeoutException, ) from androidtv import ha_state_detection_rules_validator -from androidtv.adb_manager.adb_manager_sync import ADBPythonSync from androidtv.constants import APPS, KEYS from androidtv.exceptions import LockNotAcquiredException -from androidtv.setup_async import setup import voluptuous as vol from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity @@ -33,25 +31,58 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( ATTR_COMMAND, - ATTR_ENTITY_ID, + ATTR_CONNECTIONS, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_SW_VERSION, CONF_DEVICE_CLASS, CONF_HOST, CONF_NAME, CONF_PORT, - EVENT_HOMEASSISTANT_STOP, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, STATE_STANDBY, ) -from homeassistant.exceptions import PlatformNotReady +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform -from homeassistant.helpers.storage import STORAGE_DIR +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType -ANDROIDTV_DOMAIN = "androidtv" +from .const import ( + ANDROID_DEV, + ANDROID_DEV_OPT, + CONF_ADB_SERVER_IP, + CONF_ADB_SERVER_PORT, + CONF_ADBKEY, + CONF_APPS, + CONF_EXCLUDE_UNNAMED_APPS, + CONF_GET_SOURCES, + CONF_MIGRATION_OPTIONS, + CONF_SCREENCAP, + CONF_STATE_DETECTION_RULES, + CONF_TURN_OFF_COMMAND, + CONF_TURN_ON_COMMAND, + DEFAULT_ADB_SERVER_PORT, + DEFAULT_DEVICE_CLASS, + DEFAULT_EXCLUDE_UNNAMED_APPS, + DEFAULT_GET_SOURCES, + DEFAULT_PORT, + DEFAULT_SCREENCAP, + DEVICE_ANDROIDTV, + DEVICE_CLASSES, + DOMAIN, + PROP_ETHMAC, + PROP_WIFIMAC, + SIGNAL_CONFIG_ENTITY, +) _LOGGER = logging.getLogger(__name__) @@ -85,77 +116,45 @@ ATTR_DEVICE_PATH = "device_path" ATTR_HDMI_INPUT = "hdmi_input" ATTR_LOCAL_PATH = "local_path" -CONF_ADBKEY = "adbkey" -CONF_ADB_SERVER_IP = "adb_server_ip" -CONF_ADB_SERVER_PORT = "adb_server_port" -CONF_APPS = "apps" -CONF_EXCLUDE_UNNAMED_APPS = "exclude_unnamed_apps" -CONF_GET_SOURCES = "get_sources" -CONF_STATE_DETECTION_RULES = "state_detection_rules" -CONF_TURN_ON_COMMAND = "turn_on_command" -CONF_TURN_OFF_COMMAND = "turn_off_command" -CONF_SCREENCAP = "screencap" - -DEFAULT_NAME = "Android TV" -DEFAULT_PORT = 5555 -DEFAULT_ADB_SERVER_PORT = 5037 -DEFAULT_GET_SOURCES = True -DEFAULT_DEVICE_CLASS = "auto" -DEFAULT_SCREENCAP = True - -DEVICE_ANDROIDTV = "androidtv" -DEVICE_FIRETV = "firetv" -DEVICE_CLASSES = [DEFAULT_DEVICE_CLASS, DEVICE_ANDROIDTV, DEVICE_FIRETV] - SERVICE_ADB_COMMAND = "adb_command" SERVICE_DOWNLOAD = "download" SERVICE_LEARN_SENDEVENT = "learn_sendevent" SERVICE_UPLOAD = "upload" -SERVICE_ADB_COMMAND_SCHEMA = vol.Schema( - {vol.Required(ATTR_ENTITY_ID): cv.entity_ids, vol.Required(ATTR_COMMAND): cv.string} -) +DEFAULT_NAME = "Android TV" -SERVICE_DOWNLOAD_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_id, - vol.Required(ATTR_DEVICE_PATH): cv.string, - vol.Required(ATTR_LOCAL_PATH): cv.string, - } -) - -SERVICE_UPLOAD_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_DEVICE_PATH): cv.string, - vol.Required(ATTR_LOCAL_PATH): cv.string, - } -) - - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_DEVICE_CLASS, default=DEFAULT_DEVICE_CLASS): vol.In( - DEVICE_CLASSES +# Deprecated in Home Assistant 2022.2 +PLATFORM_SCHEMA = cv.deprecated( + vol.All( + PLATFORM_SCHEMA=PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_DEVICE_CLASS, default=DEFAULT_DEVICE_CLASS): vol.In( + DEVICE_CLASSES + ), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_ADBKEY): cv.isfile, + vol.Optional(CONF_ADB_SERVER_IP): cv.string, + vol.Optional( + CONF_ADB_SERVER_PORT, default=DEFAULT_ADB_SERVER_PORT + ): cv.port, + vol.Optional(CONF_GET_SOURCES, default=DEFAULT_GET_SOURCES): cv.boolean, + vol.Optional(CONF_APPS, default={}): vol.Schema( + {cv.string: vol.Any(cv.string, None)} + ), + vol.Optional(CONF_TURN_ON_COMMAND): cv.string, + vol.Optional(CONF_TURN_OFF_COMMAND): cv.string, + vol.Optional(CONF_STATE_DETECTION_RULES, default={}): vol.Schema( + {cv.string: ha_state_detection_rules_validator(vol.Invalid)} + ), + vol.Optional( + CONF_EXCLUDE_UNNAMED_APPS, default=DEFAULT_EXCLUDE_UNNAMED_APPS + ): cv.boolean, + vol.Optional(CONF_SCREENCAP, default=DEFAULT_SCREENCAP): cv.boolean, + } ), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_ADBKEY): cv.isfile, - vol.Optional(CONF_ADB_SERVER_IP): cv.string, - vol.Optional(CONF_ADB_SERVER_PORT, default=DEFAULT_ADB_SERVER_PORT): cv.port, - vol.Optional(CONF_GET_SOURCES, default=DEFAULT_GET_SOURCES): cv.boolean, - vol.Optional(CONF_APPS, default={}): vol.Schema( - {cv.string: vol.Any(cv.string, None)} - ), - vol.Optional(CONF_TURN_ON_COMMAND): cv.string, - vol.Optional(CONF_TURN_OFF_COMMAND): cv.string, - vol.Optional(CONF_STATE_DETECTION_RULES, default={}): vol.Schema( - {cv.string: ha_state_detection_rules_validator(vol.Invalid)} - ), - vol.Optional(CONF_EXCLUDE_UNNAMED_APPS, default=False): cv.boolean, - vol.Optional(CONF_SCREENCAP, default=DEFAULT_SCREENCAP): cv.boolean, - } + ) ) # Translate from `AndroidTV` / `FireTV` reported state to HA state. @@ -168,180 +167,108 @@ ANDROIDTV_STATES = { } -def setup_androidtv(hass, config): - """Generate an ADB key (if needed) and load it.""" - adbkey = config.get(CONF_ADBKEY, hass.config.path(STORAGE_DIR, "androidtv_adbkey")) - if CONF_ADB_SERVER_IP not in config: - # Use "adb_shell" (Python ADB implementation) - if not os.path.isfile(adbkey): - # Generate ADB key files - keygen(adbkey) - - # Load the ADB key - signer = ADBPythonSync.load_adbkey(adbkey) - adb_log = f"using Python ADB implementation with adbkey='{adbkey}'" - - else: - # Use "pure-python-adb" (communicate with ADB server) - signer = None - adb_log = f"using ADB server at {config[CONF_ADB_SERVER_IP]}:{config[CONF_ADB_SERVER_PORT]}" - - return adbkey, signer, adb_log - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info=None, +) -> None: """Set up the Android TV / Fire TV platform.""" - hass.data.setdefault(ANDROIDTV_DOMAIN, {}) - address = f"{config[CONF_HOST]}:{config[CONF_PORT]}" + host = config[CONF_HOST] - if address in hass.data[ANDROIDTV_DOMAIN]: - _LOGGER.warning("Platform already setup on %s, skipping", address) - return + # get main data + config_data = { + CONF_HOST: host, + CONF_DEVICE_CLASS: config.get(CONF_DEVICE_CLASS, DEFAULT_DEVICE_CLASS), + CONF_PORT: config.get(CONF_PORT, DEFAULT_PORT), + } + for key in (CONF_ADBKEY, CONF_ADB_SERVER_IP, CONF_ADB_SERVER_PORT, CONF_NAME): + if key in config: + config_data[key] = config[key] - adbkey, signer, adb_log = await hass.async_add_executor_job( - setup_androidtv, hass, config - ) - - aftv = await setup( - config[CONF_HOST], - config[CONF_PORT], - adbkey, - config.get(CONF_ADB_SERVER_IP, ""), - config[CONF_ADB_SERVER_PORT], - config[CONF_STATE_DETECTION_RULES], - config[CONF_DEVICE_CLASS], - 10.0, - signer, - ) - - if not aftv.available: - # Determine the name that will be used for the device in the log - if CONF_NAME in config: - device_name = config[CONF_NAME] - elif config[CONF_DEVICE_CLASS] == DEVICE_ANDROIDTV: - device_name = "Android TV device" - elif config[CONF_DEVICE_CLASS] == DEVICE_FIRETV: - device_name = "Fire TV device" - else: - device_name = "Android TV / Fire TV device" - - _LOGGER.warning( - "Could not connect to %s at %s %s", device_name, address, adb_log + # get options + config_options = { + key: config[key] + for key in ( + CONF_APPS, + CONF_EXCLUDE_UNNAMED_APPS, + CONF_GET_SOURCES, + CONF_SCREENCAP, + CONF_STATE_DETECTION_RULES, + CONF_TURN_OFF_COMMAND, + CONF_TURN_ON_COMMAND, ) - raise PlatformNotReady + if key in config + } - async def _async_close(event): - """Close the ADB socket connection when HA stops.""" - await aftv.adb_close() + # save option to use with entry + if config_options: + config_data[CONF_MIGRATION_OPTIONS] = config_options - # Close the ADB connection when HA stops - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_close) + # Launch config entries setup + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config_data + ) + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Android TV entity.""" + aftv = hass.data[DOMAIN][entry.entry_id][ANDROID_DEV] + device_class = aftv.DEVICE_CLASS + device_type = "Android TV" if device_class == DEVICE_ANDROIDTV else "Fire TV" + if CONF_NAME in entry.data: + device_name = entry.data[CONF_NAME] + else: + device_name = f"{device_type} {entry.data[CONF_HOST]}" device_args = [ aftv, - config[CONF_NAME], - config[CONF_APPS], - config[CONF_GET_SOURCES], - config.get(CONF_TURN_ON_COMMAND), - config.get(CONF_TURN_OFF_COMMAND), - config[CONF_EXCLUDE_UNNAMED_APPS], - config[CONF_SCREENCAP], + device_name, + device_type, + entry.unique_id, + entry.entry_id, + hass.data[DOMAIN][entry.entry_id], ] - if aftv.DEVICE_CLASS == DEVICE_ANDROIDTV: - device = AndroidTVDevice(*device_args) - device_name = config.get(CONF_NAME, "Android TV") - else: - device = FireTVDevice(*device_args) - device_name = config.get(CONF_NAME, "Fire TV") - - async_add_entities([device]) - _LOGGER.debug("Setup %s at %s %s", device_name, address, adb_log) - hass.data[ANDROIDTV_DOMAIN][address] = device - - if hass.services.has_service(ANDROIDTV_DOMAIN, SERVICE_ADB_COMMAND): - return - - platform = entity_platform.async_get_current_platform() - - async def service_adb_command(service): - """Dispatch service calls to target entities.""" - cmd = service.data[ATTR_COMMAND] - entity_id = service.data[ATTR_ENTITY_ID] - target_devices = [ - dev - for dev in hass.data[ANDROIDTV_DOMAIN].values() - if dev.entity_id in entity_id + async_add_entities( + [ + AndroidTVDevice(*device_args) + if device_class == DEVICE_ANDROIDTV + else FireTVDevice(*device_args) ] - - for target_device in target_devices: - output = await target_device.adb_command(cmd) - - # log the output, if there is any - if output: - _LOGGER.info( - "Output of command '%s' from '%s': %s", - cmd, - target_device.entity_id, - output, - ) - - hass.services.async_register( - ANDROIDTV_DOMAIN, - SERVICE_ADB_COMMAND, - service_adb_command, - schema=SERVICE_ADB_COMMAND_SCHEMA, ) + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service( + SERVICE_ADB_COMMAND, + {vol.Required(ATTR_COMMAND): cv.string}, + "adb_command", + ) platform.async_register_entity_service( SERVICE_LEARN_SENDEVENT, {}, "learn_sendevent" ) - - async def service_download(service): - """Download a file from your Android TV / Fire TV device to your Home Assistant instance.""" - local_path = service.data[ATTR_LOCAL_PATH] - if not hass.config.is_allowed_path(local_path): - _LOGGER.warning("'%s' is not secure to load data from!", local_path) - return - - device_path = service.data[ATTR_DEVICE_PATH] - entity_id = service.data[ATTR_ENTITY_ID] - target_device = [ - dev - for dev in hass.data[ANDROIDTV_DOMAIN].values() - if dev.entity_id in entity_id - ][0] - - await target_device.adb_pull(local_path, device_path) - - hass.services.async_register( - ANDROIDTV_DOMAIN, + platform.async_register_entity_service( SERVICE_DOWNLOAD, - service_download, - schema=SERVICE_DOWNLOAD_SCHEMA, + { + vol.Required(ATTR_DEVICE_PATH): cv.string, + vol.Required(ATTR_LOCAL_PATH): cv.string, + }, + "service_download", ) - - async def service_upload(service): - """Upload a file from your Home Assistant instance to an Android TV / Fire TV device.""" - local_path = service.data[ATTR_LOCAL_PATH] - if not hass.config.is_allowed_path(local_path): - _LOGGER.warning("'%s' is not secure to load data from!", local_path) - return - - device_path = service.data[ATTR_DEVICE_PATH] - entity_id = service.data[ATTR_ENTITY_ID] - target_devices = [ - dev - for dev in hass.data[ANDROIDTV_DOMAIN].values() - if dev.entity_id in entity_id - ] - - for target_device in target_devices: - await target_device.adb_push(local_path, device_path) - - hass.services.async_register( - ANDROIDTV_DOMAIN, SERVICE_UPLOAD, service_upload, schema=SERVICE_UPLOAD_SCHEMA + platform.async_register_entity_service( + SERVICE_UPLOAD, + { + vol.Required(ATTR_DEVICE_PATH): cv.string, + vol.Required(ATTR_LOCAL_PATH): cv.string, + }, + "service_upload", ) @@ -398,37 +325,42 @@ class ADBDevice(MediaPlayerEntity): self, aftv, name, - apps, - get_sources, - turn_on_command, - turn_off_command, - exclude_unnamed_apps, - screencap, + dev_type, + unique_id, + entry_id, + entry_data, ): """Initialize the Android TV / Fire TV device.""" self.aftv = aftv self._attr_name = name - self._app_id_to_name = APPS.copy() - self._app_id_to_name.update(apps) - self._app_name_to_id = { - value: key for key, value in self._app_id_to_name.items() if value - } + self._attr_unique_id = unique_id + self._entry_id = entry_id + self._entry_data = entry_data - # Make sure that apps overridden via the `apps` parameter are reflected - # in `self._app_name_to_id` - for key, value in apps.items(): - self._app_name_to_id[value] = key - self._get_sources = get_sources - self._attr_unique_id = self.aftv.device_properties.get("serialno") + info = aftv.device_properties + model = info.get(ATTR_MODEL) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + model=f"{model} ({dev_type})" if model else dev_type, + name=name, + ) + if manufacturer := info.get(ATTR_MANUFACTURER): + self._attr_device_info[ATTR_MANUFACTURER] = manufacturer + if sw_version := info.get(ATTR_SW_VERSION): + self._attr_device_info[ATTR_SW_VERSION] = sw_version + if mac := format_mac(info.get(PROP_ETHMAC) or info.get(PROP_WIFIMAC, "")): + self._attr_device_info[ATTR_CONNECTIONS] = {(CONNECTION_NETWORK_MAC, mac)} - self.turn_on_command = turn_on_command - self.turn_off_command = turn_off_command - - self._exclude_unnamed_apps = exclude_unnamed_apps - self._screencap = screencap + self._app_id_to_name = {} + self._app_name_to_id = {} + self._get_sources = DEFAULT_GET_SOURCES + self._exclude_unnamed_apps = DEFAULT_EXCLUDE_UNNAMED_APPS + self._screencap = DEFAULT_SCREENCAP + self.turn_on_command = None + self.turn_off_command = None # ADB exceptions to catch - if not self.aftv.adb_server_ip: + if not aftv.adb_server_ip: # Using "adb_shell" (Python ADB implementation) self.exceptions = ( AdbTimeoutError, @@ -450,8 +382,46 @@ class ADBDevice(MediaPlayerEntity): ATTR_HDMI_INPUT: None, } + def _process_config(self): + """Load the config options.""" + _LOGGER.debug("Loading configuration options") + options = self._entry_data[ANDROID_DEV_OPT] + + apps = options.get(CONF_APPS, {}) + self._app_id_to_name = APPS.copy() + self._app_id_to_name.update(apps) + self._app_name_to_id = { + value: key for key, value in self._app_id_to_name.items() if value + } + + # Make sure that apps overridden via the `apps` parameter are reflected + # in `self._app_name_to_id` + for key, value in apps.items(): + self._app_name_to_id[value] = key + + self._get_sources = options.get(CONF_GET_SOURCES, DEFAULT_GET_SOURCES) + self._exclude_unnamed_apps = options.get( + CONF_EXCLUDE_UNNAMED_APPS, DEFAULT_EXCLUDE_UNNAMED_APPS + ) + self._screencap = options.get(CONF_SCREENCAP, DEFAULT_SCREENCAP) + self.turn_off_command = options.get(CONF_TURN_OFF_COMMAND) + self.turn_on_command = options.get(CONF_TURN_ON_COMMAND) + + async def async_added_to_hass(self): + """Set config parameter when add to hass.""" + await super().async_added_to_hass() + self._process_config() + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{SIGNAL_CONFIG_ENTITY}_{self._entry_id}", + self._process_config, + ) + ) + return + @property - def media_image_hash(self): + def media_image_hash(self) -> str | None: """Hash value for media image.""" return f"{datetime.now().timestamp()}" if self._screencap else None @@ -531,13 +501,13 @@ class ADBDevice(MediaPlayerEntity): await self.aftv.stop_app(self._app_name_to_id.get(source_, source_)) @adb_decorator() - async def adb_command(self, cmd): + async def adb_command(self, command): """Send an ADB command to an Android TV / Fire TV device.""" - if key := KEYS.get(cmd): + if key := KEYS.get(command): await self.aftv.adb_shell(f"input keyevent {key}") return - if cmd == "GET_PROPERTIES": + if command == "GET_PROPERTIES": self._attr_extra_state_attributes[ATTR_ADB_RESPONSE] = str( await self.aftv.get_properties_dict() ) @@ -545,7 +515,7 @@ class ADBDevice(MediaPlayerEntity): return try: - response = await self.aftv.adb_shell(cmd) + response = await self.aftv.adb_shell(command) except UnicodeDecodeError: return @@ -571,13 +541,21 @@ class ADBDevice(MediaPlayerEntity): _LOGGER.info("%s", msg) @adb_decorator() - async def adb_pull(self, local_path, device_path): + async def service_download(self, device_path, local_path): """Download a file from your Android TV / Fire TV device to your Home Assistant instance.""" + if not self.hass.config.is_allowed_path(local_path): + _LOGGER.warning("'%s' is not secure to load data from!", local_path) + return + await self.aftv.adb_pull(local_path, device_path) @adb_decorator() - async def adb_push(self, local_path, device_path): + async def service_upload(self, device_path, local_path): """Upload a file from your Home Assistant instance to an Android TV / Fire TV device.""" + if not self.hass.config.is_allowed_path(local_path): + _LOGGER.warning("'%s' is not secure to load data from!", local_path) + return + await self.aftv.adb_push(local_path, device_path) diff --git a/homeassistant/components/androidtv/services.yaml b/homeassistant/components/androidtv/services.yaml index 6c8469f46c0..fef06266e52 100644 --- a/homeassistant/components/androidtv/services.yaml +++ b/homeassistant/components/androidtv/services.yaml @@ -3,14 +3,11 @@ adb_command: name: ADB command description: Send an ADB command to an Android TV / Fire TV device. + target: + entity: + integration: androidtv + domain: media_player fields: - entity_id: - description: Name(s) of Android TV / Fire TV entities. - required: true - selector: - entity: - integration: androidtv - domain: media_player command: name: Command description: Either a key command or an ADB shell command. @@ -21,14 +18,11 @@ adb_command: download: name: Download description: Download a file from your Android TV / Fire TV device to your Home Assistant instance. + target: + entity: + integration: androidtv + domain: media_player fields: - entity_id: - description: Name of Android TV / Fire TV entity. - required: true - selector: - entity: - integration: androidtv - domain: media_player device_path: name: Device path description: The filepath on the Android TV / Fire TV device. @@ -46,14 +40,11 @@ download: upload: name: Upload description: Upload a file from your Home Assistant instance to an Android TV / Fire TV device. + target: + entity: + integration: androidtv + domain: media_player fields: - entity_id: - description: Name(s) of Android TV / Fire TV entities. - required: true - selector: - entity: - integration: androidtv - domain: media_player device_path: name: Device path description: The filepath on the Android TV / Fire TV device. diff --git a/homeassistant/components/androidtv/strings.json b/homeassistant/components/androidtv/strings.json new file mode 100644 index 00000000000..c6840f622c7 --- /dev/null +++ b/homeassistant/components/androidtv/strings.json @@ -0,0 +1,66 @@ +{ + "config": { + "step": { + "user": { + "title": "Android TV", + "description": "Set required parameters to connect to your Android TV device", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "adbkey": "Path to your ADB key file (leave empty to auto generate)", + "adb_server_ip": "IP address of the ADB server (leave empty to not use)", + "adb_server_port": "Port of the ADB server", + "device_class": "The type of device", + "port": "[%key:common::config_flow::data::port%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_host": "[%key:common::config_flow::error::invalid_host%]", + "adbkey_not_file": "ADB key file not found", + "key_and_server": "Only provide ADB Key or ADB Server", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "invalid_unique_id": "Impossible to determine a valid unique id for the device" + } + }, + "options": { + "step": { + "init": { + "title": "Android TV Options", + "data": { + "apps": "Configure applications list", + "get_sources": "Whether or not to retrieve the running apps as the list of sources", + "exclude_unnamed_apps": "Exclude app with unknown name", + "screencap": "Determines if album art should be pulled from what is shown on screen", + "state_detection_rules": "Configure state detection rules", + "turn_off_command": "ADB shell command to override default turn_off command", + "turn_on_command": "ADB shell command to override default turn_on command" + } + }, + "apps": { + "title": "Configure Android TV Apps", + "description": "Configure application id {app_id}", + "data": { + "app_name": "Application Name", + "app_id": "Application ID", + "app_delete": "Check to delete this application" + } + }, + "rules": { + "title": "Configure Android TV state detection rules", + "description": "Configure detection rule for application id {rule_id}", + "data": { + "rule_id": "Application ID", + "rule_values": "List of state detection rules (see documentation)", + "rule_delete": "Check to delete this rule" + } + } + }, + "error": { + "invalid_det_rules": "Invalid state detection rules" + } + } +} diff --git a/homeassistant/components/androidtv/translations/en.json b/homeassistant/components/androidtv/translations/en.json new file mode 100644 index 00000000000..2b7b86e8bfe --- /dev/null +++ b/homeassistant/components/androidtv/translations/en.json @@ -0,0 +1,66 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "invalid_unique_id": "Impossible to determine a valid unique id for the device" + }, + "error": { + "adbkey_not_file": "ADB key file not found", + "cannot_connect": "Failed to connect", + "invalid_host": "Invalid hostname or IP address", + "key_and_server": "Only provide ADB Key or ADB Server", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "adb_server_ip": "IP address of the ADB server (leave empty to not use)", + "adb_server_port": "Port of the ADB server", + "adbkey": "Path to your ADB key file (leave empty to auto generate)", + "device_class": "The type of device", + "host": "Host", + "port": "Port" + }, + "description": "Set required parameters to connect to your Android TV device", + "title": "Android TV" + } + } + }, + "options": { + "error": { + "invalid_det_rules": "Invalid state detection rules" + }, + "step": { + "apps": { + "data": { + "app_delete": "Check to delete this application", + "app_id": "Application ID", + "app_name": "Application Name" + }, + "description": "Configure application id {app_id}", + "title": "Configure Android TV Apps" + }, + "init": { + "data": { + "apps": "Configure applications list", + "exclude_unnamed_apps": "Exclude app with unknown name", + "get_sources": "Whether or not to retrieve the running apps as the list of sources", + "screencap": "Determines if album art should be pulled from what is shown on screen", + "state_detection_rules": "Configure state detection rules", + "turn_off_command": "ADB shell command to override default turn_off command", + "turn_on_command": "ADB shell command to override default turn_on command" + }, + "title": "Android TV Options" + }, + "rules": { + "data": { + "rule_delete": "Check to delete this rule", + "rule_id": "Application ID", + "rule_values": "List of state detection rules (see documentation)" + }, + "description": "Configure detection rule for application id {rule_id}", + "title": "Configure Android TV state detection rules" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 74a90052fa4..f1474f415d0 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -25,6 +25,7 @@ FLOWS = [ "amberelectric", "ambiclimate", "ambient_station", + "androidtv", "apple_tv", "arcam_fmj", "aseko_pool_live", diff --git a/tests/components/androidtv/patchers.py b/tests/components/androidtv/patchers.py index b8ee4aaa2cd..c92ac11ba4b 100644 --- a/tests/components/androidtv/patchers.py +++ b/tests/components/androidtv/patchers.py @@ -139,9 +139,9 @@ PATCH_ADB_DEVICE_TCP = patch( PATCH_ANDROIDTV_OPEN = patch( "homeassistant.components.androidtv.media_player.open", mock_open() ) -PATCH_KEYGEN = patch("homeassistant.components.androidtv.media_player.keygen") +PATCH_KEYGEN = patch("homeassistant.components.androidtv.keygen") PATCH_SIGNER = patch( - "homeassistant.components.androidtv.media_player.ADBPythonSync.load_adbkey", + "homeassistant.components.androidtv.ADBPythonSync.load_adbkey", return_value="signer for testing", ) @@ -151,10 +151,6 @@ def isfile(filepath): return filepath.endswith("adbkey") -PATCH_ISFILE = patch("os.path.isfile", isfile) -PATCH_ACCESS = patch("os.access", return_value=True) - - def patch_firetv_update(state, current_app, running_apps, hdmi_input): """Patch the `FireTV.update()` method.""" return patch( diff --git a/tests/components/androidtv/test_config_flow.py b/tests/components/androidtv/test_config_flow.py new file mode 100644 index 00000000000..3da1a113887 --- /dev/null +++ b/tests/components/androidtv/test_config_flow.py @@ -0,0 +1,581 @@ +"""Tests for the AndroidTV config flow.""" +import json +from socket import gaierror +from unittest.mock import patch + +from homeassistant import data_entry_flow +from homeassistant.components.androidtv.config_flow import ( + APPS_NEW_ID, + CONF_APP_DELETE, + CONF_APP_ID, + CONF_APP_NAME, + CONF_RULE_DELETE, + CONF_RULE_ID, + CONF_RULE_VALUES, + RULES_NEW_ID, +) +from homeassistant.components.androidtv.const import ( + CONF_ADB_SERVER_IP, + CONF_ADB_SERVER_PORT, + CONF_ADBKEY, + CONF_APPS, + CONF_EXCLUDE_UNNAMED_APPS, + CONF_GET_SOURCES, + CONF_SCREENCAP, + CONF_STATE_DETECTION_RULES, + CONF_TURN_OFF_COMMAND, + CONF_TURN_ON_COMMAND, + DEFAULT_ADB_SERVER_PORT, + DEFAULT_PORT, + DOMAIN, + PROP_ETHMAC, +) +from homeassistant.components.media_player import DOMAIN as MP_DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import CONF_DEVICE_CLASS, CONF_HOST, CONF_PLATFORM, CONF_PORT +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry +from tests.components.androidtv.patchers import isfile + +ADBKEY = "adbkey" +ETH_MAC = "a1:b1:c1:d1:e1:f1" +HOST = "127.0.0.1" +VALID_DETECT_RULE = [{"paused": {"media_session_state": 3}}] + +# Android TV device with Python ADB implementation +CONFIG_PYTHON_ADB = { + CONF_HOST: HOST, + CONF_PORT: DEFAULT_PORT, + CONF_DEVICE_CLASS: "androidtv", + CONF_ADB_SERVER_PORT: DEFAULT_ADB_SERVER_PORT, +} + +# Android TV device with ADB server +CONFIG_ADB_SERVER = { + CONF_HOST: HOST, + CONF_PORT: DEFAULT_PORT, + CONF_DEVICE_CLASS: "androidtv", + CONF_ADB_SERVER_IP: "127.0.0.1", + CONF_ADB_SERVER_PORT: DEFAULT_ADB_SERVER_PORT, +} + +CONNECT_METHOD = ( + "homeassistant.components.androidtv.config_flow.async_connect_androidtv" +) +PATCH_ACCESS = patch( + "homeassistant.components.androidtv.config_flow.os.access", return_value=True +) +PATCH_GET_HOST_IP = patch( + "homeassistant.components.androidtv.config_flow.socket.gethostbyname", + return_value=HOST, +) +PATCH_ISFILE = patch( + "homeassistant.components.androidtv.config_flow.os.path.isfile", isfile +) +PATCH_SETUP_ENTRY = patch( + "homeassistant.components.androidtv.async_setup_entry", + return_value=True, +) + + +class MockConfigDevice: + """Mock class to emulate Android TV device.""" + + def __init__(self, eth_mac=ETH_MAC): + """Initialize a fake device to test config flow.""" + self.available = True + self.device_properties = {PROP_ETHMAC: eth_mac} + + async def adb_close(self): + """Fake method to close connection.""" + self.available = False + + +async def _test_user(hass, config): + """Test user config.""" + flow_result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER, "show_advanced_options": True} + ) + assert flow_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert flow_result["step_id"] == "user" + + # test with all provided + with patch( + CONNECT_METHOD, + return_value=(MockConfigDevice(), None), + ), PATCH_SETUP_ENTRY as mock_setup_entry, PATCH_GET_HOST_IP: + result = await hass.config_entries.flow.async_configure( + flow_result["flow_id"], user_input=config + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == HOST + assert result["data"] == config + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_python_adb(hass): + """Test user config for Python ADB.""" + await _test_user(hass, CONFIG_PYTHON_ADB) + + +async def test_user_adb_server(hass): + """Test user config for ADB server.""" + await _test_user(hass, CONFIG_ADB_SERVER) + + +async def test_import(hass): + """Test import config.""" + + # test with all provided + with patch( + CONNECT_METHOD, + return_value=(MockConfigDevice(), None), + ), PATCH_SETUP_ENTRY as mock_setup_entry, PATCH_GET_HOST_IP: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=CONFIG_PYTHON_ADB, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == HOST + assert result["data"] == CONFIG_PYTHON_ADB + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_adbkey(hass): + """Test user step with adbkey file.""" + config_data = CONFIG_PYTHON_ADB.copy() + config_data[CONF_ADBKEY] = ADBKEY + + with patch( + CONNECT_METHOD, + return_value=(MockConfigDevice(), None), + ), PATCH_SETUP_ENTRY as mock_setup_entry, PATCH_GET_HOST_IP, PATCH_ISFILE, PATCH_ACCESS: + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER, "show_advanced_options": True}, + data=config_data, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == HOST + assert result["data"] == config_data + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_data(hass): + """Test import from configuration file.""" + config_data = CONFIG_PYTHON_ADB.copy() + config_data[CONF_PLATFORM] = DOMAIN + config_data[CONF_ADBKEY] = ADBKEY + config_data[CONF_TURN_OFF_COMMAND] = "off" + config_data[CONF_STATE_DETECTION_RULES] = {"a": "b"} + platform_data = {MP_DOMAIN: config_data} + + with patch( + CONNECT_METHOD, + return_value=(MockConfigDevice(), None), + ), PATCH_SETUP_ENTRY as mock_setup_entry, PATCH_GET_HOST_IP, PATCH_ISFILE, PATCH_ACCESS: + + assert await async_setup_component(hass, MP_DOMAIN, platform_data) + await hass.async_block_till_done() + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_error_both_key_server(hass): + """Test we abort if both adb key and server are provided.""" + config_data = CONFIG_ADB_SERVER.copy() + + config_data[CONF_ADBKEY] = ADBKEY + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER, "show_advanced_options": True}, + data=config_data, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "key_and_server"} + + with patch( + CONNECT_METHOD, + return_value=(MockConfigDevice(), None), + ), PATCH_SETUP_ENTRY, PATCH_GET_HOST_IP: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=CONFIG_ADB_SERVER + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == HOST + assert result2["data"] == CONFIG_ADB_SERVER + + +async def test_error_invalid_key(hass): + """Test we abort if component is already setup.""" + config_data = CONFIG_PYTHON_ADB.copy() + config_data[CONF_ADBKEY] = ADBKEY + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER, "show_advanced_options": True}, + data=config_data, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "adbkey_not_file"} + + with patch( + CONNECT_METHOD, + return_value=(MockConfigDevice(), None), + ), PATCH_SETUP_ENTRY, PATCH_GET_HOST_IP: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=CONFIG_ADB_SERVER + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == HOST + assert result2["data"] == CONFIG_ADB_SERVER + + +async def test_error_invalid_host(hass): + """Test we abort if host name is invalid.""" + with patch( + "socket.gethostbyname", + side_effect=gaierror, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER, "show_advanced_options": True}, + data=CONFIG_ADB_SERVER, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "invalid_host"} + + with patch( + CONNECT_METHOD, + return_value=(MockConfigDevice(), None), + ), PATCH_SETUP_ENTRY, PATCH_GET_HOST_IP: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=CONFIG_ADB_SERVER + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == HOST + assert result2["data"] == CONFIG_ADB_SERVER + + +async def test_invalid_serial(hass): + """Test for invallid serialno.""" + with patch( + CONNECT_METHOD, + return_value=(MockConfigDevice(eth_mac=""), None), + ), PATCH_GET_HOST_IP: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=CONFIG_ADB_SERVER, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "invalid_unique_id" + + +async def test_abort_if_host_exist(hass): + """Test we abort if component is already setup.""" + MockConfigEntry( + domain=DOMAIN, data=CONFIG_ADB_SERVER, unique_id=ETH_MAC + ).add_to_hass(hass) + + config_data = CONFIG_ADB_SERVER.copy() + config_data[CONF_HOST] = "name" + # Should fail, same IP Address (by PATCH_GET_HOST_IP) + with PATCH_GET_HOST_IP: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=config_data, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_abort_import_if_host_exist(hass): + """Test we abort if component is already setup.""" + MockConfigEntry( + domain=DOMAIN, data=CONFIG_ADB_SERVER, unique_id=ETH_MAC + ).add_to_hass(hass) + + # Should fail, same Host in entry + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=CONFIG_ADB_SERVER, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_abort_if_unique_exist(hass): + """Test we abort if component is already setup.""" + config_data = CONFIG_ADB_SERVER.copy() + config_data[CONF_HOST] = "127.0.0.2" + MockConfigEntry(domain=DOMAIN, data=config_data, unique_id=ETH_MAC).add_to_hass( + hass + ) + + # Should fail, same SerialNo + with patch( + CONNECT_METHOD, + return_value=(MockConfigDevice(), None), + ), PATCH_GET_HOST_IP: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=CONFIG_ADB_SERVER, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_on_connect_failed(hass): + """Test when we have errors connecting the router.""" + flow_result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER, "show_advanced_options": True}, + ) + + with patch(CONNECT_METHOD, return_value=(None, "Error")), PATCH_GET_HOST_IP: + result = await hass.config_entries.flow.async_configure( + flow_result["flow_id"], user_input=CONFIG_ADB_SERVER + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_connect"} + + with patch( + CONNECT_METHOD, + side_effect=TypeError, + ), PATCH_GET_HOST_IP: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=CONFIG_ADB_SERVER + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "unknown"} + + with patch( + CONNECT_METHOD, + return_value=(MockConfigDevice(), None), + ), PATCH_SETUP_ENTRY, PATCH_GET_HOST_IP: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], user_input=CONFIG_ADB_SERVER + ) + await hass.async_block_till_done() + + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["title"] == HOST + assert result3["data"] == CONFIG_ADB_SERVER + + +async def test_options_flow(hass): + """Test config flow options.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=CONFIG_ADB_SERVER, + unique_id=ETH_MAC, + options={ + CONF_APPS: {"app1": "App1"}, + CONF_STATE_DETECTION_RULES: {"com.plexapp.android": VALID_DETECT_RULE}, + }, + ) + config_entry.add_to_hass(hass) + + with PATCH_SETUP_ENTRY: + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + result = await hass.config_entries.options.async_init(config_entry.entry_id) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + # test app form with existing app + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_APPS: "app1", + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "apps" + + # test change value in apps form + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_APP_NAME: "Appl1", + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + # test app form with new app + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_APPS: APPS_NEW_ID, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "apps" + + # test save value for new app + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_APP_ID: "app2", + CONF_APP_NAME: "Appl2", + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + # test app form for delete + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_APPS: "app1", + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "apps" + + # test delete app1 + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_APP_NAME: "Appl1", + CONF_APP_DELETE: True, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + # test rules form with existing rule + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_STATE_DETECTION_RULES: "com.plexapp.android", + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "rules" + + # test change value in rule form with invalid json rule + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_RULE_VALUES: "a", + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "rules" + assert result["errors"] == {"base": "invalid_det_rules"} + + # test change value in rule form with invalid rule + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_RULE_VALUES: json.dumps({"a": "b"}), + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "rules" + assert result["errors"] == {"base": "invalid_det_rules"} + + # test change value in rule form with valid rule + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_RULE_VALUES: json.dumps(["standby"]), + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + # test rule form with new rule + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_STATE_DETECTION_RULES: RULES_NEW_ID, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "rules" + + # test save value for new rule + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_RULE_ID: "rule2", + CONF_RULE_VALUES: json.dumps(VALID_DETECT_RULE), + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + # test rules form with delete existing rule + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_STATE_DETECTION_RULES: "com.plexapp.android", + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "rules" + + # test delete rule + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_RULE_DELETE: True, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_GET_SOURCES: True, + CONF_EXCLUDE_UNNAMED_APPS: True, + CONF_SCREENCAP: True, + CONF_TURN_OFF_COMMAND: "off", + CONF_TURN_ON_COMMAND: "on", + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + apps_options = config_entry.options[CONF_APPS] + assert apps_options.get("app1") is None + assert apps_options["app2"] == "Appl2" + + assert config_entry.options[CONF_GET_SOURCES] is True + assert config_entry.options[CONF_EXCLUDE_UNNAMED_APPS] is True + assert config_entry.options[CONF_SCREENCAP] is True + assert config_entry.options[CONF_TURN_OFF_COMMAND] == "off" + assert config_entry.options[CONF_TURN_ON_COMMAND] == "on" diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index d72cf36438b..f8f08fa2060 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -4,22 +4,25 @@ import copy import logging from unittest.mock import patch -from androidtv.constants import APPS as ANDROIDTV_APPS +from androidtv.constants import APPS as ANDROIDTV_APPS, KEYS from androidtv.exceptions import LockNotAcquiredException import pytest -from homeassistant.components.androidtv.media_player import ( - ANDROIDTV_DOMAIN, - ATTR_COMMAND, - ATTR_DEVICE_PATH, - ATTR_LOCAL_PATH, +from homeassistant.components.androidtv.const import ( CONF_ADB_SERVER_IP, + CONF_ADB_SERVER_PORT, CONF_ADBKEY, CONF_APPS, CONF_EXCLUDE_UNNAMED_APPS, CONF_TURN_OFF_COMMAND, CONF_TURN_ON_COMMAND, - KEYS, + DEFAULT_ADB_SERVER_PORT, + DEFAULT_PORT, + DOMAIN, +) +from homeassistant.components.androidtv.media_player import ( + ATTR_DEVICE_PATH, + ATTR_LOCAL_PATH, SERVICE_ADB_COMMAND, SERVICE_DOWNLOAD, SERVICE_LEARN_SENDEVENT, @@ -29,7 +32,7 @@ from homeassistant.components.media_player import ( ATTR_INPUT_SOURCE, ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, - DOMAIN, + DOMAIN as MP_DOMAIN, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, @@ -46,63 +49,71 @@ from homeassistant.components.media_player import ( ) from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.const import ( + ATTR_COMMAND, ATTR_ENTITY_ID, CONF_DEVICE_CLASS, CONF_HOST, - CONF_NAME, - CONF_PLATFORM, + CONF_PORT, EVENT_HOMEASSISTANT_STOP, STATE_OFF, STATE_PLAYING, STATE_STANDBY, STATE_UNAVAILABLE, ) -from homeassistant.setup import async_setup_component +from homeassistant.util import slugify +from tests.common import MockConfigEntry from tests.components.androidtv import patchers +CONF_OPTIONS = "options" + +PATCH_ACCESS = patch("homeassistant.components.androidtv.os.access", return_value=True) +PATCH_ISFILE = patch( + "homeassistant.components.androidtv.os.path.isfile", patchers.isfile +) + SHELL_RESPONSE_OFF = "" SHELL_RESPONSE_STANDBY = "1" # Android TV device with Python ADB implementation CONFIG_ANDROIDTV_PYTHON_ADB = { DOMAIN: { - CONF_PLATFORM: ANDROIDTV_DOMAIN, CONF_HOST: "127.0.0.1", - CONF_NAME: "Android TV", + CONF_PORT: DEFAULT_PORT, CONF_DEVICE_CLASS: "androidtv", + CONF_ADB_SERVER_PORT: DEFAULT_ADB_SERVER_PORT, } } # Android TV device with ADB server CONFIG_ANDROIDTV_ADB_SERVER = { DOMAIN: { - CONF_PLATFORM: ANDROIDTV_DOMAIN, CONF_HOST: "127.0.0.1", - CONF_NAME: "Android TV", + CONF_PORT: DEFAULT_PORT, CONF_DEVICE_CLASS: "androidtv", CONF_ADB_SERVER_IP: "127.0.0.1", + CONF_ADB_SERVER_PORT: DEFAULT_ADB_SERVER_PORT, } } # Fire TV device with Python ADB implementation CONFIG_FIRETV_PYTHON_ADB = { DOMAIN: { - CONF_PLATFORM: ANDROIDTV_DOMAIN, CONF_HOST: "127.0.0.1", - CONF_NAME: "Fire TV", + CONF_PORT: DEFAULT_PORT, CONF_DEVICE_CLASS: "firetv", + CONF_ADB_SERVER_PORT: DEFAULT_ADB_SERVER_PORT, } } # Fire TV device with ADB server CONFIG_FIRETV_ADB_SERVER = { DOMAIN: { - CONF_PLATFORM: ANDROIDTV_DOMAIN, CONF_HOST: "127.0.0.1", - CONF_NAME: "Fire TV", + CONF_PORT: DEFAULT_PORT, CONF_DEVICE_CLASS: "firetv", CONF_ADB_SERVER_IP: "127.0.0.1", + CONF_ADB_SERVER_PORT: DEFAULT_ADB_SERVER_PORT, } } @@ -114,12 +125,42 @@ def _setup(config): else: patch_key = "server" + host = config[DOMAIN][CONF_HOST] if config[DOMAIN].get(CONF_DEVICE_CLASS) != "firetv": - entity_id = "media_player.android_tv" + entity_id = slugify(f"Android TV {host}") else: - entity_id = "media_player.fire_tv" + entity_id = slugify(f"Fire TV {host}") + entity_id = f"{MP_DOMAIN}.{entity_id}" - return patch_key, entity_id + config_entry = MockConfigEntry( + domain=DOMAIN, + data=config[DOMAIN], + unique_id="a1:b1:c1:d1:e1:f1", + options=config[DOMAIN].get(CONF_OPTIONS), + ) + + return patch_key, entity_id, config_entry + + +async def test_setup_with_properties(hass): + """Test that setup succeeds with device properties. + + the response must be a string with the following info separated with line break: + "manufacturer, model, serialno, version, mac_wlan0_output, mac_eth0_output" + + """ + + patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_ADB_SERVER) + config_entry.add_to_hass(hass) + response = "fake\nfake\n0123456\nfake\nether a1:b1:c1:d1:e1:f1 brd\nnone" + + with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ + patch_key + ], patchers.patch_shell(response)[patch_key]: + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state is not None async def _test_reconnect(hass, caplog, config): @@ -130,14 +171,16 @@ async def _test_reconnect(hass, caplog, config): https://developers.home-assistant.io/docs/en/integration_quality_scale_index.html """ - patch_key, entity_id = _setup(config) + patch_key, entity_id, config_entry = _setup(config) + config_entry.add_to_hass(hass) with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[ patch_key ], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: - assert await async_setup_component(hass, DOMAIN, config) + assert await hass.config_entries.async_setup(config_entry.entry_id) + # assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() await hass.helpers.entity_component.async_update_entity(entity_id) @@ -190,14 +233,15 @@ async def _test_adb_shell_returns_none(hass, config): The state should be `None` and the device should be unavailable. """ - patch_key, entity_id = _setup(config) + patch_key, entity_id, config_entry = _setup(config) + config_entry.add_to_hass(hass) with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[ patch_key ], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: - assert await async_setup_component(hass, DOMAIN, config) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) @@ -299,14 +343,15 @@ async def test_setup_with_adbkey(hass): """Test that setup succeeds when using an ADB key.""" config = copy.deepcopy(CONFIG_ANDROIDTV_PYTHON_ADB) config[DOMAIN][CONF_ADBKEY] = hass.config.path("user_provided_adbkey") - patch_key, entity_id = _setup(config) + patch_key, entity_id, config_entry = _setup(config) + config_entry.add_to_hass(hass) with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[ patch_key - ], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER, patchers.PATCH_ISFILE, patchers.PATCH_ACCESS: - assert await async_setup_component(hass, DOMAIN, config) + ], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER, PATCH_ISFILE, PATCH_ACCESS: + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) @@ -317,17 +362,22 @@ async def test_setup_with_adbkey(hass): async def _test_sources(hass, config0): """Test that sources (i.e., apps) are handled correctly for Android TV and Fire TV devices.""" config = copy.deepcopy(config0) - config[DOMAIN][CONF_APPS] = { - "com.app.test1": "TEST 1", - "com.app.test3": None, - "com.app.test4": SHELL_RESPONSE_OFF, - } - patch_key, entity_id = _setup(config) + config[DOMAIN].setdefault(CONF_OPTIONS, {}).update( + { + CONF_APPS: { + "com.app.test1": "TEST 1", + "com.app.test3": None, + "com.app.test4": SHELL_RESPONSE_OFF, + } + } + ) + patch_key, entity_id, config_entry = _setup(config) + config_entry.add_to_hass(hass) with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - assert await async_setup_component(hass, DOMAIN, config) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) @@ -402,17 +452,22 @@ async def test_firetv_sources(hass): async def _test_exclude_sources(hass, config0, expected_sources): """Test that sources (i.e., apps) are handled correctly when the `exclude_unnamed_apps` config parameter is provided.""" config = copy.deepcopy(config0) - config[DOMAIN][CONF_APPS] = { - "com.app.test1": "TEST 1", - "com.app.test3": None, - "com.app.test4": SHELL_RESPONSE_OFF, - } - patch_key, entity_id = _setup(config) + config[DOMAIN].setdefault(CONF_OPTIONS, {}).update( + { + CONF_APPS: { + "com.app.test1": "TEST 1", + "com.app.test3": None, + "com.app.test4": SHELL_RESPONSE_OFF, + } + } + ) + patch_key, entity_id, config_entry = _setup(config) + config_entry.add_to_hass(hass) with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - assert await async_setup_component(hass, DOMAIN, config) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) @@ -463,31 +518,36 @@ async def _test_exclude_sources(hass, config0, expected_sources): async def test_androidtv_exclude_sources(hass): """Test that sources (i.e., apps) are handled correctly for Android TV devices when the `exclude_unnamed_apps` config parameter is provided as true.""" config = copy.deepcopy(CONFIG_ANDROIDTV_ADB_SERVER) - config[DOMAIN][CONF_EXCLUDE_UNNAMED_APPS] = True + config[DOMAIN][CONF_OPTIONS] = {CONF_EXCLUDE_UNNAMED_APPS: True} assert await _test_exclude_sources(hass, config, ["TEST 1"]) async def test_firetv_exclude_sources(hass): """Test that sources (i.e., apps) are handled correctly for Fire TV devices when the `exclude_unnamed_apps` config parameter is provided as true.""" config = copy.deepcopy(CONFIG_FIRETV_ADB_SERVER) - config[DOMAIN][CONF_EXCLUDE_UNNAMED_APPS] = True + config[DOMAIN][CONF_OPTIONS] = {CONF_EXCLUDE_UNNAMED_APPS: True} assert await _test_exclude_sources(hass, config, ["TEST 1"]) async def _test_select_source(hass, config0, source, expected_arg, method_patch): """Test that the methods for launching and stopping apps are called correctly when selecting a source.""" config = copy.deepcopy(config0) - config[DOMAIN][CONF_APPS] = { - "com.app.test1": "TEST 1", - "com.app.test3": None, - "com.youtube.test": "YouTube", - } - patch_key, entity_id = _setup(config) + config[DOMAIN].setdefault(CONF_OPTIONS, {}).update( + { + CONF_APPS: { + "com.app.test1": "TEST 1", + "com.app.test3": None, + "com.youtube.test": "YouTube", + } + } + ) + patch_key, entity_id, config_entry = _setup(config) + config_entry.add_to_hass(hass) with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - assert await async_setup_component(hass, DOMAIN, config) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) @@ -496,7 +556,7 @@ async def _test_select_source(hass, config0, source, expected_arg, method_patch) with method_patch as method_patch_: await hass.services.async_call( - DOMAIN, + MP_DOMAIN, SERVICE_SELECT_SOURCE, {ATTR_ENTITY_ID: entity_id, ATTR_INPUT_SOURCE: source}, blocking=True, @@ -698,14 +758,15 @@ async def test_firetv_select_source_stop_hidden(hass): async def _test_setup_fail(hass, config): """Test that the entity is not created when the ADB connection is not established.""" - patch_key, entity_id = _setup(config) + patch_key, entity_id, config_entry = _setup(config) + config_entry.add_to_hass(hass) with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(False)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[ patch_key ], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: - assert await async_setup_component(hass, DOMAIN, config) + assert await hass.config_entries.async_setup(config_entry.entry_id) is False await hass.async_block_till_done() await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) @@ -724,177 +785,134 @@ async def test_setup_fail_firetv(hass): assert await _test_setup_fail(hass, CONFIG_FIRETV_PYTHON_ADB) -async def test_setup_two_devices(hass): - """Test that two devices can be set up.""" - config = { - DOMAIN: [ - CONFIG_ANDROIDTV_ADB_SERVER[DOMAIN], - copy.deepcopy(CONFIG_FIRETV_ADB_SERVER[DOMAIN]), - ] - } - config[DOMAIN][1][CONF_HOST] = "127.0.0.2" - - patch_key = "server" - with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ - patch_key - ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - - for entity_id in ["media_player.android_tv", "media_player.fire_tv"]: - await hass.helpers.entity_component.async_update_entity(entity_id) - state = hass.states.get(entity_id) - assert state is not None - assert state.state == STATE_OFF - - -async def test_setup_same_device_twice(hass): - """Test that setup succeeds with a duplicated config entry.""" - patch_key, entity_id = _setup(CONFIG_ANDROIDTV_ADB_SERVER) - - with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ - patch_key - ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER) - await hass.async_block_till_done() - state = hass.states.get(entity_id) - assert state is not None - - assert hass.services.has_service(ANDROIDTV_DOMAIN, SERVICE_ADB_COMMAND) - - with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ - patch_key - ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER) - await hass.async_block_till_done() - - async def test_adb_command(hass): """Test sending a command via the `androidtv.adb_command` service.""" - patch_key, entity_id = _setup(CONFIG_ANDROIDTV_ADB_SERVER) + patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_ADB_SERVER) + config_entry.add_to_hass(hass) command = "test command" response = "test response" with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - with patch( - "androidtv.basetv.basetv_async.BaseTVAsync.adb_shell", return_value=response - ) as patch_shell: - await hass.services.async_call( - ANDROIDTV_DOMAIN, - SERVICE_ADB_COMMAND, - {ATTR_ENTITY_ID: entity_id, ATTR_COMMAND: command}, - blocking=True, - ) + with patch( + "androidtv.basetv.basetv_async.BaseTVAsync.adb_shell", return_value=response + ) as patch_shell: + await hass.services.async_call( + DOMAIN, + SERVICE_ADB_COMMAND, + {ATTR_ENTITY_ID: entity_id, ATTR_COMMAND: command}, + blocking=True, + ) - patch_shell.assert_called_with(command) - state = hass.states.get(entity_id) - assert state is not None - assert state.attributes["adb_response"] == response + patch_shell.assert_called_with(command) + state = hass.states.get(entity_id) + assert state is not None + assert state.attributes["adb_response"] == response async def test_adb_command_unicode_decode_error(hass): """Test sending a command via the `androidtv.adb_command` service that raises a UnicodeDecodeError exception.""" - patch_key, entity_id = _setup(CONFIG_ANDROIDTV_ADB_SERVER) + patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_ADB_SERVER) + config_entry.add_to_hass(hass) command = "test command" response = b"test response" with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - with patch( - "androidtv.basetv.basetv_async.BaseTVAsync.adb_shell", - side_effect=UnicodeDecodeError("utf-8", response, 0, len(response), "TEST"), - ): - await hass.services.async_call( - ANDROIDTV_DOMAIN, - SERVICE_ADB_COMMAND, - {ATTR_ENTITY_ID: entity_id, ATTR_COMMAND: command}, - blocking=True, - ) + with patch( + "androidtv.basetv.basetv_async.BaseTVAsync.adb_shell", + side_effect=UnicodeDecodeError("utf-8", response, 0, len(response), "TEST"), + ): + await hass.services.async_call( + DOMAIN, + SERVICE_ADB_COMMAND, + {ATTR_ENTITY_ID: entity_id, ATTR_COMMAND: command}, + blocking=True, + ) - # patch_shell.assert_called_with(command) - state = hass.states.get(entity_id) - assert state is not None - assert state.attributes["adb_response"] is None + # patch_shell.assert_called_with(command) + state = hass.states.get(entity_id) + assert state is not None + assert state.attributes["adb_response"] is None async def test_adb_command_key(hass): """Test sending a key command via the `androidtv.adb_command` service.""" - patch_key = "server" - entity_id = "media_player.android_tv" + patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_ADB_SERVER) + config_entry.add_to_hass(hass) command = "HOME" response = None with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - with patch( - "androidtv.basetv.basetv_async.BaseTVAsync.adb_shell", return_value=response - ) as patch_shell: - await hass.services.async_call( - ANDROIDTV_DOMAIN, - SERVICE_ADB_COMMAND, - {ATTR_ENTITY_ID: entity_id, ATTR_COMMAND: command}, - blocking=True, - ) + with patch( + "androidtv.basetv.basetv_async.BaseTVAsync.adb_shell", return_value=response + ) as patch_shell: + await hass.services.async_call( + DOMAIN, + SERVICE_ADB_COMMAND, + {ATTR_ENTITY_ID: entity_id, ATTR_COMMAND: command}, + blocking=True, + ) - patch_shell.assert_called_with(f"input keyevent {KEYS[command]}") - state = hass.states.get(entity_id) - assert state is not None - assert state.attributes["adb_response"] is None + patch_shell.assert_called_with(f"input keyevent {KEYS[command]}") + state = hass.states.get(entity_id) + assert state is not None + assert state.attributes["adb_response"] is None async def test_adb_command_get_properties(hass): """Test sending the "GET_PROPERTIES" command via the `androidtv.adb_command` service.""" - patch_key = "server" - entity_id = "media_player.android_tv" + patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_ADB_SERVER) + config_entry.add_to_hass(hass) command = "GET_PROPERTIES" response = {"test key": "test value"} with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - with patch( - "androidtv.androidtv.androidtv_async.AndroidTVAsync.get_properties_dict", - return_value=response, - ) as patch_get_props: - await hass.services.async_call( - ANDROIDTV_DOMAIN, - SERVICE_ADB_COMMAND, - {ATTR_ENTITY_ID: entity_id, ATTR_COMMAND: command}, - blocking=True, - ) + with patch( + "androidtv.androidtv.androidtv_async.AndroidTVAsync.get_properties_dict", + return_value=response, + ) as patch_get_props: + await hass.services.async_call( + DOMAIN, + SERVICE_ADB_COMMAND, + {ATTR_ENTITY_ID: entity_id, ATTR_COMMAND: command}, + blocking=True, + ) - patch_get_props.assert_called() - state = hass.states.get(entity_id) - assert state is not None - assert state.attributes["adb_response"] == str(response) + patch_get_props.assert_called() + state = hass.states.get(entity_id) + assert state is not None + assert state.attributes["adb_response"] == str(response) async def test_learn_sendevent(hass): """Test the `androidtv.learn_sendevent` service.""" - patch_key = "server" - entity_id = "media_player.android_tv" + patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_ADB_SERVER) + config_entry.add_to_hass(hass) response = "sendevent 1 2 3 4" with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() with patch( @@ -902,7 +920,7 @@ async def test_learn_sendevent(hass): return_value=response, ) as patch_learn_sendevent: await hass.services.async_call( - ANDROIDTV_DOMAIN, + DOMAIN, SERVICE_LEARN_SENDEVENT, {ATTR_ENTITY_ID: entity_id}, blocking=True, @@ -916,12 +934,13 @@ async def test_learn_sendevent(hass): async def test_update_lock_not_acquired(hass): """Test that the state does not get updated when a `LockNotAcquiredException` is raised.""" - patch_key, entity_id = _setup(CONFIG_ANDROIDTV_ADB_SERVER) + patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_ADB_SERVER) + config_entry.add_to_hass(hass) with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: @@ -948,105 +967,112 @@ async def test_update_lock_not_acquired(hass): async def test_download(hass): """Test the `androidtv.download` service.""" - patch_key, entity_id = _setup(CONFIG_ANDROIDTV_ADB_SERVER) + patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_ADB_SERVER) + config_entry.add_to_hass(hass) device_path = "device/path" local_path = "local/path" with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - # Failed download because path is not whitelisted - with patch("androidtv.basetv.basetv_async.BaseTVAsync.adb_pull") as patch_pull: - await hass.services.async_call( - ANDROIDTV_DOMAIN, - SERVICE_DOWNLOAD, - { - ATTR_ENTITY_ID: entity_id, - ATTR_DEVICE_PATH: device_path, - ATTR_LOCAL_PATH: local_path, - }, - blocking=True, - ) - patch_pull.assert_not_called() + # Failed download because path is not whitelisted + with patch("androidtv.basetv.basetv_async.BaseTVAsync.adb_pull") as patch_pull: + await hass.services.async_call( + DOMAIN, + SERVICE_DOWNLOAD, + { + ATTR_ENTITY_ID: entity_id, + ATTR_DEVICE_PATH: device_path, + ATTR_LOCAL_PATH: local_path, + }, + blocking=True, + ) + patch_pull.assert_not_called() - # Successful download - with patch( - "androidtv.basetv.basetv_async.BaseTVAsync.adb_pull" - ) as patch_pull, patch.object(hass.config, "is_allowed_path", return_value=True): - await hass.services.async_call( - ANDROIDTV_DOMAIN, - SERVICE_DOWNLOAD, - { - ATTR_ENTITY_ID: entity_id, - ATTR_DEVICE_PATH: device_path, - ATTR_LOCAL_PATH: local_path, - }, - blocking=True, - ) - patch_pull.assert_called_with(local_path, device_path) + # Successful download + with patch( + "androidtv.basetv.basetv_async.BaseTVAsync.adb_pull" + ) as patch_pull, patch.object( + hass.config, "is_allowed_path", return_value=True + ): + await hass.services.async_call( + DOMAIN, + SERVICE_DOWNLOAD, + { + ATTR_ENTITY_ID: entity_id, + ATTR_DEVICE_PATH: device_path, + ATTR_LOCAL_PATH: local_path, + }, + blocking=True, + ) + patch_pull.assert_called_with(local_path, device_path) async def test_upload(hass): """Test the `androidtv.upload` service.""" - patch_key, entity_id = _setup(CONFIG_ANDROIDTV_ADB_SERVER) + patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_ADB_SERVER) + config_entry.add_to_hass(hass) device_path = "device/path" local_path = "local/path" with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - # Failed upload because path is not whitelisted - with patch("androidtv.basetv.basetv_async.BaseTVAsync.adb_push") as patch_push: - await hass.services.async_call( - ANDROIDTV_DOMAIN, - SERVICE_UPLOAD, - { - ATTR_ENTITY_ID: entity_id, - ATTR_DEVICE_PATH: device_path, - ATTR_LOCAL_PATH: local_path, - }, - blocking=True, - ) - patch_push.assert_not_called() + # Failed upload because path is not whitelisted + with patch("androidtv.basetv.basetv_async.BaseTVAsync.adb_push") as patch_push: + await hass.services.async_call( + DOMAIN, + SERVICE_UPLOAD, + { + ATTR_ENTITY_ID: entity_id, + ATTR_DEVICE_PATH: device_path, + ATTR_LOCAL_PATH: local_path, + }, + blocking=True, + ) + patch_push.assert_not_called() - # Successful upload - with patch( - "androidtv.basetv.basetv_async.BaseTVAsync.adb_push" - ) as patch_push, patch.object(hass.config, "is_allowed_path", return_value=True): - await hass.services.async_call( - ANDROIDTV_DOMAIN, - SERVICE_UPLOAD, - { - ATTR_ENTITY_ID: entity_id, - ATTR_DEVICE_PATH: device_path, - ATTR_LOCAL_PATH: local_path, - }, - blocking=True, - ) - patch_push.assert_called_with(local_path, device_path) + # Successful upload + with patch( + "androidtv.basetv.basetv_async.BaseTVAsync.adb_push" + ) as patch_push, patch.object( + hass.config, "is_allowed_path", return_value=True + ): + await hass.services.async_call( + DOMAIN, + SERVICE_UPLOAD, + { + ATTR_ENTITY_ID: entity_id, + ATTR_DEVICE_PATH: device_path, + ATTR_LOCAL_PATH: local_path, + }, + blocking=True, + ) + patch_push.assert_called_with(local_path, device_path) async def test_androidtv_volume_set(hass): """Test setting the volume for an Android TV device.""" - patch_key, entity_id = _setup(CONFIG_ANDROIDTV_ADB_SERVER) + patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_ADB_SERVER) + config_entry.add_to_hass(hass) with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() with patch( "androidtv.basetv.basetv_async.BaseTVAsync.set_volume_level", return_value=0.5 ) as patch_set_volume_level: await hass.services.async_call( - DOMAIN, + MP_DOMAIN, SERVICE_VOLUME_SET, {ATTR_ENTITY_ID: entity_id, ATTR_MEDIA_VOLUME_LEVEL: 0.5}, blocking=True, @@ -1060,12 +1086,13 @@ async def test_get_image(hass, hass_ws_client): This is based on `test_get_image` in tests/components/media_player/test_init.py. """ - patch_key, entity_id = _setup(CONFIG_ANDROIDTV_ADB_SERVER) + patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_ADB_SERVER) + config_entry.add_to_hass(hass) with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() with patchers.patch_shell("11")[patch_key]: @@ -1126,7 +1153,7 @@ async def _test_service( f"androidtv.{androidtv_patch}.{androidtv_method}", return_value=return_value ) as service_call: await hass.services.async_call( - DOMAIN, + MP_DOMAIN, ha_service_name, service_data=service_data, blocking=True, @@ -1136,13 +1163,12 @@ async def _test_service( async def test_services_androidtv(hass): """Test media player services for an Android TV device.""" - patch_key, entity_id = _setup(CONFIG_ANDROIDTV_ADB_SERVER) + patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_ADB_SERVER) + config_entry.add_to_hass(hass) with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[patch_key]: with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - assert await async_setup_component( - hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER - ) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key]: @@ -1185,14 +1211,17 @@ async def test_services_androidtv(hass): async def test_services_firetv(hass): """Test media player services for a Fire TV device.""" - patch_key, entity_id = _setup(CONFIG_FIRETV_ADB_SERVER) config = copy.deepcopy(CONFIG_FIRETV_ADB_SERVER) - config[DOMAIN][CONF_TURN_OFF_COMMAND] = "test off" - config[DOMAIN][CONF_TURN_ON_COMMAND] = "test on" + config[DOMAIN][CONF_OPTIONS] = { + CONF_TURN_OFF_COMMAND: "test off", + CONF_TURN_ON_COMMAND: "test on", + } + patch_key, entity_id, config_entry = _setup(config) + config_entry.add_to_hass(hass) with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[patch_key]: with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - assert await async_setup_component(hass, DOMAIN, config) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key]: @@ -1203,12 +1232,13 @@ async def test_services_firetv(hass): async def test_connection_closed_on_ha_stop(hass): """Test that the ADB socket connection is closed when HA stops.""" - patch_key, entity_id = _setup(CONFIG_ANDROIDTV_ADB_SERVER) + patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_ADB_SERVER) + config_entry.add_to_hass(hass) with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() with patch( @@ -1224,14 +1254,15 @@ async def test_exception(hass): HA will attempt to reconnect on the next update. """ - patch_key, entity_id = _setup(CONFIG_ANDROIDTV_PYTHON_ADB) + patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_PYTHON_ADB) + config_entry.add_to_hass(hass) with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell(SHELL_RESPONSE_OFF)[ patch_key ], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: - assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_PYTHON_ADB) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() await hass.helpers.entity_component.async_update_entity(entity_id) From a7a0cfd9e672637d6957f52b4898302c35834090 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 20 Dec 2021 20:18:11 +0100 Subject: [PATCH 0840/2644] Make it possible to turn on audio only google cast devices (#62420) --- homeassistant/components/cast/media_player.py | 13 ++++++++----- tests/components/cast/test_media_player.py | 2 ++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index b687f96948b..8160c1f5bf0 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -395,11 +395,14 @@ class CastDevice(MediaPlayerEntity): return if self._chromecast.app_id is not None: - # Quit the previous app before starting splash screen + # Quit the previous app before starting splash screen or media player self._chromecast.quit_app() # The only way we can turn the Chromecast is on is by launching an app - self._chromecast.play_media(CAST_SPLASH, pychromecast.STREAM_TYPE_BUFFERED) + if self._chromecast.cast_type == pychromecast.const.CAST_TYPE_CHROMECAST: + self._chromecast.play_media(CAST_SPLASH, pychromecast.STREAM_TYPE_BUFFERED) + else: + self._chromecast.start_app(pychromecast.config.APP_MEDIA_RECEIVER) def turn_off(self): """Turn off the cast device.""" @@ -674,9 +677,9 @@ class CastDevice(MediaPlayerEntity): support = SUPPORT_CAST media_status = self._media_status()[0] - if ( - self._chromecast - and self._chromecast.cast_type == pychromecast.const.CAST_TYPE_CHROMECAST + if self._chromecast and self._chromecast.cast_type in ( + pychromecast.const.CAST_TYPE_CHROMECAST, + pychromecast.const.CAST_TYPE_AUDIO, ): support |= SUPPORT_TURN_ON diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index dae9981ae67..3c5d4705713 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -683,10 +683,12 @@ async def test_entity_cast_status(hass: HomeAssistant): | SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_TURN_OFF + | SUPPORT_TURN_ON | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET, SUPPORT_PLAY_MEDIA | SUPPORT_TURN_OFF + | SUPPORT_TURN_ON | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET, ), From 6a5192b1705e72175dffb1a13557f7c40280c883 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Dec 2021 20:55:37 +0100 Subject: [PATCH 0841/2644] Use new enums in watttime (#62430) Co-authored-by: epenet --- homeassistant/components/watttime/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/watttime/sensor.py b/homeassistant/components/watttime/sensor.py index 0b1ae54b5d1..41842c6ed70 100644 --- a/homeassistant/components/watttime/sensor.py +++ b/homeassistant/components/watttime/sensor.py @@ -5,9 +5,9 @@ from collections.abc import Mapping from typing import Any, cast from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, MASS_POUNDS, PERCENTAGE @@ -38,14 +38,14 @@ REALTIME_EMISSIONS_SENSOR_DESCRIPTIONS = ( name="Marginal Operating Emissions Rate", icon="mdi:blur", native_unit_of_measurement=f"{MASS_POUNDS} CO2/MWh", - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_TYPE_REALTIME_EMISSIONS_PERCENT, name="Relative Marginal Emissions Intensity", icon="mdi:blur", native_unit_of_measurement=PERCENTAGE, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, ), ) From 2f8e44641b104ee523935c5be84de52fa5ba1bfb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Dec 2021 20:56:33 +0100 Subject: [PATCH 0842/2644] Use new enums in verisure (#62433) Co-authored-by: epenet --- homeassistant/components/verisure/binary_sensor.py | 5 ++--- homeassistant/components/verisure/sensor.py | 6 +++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/verisure/binary_sensor.py b/homeassistant/components/verisure/binary_sensor.py index ba276aa3675..05e0d77845a 100644 --- a/homeassistant/components/verisure/binary_sensor.py +++ b/homeassistant/components/verisure/binary_sensor.py @@ -6,9 +6,8 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import DeviceInfo, Entity, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -87,7 +86,7 @@ class VerisureEthernetStatus(CoordinatorEntity, BinarySensorEntity): _attr_name = "Verisure Ethernet status" _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY - _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_entity_category = EntityCategory.DIAGNOSTIC @property def unique_id(self) -> str: diff --git a/homeassistant/components/verisure/sensor.py b/homeassistant/components/verisure/sensor.py index ea65b1fbcd8..9e19e5d865f 100644 --- a/homeassistant/components/verisure/sensor.py +++ b/homeassistant/components/verisure/sensor.py @@ -2,9 +2,9 @@ from __future__ import annotations from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, SensorDeviceClass, SensorEntity, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, TEMP_CELSIUS @@ -52,7 +52,7 @@ class VerisureThermometer(CoordinatorEntity, SensorEntity): _attr_device_class = SensorDeviceClass.TEMPERATURE _attr_native_unit_of_measurement = TEMP_CELSIUS - _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_state_class = SensorStateClass.MEASUREMENT def __init__( self, coordinator: VerisureDataUpdateCoordinator, serial_number: str @@ -107,7 +107,7 @@ class VerisureHygrometer(CoordinatorEntity, SensorEntity): _attr_device_class = SensorDeviceClass.HUMIDITY _attr_native_unit_of_measurement = PERCENTAGE - _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_state_class = SensorStateClass.MEASUREMENT def __init__( self, coordinator: VerisureDataUpdateCoordinator, serial_number: str From 666e14b11d2172ca04976476256da635eca64296 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Dec 2021 20:57:05 +0100 Subject: [PATCH 0843/2644] Use new enums in wled (#62431) Co-authored-by: epenet --- homeassistant/components/wled/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py index 05d92a91e3d..b37238aef64 100644 --- a/homeassistant/components/wled/sensor.py +++ b/homeassistant/components/wled/sensor.py @@ -8,10 +8,10 @@ from datetime import datetime, timedelta from wled import Device as WLEDDevice from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -51,7 +51,7 @@ SENSORS: tuple[WLEDSensorEntityDescription, ...] = ( name="Estimated Current", native_unit_of_measurement=ELECTRIC_CURRENT_MILLIAMPERE, device_class=SensorDeviceClass.CURRENT, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.info.leds.power, ), @@ -82,7 +82,7 @@ SENSORS: tuple[WLEDSensorEntityDescription, ...] = ( name="Free Memory", icon="mdi:memory", native_unit_of_measurement=DATA_BYTES, - state_class=STATE_CLASS_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda device: device.info.free_heap, From 418221bd2127c0f16c2155f75d3ae1effd5d8702 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Dec 2021 20:58:17 +0100 Subject: [PATCH 0844/2644] Use new enums in tesla_wall_connector (#62434) Co-authored-by: epenet --- .../tesla_wall_connector/binary_sensor.py | 13 ++--- .../components/tesla_wall_connector/sensor.py | 55 +++++++++---------- 2 files changed, 32 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/tesla_wall_connector/binary_sensor.py b/homeassistant/components/tesla_wall_connector/binary_sensor.py index 8aef4f86478..c1115065eec 100644 --- a/homeassistant/components/tesla_wall_connector/binary_sensor.py +++ b/homeassistant/components/tesla_wall_connector/binary_sensor.py @@ -3,12 +3,11 @@ from dataclasses import dataclass import logging from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_BATTERY_CHARGING, - DEVICE_CLASS_PLUG, + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC +from homeassistant.helpers.entity import EntityCategory from . import ( WallConnectorData, @@ -32,16 +31,16 @@ WALL_CONNECTOR_SENSORS = [ WallConnectorBinarySensorDescription( key="vehicle_connected", name=prefix_entity_name("Vehicle connected"), - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda data: data[WALLCONNECTOR_DATA_VITALS].vehicle_connected, - device_class=DEVICE_CLASS_PLUG, + device_class=BinarySensorDeviceClass.PLUG, ), WallConnectorBinarySensorDescription( key="contactor_closed", name=prefix_entity_name("Contactor closed"), - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda data: data[WALLCONNECTOR_DATA_VITALS].contactor_closed, - device_class=DEVICE_CLASS_BATTERY_CHARGING, + device_class=BinarySensorDeviceClass.BATTERY_CHARGING, ), ] diff --git a/homeassistant/components/tesla_wall_connector/sensor.py b/homeassistant/components/tesla_wall_connector/sensor.py index 8219d121ae3..f11b9c107ba 100644 --- a/homeassistant/components/tesla_wall_connector/sensor.py +++ b/homeassistant/components/tesla_wall_connector/sensor.py @@ -3,22 +3,19 @@ from dataclasses import dataclass import logging from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.const import ( - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_VOLTAGE, ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_WATT_HOUR, - ENTITY_CATEGORY_DIAGNOSTIC, FREQUENCY_HERTZ, TEMP_CELSIUS, ) +from homeassistant.helpers.entity import EntityCategory from . import ( WallConnectorData, @@ -42,7 +39,7 @@ WALL_CONNECTOR_SENSORS = [ WallConnectorSensorDescription( key="evse_state", name=prefix_entity_name("State"), - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda data: data[WALLCONNECTOR_DATA_VITALS].evse_state, ), WallConnectorSensorDescription( @@ -50,82 +47,82 @@ WALL_CONNECTOR_SENSORS = [ name=prefix_entity_name("Handle Temperature"), native_unit_of_measurement=TEMP_CELSIUS, value_fn=lambda data: round(data[WALLCONNECTOR_DATA_VITALS].handle_temp_c, 1), - device_class=DEVICE_CLASS_TEMPERATURE, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + entity_category=EntityCategory.DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, ), WallConnectorSensorDescription( key="grid_v", name=prefix_entity_name("Grid Voltage"), native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, value_fn=lambda data: round(data[WALLCONNECTOR_DATA_VITALS].grid_v, 1), - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), WallConnectorSensorDescription( key="grid_hz", name=prefix_entity_name("Grid Frequency"), native_unit_of_measurement=FREQUENCY_HERTZ, value_fn=lambda data: round(data[WALLCONNECTOR_DATA_VITALS].grid_hz, 3), - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), WallConnectorSensorDescription( key="current_a_a", name=prefix_entity_name("Phase A Current"), native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, value_fn=lambda data: data[WALLCONNECTOR_DATA_VITALS].currentA_a, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), WallConnectorSensorDescription( key="current_b_a", name=prefix_entity_name("Phase B Current"), native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, value_fn=lambda data: data[WALLCONNECTOR_DATA_VITALS].currentB_a, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), WallConnectorSensorDescription( key="current_c_a", name=prefix_entity_name("Phase C Current"), native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, value_fn=lambda data: data[WALLCONNECTOR_DATA_VITALS].currentC_a, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), WallConnectorSensorDescription( key="voltage_a_v", name=prefix_entity_name("Phase A Voltage"), native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, value_fn=lambda data: data[WALLCONNECTOR_DATA_VITALS].voltageA_v, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), WallConnectorSensorDescription( key="voltage_b_v", name=prefix_entity_name("Phase B Voltage"), native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, value_fn=lambda data: data[WALLCONNECTOR_DATA_VITALS].voltageB_v, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), WallConnectorSensorDescription( key="voltage_c_v", name=prefix_entity_name("Phase C Voltage"), native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, value_fn=lambda data: data[WALLCONNECTOR_DATA_VITALS].voltageC_v, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), WallConnectorSensorDescription( key="energy_kWh", name=prefix_entity_name("Energy"), native_unit_of_measurement=ENERGY_WATT_HOUR, value_fn=lambda data: data[WALLCONNECTOR_DATA_LIFETIME].energy_wh, - state_class=STATE_CLASS_TOTAL_INCREASING, - device_class=DEVICE_CLASS_ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, ), ] From 314dce914fdedc2889adccdb39cc86db54d847f8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Dec 2021 20:58:54 +0100 Subject: [PATCH 0845/2644] Use new enums in temper (#62428) Co-authored-by: epenet --- homeassistant/components/temper/sensor.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/temper/sensor.py b/homeassistant/components/temper/sensor.py index 0274043c089..2164ff974c6 100644 --- a/homeassistant/components/temper/sensor.py +++ b/homeassistant/components/temper/sensor.py @@ -4,11 +4,14 @@ import logging from temperusb.temper import TemperHandler import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorDeviceClass, + SensorEntity, +) from homeassistant.const import ( CONF_NAME, CONF_OFFSET, - DEVICE_CLASS_TEMPERATURE, DEVICE_DEFAULT_NAME, TEMP_CELSIUS, ) @@ -60,7 +63,7 @@ def reset_devices(): class TemperSensor(SensorEntity): """Representation of a Temper temperature sensor.""" - _attr_device_class = DEVICE_CLASS_TEMPERATURE + _attr_device_class = SensorDeviceClass.TEMPERATURE _attr_native_unit_of_measurement = TEMP_CELSIUS def __init__(self, temper_device, name, scaling): From 69df7bc43d76adda624ab5b5557b57b319128309 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Dec 2021 20:59:50 +0100 Subject: [PATCH 0846/2644] Use new enums in ted5000 (#62425) Co-authored-by: epenet --- homeassistant/components/ted5000/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ted5000/sensor.py b/homeassistant/components/ted5000/sensor.py index a7162ee9c63..c6aedbb5046 100644 --- a/homeassistant/components/ted5000/sensor.py +++ b/homeassistant/components/ted5000/sensor.py @@ -9,8 +9,8 @@ import xmltodict from homeassistant.components.sensor import ( PLATFORM_SCHEMA, - STATE_CLASS_MEASUREMENT, SensorEntity, + SensorStateClass, ) from homeassistant.const import ( CONF_HOST, @@ -62,7 +62,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class Ted5000Sensor(SensorEntity): """Implementation of a Ted5000 sensor.""" - _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_state_class = SensorStateClass.MEASUREMENT def __init__(self, gateway, name, mtu, unit): """Initialize the sensor.""" From 3724a4fb2fe775d15d7a54ad46d0af9b037d8120 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Dec 2021 21:00:53 +0100 Subject: [PATCH 0847/2644] Use new enums in tasmota (#62427) Co-authored-by: epenet --- homeassistant/components/tasmota/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index 961a89cfb31..ece45b9dfd7 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -22,7 +22,6 @@ from homeassistant.const import ( ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, - ENTITY_CATEGORY_DIAGNOSTIC, FREQUENCY_HERTZ, LENGTH_CENTIMETERS, LIGHT_LUX, @@ -42,6 +41,7 @@ from homeassistant.const import ( ) 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 .const import DATA_REMOVE_DISCOVER_COMPONENT @@ -277,7 +277,7 @@ class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, SensorEntity): def entity_category(self) -> str | None: """Return the category of the entity, if any.""" if self._tasmota_entity.quantity in status_sensor.SENSORS: - return ENTITY_CATEGORY_DIAGNOSTIC + return EntityCategory.DIAGNOSTIC return None @property From 9eb1a44c0388c3ef9babbf2f2cb69422d99c38b5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Dec 2021 21:04:24 +0100 Subject: [PATCH 0848/2644] Use new enums in tellduslive (#62426) Co-authored-by: epenet --- homeassistant/components/tellduslive/sensor.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/tellduslive/sensor.py b/homeassistant/components/tellduslive/sensor.py index 729b6052507..8c87ce16341 100644 --- a/homeassistant/components/tellduslive/sensor.py +++ b/homeassistant/components/tellduslive/sensor.py @@ -2,11 +2,12 @@ from __future__ import annotations from homeassistant.components import sensor, tellduslive -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) from homeassistant.const import ( - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_TEMPERATURE, LENGTH_MILLIMETERS, LIGHT_LUX, PERCENTAGE, @@ -38,13 +39,13 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { key=SENSOR_TYPE_TEMPERATURE, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), SENSOR_TYPE_HUMIDITY: SensorEntityDescription( key=SENSOR_TYPE_HUMIDITY, name="Humidity", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, ), SENSOR_TYPE_RAINRATE: SensorEntityDescription( key=SENSOR_TYPE_RAINRATE, @@ -86,13 +87,13 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { key=SENSOR_TYPE_LUMINANCE, name="Luminance", native_unit_of_measurement=LIGHT_LUX, - device_class=DEVICE_CLASS_ILLUMINANCE, + device_class=SensorDeviceClass.ILLUMINANCE, ), SENSOR_TYPE_DEW_POINT: SensorEntityDescription( key=SENSOR_TYPE_DEW_POINT, name="Dew Point", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, ), SENSOR_TYPE_BAROMETRIC_PRESSURE: SensorEntityDescription( key=SENSOR_TYPE_BAROMETRIC_PRESSURE, From b051704c4b736995d77c4867a13b84c0a4874d05 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 20 Dec 2021 13:11:26 -0700 Subject: [PATCH 0849/2644] Add reauth flow to Tile (#62415) --- homeassistant/components/tile/__init__.py | 9 +- homeassistant/components/tile/config_flow.py | 91 +++++++++++++++---- homeassistant/components/tile/strings.json | 9 +- .../components/tile/translations/en.json | 9 +- tests/components/tile/test_config_flow.py | 55 +++++++++-- 5 files changed, 141 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/tile/__init__.py b/homeassistant/components/tile/__init__.py index 388ffbff7fd..0787e00a489 100644 --- a/homeassistant/components/tile/__init__.py +++ b/homeassistant/components/tile/__init__.py @@ -11,7 +11,7 @@ from pytile.tile import Tile from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -68,9 +68,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: session=websession, ) tiles = await client.async_get_tiles() - except InvalidAuthError: - LOGGER.error("Invalid credentials provided") - return False + except InvalidAuthError as err: + raise ConfigEntryAuthFailed("Invalid credentials") from err except TileError as err: raise ConfigEntryNotReady("Error during integration setup") from err @@ -78,6 +77,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Update the Tile.""" try: await tile.async_update() + except InvalidAuthError as err: + raise ConfigEntryAuthFailed("Invalid credentials") from err except SessionExpiredError: LOGGER.info("Tile session expired; creating a new one") await client.async_init() diff --git a/homeassistant/components/tile/config_flow.py b/homeassistant/components/tile/config_flow.py index 3c78e5d2bca..58bb929e446 100644 --- a/homeassistant/components/tile/config_flow.py +++ b/homeassistant/components/tile/config_flow.py @@ -4,15 +4,29 @@ from __future__ import annotations from typing import Any from pytile import async_login -from pytile.errors import TileError +from pytile.errors import InvalidAuthError, TileError import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN +from .const import DOMAIN, LOGGER + +STEP_REAUTH_SCHEMA = vol.Schema( + { + vol.Required(CONF_PASSWORD): str, + } +) + +STEP_USER_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } +) class TileFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -22,37 +36,74 @@ class TileFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self.data_schema = vol.Schema( - {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} - ) + self._password: str | None = None + self._username: str | None = None - async def _show_form(self, errors: dict[str, Any] | None = None) -> FlowResult: - """Show the form to the user.""" - return self.async_show_form( - step_id="user", data_schema=self.data_schema, errors=errors or {} - ) + async def _async_verify(self, step_id: str, schema: vol.Schema) -> FlowResult: + """Attempt to authenticate the provided credentials.""" + assert self._username + assert self._password + + errors = {} + session = aiohttp_client.async_get_clientsession(self.hass) + + try: + await async_login(self._username, self._password, session=session) + except InvalidAuthError: + errors["base"] = "invalid_auth" + except TileError as err: + LOGGER.error("Unknown Tile error: %s", err) + errors["base"] = "unknown" + + if errors: + return self.async_show_form( + step_id=step_id, data_schema=schema, errors=errors + ) + + data = {CONF_USERNAME: self._username, CONF_PASSWORD: self._password} + + if existing_entry := await self.async_set_unique_id(self._username): + self.hass.config_entries.async_update_entry(existing_entry, data=data) + self.hass.async_create_task( + self.hass.config_entries.async_reload(existing_entry.entry_id) + ) + return self.async_abort(reason="reauth_successful") + + return self.async_create_entry(title=self._username, data=data) async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: """Import a config entry from configuration.yaml.""" return await self.async_step_user(import_config) + async def async_step_reauth(self, config: ConfigType) -> FlowResult: + """Handle configuration by re-auth.""" + self._username = config[CONF_USERNAME] + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Handle re-auth completion.""" + if not user_input: + return self.async_show_form( + step_id="reauth_confirm", data_schema=STEP_REAUTH_SCHEMA + ) + + self._password = user_input[CONF_PASSWORD] + + return await self._async_verify("reauth_confirm", STEP_REAUTH_SCHEMA) + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle the start of the config flow.""" if not user_input: - return await self._show_form() + return self.async_show_form(step_id="user", data_schema=STEP_USER_SCHEMA) await self.async_set_unique_id(user_input[CONF_USERNAME]) self._abort_if_unique_id_configured() - session = aiohttp_client.async_get_clientsession(self.hass) + self._username = user_input[CONF_USERNAME] + self._password = user_input[CONF_PASSWORD] - try: - await async_login( - user_input[CONF_USERNAME], user_input[CONF_PASSWORD], session=session - ) - except TileError: - return await self._show_form({"base": "invalid_auth"}) - - return self.async_create_entry(title=user_input[CONF_USERNAME], data=user_input) + return await self._async_verify("user", STEP_USER_SCHEMA) diff --git a/homeassistant/components/tile/strings.json b/homeassistant/components/tile/strings.json index cdddbe54f96..da53f79b697 100644 --- a/homeassistant/components/tile/strings.json +++ b/homeassistant/components/tile/strings.json @@ -1,6 +1,12 @@ { "config": { "step": { + "reauth_confirm": { + "title": "Re-authenticate Tile", + "data": { + "password": "[%key:common::config_flow::data::password%]" + } + }, "user": { "title": "Configure Tile", "data": { @@ -13,7 +19,8 @@ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, "options": { diff --git a/homeassistant/components/tile/translations/en.json b/homeassistant/components/tile/translations/en.json index c052cad6d70..998f3a53392 100644 --- a/homeassistant/components/tile/translations/en.json +++ b/homeassistant/components/tile/translations/en.json @@ -1,12 +1,19 @@ { "config": { "abort": { - "already_configured": "Account is already configured" + "already_configured": "Account is already configured", + "reauth_successful": "Re-authentication was successful" }, "error": { "invalid_auth": "Invalid authentication" }, "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "title": "Re-authenticate Tile" + }, "user": { "data": { "password": "Password", diff --git a/tests/components/tile/test_config_flow.py b/tests/components/tile/test_config_flow.py index e5561133a35..ae86bc4dc7c 100644 --- a/tests/components/tile/test_config_flow.py +++ b/tests/components/tile/test_config_flow.py @@ -1,11 +1,12 @@ """Define tests for the Tile config flow.""" from unittest.mock import patch -from pytile.errors import TileError +import pytest +from pytile.errors import InvalidAuthError, TileError from homeassistant import data_entry_flow from homeassistant.components.tile import DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from tests.common import MockConfigEntry @@ -30,8 +31,15 @@ async def test_duplicate_error(hass): assert result["reason"] == "already_configured" -async def test_invalid_credentials(hass): - """Test that invalid credentials key throws an error.""" +@pytest.mark.parametrize( + "err,err_string", + [ + (InvalidAuthError, "invalid_auth"), + (TileError, "unknown"), + ], +) +async def test_errors(hass, err, err_string): + """Test that errors are handled correctly.""" conf = { CONF_USERNAME: "user@host.com", CONF_PASSWORD: "123abc", @@ -39,13 +47,13 @@ async def test_invalid_credentials(hass): with patch( "homeassistant.components.tile.config_flow.async_login", - side_effect=TileError, + side_effect=err, ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=conf ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "invalid_auth"} + assert result["errors"] == {"base": err_string} async def test_step_import(hass): @@ -69,6 +77,41 @@ async def test_step_import(hass): } +async def test_step_reauth(hass): + """Test that the reauth step works.""" + conf = { + CONF_USERNAME: "user@host.com", + CONF_PASSWORD: "123abc", + } + + MockConfigEntry(domain=DOMAIN, unique_id="user@host.com", data=conf).add_to_hass( + hass + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH}, + data={CONF_USERNAME: "user@host.com", CONF_PASSWORD: "password"}, + ) + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" + + with patch( + "homeassistant.components.tile.async_setup_entry", return_value=True + ), patch("homeassistant.components.tile.config_flow.async_login"): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_PASSWORD: "password"} + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "reauth_successful" + assert len(hass.config_entries.async_entries()) == 1 + + async def test_step_user(hass): """Test that the user step works.""" conf = { From 369041e0d258b5f8677388bbc7f3abacf71310c3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Dec 2021 21:12:20 +0100 Subject: [PATCH 0850/2644] Use new enums in tahoma (#62424) Co-authored-by: epenet --- .../components/tahoma/binary_sensor.py | 4 +- homeassistant/components/tahoma/cover.py | 49 ++++++++----------- 2 files changed, 22 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/tahoma/binary_sensor.py b/homeassistant/components/tahoma/binary_sensor.py index 55be39377bf..01e2976bda6 100644 --- a/homeassistant/components/tahoma/binary_sensor.py +++ b/homeassistant/components/tahoma/binary_sensor.py @@ -3,7 +3,7 @@ from datetime import timedelta import logging from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_SMOKE, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON @@ -48,7 +48,7 @@ class TahomaBinarySensor(TahomaDevice, BinarySensorEntity): def device_class(self): """Return the class of the device.""" if self.tahoma_device.type == "rtds:RTDSSmokeSensor": - return DEVICE_CLASS_SMOKE + return BinarySensorDeviceClass.SMOKE return None @property diff --git a/homeassistant/components/tahoma/cover.py b/homeassistant/components/tahoma/cover.py index c7bd1540769..11881cac6a0 100644 --- a/homeassistant/components/tahoma/cover.py +++ b/homeassistant/components/tahoma/cover.py @@ -2,16 +2,7 @@ from datetime import timedelta import logging -from homeassistant.components.cover import ( - ATTR_POSITION, - DEVICE_CLASS_AWNING, - DEVICE_CLASS_BLIND, - DEVICE_CLASS_CURTAIN, - DEVICE_CLASS_GARAGE, - DEVICE_CLASS_SHUTTER, - DEVICE_CLASS_WINDOW, - CoverEntity, -) +from homeassistant.components.cover import ATTR_POSITION, CoverDeviceClass, CoverEntity from homeassistant.util.dt import utcnow from . import DOMAIN as TAHOMA_DOMAIN, TahomaDevice @@ -28,25 +19,25 @@ ATTR_LOCK_ORIG = "lock_originator" HORIZONTAL_AWNING = "io:HorizontalAwningIOComponent" TAHOMA_DEVICE_CLASSES = { - HORIZONTAL_AWNING: DEVICE_CLASS_AWNING, - "io:AwningValanceIOComponent": DEVICE_CLASS_AWNING, - "io:DiscreteGarageOpenerWithPartialPositionIOComponent": DEVICE_CLASS_GARAGE, - "io:DiscreteGarageOpenerIOComponent": DEVICE_CLASS_GARAGE, - "io:ExteriorVenetianBlindIOComponent": DEVICE_CLASS_BLIND, - "io:GarageOpenerIOComponent": DEVICE_CLASS_GARAGE, - "io:RollerShutterGenericIOComponent": DEVICE_CLASS_SHUTTER, - "io:RollerShutterUnoIOComponent": DEVICE_CLASS_SHUTTER, - "io:RollerShutterVeluxIOComponent": DEVICE_CLASS_SHUTTER, - "io:RollerShutterWithLowSpeedManagementIOComponent": DEVICE_CLASS_SHUTTER, - "io:VerticalExteriorAwningIOComponent": DEVICE_CLASS_AWNING, - "io:VerticalInteriorBlindVeluxIOComponent": DEVICE_CLASS_BLIND, - "io:WindowOpenerVeluxIOComponent": DEVICE_CLASS_WINDOW, - "rts:BlindRTSComponent": DEVICE_CLASS_BLIND, - "rts:CurtainRTSComponent": DEVICE_CLASS_CURTAIN, - "rts:DualCurtainRTSComponent": DEVICE_CLASS_CURTAIN, - "rts:ExteriorVenetianBlindRTSComponent": DEVICE_CLASS_BLIND, - "rts:RollerShutterRTSComponent": DEVICE_CLASS_SHUTTER, - "rts:VenetianBlindRTSComponent": DEVICE_CLASS_BLIND, + HORIZONTAL_AWNING: CoverDeviceClass.AWNING, + "io:AwningValanceIOComponent": CoverDeviceClass.AWNING, + "io:DiscreteGarageOpenerWithPartialPositionIOComponent": CoverDeviceClass.GARAGE, + "io:DiscreteGarageOpenerIOComponent": CoverDeviceClass.GARAGE, + "io:ExteriorVenetianBlindIOComponent": CoverDeviceClass.BLIND, + "io:GarageOpenerIOComponent": CoverDeviceClass.GARAGE, + "io:RollerShutterGenericIOComponent": CoverDeviceClass.SHUTTER, + "io:RollerShutterUnoIOComponent": CoverDeviceClass.SHUTTER, + "io:RollerShutterVeluxIOComponent": CoverDeviceClass.SHUTTER, + "io:RollerShutterWithLowSpeedManagementIOComponent": CoverDeviceClass.SHUTTER, + "io:VerticalExteriorAwningIOComponent": CoverDeviceClass.AWNING, + "io:VerticalInteriorBlindVeluxIOComponent": CoverDeviceClass.BLIND, + "io:WindowOpenerVeluxIOComponent": CoverDeviceClass.WINDOW, + "rts:BlindRTSComponent": CoverDeviceClass.BLIND, + "rts:CurtainRTSComponent": CoverDeviceClass.CURTAIN, + "rts:DualCurtainRTSComponent": CoverDeviceClass.CURTAIN, + "rts:ExteriorVenetianBlindRTSComponent": CoverDeviceClass.BLIND, + "rts:RollerShutterRTSComponent": CoverDeviceClass.SHUTTER, + "rts:VenetianBlindRTSComponent": CoverDeviceClass.BLIND, } From 4176cb15f61a726323edc7c070eb82736a989d6b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Dec 2021 21:16:12 +0100 Subject: [PATCH 0851/2644] Use new enums in tado (#62423) Co-authored-by: epenet --- .../components/tado/binary_sensor.py | 19 +++++++-------- homeassistant/components/tado/sensor.py | 23 +++++++++---------- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/tado/binary_sensor.py b/homeassistant/components/tado/binary_sensor.py index 9f68aa8a4e7..4d61ebe4651 100644 --- a/homeassistant/components/tado/binary_sensor.py +++ b/homeassistant/components/tado/binary_sensor.py @@ -2,10 +2,7 @@ import logging from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_CONNECTIVITY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_WINDOW, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -143,9 +140,9 @@ class TadoDeviceBinarySensor(TadoDeviceEntity, BinarySensorEntity): def device_class(self): """Return the class of this sensor.""" if self.device_variable == "battery state": - return DEVICE_CLASS_BATTERY + return BinarySensorDeviceClass.BATTERY if self.device_variable == "connection state": - return DEVICE_CLASS_CONNECTIVITY + return BinarySensorDeviceClass.CONNECTIVITY return None @callback @@ -219,15 +216,15 @@ class TadoZoneBinarySensor(TadoZoneEntity, BinarySensorEntity): def device_class(self): """Return the class of this sensor.""" if self.zone_variable == "early start": - return DEVICE_CLASS_POWER + return BinarySensorDeviceClass.POWER if self.zone_variable == "link": - return DEVICE_CLASS_CONNECTIVITY + return BinarySensorDeviceClass.CONNECTIVITY if self.zone_variable == "open window": - return DEVICE_CLASS_WINDOW + return BinarySensorDeviceClass.WINDOW if self.zone_variable == "overlay": - return DEVICE_CLASS_POWER + return BinarySensorDeviceClass.POWER if self.zone_variable == "power": - return DEVICE_CLASS_POWER + return BinarySensorDeviceClass.POWER return None @property diff --git a/homeassistant/components/tado/sensor.py b/homeassistant/components/tado/sensor.py index 872e2cbb42e..5a124dff3b8 100644 --- a/homeassistant/components/tado/sensor.py +++ b/homeassistant/components/tado/sensor.py @@ -1,14 +1,13 @@ """Support for Tado sensors for each zone.""" import logging -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_TEMPERATURE, - PERCENTAGE, - TEMP_CELSIUS, +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorStateClass, ) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE, TEMP_CELSIUS from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -150,14 +149,14 @@ class TadoHomeSensor(TadoHomeEntity, SensorEntity): def device_class(self): """Return the device class.""" if self.home_variable == "outdoor temperature": - return DEVICE_CLASS_TEMPERATURE + return SensorDeviceClass.TEMPERATURE return None @property def state_class(self): """Return the state class.""" if self.home_variable in ["outdoor temperature", "solar percentage"]: - return STATE_CLASS_MEASUREMENT + return SensorStateClass.MEASUREMENT return None @callback @@ -261,16 +260,16 @@ class TadoZoneSensor(TadoZoneEntity, SensorEntity): def device_class(self): """Return the device class.""" if self.zone_variable == "humidity": - return DEVICE_CLASS_HUMIDITY + return SensorDeviceClass.HUMIDITY if self.zone_variable == "temperature": - return DEVICE_CLASS_TEMPERATURE + return SensorDeviceClass.TEMPERATURE return None @property def state_class(self): """Return the state class.""" if self.zone_variable in ["ac", "heating", "humidity", "temperature"]: - return STATE_CLASS_MEASUREMENT + return SensorStateClass.MEASUREMENT return None @callback From bd63b707fc2e413f80448e1d6b2b86e593164b3f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Dec 2021 21:17:37 +0100 Subject: [PATCH 0852/2644] Use attr** in tellstick sensor (#62422) Co-authored-by: epenet --- homeassistant/components/tellstick/sensor.py | 37 +++++++------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/tellstick/sensor.py b/homeassistant/components/tellstick/sensor.py index 74548f94d1b..8ddd71c73a2 100644 --- a/homeassistant/components/tellstick/sensor.py +++ b/homeassistant/components/tellstick/sensor.py @@ -6,13 +6,15 @@ from tellcore import telldus import tellcore.constants as tellcore_constants import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorDeviceClass, + SensorEntity, +) from homeassistant.const import ( CONF_ID, CONF_NAME, CONF_PROTOCOL, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_TEMPERATURE, PERCENTAGE, TEMP_CELSIUS, ) @@ -62,12 +64,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensor_value_descriptions = { tellcore_constants.TELLSTICK_TEMPERATURE: DatatypeDescription( - "temperature", config.get(CONF_TEMPERATURE_SCALE), DEVICE_CLASS_TEMPERATURE + "temperature", + config.get(CONF_TEMPERATURE_SCALE), + SensorDeviceClass.TEMPERATURE, ), tellcore_constants.TELLSTICK_HUMIDITY: DatatypeDescription( "humidity", PERCENTAGE, - DEVICE_CLASS_HUMIDITY, + SensorDeviceClass.HUMIDITY, ), tellcore_constants.TELLSTICK_RAINRATE: DatatypeDescription( "rain rate", "", None @@ -143,26 +147,9 @@ class TellstickSensor(SensorEntity): """Initialize the sensor.""" self._datatype = datatype self._tellcore_sensor = tellcore_sensor - self._unit_of_measurement = sensor_info.unit or None - self._value = None - - self._name = f"{name} {sensor_info.name}" - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._value - - @property - def native_unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement + self._attr_native_unit_of_measurement = sensor_info.unit or None + self._attr_name = f"{name} {sensor_info.name}" def update(self): """Update tellstick sensor.""" - self._value = self._tellcore_sensor.value(self._datatype).value + self._attr_native_value = self._tellcore_sensor.value(self._datatype).value From ce93364a367b38ac26d51ea11edba19a47e02e10 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Dec 2021 21:18:15 +0100 Subject: [PATCH 0853/2644] Use new enums in zwave_js (#62432) Co-authored-by: epenet --- homeassistant/components/zwave_js/select.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/select.py b/homeassistant/components/zwave_js/select.py index 08f9059e125..206ba8bee5e 100644 --- a/homeassistant/components/zwave_js/select.py +++ b/homeassistant/components/zwave_js/select.py @@ -9,9 +9,9 @@ from zwave_js_server.const.command_class.sound_switch import ToneID from homeassistant.components.select import DOMAIN as SELECT_DOMAIN, SelectEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENTITY_CATEGORY_CONFIG 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 .const import DATA_CLIENT, DOMAIN @@ -53,7 +53,7 @@ async def async_setup_entry( class ZwaveSelectEntity(ZWaveBaseEntity, SelectEntity): """Representation of a Z-Wave select entity.""" - _attr_entity_category = ENTITY_CATEGORY_CONFIG + _attr_entity_category = EntityCategory.CONFIG def __init__( self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo @@ -89,7 +89,7 @@ class ZwaveSelectEntity(ZWaveBaseEntity, SelectEntity): class ZwaveDefaultToneSelectEntity(ZWaveBaseEntity, SelectEntity): """Representation of a Z-Wave default tone select entity.""" - _attr_entity_category = ENTITY_CATEGORY_CONFIG + _attr_entity_category = EntityCategory.CONFIG def __init__( self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo From c5e6489475e9e7e866764f31a6d1e6add2404c97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 20 Dec 2021 23:18:53 +0200 Subject: [PATCH 0854/2644] Use DeviceAutomationType in tests/components/[h-l]* (#62441) --- tests/components/homekit/test_type_triggers.py | 5 ++++- .../specific_devices/test_aqara_switch.py | 5 ++++- .../specific_devices/test_hue_bridge.py | 5 ++++- .../specific_devices/test_lg_tv.py | 5 ++++- .../specific_devices/test_netamo_doorbell.py | 5 ++++- .../homekit_controller/test_device_trigger.py | 13 ++++++++++--- tests/components/hue/test_device_trigger_v1.py | 9 +++++++-- tests/components/hue/test_device_trigger_v2.py | 3 ++- tests/components/humidifier/test_device_action.py | 5 ++++- .../components/humidifier/test_device_condition.py | 5 ++++- tests/components/humidifier/test_device_trigger.py | 5 ++++- tests/components/kodi/test_device_trigger.py | 5 ++++- tests/components/lcn/test_device_trigger.py | 9 +++++++-- tests/components/light/test_device_action.py | 13 ++++++++++--- tests/components/light/test_device_condition.py | 9 +++++++-- tests/components/light/test_device_trigger.py | 9 +++++++-- tests/components/lock/test_device_action.py | 5 ++++- tests/components/lock/test_device_condition.py | 5 ++++- tests/components/lock/test_device_trigger.py | 9 +++++++-- .../components/lutron_caseta/test_device_trigger.py | 9 +++++++-- 20 files changed, 108 insertions(+), 30 deletions(-) diff --git a/tests/components/homekit/test_type_triggers.py b/tests/components/homekit/test_type_triggers.py index 4a265858cb3..b6d46b8cda5 100644 --- a/tests/components/homekit/test_type_triggers.py +++ b/tests/components/homekit/test_type_triggers.py @@ -2,6 +2,7 @@ from unittest.mock import MagicMock +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.homekit.type_triggers import DeviceTriggerAccessory from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component @@ -26,7 +27,9 @@ async def test_programmable_switch_button_fires_on_trigger( assert entry is not None device_id = entry.device_id - device_triggers = await async_get_device_automations(hass, "trigger", device_id) + device_triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_id + ) acc = DeviceTriggerAccessory( hass, hk_driver, diff --git a/tests/components/homekit_controller/specific_devices/test_aqara_switch.py b/tests/components/homekit_controller/specific_devices/test_aqara_switch.py index 6ed0d861193..945d950ecc9 100644 --- a/tests/components/homekit_controller/specific_devices/test_aqara_switch.py +++ b/tests/components/homekit_controller/specific_devices/test_aqara_switch.py @@ -7,6 +7,7 @@ service-label-index despite not being linked to a service-label. https://github.com/home-assistant/core/pull/39090 """ +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.helpers import entity_registry as er from tests.common import assert_lists_same, async_get_device_automations @@ -50,5 +51,7 @@ async def test_aqara_switch_setup(hass): } ) - triggers = await async_get_device_automations(hass, "trigger", battery.device_id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, battery.device_id + ) assert_lists_same(triggers, expected) diff --git a/tests/components/homekit_controller/specific_devices/test_hue_bridge.py b/tests/components/homekit_controller/specific_devices/test_hue_bridge.py index 0452407bfb8..5bc540a50a4 100644 --- a/tests/components/homekit_controller/specific_devices/test_hue_bridge.py +++ b/tests/components/homekit_controller/specific_devices/test_hue_bridge.py @@ -1,5 +1,6 @@ """Tests for handling accessories on a Hue bridge via HomeKit.""" +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.common import assert_lists_same, async_get_device_automations @@ -63,5 +64,7 @@ async def test_hue_bridge_setup(hass): } ) - triggers = await async_get_device_automations(hass, "trigger", device.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device.id + ) assert_lists_same(triggers, expected) diff --git a/tests/components/homekit_controller/specific_devices/test_lg_tv.py b/tests/components/homekit_controller/specific_devices/test_lg_tv.py index bce8ed7418c..592ad2088b8 100644 --- a/tests/components/homekit_controller/specific_devices/test_lg_tv.py +++ b/tests/components/homekit_controller/specific_devices/test_lg_tv.py @@ -1,5 +1,6 @@ """Make sure that handling real world LG HomeKit characteristics isn't broken.""" +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.media_player.const import ( SUPPORT_PAUSE, SUPPORT_PLAY, @@ -66,6 +67,8 @@ async def test_lg_tv(hass): assert device.hw_version == "1" # A TV has media player device triggers - triggers = await async_get_device_automations(hass, "trigger", device.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device.id + ) for trigger in triggers: assert trigger["domain"] == "media_player" diff --git a/tests/components/homekit_controller/specific_devices/test_netamo_doorbell.py b/tests/components/homekit_controller/specific_devices/test_netamo_doorbell.py index 58fe9df077f..d213e72c59c 100644 --- a/tests/components/homekit_controller/specific_devices/test_netamo_doorbell.py +++ b/tests/components/homekit_controller/specific_devices/test_netamo_doorbell.py @@ -4,6 +4,7 @@ Regression tests for Netamo Doorbell. https://github.com/home-assistant/core/issues/44596 """ +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.common import assert_lists_same, async_get_device_automations @@ -69,5 +70,7 @@ async def test_netamo_doorbell_setup(hass): } ) - triggers = await async_get_device_automations(hass, "trigger", doorbell.device_id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, doorbell.device_id + ) assert_lists_same(triggers, expected) diff --git a/tests/components/homekit_controller/test_device_trigger.py b/tests/components/homekit_controller/test_device_trigger.py index 7c02c1a6456..8541bbd2fe6 100644 --- a/tests/components/homekit_controller/test_device_trigger.py +++ b/tests/components/homekit_controller/test_device_trigger.py @@ -4,6 +4,7 @@ from aiohomekit.model.services import ServicesTypes import pytest import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.homekit_controller.const import DOMAIN from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component @@ -111,7 +112,9 @@ async def test_enumerate_remote(hass, utcnow): } ) - triggers = await async_get_device_automations(hass, "trigger", device.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device.id + ) assert_lists_same(triggers, expected) @@ -146,7 +149,9 @@ async def test_enumerate_button(hass, utcnow): } ) - triggers = await async_get_device_automations(hass, "trigger", device.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device.id + ) assert_lists_same(triggers, expected) @@ -181,7 +186,9 @@ async def test_enumerate_doorbell(hass, utcnow): } ) - triggers = await async_get_device_automations(hass, "trigger", device.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device.id + ) assert_lists_same(triggers, expected) diff --git a/tests/components/hue/test_device_trigger_v1.py b/tests/components/hue/test_device_trigger_v1.py index fcb6ca5668e..bcb49fe9a16 100644 --- a/tests/components/hue/test_device_trigger_v1.py +++ b/tests/components/hue/test_device_trigger_v1.py @@ -1,6 +1,7 @@ """The tests for Philips Hue device triggers for V1 bridge.""" from homeassistant.components import automation, hue +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.hue.v1 import device_trigger from homeassistant.setup import async_setup_component @@ -25,7 +26,9 @@ async def test_get_triggers(hass, mock_bridge_v1, device_reg): hue_tap_device = device_reg.async_get_device( {(hue.DOMAIN, "00:00:00:00:00:44:23:08")} ) - triggers = await async_get_device_automations(hass, "trigger", hue_tap_device.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, hue_tap_device.id + ) expected_triggers = [ { @@ -43,7 +46,9 @@ async def test_get_triggers(hass, mock_bridge_v1, device_reg): hue_dimmer_device = device_reg.async_get_device( {(hue.DOMAIN, "00:17:88:01:10:3e:3a:dc")} ) - triggers = await async_get_device_automations(hass, "trigger", hue_dimmer_device.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, hue_dimmer_device.id + ) trigger_batt = { "platform": "device", diff --git a/tests/components/hue/test_device_trigger_v2.py b/tests/components/hue/test_device_trigger_v2.py index 0641281b9fa..e6de70d12cb 100644 --- a/tests/components/hue/test_device_trigger_v2.py +++ b/tests/components/hue/test_device_trigger_v2.py @@ -2,6 +2,7 @@ from aiohue.v2.models.button import ButtonEvent from homeassistant.components import hue +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.hue.v2.device import async_setup_devices from homeassistant.components.hue.v2.hue_event import async_setup_hue_events @@ -52,7 +53,7 @@ async def test_get_triggers(hass, mock_bridge_v2, v2_resources_test_data, device {(hue.DOMAIN, "3ff06175-29e8-44a8-8fe7-af591b0025da")} ) triggers = await async_get_device_automations( - hass, "trigger", hue_wall_switch_device.id + hass, DeviceAutomationType.TRIGGER, hue_wall_switch_device.id ) trigger_batt = { diff --git a/tests/components/humidifier/test_device_action.py b/tests/components/humidifier/test_device_action.py index 39767b569ac..f8391e2509b 100644 --- a/tests/components/humidifier/test_device_action.py +++ b/tests/components/humidifier/test_device_action.py @@ -3,6 +3,7 @@ import pytest import voluptuous_serialize import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.humidifier import DOMAIN, const, device_action from homeassistant.const import STATE_ON from homeassistant.helpers import config_validation as cv, device_registry @@ -87,7 +88,9 @@ async def test_get_actions( } for action in expected_action_types ] - actions = await async_get_device_automations(hass, "action", device_entry.id) + actions = await async_get_device_automations( + hass, DeviceAutomationType.ACTION, device_entry.id + ) assert_lists_same(actions, expected_actions) diff --git a/tests/components/humidifier/test_device_condition.py b/tests/components/humidifier/test_device_condition.py index 0d0f65d2c97..aed1079b915 100644 --- a/tests/components/humidifier/test_device_condition.py +++ b/tests/components/humidifier/test_device_condition.py @@ -3,6 +3,7 @@ import pytest import voluptuous_serialize import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.humidifier import DOMAIN, const, device_condition from homeassistant.const import ATTR_MODE, STATE_OFF, STATE_ON from homeassistant.helpers import config_validation as cv, device_registry @@ -95,7 +96,9 @@ async def test_get_conditions( } for condition in expected_condition_types ] - conditions = await async_get_device_automations(hass, "condition", device_entry.id) + conditions = await async_get_device_automations( + hass, DeviceAutomationType.CONDITION, device_entry.id + ) assert_lists_same(conditions, expected_conditions) diff --git a/tests/components/humidifier/test_device_trigger.py b/tests/components/humidifier/test_device_trigger.py index 12918684df7..e11702a3468 100644 --- a/tests/components/humidifier/test_device_trigger.py +++ b/tests/components/humidifier/test_device_trigger.py @@ -5,6 +5,7 @@ import pytest import voluptuous_serialize import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.humidifier import DOMAIN, const, device_trigger from homeassistant.const import ATTR_MODE, ATTR_SUPPORTED_FEATURES, STATE_OFF, STATE_ON from homeassistant.helpers import config_validation as cv, device_registry @@ -84,7 +85,9 @@ async def test_get_triggers(hass, device_reg, entity_reg): "entity_id": f"{DOMAIN}.test_5678", }, ] - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert_lists_same(triggers, expected_triggers) diff --git a/tests/components/kodi/test_device_trigger.py b/tests/components/kodi/test_device_trigger.py index 462d2381cec..2bb5dbcd578 100644 --- a/tests/components/kodi/test_device_trigger.py +++ b/tests/components/kodi/test_device_trigger.py @@ -2,6 +2,7 @@ import pytest import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.kodi import DOMAIN from homeassistant.components.media_player.const import DOMAIN as MP_DOMAIN from homeassistant.setup import async_setup_component @@ -70,7 +71,9 @@ async def test_get_triggers(hass, device_reg, entity_reg): ] # Test triggers are either kodi specific triggers or media_player entity triggers - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) for expected_trigger in expected_triggers: assert expected_trigger in triggers for trigger in triggers: diff --git a/tests/components/lcn/test_device_trigger.py b/tests/components/lcn/test_device_trigger.py index 4030a9b26da..aced80642e6 100644 --- a/tests/components/lcn/test_device_trigger.py +++ b/tests/components/lcn/test_device_trigger.py @@ -5,6 +5,7 @@ from pypck.lcn_defs import AccessControlPeriphery, KeyAction, SendKeyCommand import voluptuous_serialize from homeassistant.components import automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.lcn import device_trigger from homeassistant.components.lcn.const import DOMAIN, KEY_ACTIONS, SENDKEYS from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE @@ -47,7 +48,9 @@ async def test_get_triggers_module_device(hass, entry, lcn_connection): }, ] - triggers = await async_get_device_automations(hass, "trigger", device.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device.id + ) assert_lists_same(triggers, expected_triggers) @@ -63,7 +66,9 @@ async def test_get_triggers_non_module_device(hass, entry, lcn_connection): ) for device in (host_device, group_device, resource_device): - triggers = await async_get_device_automations(hass, "trigger", device.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device.id + ) for trigger in triggers: assert trigger[CONF_TYPE] not in not_included_types diff --git a/tests/components/light/test_device_action.py b/tests/components/light/test_device_action.py index 58743b1ae05..6d38b4784a7 100644 --- a/tests/components/light/test_device_action.py +++ b/tests/components/light/test_device_action.py @@ -2,6 +2,7 @@ import pytest import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.light import ( ATTR_SUPPORTED_COLOR_MODES, COLOR_MODE_BRIGHTNESS, @@ -97,7 +98,9 @@ async def test_get_actions(hass, device_reg, entity_reg): "entity_id": f"{DOMAIN}.test_5678", }, ] - actions = await async_get_device_automations(hass, "action", device_entry.id) + actions = await async_get_device_automations( + hass, DeviceAutomationType.ACTION, device_entry.id + ) assert actions == expected_actions @@ -116,7 +119,9 @@ async def test_get_action_capabilities(hass, device_reg, entity_reg): "5678", device_id=device_entry.id, ).entity_id - actions = await async_get_device_automations(hass, "action", device_entry.id) + actions = await async_get_device_automations( + hass, DeviceAutomationType.ACTION, device_entry.id + ) assert len(actions) == 3 action_types = {action["type"] for action in actions} assert action_types == {"turn_on", "toggle", "turn_off"} @@ -260,7 +265,9 @@ async def test_get_action_capabilities_features( {"supported_features": supported_features_state, **attributes_state}, ) - actions = await async_get_device_automations(hass, "action", device_entry.id) + actions = await async_get_device_automations( + hass, DeviceAutomationType.ACTION, device_entry.id + ) assert len(actions) == len(expected_actions) action_types = {action["type"] for action in actions} assert action_types == expected_actions diff --git a/tests/components/light/test_device_condition.py b/tests/components/light/test_device_condition.py index b174a312cd9..7afd1272017 100644 --- a/tests/components/light/test_device_condition.py +++ b/tests/components/light/test_device_condition.py @@ -5,6 +5,7 @@ from unittest.mock import patch import pytest import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.light import DOMAIN from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON from homeassistant.helpers import device_registry @@ -65,7 +66,9 @@ async def test_get_conditions(hass, device_reg, entity_reg): "entity_id": f"{DOMAIN}.test_5678", }, ] - conditions = await async_get_device_automations(hass, "condition", device_entry.id) + conditions = await async_get_device_automations( + hass, DeviceAutomationType.CONDITION, device_entry.id + ) assert conditions == expected_conditions @@ -83,7 +86,9 @@ async def test_get_condition_capabilities(hass, device_reg, entity_reg): {"name": "for", "optional": True, "type": "positive_time_period_dict"} ] } - conditions = await async_get_device_automations(hass, "condition", device_entry.id) + conditions = await async_get_device_automations( + hass, DeviceAutomationType.CONDITION, device_entry.id + ) for condition in conditions: capabilities = await async_get_device_automation_capabilities( hass, "condition", condition diff --git a/tests/components/light/test_device_trigger.py b/tests/components/light/test_device_trigger.py index 3217eb461b0..342a761e7c4 100644 --- a/tests/components/light/test_device_trigger.py +++ b/tests/components/light/test_device_trigger.py @@ -4,6 +4,7 @@ from datetime import timedelta import pytest import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.light import DOMAIN from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON from homeassistant.helpers import device_registry @@ -65,7 +66,9 @@ async def test_get_triggers(hass, device_reg, entity_reg): "entity_id": f"{DOMAIN}.test_5678", }, ] - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert triggers == expected_triggers @@ -83,7 +86,9 @@ async def test_get_trigger_capabilities(hass, device_reg, entity_reg): {"name": "for", "optional": True, "type": "positive_time_period_dict"} ] } - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) for trigger in triggers: capabilities = await async_get_device_automation_capabilities( hass, "trigger", trigger diff --git a/tests/components/lock/test_device_action.py b/tests/components/lock/test_device_action.py index c5a9b19d949..4ee03bcbb0f 100644 --- a/tests/components/lock/test_device_action.py +++ b/tests/components/lock/test_device_action.py @@ -2,6 +2,7 @@ import pytest import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.lock import DOMAIN, SUPPORT_OPEN from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component @@ -85,7 +86,9 @@ async def test_get_actions( } for action in expected_action_types ] - actions = await async_get_device_automations(hass, "action", device_entry.id) + actions = await async_get_device_automations( + hass, DeviceAutomationType.ACTION, device_entry.id + ) assert_lists_same(actions, expected_actions) diff --git a/tests/components/lock/test_device_condition.py b/tests/components/lock/test_device_condition.py index aeb304cb1c8..08cc8c97925 100644 --- a/tests/components/lock/test_device_condition.py +++ b/tests/components/lock/test_device_condition.py @@ -2,6 +2,7 @@ import pytest import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.lock import DOMAIN from homeassistant.const import ( STATE_JAMMED, @@ -88,7 +89,9 @@ async def test_get_conditions(hass, device_reg, entity_reg): "entity_id": f"{DOMAIN}.test_5678", }, ] - conditions = await async_get_device_automations(hass, "condition", device_entry.id) + conditions = await async_get_device_automations( + hass, DeviceAutomationType.CONDITION, device_entry.id + ) assert_lists_same(conditions, expected_conditions) diff --git a/tests/components/lock/test_device_trigger.py b/tests/components/lock/test_device_trigger.py index c3539288f94..000ed8b44aa 100644 --- a/tests/components/lock/test_device_trigger.py +++ b/tests/components/lock/test_device_trigger.py @@ -4,6 +4,7 @@ from datetime import timedelta import pytest import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.lock import DOMAIN from homeassistant.const import ( STATE_JAMMED, @@ -93,7 +94,9 @@ async def test_get_triggers(hass, device_reg, entity_reg): "entity_id": f"{DOMAIN}.test_5678", }, ] - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert_lists_same(triggers, expected_triggers) @@ -107,7 +110,9 @@ async def test_get_trigger_capabilities(hass, device_reg, entity_reg): ) entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert len(triggers) == 5 for trigger in triggers: capabilities = await async_get_device_automation_capabilities( diff --git a/tests/components/lutron_caseta/test_device_trigger.py b/tests/components/lutron_caseta/test_device_trigger.py index 23faa929574..97c31e980d0 100644 --- a/tests/components/lutron_caseta/test_device_trigger.py +++ b/tests/components/lutron_caseta/test_device_trigger.py @@ -2,6 +2,7 @@ import pytest from homeassistant.components import automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) @@ -166,7 +167,9 @@ async def test_get_triggers(hass, device_reg): }, ] - triggers = await async_get_device_automations(hass, "trigger", device_id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_id + ) assert_lists_same(triggers, expected_triggers) @@ -180,7 +183,9 @@ async def test_get_triggers_for_invalid_device_id(hass, device_reg): ) with pytest.raises(InvalidDeviceAutomationConfig): - await async_get_device_automations(hass, "trigger", invalid_device.id) + await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, invalid_device.id + ) async def test_if_fires_on_button_event(hass, calls, device_reg): From 5926961ed5965ba684c723c1b3a3450cd8fa9f74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 20 Dec 2021 23:26:29 +0200 Subject: [PATCH 0855/2644] Use DeviceAutomationType in tests/components/[a-f]* (#62440) --- .../alarm_control_panel/test_device_action.py | 17 +++++++++++++---- .../test_device_condition.py | 5 ++++- .../alarm_control_panel/test_device_trigger.py | 9 +++++++-- .../components/arcam_fmj/test_device_trigger.py | 9 +++++++-- .../binary_sensor/test_device_condition.py | 13 ++++++++++--- .../binary_sensor/test_device_trigger.py | 13 ++++++++++--- tests/components/button/test_device_action.py | 5 ++++- tests/components/button/test_device_trigger.py | 5 ++++- tests/components/climate/test_device_action.py | 5 ++++- .../components/climate/test_device_condition.py | 5 ++++- tests/components/climate/test_device_trigger.py | 5 ++++- tests/components/cover/test_device_action.py | 17 +++++++++++++---- tests/components/cover/test_device_condition.py | 17 +++++++++++++---- tests/components/cover/test_device_trigger.py | 17 +++++++++++++---- tests/components/deconz/test_device_trigger.py | 9 +++++++-- .../device_tracker/test_device_condition.py | 5 ++++- .../device_tracker/test_device_trigger.py | 5 ++++- tests/components/fan/test_device_action.py | 5 ++++- tests/components/fan/test_device_condition.py | 5 ++++- tests/components/fan/test_device_trigger.py | 9 +++++++-- 20 files changed, 140 insertions(+), 40 deletions(-) diff --git a/tests/components/alarm_control_panel/test_device_action.py b/tests/components/alarm_control_panel/test_device_action.py index 75f1dc76aaf..f4b0832ad97 100644 --- a/tests/components/alarm_control_panel/test_device_action.py +++ b/tests/components/alarm_control_panel/test_device_action.py @@ -3,6 +3,7 @@ import pytest from homeassistant.components.alarm_control_panel import DOMAIN, const import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.const import ( CONF_PLATFORM, STATE_ALARM_ARMED_AWAY, @@ -92,7 +93,9 @@ async def test_get_actions( } for action in expected_action_types ] - actions = await async_get_device_automations(hass, "action", device_entry.id) + actions = await async_get_device_automations( + hass, DeviceAutomationType.ACTION, device_entry.id + ) assert_lists_same(actions, expected_actions) @@ -122,7 +125,9 @@ async def test_get_actions_arm_night_only(hass, device_reg, entity_reg): "entity_id": "alarm_control_panel.test_5678", }, ] - actions = await async_get_device_automations(hass, "action", device_entry.id) + actions = await async_get_device_automations( + hass, DeviceAutomationType.ACTION, device_entry.id + ) assert_lists_same(actions, expected_actions) @@ -158,7 +163,9 @@ async def test_get_action_capabilities( }, "trigger": {"extra_fields": []}, } - actions = await async_get_device_automations(hass, "action", device_entry.id) + actions = await async_get_device_automations( + hass, DeviceAutomationType.ACTION, device_entry.id + ) assert len(actions) == 6 assert {action["type"] for action in actions} == set(expected_capabilities) for action in actions: @@ -208,7 +215,9 @@ async def test_get_action_capabilities_arm_code( }, "trigger": {"extra_fields": []}, } - actions = await async_get_device_automations(hass, "action", device_entry.id) + actions = await async_get_device_automations( + hass, DeviceAutomationType.ACTION, device_entry.id + ) assert len(actions) == 6 assert {action["type"] for action in actions} == set(expected_capabilities) for action in actions: diff --git a/tests/components/alarm_control_panel/test_device_condition.py b/tests/components/alarm_control_panel/test_device_condition.py index d0644562850..b44e6ba7e8b 100644 --- a/tests/components/alarm_control_panel/test_device_condition.py +++ b/tests/components/alarm_control_panel/test_device_condition.py @@ -3,6 +3,7 @@ import pytest from homeassistant.components.alarm_control_panel import DOMAIN, const import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS, @@ -112,7 +113,9 @@ async def test_get_conditions( } for condition in expected_condition_types ] - conditions = await async_get_device_automations(hass, "condition", device_entry.id) + conditions = await async_get_device_automations( + hass, DeviceAutomationType.CONDITION, device_entry.id + ) assert_lists_same(conditions, expected_conditions) diff --git a/tests/components/alarm_control_panel/test_device_trigger.py b/tests/components/alarm_control_panel/test_device_trigger.py index 9edda7e98e2..e874b50baa0 100644 --- a/tests/components/alarm_control_panel/test_device_trigger.py +++ b/tests/components/alarm_control_panel/test_device_trigger.py @@ -5,6 +5,7 @@ import pytest from homeassistant.components.alarm_control_panel import DOMAIN import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, @@ -125,7 +126,9 @@ async def test_get_triggers( } for trigger in expected_trigger_types ] - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert_lists_same(triggers, expected_triggers) @@ -142,7 +145,9 @@ async def test_get_trigger_capabilities(hass, device_reg, entity_reg): "alarm_control_panel.test_5678", "attributes", {"supported_features": 15} ) - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert len(triggers) == 6 for trigger in triggers: capabilities = await async_get_device_automation_capabilities( diff --git a/tests/components/arcam_fmj/test_device_trigger.py b/tests/components/arcam_fmj/test_device_trigger.py index d5ab820ce46..fe2507e3d8e 100644 --- a/tests/components/arcam_fmj/test_device_trigger.py +++ b/tests/components/arcam_fmj/test_device_trigger.py @@ -3,6 +3,7 @@ import pytest from homeassistant.components.arcam_fmj.const import DOMAIN import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.setup import async_setup_component from tests.common import ( @@ -53,10 +54,14 @@ async def test_get_triggers(hass, device_reg, entity_reg): "entity_id": "media_player.arcam_fmj_5678", }, ] - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) # Test triggers are either arcam_fmj specific or media_player entity triggers - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) for expected_trigger in expected_triggers: assert expected_trigger in triggers for trigger in triggers: diff --git a/tests/components/binary_sensor/test_device_condition.py b/tests/components/binary_sensor/test_device_condition.py index 5a609fbc30a..2e23fd799f2 100644 --- a/tests/components/binary_sensor/test_device_condition.py +++ b/tests/components/binary_sensor/test_device_condition.py @@ -7,6 +7,7 @@ import pytest import homeassistant.components.automation as automation from homeassistant.components.binary_sensor import DEVICE_CLASSES, DOMAIN from homeassistant.components.binary_sensor.device_condition import ENTITY_CONDITIONS +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component @@ -74,7 +75,9 @@ async def test_get_conditions(hass, device_reg, entity_reg, enable_custom_integr for device_class in DEVICE_CLASSES for condition in ENTITY_CONDITIONS[device_class] ] - conditions = await async_get_device_automations(hass, "condition", device_entry.id) + conditions = await async_get_device_automations( + hass, DeviceAutomationType.CONDITION, device_entry.id + ) assert conditions == expected_conditions @@ -109,7 +112,9 @@ async def test_get_conditions_no_state(hass, device_reg, entity_reg): for device_class in DEVICE_CLASSES for condition in ENTITY_CONDITIONS[device_class] ] - conditions = await async_get_device_automations(hass, "condition", device_entry.id) + conditions = await async_get_device_automations( + hass, DeviceAutomationType.CONDITION, device_entry.id + ) assert conditions == expected_conditions @@ -127,7 +132,9 @@ async def test_get_condition_capabilities(hass, device_reg, entity_reg): {"name": "for", "optional": True, "type": "positive_time_period_dict"} ] } - conditions = await async_get_device_automations(hass, "condition", device_entry.id) + conditions = await async_get_device_automations( + hass, DeviceAutomationType.CONDITION, device_entry.id + ) for condition in conditions: capabilities = await async_get_device_automation_capabilities( hass, "condition", condition diff --git a/tests/components/binary_sensor/test_device_trigger.py b/tests/components/binary_sensor/test_device_trigger.py index 0cf76453238..001af0b1e64 100644 --- a/tests/components/binary_sensor/test_device_trigger.py +++ b/tests/components/binary_sensor/test_device_trigger.py @@ -6,6 +6,7 @@ import pytest import homeassistant.components.automation as automation from homeassistant.components.binary_sensor import DEVICE_CLASSES, DOMAIN from homeassistant.components.binary_sensor.device_trigger import ENTITY_TRIGGERS +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component @@ -74,7 +75,9 @@ async def test_get_triggers(hass, device_reg, entity_reg, enable_custom_integrat for device_class in DEVICE_CLASSES for trigger in ENTITY_TRIGGERS[device_class] ] - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert triggers == expected_triggers @@ -112,7 +115,9 @@ async def test_get_triggers_no_state(hass, device_reg, entity_reg): for device_class in DEVICE_CLASSES for trigger in ENTITY_TRIGGERS[device_class] ] - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert triggers == expected_triggers @@ -130,7 +135,9 @@ async def test_get_trigger_capabilities(hass, device_reg, entity_reg): {"name": "for", "optional": True, "type": "positive_time_period_dict"} ] } - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) for trigger in triggers: capabilities = await async_get_device_automation_capabilities( hass, "trigger", trigger diff --git a/tests/components/button/test_device_action.py b/tests/components/button/test_device_action.py index 984be163d42..ab1e145cdb1 100644 --- a/tests/components/button/test_device_action.py +++ b/tests/components/button/test_device_action.py @@ -3,6 +3,7 @@ import pytest from homeassistant.components import automation from homeassistant.components.button import DOMAIN +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry, entity_registry from homeassistant.setup import async_setup_component @@ -50,7 +51,9 @@ async def test_get_actions( "entity_id": "button.test_5678", } ] - actions = await async_get_device_automations(hass, "action", device_entry.id) + actions = await async_get_device_automations( + hass, DeviceAutomationType.ACTION, device_entry.id + ) assert_lists_same(actions, expected_actions) diff --git a/tests/components/button/test_device_trigger.py b/tests/components/button/test_device_trigger.py index 913aec9088e..88534941aa9 100644 --- a/tests/components/button/test_device_trigger.py +++ b/tests/components/button/test_device_trigger.py @@ -5,6 +5,7 @@ import pytest from homeassistant.components import automation from homeassistant.components.button import DOMAIN +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import device_registry from homeassistant.helpers.entity_registry import EntityRegistry @@ -60,7 +61,9 @@ async def test_get_triggers( "entity_id": f"{DOMAIN}.test_5678", } ] - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert_lists_same(triggers, expected_triggers) diff --git a/tests/components/climate/test_device_action.py b/tests/components/climate/test_device_action.py index 3f9b1148443..4f926e1b094 100644 --- a/tests/components/climate/test_device_action.py +++ b/tests/components/climate/test_device_action.py @@ -4,6 +4,7 @@ import voluptuous_serialize import homeassistant.components.automation as automation from homeassistant.components.climate import DOMAIN, const, device_action +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.helpers import config_validation as cv, device_registry from homeassistant.setup import async_setup_component @@ -79,7 +80,9 @@ async def test_get_actions( for action in expected_action_types ] - actions = await async_get_device_automations(hass, "action", device_entry.id) + actions = await async_get_device_automations( + hass, DeviceAutomationType.ACTION, device_entry.id + ) assert_lists_same(actions, expected_actions) diff --git a/tests/components/climate/test_device_condition.py b/tests/components/climate/test_device_condition.py index 2d6aa82fdf1..65c1e17048b 100644 --- a/tests/components/climate/test_device_condition.py +++ b/tests/components/climate/test_device_condition.py @@ -4,6 +4,7 @@ import voluptuous_serialize import homeassistant.components.automation as automation from homeassistant.components.climate import DOMAIN, const, device_condition +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.helpers import config_validation as cv, device_registry from homeassistant.setup import async_setup_component @@ -83,7 +84,9 @@ async def test_get_conditions( } for condition in expected_condition_types ] - conditions = await async_get_device_automations(hass, "condition", device_entry.id) + conditions = await async_get_device_automations( + hass, DeviceAutomationType.CONDITION, device_entry.id + ) assert_lists_same(conditions, expected_conditions) diff --git a/tests/components/climate/test_device_trigger.py b/tests/components/climate/test_device_trigger.py index 017385362d1..523a9f04a8b 100644 --- a/tests/components/climate/test_device_trigger.py +++ b/tests/components/climate/test_device_trigger.py @@ -4,6 +4,7 @@ import voluptuous_serialize import homeassistant.components.automation as automation from homeassistant.components.climate import DOMAIN, const, device_trigger +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers import config_validation as cv, device_registry from homeassistant.setup import async_setup_component @@ -79,7 +80,9 @@ async def test_get_triggers(hass, device_reg, entity_reg): "entity_id": entity_id, }, ] - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert_lists_same(triggers, expected_triggers) diff --git a/tests/components/cover/test_device_action.py b/tests/components/cover/test_device_action.py index 491b59d9acb..7954b3389e1 100644 --- a/tests/components/cover/test_device_action.py +++ b/tests/components/cover/test_device_action.py @@ -13,6 +13,7 @@ from homeassistant.components.cover import ( SUPPORT_STOP, SUPPORT_STOP_TILT, ) +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.const import CONF_PLATFORM from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component @@ -101,7 +102,9 @@ async def test_get_actions( } for action in expected_action_types ] - actions = await async_get_device_automations(hass, "action", device_entry.id) + actions = await async_get_device_automations( + hass, DeviceAutomationType.ACTION, device_entry.id + ) assert_lists_same(actions, expected_actions) @@ -140,7 +143,9 @@ async def test_get_action_capabilities( assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() - actions = await async_get_device_automations(hass, "action", device_entry.id) + actions = await async_get_device_automations( + hass, DeviceAutomationType.ACTION, device_entry.id + ) assert len(actions) == 5 # open, close, open_tilt, close_tilt action_types = {action["type"] for action in actions} assert action_types == {"open", "close", "stop", "open_tilt", "close_tilt"} @@ -184,7 +189,9 @@ async def test_get_action_capabilities_set_pos( } ] } - actions = await async_get_device_automations(hass, "action", device_entry.id) + actions = await async_get_device_automations( + hass, DeviceAutomationType.ACTION, device_entry.id + ) assert len(actions) == 1 # set_position action_types = {action["type"] for action in actions} assert action_types == {"set_position"} @@ -231,7 +238,9 @@ async def test_get_action_capabilities_set_tilt_pos( } ] } - actions = await async_get_device_automations(hass, "action", device_entry.id) + actions = await async_get_device_automations( + hass, DeviceAutomationType.ACTION, device_entry.id + ) assert len(actions) == 3 action_types = {action["type"] for action in actions} assert action_types == {"open", "close", "set_tilt_position"} diff --git a/tests/components/cover/test_device_condition.py b/tests/components/cover/test_device_condition.py index efa8e8b3383..b964ad8bc0d 100644 --- a/tests/components/cover/test_device_condition.py +++ b/tests/components/cover/test_device_condition.py @@ -9,6 +9,7 @@ from homeassistant.components.cover import ( SUPPORT_SET_POSITION, SUPPORT_SET_TILT_POSITION, ) +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.const import ( CONF_PLATFORM, STATE_CLOSED, @@ -105,7 +106,9 @@ async def test_get_conditions( } for condition in expected_condition_types ] - conditions = await async_get_device_automations(hass, "condition", device_entry.id) + conditions = await async_get_device_automations( + hass, DeviceAutomationType.CONDITION, device_entry.id + ) assert_lists_same(conditions, expected_conditions) @@ -129,7 +132,9 @@ async def test_get_condition_capabilities( assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - conditions = await async_get_device_automations(hass, "condition", device_entry.id) + conditions = await async_get_device_automations( + hass, DeviceAutomationType.CONDITION, device_entry.id + ) assert len(conditions) == 4 for condition in conditions: capabilities = await async_get_device_automation_capabilities( @@ -178,7 +183,9 @@ async def test_get_condition_capabilities_set_pos( }, ] } - conditions = await async_get_device_automations(hass, "condition", device_entry.id) + conditions = await async_get_device_automations( + hass, DeviceAutomationType.CONDITION, device_entry.id + ) assert len(conditions) == 5 for condition in conditions: capabilities = await async_get_device_automation_capabilities( @@ -230,7 +237,9 @@ async def test_get_condition_capabilities_set_tilt_pos( }, ] } - conditions = await async_get_device_automations(hass, "condition", device_entry.id) + conditions = await async_get_device_automations( + hass, DeviceAutomationType.CONDITION, device_entry.id + ) assert len(conditions) == 5 for condition in conditions: capabilities = await async_get_device_automation_capabilities( diff --git a/tests/components/cover/test_device_trigger.py b/tests/components/cover/test_device_trigger.py index 0c7c99bc521..323394e9fe3 100644 --- a/tests/components/cover/test_device_trigger.py +++ b/tests/components/cover/test_device_trigger.py @@ -10,6 +10,7 @@ from homeassistant.components.cover import ( SUPPORT_SET_POSITION, SUPPORT_SET_TILT_POSITION, ) +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.const import ( CONF_PLATFORM, STATE_CLOSED, @@ -125,7 +126,9 @@ async def test_get_triggers( } for trigger in expected_trigger_types ] - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert_lists_same(triggers, expected_triggers) @@ -149,7 +152,9 @@ async def test_get_trigger_capabilities( assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert len(triggers) == 4 for trigger in triggers: capabilities = await async_get_device_automation_capabilities( @@ -202,7 +207,9 @@ async def test_get_trigger_capabilities_set_pos( }, ] } - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert len(triggers) == 5 for trigger in triggers: capabilities = await async_get_device_automation_capabilities( @@ -262,7 +269,9 @@ async def test_get_trigger_capabilities_set_tilt_pos( }, ] } - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert len(triggers) == 5 for trigger in triggers: capabilities = await async_get_device_automation_capabilities( diff --git a/tests/components/deconz/test_device_trigger.py b/tests/components/deconz/test_device_trigger.py index 265aec78270..15e63a6a81f 100644 --- a/tests/components/deconz/test_device_trigger.py +++ b/tests/components/deconz/test_device_trigger.py @@ -8,6 +8,7 @@ from homeassistant.components.automation import DOMAIN as AUTOMATION_DOMAIN from homeassistant.components.deconz import device_trigger from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN from homeassistant.components.deconz.device_trigger import CONF_SUBTYPE +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( ATTR_BATTERY_LEVEL, @@ -69,7 +70,9 @@ async def test_get_triggers(hass, aioclient_mock): identifiers={(DECONZ_DOMAIN, "d0:cf:5e:ff:fe:71:a4:3a")} ) - triggers = await async_get_device_automations(hass, "trigger", device.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device.id + ) expected_triggers = [ { @@ -158,7 +161,9 @@ async def test_get_triggers_manage_unsupported_remotes(hass, aioclient_mock): identifiers={(DECONZ_DOMAIN, "d0:cf:5e:ff:fe:71:a4:3a")} ) - triggers = await async_get_device_automations(hass, "trigger", device.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device.id + ) expected_triggers = [] diff --git a/tests/components/device_tracker/test_device_condition.py b/tests/components/device_tracker/test_device_condition.py index 7e3f79712c4..070d2b6fec3 100644 --- a/tests/components/device_tracker/test_device_condition.py +++ b/tests/components/device_tracker/test_device_condition.py @@ -2,6 +2,7 @@ import pytest import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.device_tracker import DOMAIN from homeassistant.const import STATE_HOME from homeassistant.helpers import device_registry @@ -61,7 +62,9 @@ async def test_get_conditions(hass, device_reg, entity_reg): "entity_id": f"{DOMAIN}.test_5678", }, ] - conditions = await async_get_device_automations(hass, "condition", device_entry.id) + conditions = await async_get_device_automations( + hass, DeviceAutomationType.CONDITION, device_entry.id + ) assert_lists_same(conditions, expected_conditions) diff --git a/tests/components/device_tracker/test_device_trigger.py b/tests/components/device_tracker/test_device_trigger.py index 0ec1ee67d36..dc9a5fe80fe 100644 --- a/tests/components/device_tracker/test_device_trigger.py +++ b/tests/components/device_tracker/test_device_trigger.py @@ -3,6 +3,7 @@ import pytest import voluptuous_serialize import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.device_tracker import DOMAIN, device_trigger import homeassistant.components.zone as zone from homeassistant.helpers import config_validation as cv, device_registry @@ -87,7 +88,9 @@ async def test_get_triggers(hass, device_reg, entity_reg): "entity_id": f"{DOMAIN}.test_5678", }, ] - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert_lists_same(triggers, expected_triggers) diff --git a/tests/components/fan/test_device_action.py b/tests/components/fan/test_device_action.py index 491e6afab6a..eb31a2ece42 100644 --- a/tests/components/fan/test_device_action.py +++ b/tests/components/fan/test_device_action.py @@ -2,6 +2,7 @@ import pytest import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.fan import DOMAIN from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component @@ -52,7 +53,9 @@ async def test_get_actions(hass, device_reg, entity_reg): "entity_id": "fan.test_5678", }, ] - actions = await async_get_device_automations(hass, "action", device_entry.id) + actions = await async_get_device_automations( + hass, DeviceAutomationType.ACTION, device_entry.id + ) assert_lists_same(actions, expected_actions) diff --git a/tests/components/fan/test_device_condition.py b/tests/components/fan/test_device_condition.py index 8f11d963ed3..d8e60e7d6ca 100644 --- a/tests/components/fan/test_device_condition.py +++ b/tests/components/fan/test_device_condition.py @@ -2,6 +2,7 @@ import pytest import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.fan import DOMAIN from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers import device_registry @@ -61,7 +62,9 @@ async def test_get_conditions(hass, device_reg, entity_reg): "entity_id": f"{DOMAIN}.test_5678", }, ] - conditions = await async_get_device_automations(hass, "condition", device_entry.id) + conditions = await async_get_device_automations( + hass, DeviceAutomationType.CONDITION, device_entry.id + ) assert_lists_same(conditions, expected_conditions) diff --git a/tests/components/fan/test_device_trigger.py b/tests/components/fan/test_device_trigger.py index cecb4151c3f..c58b9004cde 100644 --- a/tests/components/fan/test_device_trigger.py +++ b/tests/components/fan/test_device_trigger.py @@ -4,6 +4,7 @@ from datetime import timedelta import pytest import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.fan import DOMAIN from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers import device_registry @@ -66,7 +67,9 @@ async def test_get_triggers(hass, device_reg, entity_reg): "entity_id": f"{DOMAIN}.test_5678", }, ] - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert_lists_same(triggers, expected_triggers) @@ -84,7 +87,9 @@ async def test_get_trigger_capabilities(hass, device_reg, entity_reg): {"name": "for", "optional": True, "type": "positive_time_period_dict"} ] } - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) for trigger in triggers: capabilities = await async_get_device_automation_capabilities( hass, "trigger", trigger From f913961d63287e4cbac62b86be73061b7bbbe25c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 20 Dec 2021 23:29:22 +0200 Subject: [PATCH 0856/2644] Use DeviceAutomationType in tests/components/[m-r]* (#62443) --- .../media_player/test_device_condition.py | 5 +- .../media_player/test_device_trigger.py | 9 ++- .../mobile_app/test_device_action.py | 6 +- tests/components/mqtt/test_device_trigger.py | 57 ++++++++++++++----- tests/components/mqtt/test_tag.py | 9 ++- tests/components/nest/test_device_trigger.py | 21 +++++-- .../components/netatmo/test_device_trigger.py | 3 +- tests/components/number/test_device_action.py | 9 ++- .../philips_js/test_device_trigger.py | 5 +- tests/components/remote/test_device_action.py | 5 +- .../remote/test_device_condition.py | 11 +++- .../components/remote/test_device_trigger.py | 11 +++- tests/components/rfxtrx/test_device_action.py | 5 +- .../components/rfxtrx/test_device_trigger.py | 5 +- 14 files changed, 121 insertions(+), 40 deletions(-) diff --git a/tests/components/media_player/test_device_condition.py b/tests/components/media_player/test_device_condition.py index 63cdb1c55a7..60e1ae7d19b 100644 --- a/tests/components/media_player/test_device_condition.py +++ b/tests/components/media_player/test_device_condition.py @@ -2,6 +2,7 @@ import pytest import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.media_player import DOMAIN from homeassistant.const import ( STATE_IDLE, @@ -88,7 +89,9 @@ async def test_get_conditions(hass, device_reg, entity_reg): "entity_id": f"{DOMAIN}.test_5678", }, ] - conditions = await async_get_device_automations(hass, "condition", device_entry.id) + conditions = await async_get_device_automations( + hass, DeviceAutomationType.CONDITION, device_entry.id + ) assert_lists_same(conditions, expected_conditions) diff --git a/tests/components/media_player/test_device_trigger.py b/tests/components/media_player/test_device_trigger.py index 2e3641242f9..6440430e2d2 100644 --- a/tests/components/media_player/test_device_trigger.py +++ b/tests/components/media_player/test_device_trigger.py @@ -4,6 +4,7 @@ from datetime import timedelta import pytest import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.media_player import DOMAIN from homeassistant.const import ( STATE_IDLE, @@ -68,7 +69,9 @@ async def test_get_triggers(hass, device_reg, entity_reg): } for trigger in trigger_types ] - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert_lists_same(triggers, expected_triggers) @@ -82,7 +85,9 @@ async def test_get_trigger_capabilities(hass, device_reg, entity_reg): ) entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert len(triggers) == 5 for trigger in triggers: capabilities = await async_get_device_automation_capabilities( diff --git a/tests/components/mobile_app/test_device_action.py b/tests/components/mobile_app/test_device_action.py index f7846ecc377..dd4d2a55d82 100644 --- a/tests/components/mobile_app/test_device_action.py +++ b/tests/components/mobile_app/test_device_action.py @@ -11,9 +11,9 @@ async def test_get_actions(hass, push_registration): webhook_id = push_registration["webhook_id"] device_id = hass.data[DOMAIN][DATA_DEVICES][webhook_id].id - assert await async_get_device_automations(hass, "action", device_id) == [ - {"domain": DOMAIN, "device_id": device_id, "type": "notify"} - ] + assert await async_get_device_automations( + hass, device_automation.DeviceAutomationType.ACTION, device_id + ) == [{"domain": DOMAIN, "device_id": device_id, "type": "notify"}] capabilitites = await device_automation._async_get_device_automation_capabilities( hass, diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py index c3b16e3de43..a5359563d92 100644 --- a/tests/components/mqtt/test_device_trigger.py +++ b/tests/components/mqtt/test_device_trigger.py @@ -4,6 +4,7 @@ import json import pytest import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.mqtt import _LOGGER, DOMAIN, debug_info from homeassistant.helpers import device_registry as dr from homeassistant.helpers.trigger import async_initialize_triggers @@ -62,7 +63,9 @@ async def test_get_triggers(hass, device_reg, entity_reg, mqtt_mock): "subtype": "button_1", }, ] - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert_lists_same(triggers, expected_triggers) @@ -102,7 +105,9 @@ async def test_get_unknown_triggers(hass, device_reg, entity_reg, mqtt_mock): }, ) - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert_lists_same(triggers, []) @@ -118,7 +123,9 @@ async def test_get_non_existing_triggers(hass, device_reg, entity_reg, mqtt_mock await hass.async_block_till_done() device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert_lists_same(triggers, []) @@ -161,7 +168,9 @@ async def test_discover_bad_triggers(hass, device_reg, entity_reg, mqtt_mock): "subtype": "button_1", }, ] - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert_lists_same(triggers, expected_triggers) @@ -206,14 +215,18 @@ async def test_update_remove_triggers(hass, device_reg, entity_reg, mqtt_mock): expected_triggers2 = [dict(expected_triggers1[0])] expected_triggers2[0]["subtype"] = "button_2" - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert_lists_same(triggers, expected_triggers1) # Update trigger async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", data2) await hass.async_block_till_done() - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert_lists_same(triggers, expected_triggers2) # Remove trigger @@ -972,7 +985,9 @@ async def test_cleanup_trigger(hass, device_reg, entity_reg, mqtt_mock): device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert triggers[0]["type"] == "foo" device_reg.async_remove_device(device_entry.id) @@ -1007,7 +1022,9 @@ async def test_cleanup_device(hass, device_reg, entity_reg, mqtt_mock): device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert triggers[0]["type"] == "foo" async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", "") @@ -1047,7 +1064,9 @@ async def test_cleanup_device_several_triggers(hass, device_reg, entity_reg, mqt device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert len(triggers) == 2 assert triggers[0]["type"] == "foo" assert triggers[1]["type"] == "foo2" @@ -1059,7 +1078,9 @@ async def test_cleanup_device_several_triggers(hass, device_reg, entity_reg, mqt device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert len(triggers) == 1 assert triggers[0]["type"] == "foo2" @@ -1102,7 +1123,9 @@ async def test_cleanup_device_with_entity1(hass, device_reg, entity_reg, mqtt_mo device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert len(triggers) == 3 # 2 binary_sensor triggers + device trigger async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", "") @@ -1112,7 +1135,9 @@ async def test_cleanup_device_with_entity1(hass, device_reg, entity_reg, mqtt_mo device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert len(triggers) == 2 # 2 binary_sensor triggers async_fire_mqtt_message(hass, "homeassistant/binary_sensor/bla2/config", "") @@ -1154,7 +1179,9 @@ async def test_cleanup_device_with_entity2(hass, device_reg, entity_reg, mqtt_mo device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert len(triggers) == 3 # 2 binary_sensor triggers + device trigger async_fire_mqtt_message(hass, "homeassistant/binary_sensor/bla2/config", "") @@ -1164,7 +1191,9 @@ async def test_cleanup_device_with_entity2(hass, device_reg, entity_reg, mqtt_mo device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert len(triggers) == 1 # device trigger async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", "") diff --git a/tests/components/mqtt/test_tag.py b/tests/components/mqtt/test_tag.py index dfc0bddeec4..e1f3de83a0d 100644 --- a/tests/components/mqtt/test_tag.py +++ b/tests/components/mqtt/test_tag.py @@ -5,6 +5,7 @@ from unittest.mock import ANY, patch import pytest +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.helpers import device_registry as dr from tests.common import ( @@ -609,7 +610,9 @@ async def test_cleanup_device_with_entity_and_trigger_1( device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert len(triggers) == 3 # 2 binary_sensor triggers + device trigger async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", "") @@ -669,7 +672,9 @@ async def test_cleanup_device_with_entity2(hass, device_reg, entity_reg, mqtt_mo device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert len(triggers) == 3 # 2 binary_sensor triggers + device trigger async_fire_mqtt_message(hass, "homeassistant/device_automation/bla2/config", "") diff --git a/tests/components/nest/test_device_trigger.py b/tests/components/nest/test_device_trigger.py index c9dc9992410..69475fe1437 100644 --- a/tests/components/nest/test_device_trigger.py +++ b/tests/components/nest/test_device_trigger.py @@ -4,6 +4,7 @@ from google_nest_sdm.event import EventMessage import pytest import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) @@ -119,7 +120,9 @@ async def test_get_triggers(hass): "device_id": device_entry.id, }, ] - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert_lists_same(triggers, expected_triggers) @@ -147,7 +150,9 @@ async def test_multiple_devices(hass): entry2 = registry.async_get("camera.camera_2") assert entry2.unique_id == "device-id-2-camera" - triggers = await async_get_device_automations(hass, "trigger", entry1.device_id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, entry1.device_id + ) assert len(triggers) == 1 assert triggers[0] == { "platform": "device", @@ -156,7 +161,9 @@ async def test_multiple_devices(hass): "device_id": entry1.device_id, } - triggers = await async_get_device_automations(hass, "trigger", entry2.device_id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, entry2.device_id + ) assert len(triggers) == 1 assert triggers[0] == { "platform": "device", @@ -191,7 +198,9 @@ async def test_triggers_for_invalid_device_id(hass): assert device_entry_2 is not None with pytest.raises(InvalidDeviceAutomationConfig): - await async_get_device_automations(hass, "trigger", device_entry_2.id) + await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry_2.id + ) async def test_no_triggers(hass): @@ -203,7 +212,9 @@ async def test_no_triggers(hass): entry = registry.async_get("camera.my_camera") assert entry.unique_id == "some-device-id-camera" - triggers = await async_get_device_automations(hass, "trigger", entry.device_id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, entry.device_id + ) assert triggers == [] diff --git a/tests/components/netatmo/test_device_trigger.py b/tests/components/netatmo/test_device_trigger.py index 7e014d2648f..bbd4dd1e5ec 100644 --- a/tests/components/netatmo/test_device_trigger.py +++ b/tests/components/netatmo/test_device_trigger.py @@ -2,6 +2,7 @@ import pytest import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.netatmo import DOMAIN as NETATMO_DOMAIN from homeassistant.components.netatmo.const import ( CLIMATE_TRIGGERS, @@ -98,7 +99,7 @@ async def test_get_triggers( triggers = [ trigger for trigger in await async_get_device_automations( - hass, "trigger", device_entry.id + hass, DeviceAutomationType.TRIGGER, device_entry.id ) if trigger["domain"] == NETATMO_DOMAIN ] diff --git a/tests/components/number/test_device_action.py b/tests/components/number/test_device_action.py index a981a331e7e..806bd8771b2 100644 --- a/tests/components/number/test_device_action.py +++ b/tests/components/number/test_device_action.py @@ -3,6 +3,7 @@ import pytest import voluptuous_serialize import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.number import DOMAIN, device_action from homeassistant.helpers import config_validation as cv, device_registry from homeassistant.setup import async_setup_component @@ -48,7 +49,9 @@ async def test_get_actions(hass, device_reg, entity_reg): "entity_id": "number.test_5678", }, ] - actions = await async_get_device_automations(hass, "action", device_entry.id) + actions = await async_get_device_automations( + hass, DeviceAutomationType.ACTION, device_entry.id + ) assert_lists_same(actions, expected_actions) @@ -69,7 +72,9 @@ async def test_get_action_no_state(hass, device_reg, entity_reg): "entity_id": "number.test_5678", }, ] - actions = await async_get_device_automations(hass, "action", device_entry.id) + actions = await async_get_device_automations( + hass, DeviceAutomationType.ACTION, device_entry.id + ) assert_lists_same(actions, expected_actions) diff --git a/tests/components/philips_js/test_device_trigger.py b/tests/components/philips_js/test_device_trigger.py index 936dd65f115..9980bf5627d 100644 --- a/tests/components/philips_js/test_device_trigger.py +++ b/tests/components/philips_js/test_device_trigger.py @@ -2,6 +2,7 @@ import pytest import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.philips_js.const import DOMAIN from homeassistant.setup import async_setup_component @@ -29,7 +30,9 @@ async def test_get_triggers(hass, mock_device): "device_id": mock_device.id, }, ] - triggers = await async_get_device_automations(hass, "trigger", mock_device.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, mock_device.id + ) assert_lists_same(triggers, expected_triggers) diff --git a/tests/components/remote/test_device_action.py b/tests/components/remote/test_device_action.py index 48e741a12a4..9c113c26578 100644 --- a/tests/components/remote/test_device_action.py +++ b/tests/components/remote/test_device_action.py @@ -2,6 +2,7 @@ import pytest import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.remote import DOMAIN from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON from homeassistant.helpers import device_registry @@ -64,7 +65,9 @@ async def test_get_actions(hass, device_reg, entity_reg): "entity_id": f"{DOMAIN}.test_5678", }, ] - actions = await async_get_device_automations(hass, "action", device_entry.id) + actions = await async_get_device_automations( + hass, DeviceAutomationType.ACTION, device_entry.id + ) assert actions == expected_actions diff --git a/tests/components/remote/test_device_condition.py b/tests/components/remote/test_device_condition.py index 6f3c0e1c0a2..e3ed3059d8b 100644 --- a/tests/components/remote/test_device_condition.py +++ b/tests/components/remote/test_device_condition.py @@ -5,6 +5,7 @@ from unittest.mock import patch import pytest import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.remote import DOMAIN from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON from homeassistant.helpers import device_registry @@ -65,7 +66,9 @@ async def test_get_conditions(hass, device_reg, entity_reg): "entity_id": f"{DOMAIN}.test_5678", }, ] - conditions = await async_get_device_automations(hass, "condition", device_entry.id) + conditions = await async_get_device_automations( + hass, DeviceAutomationType.CONDITION, device_entry.id + ) assert conditions == expected_conditions @@ -83,10 +86,12 @@ async def test_get_condition_capabilities(hass, device_reg, entity_reg): {"name": "for", "optional": True, "type": "positive_time_period_dict"} ] } - conditions = await async_get_device_automations(hass, "condition", device_entry.id) + conditions = await async_get_device_automations( + hass, DeviceAutomationType.CONDITION, device_entry.id + ) for condition in conditions: capabilities = await async_get_device_automation_capabilities( - hass, "condition", condition + hass, DeviceAutomationType.CONDITION, condition ) assert capabilities == expected_capabilities diff --git a/tests/components/remote/test_device_trigger.py b/tests/components/remote/test_device_trigger.py index 3afa731cf59..00c444b232e 100644 --- a/tests/components/remote/test_device_trigger.py +++ b/tests/components/remote/test_device_trigger.py @@ -4,6 +4,7 @@ from datetime import timedelta import pytest import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.remote import DOMAIN from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON from homeassistant.helpers import device_registry @@ -65,7 +66,9 @@ async def test_get_triggers(hass, device_reg, entity_reg): "entity_id": f"{DOMAIN}.test_5678", }, ] - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert triggers == expected_triggers @@ -83,10 +86,12 @@ async def test_get_trigger_capabilities(hass, device_reg, entity_reg): {"name": "for", "optional": True, "type": "positive_time_period_dict"} ] } - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) for trigger in triggers: capabilities = await async_get_device_automation_capabilities( - hass, "trigger", trigger + hass, DeviceAutomationType.TRIGGER, trigger ) assert capabilities == expected_capabilities diff --git a/tests/components/rfxtrx/test_device_action.py b/tests/components/rfxtrx/test_device_action.py index 6714bfdaea9..54f6430fc1d 100644 --- a/tests/components/rfxtrx/test_device_action.py +++ b/tests/components/rfxtrx/test_device_action.py @@ -7,6 +7,7 @@ import RFXtrx import pytest import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.rfxtrx import DOMAIN from homeassistant.helpers.device_registry import DeviceRegistry from homeassistant.setup import async_setup_component @@ -98,7 +99,9 @@ async def test_get_actions(hass, device_reg: DeviceRegistry, device, expected): device_entry = device_reg.async_get_device(device.device_identifiers, set()) assert device_entry - actions = await async_get_device_automations(hass, "action", device_entry.id) + actions = await async_get_device_automations( + hass, DeviceAutomationType.ACTION, device_entry.id + ) actions = [action for action in actions if action["domain"] == DOMAIN] expected_actions = [ diff --git a/tests/components/rfxtrx/test_device_trigger.py b/tests/components/rfxtrx/test_device_trigger.py index 90be97bd56f..eee437d495c 100644 --- a/tests/components/rfxtrx/test_device_trigger.py +++ b/tests/components/rfxtrx/test_device_trigger.py @@ -6,6 +6,7 @@ from typing import Any, NamedTuple import pytest import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.rfxtrx import DOMAIN from homeassistant.helpers.device_registry import DeviceRegistry from homeassistant.setup import async_setup_component @@ -93,7 +94,9 @@ async def test_get_triggers(hass, device_reg, event: EventTestData, expected): for expect in expected ] - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) triggers = [value for value in triggers if value["domain"] == "rfxtrx"] assert_lists_same(triggers, expected_triggers) From b483754ad3713d1033c88d7f5031850b65d310b7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Dec 2021 22:30:22 +0100 Subject: [PATCH 0857/2644] Use new enums in sma (#62439) Co-authored-by: epenet --- homeassistant/components/sma/sensor.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sma/sensor.py b/homeassistant/components/sma/sensor.py index 7fa84f25bd2..429667ff0e4 100644 --- a/homeassistant/components/sma/sensor.py +++ b/homeassistant/components/sma/sensor.py @@ -9,9 +9,9 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntity, + SensorStateClass, ) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( @@ -21,8 +21,6 @@ from homeassistant.const import ( CONF_SENSORS, CONF_SSL, CONF_VERIFY_SSL, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR, POWER_WATT, ) @@ -167,11 +165,11 @@ class SMAsensor(CoordinatorEntity, SensorEntity): self._attr_device_info = device_info if self.native_unit_of_measurement == ENERGY_KILO_WATT_HOUR: - self._attr_state_class = STATE_CLASS_TOTAL_INCREASING - self._attr_device_class = DEVICE_CLASS_ENERGY + self._attr_state_class = SensorStateClass.TOTAL_INCREASING + self._attr_device_class = SensorDeviceClass.ENERGY if self.native_unit_of_measurement == POWER_WATT: - self._attr_state_class = STATE_CLASS_MEASUREMENT - self._attr_device_class = DEVICE_CLASS_POWER + self._attr_state_class = SensorStateClass.MEASUREMENT + self._attr_device_class = SensorDeviceClass.POWER # Set sensor enabled to False. # Will be enabled by async_added_to_hass if actually used. From d22012ac43267658d233cfba08f35d029b2bf6a5 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 20 Dec 2021 22:45:38 +0100 Subject: [PATCH 0858/2644] bump aiohue to 3.0.7 (#62444) --- homeassistant/components/hue/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index 7003a3a8ccf..ba2d97f44a5 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==3.0.6"], + "requirements": ["aiohue==3.0.7"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 1a1cbe487f1..1a3574dc971 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -192,7 +192,7 @@ aiohomekit==0.6.4 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.6 +aiohue==3.0.7 # homeassistant.components.imap aioimaplib==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f0f1a4cd54e..f117452b773 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -137,7 +137,7 @@ aiohomekit==0.6.4 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.6 +aiohue==3.0.7 # homeassistant.components.apache_kafka aiokafka==0.6.0 From aaac1d4c5a6bb238b187adea3d09d76ec2cfb331 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Mon, 20 Dec 2021 23:25:26 +0100 Subject: [PATCH 0859/2644] Bump bimmer_connected to 0.8.7 (#62435) Co-authored-by: rikroe --- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index fc641548aff..63046d9d441 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -2,7 +2,7 @@ "domain": "bmw_connected_drive", "name": "BMW Connected Drive", "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", - "requirements": ["bimmer_connected==0.8.5"], + "requirements": ["bimmer_connected==0.8.7"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 1a3574dc971..3939bfa9f68 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -390,7 +390,7 @@ beautifulsoup4==4.10.0 bellows==0.29.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.8.5 +bimmer_connected==0.8.7 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f117452b773..a20c95e1603 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -260,7 +260,7 @@ base36==0.1.1 bellows==0.29.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.8.5 +bimmer_connected==0.8.7 # homeassistant.components.blebox blebox_uniapi==1.3.3 From 92454e3ac87799ed4b19cbdb23ab8738ec587210 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 21 Dec 2021 01:09:14 +0100 Subject: [PATCH 0860/2644] Change Hue availability blacklist logic a bit (#62446) --- homeassistant/components/hue/v2/entity.py | 63 ++++++++++++++++++----- 1 file changed, 49 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/hue/v2/entity.py b/homeassistant/components/hue/v2/entity.py index ae345238c23..7371efff3bb 100644 --- a/homeassistant/components/hue/v2/entity.py +++ b/homeassistant/components/hue/v2/entity.py @@ -47,20 +47,9 @@ class HueBaseEntity(Entity): self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self.device.id)}, ) - # some (3th party) Hue lights report their connection status incorrectly - # causing the zigbee availability to report as disconnected while in fact - # it can be controlled. Although this is in fact something the device manufacturer - # should fix, we work around it here. If the light is reported unavailable at - # startup, we ignore the availability status of the zigbee connection - self._ignore_availability = False - if self.device is None: - return - if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id): - self._ignore_availability = ( - # Official Hue lights are reliable - self.device.product_data.manufacturer_name != "Signify Netherlands B.V." - and zigbee.status != ConnectivityServiceStatus.CONNECTED - ) + # used for availability workaround + self._ignore_availability = None + self._last_state = None @property def name(self) -> str: @@ -82,6 +71,7 @@ class HueBaseEntity(Entity): async def async_added_to_hass(self) -> None: """Call when entity is added.""" + self._check_availability_workaround() # Add value_changed callbacks. self.async_on_remove( self.controller.subscribe( @@ -140,5 +130,50 @@ class HueBaseEntity(Entity): ent_reg.async_remove(self.entity_id) else: self.logger.debug("Received status update for %s", self.entity_id) + self._check_availability_workaround() self.on_update() self.async_write_ha_state() + + @callback + def _check_availability_workaround(self): + """Check availability of the device.""" + if self.resource.type != ResourceTypes.LIGHT: + return + if self._ignore_availability is not None: + # already processed + return + cur_state = self.resource.on.on + if self._last_state is None: + self._last_state = cur_state + return + # some (3th party) Hue lights report their connection status incorrectly + # causing the zigbee availability to report as disconnected while in fact + # it can be controlled. Although this is in fact something the device manufacturer + # should fix, we work around it here. If the light is reported unavailable + # by the zigbee connectivity but the state changesm its considered as a + # malfunctioning device and we report it. + # while the user should actually fix this issue instead of ignoring it, we + # ignore the availability for this light from this point. + if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id): + if ( + self._last_state != cur_state + and zigbee.status != ConnectivityServiceStatus.CONNECTED + ): + # the device state changed from on->off or off->on + # while it was reported as not connected! + self.logger.warning( + "Light %s changed state while reported as disconnected. " + "This is an indicator that routing is not working properly for this device. " + "Home Assistant will ignore availability for this light from now on. " + "Device details: %s - %s (%s) fw: %s", + self.name, + self.device.product_data.manufacturer_name, + self.device.product_data.product_name, + self.device.product_data.model_id, + self.device.product_data.software_version, + ) + # do we want to store this in some persistent storage? + self._ignore_availability = True + else: + self._ignore_availability = False + self._last_state = cur_state From babd7536107b95a67d73303ea8025ebfd2b221cb Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 21 Dec 2021 00:15:06 +0000 Subject: [PATCH 0861/2644] [ci skip] Translation update --- .../components/abode/translations/it.json | 4 +- .../components/adax/translations/it.json | 4 +- .../components/airvisual/translations/it.json | 2 +- .../components/ambee/translations/it.json | 2 +- .../components/androidtv/translations/ca.json | 66 +++++++++++++++++++ .../components/androidtv/translations/et.json | 66 +++++++++++++++++++ .../components/androidtv/translations/id.json | 21 ++++++ .../components/androidtv/translations/no.json | 28 ++++++++ .../components/apple_tv/translations/it.json | 4 +- .../aseko_pool_live/translations/it.json | 2 +- .../components/august/translations/it.json | 2 +- .../components/awair/translations/it.json | 4 +- .../azure_devops/translations/it.json | 4 +- .../azure_event_hub/translations/fr.json | 11 ++++ .../azure_event_hub/translations/nl.json | 48 ++++++++++++++ .../azure_event_hub/translations/no.json | 7 ++ .../components/bosch_shc/translations/it.json | 2 +- .../components/brunt/translations/it.json | 2 +- .../components/cloud/translations/it.json | 4 +- .../cloudflare/translations/it.json | 2 +- .../components/coinbase/translations/it.json | 2 +- .../crownstone/translations/it.json | 8 +-- .../components/daikin/translations/it.json | 4 +- .../devolo_home_control/translations/it.json | 6 +- .../components/econet/translations/it.json | 2 +- .../components/elmax/translations/it.json | 4 +- .../components/elmax/translations/no.json | 2 +- .../components/esphome/translations/it.json | 4 +- .../evil_genius_labs/translations/ca.json | 1 + .../evil_genius_labs/translations/de.json | 1 + .../evil_genius_labs/translations/en.json | 1 + .../evil_genius_labs/translations/et.json | 1 + .../evil_genius_labs/translations/hu.json | 1 + .../evil_genius_labs/translations/it.json | 1 + .../evil_genius_labs/translations/ja.json | 1 + .../evil_genius_labs/translations/nl.json | 1 + .../evil_genius_labs/translations/ru.json | 1 + .../translations/zh-Hant.json | 1 + .../fireservicerota/translations/it.json | 2 +- .../flick_electric/translations/it.json | 2 +- .../components/flipr/translations/it.json | 2 +- .../components/flume/translations/it.json | 2 +- .../components/flux_led/translations/it.json | 2 +- .../components/hangouts/translations/it.json | 4 +- .../components/hue/translations/it.json | 2 +- .../components/icloud/translations/it.json | 4 +- .../components/knx/translations/nl.json | 2 + .../components/lcn/translations/it.json | 2 +- .../components/lyric/translations/it.json | 2 +- .../components/mazda/translations/it.json | 4 +- .../components/melcloud/translations/it.json | 4 +- .../components/mqtt/translations/it.json | 6 +- .../components/myq/translations/it.json | 2 +- .../components/nam/translations/it.json | 2 +- .../components/nest/translations/it.json | 2 +- .../components/netatmo/translations/it.json | 4 +- .../nfandroidtv/translations/it.json | 2 +- .../components/notion/translations/it.json | 2 +- .../components/nuki/translations/it.json | 2 +- .../components/nws/translations/it.json | 2 +- .../components/octoprint/translations/no.json | 2 +- .../open_meteo/translations/fr.json | 11 ++++ .../open_meteo/translations/it.json | 12 ++++ .../open_meteo/translations/nl.json | 12 ++++ .../ovo_energy/translations/it.json | 4 +- .../plum_lightpad/translations/it.json | 2 +- .../components/poolsense/translations/it.json | 2 +- .../components/renault/translations/it.json | 4 +- .../components/renault/translations/no.json | 2 +- .../components/rfxtrx/translations/it.json | 2 +- .../components/ridwell/translations/it.json | 2 +- .../translations/it.json | 2 +- .../components/sense/translations/it.json | 2 +- .../simplisafe/translations/it.json | 8 +-- .../components/smarthab/translations/it.json | 2 +- .../smartthings/translations/it.json | 2 +- .../components/smarttub/translations/it.json | 6 +- .../components/sonarr/translations/it.json | 2 +- .../components/spotify/translations/it.json | 4 +- .../srp_energy/translations/it.json | 2 +- .../synology_dsm/translations/it.json | 4 +- .../components/tile/translations/ca.json | 9 ++- .../components/tile/translations/id.json | 8 ++- .../components/tile/translations/it.json | 2 +- .../components/tile/translations/no.json | 8 ++- .../components/tolo/translations/it.json | 2 +- .../totalconnect/translations/it.json | 2 +- .../components/tractive/translations/it.json | 2 +- .../components/tradfri/translations/it.json | 2 +- .../uptimerobot/translations/it.json | 2 +- .../components/verisure/translations/it.json | 4 +- .../components/vesync/translations/it.json | 2 +- .../components/vicare/translations/bg.json | 23 +++++++ .../components/vicare/translations/fr.json | 21 ++++++ .../components/vicare/translations/id.json | 23 +++++++ .../components/vicare/translations/it.json | 26 ++++++++ .../components/vicare/translations/nl.json | 26 ++++++++ .../components/vicare/translations/no.json | 16 +++++ .../components/vizio/translations/it.json | 2 +- .../components/wallbox/translations/it.json | 2 +- .../components/watttime/translations/it.json | 6 +- .../components/withings/translations/it.json | 2 +- .../xiaomi_miio/translations/it.json | 8 +-- 103 files changed, 560 insertions(+), 112 deletions(-) create mode 100644 homeassistant/components/androidtv/translations/ca.json create mode 100644 homeassistant/components/androidtv/translations/et.json create mode 100644 homeassistant/components/androidtv/translations/id.json create mode 100644 homeassistant/components/androidtv/translations/no.json create mode 100644 homeassistant/components/azure_event_hub/translations/fr.json create mode 100644 homeassistant/components/azure_event_hub/translations/nl.json create mode 100644 homeassistant/components/azure_event_hub/translations/no.json create mode 100644 homeassistant/components/open_meteo/translations/fr.json create mode 100644 homeassistant/components/open_meteo/translations/it.json create mode 100644 homeassistant/components/open_meteo/translations/nl.json create mode 100644 homeassistant/components/vicare/translations/bg.json create mode 100644 homeassistant/components/vicare/translations/fr.json create mode 100644 homeassistant/components/vicare/translations/id.json create mode 100644 homeassistant/components/vicare/translations/it.json create mode 100644 homeassistant/components/vicare/translations/nl.json create mode 100644 homeassistant/components/vicare/translations/no.json diff --git a/homeassistant/components/abode/translations/it.json b/homeassistant/components/abode/translations/it.json index 6cb571df8e5..76e8bec17be 100644 --- a/homeassistant/components/abode/translations/it.json +++ b/homeassistant/components/abode/translations/it.json @@ -19,14 +19,14 @@ "reauth_confirm": { "data": { "password": "Password", - "username": "E-mail" + "username": "Email" }, "title": "Inserisci le tue informazioni di accesso Abode" }, "user": { "data": { "password": "Password", - "username": "E-mail" + "username": "Email" }, "title": "Inserisci le tue informazioni di accesso Abode" } diff --git a/homeassistant/components/adax/translations/it.json b/homeassistant/components/adax/translations/it.json index 6af9bf7743c..0ec4566e73b 100644 --- a/homeassistant/components/adax/translations/it.json +++ b/homeassistant/components/adax/translations/it.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", - "heater_not_available": "Riscaldatore non disponibile. Prova a resettare il riscaldatore premendo + e OK per alcuni secondi.", + "heater_not_available": "Riscaldatore non disponibile. Prova a ripristinare il riscaldatore premendo + e OK per alcuni secondi.", "heater_not_found": "Riscaldatore non trovato. Prova ad avvicinare il riscaldatore al computer Home Assistant.", "invalid_auth": "Autenticazione non valida" }, @@ -22,7 +22,7 @@ "wifi_pswd": "Password Wi-Fi", "wifi_ssid": "SSID Wi-Fi" }, - "description": "Ripristinare il riscaldatore premendo + e OK finch\u00e9 il display non mostra 'Reset'. Quindi premere e tenere premuto il pulsante OK sul riscaldatore fino a quando il led blu inizia a lampeggiare prima di premere Invia. La configurazione del riscaldatore potrebbe richiedere alcuni minuti." + "description": "Ripristina il riscaldatore premendo + e OK finch\u00e9 il display non mostra 'Reset'. Quindi premere e tenere premuto il pulsante OK sul riscaldatore fino a quando il led blu inizia a lampeggiare prima di premere Invia. La configurazione del riscaldatore potrebbe richiedere alcuni minuti." }, "user": { "data": { diff --git a/homeassistant/components/airvisual/translations/it.json b/homeassistant/components/airvisual/translations/it.json index 6201c6f19d9..581a1256c15 100644 --- a/homeassistant/components/airvisual/translations/it.json +++ b/homeassistant/components/airvisual/translations/it.json @@ -42,7 +42,7 @@ "data": { "api_key": "Chiave API" }, - "title": "Riautenticare AirVisual" + "title": "Autentica nuovamente AirVisual" }, "user": { "description": "Scegliere il tipo di dati AirVisual che si desidera monitorare.", diff --git a/homeassistant/components/ambee/translations/it.json b/homeassistant/components/ambee/translations/it.json index 178e52a979f..fe97ce33686 100644 --- a/homeassistant/components/ambee/translations/it.json +++ b/homeassistant/components/ambee/translations/it.json @@ -11,7 +11,7 @@ "reauth_confirm": { "data": { "api_key": "Chiave API", - "description": "Riautenticati con il tuo account Ambee." + "description": "Autenticati nuovamente con il tuo account Ambee." } }, "user": { diff --git a/homeassistant/components/androidtv/translations/ca.json b/homeassistant/components/androidtv/translations/ca.json new file mode 100644 index 00000000000..0e3e6263fbf --- /dev/null +++ b/homeassistant/components/androidtv/translations/ca.json @@ -0,0 +1,66 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "invalid_unique_id": "No s'ha pogut determinar cap identificador \u00fanic v\u00e0lid del dispositiu" + }, + "error": { + "adbkey_not_file": "No s'ha trobat el fitxer de clau ADB", + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_host": "Nom de l'amfitri\u00f3 o adre\u00e7a IP inv\u00e0lids", + "key_and_server": "Proporciona nom\u00e9s la clau ADB o el servidor ADB", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "adb_server_ip": "Adre\u00e7a IP del servidor ADB (deixeu-ho en blanc per no utilitzar-la)", + "adb_server_port": "Port del servidor ADB", + "adbkey": "Ruta al fitxer de clau ADB (deixa-ho en blanc per generar-la autom\u00e0ticament)", + "device_class": "Tipus de dispositiu", + "host": "Amfitri\u00f3", + "port": "Port" + }, + "description": "Estableix els par\u00e0metres necessaris per connectar al teu dispositiu Android TV", + "title": "Android TV" + } + } + }, + "options": { + "error": { + "invalid_det_rules": "Regles de detecci\u00f3 d'estat no v\u00e0lides" + }, + "step": { + "apps": { + "data": { + "app_delete": "Marca-ho per eliminar aquesta aplicaci\u00f3", + "app_id": "ID de l'aplicaci\u00f3", + "app_name": "Nom de l'aplicaci\u00f3" + }, + "description": "Configura l'identificador d'aplicaci\u00f3 {app_id}", + "title": "Configuraci\u00f3 d'aplicacions d'Android TV" + }, + "init": { + "data": { + "apps": "Configura la llista d'aplicacions", + "exclude_unnamed_apps": "Exclou aplicacions amb nom desconegut", + "get_sources": "Si s'han d'obtenir, o no, les aplicacions en execuci\u00f3 com a llista de fonts", + "screencap": "Determina si la imatge d'\u00e0lbum s'ha d'extreure del que es mostra en pantalla", + "state_detection_rules": "Configura les regles de detecci\u00f3 d'estat", + "turn_off_command": "Comanda de shell ADB per substituir la comanda predeterminada turn_off", + "turn_on_command": "Comanda de shell ADB per substituir la comanda predeterminada turn_on" + }, + "title": "Opcions d'Android TV" + }, + "rules": { + "data": { + "rule_delete": "Marca-ho per eliminar aquesta regla", + "rule_id": "ID de l'aplicaci\u00f3", + "rule_values": "Llista de regles de detecci\u00f3 d'estat (consulta la documentaci\u00f3)" + }, + "description": "Configura regla de detecci\u00f3 de l'identificador d'aplicaci\u00f3 {rule_id}", + "title": "Configuraci\u00f3 de regles de detecci\u00f3 d'estat d'Android TV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/androidtv/translations/et.json b/homeassistant/components/androidtv/translations/et.json new file mode 100644 index 00000000000..9800fdb3946 --- /dev/null +++ b/homeassistant/components/androidtv/translations/et.json @@ -0,0 +1,66 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "invalid_unique_id": "Seadme jaoks ei ole v\u00f5imalik kehtivat kordumatut ID-d tuvastada" + }, + "error": { + "adbkey_not_file": "ADB v\u00f5tmefaili ei leitud", + "cannot_connect": "\u00dchendamine nurjus", + "invalid_host": "Sobimatu hosti v\u00f5i IP aadress", + "key_and_server": "Esita ainult ADB-v\u00f5ti v\u00f5i ADB-server", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "adb_server_ip": "ADB-serveri IP-aadress (j\u00e4ta t\u00fchjaks kui ei kasuta)", + "adb_server_port": "ADB serveri port", + "adbkey": "ADB v\u00f5tmefaili tee (automaatse genereerimise korral j\u00e4ta t\u00fchjaks)", + "device_class": "Seadme t\u00fc\u00fcp", + "host": "Host", + "port": "Port" + }, + "description": "Seadista Android TV seadmega \u00fchenduse loomiseks vajalikud parameetrid", + "title": "Android TV" + } + } + }, + "options": { + "error": { + "invalid_det_rules": "Kehtetud olekutuvastusreeglid" + }, + "step": { + "apps": { + "data": { + "app_delete": "M\u00e4rgi selle rakenduse kustutamiseks", + "app_id": "Rakenduse ID", + "app_name": "Rakenduse nimi" + }, + "description": "Rakenduse ID {app_id} seadistamine", + "title": "Seadista Android TV rakendused" + }, + "init": { + "data": { + "apps": "Rakenduste loendi seadistamine", + "exclude_unnamed_apps": "V\u00e4lista tundmatu nimega rakendus", + "get_sources": "Kas tuua t\u00f6\u00f6tavad rakendused allikate loendina v\u00f5i mitte", + "screencap": "M\u00e4\u00e4rab kas albumi kujundus tuleks ekraanil kuvatavast pildist v\u00f5tta", + "state_detection_rules": "M\u00e4\u00e4ra oleku tuvastamise reeglid", + "turn_off_command": "ADB k\u00e4sk vaikimisi k\u00e4su turn_off t\u00fchistamiseks", + "turn_on_command": "ADB k\u00e4sk vaikimisi k\u00e4su turn_on t\u00fchistamiseks" + }, + "title": "Android TV suvandid" + }, + "rules": { + "data": { + "rule_delete": "M\u00e4rgi selle reegli kustutamiseks", + "rule_id": "Rakenduse ID", + "rule_values": "Oleku tuvastamise reeglite loend (vt dokumentatsiooni)" + }, + "description": "Seadista rakenduse ID {rule_id} tuvastusreegel.", + "title": "Seadista Android TV oleku tuvastamise reeglid" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/androidtv/translations/id.json b/homeassistant/components/androidtv/translations/id.json new file mode 100644 index 00000000000..d85048243ae --- /dev/null +++ b/homeassistant/components/androidtv/translations/id.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_host": "Nama host atau alamat IP tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "title": "Android TV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/androidtv/translations/no.json b/homeassistant/components/androidtv/translations/no.json new file mode 100644 index 00000000000..830ee10f756 --- /dev/null +++ b/homeassistant/components/androidtv/translations/no.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "invalid_unique_id": "Umulig \u00e5 bestemme en gyldig unik ID for enheten" + }, + "error": { + "adbkey_not_file": "Finner ikke ADB-n\u00f8kkelfil", + "cannot_connect": "Tilkobling mislyktes", + "invalid_host": "Ugyldig vertsnavn eller IP-adresse", + "key_and_server": "Oppgi kun ADB-n\u00f8kkel eller ADB-server", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "adb_server_ip": "IP-adressen til ADB-serveren (la den st\u00e5 tom for ikke \u00e5 bruke)", + "adb_server_port": "Port til ADB-serveren", + "adbkey": "Bane til ADB-n\u00f8kkelfilen (la den st\u00e5 tom for automatisk generering)", + "device_class": "Type enhet", + "host": "Vert", + "port": "Port" + }, + "description": "Angi n\u00f8dvendige parametere for \u00e5 koble til Android TV-enheten din", + "title": "" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/it.json b/homeassistant/components/apple_tv/translations/it.json index 2162fad6765..efee1a1395f 100644 --- a/homeassistant/components/apple_tv/translations/it.json +++ b/homeassistant/components/apple_tv/translations/it.json @@ -43,11 +43,11 @@ "title": "Password richiesta" }, "protocol_disabled": { - "description": "L'associazione \u00e8 necessaria per `{protocol}` ma \u00e8 disabilitata sul dispositivo. Si prega di rivedere le potenziali restrizioni di accesso (ad esempio consentire a tutti i dispositivi sulla rete locale di connettersi) sul dispositivo. \n\n Puoi continuare senza associare questo protocollo, ma alcune funzionalit\u00e0 saranno limitate.", + "description": "L'associazione \u00e8 necessaria per `{protocol}`, ma \u00e8 disabilitata sul dispositivo. Rivedi le potenziali restrizioni di accesso (ad esempio consentire a tutti i dispositivi sulla rete locale di connettersi) sul dispositivo. \n\n Puoi continuare senza associare questo protocollo, ma alcune funzionalit\u00e0 saranno limitate.", "title": "Associazione non possibile" }, "reconfigure": { - "description": "Riconfigurare questo dispositivo per ripristinarne la funzionalit\u00e0.", + "description": "Riconfigura questo dispositivo per ripristinarne la funzionalit\u00e0.", "title": "Riconfigurazione del dispositivo" }, "service_problem": { diff --git a/homeassistant/components/aseko_pool_live/translations/it.json b/homeassistant/components/aseko_pool_live/translations/it.json index 30f36eb67ca..1cb1594adc7 100644 --- a/homeassistant/components/aseko_pool_live/translations/it.json +++ b/homeassistant/components/aseko_pool_live/translations/it.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "email": "E-mail", + "email": "Email", "password": "Password" } } diff --git a/homeassistant/components/august/translations/it.json b/homeassistant/components/august/translations/it.json index 0eb8b20f0a8..aebfc1b14cd 100644 --- a/homeassistant/components/august/translations/it.json +++ b/homeassistant/components/august/translations/it.json @@ -15,7 +15,7 @@ "password": "Password" }, "description": "Inserisci la password per {username}.", - "title": "Riautentica un account di August" + "title": "Autentica nuovamente un account di August" }, "user_validate": { "data": { diff --git a/homeassistant/components/awair/translations/it.json b/homeassistant/components/awair/translations/it.json index cad2b8555a8..c9480ecaaa0 100644 --- a/homeassistant/components/awair/translations/it.json +++ b/homeassistant/components/awair/translations/it.json @@ -13,14 +13,14 @@ "reauth": { "data": { "access_token": "Token di accesso", - "email": "E-mail" + "email": "Email" }, "description": "Inserisci nuovamente il tuo token di accesso per sviluppatori Awair." }, "user": { "data": { "access_token": "Token di accesso", - "email": "E-mail" + "email": "Email" }, "description": "\u00c8 necessario registrarsi per un token di accesso per sviluppatori Awair all'indirizzo: https://developer.getawair.com/onboard/login" } diff --git a/homeassistant/components/azure_devops/translations/it.json b/homeassistant/components/azure_devops/translations/it.json index 02900b93935..673eaca5475 100644 --- a/homeassistant/components/azure_devops/translations/it.json +++ b/homeassistant/components/azure_devops/translations/it.json @@ -13,10 +13,10 @@ "step": { "reauth": { "data": { - "personal_access_token": "Token di Accesso Personale (PAT)" + "personal_access_token": "Token di accesso personale (PAT)" }, "description": "Autenticazione non riuscita per {project_url}. Si prega di inserire le proprie credenziali attuali.", - "title": "Riautenticazione" + "title": "Nuova autenticazione" }, "user": { "data": { diff --git a/homeassistant/components/azure_event_hub/translations/fr.json b/homeassistant/components/azure_event_hub/translations/fr.json new file mode 100644 index 00000000000..317479bdf95 --- /dev/null +++ b/homeassistant/components/azure_event_hub/translations/fr.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "Impossible de se connecter", + "unknown": "Erreur inattendue" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/azure_event_hub/translations/nl.json b/homeassistant/components/azure_event_hub/translations/nl.json new file mode 100644 index 00000000000..5ce2dd9f69c --- /dev/null +++ b/homeassistant/components/azure_event_hub/translations/nl.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "already_configured": "Service is al geconfigureerd", + "cannot_connect": "Verbinding maken met de credentails uit de configuration.yaml is mislukt, verwijder deze uit de yaml en gebruik de config flow.", + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk.", + "unknown": "Verbinding maken met de credentails uit de configuration.yaml is mislukt met een onbekende fout, verwijder a.u.b. de yaml en gebruik de config flow." + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "unknown": "Onverwachte fout" + }, + "step": { + "conn_string": { + "data": { + "event_hub_connection_string": "Event Hub Connection String" + }, + "description": "Voer de connection string in voor: {event_hub_instance_name}", + "title": "Connection String methode" + }, + "sas": { + "data": { + "event_hub_namespace": "Event Hub Namespace", + "event_hub_sas_key": "Event Hub SAS-sleutel", + "event_hub_sas_policy": "Event Hub SAS Policy" + }, + "description": "Voer de SAS-gegevens (shared access signature) in voor: {event_hub_instance_name}", + "title": "SAS Credentials methode" + }, + "user": { + "data": { + "use_connection_string": "Gebruik Connection String" + }, + "title": "Stel uw Azure Event Hub integratie in" + } + } + }, + "options": { + "step": { + "options": { + "data": { + "send_interval": "Interval tussen het verzenden van batches naar de hub." + }, + "title": "Opties voor de Azure Event Hub." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/azure_event_hub/translations/no.json b/homeassistant/components/azure_event_hub/translations/no.json new file mode 100644 index 00000000000..a22f7eef3d6 --- /dev/null +++ b/homeassistant/components/azure_event_hub/translations/no.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown": "Uventet feil" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/it.json b/homeassistant/components/bosch_shc/translations/it.json index d6e971cb079..c96ea617bf8 100644 --- a/homeassistant/components/bosch_shc/translations/it.json +++ b/homeassistant/components/bosch_shc/translations/it.json @@ -23,7 +23,7 @@ }, "reauth_confirm": { "description": "L'integrazione bosch_shc deve autenticare nuovamente il tuo account", - "title": "Autenticare nuovamente l'integrazione" + "title": "Autentica nuovamente l'integrazione" }, "user": { "data": { diff --git a/homeassistant/components/brunt/translations/it.json b/homeassistant/components/brunt/translations/it.json index 0a928af664d..7b6d11836d0 100644 --- a/homeassistant/components/brunt/translations/it.json +++ b/homeassistant/components/brunt/translations/it.json @@ -15,7 +15,7 @@ "password": "Password" }, "description": "Reinserisci la password per: {username}", - "title": "Autenticare nuovamente l'integrazione" + "title": "Autentica nuovamente l'integrazione" }, "user": { "data": { diff --git a/homeassistant/components/cloud/translations/it.json b/homeassistant/components/cloud/translations/it.json index e867bbacc26..5c8976d63a3 100644 --- a/homeassistant/components/cloud/translations/it.json +++ b/homeassistant/components/cloud/translations/it.json @@ -2,9 +2,9 @@ "system_health": { "info": { "alexa_enabled": "Alexa abilitato", - "can_reach_cert_server": "Server dei Certificati raggiungibile", + "can_reach_cert_server": "Server dei certificati raggiungibile", "can_reach_cloud": "Home Assistant Cloud raggiungibile", - "can_reach_cloud_auth": "Server di Autenticazione raggiungibile", + "can_reach_cloud_auth": "Server di autenticazione raggiungibile", "google_enabled": "Google abilitato", "logged_in": "Accesso effettuato", "relayer_connected": "Relayer connesso", diff --git a/homeassistant/components/cloudflare/translations/it.json b/homeassistant/components/cloudflare/translations/it.json index fae00a790dc..db478015dfa 100644 --- a/homeassistant/components/cloudflare/translations/it.json +++ b/homeassistant/components/cloudflare/translations/it.json @@ -15,7 +15,7 @@ "reauth_confirm": { "data": { "api_token": "Token API", - "description": "Eseguire nuovamente l'autenticazione con l'account Cloudflare." + "description": "Esegui nuovamente l'autenticazione con l'account Cloudflare." } }, "records": { diff --git a/homeassistant/components/coinbase/translations/it.json b/homeassistant/components/coinbase/translations/it.json index af5d07805de..117521b1933 100644 --- a/homeassistant/components/coinbase/translations/it.json +++ b/homeassistant/components/coinbase/translations/it.json @@ -12,7 +12,7 @@ "user": { "data": { "api_key": "Chiave API", - "api_token": " API Segreta", + "api_token": "API segreta", "currencies": "Valute del saldo del conto", "exchange_rates": "Tassi di cambio" }, diff --git a/homeassistant/components/crownstone/translations/it.json b/homeassistant/components/crownstone/translations/it.json index 1fb43e75684..ee267a46004 100644 --- a/homeassistant/components/crownstone/translations/it.json +++ b/homeassistant/components/crownstone/translations/it.json @@ -6,7 +6,7 @@ "usb_setup_unsuccessful": "La configurazione USB di Crownstone non ha avuto successo." }, "error": { - "account_not_verified": "Account non verificato. Attiva il tuo account tramite l'e-mail di attivazione di Crownstone.", + "account_not_verified": "Account non verificato. Attiva il tuo account tramite l'email di attivazione di Crownstone.", "invalid_auth": "Autenticazione non valida", "unknown": "Errore imprevisto" }, @@ -22,7 +22,7 @@ "data": { "usb_manual_path": "Percorso del dispositivo USB" }, - "description": "Immettere manualmente il percorso di una chiavetta USB Crownstone.", + "description": "Immetti manualmente il percorso di una chiavetta USB Crownstone.", "title": "Percorso manuale della chiavetta USB Crownstone" }, "usb_sphere_config": { @@ -34,7 +34,7 @@ }, "user": { "data": { - "email": "E-mail", + "email": "Email", "password": "Password" }, "title": "Account Crownstone" @@ -46,7 +46,7 @@ "init": { "data": { "usb_sphere_option": "Sfera di Crownstone dove si trova l'USB", - "use_usb_option": "Utilizzare una chiavetta USB Crownstone per la trasmissione locale dei dati" + "use_usb_option": "Utilizza una chiavetta USB Crownstone per la trasmissione locale dei dati" } }, "usb_config": { diff --git a/homeassistant/components/daikin/translations/it.json b/homeassistant/components/daikin/translations/it.json index 203335c16eb..39b3451467f 100644 --- a/homeassistant/components/daikin/translations/it.json +++ b/homeassistant/components/daikin/translations/it.json @@ -5,7 +5,7 @@ "cannot_connect": "Impossibile connettersi" }, "error": { - "api_password": "Autenticazione non valida, utilizzare la chiave API o la password.", + "api_password": "Autenticazione non valida, utilizza la chiave API o la password.", "cannot_connect": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida", "unknown": "Errore imprevisto" @@ -17,7 +17,7 @@ "host": "Host", "password": "Password" }, - "description": "Inserire l'Indirizzo IP del tuo condizionatore d'aria Daikin.\n\nNotare che solo la Chiave API e la Password sono rispettivamente usati dai dispositivi BRP072Cxx e SKYFi.", + "description": "Inserisci l'Indirizzo IP del tuo condizionatore d'aria Daikin.\n\nNota che solo la Chiave API e la Password sono rispettivamente usati dai dispositivi BRP072Cxx e SKYFi.", "title": "Configura Daikin AC" } } diff --git a/homeassistant/components/devolo_home_control/translations/it.json b/homeassistant/components/devolo_home_control/translations/it.json index 1197702ae2a..e31377a47de 100644 --- a/homeassistant/components/devolo_home_control/translations/it.json +++ b/homeassistant/components/devolo_home_control/translations/it.json @@ -6,21 +6,21 @@ }, "error": { "invalid_auth": "Autenticazione non valida", - "reauth_failed": "Si prega di utilizzare lo stesso utente mydevolo di prima." + "reauth_failed": "Utilizza lo stesso utente mydevolo di prima." }, "step": { "user": { "data": { "mydevolo_url": "URL di mydevolo", "password": "Password", - "username": "E-mail / devolo ID" + "username": "Email / devolo ID" } }, "zeroconf_confirm": { "data": { "mydevolo_url": "URL mydevolo", "password": "Password", - "username": "E-mail / ID devolo" + "username": "Email / ID devolo" } } } diff --git a/homeassistant/components/econet/translations/it.json b/homeassistant/components/econet/translations/it.json index 3074c72b083..1c966d7b5ec 100644 --- a/homeassistant/components/econet/translations/it.json +++ b/homeassistant/components/econet/translations/it.json @@ -12,7 +12,7 @@ "step": { "user": { "data": { - "email": "E-mail", + "email": "Email", "password": "Password" }, "title": "Imposta account Rheem EcoNet" diff --git a/homeassistant/components/elmax/translations/it.json b/homeassistant/components/elmax/translations/it.json index c80e64c81c9..ffcd23629b2 100644 --- a/homeassistant/components/elmax/translations/it.json +++ b/homeassistant/components/elmax/translations/it.json @@ -7,7 +7,7 @@ "bad_auth": "Autenticazione non valida", "invalid_pin": "Il pin fornito non \u00e8 valido", "network_error": "Si \u00e8 verificato un errore di rete", - "no_panel_online": "Non \u00e8 stato trovato alcun pannello di controllo Elmax online.", + "no_panel_online": "Non \u00e8 stato trovato alcun pannello di controllo Elmax in linea.", "unknown_error": "Si \u00e8 verificato un errore imprevisto" }, "step": { @@ -25,7 +25,7 @@ "password": "Password", "username": "Nome utente" }, - "description": "Effettua il login al cloud di Elmax utilizzando le tue credenziali", + "description": "Esegui l'accesso al cloud di Elmax utilizzando le tue credenziali", "title": "Accesso all'account" } } diff --git a/homeassistant/components/elmax/translations/no.json b/homeassistant/components/elmax/translations/no.json index bc15c853aa4..c3215516023 100644 --- a/homeassistant/components/elmax/translations/no.json +++ b/homeassistant/components/elmax/translations/no.json @@ -8,7 +8,7 @@ "invalid_pin": "Den angitte PIN-koden er ugyldig", "network_error": "Det oppstod en nettverksfeil", "no_panel_online": "Ingen online Elmax kontrollpanel ble funnet.", - "unknown_error": "En uventet feil oppstod" + "unknown_error": "Uventet feil" }, "step": { "panels": { diff --git a/homeassistant/components/esphome/translations/it.json b/homeassistant/components/esphome/translations/it.json index 390054bc345..a67710bf844 100644 --- a/homeassistant/components/esphome/translations/it.json +++ b/homeassistant/components/esphome/translations/it.json @@ -31,9 +31,9 @@ }, "reauth_confirm": { "data": { - "noise_psk": "Chiave di crittografia" + "noise_psk": "Chiave di cifratura" }, - "description": "Il dispositivo ESPHome {name} ha abilitato la crittografia del trasporto o ha modificato la chiave di crittografia. Inserisci la chiave aggiornata." + "description": "Il dispositivo ESPHome {name} ha abilitato la cifratura del trasporto o ha modificato la chiave di cifratura. Inserisci la chiave aggiornata." }, "user": { "data": { diff --git a/homeassistant/components/evil_genius_labs/translations/ca.json b/homeassistant/components/evil_genius_labs/translations/ca.json index e77c84008c7..aa6d7355314 100644 --- a/homeassistant/components/evil_genius_labs/translations/ca.json +++ b/homeassistant/components/evil_genius_labs/translations/ca.json @@ -2,6 +2,7 @@ "config": { "error": { "cannot_connect": "Ha fallat la connexi\u00f3", + "timeout": "Temps m\u00e0xim d'espera per establir la connexi\u00f3 esgotat", "unknown": "Error inesperat" }, "step": { diff --git a/homeassistant/components/evil_genius_labs/translations/de.json b/homeassistant/components/evil_genius_labs/translations/de.json index e45f7c2b063..296d38e6f63 100644 --- a/homeassistant/components/evil_genius_labs/translations/de.json +++ b/homeassistant/components/evil_genius_labs/translations/de.json @@ -2,6 +2,7 @@ "config": { "error": { "cannot_connect": "Verbindung fehlgeschlagen", + "timeout": "Zeit\u00fcberschreitung beim Verbindungsaufbau", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/evil_genius_labs/translations/en.json b/homeassistant/components/evil_genius_labs/translations/en.json index b059e11aa28..75c630665ab 100644 --- a/homeassistant/components/evil_genius_labs/translations/en.json +++ b/homeassistant/components/evil_genius_labs/translations/en.json @@ -2,6 +2,7 @@ "config": { "error": { "cannot_connect": "Failed to connect", + "timeout": "Timeout establishing connection", "unknown": "Unexpected error" }, "step": { diff --git a/homeassistant/components/evil_genius_labs/translations/et.json b/homeassistant/components/evil_genius_labs/translations/et.json index dedad93b36c..84dd92bfbe1 100644 --- a/homeassistant/components/evil_genius_labs/translations/et.json +++ b/homeassistant/components/evil_genius_labs/translations/et.json @@ -2,6 +2,7 @@ "config": { "error": { "cannot_connect": "\u00dchendamine nurjus", + "timeout": "\u00dchenduse loomise ajal\u00f5pp", "unknown": "Ootamatu t\u00f5rge" }, "step": { diff --git a/homeassistant/components/evil_genius_labs/translations/hu.json b/homeassistant/components/evil_genius_labs/translations/hu.json index 6ed148ceabb..c1431690d6e 100644 --- a/homeassistant/components/evil_genius_labs/translations/hu.json +++ b/homeassistant/components/evil_genius_labs/translations/hu.json @@ -2,6 +2,7 @@ "config": { "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a kapcsolat l\u00e9trehoz\u00e1sa sor\u00e1n", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { diff --git a/homeassistant/components/evil_genius_labs/translations/it.json b/homeassistant/components/evil_genius_labs/translations/it.json index 8e3e5b34899..fa418257b0e 100644 --- a/homeassistant/components/evil_genius_labs/translations/it.json +++ b/homeassistant/components/evil_genius_labs/translations/it.json @@ -2,6 +2,7 @@ "config": { "error": { "cannot_connect": "Impossibile connettersi", + "timeout": "Tempo scaduto per stabile la connessione.", "unknown": "Errore imprevisto" }, "step": { diff --git a/homeassistant/components/evil_genius_labs/translations/ja.json b/homeassistant/components/evil_genius_labs/translations/ja.json index db894cded4e..9ee1382e424 100644 --- a/homeassistant/components/evil_genius_labs/translations/ja.json +++ b/homeassistant/components/evil_genius_labs/translations/ja.json @@ -2,6 +2,7 @@ "config": { "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "timeout": "\u63a5\u7d9a\u78ba\u7acb\u6642\u306b\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { diff --git a/homeassistant/components/evil_genius_labs/translations/nl.json b/homeassistant/components/evil_genius_labs/translations/nl.json index 29eb87c44a0..88c6875cc58 100644 --- a/homeassistant/components/evil_genius_labs/translations/nl.json +++ b/homeassistant/components/evil_genius_labs/translations/nl.json @@ -2,6 +2,7 @@ "config": { "error": { "cannot_connect": "Kan geen verbinding maken", + "timeout": "Time-out bij het maken van verbinding", "unknown": "Onverwachte fout" }, "step": { diff --git a/homeassistant/components/evil_genius_labs/translations/ru.json b/homeassistant/components/evil_genius_labs/translations/ru.json index f34931312ec..007c1f7b374 100644 --- a/homeassistant/components/evil_genius_labs/translations/ru.json +++ b/homeassistant/components/evil_genius_labs/translations/ru.json @@ -2,6 +2,7 @@ "config": { "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/evil_genius_labs/translations/zh-Hant.json b/homeassistant/components/evil_genius_labs/translations/zh-Hant.json index a91b061b492..c32ec5190e5 100644 --- a/homeassistant/components/evil_genius_labs/translations/zh-Hant.json +++ b/homeassistant/components/evil_genius_labs/translations/zh-Hant.json @@ -2,6 +2,7 @@ "config": { "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", + "timeout": "\u5efa\u7acb\u9023\u7dda\u903e\u6642", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { diff --git a/homeassistant/components/fireservicerota/translations/it.json b/homeassistant/components/fireservicerota/translations/it.json index 6960b68b2a2..8a437e45900 100644 --- a/homeassistant/components/fireservicerota/translations/it.json +++ b/homeassistant/components/fireservicerota/translations/it.json @@ -15,7 +15,7 @@ "data": { "password": "Password" }, - "description": "I token di autenticazione non sono validi, effettua il login per ricrearli." + "description": "I token di autenticazione non sono validi, esegui l'accesso per ricrearli." }, "user": { "data": { diff --git a/homeassistant/components/flick_electric/translations/it.json b/homeassistant/components/flick_electric/translations/it.json index a505f915c6f..ba3e26163c9 100644 --- a/homeassistant/components/flick_electric/translations/it.json +++ b/homeassistant/components/flick_electric/translations/it.json @@ -12,7 +12,7 @@ "user": { "data": { "client_id": "ID cliente (opzionale)", - "client_secret": "Client Secret (opzionale)", + "client_secret": "Segreto client (opzionale)", "password": "Password", "username": "Nome utente" }, diff --git a/homeassistant/components/flipr/translations/it.json b/homeassistant/components/flipr/translations/it.json index 399d4c6b9d3..fd3b36cbdf3 100644 --- a/homeassistant/components/flipr/translations/it.json +++ b/homeassistant/components/flipr/translations/it.json @@ -19,7 +19,7 @@ }, "user": { "data": { - "email": "E-mail", + "email": "Email", "password": "Password" }, "description": "Connettiti usando il tuo account Flipr.", diff --git a/homeassistant/components/flume/translations/it.json b/homeassistant/components/flume/translations/it.json index 3fdca3a5cb4..ecf59ad8b06 100644 --- a/homeassistant/components/flume/translations/it.json +++ b/homeassistant/components/flume/translations/it.json @@ -15,7 +15,7 @@ "password": "Password" }, "description": "La password per {username} non \u00e8 pi\u00f9 valida.", - "title": "Riautentica il tuo account Flume" + "title": "Autentica nuovamente il tuo account Flume" }, "user": { "data": { diff --git a/homeassistant/components/flux_led/translations/it.json b/homeassistant/components/flux_led/translations/it.json index 91654fb3542..13b522906ba 100644 --- a/homeassistant/components/flux_led/translations/it.json +++ b/homeassistant/components/flux_led/translations/it.json @@ -17,7 +17,7 @@ "data": { "host": "Host" }, - "description": "Se lasci vuoto l'host, il rilevamento verr\u00e0 utilizzato per trovare i dispositivi." + "description": "Se lasci vuoto l'host, il rilevamento sar\u00e0 utilizzato per trovare i dispositivi." } } }, diff --git a/homeassistant/components/hangouts/translations/it.json b/homeassistant/components/hangouts/translations/it.json index 3e89327ca30..df3b1cf4dee 100644 --- a/homeassistant/components/hangouts/translations/it.json +++ b/homeassistant/components/hangouts/translations/it.json @@ -7,7 +7,7 @@ "error": { "invalid_2fa": "Autenticazione a 2 fattori non valida, riprovare.", "invalid_2fa_method": "Metodo 2FA non valido (verifica sul telefono).", - "invalid_login": "Accesso non valido, si prega di riprovare." + "invalid_login": "Accesso non valido, riprova." }, "step": { "2fa": { @@ -20,7 +20,7 @@ "user": { "data": { "authorization_code": "Codice di autorizzazione (necessario per l'autenticazione manuale)", - "email": "E-mail", + "email": "Email", "password": "Password" }, "description": "Vuoto", diff --git a/homeassistant/components/hue/translations/it.json b/homeassistant/components/hue/translations/it.json index 29298f736c3..84bf9aa718b 100644 --- a/homeassistant/components/hue/translations/it.json +++ b/homeassistant/components/hue/translations/it.json @@ -69,7 +69,7 @@ "data": { "allow_hue_groups": "Consenti gruppi Hue", "allow_hue_scenes": "Consenti scene Tonalit\u00e0", - "allow_unreachable": "Consentire alle lampadine irraggiungibili di segnalare correttamente il loro stato" + "allow_unreachable": "Consenti alle lampadine irraggiungibili di segnalare correttamente il loro stato" } } } diff --git a/homeassistant/components/icloud/translations/it.json b/homeassistant/components/icloud/translations/it.json index cfb18caee1e..0661519b714 100644 --- a/homeassistant/components/icloud/translations/it.json +++ b/homeassistant/components/icloud/translations/it.json @@ -16,7 +16,7 @@ "password": "Password" }, "description": "La password inserita in precedenza per {username} non funziona pi\u00f9. Aggiorna la tua password per continuare a utilizzare questa integrazione.", - "title": "Autenticare nuovamente l'integrazione" + "title": "Autentica nuovamente l'integrazione" }, "trusted_device": { "data": { @@ -28,7 +28,7 @@ "user": { "data": { "password": "Password", - "username": "E-mail", + "username": "Email", "with_family": "Con la famiglia" }, "description": "Inserisci le tue credenziali", diff --git a/homeassistant/components/knx/translations/nl.json b/homeassistant/components/knx/translations/nl.json index 004d32ba900..b7a00b5187f 100644 --- a/homeassistant/components/knx/translations/nl.json +++ b/homeassistant/components/knx/translations/nl.json @@ -21,6 +21,7 @@ "routing": { "data": { "individual_address": "Individueel adres voor de routing verbinding", + "local_ip": "Lokaal IP (laat leeg indien niet zeker)", "multicast_group": "De multicast groep gebruikt voor de routing", "multicast_port": "De multicast-poort gebruikt voor de routing" }, @@ -46,6 +47,7 @@ "data": { "connection_type": "KNX-verbindingstype", "individual_address": "Standaard individueel adres", + "local_ip": "Lokaal IP (laat leeg indien niet zeker)", "multicast_group": "Multicast groep gebruikt voor routing en ontdekking", "multicast_port": "Multicast poort gebruikt voor routing en ontdekking", "rate_limit": "Maximaal aantal uitgaande telegrammen per seconde", diff --git a/homeassistant/components/lcn/translations/it.json b/homeassistant/components/lcn/translations/it.json index c8f61e2570a..e42f52b9c62 100644 --- a/homeassistant/components/lcn/translations/it.json +++ b/homeassistant/components/lcn/translations/it.json @@ -4,7 +4,7 @@ "fingerprint": "codice impronta digitale ricevuto", "send_keys": "invia chiavi ricevute", "transmitter": "codice trasmettitore ricevuto", - "transponder": "codice transpoder ricevuto" + "transponder": "codice transponder ricevuto" } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/it.json b/homeassistant/components/lyric/translations/it.json index 809e6608b80..c544598079e 100644 --- a/homeassistant/components/lyric/translations/it.json +++ b/homeassistant/components/lyric/translations/it.json @@ -14,7 +14,7 @@ }, "reauth_confirm": { "description": "L'integrazione di Lyric deve autenticare nuovamente il tuo account.", - "title": "Autenticare nuovamente l'integrazione" + "title": "Autentica nuovamente l'integrazione" } } } diff --git a/homeassistant/components/mazda/translations/it.json b/homeassistant/components/mazda/translations/it.json index 7d7ab19a329..21d003b030b 100644 --- a/homeassistant/components/mazda/translations/it.json +++ b/homeassistant/components/mazda/translations/it.json @@ -13,11 +13,11 @@ "step": { "user": { "data": { - "email": "E-mail", + "email": "Email", "password": "Password", "region": "Area geografica" }, - "description": "Inserisci l'indirizzo e-mail e la password che utilizzi per accedere all'app mobile MyMazda.", + "description": "Inserisci l'indirizzo email e la password che utilizzi per accedere all'app mobile MyMazda.", "title": "Mazda Connected Services - Aggiungi account" } } diff --git a/homeassistant/components/melcloud/translations/it.json b/homeassistant/components/melcloud/translations/it.json index 15d9f0d06fc..c76f2fd9cf2 100644 --- a/homeassistant/components/melcloud/translations/it.json +++ b/homeassistant/components/melcloud/translations/it.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Integrazione MELCloud gi\u00e0 configurata per questa e-mail. Il token di accesso \u00e8 stato aggiornato." + "already_configured": "Integrazione MELCloud gi\u00e0 configurata per questa email. Il token di accesso \u00e8 stato aggiornato." }, "error": { "cannot_connect": "Impossibile connettersi", @@ -12,7 +12,7 @@ "user": { "data": { "password": "Password", - "username": "E-mail" + "username": "Email" }, "description": "Connettiti utilizzando il tuo account MELCloud.", "title": "Connettersi a MELCloud" diff --git a/homeassistant/components/mqtt/translations/it.json b/homeassistant/components/mqtt/translations/it.json index c1a2a3745c6..0ed8311339f 100644 --- a/homeassistant/components/mqtt/translations/it.json +++ b/homeassistant/components/mqtt/translations/it.json @@ -11,7 +11,7 @@ "broker": { "data": { "broker": "Broker", - "discovery": "Attiva l'individuazione", + "discovery": "Attiva il rilevamento", "password": "Password", "port": "Porta", "username": "Nome utente" @@ -20,7 +20,7 @@ }, "hassio_confirm": { "data": { - "discovery": "Attiva l'individuazione" + "discovery": "Attiva il rilevamento" }, "description": "Vuoi configurare Home Assistant per connettersi al broker MQTT fornito dal componente aggiuntivo: {addon}?", "title": "Broker MQTT tramite il componente aggiuntivo di Home Assistant" @@ -73,7 +73,7 @@ "birth_qos": "QoS del messaggio birth", "birth_retain": "Persistenza del messaggio birth", "birth_topic": "Argomento del messaggio birth", - "discovery": "Attiva l'individuazione", + "discovery": "Attiva il rilevamento", "will_enable": "Abilita il messaggio testamento", "will_payload": "Payload del messaggio testamento", "will_qos": "QoS del messaggio testamento", diff --git a/homeassistant/components/myq/translations/it.json b/homeassistant/components/myq/translations/it.json index 3ed9280b462..78747578d0e 100644 --- a/homeassistant/components/myq/translations/it.json +++ b/homeassistant/components/myq/translations/it.json @@ -15,7 +15,7 @@ "password": "Password" }, "description": "La password per {username} non \u00e8 pi\u00f9 valida.", - "title": "Riautentica il tuo account MyQ" + "title": "Autentica nuovamente il tuo account MyQ" }, "user": { "data": { diff --git a/homeassistant/components/nam/translations/it.json b/homeassistant/components/nam/translations/it.json index e82a0db8ddf..1bbacd849fe 100644 --- a/homeassistant/components/nam/translations/it.json +++ b/homeassistant/components/nam/translations/it.json @@ -4,7 +4,7 @@ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "device_unsupported": "Il dispositivo non \u00e8 supportato.", "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", - "reauth_unsuccessful": "La riautenticazione non \u00e8 andata a buon fine, rimuovere l'integrazione e configurarla di nuovo." + "reauth_unsuccessful": "La nuova autenticazione non \u00e8 andata a buon fine, rimuovi l'integrazione e configurala di nuovo." }, "error": { "cannot_connect": "Impossibile connettersi", diff --git a/homeassistant/components/nest/translations/it.json b/homeassistant/components/nest/translations/it.json index 24318d779a6..682ab1728ea 100644 --- a/homeassistant/components/nest/translations/it.json +++ b/homeassistant/components/nest/translations/it.json @@ -55,7 +55,7 @@ }, "reauth_confirm": { "description": "L'integrazione di Nest deve autenticare nuovamente il tuo account", - "title": "Autenticare nuovamente l'integrazione" + "title": "Autentica nuovamente l'integrazione" } } }, diff --git a/homeassistant/components/netatmo/translations/it.json b/homeassistant/components/netatmo/translations/it.json index 3f9e7df3ad6..279bbe73a97 100644 --- a/homeassistant/components/netatmo/translations/it.json +++ b/homeassistant/components/netatmo/translations/it.json @@ -15,8 +15,8 @@ "title": "Scegli il metodo di autenticazione" }, "reauth_confirm": { - "description": "L'integrazione Netatmo deve riautenticare il tuo account", - "title": "Autenticare nuovamente l'integrazione" + "description": "L'integrazione Netatmo deve autenticare nuovamente il tuo account", + "title": "Autentica nuovamente l'integrazione" } } }, diff --git a/homeassistant/components/nfandroidtv/translations/it.json b/homeassistant/components/nfandroidtv/translations/it.json index 3b8d089b5a5..6251a31e3bc 100644 --- a/homeassistant/components/nfandroidtv/translations/it.json +++ b/homeassistant/components/nfandroidtv/translations/it.json @@ -13,7 +13,7 @@ "host": "Host", "name": "Nome" }, - "description": "Questa integrazione richiede l'app Notifiche per Android TV. \n\nPer Android TV: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nPer Fire TV: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK \n\n\u00c8 necessario impostare la prenotazione DHCP sul router (fare riferimento al manuale utente del router) o un indirizzo IP statico sul dispositivo. In caso contrario, il dispositivo alla fine non sar\u00e0 pi\u00f9 disponibile.", + "description": "Questa integrazione richiede l'app Notifiche per Android TV. \n\nPer Android TV: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nPer Fire TV: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK \n\nDovresti configurare la prenotazione DHCP sul router (fai riferimento al manuale utente del router) o un indirizzo IP statico sul dispositivo. In caso contrario, il dispositivo alla fine non sar\u00e0 pi\u00f9 disponibile.", "title": "Notifiche per Android TV / Fire TV" } } diff --git a/homeassistant/components/notion/translations/it.json b/homeassistant/components/notion/translations/it.json index 69d2294394b..811884dddb4 100644 --- a/homeassistant/components/notion/translations/it.json +++ b/homeassistant/components/notion/translations/it.json @@ -15,7 +15,7 @@ "password": "Password" }, "description": "Inserisci nuovamente la password per {username}.", - "title": "Autenticare nuovamente l'integrazione" + "title": "Autentica nuovamente l'integrazione" }, "user": { "data": { diff --git a/homeassistant/components/nuki/translations/it.json b/homeassistant/components/nuki/translations/it.json index eaf0a8e52e4..74249677e9b 100644 --- a/homeassistant/components/nuki/translations/it.json +++ b/homeassistant/components/nuki/translations/it.json @@ -14,7 +14,7 @@ "token": "Token di accesso" }, "description": "L'integrazione Nuki deve essere nuovamente autenticata con il tuo bridge.", - "title": "Autenticare nuovamente l'integrazione" + "title": "Autentica nuovamente l'integrazione" }, "user": { "data": { diff --git a/homeassistant/components/nws/translations/it.json b/homeassistant/components/nws/translations/it.json index 3b651ce82b0..c05a2ec28b7 100644 --- a/homeassistant/components/nws/translations/it.json +++ b/homeassistant/components/nws/translations/it.json @@ -15,7 +15,7 @@ "longitude": "Logitudine", "station": "Codice stazione METAR" }, - "description": "Se non \u00e8 specificato un codice stazione METAR, la latitudine e la longitudine saranno utilizzate per trovare la stazione pi\u00f9 vicina. Per ora, una chiave API pu\u00f2 essere qualsiasi cosa. Si consiglia di utilizzare un indirizzo e-mail valido.", + "description": "Se non \u00e8 specificato un codice stazione METAR, la latitudine e la longitudine saranno utilizzate per trovare la stazione pi\u00f9 vicina. Per ora, una chiave API pu\u00f2 essere qualsiasi cosa. Si consiglia di utilizzare un indirizzo email valido.", "title": "Collegati al Servizio Meteorologico Nazionale" } } diff --git a/homeassistant/components/octoprint/translations/no.json b/homeassistant/components/octoprint/translations/no.json index a94360f5bd2..f6b4b607aa1 100644 --- a/homeassistant/components/octoprint/translations/no.json +++ b/homeassistant/components/octoprint/translations/no.json @@ -19,7 +19,7 @@ "data": { "host": "Vert", "path": "Bane til program", - "port": "Portnummer", + "port": "Port", "ssl": "Bruk SSL", "username": "Brukernavn" } diff --git a/homeassistant/components/open_meteo/translations/fr.json b/homeassistant/components/open_meteo/translations/fr.json new file mode 100644 index 00000000000..c3d33db9e25 --- /dev/null +++ b/homeassistant/components/open_meteo/translations/fr.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "zone": "Zone" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/open_meteo/translations/it.json b/homeassistant/components/open_meteo/translations/it.json new file mode 100644 index 00000000000..d57db9732b8 --- /dev/null +++ b/homeassistant/components/open_meteo/translations/it.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "zone": "Zona" + }, + "description": "Seleziona la posizione da utilizzare per le previsioni del tempo" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/open_meteo/translations/nl.json b/homeassistant/components/open_meteo/translations/nl.json new file mode 100644 index 00000000000..7a14ede64c9 --- /dev/null +++ b/homeassistant/components/open_meteo/translations/nl.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "zone": "Zone" + }, + "description": "Kies locatie om te gebruiken voor weersvoorspelling" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/translations/it.json b/homeassistant/components/ovo_energy/translations/it.json index 0f8dd492d02..be21bde5665 100644 --- a/homeassistant/components/ovo_energy/translations/it.json +++ b/homeassistant/components/ovo_energy/translations/it.json @@ -11,8 +11,8 @@ "data": { "password": "Password" }, - "description": "Autenticazione non riuscita per OVO Energy. Immettere le credenziali correnti.", - "title": "Riautenticazione" + "description": "Autenticazione non riuscita per OVO Energy. Digita le credenziali attuali.", + "title": "Nuova autenticazione" }, "user": { "data": { diff --git a/homeassistant/components/plum_lightpad/translations/it.json b/homeassistant/components/plum_lightpad/translations/it.json index 8ed082face7..cf115e336b5 100644 --- a/homeassistant/components/plum_lightpad/translations/it.json +++ b/homeassistant/components/plum_lightpad/translations/it.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "Password", - "username": "E-mail" + "username": "Email" } } } diff --git a/homeassistant/components/poolsense/translations/it.json b/homeassistant/components/poolsense/translations/it.json index 1dc8c700e21..e6ea298e31d 100644 --- a/homeassistant/components/poolsense/translations/it.json +++ b/homeassistant/components/poolsense/translations/it.json @@ -9,7 +9,7 @@ "step": { "user": { "data": { - "email": "E-mail", + "email": "Email", "password": "Password" }, "description": "Vuoi iniziare la configurazione?", diff --git a/homeassistant/components/renault/translations/it.json b/homeassistant/components/renault/translations/it.json index f315a8b5826..8086620dbc1 100644 --- a/homeassistant/components/renault/translations/it.json +++ b/homeassistant/components/renault/translations/it.json @@ -20,13 +20,13 @@ "password": "Password" }, "description": "Aggiorna la tua password per {username}", - "title": "Autenticare nuovamente l'integrazione" + "title": "Autentica nuovamente l'integrazione" }, "user": { "data": { "locale": "Locale", "password": "Password", - "username": "E-mail" + "username": "Email" }, "title": "Imposta le credenziali Renault" } diff --git a/homeassistant/components/renault/translations/no.json b/homeassistant/components/renault/translations/no.json index 9ae887830f2..1e53c2718eb 100644 --- a/homeassistant/components/renault/translations/no.json +++ b/homeassistant/components/renault/translations/no.json @@ -26,7 +26,7 @@ "data": { "locale": "Lokal", "password": "Passord", - "username": "E-Post" + "username": "E-post" }, "title": "Angi Renault-legitimasjon" } diff --git a/homeassistant/components/rfxtrx/translations/it.json b/homeassistant/components/rfxtrx/translations/it.json index d5bb516b26b..5e05af403ae 100644 --- a/homeassistant/components/rfxtrx/translations/it.json +++ b/homeassistant/components/rfxtrx/translations/it.json @@ -66,7 +66,7 @@ "debug": "Attivare il debug", "device": "Selezionare il dispositivo da configurare", "event_code": "Inserire il codice dell'evento da aggiungere", - "remove_device": "Selezionare il dispositivo da cancellare" + "remove_device": "Seleziona il dispositivo da eliminare" }, "title": "Opzioni Rfxtrx" }, diff --git a/homeassistant/components/ridwell/translations/it.json b/homeassistant/components/ridwell/translations/it.json index b603f14a1ac..fc7101c6c3c 100644 --- a/homeassistant/components/ridwell/translations/it.json +++ b/homeassistant/components/ridwell/translations/it.json @@ -14,7 +14,7 @@ "password": "Password" }, "description": "Inserisci nuovamente la password per {username}:", - "title": "Autenticare nuovamente l'integrazione" + "title": "Autentica nuovamente l'integrazione" }, "user": { "data": { diff --git a/homeassistant/components/rituals_perfume_genie/translations/it.json b/homeassistant/components/rituals_perfume_genie/translations/it.json index 6dfb2230285..ed65411e8d5 100644 --- a/homeassistant/components/rituals_perfume_genie/translations/it.json +++ b/homeassistant/components/rituals_perfume_genie/translations/it.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "email": "E-mail", + "email": "Email", "password": "Password" }, "title": "Collegati al tuo account Rituals" diff --git a/homeassistant/components/sense/translations/it.json b/homeassistant/components/sense/translations/it.json index 2ab80941a6a..66c09d294c1 100644 --- a/homeassistant/components/sense/translations/it.json +++ b/homeassistant/components/sense/translations/it.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "email": "E-mail", + "email": "Email", "password": "Password", "timeout": "Tempo scaduto" }, diff --git a/homeassistant/components/simplisafe/translations/it.json b/homeassistant/components/simplisafe/translations/it.json index 6b7d9231360..9cc1d676bbf 100644 --- a/homeassistant/components/simplisafe/translations/it.json +++ b/homeassistant/components/simplisafe/translations/it.json @@ -20,7 +20,7 @@ "title": "Completa l'autorizzazione" }, "mfa": { - "description": "Controlla la tua e-mail per trovare un link da SimpliSafe. Dopo aver verificato il link, torna qui per completare l'installazione dell'integrazione.", + "description": "Controlla la tua email per trovare un link da SimpliSafe. Dopo aver verificato il link, torna qui per completare l'installazione dell'integrazione.", "title": "Autenticazione a pi\u00f9 fattori (MFA) SimpliSafe " }, "reauth_confirm": { @@ -28,16 +28,16 @@ "password": "Password" }, "description": "Il tuo accesso \u00e8 scaduto o revocato. Inserisci la password per ricollegare il tuo account.", - "title": "Autenticare nuovamente l'integrazione" + "title": "Autentica nuovamente l'integrazione" }, "user": { "data": { "auth_code": "Codice di autorizzazione", "code": "Codice (utilizzato nell'Interfaccia Utente di Home Assistant)", "password": "Password", - "username": "E-mail" + "username": "Email" }, - "description": "SimpliSafe si autentica con Home Assistant tramite l'app Web SimpliSafe. A causa di limitazioni tecniche, alla fine di questo processo \u00e8 previsto un passaggio manuale; assicurati di leggere la [documentazione]({docs_url}) prima di iniziare. \n\n 1. Fare clic su [qui]({url}) per aprire l'app Web SimpliSafe e inserire le proprie credenziali. \n\n 2. Quando il processo di accesso \u00e8 completo, torna qui e inserisci il codice di autorizzazione di seguito.", + "description": "SimpliSafe si autentica con Home Assistant tramite l'app Web SimpliSafe. A causa di limitazioni tecniche, alla fine di questo processo \u00e8 previsto un passaggio manuale; assicurati di leggere la [documentazione]({docs_url}) prima di iniziare. \n\n 1. Fai clic su [qui]({url}) per aprire l'app Web SimpliSafe e inserire le tue credenziali. \n\n 2. Quando il processo di accesso \u00e8 completo, torna qui e inserisci il codice di autorizzazione di seguito.", "title": "Inserisci le tue informazioni." } } diff --git a/homeassistant/components/smarthab/translations/it.json b/homeassistant/components/smarthab/translations/it.json index 298520459f0..ffd1ff6d9c6 100644 --- a/homeassistant/components/smarthab/translations/it.json +++ b/homeassistant/components/smarthab/translations/it.json @@ -8,7 +8,7 @@ "step": { "user": { "data": { - "email": "E-mail", + "email": "Email", "password": "Password" }, "description": "Per motivi tecnici, assicurati di utilizzare un account secondario specifico per la tua configurazione di Home Assistant. \u00c8 possibile crearne uno dall'applicazione SmartHab.", diff --git a/homeassistant/components/smartthings/translations/it.json b/homeassistant/components/smartthings/translations/it.json index 248ea85eb3f..e73bf6d4e55 100644 --- a/homeassistant/components/smartthings/translations/it.json +++ b/homeassistant/components/smartthings/translations/it.json @@ -26,7 +26,7 @@ "data": { "location_id": "Posizione" }, - "description": "Selezionare la posizione SmartThings che si desidera aggiungere a Home Assistant. Apriremo quindi una nuova finestra e vi chiederemo di effettuare il login e di autorizzare l'installazione dell'integrazione dell'Home Assistant nella posizione selezionata.", + "description": "Seleziona la posizione SmartThings che desideri aggiungere a Home Assistant. Apriremo quindi una nuova finestra e ti chiederemo di eseguire l'accesso e di autorizzare l'installazione dell'integrazione dell'Home Assistant nella posizione selezionata.", "title": "Seleziona posizione" }, "user": { diff --git a/homeassistant/components/smarttub/translations/it.json b/homeassistant/components/smarttub/translations/it.json index 6199cb35bea..680e8f68c52 100644 --- a/homeassistant/components/smarttub/translations/it.json +++ b/homeassistant/components/smarttub/translations/it.json @@ -10,14 +10,14 @@ "step": { "reauth_confirm": { "description": "L'integrazione di SmartTub deve autenticare nuovamente il tuo account", - "title": "Autenticare nuovamente l'integrazione" + "title": "Autentica nuovamente l'integrazione" }, "user": { "data": { - "email": "E-mail", + "email": "Email", "password": "Password" }, - "description": "Inserisci il tuo indirizzo e-mail e la password SmartTub per accedere", + "description": "Inserisci il tuo indirizzo email e la password SmartTub per accedere", "title": "Accesso" } } diff --git a/homeassistant/components/sonarr/translations/it.json b/homeassistant/components/sonarr/translations/it.json index 9189c82692d..f912fcd9f23 100644 --- a/homeassistant/components/sonarr/translations/it.json +++ b/homeassistant/components/sonarr/translations/it.json @@ -13,7 +13,7 @@ "step": { "reauth_confirm": { "description": "L'integrazione di Sonarr deve essere nuovamente autenticata manualmente con l'API Sonarr ospitata su: {host}", - "title": "Autenticare nuovamente l'integrazione" + "title": "Autentica nuovamente l'integrazione" }, "user": { "data": { diff --git a/homeassistant/components/spotify/translations/it.json b/homeassistant/components/spotify/translations/it.json index 28d821c81f1..d40fa60b4d4 100644 --- a/homeassistant/components/spotify/translations/it.json +++ b/homeassistant/components/spotify/translations/it.json @@ -4,7 +4,7 @@ "authorize_url_timeout": "Tempo scaduto nella generazione dell'URL di autorizzazione.", "missing_configuration": "L'integrazione di Spotify non \u00e8 configurata. Si prega di seguire la documentazione.", "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})", - "reauth_account_mismatch": "L'account Spotify con cui si \u00e8 autenticati non corrisponde all'account necessario per la ri-autenticazione." + "reauth_account_mismatch": "L'account Spotify con cui sei autenticato non corrisponde all'account necessario per la nuova autenticazione." }, "create_entry": { "default": "Autenticato con successo con Spotify." @@ -15,7 +15,7 @@ }, "reauth_confirm": { "description": "L'integrazione di Spotify deve essere nuovamente autenticata con Spotify per l'account: {account}", - "title": "Autenticare nuovamente l'integrazione" + "title": "Autentica nuovamente l'integrazione" } } }, diff --git a/homeassistant/components/srp_energy/translations/it.json b/homeassistant/components/srp_energy/translations/it.json index dd8b93a8de2..d5ccf02d74c 100644 --- a/homeassistant/components/srp_energy/translations/it.json +++ b/homeassistant/components/srp_energy/translations/it.json @@ -13,7 +13,7 @@ "user": { "data": { "id": "Account ID", - "is_tou": "E' la tariffa per periodo di utilizzo", + "is_tou": "\u00c8 la tariffa per periodo di utilizzo", "password": "Password", "username": "Nome utente" } diff --git a/homeassistant/components/synology_dsm/translations/it.json b/homeassistant/components/synology_dsm/translations/it.json index 41c535f714f..eb4a7f6df8d 100644 --- a/homeassistant/components/synology_dsm/translations/it.json +++ b/homeassistant/components/synology_dsm/translations/it.json @@ -37,14 +37,14 @@ "username": "Nome utente" }, "description": "Motivo: {details}", - "title": "Synology DSM Autenticare nuovamente l'integrazione" + "title": "Synology DSM Autentica nuovamente l'integrazione" }, "reauth_confirm": { "data": { "password": "Password", "username": "Nome utente" }, - "title": "Autenticare nuovamente l'integrazione Synology DSM " + "title": "Autentica nuovamente l'integrazione Synology DSM " }, "user": { "data": { diff --git a/homeassistant/components/tile/translations/ca.json b/homeassistant/components/tile/translations/ca.json index 1d70a94f7af..0bfa2ee795b 100644 --- a/homeassistant/components/tile/translations/ca.json +++ b/homeassistant/components/tile/translations/ca.json @@ -1,12 +1,19 @@ { "config": { "abort": { - "already_configured": "El compte ja est\u00e0 configurat" + "already_configured": "El compte ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" }, "step": { + "reauth_confirm": { + "data": { + "password": "Contrasenya" + }, + "title": "Re-autenticaci\u00f3 de Tile" + }, "user": { "data": { "password": "Contrasenya", diff --git a/homeassistant/components/tile/translations/id.json b/homeassistant/components/tile/translations/id.json index 5b5c710594d..80c72a8b7e3 100644 --- a/homeassistant/components/tile/translations/id.json +++ b/homeassistant/components/tile/translations/id.json @@ -1,12 +1,18 @@ { "config": { "abort": { - "already_configured": "Akun sudah dikonfigurasi" + "already_configured": "Akun sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" }, "error": { "invalid_auth": "Autentikasi tidak valid" }, "step": { + "reauth_confirm": { + "data": { + "password": "Kata Sandi" + } + }, "user": { "data": { "password": "Kata Sandi", diff --git a/homeassistant/components/tile/translations/it.json b/homeassistant/components/tile/translations/it.json index e9655775012..2b4cbacaa39 100644 --- a/homeassistant/components/tile/translations/it.json +++ b/homeassistant/components/tile/translations/it.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "Password", - "username": "E-mail" + "username": "Email" }, "title": "Configura Tile" } diff --git a/homeassistant/components/tile/translations/no.json b/homeassistant/components/tile/translations/no.json index 9c8c55cd0ed..182e2eb654a 100644 --- a/homeassistant/components/tile/translations/no.json +++ b/homeassistant/components/tile/translations/no.json @@ -1,12 +1,18 @@ { "config": { "abort": { - "already_configured": "Kontoen er allerede konfigurert" + "already_configured": "Kontoen er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "invalid_auth": "Ugyldig godkjenning" }, "step": { + "reauth_confirm": { + "data": { + "password": "Passord" + } + }, "user": { "data": { "password": "Passord", diff --git a/homeassistant/components/tolo/translations/it.json b/homeassistant/components/tolo/translations/it.json index 28220e7ab99..83704e3e767 100644 --- a/homeassistant/components/tolo/translations/it.json +++ b/homeassistant/components/tolo/translations/it.json @@ -16,7 +16,7 @@ "data": { "host": "Host" }, - "description": "Inserisci il nome host o l'indirizzo IP del tuo dispositivo TOLO Sauna." + "description": "Digita il nome host o l'indirizzo IP del tuo dispositivo TOLO Sauna." } } } diff --git a/homeassistant/components/totalconnect/translations/it.json b/homeassistant/components/totalconnect/translations/it.json index 437edd55a44..ee66d81d83d 100644 --- a/homeassistant/components/totalconnect/translations/it.json +++ b/homeassistant/components/totalconnect/translations/it.json @@ -20,7 +20,7 @@ }, "reauth_confirm": { "description": "Total Connect deve autenticare nuovamente il tuo account", - "title": "Autenticare nuovamente l'integrazione" + "title": "Autentica nuovamente l'integrazione" }, "user": { "data": { diff --git a/homeassistant/components/tractive/translations/it.json b/homeassistant/components/tractive/translations/it.json index 44cdc2df3d7..5774d564098 100644 --- a/homeassistant/components/tractive/translations/it.json +++ b/homeassistant/components/tractive/translations/it.json @@ -12,7 +12,7 @@ "step": { "user": { "data": { - "email": "E-mail", + "email": "Email", "password": "Password" } } diff --git a/homeassistant/components/tradfri/translations/it.json b/homeassistant/components/tradfri/translations/it.json index 7af5aaa7dd4..21b05c3a46a 100644 --- a/homeassistant/components/tradfri/translations/it.json +++ b/homeassistant/components/tradfri/translations/it.json @@ -5,7 +5,7 @@ "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso" }, "error": { - "cannot_authenticate": "Impossibile eseguire l'autenticazione, il Gateway \u00e8 associato a un altro server come ad esempio Homekit?", + "cannot_authenticate": "Impossibile eseguire l'autenticazione, il gateway \u00e8 associato a un altro server come ad esempio Homekit?", "cannot_connect": "Impossibile connettersi", "invalid_key": "Impossibile registrarsi con la chiave fornita. Se questo continua a succedere, prova a riavviare il gateway.", "timeout": "Tempo scaduto per la validazione del codice." diff --git a/homeassistant/components/uptimerobot/translations/it.json b/homeassistant/components/uptimerobot/translations/it.json index 521f2417125..3a6c87c5ea9 100644 --- a/homeassistant/components/uptimerobot/translations/it.json +++ b/homeassistant/components/uptimerobot/translations/it.json @@ -18,7 +18,7 @@ "api_key": "Chiave API" }, "description": "Devi fornire una nuova chiave API di sola lettura da UptimeRobot", - "title": "Autenticare nuovamente l'integrazione" + "title": "Autentica nuovamente l'integrazione" }, "user": { "data": { diff --git a/homeassistant/components/verisure/translations/it.json b/homeassistant/components/verisure/translations/it.json index 1c90871db71..e30976a37a6 100644 --- a/homeassistant/components/verisure/translations/it.json +++ b/homeassistant/components/verisure/translations/it.json @@ -18,14 +18,14 @@ "reauth_confirm": { "data": { "description": "Autenticati nuovamente con il tuo account Verisure My Pages.", - "email": "E-mail", + "email": "Email", "password": "Password" } }, "user": { "data": { "description": "Accedi con il tuo account Verisure My Pages.", - "email": "E-mail", + "email": "Email", "password": "Password" } } diff --git a/homeassistant/components/vesync/translations/it.json b/homeassistant/components/vesync/translations/it.json index f4addae1e1f..38f0c519ff1 100644 --- a/homeassistant/components/vesync/translations/it.json +++ b/homeassistant/components/vesync/translations/it.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "Password", - "username": "E-mail" + "username": "Email" }, "title": "Immettere nome utente e password" } diff --git a/homeassistant/components/vicare/translations/bg.json b/homeassistant/components/vicare/translations/bg.json new file mode 100644 index 00000000000..64c6b2b67a6 --- /dev/null +++ b/homeassistant/components/vicare/translations/bg.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "client_id": "API \u043a\u043b\u044e\u0447", + "name": "\u0418\u043c\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "Email" + }, + "title": "{name}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vicare/translations/fr.json b/homeassistant/components/vicare/translations/fr.json new file mode 100644 index 00000000000..48560a1bb3a --- /dev/null +++ b/homeassistant/components/vicare/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "unknown": "Erreur inatendue" + }, + "error": { + "invalid_auth": "Authentification invalide" + }, + "step": { + "user": { + "data": { + "client_id": "Cl\u00e9 API", + "name": "Nom", + "password": "Mot de passe", + "username": "Email" + }, + "description": "Configurer l'int\u00e9gration ViCare. Pour g\u00e9n\u00e9rer une cl\u00e9 API se rendre sur https://developer.viessmann.com" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vicare/translations/id.json b/homeassistant/components/vicare/translations/id.json new file mode 100644 index 00000000000..0950534fe32 --- /dev/null +++ b/homeassistant/components/vicare/translations/id.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "error": { + "invalid_auth": "Autentikasi tidak valid" + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "client_id": "Kunci API", + "name": "Nama", + "password": "Kata Sandi", + "username": "Email" + }, + "title": "{name}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vicare/translations/it.json b/homeassistant/components/vicare/translations/it.json new file mode 100644 index 00000000000..151e0c84509 --- /dev/null +++ b/homeassistant/components/vicare/translations/it.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione.", + "unknown": "Errore imprevisto" + }, + "error": { + "invalid_auth": "Autenticazione non valida" + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "client_id": "Chiave API", + "heating_type": "Modalit\u00e0 di riscaldamento", + "name": "Nome", + "password": "Password", + "scan_interval": "Intervallo di scansione (secondi)", + "username": "Email" + }, + "description": "Configura l'integrazione ViCare. Per generare la chiave API vai su https://developer.viessmann.com", + "title": "{name}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vicare/translations/nl.json b/homeassistant/components/vicare/translations/nl.json new file mode 100644 index 00000000000..d16758be360 --- /dev/null +++ b/homeassistant/components/vicare/translations/nl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk.", + "unknown": "Onverwachte fout" + }, + "error": { + "invalid_auth": "Ongeldige authenticatie" + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "client_id": "API-sleutel", + "heating_type": "Verwarmingstype", + "name": "Naam", + "password": "Wachtwoord", + "scan_interval": "Scaninterval (seconden)", + "username": "E-mail" + }, + "description": "ViCare integratie instellen. Ga naar https://developer.viessmann.com om een API-sleutel te genereren.", + "title": "{name}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vicare/translations/no.json b/homeassistant/components/vicare/translations/no.json new file mode 100644 index 00000000000..0f88d6b219d --- /dev/null +++ b/homeassistant/components/vicare/translations/no.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "invalid_auth": "Ugyldig godkjenning" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "username": "E-post" + }, + "title": "" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/translations/it.json b/homeassistant/components/vizio/translations/it.json index e7efb348332..6dad0225765 100644 --- a/homeassistant/components/vizio/translations/it.json +++ b/homeassistant/components/vizio/translations/it.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "Impossibile connettersi", "complete_pairing_failed": "Impossibile completare l'associazione. Assicurarsi che il PIN fornito sia corretto e che la TV sia ancora accesa e collegata alla rete prima di inviare nuovamente.", - "existing_config_entry_found": "\u00c8 gi\u00e0 stata configurata una voce di configurazione esistente Dispositivo SmartCast VIZIO con lo stesso numero di serie. \u00c8 necessario eliminare la voce esistente per configurare questa." + "existing_config_entry_found": "\u00c8 gi\u00e0 stata configurata una voce di configurazione esistente Dispositivo SmartCast VIZIO con lo stesso numero di serie. Devi eliminare la voce esistente per configurare questa." }, "step": { "pair_tv": { diff --git a/homeassistant/components/wallbox/translations/it.json b/homeassistant/components/wallbox/translations/it.json index 112a0c13970..ce4bdef9d95 100644 --- a/homeassistant/components/wallbox/translations/it.json +++ b/homeassistant/components/wallbox/translations/it.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida", - "reauth_invalid": "Riautenticazione non riuscita; Il numero di serie non corrisponde all'originale", + "reauth_invalid": "Nuova autenticazione non riuscita; Il numero di serie non corrisponde all'originale", "unknown": "Errore imprevisto" }, "step": { diff --git a/homeassistant/components/watttime/translations/it.json b/homeassistant/components/watttime/translations/it.json index ecca75e5b5d..c5142df73b8 100644 --- a/homeassistant/components/watttime/translations/it.json +++ b/homeassistant/components/watttime/translations/it.json @@ -15,7 +15,7 @@ "latitude": "Latitudine", "longitude": "Logitudine" }, - "description": "Immettere la latitudine e la longitudine da monitorare:" + "description": "Immetti la latitudine e la longitudine da monitorare:" }, "location": { "data": { @@ -27,8 +27,8 @@ "data": { "password": "Password" }, - "description": "Si prega di reinserire la password per {username}:", - "title": "Autenticare nuovamente l'integrazione" + "description": "Digita nuovamente la password per {username}:", + "title": "Autentica nuovamente l'integrazione" }, "user": { "data": { diff --git a/homeassistant/components/withings/translations/it.json b/homeassistant/components/withings/translations/it.json index 77e29c71e0c..f97933593a0 100644 --- a/homeassistant/components/withings/translations/it.json +++ b/homeassistant/components/withings/translations/it.json @@ -26,7 +26,7 @@ }, "reauth": { "description": "Il profilo \"{profile}\" deve essere autenticato nuovamente per continuare a ricevere i dati Withings.", - "title": "Autenticare nuovamente l'integrazione" + "title": "Autentica nuovamente l'integrazione" } } } diff --git a/homeassistant/components/xiaomi_miio/translations/it.json b/homeassistant/components/xiaomi_miio/translations/it.json index df69b9a2736..56198ab443a 100644 --- a/homeassistant/components/xiaomi_miio/translations/it.json +++ b/homeassistant/components/xiaomi_miio/translations/it.json @@ -14,7 +14,7 @@ "cloud_no_devices": "Nessun dispositivo trovato in questo account cloud Xiaomi Miio.", "no_device_selected": "Nessun dispositivo selezionato, selezionare un dispositivo.", "unknown_device": "Il modello del dispositivo non \u00e8 noto, non \u00e8 possibile configurare il dispositivo utilizzando il flusso di configurazione.", - "wrong_token": "Errore di checksum, token errato" + "wrong_token": "Errore del codice di controllo, token errato" }, "flow_title": "{name}", "step": { @@ -51,7 +51,7 @@ "name": "Nome del Gateway", "token": "Token API" }, - "description": "E' necessaria la Token API di 32 caratteri, vedere https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token per le istruzioni. Notare che questa Token API \u00e8 differente dalla chiave usata dall'integrazione di Xiaomi Aqara.", + "description": "\u00c8 necessaria la Token API di 32 caratteri, vedi https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token per le istruzioni. Nota che questa Token API \u00e8 differente dalla chiave usata dall'integrazione di Xiaomi Aqara.", "title": "Connessione a un Xiaomi Gateway " }, "manual": { @@ -63,8 +63,8 @@ "title": "Connettiti a un dispositivo Xiaomi Miio o Xiaomi Gateway" }, "reauth_confirm": { - "description": "L'integrazione di Xiaomi Miio deve riautenticare il tuo account per aggiornare i token o aggiungere credenziali cloud mancanti.", - "title": "Autenticare nuovamente l'integrazione" + "description": "L'integrazione di Xiaomi Miio deve autenticare nuovamente il tuo account per aggiornare i token o aggiungere credenziali cloud mancanti.", + "title": "Autentica nuovamente l'integrazione" }, "select": { "data": { From a7cae0272512186d624af5e2e5297011d91d870e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 20 Dec 2021 20:35:50 -0600 Subject: [PATCH 0862/2644] Add support for changing Magic Home socket power restore state (#62301) --- homeassistant/components/flux_led/__init__.py | 2 +- homeassistant/components/flux_led/select.py | 67 +++++++++++++++++++ tests/components/flux_led/__init__.py | 9 ++- tests/components/flux_led/test_select.py | 49 ++++++++++++++ 4 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/flux_led/select.py create mode 100644 tests/components/flux_led/test_select.py diff --git a/homeassistant/components/flux_led/__init__.py b/homeassistant/components/flux_led/__init__.py index 25f8b2554ea..0630f49de84 100644 --- a/homeassistant/components/flux_led/__init__.py +++ b/homeassistant/components/flux_led/__init__.py @@ -45,7 +45,7 @@ PLATFORMS_BY_TYPE: Final = { Platform.NUMBER, Platform.SWITCH, ], - DeviceType.Switch: [Platform.BUTTON, Platform.SWITCH], + DeviceType.Switch: [Platform.BUTTON, Platform.SELECT, Platform.SWITCH], } DISCOVERY_INTERVAL: Final = timedelta(minutes=15) REQUEST_REFRESH_DELAY: Final = 1.5 diff --git a/homeassistant/components/flux_led/select.py b/homeassistant/components/flux_led/select.py new file mode 100644 index 00000000000..92b5b936784 --- /dev/null +++ b/homeassistant/components/flux_led/select.py @@ -0,0 +1,67 @@ +"""Support for Magic Home select.""" +from __future__ import annotations + +from flux_led.aio import AIOWifiLedBulb +from flux_led.protocol import PowerRestoreState + +from homeassistant import config_entries +from homeassistant.components.select import SelectEntity +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import FluxLedUpdateCoordinator +from .entity import FluxBaseEntity + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Flux selects.""" + coordinator: FluxLedUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities([FluxPowerState(coordinator.device, entry)]) + + +def _human_readable_option(const_option: str) -> str: + return const_option.replace("_", " ").title() + + +class FluxPowerState(FluxBaseEntity, SelectEntity): + """Representation of a Flux power restore state option.""" + + _attr_should_poll = False + + def __init__( + self, + device: AIOWifiLedBulb, + entry: config_entries.ConfigEntry, + ) -> None: + """Initialize the power state select.""" + super().__init__(device, entry) + self._attr_entity_category = EntityCategory.CONFIG + self._attr_name = f"{entry.data[CONF_NAME]} Power Restored" + if entry.unique_id: + self._attr_unique_id = f"{entry.unique_id}_power_restored" + self._name_to_state = { + _human_readable_option(option.name): option for option in PowerRestoreState + } + self._attr_options = list(self._name_to_state) + self._async_set_current_option_from_device() + + @callback + def _async_set_current_option_from_device(self) -> None: + """Set the option from the current power state.""" + restore_states = self._device.power_restore_states + assert restore_states is not None + assert restore_states.channel1 is not None + self._attr_current_option = _human_readable_option(restore_states.channel1.name) + + async def async_select_option(self, option: str) -> None: + """Change the power state.""" + await self._device.async_set_power_restore(channel1=self._name_to_state[option]) + self._async_set_current_option_from_device() + self.async_write_ha_state() diff --git a/tests/components/flux_led/__init__.py b/tests/components/flux_led/__init__.py index d23c4281481..ae03726fdfa 100644 --- a/tests/components/flux_led/__init__.py +++ b/tests/components/flux_led/__init__.py @@ -14,7 +14,7 @@ from flux_led.const import ( COLOR_MODE_RGB as FLUX_COLOR_MODE_RGB, ) from flux_led.models_db import MODEL_MAP -from flux_led.protocol import LEDENETRawState +from flux_led.protocol import LEDENETRawState, PowerRestoreState, PowerRestoreStates from flux_led.scanner import FluxLEDDiscovery from homeassistant.components import dhcp @@ -124,9 +124,16 @@ def _mocked_switch() -> AIOWifiLedBulb: switch.data_receive_callback = callback switch.device_type = DeviceType.Switch + switch.power_restore_states = PowerRestoreStates( + channel1=PowerRestoreState.LAST_STATE, + channel2=PowerRestoreState.LAST_STATE, + channel3=PowerRestoreState.LAST_STATE, + channel4=PowerRestoreState.LAST_STATE, + ) switch.requires_turn_on = True switch.async_reboot = AsyncMock() switch.async_setup = AsyncMock(side_effect=_save_setup_callback) + switch.async_set_power_restore = AsyncMock() switch.async_stop = AsyncMock() switch.async_update = AsyncMock() switch.async_turn_off = AsyncMock() diff --git a/tests/components/flux_led/test_select.py b/tests/components/flux_led/test_select.py new file mode 100644 index 00000000000..01a92e5a350 --- /dev/null +++ b/tests/components/flux_led/test_select.py @@ -0,0 +1,49 @@ +"""Tests for select platform.""" +from flux_led.protocol import PowerRestoreState + +from homeassistant.components import flux_led +from homeassistant.components.flux_led.const import DOMAIN +from homeassistant.components.select import DOMAIN as SELECT_DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION, CONF_HOST, CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from . import ( + DEFAULT_ENTRY_TITLE, + IP_ADDRESS, + MAC_ADDRESS, + _mocked_switch, + _patch_discovery, + _patch_wifibulb, +) + +from tests.common import MockConfigEntry + + +async def test_switch_power_restore_state(hass: HomeAssistant) -> None: + """Test a smart plug power restore state.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + switch = _mocked_switch() + with _patch_discovery(), _patch_wifibulb(device=switch): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "select.bulb_rgbcw_ddeeff_power_restored" + + state = hass.states.get(entity_id) + assert state.state == "Last State" + + await hass.services.async_call( + SELECT_DOMAIN, + "select_option", + {ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "Always On"}, + blocking=True, + ) + switch.async_set_power_restore.assert_called_once_with( + channel1=PowerRestoreState.ALWAYS_ON + ) From d60540d4f5ad09ed2f22478129b3be4ae646048a Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Mon, 20 Dec 2021 20:34:34 -0800 Subject: [PATCH 0863/2644] Cast types in wemo rather than converting (#62454) --- .../components/wemo/binary_sensor.py | 12 +++------ homeassistant/components/wemo/entity.py | 20 ++++++++++++--- homeassistant/components/wemo/fan.py | 13 ++-------- homeassistant/components/wemo/light.py | 25 +++++++------------ homeassistant/components/wemo/sensor.py | 3 ++- homeassistant/components/wemo/switch.py | 13 +++------- 6 files changed, 37 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/wemo/binary_sensor.py b/homeassistant/components/wemo/binary_sensor.py index 766ca61c560..cde13d632fe 100644 --- a/homeassistant/components/wemo/binary_sensor.py +++ b/homeassistant/components/wemo/binary_sensor.py @@ -1,5 +1,6 @@ """Support for WeMo binary sensors.""" import asyncio +from typing import cast from pywemo import Insight, Maker @@ -10,7 +11,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN as WEMO_DOMAIN -from .entity import WemoEntity +from .entity import WemoBinaryStateEntity, WemoEntity from .wemo_device import DeviceCoordinator @@ -40,14 +41,9 @@ async def async_setup_entry( ) -class WemoBinarySensor(WemoEntity, BinarySensorEntity): +class WemoBinarySensor(WemoBinaryStateEntity, BinarySensorEntity): """Representation a WeMo binary sensor.""" - @property - def is_on(self) -> bool: - """Return true if the state is on. Standby is on.""" - return bool(self.wemo.get_state()) - class MakerBinarySensor(WemoEntity, BinarySensorEntity): """Maker device's sensor port.""" @@ -57,7 +53,7 @@ class MakerBinarySensor(WemoEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Return true if the Maker's sensor is pulled low.""" - return bool(self.wemo.has_sensor) and self.wemo.sensor_state == 0 + return cast(int, self.wemo.has_sensor) != 0 and self.wemo.sensor_state == 0 class InsightBinarySensor(WemoBinarySensor): diff --git a/homeassistant/components/wemo/entity.py b/homeassistant/components/wemo/entity.py index 824fcacae7c..44f6b5a4e63 100644 --- a/homeassistant/components/wemo/entity.py +++ b/homeassistant/components/wemo/entity.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Generator import contextlib import logging +from typing import cast from pywemo.exceptions import ActionException @@ -40,9 +41,10 @@ class WemoEntity(CoordinatorEntity): @property def name(self) -> str: """Return the name of the device if any.""" + wemo_name: str = self.wemo.name if suffix := self.name_suffix: - return f"{self.wemo.name} {suffix}" - return str(self.wemo.name) + return f"{wemo_name} {suffix}" + return wemo_name @property def available(self) -> bool: @@ -59,9 +61,10 @@ class WemoEntity(CoordinatorEntity): @property def unique_id(self) -> str: """Return the id of this WeMo device.""" + serial_number: str = self.wemo.serialnumber if suffix := self.unique_id_suffix: - return f"{self.wemo.serialnumber}_{suffix}" - return str(self.wemo.serialnumber) + return f"{serial_number}_{suffix}" + return serial_number @property def device_info(self) -> DeviceInfo: @@ -82,3 +85,12 @@ class WemoEntity(CoordinatorEntity): except ActionException as err: _LOGGER.warning("Could not %s for %s (%s)", message, self.name, err) self._available = False + + +class WemoBinaryStateEntity(WemoEntity): + """Base for devices that return on/off state via device.get_state().""" + + @property + def is_on(self) -> bool: + """Return true if the state is on.""" + return cast(int, self.wemo.get_state()) != 0 diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index 003dfa7e633..03c5964a313 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -25,7 +25,7 @@ from .const import ( SERVICE_RESET_FILTER_LIFE, SERVICE_SET_HUMIDITY, ) -from .entity import WemoEntity +from .entity import WemoBinaryStateEntity from .wemo_device import DeviceCoordinator SCAN_INTERVAL = timedelta(seconds=10) @@ -38,10 +38,6 @@ ATTR_FILTER_LIFE = "filter_life" ATTR_FILTER_EXPIRED = "filter_expired" ATTR_WATER_LEVEL = "water_level" -# The WEMO_ constants below come from pywemo itself -WEMO_ON = 1 -WEMO_OFF = 0 - WEMO_HUMIDITY_45 = 0 WEMO_HUMIDITY_50 = 1 WEMO_HUMIDITY_55 = 2 @@ -102,7 +98,7 @@ async def async_setup_entry( ) -class WemoHumidifier(WemoEntity, FanEntity): +class WemoHumidifier(WemoBinaryStateEntity, FanEntity): """Representation of a WeMo humidifier.""" def __init__(self, coordinator: DeviceCoordinator) -> None: @@ -152,11 +148,6 @@ class WemoHumidifier(WemoEntity, FanEntity): self._last_fan_on_mode = self.wemo.fan_mode super()._handle_coordinator_update() - @property - def is_on(self) -> bool: - """Return true if the state is on.""" - return bool(self.wemo.get_state()) - def turn_on( self, speed: str | None = None, diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py index eb19355f2b3..fbb1c84e449 100644 --- a/homeassistant/components/wemo/light.py +++ b/homeassistant/components/wemo/light.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from typing import Any +from typing import Any, Optional, cast from pywemo.ouimeaux_device import bridge @@ -26,7 +26,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.color as color_util from .const import DOMAIN as WEMO_DOMAIN -from .entity import WemoEntity +from .entity import WemoBinaryStateEntity, WemoEntity from .wemo_device import DeviceCoordinator SUPPORT_WEMO = ( @@ -101,7 +101,7 @@ class WemoLight(WemoEntity, LightEntity): @property def name(self) -> str: """Return the name of the device if any.""" - return str(self.light.name) + return cast(str, self.light.name) @property def available(self) -> bool: @@ -111,7 +111,7 @@ class WemoLight(WemoEntity, LightEntity): @property def unique_id(self) -> str: """Return the ID of this light.""" - return str(self.light.uniqueID) + return cast(str, self.light.uniqueID) @property def device_info(self) -> DeviceInfo: @@ -127,7 +127,7 @@ class WemoLight(WemoEntity, LightEntity): @property def brightness(self) -> int: """Return the brightness of this light between 0..255.""" - return int(self.light.state.get("level", 255)) + return cast(int, self.light.state.get("level", 255)) @property def hs_color(self) -> tuple[float, float] | None: @@ -139,14 +139,12 @@ class WemoLight(WemoEntity, LightEntity): @property def color_temp(self) -> int | None: """Return the color temperature of this light in mireds.""" - if (temp := self.light.state.get("temperature_mireds")) is not None: - return int(temp) - return None + return cast(Optional[int], self.light.state.get("temperature_mireds")) @property def is_on(self) -> bool: """Return true if device is on.""" - return bool(self.light.state.get("onoff") != WEMO_OFF) + return cast(int, self.light.state.get("onoff")) != WEMO_OFF @property def supported_features(self) -> int: @@ -194,7 +192,7 @@ class WemoLight(WemoEntity, LightEntity): self.schedule_update_ha_state() -class WemoDimmer(WemoEntity, LightEntity): +class WemoDimmer(WemoBinaryStateEntity, LightEntity): """Representation of a WeMo dimmer.""" @property @@ -205,14 +203,9 @@ class WemoDimmer(WemoEntity, LightEntity): @property def brightness(self) -> int: """Return the brightness of this light between 1 and 100.""" - wemo_brightness = int(self.wemo.get_brightness()) + wemo_brightness: int = self.wemo.get_brightness() return int((wemo_brightness * 255) / 100) - @property - def is_on(self) -> bool: - """Return true if the state is on.""" - return bool(self.wemo.get_state()) - def turn_on(self, **kwargs: Any) -> None: """Turn the dimmer on.""" # Wemo dimmer switches use a range of [0, 100] to control diff --git a/homeassistant/components/wemo/sensor.py b/homeassistant/components/wemo/sensor.py index c46e8928a03..eed5c510936 100644 --- a/homeassistant/components/wemo/sensor.py +++ b/homeassistant/components/wemo/sensor.py @@ -49,7 +49,8 @@ class InsightSensor(WemoEntity, SensorEntity): @property def name_suffix(self) -> str: """Return the name of the entity if any.""" - return str(self.entity_description.name) + assert self.entity_description.name + return self.entity_description.name @property def unique_id_suffix(self) -> str: diff --git a/homeassistant/components/wemo/switch.py b/homeassistant/components/wemo/switch.py index 39a6219b98b..9982274bbc0 100644 --- a/homeassistant/components/wemo/switch.py +++ b/homeassistant/components/wemo/switch.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio from datetime import datetime, timedelta -from typing import Any +from typing import Any, cast from pywemo import CoffeeMaker, Insight, Maker @@ -16,7 +16,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import convert from .const import DOMAIN as WEMO_DOMAIN -from .entity import WemoEntity +from .entity import WemoBinaryStateEntity from .wemo_device import DeviceCoordinator SCAN_INTERVAL = timedelta(seconds=10) @@ -57,7 +57,7 @@ async def async_setup_entry( ) -class WemoSwitch(WemoEntity, SwitchEntity): +class WemoSwitch(WemoBinaryStateEntity, SwitchEntity): """Representation of a WeMo switch.""" @property @@ -133,7 +133,7 @@ class WemoSwitch(WemoEntity, SwitchEntity): def detail_state(self) -> str: """Return the state of the device.""" if isinstance(self.wemo, CoffeeMaker): - return str(self.wemo.mode_string) + return cast(str, self.wemo.mode_string) if isinstance(self.wemo, Insight): standby_state = int(self.wemo.insight_params.get("state", 0)) if standby_state == WEMO_ON: @@ -152,11 +152,6 @@ class WemoSwitch(WemoEntity, SwitchEntity): return "mdi:coffee" return None - @property - def is_on(self) -> bool: - """Return true if the state is on. Standby is on.""" - return bool(self.wemo.get_state()) - def turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" with self._wemo_exception_handler("turn on"): From d73081f875ebf1e0adbfa360f74e2baf4cdcc0a2 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 21 Dec 2021 04:01:43 -0500 Subject: [PATCH 0864/2644] Remove deprecated yaml config from nuki (#62470) --- .../components/nuki/binary_sensor.py | 4 --- homeassistant/components/nuki/config_flow.py | 4 --- homeassistant/components/nuki/lock.py | 22 +------------- tests/components/nuki/test_config_flow.py | 30 ------------------- 4 files changed, 1 insertion(+), 59 deletions(-) diff --git a/homeassistant/components/nuki/binary_sensor.py b/homeassistant/components/nuki/binary_sensor.py index 52cf1090d9e..08ad20e7c87 100644 --- a/homeassistant/components/nuki/binary_sensor.py +++ b/homeassistant/components/nuki/binary_sensor.py @@ -1,7 +1,5 @@ """Doorsensor Support for the Nuki Lock.""" -import logging - from pynuki import STATE_DOORSENSOR_OPENED from homeassistant.components.binary_sensor import ( @@ -12,8 +10,6 @@ from homeassistant.components.binary_sensor import ( from . import NukiEntity from .const import ATTR_NUKI_ID, DATA_COORDINATOR, DATA_LOCKS, DOMAIN as NUKI_DOMAIN -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry(hass, entry, async_add_entities): """Set up the Nuki lock binary sensor.""" diff --git a/homeassistant/components/nuki/config_flow.py b/homeassistant/components/nuki/config_flow.py index bd2c5a0d750..dae1d82a104 100644 --- a/homeassistant/components/nuki/config_flow.py +++ b/homeassistant/components/nuki/config_flow.py @@ -59,10 +59,6 @@ class NukiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.discovery_schema = {} self._data = {} - async def async_step_import(self, user_input=None): - """Handle a flow initiated by import.""" - return await self.async_step_validate(user_input) - async def async_step_user(self, user_input=None): """Handle a flow initiated by the user.""" return await self.async_step_validate(user_input) diff --git a/homeassistant/components/nuki/lock.py b/homeassistant/components/nuki/lock.py index 25644a49f0f..f2ca85c5896 100644 --- a/homeassistant/components/nuki/lock.py +++ b/homeassistant/components/nuki/lock.py @@ -1,12 +1,10 @@ """Nuki.io lock platform.""" from abc import ABC, abstractmethod -import logging from pynuki import MODE_OPENER_CONTINUOUS import voluptuous as vol -from homeassistant.components.lock import PLATFORM_SCHEMA, SUPPORT_OPEN, LockEntity -from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN +from homeassistant.components.lock import SUPPORT_OPEN, LockEntity from homeassistant.helpers import config_validation as cv, entity_platform from . import NukiEntity @@ -18,28 +16,10 @@ from .const import ( DATA_COORDINATOR, DATA_LOCKS, DATA_OPENERS, - DEFAULT_PORT, DOMAIN as NUKI_DOMAIN, ERROR_STATES, ) -_LOGGER = logging.getLogger(__name__) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Required(CONF_TOKEN): cv.string, - } -) - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Nuki lock platform.""" - _LOGGER.warning( - "Loading Nuki by lock platform configuration is deprecated and will be removed in the future" - ) - async def async_setup_entry(hass, entry, async_add_entities): """Set up the Nuki lock platform.""" diff --git a/tests/components/nuki/test_config_flow.py b/tests/components/nuki/test_config_flow.py index fd7bfa2137b..d9e2ca3de50 100644 --- a/tests/components/nuki/test_config_flow.py +++ b/tests/components/nuki/test_config_flow.py @@ -51,36 +51,6 @@ async def test_form(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_import(hass): - """Test that the import works.""" - - with patch( - "homeassistant.components.nuki.config_flow.NukiBridge.info", - return_value=MOCK_INFO, - ), patch( - "homeassistant.components.nuki.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.nuki.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={"host": "1.1.1.1", "port": 8080, "token": "test-token"}, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == 123456789 - assert result["data"] == { - "host": "1.1.1.1", - "port": 8080, - "token": "test-token", - } - - await hass.async_block_till_done() - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 - - async def test_form_invalid_auth(hass): """Test we handle invalid auth.""" result = await hass.config_entries.flow.async_init( From 7cdfc7558e8a362e1356f3bacfaa5bf3d46794b7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 21 Dec 2021 10:31:41 +0100 Subject: [PATCH 0865/2644] Cleanup stale setup/import from Nuki (#62476) * Cleanup stale setup/import from Nuki * Adjust tests --- homeassistant/components/nuki/__init__.py | 35 +---------------------- tests/components/nuki/test_config_flow.py | 8 +----- 2 files changed, 2 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index a7393666204..2397b211e48 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -9,14 +9,7 @@ from pynuki.bridge import InvalidCredentialsException from requests.exceptions import RequestException from homeassistant import exceptions -from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import ( - CONF_HOST, - CONF_PLATFORM, - CONF_PORT, - CONF_TOKEN, - Platform, -) +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN, Platform from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -28,7 +21,6 @@ from .const import ( DATA_COORDINATOR, DATA_LOCKS, DATA_OPENERS, - DEFAULT_PORT, DEFAULT_TIMEOUT, DOMAIN, ERROR_STATES, @@ -56,31 +48,6 @@ def _update_devices(devices): break -async def async_setup(hass, config): - """Set up the Nuki component.""" - hass.data.setdefault(DOMAIN, {}) - - for platform in PLATFORMS: - if (confs := config.get(platform)) is None: - continue - - for conf in confs: - if CONF_PLATFORM in conf and conf[CONF_PLATFORM] == DOMAIN: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_HOST: conf[CONF_HOST], - CONF_PORT: conf.get(CONF_PORT, DEFAULT_PORT), - CONF_TOKEN: conf[CONF_TOKEN], - }, - ) - ) - - return True - - async def async_setup_entry(hass, entry): """Set up the Nuki entry.""" diff --git a/tests/components/nuki/test_config_flow.py b/tests/components/nuki/test_config_flow.py index d9e2ca3de50..afd941ef00a 100644 --- a/tests/components/nuki/test_config_flow.py +++ b/tests/components/nuki/test_config_flow.py @@ -25,8 +25,6 @@ async def test_form(hass): "homeassistant.components.nuki.config_flow.NukiBridge.info", return_value=MOCK_INFO, ), patch( - "homeassistant.components.nuki.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.nuki.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -47,7 +45,6 @@ async def test_form(hass): "port": 8080, "token": "test-token", } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -159,8 +156,6 @@ async def test_dhcp_flow(hass): "homeassistant.components.nuki.config_flow.NukiBridge.info", return_value=MOCK_INFO, ), patch( - "homeassistant.components.nuki.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.nuki.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -182,7 +177,6 @@ async def test_dhcp_flow(hass): } await hass.async_block_till_done() - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -212,7 +206,7 @@ async def test_reauth_success(hass): with patch( "homeassistant.components.nuki.config_flow.NukiBridge.info", return_value=MOCK_INFO, - ), patch("homeassistant.components.nuki.async_setup", return_value=True), patch( + ), patch( "homeassistant.components.nuki.async_setup_entry", return_value=True, ): From 7fb79b363aba186bceecaefb15352ef4bce6d316 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 21 Dec 2021 10:35:48 +0100 Subject: [PATCH 0866/2644] Enable PYTHONASYNCIODEBUG in tests (#62104) --- .github/workflows/ci.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 23bfe6adbaf..fbaba7ba7e2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -14,6 +14,7 @@ env: DEFAULT_PYTHON: 3.8 PRE_COMMIT_CACHE: ~/.cache/pre-commit SQLALCHEMY_WARN_20: 1 + PYTHONASYNCIODEBUG: 1 concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} From 64e3383b72ccb3f3f72cd303cc7ac3f85e6a3a55 Mon Sep 17 00:00:00 2001 From: Mark Zachmann Date: Tue, 21 Dec 2021 04:37:29 -0500 Subject: [PATCH 0867/2644] Use on_level when turning an Insteon dimmer on (#62321) Co-authored-by: Franck Nijhof --- homeassistant/components/insteon/insteon_entity.py | 12 +++++++++++- homeassistant/components/insteon/light.py | 5 +++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/insteon/insteon_entity.py b/homeassistant/components/insteon/insteon_entity.py index 719b58a3d9f..d8fd9b2cbc9 100644 --- a/homeassistant/components/insteon/insteon_entity.py +++ b/homeassistant/components/insteon/insteon_entity.py @@ -75,7 +75,10 @@ class InsteonEntity(Entity): @property def extra_state_attributes(self): """Provide attributes for display on device card.""" - return {"insteon_address": self.address, "insteon_group": self.group} + return { + "insteon_address": self.address, + "insteon_group": self.group, + } @property def device_info(self) -> DeviceInfo: @@ -148,6 +151,13 @@ class InsteonEntity(Entity): """Print the device ALDB to the log file.""" print_aldb_to_log(self._insteon_device.aldb) + def get_device_property(self, name: str): + """Get a single Insteon device property value (raw).""" + value = None + if (prop := self._insteon_device.properties.get(name)) is not None: + value = prop.value if prop.new_value is None else prop.new_value + return value + def _get_label(self): """Get the device label for grouped devices.""" label = "" diff --git a/homeassistant/components/insteon/light.py b/homeassistant/components/insteon/light.py index 206aa078dc3..e8b95598fb8 100644 --- a/homeassistant/components/insteon/light.py +++ b/homeassistant/components/insteon/light.py @@ -1,5 +1,7 @@ """Support for Insteon lights via PowerLinc Modem.""" +from pyinsteon.extended_property import ON_LEVEL + from homeassistant.components.light import ( ATTR_BRIGHTNESS, DOMAIN as LIGHT_DOMAIN, @@ -53,6 +55,9 @@ class InsteonDimmerEntity(InsteonEntity, LightEntity): """Turn light on.""" if ATTR_BRIGHTNESS in kwargs: brightness = int(kwargs[ATTR_BRIGHTNESS]) + else: + brightness = self.get_device_property(ON_LEVEL) + if brightness is not None: await self._insteon_device.async_on( on_level=brightness, group=self._insteon_device_group.group ) From 3898dfd2486667f9183f3e4268af14d45571a910 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Dec 2021 10:40:04 +0100 Subject: [PATCH 0868/2644] Bump docker/login-action from 1.10.0 to 1.12.0 (#62462) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index e7dc7ebb270..884e7585dcd 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -118,13 +118,13 @@ jobs: echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE - name: Login to DockerHub - uses: docker/login-action@v1.10.0 + uses: docker/login-action@v1.12.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v1.10.0 + uses: docker/login-action@v1.12.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -182,13 +182,13 @@ jobs: fi - name: Login to DockerHub - uses: docker/login-action@v1.10.0 + uses: docker/login-action@v1.12.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v1.10.0 + uses: docker/login-action@v1.12.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -245,13 +245,13 @@ jobs: uses: actions/checkout@v2.4.0 - name: Login to DockerHub - uses: docker/login-action@v1.10.0 + uses: docker/login-action@v1.12.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v1.10.0 + uses: docker/login-action@v1.12.0 with: registry: ghcr.io username: ${{ github.repository_owner }} From d1980e7351e2c47c78b939a9d69e17e89e42008a Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 21 Dec 2021 04:53:07 -0500 Subject: [PATCH 0869/2644] Remove deprecated yaml config from honeywell (#62469) --- homeassistant/components/honeywell/climate.py | 57 +------------------ .../components/honeywell/config_flow.py | 13 +---- homeassistant/components/honeywell/const.py | 2 - .../components/honeywell/test_config_flow.py | 14 +---- 4 files changed, 4 insertions(+), 82 deletions(-) diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 6c686e92b8e..57ce0125c93 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -5,9 +5,8 @@ import datetime from typing import Any import somecomfort -import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, @@ -31,25 +30,12 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, ) -from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import ( - ATTR_TEMPERATURE, - CONF_PASSWORD, - CONF_REGION, - CONF_USERNAME, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, -) -import homeassistant.helpers.config_validation as cv +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from .const import ( _LOGGER, CONF_COOL_AWAY_TEMPERATURE, - CONF_DEV_ID, CONF_HEAT_AWAY_TEMPERATURE, - CONF_LOC_ID, - DEFAULT_COOL_AWAY_TEMPERATURE, - DEFAULT_HEAT_AWAY_TEMPERATURE, DOMAIN, ) @@ -59,25 +45,6 @@ ATTR_PERMANENT_HOLD = "permanent_hold" PRESET_HOLD = "Hold" -PLATFORM_SCHEMA = vol.All( - cv.deprecated(CONF_REGION), - PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional( - CONF_COOL_AWAY_TEMPERATURE, default=DEFAULT_COOL_AWAY_TEMPERATURE - ): vol.Coerce(int), - vol.Optional( - CONF_HEAT_AWAY_TEMPERATURE, default=DEFAULT_HEAT_AWAY_TEMPERATURE - ): vol.Coerce(int), - vol.Optional(CONF_REGION): cv.string, - vol.Optional(CONF_DEV_ID): cv.string, - vol.Optional(CONF_LOC_ID): cv.string, - } - ), -) - HVAC_MODE_TO_HW_MODE = { "SwitchOffAllowed": {HVAC_MODE_OFF: "off"}, "SwitchAutoAllowed": {HVAC_MODE_HEAT_COOL: "auto"}, @@ -127,26 +94,6 @@ async def async_setup_entry(hass, config, async_add_entities, discovery_info=Non ) -async def async_setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Honeywell climate platform. - - Honeywell uses config flow for configuration now. If an entry exists in - configuration.yaml, the import flow will attempt to import it and create - a config entry. - """ - - if config["platform"] == "honeywell": - _LOGGER.warning( - "Loading honeywell via platform config is deprecated; The configuration" - " has been migrated to a config entry and can be safely removed" - ) - # No config entry exists and configuration.yaml config exists, trigger the import flow. - if not hass.config_entries.async_entries(DOMAIN): - await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config - ) - - class HoneywellUSThermostat(ClimateEntity): """Representation of a Honeywell US Thermostat.""" diff --git a/homeassistant/components/honeywell/config_flow.py b/homeassistant/components/honeywell/config_flow.py index ecf42f4533f..505a49f062b 100644 --- a/homeassistant/components/honeywell/config_flow.py +++ b/homeassistant/components/honeywell/config_flow.py @@ -5,7 +5,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from . import get_somecomfort_client -from .const import CONF_COOL_AWAY_TEMPERATURE, CONF_HEAT_AWAY_TEMPERATURE, DOMAIN +from .const import DOMAIN class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -42,14 +42,3 @@ class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return client is not None - - async def async_step_import(self, import_data): - """Import entry from configuration.yaml.""" - return await self.async_step_user( - { - CONF_USERNAME: import_data[CONF_USERNAME], - CONF_PASSWORD: import_data[CONF_PASSWORD], - CONF_COOL_AWAY_TEMPERATURE: import_data[CONF_COOL_AWAY_TEMPERATURE], - CONF_HEAT_AWAY_TEMPERATURE: import_data[CONF_HEAT_AWAY_TEMPERATURE], - } - ) diff --git a/homeassistant/components/honeywell/const.py b/homeassistant/components/honeywell/const.py index 6102f30d3de..2dce56046a3 100644 --- a/homeassistant/components/honeywell/const.py +++ b/homeassistant/components/honeywell/const.py @@ -3,8 +3,6 @@ import logging DOMAIN = "honeywell" -DEFAULT_COOL_AWAY_TEMPERATURE = 88 -DEFAULT_HEAT_AWAY_TEMPERATURE = 61 CONF_COOL_AWAY_TEMPERATURE = "away_cool_temperature" CONF_HEAT_AWAY_TEMPERATURE = "away_heat_temperature" CONF_DEV_ID = "thermostat" diff --git a/tests/components/honeywell/test_config_flow.py b/tests/components/honeywell/test_config_flow.py index 65f47ddf35f..47897cf246d 100644 --- a/tests/components/honeywell/test_config_flow.py +++ b/tests/components/honeywell/test_config_flow.py @@ -5,7 +5,7 @@ import somecomfort from homeassistant import data_entry_flow from homeassistant.components.honeywell.const import DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant FAKE_CONFIG = { @@ -49,15 +49,3 @@ async def test_create_entry(hass: HomeAssistant) -> None: ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"] == FAKE_CONFIG - - -async def test_async_step_import(hass: HomeAssistant) -> None: - """Test that the import step works.""" - with patch( - "somecomfort.SomeComfort", - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=FAKE_CONFIG - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["data"] == FAKE_CONFIG From 101341f186938e27acbc6e8e08145cc84cbb7da6 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 21 Dec 2021 05:00:11 -0500 Subject: [PATCH 0870/2644] Remove deprecated yaml config from google travel time (#62468) --- .../components/google_travel_time/__init__.py | 3 - .../google_travel_time/config_flow.py | 73 +--- .../components/google_travel_time/sensor.py | 71 +--- .../google_travel_time/test_config_flow.py | 379 ------------------ 4 files changed, 10 insertions(+), 516 deletions(-) diff --git a/homeassistant/components/google_travel_time/__init__.py b/homeassistant/components/google_travel_time/__init__.py index 1bc1c285bee..2012e38e0a2 100644 --- a/homeassistant/components/google_travel_time/__init__.py +++ b/homeassistant/components/google_travel_time/__init__.py @@ -1,6 +1,4 @@ """The google_travel_time component.""" -import logging - from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -10,7 +8,6 @@ from homeassistant.helpers.entity_registry import ( ) PLATFORMS = [Platform.SENSOR] -_LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/google_travel_time/config_flow.py b/homeassistant/components/google_travel_time/config_flow.py index 931e1f355aa..39425f01d84 100644 --- a/homeassistant/components/google_travel_time/config_flow.py +++ b/homeassistant/components/google_travel_time/config_flow.py @@ -2,13 +2,12 @@ from __future__ import annotations import logging -from typing import Any import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_API_KEY, CONF_MODE, CONF_NAME -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from .const import ( @@ -20,14 +19,12 @@ from .const import ( CONF_DEPARTURE_TIME, CONF_DESTINATION, CONF_LANGUAGE, - CONF_OPTIONS, CONF_ORIGIN, CONF_TIME, CONF_TIME_TYPE, CONF_TRAFFIC_MODEL, CONF_TRANSIT_MODE, CONF_TRANSIT_ROUTING_PREFERENCE, - CONF_TRAVEL_MODE, CONF_UNITS, DEFAULT_NAME, DEPARTURE_TIME, @@ -44,47 +41,6 @@ from .helpers import is_valid_config_entry _LOGGER = logging.getLogger(__name__) -def is_dupe_import( - hass: HomeAssistant, entry: config_entries.ConfigEntry, user_input: dict[str, Any] -) -> bool: - """Return whether imported config already exists.""" - # Check the main data keys - if any( - entry.data[key] != user_input[key] - for key in (CONF_API_KEY, CONF_DESTINATION, CONF_ORIGIN) - ): - return False - - options = user_input.get(CONF_OPTIONS, {}) - - # We have to check for units differently because there is a default - units = options.get(CONF_UNITS) or hass.config.units.name - if entry.options[CONF_UNITS] != units: - return False - - # We have to check for travel mode differently because of the default and because - # it can be provided in two different ways. We have to give mode preference over - # travel mode because that's the way that entry setup works. - mode = options.get(CONF_MODE) or user_input.get(CONF_TRAVEL_MODE) or "driving" - if entry.options[CONF_MODE] != mode: - return False - - # We have to check for options that don't have defaults - for key in ( - CONF_LANGUAGE, - CONF_AVOID, - CONF_ARRIVAL_TIME, - CONF_DEPARTURE_TIME, - CONF_TRAFFIC_MODEL, - CONF_TRANSIT_MODE, - CONF_TRANSIT_ROUTING_PREFERENCE, - ): - if options.get(key) != entry.options.get(key): - return False - - return True - - class GoogleOptionsFlow(config_entries.OptionsFlow): """Handle an options flow for Google Travel Time.""" @@ -171,24 +127,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors = {} user_input = user_input or {} if user_input: - # We need to prevent duplicate imports - if self.source == config_entries.SOURCE_IMPORT and any( - is_dupe_import(self.hass, entry, user_input) - for entry in self.hass.config_entries.async_entries(DOMAIN) - if entry.source == config_entries.SOURCE_IMPORT - ): - return self.async_abort(reason="already_configured") - - if ( - self.source == config_entries.SOURCE_IMPORT - or await self.hass.async_add_executor_job( - is_valid_config_entry, - self.hass, - _LOGGER, - user_input[CONF_API_KEY], - user_input[CONF_ORIGIN], - user_input[CONF_DESTINATION], - ) + if await self.hass.async_add_executor_job( + is_valid_config_entry, + self.hass, + _LOGGER, + user_input[CONF_API_KEY], + user_input[CONF_ORIGIN], + user_input[CONF_DESTINATION], ): return self.async_create_entry( title=user_input.get(CONF_NAME, DEFAULT_NAME), @@ -212,5 +157,3 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ), errors=errors, ) - - async_step_import = async_step_user diff --git a/homeassistant/components/google_travel_time/sensor.py b/homeassistant/components/google_travel_time/sensor.py index 2bd8e15795d..08104619bb9 100644 --- a/homeassistant/components/google_travel_time/sensor.py +++ b/homeassistant/components/google_travel_time/sensor.py @@ -6,51 +6,35 @@ import logging from googlemaps import Client from googlemaps.distance_matrix import distance_matrix -import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_API_KEY, - CONF_ENTITY_NAMESPACE, CONF_MODE, CONF_NAME, - CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_STARTED, TIME_MINUTES, ) from homeassistant.core import CoreState, HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util from .const import ( - ALL_LANGUAGES, ATTRIBUTION, - AVOID, CONF_ARRIVAL_TIME, - CONF_AVOID, CONF_DEPARTURE_TIME, CONF_DESTINATION, - CONF_LANGUAGE, CONF_OPTIONS, CONF_ORIGIN, - CONF_TRAFFIC_MODEL, - CONF_TRANSIT_MODE, - CONF_TRANSIT_ROUTING_PREFERENCE, CONF_TRAVEL_MODE, CONF_UNITS, DEFAULT_NAME, DOMAIN, TRACKABLE_DOMAINS, - TRANSIT_PREFS, - TRANSPORT_TYPE, - TRAVEL_MODE, - TRAVEL_MODEL, - UNITS, ) from .helpers import get_location_from_entity, resolve_zone @@ -58,38 +42,6 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(minutes=5) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_DESTINATION): cv.string, - vol.Required(CONF_ORIGIN): cv.string, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_TRAVEL_MODE): vol.In(TRAVEL_MODE), - vol.Optional(CONF_OPTIONS, default={CONF_MODE: "driving"}): vol.All( - dict, - vol.Schema( - { - vol.Optional(CONF_MODE, default="driving"): vol.In(TRAVEL_MODE), - vol.Optional(CONF_LANGUAGE): vol.In(ALL_LANGUAGES), - vol.Optional(CONF_AVOID): vol.In(AVOID), - vol.Optional(CONF_UNITS): vol.In(UNITS), - vol.Exclusive(CONF_ARRIVAL_TIME, "time"): cv.string, - vol.Exclusive(CONF_DEPARTURE_TIME, "time"): cv.string, - vol.Optional(CONF_TRAFFIC_MODEL): vol.In(TRAVEL_MODEL), - vol.Optional(CONF_TRANSIT_MODE): vol.In(TRANSPORT_TYPE), - vol.Optional(CONF_TRANSIT_ROUTING_PREFERENCE): vol.In( - TRANSIT_PREFS - ), - } - ), - ), - # Remove options to exclude from import - vol.Remove(CONF_ENTITY_NAMESPACE): cv.string, - vol.Remove(CONF_SCAN_INTERVAL): cv.time_period, - }, - extra=vol.REMOVE_EXTRA, -) - def convert_time_to_utc(timestr): """Take a string like 08:00:00 and convert it to a unix timestamp.""" @@ -145,25 +97,6 @@ async def async_setup_entry( async_add_entities([sensor], False) -async def async_setup_platform( - hass: HomeAssistant, config, add_entities_callback, discovery_info=None -): - """Set up the Google travel time platform.""" - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=config, - ) - ) - - _LOGGER.warning( - "Your Google travel time configuration has been imported into the UI; " - "please remove it from configuration.yaml as support for it will be " - "removed in a future release" - ) - - class GoogleTravelTimeSensor(SensorEntity): """Representation of a Google travel time sensor.""" diff --git a/tests/components/google_travel_time/test_config_flow.py b/tests/components/google_travel_time/test_config_flow.py index 6118aac7cfa..9b615afbbe1 100644 --- a/tests/components/google_travel_time/test_config_flow.py +++ b/tests/components/google_travel_time/test_config_flow.py @@ -7,14 +7,12 @@ from homeassistant.components.google_travel_time.const import ( CONF_DEPARTURE_TIME, CONF_DESTINATION, CONF_LANGUAGE, - CONF_OPTIONS, CONF_ORIGIN, CONF_TIME, CONF_TIME_TYPE, CONF_TRAFFIC_MODEL, CONF_TRANSIT_MODE, CONF_TRANSIT_ROUTING_PREFERENCE, - CONF_TRAVEL_MODE, CONF_UNITS, DEFAULT_NAME, DEPARTURE_TIME, @@ -25,7 +23,6 @@ from homeassistant.const import ( CONF_MODE, CONF_NAME, CONF_UNIT_SYSTEM_IMPERIAL, - CONF_UNIT_SYSTEM_METRIC, ) from tests.common import MockConfigEntry @@ -236,379 +233,3 @@ async def test_dupe(hass, validate_config_entry, bypass_setup): await hass.async_block_till_done() assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - - -async def test_import_flow(hass, validate_config_entry, bypass_update): - """Test import_flow.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_API_KEY: "api_key", - CONF_ORIGIN: "location1", - CONF_DESTINATION: "location2", - CONF_NAME: "test_name", - CONF_OPTIONS: { - CONF_MODE: "driving", - CONF_LANGUAGE: "en", - CONF_AVOID: "tolls", - CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, - CONF_ARRIVAL_TIME: "test", - CONF_TRAFFIC_MODEL: "best_guess", - CONF_TRANSIT_MODE: "train", - CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", - }, - }, - ) - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "test_name" - assert result["data"] == { - CONF_API_KEY: "api_key", - CONF_ORIGIN: "location1", - CONF_DESTINATION: "location2", - CONF_NAME: "test_name", - CONF_OPTIONS: { - CONF_MODE: "driving", - CONF_LANGUAGE: "en", - CONF_AVOID: "tolls", - CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, - CONF_ARRIVAL_TIME: "test", - CONF_TRAFFIC_MODEL: "best_guess", - CONF_TRANSIT_MODE: "train", - CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", - }, - } - - entry = hass.config_entries.async_entries(DOMAIN)[0] - assert entry.data == { - CONF_NAME: "test_name", - CONF_API_KEY: "api_key", - CONF_ORIGIN: "location1", - CONF_DESTINATION: "location2", - } - assert entry.options == { - CONF_MODE: "driving", - CONF_LANGUAGE: "en", - CONF_AVOID: "tolls", - CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, - CONF_ARRIVAL_TIME: "test", - CONF_TRAFFIC_MODEL: "best_guess", - CONF_TRANSIT_MODE: "train", - CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", - } - - -async def test_dupe_import_no_options(hass, bypass_update): - """Test duplicate import with no options.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_API_KEY: "api_key", - CONF_ORIGIN: "location1", - CONF_DESTINATION: "location2", - CONF_NAME: "test_name", - }, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - await hass.async_block_till_done() - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_API_KEY: "api_key", - CONF_ORIGIN: "location1", - CONF_DESTINATION: "location2", - CONF_NAME: "test_name", - }, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - -async def test_dupe_import_default_options(hass, bypass_update): - """Test duplicate import with default options.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_API_KEY: "api_key", - CONF_ORIGIN: "location1", - CONF_DESTINATION: "location2", - CONF_NAME: "test_name", - CONF_OPTIONS: { - CONF_LANGUAGE: "en", - CONF_AVOID: "tolls", - CONF_ARRIVAL_TIME: "test", - CONF_TRAFFIC_MODEL: "best_guess", - CONF_TRANSIT_MODE: "train", - CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", - }, - }, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - await hass.async_block_till_done() - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_API_KEY: "api_key", - CONF_ORIGIN: "location1", - CONF_DESTINATION: "location2", - CONF_NAME: "test_name", - CONF_OPTIONS: { - CONF_LANGUAGE: "en", - CONF_AVOID: "tolls", - CONF_ARRIVAL_TIME: "test", - CONF_TRAFFIC_MODEL: "best_guess", - CONF_TRANSIT_MODE: "train", - CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", - }, - }, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - -async def _setup_dupe_import(hass, bypass_update): - """Set up dupe import.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_API_KEY: "api_key", - CONF_ORIGIN: "location1", - CONF_DESTINATION: "location2", - CONF_NAME: "test_name", - CONF_OPTIONS: { - CONF_MODE: "walking", - CONF_LANGUAGE: "en", - CONF_AVOID: "tolls", - CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, - CONF_ARRIVAL_TIME: "test", - CONF_TRAFFIC_MODEL: "best_guess", - CONF_TRANSIT_MODE: "train", - CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", - }, - }, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - await hass.async_block_till_done() - - -async def test_dupe_import(hass, bypass_update): - """Test duplicate import.""" - await _setup_dupe_import(hass, bypass_update) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_API_KEY: "api_key", - CONF_ORIGIN: "location1", - CONF_DESTINATION: "location2", - CONF_NAME: "test_name", - CONF_OPTIONS: { - CONF_MODE: "walking", - CONF_LANGUAGE: "en", - CONF_AVOID: "tolls", - CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, - CONF_ARRIVAL_TIME: "test", - CONF_TRAFFIC_MODEL: "best_guess", - CONF_TRANSIT_MODE: "train", - CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", - }, - }, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - -async def test_dupe_import_false_check_data_keys(hass, bypass_update): - """Test false duplicate import check when data keys differ.""" - await _setup_dupe_import(hass, bypass_update) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_API_KEY: "api_key2", - CONF_ORIGIN: "location1", - CONF_DESTINATION: "location2", - CONF_NAME: "test_name", - CONF_OPTIONS: { - CONF_MODE: "walking", - CONF_LANGUAGE: "en", - CONF_AVOID: "tolls", - CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, - CONF_ARRIVAL_TIME: "test", - CONF_TRAFFIC_MODEL: "best_guess", - CONF_TRANSIT_MODE: "train", - CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", - }, - }, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - - -async def test_dupe_import_false_check_no_units(hass, bypass_update): - """Test false duplicate import check when units aren't provided.""" - await _setup_dupe_import(hass, bypass_update) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_API_KEY: "api_key", - CONF_ORIGIN: "location1", - CONF_DESTINATION: "location2", - CONF_NAME: "test_name", - CONF_OPTIONS: { - CONF_MODE: "walking", - CONF_LANGUAGE: "en", - CONF_AVOID: "tolls", - CONF_ARRIVAL_TIME: "test", - CONF_TRAFFIC_MODEL: "best_guess", - CONF_TRANSIT_MODE: "train", - CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", - }, - }, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - - -async def test_dupe_import_false_check_units(hass, bypass_update): - """Test false duplicate import check when units are provided but different.""" - await _setup_dupe_import(hass, bypass_update) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_API_KEY: "api_key", - CONF_ORIGIN: "location1", - CONF_DESTINATION: "location2", - CONF_NAME: "test_name", - CONF_OPTIONS: { - CONF_MODE: "walking", - CONF_LANGUAGE: "en", - CONF_AVOID: "tolls", - CONF_UNITS: CONF_UNIT_SYSTEM_METRIC, - CONF_ARRIVAL_TIME: "test", - CONF_TRAFFIC_MODEL: "best_guess", - CONF_TRANSIT_MODE: "train", - CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", - }, - }, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - - -async def test_dupe_import_false_check_travel_mode(hass, bypass_update): - """Test false duplicate import check when travel mode differs.""" - await _setup_dupe_import(hass, bypass_update) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_API_KEY: "api_key", - CONF_ORIGIN: "location1", - CONF_DESTINATION: "location2", - CONF_NAME: "test_name", - CONF_TRAVEL_MODE: "driving", - CONF_OPTIONS: { - CONF_LANGUAGE: "en", - CONF_AVOID: "tolls", - CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, - CONF_ARRIVAL_TIME: "test", - CONF_TRAFFIC_MODEL: "best_guess", - CONF_TRANSIT_MODE: "train", - CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", - }, - }, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - - -async def test_dupe_import_false_check_mode(hass, bypass_update): - """Test false duplicate import check when mode diiffers.""" - await _setup_dupe_import(hass, bypass_update) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_API_KEY: "api_key", - CONF_ORIGIN: "location1", - CONF_DESTINATION: "location2", - CONF_NAME: "test_name", - CONF_OPTIONS: { - CONF_MODE: "driving", - CONF_LANGUAGE: "en", - CONF_AVOID: "tolls", - CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, - CONF_ARRIVAL_TIME: "test", - CONF_TRAFFIC_MODEL: "best_guess", - CONF_TRANSIT_MODE: "train", - CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", - }, - }, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - - -async def test_dupe_import_false_check_no_mode(hass, bypass_update): - """Test false duplicate import check when no mode is provided.""" - await _setup_dupe_import(hass, bypass_update) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_API_KEY: "api_key", - CONF_ORIGIN: "location1", - CONF_DESTINATION: "location2", - CONF_NAME: "test_name", - CONF_OPTIONS: { - CONF_LANGUAGE: "en", - CONF_AVOID: "tolls", - CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, - CONF_ARRIVAL_TIME: "test", - CONF_TRAFFIC_MODEL: "best_guess", - CONF_TRANSIT_MODE: "train", - CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", - }, - }, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - - -async def test_dupe_import_false_check_options(hass, bypass_update): - """Test false duplicate import check when options differ.""" - await _setup_dupe_import(hass, bypass_update) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_API_KEY: "api_key", - CONF_ORIGIN: "location1", - CONF_DESTINATION: "location2", - CONF_NAME: "test_name", - CONF_OPTIONS: { - CONF_MODE: "walking", - CONF_AVOID: "tolls", - CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, - CONF_ARRIVAL_TIME: "test", - CONF_TRAFFIC_MODEL: "best_guess", - CONF_TRANSIT_MODE: "train", - CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", - }, - }, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY From 71852fd7a8fc1e78d2e93ab360d98cfb5f685815 Mon Sep 17 00:00:00 2001 From: Petru Paler Date: Tue, 21 Dec 2021 10:06:44 +0000 Subject: [PATCH 0871/2644] Update Solax library to 0.2.9 (#62464) --- homeassistant/components/solax/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/solax/manifest.json b/homeassistant/components/solax/manifest.json index f6a6f581e12..311bff3f66e 100644 --- a/homeassistant/components/solax/manifest.json +++ b/homeassistant/components/solax/manifest.json @@ -2,7 +2,7 @@ "domain": "solax", "name": "SolaX Power", "documentation": "https://www.home-assistant.io/integrations/solax", - "requirements": ["solax==0.2.8"], + "requirements": ["solax==0.2.9"], "codeowners": ["@squishykid"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 3939bfa9f68..b4e018cb7ca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2205,7 +2205,7 @@ solaredge-local==0.2.0 solaredge==0.0.2 # homeassistant.components.solax -solax==0.2.8 +solax==0.2.9 # homeassistant.components.honeywell somecomfort==0.8.0 From a9c45fdcc0b22c81fa376113960f23288a3903a1 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 21 Dec 2021 05:11:25 -0500 Subject: [PATCH 0872/2644] Remove deprecated yaml config from philips_js (#62471) --- .../components/philips_js/config_flow.py | 11 ----- .../components/philips_js/media_player.py | 41 +------------------ tests/components/philips_js/__init__.py | 2 - .../components/philips_js/test_config_flow.py | 27 ------------ 4 files changed, 1 insertion(+), 80 deletions(-) diff --git a/homeassistant/components/philips_js/config_flow.py b/homeassistant/components/philips_js/config_flow.py index 59403b2ec86..89f13ffadbf 100644 --- a/homeassistant/components/philips_js/config_flow.py +++ b/homeassistant/components/philips_js/config_flow.py @@ -47,17 +47,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._hub: PhilipsTV | None = None self._pair_state: Any = None - async def async_step_import(self, conf: dict) -> dict: - """Import a configuration from config.yaml.""" - self._async_abort_entries_match({CONF_HOST: conf[CONF_HOST]}) - - return await self.async_step_user( - { - CONF_HOST: conf[CONF_HOST], - CONF_API_VERSION: conf[CONF_API_VERSION], - } - ) - async def _async_create_current(self): system = self._current[CONF_SYSTEM] diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 8e9ddfc353f..fa5643503bc 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -4,11 +4,9 @@ from __future__ import annotations from typing import Any from haphilipsjs import ConnectionFailure -import voluptuous as vol from homeassistant import config_entries from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, BrowseMedia, MediaPlayerDeviceClass, MediaPlayerEntity, @@ -36,15 +34,8 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_STEP, ) from homeassistant.components.media_player.errors import BrowseError -from homeassistant.const import ( - CONF_API_VERSION, - CONF_HOST, - CONF_NAME, - STATE_OFF, - STATE_ON, -) +from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant, callback -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -68,41 +59,11 @@ SUPPORT_PHILIPS_JS = ( CONF_ON_ACTION = "turn_on_action" -DEFAULT_API_VERSION = 1 - -PLATFORM_SCHEMA = vol.All( - cv.deprecated(CONF_HOST), - cv.deprecated(CONF_NAME), - cv.deprecated(CONF_API_VERSION), - cv.deprecated(CONF_ON_ACTION), - PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Remove(CONF_NAME): cv.string, - vol.Optional(CONF_API_VERSION, default=DEFAULT_API_VERSION): vol.Coerce( - int - ), - vol.Remove(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, - } - ), -) - def _inverted(data): return {v: k for k, v in data.items()} -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Philips TV platform.""" - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=config, - ) - ) - - async def async_setup_entry( hass: HomeAssistant, config_entry: config_entries.ConfigEntry, diff --git a/tests/components/philips_js/__init__.py b/tests/components/philips_js/__init__.py index 9dea390a600..f524a586fc8 100644 --- a/tests/components/philips_js/__init__.py +++ b/tests/components/philips_js/__init__.py @@ -58,8 +58,6 @@ MOCK_USERINPUT = { "host": "1.1.1.1", } -MOCK_IMPORT = {"host": "1.1.1.1", "api_version": 6} - MOCK_CONFIG = { "host": "1.1.1.1", "api_version": 1, diff --git a/tests/components/philips_js/test_config_flow.py b/tests/components/philips_js/test_config_flow.py index f3ab44844a2..ace62195115 100644 --- a/tests/components/philips_js/test_config_flow.py +++ b/tests/components/philips_js/test_config_flow.py @@ -10,7 +10,6 @@ from homeassistant.components.philips_js.const import CONF_ALLOW_NOTIFY, DOMAIN from . import ( MOCK_CONFIG, MOCK_CONFIG_PAIRED, - MOCK_IMPORT, MOCK_PASSWORD, MOCK_SYSTEM_UNPAIRED, MOCK_USERINPUT, @@ -45,32 +44,6 @@ async def mock_tv_pairable(mock_tv): return mock_tv -async def test_import(hass, mock_setup_entry): - """Test we get an item on import.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=MOCK_IMPORT, - ) - - assert result["type"] == "create_entry" - assert result["title"] == "Philips TV (1234567890)" - assert result["data"] == MOCK_CONFIG - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_import_exist(hass, mock_config_entry): - """Test we get an item on import.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=MOCK_IMPORT, - ) - - assert result["type"] == "abort" - assert result["reason"] == "already_configured" - - async def test_form(hass, mock_setup_entry): """Test we get the form.""" result = await hass.config_entries.flow.async_init( From e0ef0660223d7ff8ae6c7a6904d9a37882a9e593 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Dec 2021 04:24:32 -0600 Subject: [PATCH 0873/2644] Remove legacy migration and yaml from tplink (#62457) - tplink has been fully migrated to a config flow in previous versions. --- homeassistant/components/tplink/__init__.py | 136 +-------- .../components/tplink/config_flow.py | 33 +-- homeassistant/components/tplink/migration.py | 113 -------- tests/components/tplink/test_config_flow.py | 121 +------- tests/components/tplink/test_init.py | 31 --- tests/components/tplink/test_migration.py | 263 ------------------ 6 files changed, 8 insertions(+), 689 deletions(-) delete mode 100644 homeassistant/components/tplink/migration.py delete mode 100644 tests/components/tplink/test_migration.py diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index d17815d2344..70e5292f6ea 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -7,11 +7,9 @@ from typing import Any from kasa import SmartDevice, SmartDeviceException from kasa.discover import Discover -import voluptuous as vol from homeassistant import config_entries from homeassistant.components import network -from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.config_entries import ConfigEntry, ConfigEntryNotReady from homeassistant.const import ( CONF_HOST, @@ -20,60 +18,15 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STARTED, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import ( - config_validation as cv, - device_registry as dr, - entity_registry as er, -) +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType -from .const import ( - CONF_DIMMER, - CONF_DISCOVERY, - CONF_LIGHT, - CONF_STRIP, - CONF_SWITCH, - DOMAIN, - PLATFORMS, -) +from .const import DOMAIN, PLATFORMS from .coordinator import TPLinkDataUpdateCoordinator -from .migration import ( - async_migrate_entities_devices, - async_migrate_legacy_entries, - async_migrate_yaml_entries, -) DISCOVERY_INTERVAL = timedelta(minutes=15) -TPLINK_HOST_SCHEMA = vol.Schema({vol.Required(CONF_HOST): cv.string}) - -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: vol.Schema( - { - vol.Optional(CONF_LIGHT, default=[]): vol.All( - cv.ensure_list, [TPLINK_HOST_SCHEMA] - ), - vol.Optional(CONF_SWITCH, default=[]): vol.All( - cv.ensure_list, [TPLINK_HOST_SCHEMA] - ), - vol.Optional(CONF_STRIP, default=[]): vol.All( - cv.ensure_list, [TPLINK_HOST_SCHEMA] - ), - vol.Optional(CONF_DIMMER, default=[]): vol.All( - cv.ensure_list, [TPLINK_HOST_SCHEMA] - ), - vol.Optional(CONF_DISCOVERY, default=True): cv.boolean, - } - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) - @callback def async_trigger_discovery( @@ -108,30 +61,9 @@ async def async_discover_devices(hass: HomeAssistant) -> dict[str, SmartDevice]: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the TP-Link component.""" - conf = config.get(DOMAIN) hass.data[DOMAIN] = {} - legacy_entry = None - config_entries_by_mac = {} - for entry in hass.config_entries.async_entries(DOMAIN): - if async_entry_is_legacy(entry): - legacy_entry = entry - elif entry.unique_id: - config_entries_by_mac[entry.unique_id] = entry - discovered_devices = await async_discover_devices(hass) - hosts_by_mac = {mac: device.host for mac, device in discovered_devices.items()} - - if legacy_entry: - async_migrate_legacy_entries( - hass, hosts_by_mac, config_entries_by_mac, legacy_entry - ) - # Migrate the yaml entry that was previously imported - async_migrate_yaml_entries(hass, legacy_entry.data) - - if conf is not None: - async_migrate_yaml_entries(hass, conf) - - if discovered_devices: + if discovered_devices := await async_discover_devices(hass): async_trigger_discovery(hass, discovered_devices) async def _async_discovery(*_: Any) -> None: @@ -146,87 +78,27 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up TPLink from a config entry.""" - if async_entry_is_legacy(entry): - return True - - legacy_entry: ConfigEntry | None = None - for config_entry in hass.config_entries.async_entries(DOMAIN): - if async_entry_is_legacy(config_entry): - legacy_entry = config_entry - break - - if legacy_entry is not None: - await async_migrate_entities_devices(hass, legacy_entry.entry_id, entry) - try: device: SmartDevice = await Discover.discover_single(entry.data[CONF_HOST]) except SmartDeviceException as ex: raise ConfigEntryNotReady from ex - if device.is_dimmer: - async_fix_dimmer_unique_id(hass, entry, device) - hass.data[DOMAIN][entry.entry_id] = TPLinkDataUpdateCoordinator(hass, device) hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -@callback -def async_fix_dimmer_unique_id( - hass: HomeAssistant, entry: ConfigEntry, device: SmartDevice -) -> None: - """Migrate the unique id of dimmers back to the legacy one. - - Dimmers used to use the switch format since pyHS100 treated them as SmartPlug but - the old code created them as lights - - https://github.com/home-assistant/core/blob/2021.9.7/homeassistant/components/tplink/common.py#L86 - """ - - # This is the unique id before 2021.0/2021.1 - original_unique_id = legacy_device_id(device) - - # This is the unique id that was used in 2021.0/2021.1 rollout - rollout_unique_id = device.mac.replace(":", "").upper() - - entity_registry = er.async_get(hass) - - rollout_entity_id = entity_registry.async_get_entity_id( - LIGHT_DOMAIN, DOMAIN, rollout_unique_id - ) - original_entry_id = entity_registry.async_get_entity_id( - LIGHT_DOMAIN, DOMAIN, original_unique_id - ) - - # If they are now using the 2021.0/2021.1 rollout entity id - # and have deleted the original entity id, we want to update that entity id - # so they don't end up with another _2 entity, but only if they deleted - # the original - if rollout_entity_id and not original_entry_id: - entity_registry.async_update_entity( - rollout_entity_id, new_unique_id=original_unique_id - ) - - async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" hass_data: dict[str, Any] = hass.data[DOMAIN] - if entry.entry_id not in hass_data: - return True - device: SmartDevice = hass.data[DOMAIN][entry.entry_id].device + device: SmartDevice = hass_data[entry.entry_id].device if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): hass_data.pop(entry.entry_id) await device.protocol.close() return unload_ok -@callback -def async_entry_is_legacy(entry: ConfigEntry) -> bool: - """Check if a config entry is the legacy shared one.""" - return entry.unique_id is None or entry.unique_id == DOMAIN - - def legacy_device_id(device: SmartDevice) -> str: """Convert the device id so it matches what was used in the original version.""" device_id: str = device.device_id diff --git a/homeassistant/components/tplink/config_flow.py b/homeassistant/components/tplink/config_flow.py index e2c03dd43f2..5cb351989be 100644 --- a/homeassistant/components/tplink/config_flow.py +++ b/homeassistant/components/tplink/config_flow.py @@ -1,7 +1,6 @@ """Config flow for TP-Link.""" from __future__ import annotations -import logging from typing import Any from kasa import SmartDevice, SmartDeviceException @@ -10,17 +9,15 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import dhcp -from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_MAC, CONF_NAME +from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_MAC from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import device_registry as dr from homeassistant.helpers.typing import DiscoveryInfoType -from . import async_discover_devices, async_entry_is_legacy +from . import async_discover_devices from .const import DOMAIN -_LOGGER = logging.getLogger(__name__) - class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for tplink.""" @@ -114,9 +111,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self._async_create_entry_from_device(self._discovered_devices[mac]) configured_devices = { - entry.unique_id - for entry in self._async_current_entries() - if not async_entry_is_legacy(entry) + entry.unique_id for entry in self._async_current_entries() } self._discovered_devices = await async_discover_devices(self.hass) devices_name = { @@ -132,18 +127,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): data_schema=vol.Schema({vol.Required(CONF_DEVICE): vol.In(devices_name)}), ) - async def async_step_migration(self, migration_input: dict[str, Any]) -> FlowResult: - """Handle migration from legacy config entry to per device config entry.""" - mac = migration_input[CONF_MAC] - await self.async_set_unique_id(dr.format_mac(mac), raise_on_progress=False) - self._abort_if_unique_id_configured() - return self.async_create_entry( - title=migration_input[CONF_NAME], - data={ - CONF_HOST: migration_input[CONF_HOST], - }, - ) - @callback def _async_create_entry_from_device(self, device: SmartDevice) -> FlowResult: """Create a config entry from a smart device.""" @@ -155,16 +138,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): }, ) - async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: - """Handle import step.""" - host = user_input[CONF_HOST] - try: - device = await self._async_try_connect(host, raise_on_progress=False) - except SmartDeviceException: - _LOGGER.error("Failed to import %s: cannot connect", host) - return self.async_abort(reason="cannot_connect") - return self._async_create_entry_from_device(device) - async def _async_try_connect( self, host: str, raise_on_progress: bool = True ) -> SmartDevice: diff --git a/homeassistant/components/tplink/migration.py b/homeassistant/components/tplink/migration.py deleted file mode 100644 index 9344dd1532f..00000000000 --- a/homeassistant/components/tplink/migration.py +++ /dev/null @@ -1,113 +0,0 @@ -"""Component to embed TP-Link smart home devices.""" -from __future__ import annotations - -from datetime import datetime -from types import MappingProxyType -from typing import Any - -from homeassistant import config_entries -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_HOST, - CONF_MAC, - CONF_NAME, - EVENT_HOMEASSISTANT_STARTED, -) -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.helpers.typing import ConfigType - -from .const import CONF_DIMMER, CONF_LIGHT, CONF_STRIP, CONF_SWITCH, DOMAIN - - -async def async_cleanup_legacy_entry( - hass: HomeAssistant, - legacy_entry_id: str, -) -> None: - """Cleanup the legacy entry if the migration is successful.""" - entity_registry = er.async_get(hass) - if not er.async_entries_for_config_entry(entity_registry, legacy_entry_id): - await hass.config_entries.async_remove(legacy_entry_id) - - -@callback -def async_migrate_legacy_entries( - hass: HomeAssistant, - hosts_by_mac: dict[str, str], - config_entries_by_mac: dict[str, ConfigEntry], - legacy_entry: ConfigEntry, -) -> None: - """Migrate the legacy config entries to have an entry per device.""" - device_registry = dr.async_get(hass) - for dev_entry in dr.async_entries_for_config_entry( - device_registry, legacy_entry.entry_id - ): - for connection_type, mac in dev_entry.connections: - if ( - connection_type != dr.CONNECTION_NETWORK_MAC - or mac in config_entries_by_mac - ): - continue - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": "migration"}, - data={ - CONF_HOST: hosts_by_mac.get(mac), - CONF_MAC: mac, - CONF_NAME: dev_entry.name or f"TP-Link device {mac}", - }, - ) - ) - - async def _async_cleanup_legacy_entry(_now: datetime) -> None: - await async_cleanup_legacy_entry(hass, legacy_entry.entry_id) - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _async_cleanup_legacy_entry) - - -@callback -def async_migrate_yaml_entries( - hass: HomeAssistant, conf: ConfigType | MappingProxyType[str, Any] -) -> None: - """Migrate yaml to config entries.""" - for device_type in (CONF_LIGHT, CONF_SWITCH, CONF_STRIP, CONF_DIMMER): - for device in conf.get(device_type, []): - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_HOST: device[CONF_HOST], - }, - ) - ) - - -async def async_migrate_entities_devices( - hass: HomeAssistant, legacy_entry_id: str, new_entry: ConfigEntry -) -> None: - """Move entities and devices to the new config entry.""" - migrated_devices = [] - device_registry = dr.async_get(hass) - for dev_entry in dr.async_entries_for_config_entry( - device_registry, legacy_entry_id - ): - for connection_type, value in dev_entry.connections: - if ( - connection_type == dr.CONNECTION_NETWORK_MAC - and value == new_entry.unique_id - ): - migrated_devices.append(dev_entry.id) - device_registry.async_update_device( - dev_entry.id, add_config_entry_id=new_entry.entry_id - ) - - entity_registry = er.async_get(hass) - for reg_entity in er.async_entries_for_config_entry( - entity_registry, legacy_entry_id - ): - if reg_entity.device_id in migrated_devices: - entity_registry.async_update_entity( - reg_entity.entity_id, config_entry_id=new_entry.entry_id - ) diff --git a/tests/components/tplink/test_config_flow.py b/tests/components/tplink/test_config_flow.py index 5a4e672a384..fdc9fcea83e 100644 --- a/tests/components/tplink/test_config_flow.py +++ b/tests/components/tplink/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.components.tplink import DOMAIN from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_MAC, CONF_NAME @@ -175,52 +175,6 @@ async def test_discovery_no_device(hass: HomeAssistant): assert result2["reason"] == "no_devices_found" -async def test_import(hass: HomeAssistant): - """Test import from yaml.""" - config = { - CONF_HOST: IP_ADDRESS, - } - - # Cannot connect - with _patch_discovery(no_device=True), _patch_single_discovery(no_device=True): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config - ) - await hass.async_block_till_done() - - assert result["type"] == "abort" - assert result["reason"] == "cannot_connect" - - # Success - with _patch_discovery(), _patch_single_discovery(), patch( - f"{MODULE}.async_setup", return_value=True - ) as mock_setup, patch( - f"{MODULE}.async_setup_entry", return_value=True - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config - ) - await hass.async_block_till_done() - - assert result["type"] == "create_entry" - assert result["title"] == DEFAULT_ENTRY_TITLE - assert result["data"] == { - CONF_HOST: IP_ADDRESS, - } - mock_setup.assert_called_once() - mock_setup_entry.assert_called_once() - - # Duplicate - with _patch_discovery(), _patch_single_discovery(): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config - ) - await hass.async_block_till_done() - - assert result["type"] == "abort" - assert result["reason"] == "already_configured" - - async def test_manual(hass: HomeAssistant): """Test manually setup.""" result = await hass.config_entries.flow.async_init( @@ -406,76 +360,3 @@ async def test_discovered_by_dhcp_or_discovery_failed_to_get_device(hass, source await hass.async_block_till_done() assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "cannot_connect" - - -async def test_migration_device_online(hass: HomeAssistant): - """Test migration from single config entry.""" - config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) - config_entry.add_to_hass(hass) - config = {CONF_MAC: MAC_ADDRESS, CONF_NAME: ALIAS, CONF_HOST: IP_ADDRESS} - - with _patch_discovery(), _patch_single_discovery(), patch( - f"{MODULE}.async_setup_entry", return_value=True - ) as mock_setup_entry: - await setup.async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "migration"}, data=config - ) - await hass.async_block_till_done() - - assert result["type"] == "create_entry" - assert result["title"] == ALIAS - assert result["data"] == { - CONF_HOST: IP_ADDRESS, - } - assert len(mock_setup_entry.mock_calls) == 2 - - # Duplicate - with _patch_discovery(), _patch_single_discovery(): - await setup.async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "migration"}, data=config - ) - await hass.async_block_till_done() - - assert result["type"] == "abort" - assert result["reason"] == "already_configured" - - -async def test_migration_device_offline(hass: HomeAssistant): - """Test migration from single config entry.""" - config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) - config_entry.add_to_hass(hass) - config = {CONF_MAC: MAC_ADDRESS, CONF_NAME: ALIAS, CONF_HOST: None} - - with _patch_discovery(no_device=True), _patch_single_discovery( - no_device=True - ), patch(f"{MODULE}.async_setup_entry", return_value=True) as mock_setup_entry: - await setup.async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "migration"}, data=config - ) - await hass.async_block_till_done() - - assert result["type"] == "create_entry" - assert result["title"] == ALIAS - new_entry = result["result"] - assert result["data"] == { - CONF_HOST: None, - } - assert len(mock_setup_entry.mock_calls) == 2 - - # Ensure a manual import updates the missing host - config = {CONF_HOST: IP_ADDRESS} - with _patch_discovery(no_device=True), _patch_single_discovery(): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config - ) - await hass.async_block_till_done() - - assert result["type"] == "abort" - assert result["reason"] == "already_configured" - assert new_entry.data[CONF_HOST] == IP_ADDRESS diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index 73edc63e28c..d6812f6c993 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -74,37 +74,6 @@ async def test_config_entry_retry(hass): assert already_migrated_config_entry.state == ConfigEntryState.SETUP_RETRY -async def test_dimmer_switch_unique_id_fix_original_entity_was_deleted( - hass: HomeAssistant, entity_reg: EntityRegistry -): - """Test that roll out unique id entity id changed to the original unique id.""" - config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=MAC_ADDRESS) - config_entry.add_to_hass(hass) - dimmer = _mocked_dimmer() - rollout_unique_id = MAC_ADDRESS.replace(":", "").upper() - original_unique_id = tplink.legacy_device_id(dimmer) - rollout_dimmer_entity_reg = entity_reg.async_get_or_create( - config_entry=config_entry, - platform=DOMAIN, - domain="light", - unique_id=rollout_unique_id, - original_name="Rollout dimmer", - ) - - with _patch_discovery(device=dimmer), _patch_single_discovery(device=dimmer): - await setup.async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() - - migrated_dimmer_entity_reg = entity_reg.async_get_or_create( - config_entry=config_entry, - platform=DOMAIN, - domain="light", - unique_id=original_unique_id, - original_name="Migrated dimmer", - ) - assert migrated_dimmer_entity_reg.entity_id == rollout_dimmer_entity_reg.entity_id - - async def test_dimmer_switch_unique_id_fix_original_entity_still_exists( hass: HomeAssistant, entity_reg: EntityRegistry ): diff --git a/tests/components/tplink/test_migration.py b/tests/components/tplink/test_migration.py deleted file mode 100644 index a1cd581e211..00000000000 --- a/tests/components/tplink/test_migration.py +++ /dev/null @@ -1,263 +0,0 @@ -"""Test the tplink config flow.""" - -from homeassistant import setup -from homeassistant.components.tplink import CONF_DISCOVERY, CONF_SWITCH, DOMAIN -from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STARTED -from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.helpers.device_registry import DeviceRegistry -from homeassistant.helpers.entity_registry import EntityRegistry - -from . import ALIAS, IP_ADDRESS, MAC_ADDRESS, _patch_discovery, _patch_single_discovery - -from tests.common import MockConfigEntry - - -async def test_migration_device_online_end_to_end( - hass: HomeAssistant, device_reg: DeviceRegistry, entity_reg: EntityRegistry -): - """Test migration from single config entry.""" - config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) - config_entry.add_to_hass(hass) - device = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(dr.CONNECTION_NETWORK_MAC, MAC_ADDRESS)}, - name=ALIAS, - ) - switch_entity_reg = entity_reg.async_get_or_create( - config_entry=config_entry, - platform=DOMAIN, - domain="switch", - unique_id=MAC_ADDRESS, - original_name=ALIAS, - device_id=device.id, - ) - light_entity_reg = entity_reg.async_get_or_create( - config_entry=config_entry, - platform=DOMAIN, - domain="light", - unique_id=dr.format_mac(MAC_ADDRESS), - original_name=ALIAS, - device_id=device.id, - ) - power_sensor_entity_reg = entity_reg.async_get_or_create( - config_entry=config_entry, - platform=DOMAIN, - domain="sensor", - unique_id=f"{MAC_ADDRESS}_sensor", - original_name=ALIAS, - device_id=device.id, - ) - - with _patch_discovery(), _patch_single_discovery(): - await setup.async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() - - migrated_entry = None - for entry in hass.config_entries.async_entries(DOMAIN): - if entry.unique_id == DOMAIN: - migrated_entry = entry - break - - assert migrated_entry is not None - - assert device.config_entries == {migrated_entry.entry_id} - assert light_entity_reg.config_entry_id == migrated_entry.entry_id - assert switch_entity_reg.config_entry_id == migrated_entry.entry_id - assert power_sensor_entity_reg.config_entry_id == migrated_entry.entry_id - assert er.async_entries_for_config_entry(entity_reg, config_entry) == [] - - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await hass.async_block_till_done() - - legacy_entry = None - for entry in hass.config_entries.async_entries(DOMAIN): - if entry.unique_id == DOMAIN: - legacy_entry = entry - break - - assert legacy_entry is None - - -async def test_migration_device_online_end_to_end_after_downgrade( - hass: HomeAssistant, device_reg: DeviceRegistry, entity_reg: EntityRegistry -): - """Test migration from single config entry can happen again after a downgrade.""" - config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) - config_entry.add_to_hass(hass) - - already_migrated_config_entry = MockConfigEntry( - domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=MAC_ADDRESS - ) - already_migrated_config_entry.add_to_hass(hass) - device = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(dr.CONNECTION_NETWORK_MAC, MAC_ADDRESS)}, - name=ALIAS, - ) - light_entity_reg = entity_reg.async_get_or_create( - config_entry=config_entry, - platform=DOMAIN, - domain="light", - unique_id=MAC_ADDRESS, - original_name=ALIAS, - device_id=device.id, - ) - power_sensor_entity_reg = entity_reg.async_get_or_create( - config_entry=config_entry, - platform=DOMAIN, - domain="sensor", - unique_id=f"{MAC_ADDRESS}_sensor", - original_name=ALIAS, - device_id=device.id, - ) - - with _patch_discovery(), _patch_single_discovery(): - await setup.async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() - - assert device.config_entries == {config_entry.entry_id} - assert light_entity_reg.config_entry_id == config_entry.entry_id - assert power_sensor_entity_reg.config_entry_id == config_entry.entry_id - assert er.async_entries_for_config_entry(entity_reg, config_entry) == [] - - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await hass.async_block_till_done() - - legacy_entry = None - for entry in hass.config_entries.async_entries(DOMAIN): - if entry.unique_id == DOMAIN: - legacy_entry = entry - break - - assert legacy_entry is None - - -async def test_migration_device_online_end_to_end_ignores_other_devices( - hass: HomeAssistant, device_reg: DeviceRegistry, entity_reg: EntityRegistry -): - """Test migration from single config entry.""" - config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) - config_entry.add_to_hass(hass) - - other_domain_config_entry = MockConfigEntry( - domain="other_domain", data={}, unique_id="other_domain" - ) - other_domain_config_entry.add_to_hass(hass) - device = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(dr.CONNECTION_NETWORK_MAC, MAC_ADDRESS)}, - name=ALIAS, - ) - other_device = device_reg.async_get_or_create( - config_entry_id=other_domain_config_entry.entry_id, - connections={(dr.CONNECTION_NETWORK_MAC, "556655665566")}, - name=ALIAS, - ) - light_entity_reg = entity_reg.async_get_or_create( - config_entry=config_entry, - platform=DOMAIN, - domain="light", - unique_id=MAC_ADDRESS, - original_name=ALIAS, - device_id=device.id, - ) - power_sensor_entity_reg = entity_reg.async_get_or_create( - config_entry=config_entry, - platform=DOMAIN, - domain="sensor", - unique_id=f"{MAC_ADDRESS}_sensor", - original_name=ALIAS, - device_id=device.id, - ) - ignored_entity_reg = entity_reg.async_get_or_create( - config_entry=other_domain_config_entry, - platform=DOMAIN, - domain="sensor", - unique_id="00:00:00:00:00:00_sensor", - original_name=ALIAS, - device_id=device.id, - ) - garbage_entity_reg = entity_reg.async_get_or_create( - config_entry=config_entry, - platform=DOMAIN, - domain="sensor", - unique_id="garbage", - original_name=ALIAS, - device_id=other_device.id, - ) - - with _patch_discovery(), _patch_single_discovery(): - await setup.async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() - - migrated_entry = None - for entry in hass.config_entries.async_entries(DOMAIN): - if entry.unique_id == DOMAIN: - migrated_entry = entry - break - - assert migrated_entry is not None - - assert device.config_entries == {migrated_entry.entry_id} - assert light_entity_reg.config_entry_id == migrated_entry.entry_id - assert power_sensor_entity_reg.config_entry_id == migrated_entry.entry_id - assert ignored_entity_reg.config_entry_id == other_domain_config_entry.entry_id - assert garbage_entity_reg.config_entry_id == config_entry.entry_id - - assert er.async_entries_for_config_entry(entity_reg, config_entry) == [] - - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await hass.async_block_till_done() - - legacy_entry = None - for entry in hass.config_entries.async_entries(DOMAIN): - if entry.unique_id == DOMAIN: - legacy_entry = entry - break - - assert legacy_entry is not None - - -async def test_migrate_from_yaml(hass: HomeAssistant): - """Test migrate from yaml.""" - config = { - DOMAIN: { - CONF_DISCOVERY: False, - CONF_SWITCH: [{CONF_HOST: IP_ADDRESS}], - } - } - with _patch_discovery(), _patch_single_discovery(): - await setup.async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - - migrated_entry = None - for entry in hass.config_entries.async_entries(DOMAIN): - if entry.unique_id == MAC_ADDRESS: - migrated_entry = entry - break - - assert migrated_entry is not None - assert migrated_entry.data[CONF_HOST] == IP_ADDRESS - - -async def test_migrate_from_legacy_entry(hass: HomeAssistant): - """Test migrate from legacy entry that was already imported from yaml.""" - data = { - CONF_DISCOVERY: False, - CONF_SWITCH: [{CONF_HOST: IP_ADDRESS}], - } - config_entry = MockConfigEntry(domain=DOMAIN, data=data, unique_id=DOMAIN) - config_entry.add_to_hass(hass) - with _patch_discovery(), _patch_single_discovery(): - await setup.async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() - - migrated_entry = None - for entry in hass.config_entries.async_entries(DOMAIN): - if entry.unique_id == MAC_ADDRESS: - migrated_entry = entry - break - - assert migrated_entry is not None - assert migrated_entry.data[CONF_HOST] == IP_ADDRESS From 26dc52623417c4a9c7a65f14e4ecd9e6d0f5103f Mon Sep 17 00:00:00 2001 From: Brynley McDonald Date: Tue, 21 Dec 2021 23:35:54 +1300 Subject: [PATCH 0874/2644] Add slugify as a template filter (#58724) --- homeassistant/helpers/template.py | 14 +++++++++++++- tests/helpers/test_template.py | 20 ++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 0ba1d6bfa14..2a605561572 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -53,7 +53,12 @@ from homeassistant.helpers import ( ) from homeassistant.helpers.typing import TemplateVarsType from homeassistant.loader import bind_hass -from homeassistant.util import convert, dt as dt_util, location as loc_util +from homeassistant.util import ( + convert, + dt as dt_util, + location as loc_util, + slugify as slugify_util, +) from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.thread import ThreadWithException @@ -1753,6 +1758,11 @@ def urlencode(value): return urllib_urlencode(value).encode("utf-8") +def slugify(value, separator="_"): + """Convert a string into a slug, such as what is used for entity ids.""" + return slugify_util(value, separator=separator) + + @contextmanager def set_template(template_str: str, action: str) -> Generator: """Store template being parsed or rendered in a Contextvar to aid error handling.""" @@ -1866,6 +1876,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.filters["float"] = forgiving_float_filter self.filters["int"] = forgiving_int_filter self.filters["relative_time"] = relative_time + self.filters["slugify"] = slugify self.globals["log"] = logarithm self.globals["sin"] = sine self.globals["cos"] = cosine @@ -1894,6 +1905,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.globals["int"] = forgiving_int self.globals["pack"] = struct_pack self.globals["unpack"] = struct_unpack + self.globals["slugify"] = slugify self.tests["match"] = regex_match self.tests["search"] = regex_search diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 4a97b99d05d..42a725eaff5 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -865,6 +865,26 @@ def test_base64_decode(hass): ) +def test_slugify(hass): + """Test the slugify filter.""" + assert ( + template.Template('{{ slugify("Home Assistant") }}', hass).async_render() + == "home_assistant" + ) + assert ( + template.Template('{{ "Home Assistant" | slugify }}', hass).async_render() + == "home_assistant" + ) + assert ( + template.Template('{{ slugify("Home Assistant", "-") }}', hass).async_render() + == "home-assistant" + ) + assert ( + template.Template('{{ "Home Assistant" | slugify("-") }}', hass).async_render() + == "home-assistant" + ) + + def test_ordinal(hass): """Test the ordinal filter.""" tests = [ From ea58432721f3f2d7a35eb546b2c3775143d1b525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 21 Dec 2021 12:56:00 +0200 Subject: [PATCH 0875/2644] Use DeviceAutomationType in tests/components/[s-z]* (#62450) --- tests/components/select/test_device_action.py | 5 ++- .../select/test_device_condition.py | 5 ++- .../components/select/test_device_trigger.py | 5 ++- .../sensor/test_device_condition.py | 17 +++++--- .../components/sensor/test_device_trigger.py | 13 ++++-- .../components/shelly/test_device_trigger.py | 11 +++-- tests/components/switch/test_device_action.py | 5 ++- .../switch/test_device_condition.py | 11 +++-- .../components/switch/test_device_trigger.py | 11 +++-- .../components/tasmota/test_device_trigger.py | 41 ++++++++++++++----- tests/components/vacuum/test_device_action.py | 5 ++- .../vacuum/test_device_condition.py | 5 ++- .../components/vacuum/test_device_trigger.py | 11 +++-- .../water_heater/test_device_action.py | 5 ++- tests/components/wemo/test_device_trigger.py | 3 +- tests/components/zha/test_device_action.py | 5 ++- tests/components/zha/test_device_trigger.py | 9 +++- .../components/zwave_js/test_device_action.py | 9 +++- .../zwave_js/test_device_condition.py | 5 ++- .../zwave_js/test_device_trigger.py | 29 +++++++++---- 20 files changed, 157 insertions(+), 53 deletions(-) diff --git a/tests/components/select/test_device_action.py b/tests/components/select/test_device_action.py index 5c2486a4e26..1d6ebe92626 100644 --- a/tests/components/select/test_device_action.py +++ b/tests/components/select/test_device_action.py @@ -3,6 +3,7 @@ import pytest import voluptuous_serialize from homeassistant.components import automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.select import DOMAIN from homeassistant.components.select.device_action import async_get_action_capabilities from homeassistant.core import HomeAssistant @@ -56,7 +57,9 @@ async def test_get_actions( "entity_id": "select.test_5678", } ] - actions = await async_get_device_automations(hass, "action", device_entry.id) + actions = await async_get_device_automations( + hass, DeviceAutomationType.ACTION, device_entry.id + ) assert_lists_same(actions, expected_actions) diff --git a/tests/components/select/test_device_condition.py b/tests/components/select/test_device_condition.py index d5ee88156cf..5a309924f81 100644 --- a/tests/components/select/test_device_condition.py +++ b/tests/components/select/test_device_condition.py @@ -5,6 +5,7 @@ import pytest import voluptuous_serialize from homeassistant.components import automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.select import DOMAIN from homeassistant.components.select.device_condition import ( async_get_condition_capabilities, @@ -67,7 +68,9 @@ async def test_get_conditions( "entity_id": f"{DOMAIN}.test_5678", } ] - conditions = await async_get_device_automations(hass, "condition", device_entry.id) + conditions = await async_get_device_automations( + hass, DeviceAutomationType.CONDITION, device_entry.id + ) assert_lists_same(conditions, expected_conditions) diff --git a/tests/components/select/test_device_trigger.py b/tests/components/select/test_device_trigger.py index b0066e9ac22..3798d72b948 100644 --- a/tests/components/select/test_device_trigger.py +++ b/tests/components/select/test_device_trigger.py @@ -5,6 +5,7 @@ import pytest import voluptuous_serialize from homeassistant.components import automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.select import DOMAIN from homeassistant.components.select.device_trigger import ( async_get_trigger_capabilities, @@ -64,7 +65,9 @@ async def test_get_triggers( "entity_id": f"{DOMAIN}.test_5678", } ] - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert_lists_same(triggers, expected_triggers) diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py index 5504bd997af..ca3b046cbbd 100644 --- a/tests/components/sensor/test_device_condition.py +++ b/tests/components/sensor/test_device_condition.py @@ -2,6 +2,7 @@ import pytest import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.sensor import DEVICE_CLASSES, DOMAIN, SensorDeviceClass from homeassistant.components.sensor.device_condition import ENTITY_CONDITIONS from homeassistant.const import CONF_PLATFORM, PERCENTAGE, STATE_UNKNOWN @@ -73,7 +74,9 @@ async def test_get_conditions(hass, device_reg, entity_reg, enable_custom_integr for condition in ENTITY_CONDITIONS[device_class] if device_class != "none" ] - conditions = await async_get_device_automations(hass, "condition", device_entry.id) + conditions = await async_get_device_automations( + hass, DeviceAutomationType.CONDITION, device_entry.id + ) assert conditions == expected_conditions @@ -111,7 +114,9 @@ async def test_get_conditions_no_state(hass, device_reg, entity_reg): for condition in ENTITY_CONDITIONS[device_class] if device_class != "none" ] - conditions = await async_get_device_automations(hass, "condition", device_entry.id) + conditions = await async_get_device_automations( + hass, DeviceAutomationType.CONDITION, device_entry.id + ) assert conditions == expected_conditions @@ -173,11 +178,13 @@ async def test_get_condition_capabilities( }, ] } - conditions = await async_get_device_automations(hass, "condition", device_entry.id) + conditions = await async_get_device_automations( + hass, DeviceAutomationType.CONDITION, device_entry.id + ) assert len(conditions) == 1 for condition in conditions: capabilities = await async_get_device_automation_capabilities( - hass, "condition", condition + hass, DeviceAutomationType.CONDITION, condition ) assert capabilities == expected_capabilities @@ -215,7 +222,7 @@ async def test_get_condition_capabilities_none( expected_capabilities = {} for condition in conditions: capabilities = await async_get_device_automation_capabilities( - hass, "condition", condition + hass, DeviceAutomationType.CONDITION, condition ) assert capabilities == expected_capabilities diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index 41671a4faa8..b6e987b5fda 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -4,6 +4,7 @@ from datetime import timedelta import pytest import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.sensor import DEVICE_CLASSES, DOMAIN, SensorDeviceClass from homeassistant.components.sensor.device_trigger import ENTITY_TRIGGERS from homeassistant.const import CONF_PLATFORM, PERCENTAGE, STATE_UNKNOWN @@ -77,7 +78,9 @@ async def test_get_triggers(hass, device_reg, entity_reg, enable_custom_integrat for trigger in ENTITY_TRIGGERS[device_class] if device_class != "none" ] - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert len(triggers) == 24 assert triggers == expected_triggers @@ -141,11 +144,13 @@ async def test_get_trigger_capabilities( {"name": "for", "optional": True, "type": "positive_time_period_dict"}, ] } - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert len(triggers) == 1 for trigger in triggers: capabilities = await async_get_device_automation_capabilities( - hass, "trigger", trigger + hass, DeviceAutomationType.TRIGGER, trigger ) assert capabilities == expected_capabilities @@ -183,7 +188,7 @@ async def test_get_trigger_capabilities_none( expected_capabilities = {} for trigger in triggers: capabilities = await async_get_device_automation_capabilities( - hass, "trigger", trigger + hass, DeviceAutomationType.TRIGGER, trigger ) assert capabilities == expected_capabilities diff --git a/tests/components/shelly/test_device_trigger.py b/tests/components/shelly/test_device_trigger.py index a81662159c2..32fff324b55 100644 --- a/tests/components/shelly/test_device_trigger.py +++ b/tests/components/shelly/test_device_trigger.py @@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, Mock import pytest from homeassistant.components import automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) @@ -49,7 +50,7 @@ async def test_get_triggers_block_device(hass, coap_wrapper): ] triggers = await async_get_device_automations( - hass, "trigger", coap_wrapper.device_id + hass, DeviceAutomationType.TRIGGER, coap_wrapper.device_id ) assert_lists_same(triggers, expected_triggers) @@ -97,7 +98,7 @@ async def test_get_triggers_rpc_device(hass, rpc_wrapper): ] triggers = await async_get_device_automations( - hass, "trigger", rpc_wrapper.device_id + hass, DeviceAutomationType.TRIGGER, rpc_wrapper.device_id ) assert_lists_same(triggers, expected_triggers) @@ -162,7 +163,7 @@ async def test_get_triggers_button(hass): ] triggers = await async_get_device_automations( - hass, "trigger", coap_wrapper.device_id + hass, DeviceAutomationType.TRIGGER, coap_wrapper.device_id ) assert_lists_same(triggers, expected_triggers) @@ -179,7 +180,9 @@ async def test_get_triggers_for_invalid_device_id(hass, device_reg, coap_wrapper ) with pytest.raises(InvalidDeviceAutomationConfig): - await async_get_device_automations(hass, "trigger", invalid_device.id) + await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, invalid_device.id + ) async def test_if_fires_on_click_event_block_device(hass, calls, coap_wrapper): diff --git a/tests/components/switch/test_device_action.py b/tests/components/switch/test_device_action.py index 2ccfb26d3ef..bdfaac5d1ef 100644 --- a/tests/components/switch/test_device_action.py +++ b/tests/components/switch/test_device_action.py @@ -2,6 +2,7 @@ import pytest import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.switch import DOMAIN from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON from homeassistant.helpers import device_registry @@ -64,7 +65,9 @@ async def test_get_actions(hass, device_reg, entity_reg): "entity_id": f"{DOMAIN}.test_5678", }, ] - actions = await async_get_device_automations(hass, "action", device_entry.id) + actions = await async_get_device_automations( + hass, DeviceAutomationType.ACTION, device_entry.id + ) assert actions == expected_actions diff --git a/tests/components/switch/test_device_condition.py b/tests/components/switch/test_device_condition.py index e2102976f8d..b4663008b17 100644 --- a/tests/components/switch/test_device_condition.py +++ b/tests/components/switch/test_device_condition.py @@ -5,6 +5,7 @@ from unittest.mock import patch import pytest import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.switch import DOMAIN from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON from homeassistant.helpers import device_registry @@ -65,7 +66,9 @@ async def test_get_conditions(hass, device_reg, entity_reg): "entity_id": f"{DOMAIN}.test_5678", }, ] - conditions = await async_get_device_automations(hass, "condition", device_entry.id) + conditions = await async_get_device_automations( + hass, DeviceAutomationType.CONDITION, device_entry.id + ) assert conditions == expected_conditions @@ -83,10 +86,12 @@ async def test_get_condition_capabilities(hass, device_reg, entity_reg): {"name": "for", "optional": True, "type": "positive_time_period_dict"} ] } - conditions = await async_get_device_automations(hass, "condition", device_entry.id) + conditions = await async_get_device_automations( + hass, DeviceAutomationType.CONDITION, device_entry.id + ) for condition in conditions: capabilities = await async_get_device_automation_capabilities( - hass, "condition", condition + hass, DeviceAutomationType.CONDITION, condition ) assert capabilities == expected_capabilities diff --git a/tests/components/switch/test_device_trigger.py b/tests/components/switch/test_device_trigger.py index e871bf6f645..6697e9e1949 100644 --- a/tests/components/switch/test_device_trigger.py +++ b/tests/components/switch/test_device_trigger.py @@ -4,6 +4,7 @@ from datetime import timedelta import pytest import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.switch import DOMAIN from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON from homeassistant.helpers import device_registry @@ -65,7 +66,9 @@ async def test_get_triggers(hass, device_reg, entity_reg): "entity_id": f"{DOMAIN}.test_5678", }, ] - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert triggers == expected_triggers @@ -83,10 +86,12 @@ async def test_get_trigger_capabilities(hass, device_reg, entity_reg): {"name": "for", "optional": True, "type": "positive_time_period_dict"} ] } - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) for trigger in triggers: capabilities = await async_get_device_automation_capabilities( - hass, "trigger", trigger + hass, DeviceAutomationType.TRIGGER, trigger ) assert capabilities == expected_capabilities diff --git a/tests/components/tasmota/test_device_trigger.py b/tests/components/tasmota/test_device_trigger.py index aba448bcbe5..24467ed5359 100644 --- a/tests/components/tasmota/test_device_trigger.py +++ b/tests/components/tasmota/test_device_trigger.py @@ -7,6 +7,7 @@ from hatasmota.switch import TasmotaSwitchTriggerConfig import pytest import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.tasmota import _LOGGER from homeassistant.components.tasmota.const import DEFAULT_PREFIX, DOMAIN from homeassistant.helpers import device_registry as dr @@ -56,7 +57,9 @@ async def test_get_triggers_btn(hass, device_reg, entity_reg, mqtt_mock, setup_t "subtype": "button_2", }, ] - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert_lists_same(triggers, expected_triggers) @@ -82,7 +85,9 @@ async def test_get_triggers_swc(hass, device_reg, entity_reg, mqtt_mock, setup_t "subtype": "switch_1", }, ] - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert_lists_same(triggers, expected_triggers) @@ -125,7 +130,9 @@ async def test_get_unknown_triggers( }, ) - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert_lists_same(triggers, []) @@ -144,7 +151,9 @@ async def test_get_non_existing_triggers( device_entry = device_reg.async_get_device( set(), {(dr.CONNECTION_NETWORK_MAC, mac)} ) - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert_lists_same(triggers, []) @@ -170,7 +179,9 @@ async def test_discover_bad_triggers( device_entry = device_reg.async_get_device( set(), {(dr.CONNECTION_NETWORK_MAC, mac)} ) - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert_lists_same(triggers, []) # Trigger an exception when the entity is discovered @@ -204,7 +215,9 @@ async def test_discover_bad_triggers( device_entry = device_reg.async_get_device( set(), {(dr.CONNECTION_NETWORK_MAC, mac)} ) - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert_lists_same(triggers, []) # Rediscover without exception @@ -221,7 +234,9 @@ async def test_discover_bad_triggers( "subtype": "switch_1", }, ] - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert_lists_same(triggers, expected_triggers) @@ -270,7 +285,9 @@ async def test_update_remove_triggers( expected_triggers2 = copy.deepcopy(expected_triggers1) expected_triggers2[1]["type"] = "button_double_press" - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) for expected in expected_triggers1: assert expected in triggers @@ -278,7 +295,9 @@ async def test_update_remove_triggers( async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config2)) await hass.async_block_till_done() - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) for expected in expected_triggers2: assert expected in triggers @@ -286,7 +305,9 @@ async def test_update_remove_triggers( async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config3)) await hass.async_block_till_done() - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert triggers == [] diff --git a/tests/components/vacuum/test_device_action.py b/tests/components/vacuum/test_device_action.py index aa5dc1786f7..ce357f3d6ca 100644 --- a/tests/components/vacuum/test_device_action.py +++ b/tests/components/vacuum/test_device_action.py @@ -2,6 +2,7 @@ import pytest import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.vacuum import DOMAIN from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component @@ -52,7 +53,9 @@ async def test_get_actions(hass, device_reg, entity_reg): "entity_id": "vacuum.test_5678", }, ] - actions = await async_get_device_automations(hass, "action", device_entry.id) + actions = await async_get_device_automations( + hass, DeviceAutomationType.ACTION, device_entry.id + ) assert_lists_same(actions, expected_actions) diff --git a/tests/components/vacuum/test_device_condition.py b/tests/components/vacuum/test_device_condition.py index 84a25d183b8..086d34f65cd 100644 --- a/tests/components/vacuum/test_device_condition.py +++ b/tests/components/vacuum/test_device_condition.py @@ -2,6 +2,7 @@ import pytest import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.vacuum import ( DOMAIN, STATE_CLEANING, @@ -65,7 +66,9 @@ async def test_get_conditions(hass, device_reg, entity_reg): "entity_id": f"{DOMAIN}.test_5678", }, ] - conditions = await async_get_device_automations(hass, "condition", device_entry.id) + conditions = await async_get_device_automations( + hass, DeviceAutomationType.CONDITION, device_entry.id + ) assert_lists_same(conditions, expected_conditions) diff --git a/tests/components/vacuum/test_device_trigger.py b/tests/components/vacuum/test_device_trigger.py index 495ba02967b..1315952fdca 100644 --- a/tests/components/vacuum/test_device_trigger.py +++ b/tests/components/vacuum/test_device_trigger.py @@ -4,6 +4,7 @@ from datetime import timedelta import pytest import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.vacuum import DOMAIN, STATE_CLEANING, STATE_DOCKED from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component @@ -65,7 +66,9 @@ async def test_get_triggers(hass, device_reg, entity_reg): "entity_id": f"{DOMAIN}.test_5678", }, ] - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert_lists_same(triggers, expected_triggers) @@ -79,11 +82,13 @@ async def test_get_trigger_capabilities(hass, device_reg, entity_reg): ) entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device_entry.id + ) assert len(triggers) == 2 for trigger in triggers: capabilities = await async_get_device_automation_capabilities( - hass, "trigger", trigger + hass, DeviceAutomationType.TRIGGER, trigger ) assert capabilities == { "extra_fields": [ diff --git a/tests/components/water_heater/test_device_action.py b/tests/components/water_heater/test_device_action.py index 060d9ead29f..25b79910bdb 100644 --- a/tests/components/water_heater/test_device_action.py +++ b/tests/components/water_heater/test_device_action.py @@ -2,6 +2,7 @@ import pytest import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.water_heater import DOMAIN from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component @@ -52,7 +53,9 @@ async def test_get_actions(hass, device_reg, entity_reg): "entity_id": "water_heater.test_5678", }, ] - actions = await async_get_device_automations(hass, "action", device_entry.id) + actions = await async_get_device_automations( + hass, DeviceAutomationType.ACTION, device_entry.id + ) assert_lists_same(actions, expected_actions) diff --git a/tests/components/wemo/test_device_trigger.py b/tests/components/wemo/test_device_trigger.py index 0ad7d95dd7a..0d9c537b24d 100644 --- a/tests/components/wemo/test_device_trigger.py +++ b/tests/components/wemo/test_device_trigger.py @@ -3,6 +3,7 @@ import pytest from pywemo.subscribe import EVENT_TYPE_LONG_PRESS from homeassistant.components.automation import DOMAIN as AUTOMATION_DOMAIN +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.wemo.const import DOMAIN, WEMO_SUBSCRIPTION_EVENT from homeassistant.const import ( CONF_DEVICE_ID, @@ -81,7 +82,7 @@ async def test_get_triggers(hass, wemo_entity): }, ] triggers = await async_get_device_automations( - hass, "trigger", wemo_entity.device_id + hass, DeviceAutomationType.TRIGGER, wemo_entity.device_id ) assert_lists_same(triggers, expected_triggers) diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py index b67f54a0a16..38232e033cc 100644 --- a/tests/components/zha/test_device_action.py +++ b/tests/components/zha/test_device_action.py @@ -8,6 +8,7 @@ import zigpy.zcl.clusters.security as security import zigpy.zcl.foundation as zcl_f import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.zha import DOMAIN from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component @@ -51,7 +52,9 @@ async def test_get_actions(hass, device_ias): ha_device_registry = dr.async_get(hass) reg_device = ha_device_registry.async_get_device({(DOMAIN, ieee_address)}) - actions = await async_get_device_automations(hass, "action", reg_device.id) + actions = await async_get_device_automations( + hass, DeviceAutomationType.ACTION, reg_device.id + ) expected_actions = [ {"domain": DOMAIN, "type": "squawk", "device_id": reg_device.id}, diff --git a/tests/components/zha/test_device_trigger.py b/tests/components/zha/test_device_trigger.py index fbfc2144a6a..87cf6ba344d 100644 --- a/tests/components/zha/test_device_trigger.py +++ b/tests/components/zha/test_device_trigger.py @@ -7,6 +7,7 @@ import zigpy.profiles.zha import zigpy.zcl.clusters.general as general import homeassistant.components.automation as automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -90,7 +91,9 @@ async def test_triggers(hass, mock_devices): ha_device_registry = dr.async_get(hass) reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}) - triggers = await async_get_device_automations(hass, "trigger", reg_device.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, reg_device.id + ) expected_triggers = [ { @@ -148,7 +151,9 @@ async def test_no_triggers(hass, mock_devices): ha_device_registry = dr.async_get(hass) reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}) - triggers = await async_get_device_automations(hass, "trigger", reg_device.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, reg_device.id + ) assert triggers == [ { "device_id": reg_device.id, diff --git a/tests/components/zwave_js/test_device_action.py b/tests/components/zwave_js/test_device_action.py index 65bc8e4bddb..c222287b5c0 100644 --- a/tests/components/zwave_js/test_device_action.py +++ b/tests/components/zwave_js/test_device_action.py @@ -6,6 +6,7 @@ from zwave_js_server.const import CommandClass from zwave_js_server.model.node import Node from homeassistant.components import automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.zwave_js import DOMAIN, device_action from homeassistant.components.zwave_js.helpers import get_device_id from homeassistant.config_entries import ConfigEntry @@ -66,7 +67,9 @@ async def test_get_actions( "subtype": f"{node.node_id}-112-0-3 (Beeper)", }, ] - actions = await async_get_device_automations(hass, "action", device.id) + actions = await async_get_device_automations( + hass, DeviceAutomationType.ACTION, device.id + ) for action in expected_actions: assert action in actions @@ -82,7 +85,9 @@ async def test_get_actions_meter( dev_reg = device_registry.async_get(hass) device = dev_reg.async_get_device({get_device_id(client, node)}) assert device - actions = await async_get_device_automations(hass, "action", device.id) + actions = await async_get_device_automations( + hass, DeviceAutomationType.ACTION, device.id + ) filtered_actions = [action for action in actions if action["type"] == "reset_meter"] assert len(filtered_actions) > 0 diff --git a/tests/components/zwave_js/test_device_condition.py b/tests/components/zwave_js/test_device_condition.py index 725d9574605..3919edbd340 100644 --- a/tests/components/zwave_js/test_device_condition.py +++ b/tests/components/zwave_js/test_device_condition.py @@ -10,6 +10,7 @@ from zwave_js_server.const import CommandClass from zwave_js_server.event import Event from homeassistant.components import automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) @@ -60,7 +61,9 @@ async def test_get_conditions(hass, client, lock_schlage_be469, integration) -> "device_id": device.id, }, ] - conditions = await async_get_device_automations(hass, "condition", device.id) + conditions = await async_get_device_automations( + hass, DeviceAutomationType.CONDITION, device.id + ) for condition in expected_conditions: assert condition in conditions diff --git a/tests/components/zwave_js/test_device_trigger.py b/tests/components/zwave_js/test_device_trigger.py index 22496d3deed..19c86af22ed 100644 --- a/tests/components/zwave_js/test_device_trigger.py +++ b/tests/components/zwave_js/test_device_trigger.py @@ -8,6 +8,7 @@ from zwave_js_server.event import Event from zwave_js_server.model.node import Node from homeassistant.components import automation +from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) @@ -50,7 +51,9 @@ async def test_get_notification_notification_triggers( "device_id": device.id, "command_class": CommandClass.NOTIFICATION, } - triggers = await async_get_device_automations(hass, "trigger", device.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device.id + ) assert expected_trigger in triggers @@ -314,7 +317,9 @@ async def test_get_node_status_triggers(hass, client, lock_schlage_be469, integr "device_id": device.id, "entity_id": entity_id, } - triggers = await async_get_device_automations(hass, "trigger", device.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device.id + ) assert expected_trigger in triggers @@ -466,7 +471,9 @@ async def test_get_basic_value_notification_triggers( "endpoint": 0, "subtype": "Endpoint 0", } - triggers = await async_get_device_automations(hass, "trigger", device.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device.id + ) assert expected_trigger in triggers @@ -631,7 +638,9 @@ async def test_get_central_scene_value_notification_triggers( "endpoint": 0, "subtype": "Endpoint 0 Scene 001", } - triggers = await async_get_device_automations(hass, "trigger", device.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device.id + ) assert expected_trigger in triggers @@ -802,7 +811,9 @@ async def test_get_scene_activation_value_notification_triggers( "endpoint": 0, "subtype": "Endpoint 0", } - triggers = await async_get_device_automations(hass, "trigger", device.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device.id + ) assert expected_trigger in triggers @@ -962,7 +973,9 @@ async def test_get_value_updated_value_triggers( "type": "zwave_js.value_updated.value", "device_id": device.id, } - triggers = await async_get_device_automations(hass, "trigger", device.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device.id + ) assert expected_trigger in triggers @@ -1121,7 +1134,9 @@ async def test_get_value_updated_config_parameter_triggers( "command_class": CommandClass.CONFIGURATION.value, "subtype": f"{node.node_id}-112-0-3 (Beeper)", } - triggers = await async_get_device_automations(hass, "trigger", device.id) + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device.id + ) assert expected_trigger in triggers From 83f3666aa850c397740ef5658dfe3e8deb263894 Mon Sep 17 00:00:00 2001 From: GJH <46995420+OpenMyDog@users.noreply.github.com> Date: Tue, 21 Dec 2021 18:59:57 +0800 Subject: [PATCH 0876/2644] Add USB discover for Sonoff zigbee dongle plus (#62171) --- homeassistant/components/zha/manifest.json | 1 + homeassistant/generated/usb.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 960bb55e004..f3a7f4e3755 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -16,6 +16,7 @@ ], "usb": [ {"vid":"10C4","pid":"EA60","description":"*2652*","known_devices":["slae.sh cc2652rb stick"]}, + {"vid":"10C4","pid":"EA60","description":"*sonoff*plus*","known_devices":["sonoff zigbee dongle plus"]}, {"vid":"10C4","pid":"EA60","description":"*tubeszb*","known_devices":["TubesZB Coordinator"]}, {"vid":"1A86","pid":"7523","description":"*tubeszb*","known_devices":["TubesZB Coordinator"]}, {"vid":"1A86","pid":"7523","description":"*zigstar*","known_devices":["ZigStar Coordinators"]}, diff --git a/homeassistant/generated/usb.py b/homeassistant/generated/usb.py index 57f2090fee2..d9d9da5aff0 100644 --- a/homeassistant/generated/usb.py +++ b/homeassistant/generated/usb.py @@ -17,6 +17,12 @@ USB = [ "pid": "EA60", "description": "*2652*" }, + { + "domain": "zha", + "vid": "10C4", + "pid": "EA60", + "description": "*sonoff*plus*" + }, { "domain": "zha", "vid": "10C4", From ed9e17aeec1a2acaaf0a37e84b68137890e686aa Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 21 Dec 2021 06:02:21 -0500 Subject: [PATCH 0877/2644] Clean up ssdp flow in dlna_dmr (#62466) --- homeassistant/components/dlna_dmr/config_flow.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/homeassistant/components/dlna_dmr/config_flow.py b/homeassistant/components/dlna_dmr/config_flow.py index d3c1b6ca845..f8736d849ed 100644 --- a/homeassistant/components/dlna_dmr/config_flow.py +++ b/homeassistant/components/dlna_dmr/config_flow.py @@ -141,15 +141,6 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if not DmrDevice.SERVICE_IDS.issubset(discovery_service_ids): return self.async_abort(reason="not_dmr") - # Abort if a migration flow for the device's location is in progress - for progress in self._async_in_progress(include_uninitialized=True): - if progress["context"].get("unique_id") == self._location: - LOGGER.debug( - "Aborting SSDP setup because migration for %s is in progress", - self._location, - ) - return self.async_abort(reason="already_in_progress") - # Abort if another config entry has the same location, in case the # device doesn't have a static and unique UDN (breaking the UPnP spec). self._async_abort_entries_match({CONF_URL: self._location}) From eb897c6f482b46d3ee1d7e56374666953ce2870b Mon Sep 17 00:00:00 2001 From: Jonathan Keslin Date: Tue, 21 Dec 2021 03:06:08 -0800 Subject: [PATCH 0878/2644] Add device registry information to Blink entities (#62449) --- .../components/blink/alarm_control_panel.py | 6 +++++- homeassistant/components/blink/binary_sensor.py | 16 +++++++++++++++- homeassistant/components/blink/camera.py | 7 +++++++ homeassistant/components/blink/sensor.py | 11 ++++++++++- 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/blink/alarm_control_panel.py b/homeassistant/components/blink/alarm_control_panel.py index ea215ebb689..b81c0668530 100644 --- a/homeassistant/components/blink/alarm_control_panel.py +++ b/homeassistant/components/blink/alarm_control_panel.py @@ -8,8 +8,9 @@ from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_DISARMED, ) +from homeassistant.helpers.entity import DeviceInfo -from .const import DEFAULT_ATTRIBUTION, DOMAIN +from .const import DEFAULT_ATTRIBUTION, DEFAULT_BRAND, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -39,6 +40,9 @@ class BlinkSyncModule(AlarmControlPanelEntity): self._name = name self._attr_unique_id = sync.serial self._attr_name = f"{DOMAIN} {name}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, sync.serial)}, name=name, manufacturer=DEFAULT_BRAND + ) def update(self): """Update the state of the device.""" diff --git a/homeassistant/components/blink/binary_sensor.py b/homeassistant/components/blink/binary_sensor.py index 6e5dd58c368..737d5372a15 100644 --- a/homeassistant/components/blink/binary_sensor.py +++ b/homeassistant/components/blink/binary_sensor.py @@ -6,14 +6,22 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) +from homeassistant.helpers.entity import DeviceInfo, EntityCategory -from .const import DOMAIN, TYPE_BATTERY, TYPE_CAMERA_ARMED, TYPE_MOTION_DETECTED +from .const import ( + DEFAULT_BRAND, + DOMAIN, + TYPE_BATTERY, + TYPE_CAMERA_ARMED, + TYPE_MOTION_DETECTED, +) BINARY_SENSORS_TYPES: tuple[BinarySensorEntityDescription, ...] = ( BinarySensorEntityDescription( key=TYPE_BATTERY, name="Battery", device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, ), BinarySensorEntityDescription( key=TYPE_CAMERA_ARMED, @@ -49,6 +57,12 @@ class BlinkBinarySensor(BinarySensorEntity): self._attr_name = f"{DOMAIN} {camera} {description.name}" self._camera = data.cameras[camera] self._attr_unique_id = f"{self._camera.serial}-{description.key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._camera.serial)}, + name=camera, + manufacturer=DEFAULT_BRAND, + model=self._camera.camera_type, + ) def update(self): """Update sensor state.""" diff --git a/homeassistant/components/blink/camera.py b/homeassistant/components/blink/camera.py index 9a617bbc781..6a264afee35 100644 --- a/homeassistant/components/blink/camera.py +++ b/homeassistant/components/blink/camera.py @@ -5,6 +5,7 @@ import logging from homeassistant.components.camera import Camera from homeassistant.helpers import entity_platform +from homeassistant.helpers.entity import DeviceInfo from .const import DEFAULT_BRAND, DOMAIN, SERVICE_TRIGGER @@ -37,6 +38,12 @@ class BlinkCamera(Camera): self._attr_name = f"{DOMAIN} {name}" self._camera = camera self._attr_unique_id = f"{camera.serial}-camera" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, camera.serial)}, + name=name, + manufacturer=DEFAULT_BRAND, + model=camera.camera_type, + ) _LOGGER.debug("Initialized blink camera %s", self.name) @property diff --git a/homeassistant/components/blink/sensor.py b/homeassistant/components/blink/sensor.py index 8a5947ec8bf..bb58a01e534 100644 --- a/homeassistant/components/blink/sensor.py +++ b/homeassistant/components/blink/sensor.py @@ -9,8 +9,9 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.const import SIGNAL_STRENGTH_DECIBELS_MILLIWATT, TEMP_FAHRENHEIT +from homeassistant.helpers.entity import DeviceInfo, EntityCategory -from .const import DOMAIN, TYPE_TEMPERATURE, TYPE_WIFI_STRENGTH +from .const import DEFAULT_BRAND, DOMAIN, TYPE_TEMPERATURE, TYPE_WIFI_STRENGTH _LOGGER = logging.getLogger(__name__) @@ -20,12 +21,14 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( name="Temperature", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, + entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key=TYPE_WIFI_STRENGTH, name="Wifi Signal", native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=EntityCategory.DIAGNOSTIC, ), ) @@ -57,6 +60,12 @@ class BlinkSensor(SensorEntity): if description.key == "temperature" else description.key ) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._camera.serial)}, + name=camera, + manufacturer=DEFAULT_BRAND, + model=self._camera.camera_type, + ) def update(self): """Retrieve sensor data from the camera.""" From 4b30c9631f0f0a1ad59005f316b3f03975d2accd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Dec 2021 05:09:41 -0600 Subject: [PATCH 0879/2644] Add set_music_mode service to flux_led for detailed music mode control (#62429) --- homeassistant/components/flux_led/entity.py | 5 ++ homeassistant/components/flux_led/light.py | 51 +++++++++++++++++ .../components/flux_led/manifest.json | 2 +- .../components/flux_led/services.yaml | 57 +++++++++++++++++++ homeassistant/components/flux_led/switch.py | 3 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/flux_led/test_light.py | 52 +++++++++++++++++ 8 files changed, 169 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/flux_led/entity.py b/homeassistant/components/flux_led/entity.py index 1589bd0fb3a..6296f11b21a 100644 --- a/homeassistant/components/flux_led/entity.py +++ b/homeassistant/components/flux_led/entity.py @@ -76,6 +76,11 @@ class FluxEntity(CoordinatorEntity): unique_id, self._device, coordinator.entry ) + async def _async_ensure_device_on(self) -> None: + """Turn the device on if it needs to be turned on before a command.""" + if self._device.requires_turn_on and not self._device.is_on: + await self._device.async_turn_on() + @property def extra_state_attributes(self) -> dict[str, str]: """Return the attributes.""" diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index 6bde2ec3b31..5d8da698a9a 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -6,6 +6,7 @@ import logging from typing import Any, Final from flux_led.const import MultiColorEffects +from flux_led.protocol import MusicMode from flux_led.utils import ( color_temp_to_white_levels, rgbcw_brightness, @@ -73,6 +74,11 @@ MODE_ATTRS = { ATTR_WHITE, } +ATTR_FOREGROUND_COLOR: Final = "foreground_color" +ATTR_BACKGROUND_COLOR: Final = "background_color" +ATTR_SENSITIVITY: Final = "sensitivity" +ATTR_LIGHT_SCREEN: Final = "light_screen" + # Constant color temp values for 2 flux_led special modes # Warm-white and Cool-white modes COLOR_TEMP_WARM_VS_COLD_WHITE_CUT_OFF: Final = 285 @@ -81,6 +87,7 @@ EFFECT_CUSTOM: Final = "custom" SERVICE_CUSTOM_EFFECT: Final = "set_custom_effect" SERVICE_SET_ZONES: Final = "set_zones" +SERVICE_SET_MUSIC_MODE: Final = "set_music_mode" CUSTOM_EFFECT_DICT: Final = { vol.Required(CONF_COLORS): vol.All( @@ -96,6 +103,25 @@ CUSTOM_EFFECT_DICT: Final = { ), } +SET_MUSIC_MODE_DICT: Final = { + vol.Optional(ATTR_SENSITIVITY, default=100): vol.All( + vol.Range(min=0, max=100), vol.Coerce(int) + ), + vol.Optional(ATTR_BRIGHTNESS, default=100): vol.All( + vol.Range(min=0, max=100), vol.Coerce(int) + ), + vol.Optional(ATTR_EFFECT, default=1): vol.All( + vol.Range(min=1, max=16), vol.Coerce(int) + ), + vol.Optional(ATTR_LIGHT_SCREEN, default=False): bool, + vol.Optional(ATTR_FOREGROUND_COLOR): vol.All( + vol.Coerce(tuple), vol.ExactSequence((cv.byte,) * 3) + ), + vol.Optional(ATTR_BACKGROUND_COLOR): vol.All( + vol.Coerce(tuple), vol.ExactSequence((cv.byte,) * 3) + ), +} + SET_ZONES_DICT: Final = { vol.Required(CONF_COLORS): vol.All( cv.ensure_list, @@ -130,6 +156,11 @@ async def async_setup_entry( SET_ZONES_DICT, "async_set_zones", ) + platform.async_register_entity_service( + SERVICE_SET_MUSIC_MODE, + SET_MUSIC_MODE_DICT, + "async_set_music_mode", + ) options = entry.options try: @@ -330,3 +361,23 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity): speed_pct, _str_to_multi_color_effect(effect), ) + + async def async_set_music_mode( + self, + sensitivity: int, + brightness: int, + effect: int, + light_screen: bool, + foreground_color: tuple[int, int, int] | None = None, + background_color: tuple[int, int, int] | None = None, + ) -> None: + """Configure music mode.""" + await self._async_ensure_device_on() + await self._device.async_set_music_mode( + sensitivity=sensitivity, + brightness=brightness, + mode=MusicMode.LIGHT_SCREEN.value if light_screen else None, + effect=effect, + foreground_color=foreground_color, + background_color=background_color, + ) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 51498f99590..ab1e78bc1ed 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["network"], "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.27.8"], + "requirements": ["flux_led==0.27.10"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/homeassistant/components/flux_led/services.yaml b/homeassistant/components/flux_led/services.yaml index ff1e0679807..8a16f456311 100644 --- a/homeassistant/components/flux_led/services.yaml +++ b/homeassistant/components/flux_led/services.yaml @@ -77,3 +77,60 @@ set_zones: - "strobe" - "jump" - "breathing" +set_music_mode: + description: Configure music mode on Controller RGB with MIC (0x08), Addressable v2 (0xA2), and Addressable v3 (0xA3) devices that have a built-in microphone. + target: + entity: + integration: flux_led + domain: light + fields: + sensitivity: + description: Microphone sensitivity (0-100) + example: 80 + default: 100 + required: false + selector: + number: + min: 1 + step: 1 + max: 100 + unit_of_measurement: "%" + brightness: + description: Light brightness (0-100) + example: 80 + default: 100 + required: false + selector: + number: + min: 1 + step: 1 + max: 100 + unit_of_measurement: "%" + light_screen: + description: Light screen mode for 2 dimensional pixels (Addressable models only) + default: false + required: false + selector: + boolean: + effect: + description: Effect (1-16 on Addressable models, 0-3 on RGB with MIC models) + example: 1 + default: 1 + required: false + selector: + number: + min: 0 + step: 1 + max: 16 + foreground_color: + description: The foreground RGB color + example: "[255, 100, 100]" + required: false + selector: + object: + background_color: + description: The background RGB color (Addressable models only) + example: "[255, 100, 100]" + required: false + selector: + object: diff --git a/homeassistant/components/flux_led/switch.py b/homeassistant/components/flux_led/switch.py index 37f5f057353..8acb5e8861a 100644 --- a/homeassistant/components/flux_led/switch.py +++ b/homeassistant/components/flux_led/switch.py @@ -126,8 +126,7 @@ class FluxMusicSwitch(FluxEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the microphone on.""" - if self._device.requires_turn_on and not self._device.is_on: - await self._device.async_turn_on() + await self._async_ensure_device_on() await self._device.async_set_music_mode() self.async_write_ha_state() await self.coordinator.async_request_refresh() diff --git a/requirements_all.txt b/requirements_all.txt index b4e018cb7ca..2734e6309b2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -664,7 +664,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.27.8 +flux_led==0.27.10 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a20c95e1603..102bf1d3e5f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -405,7 +405,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.27.8 +flux_led==0.27.10 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/tests/components/flux_led/test_light.py b/tests/components/flux_led/test_light.py index cdf11a19a0b..66bcf8bd212 100644 --- a/tests/components/flux_led/test_light.py +++ b/tests/components/flux_led/test_light.py @@ -10,8 +10,10 @@ from flux_led.const import ( COLOR_MODE_RGBW as FLUX_COLOR_MODE_RGBW, COLOR_MODE_RGBWW as FLUX_COLOR_MODE_RGBWW, COLOR_MODES_RGB_W as FLUX_COLOR_MODES_RGB_W, + MODE_MUSIC, MultiColorEffects, ) +from flux_led.protocol import MusicMode import pytest from homeassistant.components import flux_led @@ -26,6 +28,12 @@ from homeassistant.components.flux_led.const import ( DOMAIN, TRANSITION_JUMP, ) +from homeassistant.components.flux_led.light import ( + ATTR_BACKGROUND_COLOR, + ATTR_FOREGROUND_COLOR, + ATTR_LIGHT_SCREEN, + ATTR_SENSITIVITY, +) from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_MODE, @@ -1191,3 +1199,47 @@ async def test_addressable_light(hass: HomeAssistant) -> None: bulb.async_turn_on.assert_called_once() bulb.async_turn_on.reset_mock() await async_mock_device_turn_on(hass, bulb) + + +async def test_music_mode_service(hass: HomeAssistant) -> None: + """Test music mode service.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + bulb.raw_state = bulb.raw_state._replace(model_num=0xA3) # has music mode + bulb.microphone = True + with _patch_discovery(), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.bulb_rgbcw_ddeeff" + assert hass.states.get(entity_id) + + bulb.effect = MODE_MUSIC + bulb.is_on = False + await hass.services.async_call( + DOMAIN, + "set_music_mode", + { + ATTR_ENTITY_ID: entity_id, + ATTR_EFFECT: 12, + ATTR_LIGHT_SCREEN: True, + ATTR_SENSITIVITY: 50, + ATTR_BRIGHTNESS: 50, + ATTR_FOREGROUND_COLOR: [255, 0, 0], + ATTR_BACKGROUND_COLOR: [0, 255, 0], + }, + blocking=True, + ) + bulb.async_set_music_mode.assert_called_once_with( + sensitivity=50, + brightness=50, + mode=MusicMode.LIGHT_SCREEN.value, + effect=12, + foreground_color=(255, 0, 0), + background_color=(0, 255, 0), + ) From e2fca2e3058fab7c9cd5e2d3c5285a37ba7143ff Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 21 Dec 2021 12:19:31 +0100 Subject: [PATCH 0880/2644] Support shorthand templates in condition actions (#61177) * Support shorthand templates in condition actions * Fix validation message * Fix tests --- homeassistant/helpers/config_validation.py | 50 ++++++++++++++++++-- tests/helpers/test_config_validation.py | 39 +++++++++++++++ tests/helpers/test_script.py | 55 ++++++++++++++++++++++ 3 files changed, 141 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index cbcfb551dad..32d1c5ec276 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -860,7 +860,10 @@ def removed( def key_value_schemas( - key: str, value_schemas: dict[Hashable, vol.Schema] + key: str, + value_schemas: dict[Hashable, vol.Schema], + default_schema: vol.Schema | None = None, + default_description: str | None = None, ) -> Callable[[Any], dict[Hashable, Any]]: """Create a validator that validates based on a value for specific key. @@ -876,8 +879,15 @@ def key_value_schemas( if isinstance(key_value, Hashable) and key_value in value_schemas: return cast(Dict[Hashable, Any], value_schemas[key_value](value)) + if default_schema: + with contextlib.suppress(vol.Invalid): + return cast(Dict[Hashable, Any], default_schema(value)) + + alternatives = ", ".join(str(key) for key in value_schemas) + if default_description: + alternatives += ", " + default_description raise vol.Invalid( - f"Unexpected value for {key}: '{key_value}'. Expected {', '.join(str(key) for key in value_schemas)}" + f"Unexpected value for {key}: '{key_value}'. Expected {alternatives}" ) return key_value_validator @@ -1207,6 +1217,40 @@ CONDITION_SCHEMA: vol.Schema = vol.Schema( ) ) + +dynamic_template_condition_action = vol.All( + vol.Schema( + {**CONDITION_BASE_SCHEMA, vol.Required(CONF_CONDITION): dynamic_template} + ), + lambda config: { + **config, + CONF_VALUE_TEMPLATE: config[CONF_CONDITION], + CONF_CONDITION: "template", + }, +) + + +CONDITION_ACTION_SCHEMA: vol.Schema = vol.Schema( + key_value_schemas( + CONF_CONDITION, + { + "and": AND_CONDITION_SCHEMA, + "device": DEVICE_CONDITION_SCHEMA, + "not": NOT_CONDITION_SCHEMA, + "numeric_state": NUMERIC_STATE_CONDITION_SCHEMA, + "or": OR_CONDITION_SCHEMA, + "state": STATE_CONDITION_SCHEMA, + "sun": SUN_CONDITION_SCHEMA, + "template": TEMPLATE_CONDITION_SCHEMA, + "time": TIME_CONDITION_SCHEMA, + "trigger": TRIGGER_CONDITION_SCHEMA, + "zone": ZONE_CONDITION_SCHEMA, + }, + dynamic_template_condition_action, + "a valid template", + ) +) + TRIGGER_BASE_SCHEMA = vol.Schema( {vol.Required(CONF_PLATFORM): str, vol.Optional(CONF_ID): str} ) @@ -1352,7 +1396,7 @@ ACTION_TYPE_SCHEMAS: dict[str, Callable[[Any], dict]] = { SCRIPT_ACTION_DELAY: _SCRIPT_DELAY_SCHEMA, SCRIPT_ACTION_WAIT_TEMPLATE: _SCRIPT_WAIT_TEMPLATE_SCHEMA, SCRIPT_ACTION_FIRE_EVENT: EVENT_SCHEMA, - SCRIPT_ACTION_CHECK_CONDITION: CONDITION_SCHEMA, + SCRIPT_ACTION_CHECK_CONDITION: CONDITION_ACTION_SCHEMA, SCRIPT_ACTION_DEVICE_AUTOMATION: DEVICE_ACTION_SCHEMA, SCRIPT_ACTION_ACTIVATE_SCENE: _SCRIPT_SCENE_SCHEMA, SCRIPT_ACTION_REPEAT: _SCRIPT_REPEAT_SCHEMA, diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 8327eb2e320..0c32ae5eddf 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -1184,6 +1184,45 @@ def test_key_value_schemas(): schema({"mode": mode, "data": data}) +def test_key_value_schemas_with_default(): + """Test key value schemas.""" + schema = vol.Schema( + cv.key_value_schemas( + "mode", + { + "number": vol.Schema({"mode": "number", "data": int}), + "string": vol.Schema({"mode": "string", "data": str}), + }, + vol.Schema({"mode": cv.dynamic_template}), + "a cool template", + ) + ) + + with pytest.raises(vol.Invalid) as excinfo: + schema(True) + assert str(excinfo.value) == "Expected a dictionary" + + for mode in None, {"a": "dict"}, "invalid": + with pytest.raises(vol.Invalid) as excinfo: + schema({"mode": mode}) + assert ( + str(excinfo.value) + == f"Unexpected value for mode: '{mode}'. Expected number, string, a cool template" + ) + + with pytest.raises(vol.Invalid) as excinfo: + schema({"mode": "number", "data": "string-value"}) + assert str(excinfo.value) == "expected int for dictionary value @ data['data']" + + with pytest.raises(vol.Invalid) as excinfo: + schema({"mode": "string", "data": 1}) + assert str(excinfo.value) == "expected str for dictionary value @ data['data']" + + for mode, data in (("number", 1), ("string", "hello")): + schema({"mode": mode, "data": data}) + schema({"mode": "{{ 1 + 1}}"}) + + def test_script(caplog): """Test script validation is user friendly.""" for data, msg in ( diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index b0a93c85b2b..2ee4213a688 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -1501,6 +1501,61 @@ async def test_condition_basic(hass, caplog): ) +async def test_shorthand_template_condition(hass, caplog): + """Test if we can use shorthand template conditions in a script.""" + event = "test_event" + events = async_capture_events(hass, event) + alias = "condition step" + sequence = cv.SCRIPT_SCHEMA( + [ + {"event": event}, + { + "alias": alias, + "condition": "{{ states.test.entity.state == 'hello' }}", + }, + {"event": event}, + ] + ) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + + hass.states.async_set("test.entity", "hello") + await script_obj.async_run(context=Context()) + await hass.async_block_till_done() + + assert f"Test condition {alias}: True" in caplog.text + caplog.clear() + assert len(events) == 2 + + assert_action_trace( + { + "0": [{"result": {"event": "test_event", "event_data": {}}}], + "1": [{"result": {"entities": ["test.entity"], "result": True}}], + "2": [{"result": {"event": "test_event", "event_data": {}}}], + } + ) + + hass.states.async_set("test.entity", "goodbye") + + await script_obj.async_run(context=Context()) + await hass.async_block_till_done() + + assert f"Test condition {alias}: False" in caplog.text + assert len(events) == 3 + + assert_action_trace( + { + "0": [{"result": {"event": "test_event", "event_data": {}}}], + "1": [ + { + "error_type": script._StopScript, + "result": {"entities": ["test.entity"], "result": False}, + } + ], + }, + expected_script_execution="aborted", + ) + + async def test_condition_validation(hass, caplog): """Test if we can use conditions which validate late in a script.""" registry = er.async_get(hass) From eb5a321a9f71f10e7cbffa5e784c9c1e4b2c2d96 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 21 Dec 2021 12:46:10 +0100 Subject: [PATCH 0881/2644] Mark removed config schemas as removed (#61014) --- homeassistant/components/abode/__init__.py | 2 +- homeassistant/components/airvisual/__init__.py | 2 +- homeassistant/components/ambient_station/__init__.py | 2 +- homeassistant/components/arcam_fmj/__init__.py | 2 +- homeassistant/components/cast/__init__.py | 3 +-- homeassistant/components/cloudflare/__init__.py | 2 +- homeassistant/components/daikin/__init__.py | 2 +- homeassistant/components/directv/__init__.py | 2 +- homeassistant/components/doorbird/__init__.py | 2 +- homeassistant/components/flunearyou/__init__.py | 2 +- homeassistant/components/hunterdouglas_powerview/__init__.py | 2 +- homeassistant/components/local_ip/__init__.py | 2 +- homeassistant/components/nexia/__init__.py | 2 +- homeassistant/components/notion/__init__.py | 2 +- homeassistant/components/nuheat/__init__.py | 2 +- homeassistant/components/powerwall/__init__.py | 2 +- homeassistant/components/rachio/__init__.py | 2 +- homeassistant/components/rainmachine/__init__.py | 2 +- homeassistant/components/roku/__init__.py | 2 +- homeassistant/components/sentry/__init__.py | 2 +- homeassistant/components/simplisafe/__init__.py | 2 +- homeassistant/components/solaredge/__init__.py | 2 +- homeassistant/components/somfy_mylink/__init__.py | 2 +- homeassistant/components/synology_dsm/__init__.py | 2 +- homeassistant/components/tado/__init__.py | 2 +- homeassistant/components/tibber/__init__.py | 2 +- homeassistant/components/totalconnect/__init__.py | 2 +- homeassistant/components/vesync/__init__.py | 2 +- 28 files changed, 28 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index 0d6ab95a318..93b760f5f68 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -42,7 +42,7 @@ ATTR_APP_TYPE = "app_type" ATTR_EVENT_BY = "event_by" ATTR_VALUE = "value" -CONFIG_SCHEMA = cv.deprecated(DOMAIN) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) CHANGE_SETTING_SCHEMA = vol.Schema( {vol.Required(ATTR_SETTING): cv.string, vol.Required(ATTR_VALUE): cv.string} diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index 876cb270e99..f65bb7b14c7 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -57,7 +57,7 @@ PLATFORMS = [Platform.SENSOR] DEFAULT_ATTRIBUTION = "Data provided by AirVisual" DEFAULT_NODE_PRO_UPDATE_INTERVAL = timedelta(minutes=1) -CONFIG_SCHEMA = cv.deprecated(DOMAIN) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) @callback diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index 6406b5a10df..fb4a865a72e 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -41,7 +41,7 @@ DATA_CONFIG = "config" DEFAULT_SOCKET_MIN_RETRY = 15 -CONFIG_SCHEMA = cv.deprecated(DOMAIN) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) @callback diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py index 125dd7af6ae..3cb4c83211f 100644 --- a/homeassistant/components/arcam_fmj/__init__.py +++ b/homeassistant/components/arcam_fmj/__init__.py @@ -25,7 +25,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = cv.deprecated(DOMAIN) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) PLATFORMS = [Platform.MEDIA_PLAYER] diff --git a/homeassistant/components/cast/__init__.py b/homeassistant/components/cast/__init__.py index 4401553869f..b0dffcb2e9a 100644 --- a/homeassistant/components/cast/__init__.py +++ b/homeassistant/components/cast/__init__.py @@ -12,8 +12,7 @@ from . import home_assistant_cast from .const import DOMAIN from .media_player import ENTITY_SCHEMA -# Deprecated from 2021.4, remove in 2021.6 -CONFIG_SCHEMA = cv.deprecated(DOMAIN) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/cloudflare/__init__.py b/homeassistant/components/cloudflare/__init__.py index 2d0d3145ead..dc0782aa9c0 100644 --- a/homeassistant/components/cloudflare/__init__.py +++ b/homeassistant/components/cloudflare/__init__.py @@ -23,7 +23,7 @@ from .const import CONF_RECORDS, DEFAULT_UPDATE_INTERVAL, DOMAIN, SERVICE_UPDATE _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = cv.deprecated(DOMAIN) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index f588f66761c..95189774a81 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -25,7 +25,7 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) PLATFORMS = [Platform.CLIMATE, Platform.SENSOR, Platform.SWITCH] -CONFIG_SCHEMA = cv.deprecated(DOMAIN) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/directv/__init__.py b/homeassistant/components/directv/__init__.py index 55961869f79..1068ec4ccc4 100644 --- a/homeassistant/components/directv/__init__.py +++ b/homeassistant/components/directv/__init__.py @@ -14,7 +14,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN -CONFIG_SCHEMA = cv.deprecated(DOMAIN) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) PLATFORMS = [Platform.MEDIA_PLAYER, Platform.REMOTE] SCAN_INTERVAL = timedelta(seconds=30) diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index 7497304f9e1..70198c2f2d5 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -55,7 +55,7 @@ DEVICE_SCHEMA = vol.Schema( } ) -CONFIG_SCHEMA = cv.deprecated(DOMAIN) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: diff --git a/homeassistant/components/flunearyou/__init__.py b/homeassistant/components/flunearyou/__init__.py index 1c98f4dceae..bb07e9ccc73 100644 --- a/homeassistant/components/flunearyou/__init__.py +++ b/homeassistant/components/flunearyou/__init__.py @@ -19,7 +19,7 @@ from .const import CATEGORY_CDC_REPORT, CATEGORY_USER_REPORT, DOMAIN, LOGGER DEFAULT_UPDATE_INTERVAL = timedelta(minutes=30) -CONFIG_SCHEMA = cv.deprecated(DOMAIN) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) PLATFORMS = [Platform.SENSOR] diff --git a/homeassistant/components/hunterdouglas_powerview/__init__.py b/homeassistant/components/hunterdouglas_powerview/__init__.py index 304d7e13cc1..17dd580f5cc 100644 --- a/homeassistant/components/hunterdouglas_powerview/__init__.py +++ b/homeassistant/components/hunterdouglas_powerview/__init__.py @@ -53,7 +53,7 @@ from .const import ( PARALLEL_UPDATES = 1 -CONFIG_SCHEMA = cv.deprecated(DOMAIN) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) PLATFORMS = [Platform.COVER, Platform.SCENE, Platform.SENSOR] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/local_ip/__init__.py b/homeassistant/components/local_ip/__init__.py index e97e5da7d49..cc122187f47 100644 --- a/homeassistant/components/local_ip/__init__.py +++ b/homeassistant/components/local_ip/__init__.py @@ -5,7 +5,7 @@ import homeassistant.helpers.config_validation as cv from .const import DOMAIN, PLATFORMS -CONFIG_SCHEMA = cv.deprecated(DOMAIN) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/nexia/__init__.py b/homeassistant/components/nexia/__init__.py index 827bff50d34..634f69b49d3 100644 --- a/homeassistant/components/nexia/__init__.py +++ b/homeassistant/components/nexia/__init__.py @@ -18,7 +18,7 @@ from .util import is_invalid_auth_code _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = cv.deprecated(DOMAIN) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index 0e841e12bbe..e8bd2c725d5 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -34,7 +34,7 @@ ATTR_SYSTEM_NAME = "system_name" DEFAULT_ATTRIBUTION = "Data provided by Notion" DEFAULT_SCAN_INTERVAL = timedelta(minutes=1) -CONFIG_SCHEMA = cv.deprecated(DOMAIN) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/nuheat/__init__.py b/homeassistant/components/nuheat/__init__.py index 60dc0a5d9e2..06796e836b4 100644 --- a/homeassistant/components/nuheat/__init__.py +++ b/homeassistant/components/nuheat/__init__.py @@ -17,7 +17,7 @@ from .const import CONF_SERIAL_NUMBER, DOMAIN, PLATFORMS _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = cv.deprecated(DOMAIN) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) def _get_thermostat(api, serial_number): diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index fa7b33ff2ad..c86fa71f987 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -36,7 +36,7 @@ from .const import ( UPDATE_INTERVAL, ) -CONFIG_SCHEMA = cv.deprecated(DOMAIN) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] diff --git a/homeassistant/components/rachio/__init__.py b/homeassistant/components/rachio/__init__.py index 134a10795f2..431e3bffb64 100644 --- a/homeassistant/components/rachio/__init__.py +++ b/homeassistant/components/rachio/__init__.py @@ -22,7 +22,7 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.SWITCH, Platform.BINARY_SENSOR] -CONFIG_SCHEMA = cv.deprecated(DOMAIN) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 5ffdc7951db..1123d3da281 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -55,7 +55,7 @@ DEFAULT_ICON = "mdi:water" DEFAULT_SSL = True DEFAULT_UPDATE_INTERVAL = timedelta(seconds=15) -CONFIG_SCHEMA = cv.deprecated(DOMAIN) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH] diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index b145864b726..2c59c998b65 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -13,7 +13,7 @@ from homeassistant.helpers import config_validation as cv from .const import DOMAIN from .coordinator import RokuDataUpdateCoordinator -CONFIG_SCHEMA = cv.deprecated(DOMAIN) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) PLATFORMS = [Platform.MEDIA_PLAYER, Platform.REMOTE] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sentry/__init__.py b/homeassistant/components/sentry/__init__.py index 350ccfa5b8d..c23b357066b 100644 --- a/homeassistant/components/sentry/__init__.py +++ b/homeassistant/components/sentry/__init__.py @@ -36,7 +36,7 @@ from .const import ( ENTITY_COMPONENTS, ) -CONFIG_SCHEMA = cv.deprecated(DOMAIN) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) LOGGER_INFO_REGEX = re.compile(r"^(\w+)\.?(\w+)?\.?(\w+)?\.?(\w+)?(?:\..*)?$") diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index b188b7309d8..a8cdb853749 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -243,7 +243,7 @@ WEBSOCKET_EVENTS_TO_FIRE_HASS_EVENT = [ EVENT_USER_INITIATED_TEST, ] -CONFIG_SCHEMA = cv.deprecated(DOMAIN) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) @callback diff --git a/homeassistant/components/solaredge/__init__.py b/homeassistant/components/solaredge/__init__.py index 8b83891f4e3..148e684589f 100644 --- a/homeassistant/components/solaredge/__init__.py +++ b/homeassistant/components/solaredge/__init__.py @@ -12,7 +12,7 @@ import homeassistant.helpers.config_validation as cv from .const import CONF_SITE_ID, DATA_API_CLIENT, DOMAIN, LOGGER -CONFIG_SCHEMA = cv.deprecated(DOMAIN) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) PLATFORMS = [Platform.SENSOR] diff --git a/homeassistant/components/somfy_mylink/__init__.py b/homeassistant/components/somfy_mylink/__init__.py index bf47617a302..fd036daf385 100644 --- a/homeassistant/components/somfy_mylink/__init__.py +++ b/homeassistant/components/somfy_mylink/__init__.py @@ -16,7 +16,7 @@ UNDO_UPDATE_LISTENER = "undo_update_listener" _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = cv.deprecated(DOMAIN) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index e1a9076c947..6e90233b375 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -75,7 +75,7 @@ from .const import ( SynologyDSMEntityDescription, ) -CONFIG_SCHEMA = cv.deprecated(DOMAIN) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) ATTRIBUTION = "Data provided by Synology" diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index 6858f49799a..f89ad5feedc 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -41,7 +41,7 @@ PLATFORMS = [ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=4) SCAN_INTERVAL = timedelta(minutes=5) -CONFIG_SCHEMA = cv.deprecated(DOMAIN) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py index 3b71a40b093..57d95855cca 100644 --- a/homeassistant/components/tibber/__init__.py +++ b/homeassistant/components/tibber/__init__.py @@ -21,7 +21,7 @@ from .const import DATA_HASS_CONFIG, DOMAIN PLATFORMS = [Platform.SENSOR] -CONFIG_SCHEMA = cv.deprecated(DOMAIN) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/totalconnect/__init__.py b/homeassistant/components/totalconnect/__init__.py index 679f7afc3d3..05566708f22 100644 --- a/homeassistant/components/totalconnect/__init__.py +++ b/homeassistant/components/totalconnect/__init__.py @@ -17,7 +17,7 @@ from .const import CONF_USERCODES, DOMAIN PLATFORMS = [Platform.ALARM_CONTROL_PANEL, Platform.BINARY_SENSOR] -CONFIG_SCHEMA = cv.deprecated(DOMAIN) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) SCAN_INTERVAL = timedelta(seconds=30) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/vesync/__init__.py b/homeassistant/components/vesync/__init__.py index 95370b01409..e62436fce27 100644 --- a/homeassistant/components/vesync/__init__.py +++ b/homeassistant/components/vesync/__init__.py @@ -23,7 +23,7 @@ PLATFORMS = ["switch", "fan", "light"] _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = cv.deprecated(DOMAIN) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_setup_entry(hass, config_entry): From e62148b8ff62e4d7d8c7b1ee2f3c57f9fba586f2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 21 Dec 2021 12:53:19 +0100 Subject: [PATCH 0882/2644] Remove deprecated YAML configuration from Stookalert (#61007) --- .../components/stookalert/binary_sensor.py | 44 ++----------------- .../components/stookalert/config_flow.py | 4 -- .../components/stookalert/test_config_flow.py | 15 +------ 3 files changed, 4 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/stookalert/binary_sensor.py b/homeassistant/components/stookalert/binary_sensor.py index b606e3f3d3b..974635e2efd 100644 --- a/homeassistant/components/stookalert/binary_sensor.py +++ b/homeassistant/components/stookalert/binary_sensor.py @@ -4,59 +4,21 @@ from __future__ import annotations from datetime import timedelta import stookalert -import voluptuous as vol from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_NAME +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import CONF_PROVINCE, DOMAIN, LOGGER, PROVINCES +from .const import CONF_PROVINCE, DOMAIN -DEFAULT_NAME = "Stookalert" -ATTRIBUTION = "Data provided by rivm.nl" SCAN_INTERVAL = timedelta(minutes=60) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_PROVINCE): vol.In(PROVINCES), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Import the Stookalert platform into a config entry.""" - LOGGER.warning( - "Configuration of the Stookalert platform in YAML is deprecated and will be " - "removed in Home Assistant 2022.1; Your existing configuration " - "has been imported into the UI automatically and can be safely removed " - "from your configuration.yaml file" - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_PROVINCE: config[CONF_PROVINCE], - }, - ) - ) - async def async_setup_entry( hass: HomeAssistant, @@ -71,7 +33,7 @@ async def async_setup_entry( class StookalertBinarySensor(BinarySensorEntity): """Defines a Stookalert binary sensor.""" - _attr_attribution = ATTRIBUTION + _attr_attribution = "Data provided by rivm.nl" _attr_device_class = BinarySensorDeviceClass.SAFETY def __init__(self, client: stookalert.stookalert, entry: ConfigEntry) -> None: diff --git a/homeassistant/components/stookalert/config_flow.py b/homeassistant/components/stookalert/config_flow.py index 4f625ec2d1a..02189bc161f 100644 --- a/homeassistant/components/stookalert/config_flow.py +++ b/homeassistant/components/stookalert/config_flow.py @@ -31,7 +31,3 @@ class StookalertFlowHandler(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=vol.Schema({vol.Required(CONF_PROVINCE): vol.In(PROVINCES)}), ) - - async def async_step_import(self, config: dict[str, Any]) -> FlowResult: - """Handle a flow initialized by importing a config.""" - return await self.async_step_user(config) diff --git a/tests/components/stookalert/test_config_flow.py b/tests/components/stookalert/test_config_flow.py index ceee26fa8e2..6b5dd6fd4ce 100644 --- a/tests/components/stookalert/test_config_flow.py +++ b/tests/components/stookalert/test_config_flow.py @@ -2,7 +2,7 @@ from unittest.mock import patch from homeassistant.components.stookalert.const import CONF_PROVINCE, DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, @@ -63,16 +63,3 @@ async def test_already_configured(hass: HomeAssistant) -> None: assert result2.get("type") == RESULT_TYPE_ABORT assert result2.get("reason") == "already_configured" - - -async def test_import_flow(hass: HomeAssistant) -> None: - """Test the import configuration flow.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data={"province": "Overijssel"} - ) - - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY - assert result.get("title") == "Overijssel" - assert result.get("data") == { - CONF_PROVINCE: "Overijssel", - } From 6cdd34146582c6c69864ff71d5f9de4df57b3673 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 21 Dec 2021 13:03:29 +0100 Subject: [PATCH 0883/2644] Deprecate mcp23017 integration (ADR-0019) (#62484) --- homeassistant/components/mcp23017/binary_sensor.py | 10 ++++++++++ homeassistant/components/mcp23017/switch.py | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/homeassistant/components/mcp23017/binary_sensor.py b/homeassistant/components/mcp23017/binary_sensor.py index c650393a26f..592bc4aa5e3 100644 --- a/homeassistant/components/mcp23017/binary_sensor.py +++ b/homeassistant/components/mcp23017/binary_sensor.py @@ -1,4 +1,6 @@ """Support for binary sensor using I2C MCP23017 chip.""" +import logging + from adafruit_mcp230xx.mcp23017 import MCP23017 import board import busio @@ -34,9 +36,17 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) +_LOGGER = logging.getLogger(__name__) + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the MCP23017 binary sensors.""" + _LOGGER.warning( + "The MCP23017 I/O Expander integration is deprecated and will be removed " + "in Home Assistant Core 2022.4; this integration is removed under " + "Architectural Decision Record 0019, more information can be found here: " + "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" + ) pull_mode = config[CONF_PULL_MODE] invert_logic = config[CONF_INVERT_LOGIC] i2c_address = config[CONF_I2C_ADDRESS] diff --git a/homeassistant/components/mcp23017/switch.py b/homeassistant/components/mcp23017/switch.py index 6b1ced540ae..5c4c06a585a 100644 --- a/homeassistant/components/mcp23017/switch.py +++ b/homeassistant/components/mcp23017/switch.py @@ -1,4 +1,6 @@ """Support for switch sensor using I2C MCP23017 chip.""" +import logging + from adafruit_mcp230xx.mcp23017 import MCP23017 import board import busio @@ -28,9 +30,17 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) +_LOGGER = logging.getLogger(__name__) + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the MCP23017 devices.""" + _LOGGER.warning( + "The MCP23017 I/O Expander integration is deprecated and will be removed " + "in Home Assistant Core 2022.4; this integration is removed under " + "Architectural Decision Record 0019, more information can be found here: " + "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" + ) invert_logic = config.get(CONF_INVERT_LOGIC) i2c_address = config.get(CONF_I2C_ADDRESS) From 69ba04be3c83bbf3a7ba9bfbe21b3655a2d49f65 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 21 Dec 2021 13:24:08 +0100 Subject: [PATCH 0884/2644] Deprecate pcal9535a integration (ADR-0019) (#62487) --- homeassistant/components/pcal9535a/binary_sensor.py | 11 +++++++++++ homeassistant/components/pcal9535a/switch.py | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/homeassistant/components/pcal9535a/binary_sensor.py b/homeassistant/components/pcal9535a/binary_sensor.py index 79ad8eb8016..e22d3138574 100644 --- a/homeassistant/components/pcal9535a/binary_sensor.py +++ b/homeassistant/components/pcal9535a/binary_sensor.py @@ -1,4 +1,6 @@ """Support for binary sensor using I2C PCAL9535A chip.""" +import logging + from pcal9535a import PCAL9535A import voluptuous as vol @@ -35,9 +37,18 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) +_LOGGER = logging.getLogger(__name__) + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the PCAL9535A binary sensors.""" + _LOGGER.warning( + "The PCAL9535A I/O Expander integration is deprecated and will be removed " + "in Home Assistant Core 2022.4; this integration is removed under " + "Architectural Decision Record 0019, more information can be found here: " + "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" + ) + pull_mode = config[CONF_PULL_MODE] invert_logic = config[CONF_INVERT_LOGIC] i2c_address = config[CONF_I2C_ADDRESS] diff --git a/homeassistant/components/pcal9535a/switch.py b/homeassistant/components/pcal9535a/switch.py index 897d41e9d98..028beaae4ab 100644 --- a/homeassistant/components/pcal9535a/switch.py +++ b/homeassistant/components/pcal9535a/switch.py @@ -1,4 +1,6 @@ """Support for switch sensor using I2C PCAL9535A chip.""" +import logging + from pcal9535a import PCAL9535A import voluptuous as vol @@ -36,9 +38,18 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) +_LOGGER = logging.getLogger(__name__) + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the PCAL9535A devices.""" + _LOGGER.warning( + "The PCAL9535A I/O Expander integration is deprecated and will be removed " + "in Home Assistant Core 2022.4; this integration is removed under " + "Architectural Decision Record 0019, more information can be found here: " + "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" + ) + invert_logic = config[CONF_INVERT_LOGIC] i2c_address = config[CONF_I2C_ADDRESS] bus = config[CONF_I2C_BUS] From 684c380ce20e6c7ef6023c36bb390b8a97a54603 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 21 Dec 2021 14:07:01 +0100 Subject: [PATCH 0885/2644] Use SensorDeviceClass enum in sensor device automations (#62480) --- .../components/sensor/device_condition.py | 80 ++++++------------- .../components/sensor/device_trigger.py | 73 ++++++----------- .../sensor/test_device_condition.py | 10 +-- .../components/sensor/test_device_trigger.py | 6 +- 4 files changed, 59 insertions(+), 110 deletions(-) diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index 612ebe0abd5..32fa06eb507 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -6,36 +6,8 @@ import voluptuous as vol from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) -from homeassistant.const import ( - CONF_ABOVE, - CONF_BELOW, - CONF_ENTITY_ID, - CONF_TYPE, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_CO, - DEVICE_CLASS_CO2, - DEVICE_CLASS_CURRENT, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_FREQUENCY, - DEVICE_CLASS_GAS, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_NITROGEN_DIOXIDE, - DEVICE_CLASS_NITROGEN_MONOXIDE, - DEVICE_CLASS_NITROUS_OXIDE, - DEVICE_CLASS_OZONE, - DEVICE_CLASS_PM1, - DEVICE_CLASS_PM10, - DEVICE_CLASS_PM25, - DEVICE_CLASS_POWER, - DEVICE_CLASS_POWER_FACTOR, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_SIGNAL_STRENGTH, - DEVICE_CLASS_SULPHUR_DIOXIDE, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, - DEVICE_CLASS_VOLTAGE, -) +from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.const import CONF_ABOVE, CONF_BELOW, CONF_ENTITY_ID, CONF_TYPE from homeassistant.core import HomeAssistant, HomeAssistantError, callback from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers.entity import get_device_class, get_unit_of_measurement @@ -78,32 +50,32 @@ CONF_IS_VOLTAGE = "is_voltage" CONF_IS_VALUE = "is_value" ENTITY_CONDITIONS = { - DEVICE_CLASS_BATTERY: [{CONF_TYPE: CONF_IS_BATTERY_LEVEL}], - DEVICE_CLASS_CO: [{CONF_TYPE: CONF_IS_CO}], - DEVICE_CLASS_CO2: [{CONF_TYPE: CONF_IS_CO2}], - DEVICE_CLASS_CURRENT: [{CONF_TYPE: CONF_IS_CURRENT}], - DEVICE_CLASS_ENERGY: [{CONF_TYPE: CONF_IS_ENERGY}], - DEVICE_CLASS_FREQUENCY: [{CONF_TYPE: CONF_IS_FREQUENCY}], - DEVICE_CLASS_GAS: [{CONF_TYPE: CONF_IS_GAS}], - DEVICE_CLASS_HUMIDITY: [{CONF_TYPE: CONF_IS_HUMIDITY}], - DEVICE_CLASS_ILLUMINANCE: [{CONF_TYPE: CONF_IS_ILLUMINANCE}], - DEVICE_CLASS_NITROGEN_DIOXIDE: [{CONF_TYPE: CONF_IS_NITROGEN_DIOXIDE}], - DEVICE_CLASS_NITROGEN_MONOXIDE: [{CONF_TYPE: CONF_IS_NITROGEN_MONOXIDE}], - DEVICE_CLASS_NITROUS_OXIDE: [{CONF_TYPE: CONF_IS_NITROUS_OXIDE}], - DEVICE_CLASS_OZONE: [{CONF_TYPE: CONF_IS_OZONE}], - DEVICE_CLASS_POWER: [{CONF_TYPE: CONF_IS_POWER}], - DEVICE_CLASS_POWER_FACTOR: [{CONF_TYPE: CONF_IS_POWER_FACTOR}], - DEVICE_CLASS_PM1: [{CONF_TYPE: CONF_IS_PM1}], - DEVICE_CLASS_PM10: [{CONF_TYPE: CONF_IS_PM10}], - DEVICE_CLASS_PM25: [{CONF_TYPE: CONF_IS_PM25}], - DEVICE_CLASS_PRESSURE: [{CONF_TYPE: CONF_IS_PRESSURE}], - DEVICE_CLASS_SIGNAL_STRENGTH: [{CONF_TYPE: CONF_IS_SIGNAL_STRENGTH}], - DEVICE_CLASS_SULPHUR_DIOXIDE: [{CONF_TYPE: CONF_IS_SULPHUR_DIOXIDE}], - DEVICE_CLASS_TEMPERATURE: [{CONF_TYPE: CONF_IS_TEMPERATURE}], - DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS: [ + SensorDeviceClass.BATTERY: [{CONF_TYPE: CONF_IS_BATTERY_LEVEL}], + SensorDeviceClass.CO: [{CONF_TYPE: CONF_IS_CO}], + SensorDeviceClass.CO2: [{CONF_TYPE: CONF_IS_CO2}], + SensorDeviceClass.CURRENT: [{CONF_TYPE: CONF_IS_CURRENT}], + SensorDeviceClass.ENERGY: [{CONF_TYPE: CONF_IS_ENERGY}], + SensorDeviceClass.FREQUENCY: [{CONF_TYPE: CONF_IS_FREQUENCY}], + SensorDeviceClass.GAS: [{CONF_TYPE: CONF_IS_GAS}], + SensorDeviceClass.HUMIDITY: [{CONF_TYPE: CONF_IS_HUMIDITY}], + SensorDeviceClass.ILLUMINANCE: [{CONF_TYPE: CONF_IS_ILLUMINANCE}], + SensorDeviceClass.NITROGEN_DIOXIDE: [{CONF_TYPE: CONF_IS_NITROGEN_DIOXIDE}], + SensorDeviceClass.NITROGEN_MONOXIDE: [{CONF_TYPE: CONF_IS_NITROGEN_MONOXIDE}], + SensorDeviceClass.NITROUS_OXIDE: [{CONF_TYPE: CONF_IS_NITROUS_OXIDE}], + SensorDeviceClass.OZONE: [{CONF_TYPE: CONF_IS_OZONE}], + SensorDeviceClass.POWER: [{CONF_TYPE: CONF_IS_POWER}], + SensorDeviceClass.POWER_FACTOR: [{CONF_TYPE: CONF_IS_POWER_FACTOR}], + SensorDeviceClass.PM1: [{CONF_TYPE: CONF_IS_PM1}], + SensorDeviceClass.PM10: [{CONF_TYPE: CONF_IS_PM10}], + SensorDeviceClass.PM25: [{CONF_TYPE: CONF_IS_PM25}], + SensorDeviceClass.PRESSURE: [{CONF_TYPE: CONF_IS_PRESSURE}], + SensorDeviceClass.SIGNAL_STRENGTH: [{CONF_TYPE: CONF_IS_SIGNAL_STRENGTH}], + SensorDeviceClass.SULPHUR_DIOXIDE: [{CONF_TYPE: CONF_IS_SULPHUR_DIOXIDE}], + SensorDeviceClass.TEMPERATURE: [{CONF_TYPE: CONF_IS_TEMPERATURE}], + SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: [ {CONF_TYPE: CONF_IS_VOLATILE_ORGANIC_COMPOUNDS} ], - DEVICE_CLASS_VOLTAGE: [{CONF_TYPE: CONF_IS_VOLTAGE}], + SensorDeviceClass.VOLTAGE: [{CONF_TYPE: CONF_IS_VOLTAGE}], DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_IS_VALUE}], } diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index a9d014e6856..e605ed3b797 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -8,36 +8,13 @@ from homeassistant.components.device_automation.exceptions import ( from homeassistant.components.homeassistant.triggers import ( numeric_state as numeric_state_trigger, ) +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( CONF_ABOVE, CONF_BELOW, CONF_ENTITY_ID, CONF_FOR, CONF_TYPE, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_CO, - DEVICE_CLASS_CO2, - DEVICE_CLASS_CURRENT, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_FREQUENCY, - DEVICE_CLASS_GAS, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_NITROGEN_DIOXIDE, - DEVICE_CLASS_NITROGEN_MONOXIDE, - DEVICE_CLASS_NITROUS_OXIDE, - DEVICE_CLASS_OZONE, - DEVICE_CLASS_PM1, - DEVICE_CLASS_PM10, - DEVICE_CLASS_PM25, - DEVICE_CLASS_POWER, - DEVICE_CLASS_POWER_FACTOR, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_SIGNAL_STRENGTH, - DEVICE_CLASS_SULPHUR_DIOXIDE, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, - DEVICE_CLASS_VOLTAGE, ) from homeassistant.core import HomeAssistantError from homeassistant.helpers import config_validation as cv @@ -77,32 +54,32 @@ CONF_VOLTAGE = "voltage" CONF_VALUE = "value" ENTITY_TRIGGERS = { - DEVICE_CLASS_BATTERY: [{CONF_TYPE: CONF_BATTERY_LEVEL}], - DEVICE_CLASS_CO: [{CONF_TYPE: CONF_CO}], - DEVICE_CLASS_CO2: [{CONF_TYPE: CONF_CO2}], - DEVICE_CLASS_CURRENT: [{CONF_TYPE: CONF_CURRENT}], - DEVICE_CLASS_ENERGY: [{CONF_TYPE: CONF_ENERGY}], - DEVICE_CLASS_FREQUENCY: [{CONF_TYPE: CONF_FREQUENCY}], - DEVICE_CLASS_GAS: [{CONF_TYPE: CONF_GAS}], - DEVICE_CLASS_HUMIDITY: [{CONF_TYPE: CONF_HUMIDITY}], - DEVICE_CLASS_ILLUMINANCE: [{CONF_TYPE: CONF_ILLUMINANCE}], - DEVICE_CLASS_NITROGEN_DIOXIDE: [{CONF_TYPE: CONF_NITROGEN_DIOXIDE}], - DEVICE_CLASS_NITROGEN_MONOXIDE: [{CONF_TYPE: CONF_NITROGEN_MONOXIDE}], - DEVICE_CLASS_NITROUS_OXIDE: [{CONF_TYPE: CONF_NITROUS_OXIDE}], - DEVICE_CLASS_OZONE: [{CONF_TYPE: CONF_OZONE}], - DEVICE_CLASS_PM1: [{CONF_TYPE: CONF_PM1}], - DEVICE_CLASS_PM10: [{CONF_TYPE: CONF_PM10}], - DEVICE_CLASS_PM25: [{CONF_TYPE: CONF_PM25}], - DEVICE_CLASS_POWER: [{CONF_TYPE: CONF_POWER}], - DEVICE_CLASS_POWER_FACTOR: [{CONF_TYPE: CONF_POWER_FACTOR}], - DEVICE_CLASS_PRESSURE: [{CONF_TYPE: CONF_PRESSURE}], - DEVICE_CLASS_SIGNAL_STRENGTH: [{CONF_TYPE: CONF_SIGNAL_STRENGTH}], - DEVICE_CLASS_SULPHUR_DIOXIDE: [{CONF_TYPE: CONF_SULPHUR_DIOXIDE}], - DEVICE_CLASS_TEMPERATURE: [{CONF_TYPE: CONF_TEMPERATURE}], - DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS: [ + SensorDeviceClass.BATTERY: [{CONF_TYPE: CONF_BATTERY_LEVEL}], + SensorDeviceClass.CO: [{CONF_TYPE: CONF_CO}], + SensorDeviceClass.CO2: [{CONF_TYPE: CONF_CO2}], + SensorDeviceClass.CURRENT: [{CONF_TYPE: CONF_CURRENT}], + SensorDeviceClass.ENERGY: [{CONF_TYPE: CONF_ENERGY}], + SensorDeviceClass.FREQUENCY: [{CONF_TYPE: CONF_FREQUENCY}], + SensorDeviceClass.GAS: [{CONF_TYPE: CONF_GAS}], + SensorDeviceClass.HUMIDITY: [{CONF_TYPE: CONF_HUMIDITY}], + SensorDeviceClass.ILLUMINANCE: [{CONF_TYPE: CONF_ILLUMINANCE}], + SensorDeviceClass.NITROGEN_DIOXIDE: [{CONF_TYPE: CONF_NITROGEN_DIOXIDE}], + SensorDeviceClass.NITROGEN_MONOXIDE: [{CONF_TYPE: CONF_NITROGEN_MONOXIDE}], + SensorDeviceClass.NITROUS_OXIDE: [{CONF_TYPE: CONF_NITROUS_OXIDE}], + SensorDeviceClass.OZONE: [{CONF_TYPE: CONF_OZONE}], + SensorDeviceClass.PM1: [{CONF_TYPE: CONF_PM1}], + SensorDeviceClass.PM10: [{CONF_TYPE: CONF_PM10}], + SensorDeviceClass.PM25: [{CONF_TYPE: CONF_PM25}], + SensorDeviceClass.POWER: [{CONF_TYPE: CONF_POWER}], + SensorDeviceClass.POWER_FACTOR: [{CONF_TYPE: CONF_POWER_FACTOR}], + SensorDeviceClass.PRESSURE: [{CONF_TYPE: CONF_PRESSURE}], + SensorDeviceClass.SIGNAL_STRENGTH: [{CONF_TYPE: CONF_SIGNAL_STRENGTH}], + SensorDeviceClass.SULPHUR_DIOXIDE: [{CONF_TYPE: CONF_SULPHUR_DIOXIDE}], + SensorDeviceClass.TEMPERATURE: [{CONF_TYPE: CONF_TEMPERATURE}], + SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: [ {CONF_TYPE: CONF_VOLATILE_ORGANIC_COMPOUNDS} ], - DEVICE_CLASS_VOLTAGE: [{CONF_TYPE: CONF_VOLTAGE}], + SensorDeviceClass.VOLTAGE: [{CONF_TYPE: CONF_VOLTAGE}], DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_VALUE}], } diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py index ca3b046cbbd..ebbeb5ee616 100644 --- a/tests/components/sensor/test_device_condition.py +++ b/tests/components/sensor/test_device_condition.py @@ -3,7 +3,7 @@ import pytest import homeassistant.components.automation as automation from homeassistant.components.device_automation import DeviceAutomationType -from homeassistant.components.sensor import DEVICE_CLASSES, DOMAIN, SensorDeviceClass +from homeassistant.components.sensor import DOMAIN, SensorDeviceClass from homeassistant.components.sensor.device_condition import ENTITY_CONDITIONS from homeassistant.const import CONF_PLATFORM, PERCENTAGE, STATE_UNKNOWN from homeassistant.helpers import device_registry @@ -50,7 +50,7 @@ async def test_get_conditions(hass, device_reg, entity_reg, enable_custom_integr config_entry_id=config_entry.entry_id, connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - for device_class in DEVICE_CLASSES: + for device_class in SensorDeviceClass: entity_reg.async_get_or_create( DOMAIN, "test", @@ -69,7 +69,7 @@ async def test_get_conditions(hass, device_reg, entity_reg, enable_custom_integr "device_id": device_entry.id, "entity_id": platform.ENTITIES[device_class].entity_id, } - for device_class in DEVICE_CLASSES + for device_class in SensorDeviceClass if device_class in UNITS_OF_MEASUREMENT for condition in ENTITY_CONDITIONS[device_class] if device_class != "none" @@ -89,7 +89,7 @@ async def test_get_conditions_no_state(hass, device_reg, entity_reg): connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) entity_ids = {} - for device_class in DEVICE_CLASSES: + for device_class in SensorDeviceClass: entity_ids[device_class] = entity_reg.async_get_or_create( DOMAIN, "test", @@ -109,7 +109,7 @@ async def test_get_conditions_no_state(hass, device_reg, entity_reg): "device_id": device_entry.id, "entity_id": entity_ids[device_class], } - for device_class in DEVICE_CLASSES + for device_class in SensorDeviceClass if device_class in UNITS_OF_MEASUREMENT for condition in ENTITY_CONDITIONS[device_class] if device_class != "none" diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index b6e987b5fda..b217016d339 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -5,7 +5,7 @@ import pytest import homeassistant.components.automation as automation from homeassistant.components.device_automation import DeviceAutomationType -from homeassistant.components.sensor import DEVICE_CLASSES, DOMAIN, SensorDeviceClass +from homeassistant.components.sensor import DOMAIN, SensorDeviceClass from homeassistant.components.sensor.device_trigger import ENTITY_TRIGGERS from homeassistant.const import CONF_PLATFORM, PERCENTAGE, STATE_UNKNOWN from homeassistant.helpers import device_registry @@ -54,7 +54,7 @@ async def test_get_triggers(hass, device_reg, entity_reg, enable_custom_integrat config_entry_id=config_entry.entry_id, connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - for device_class in DEVICE_CLASSES: + for device_class in SensorDeviceClass: entity_reg.async_get_or_create( DOMAIN, "test", @@ -73,7 +73,7 @@ async def test_get_triggers(hass, device_reg, entity_reg, enable_custom_integrat "device_id": device_entry.id, "entity_id": platform.ENTITIES[device_class].entity_id, } - for device_class in DEVICE_CLASSES + for device_class in SensorDeviceClass if device_class in UNITS_OF_MEASUREMENT for trigger in ENTITY_TRIGGERS[device_class] if device_class != "none" From cbcd6d458ee81b928322c5c578e5adaa27e7332b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 21 Dec 2021 14:10:05 +0100 Subject: [PATCH 0886/2644] Assert current state of script condition validation in tests (#62486) --- tests/helpers/test_config_validation.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 0c32ae5eddf..62ae79ec5cc 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -1229,6 +1229,16 @@ def test_script(caplog): ({"delay": "{{ invalid"}, "should be format 'HH:MM'"), ({"wait_template": "{{ invalid"}, "invalid template"), ({"condition": "invalid"}, "Unexpected value for condition: 'invalid'"), + ( + {"condition": "not", "conditions": {"condition": "invalid"}}, + "Unexpected value for condition: 'invalid'", + ), + # The validation error message could be improved to explain that this is not + # a valid shorthand template + ( + {"condition": "not", "conditions": "not a dynamic template"}, + "Expected a dictionary", + ), ({"event": None}, "string value is None for dictionary value @ data['event']"), ( {"device_id": None}, From cab2a74b5f9ae062648405900188071f5fcc65e4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 21 Dec 2021 14:27:35 +0100 Subject: [PATCH 0887/2644] Don't pollute config dir with deleted duplicated statistics (#62489) --- homeassistant/components/recorder/statistics.py | 5 ++++- tests/components/recorder/test_statistics.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 5310c8ed9f3..49b1f890a74 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -9,6 +9,7 @@ from datetime import datetime, timedelta from itertools import chain, groupby import json import logging +import os import re from statistics import mean from typing import TYPE_CHECKING, Any, Literal @@ -365,8 +366,10 @@ def delete_duplicates(instance: Recorder, session: scoped_session) -> None: if non_identical_duplicates: isotime = dt_util.utcnow().isoformat() - backup_file_name = f"deleted_statistics.{isotime}.json" + backup_file_name = f".deleted_statistics/deleted_statistics.{isotime}.json" backup_path = instance.hass.config.path(backup_file_name) + + os.makedirs(os.path.dirname(backup_path), exist_ok=True) with open(backup_path, "w", encoding="utf8") as backup_file: json.dump( non_identical_duplicates, diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index bd10d1e9612..77055b172ce 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -993,7 +993,7 @@ def test_delete_duplicates_non_identical(caplog, tmpdir): assert "Found duplicated" not in caplog.text isotime = dt_util.utcnow().isoformat() - backup_file_name = f"deleted_statistics.{isotime}.json" + backup_file_name = f".deleted_statistics/deleted_statistics.{isotime}.json" with open(hass.config.path(backup_file_name)) as backup_file: backup = json.load(backup_file) From 48b3d6e1c04210a78749508196c60d48dbde4272 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 21 Dec 2021 15:24:36 +0100 Subject: [PATCH 0888/2644] Save original + duplicate pairs when deleting duplicated statistics (#62498) --- .../components/recorder/statistics.py | 4 ++- tests/components/recorder/test_statistics.py | 34 +++++++++++++------ 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 49b1f890a74..54ef5f2be67 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -326,7 +326,9 @@ def _find_duplicates( duplicate_as_dict = columns_to_dict(duplicate) duplicate_ids.append(duplicate.id) if not compare_statistic_rows(original_as_dict, duplicate_as_dict): - non_identical_duplicates_as_dict.append(duplicate_as_dict) + non_identical_duplicates_as_dict.append( + {"duplicate": duplicate_as_dict, "original": original_as_dict} + ) return (duplicate_ids, non_identical_duplicates_as_dict) diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 77055b172ce..42581cf8adf 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -1000,16 +1000,30 @@ def test_delete_duplicates_non_identical(caplog, tmpdir): assert backup == [ { - "created": "2021-08-01T00:00:00", - "id": 4, - "last_reset": None, - "max": None, - "mean": None, - "metadata_id": 1, - "min": None, - "start": "2021-10-31T23:00:00", - "state": 3.0, - "sum": 5.0, + "duplicate": { + "created": "2021-08-01T00:00:00", + "id": 4, + "last_reset": None, + "max": None, + "mean": None, + "metadata_id": 1, + "min": None, + "start": "2021-10-31T23:00:00", + "state": 3.0, + "sum": 5.0, + }, + "original": { + "created": "2021-08-01T00:00:00", + "id": 5, + "last_reset": None, + "max": None, + "mean": None, + "metadata_id": 1, + "min": None, + "start": "2021-10-31T23:00:00", + "state": 3.0, + "sum": 6.0, + }, } ] From e8c972c55d18dd941e4839b3ef713ae0b78c4e6e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 21 Dec 2021 17:13:53 +0100 Subject: [PATCH 0889/2644] Deprecate Sensirion SHT31 integration (ADR-0019) (#62496) --- homeassistant/components/sht31/sensor.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/sht31/sensor.py b/homeassistant/components/sht31/sensor.py index ff8c5251ca7..f91779906d1 100644 --- a/homeassistant/components/sht31/sensor.py +++ b/homeassistant/components/sht31/sensor.py @@ -84,6 +84,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the sensor platform.""" + _LOGGER.warning( + "The Sensirion SHT31 integration is deprecated and will be removed " + "in Home Assistant Core 2022.4; this integration is removed under " + "Architectural Decision Record 0019, more information can be found here: " + "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" + ) + name = config[CONF_NAME] monitored_conditions = config[CONF_MONITORED_CONDITIONS] i2c_address = config[CONF_I2C_ADDRESS] From cdc3dcc1e6ef770a2bf61a1dd272f75796f97f19 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 21 Dec 2021 17:16:21 +0100 Subject: [PATCH 0890/2644] Deprecate DHT Sensor integration (ADR-0019) (#62495) --- homeassistant/components/dht/sensor.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/dht/sensor.py b/homeassistant/components/dht/sensor.py index 0b9248a44e8..27bf7baefc2 100644 --- a/homeassistant/components/dht/sensor.py +++ b/homeassistant/components/dht/sensor.py @@ -87,6 +87,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the DHT sensor.""" + _LOGGER.warning( + "The DHT Sensor integration is deprecated and will be removed " + "in Home Assistant Core 2022.4; this integration is removed under " + "Architectural Decision Record 0019, more information can be found here: " + "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" + ) + available_sensors = { "AM2302": adafruit_dht.DHT22, "DHT11": adafruit_dht.DHT11, From 6d1c4a4f5f97de88d4b5903495525692162a98ba Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 21 Dec 2021 17:17:58 +0100 Subject: [PATCH 0891/2644] Deprecate BH1750 integration (ADR-0019) (#62493) --- homeassistant/components/bh1750/sensor.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/bh1750/sensor.py b/homeassistant/components/bh1750/sensor.py index 8eab85e5f1d..a25a645d47f 100644 --- a/homeassistant/components/bh1750/sensor.py +++ b/homeassistant/components/bh1750/sensor.py @@ -64,6 +64,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the BH1750 sensor.""" + _LOGGER.warning( + "The BH1750 integration is deprecated and will be removed " + "in Home Assistant Core 2022.4; this integration is removed under " + "Architectural Decision Record 0019, more information can be found here: " + "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" + ) name = config[CONF_NAME] bus_number = config[CONF_I2C_BUS] From e48f567176402dbd5deba066d7bd176491567838 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 21 Dec 2021 17:20:15 +0100 Subject: [PATCH 0892/2644] Wrap shorthand template conditions during schema validation (#62485) --- homeassistant/helpers/condition.py | 19 ++++--------------- homeassistant/helpers/config_validation.py | 13 ++++++++++++- homeassistant/helpers/script.py | 2 +- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index d8d98f05ccd..f52e1bb1595 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -153,19 +153,12 @@ def trace_condition_function(condition: ConditionCheckerType) -> ConditionChecke async def async_from_config( hass: HomeAssistant, - config: ConfigType | Template, + config: ConfigType, ) -> ConditionCheckerType: """Turn a condition configuration into a method. Should be run on the event loop. """ - if isinstance(config, Template): - # We got a condition template, wrap it in a configuration to pass along. - config = { - CONF_CONDITION: "template", - CONF_VALUE_TEMPLATE: config, - } - condition = config.get(CONF_CONDITION) for fmt in (ASYNC_FROM_CONFIG_FORMAT, FROM_CONFIG_FORMAT): factory = getattr(sys.modules[__name__], fmt.format(condition), None) @@ -935,12 +928,9 @@ def state_validate_config(hass: HomeAssistant, config: ConfigType) -> ConfigType async def async_validate_condition_config( - hass: HomeAssistant, config: ConfigType | Template -) -> ConfigType | Template: + hass: HomeAssistant, config: ConfigType +) -> ConfigType: """Validate config.""" - if isinstance(config, Template): - return config - condition = config[CONF_CONDITION] if condition in ("and", "not", "or"): conditions = [] @@ -951,7 +941,6 @@ async def async_validate_condition_config( if condition == "device": config = cv.DEVICE_CONDITION_SCHEMA(config) - assert not isinstance(config, Template) platform = await async_get_device_automation_platform( hass, config[CONF_DOMAIN], DeviceAutomationType.CONDITION ) @@ -969,7 +958,7 @@ async def async_validate_condition_config( async def async_validate_conditions_config( - hass: HomeAssistant, conditions: list[ConfigType | Template] + hass: HomeAssistant, conditions: list[ConfigType] ) -> list[ConfigType | Template]: """Validate config.""" return await asyncio.gather( diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 32d1c5ec276..08f232951ae 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1195,6 +1195,16 @@ DEVICE_CONDITION_BASE_SCHEMA = vol.Schema( DEVICE_CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) +dynamic_template_condition_action = vol.All( + # Wrap a shorthand template condition in a template condition + dynamic_template, + lambda config: { + CONF_VALUE_TEMPLATE: config, + CONF_CONDITION: "template", + }, +) + + CONDITION_SCHEMA: vol.Schema = vol.Schema( vol.Any( key_value_schemas( @@ -1213,12 +1223,13 @@ CONDITION_SCHEMA: vol.Schema = vol.Schema( "zone": ZONE_CONDITION_SCHEMA, }, ), - dynamic_template, + dynamic_template_condition_action, ) ) dynamic_template_condition_action = vol.All( + # Wrap a shorthand template condition action in a template condition vol.Schema( {**CONDITION_BASE_SCHEMA, vol.Required(CONF_CONDITION): dynamic_template} ), diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 3e4432a40eb..1c199525d4d 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -262,7 +262,7 @@ async def async_validate_action_config( config = platform.ACTION_SCHEMA(config) # type: ignore elif action_type == cv.SCRIPT_ACTION_CHECK_CONDITION: - config = await condition.async_validate_condition_config(hass, config) # type: ignore + config = await condition.async_validate_condition_config(hass, config) elif action_type == cv.SCRIPT_ACTION_WAIT_FOR_TRIGGER: config[CONF_WAIT_FOR_TRIGGER] = await async_validate_trigger_config( From 6151bbe5c806eea7a9160c0110b6660ee65c54ea Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 21 Dec 2021 17:22:40 +0100 Subject: [PATCH 0893/2644] Improve debug log when warning about a dip in total_increasing sensor (#62501) --- homeassistant/components/sensor/recorder.py | 9 ++++++--- tests/components/sensor/test_recorder.py | 7 ++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 9b6c07a323d..644719f00d7 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -298,7 +298,9 @@ def _suggest_report_issue(hass: HomeAssistant, entity_id: str) -> str: return report_issue -def warn_dip(hass: HomeAssistant, entity_id: str, state: State) -> None: +def warn_dip( + hass: HomeAssistant, entity_id: str, state: State, previous_fstate: float +) -> None: """Log a warning once if a sensor with state_class_total has a decreasing value. The log will be suppressed until two dips have been seen to prevent warning due to @@ -319,11 +321,12 @@ def warn_dip(hass: HomeAssistant, entity_id: str, state: State) -> None: return _LOGGER.warning( "Entity %s %shas state class total_increasing, but its state is " - "not strictly increasing. Triggered by state %s with last_updated set to %s. " + "not strictly increasing. Triggered by state %s (%s) with last_updated set to %s. " "Please %s", entity_id, f"from integration {domain} " if domain else "", state.state, + previous_fstate, state.last_updated.isoformat(), _suggest_report_issue(hass, entity_id), ) @@ -359,7 +362,7 @@ def reset_detected( return False if 0.9 * previous_fstate <= fstate < previous_fstate: - warn_dip(hass, entity_id, state) + warn_dip(hass, entity_id, state, previous_fstate) if fstate < 0: warn_negative(hass, entity_id, state) diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 2da1a203dfd..faadb7140e2 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -1032,12 +1032,13 @@ def test_compile_hourly_sum_statistics_total_increasing_small_dip( recorder.do_adhoc_statistics(start=period2) wait_recording_done(hass) state = states["sensor.test1"][6].state + previous_state = float(states["sensor.test1"][5].state) last_updated = states["sensor.test1"][6].last_updated.isoformat() assert ( "Entity sensor.test1 has state class total_increasing, but its state is not " - f"strictly increasing. Triggered by state {state} with last_updated set to " - f"{last_updated}. Please create a bug report at https://github.com/home-assistant" - "/core/issues?q=is%3Aopen+is%3Aissue" + f"strictly increasing. Triggered by state {state} ({previous_state}) with " + f"last_updated set to {last_updated}. Please create a bug report at " + "https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue" ) in caplog.text statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ From 0e9282a404c7b0d31024b8ac1709aa3af90dbb45 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 21 Dec 2021 17:23:02 +0100 Subject: [PATCH 0894/2644] Deprecate pi4ioe5v9xxxx integration (ADR-0019) (#62491) --- .../components/pi4ioe5v9xxxx/binary_sensor.py | 11 +++++++++++ homeassistant/components/pi4ioe5v9xxxx/switch.py | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/homeassistant/components/pi4ioe5v9xxxx/binary_sensor.py b/homeassistant/components/pi4ioe5v9xxxx/binary_sensor.py index bdec7714eef..58436f3f8ea 100644 --- a/homeassistant/components/pi4ioe5v9xxxx/binary_sensor.py +++ b/homeassistant/components/pi4ioe5v9xxxx/binary_sensor.py @@ -1,4 +1,6 @@ """Support for binary sensor using RPi GPIO.""" +import logging + from pi4ioe5v9xxxx import pi4ioe5v9xxxx import voluptuous as vol @@ -30,9 +32,18 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) +_LOGGER = logging.getLogger(__name__) + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the IO expander devices.""" + _LOGGER.warning( + "The pi4ioe5v9xxxx IO Expander integration is deprecated and will be removed " + "in Home Assistant Core 2022.4; this integration is removed under " + "Architectural Decision Record 0019, more information can be found here: " + "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" + ) + pins = config[CONF_PINS] binary_sensors = [] diff --git a/homeassistant/components/pi4ioe5v9xxxx/switch.py b/homeassistant/components/pi4ioe5v9xxxx/switch.py index 85bde509070..0946bbefa8f 100644 --- a/homeassistant/components/pi4ioe5v9xxxx/switch.py +++ b/homeassistant/components/pi4ioe5v9xxxx/switch.py @@ -1,4 +1,6 @@ """Allows to configure a switch using RPi GPIO.""" +import logging + from pi4ioe5v9xxxx import pi4ioe5v9xxxx import voluptuous as vol @@ -29,9 +31,18 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) +_LOGGER = logging.getLogger(__name__) + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the swiches devices.""" + _LOGGER.warning( + "The pi4ioe5v9xxxx IO Expander integration is deprecated and will be removed " + "in Home Assistant Core 2022.4; this integration is removed under " + "Architectural Decision Record 0019, more information can be found here: " + "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" + ) + pins = config.get(CONF_PINS) switches = [] From 07e034c1c6721b7b0235606a9cda5ff9c05de1cf Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 21 Dec 2021 17:25:07 +0100 Subject: [PATCH 0895/2644] Add iif (immediate if) template function/filter (#61428) --- .pre-commit-config.yaml | 2 +- homeassistant/helpers/template.py | 21 ++++++++++++++++++++ tests/helpers/test_template.py | 33 +++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d4d9cf65354..fbd954a8103 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: hooks: - id: codespell args: - - --ignore-words-list=hass,alot,datas,dof,dur,ether,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,rime,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing,iam,incomfort,ba,haa + - --ignore-words-list=hass,alot,datas,dof,dur,ether,farenheit,hist,iff,iif,ines,ist,lightsensor,mut,nd,pres,referer,rime,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing,iam,incomfort,ba,haa - --skip="./.*,*.csv,*.json" - --quiet-level=2 exclude_types: [csv, json] diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 2a605561572..6373f63c5a0 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1763,6 +1763,25 @@ def slugify(value, separator="_"): return slugify_util(value, separator=separator) +def iif( + value: Any, if_true: Any = True, if_false: Any = False, if_none: Any = _SENTINEL +) -> Any: + """Immediate if function/filter that allow for common if/else constructs. + + https://en.wikipedia.org/wiki/IIf + + Examples: + {{ is_state("device_tracker.frenck", "home") | iif("yes", "no") }} + {{ iif(1==2, "yes", "no") }} + {{ (1 == 1) | iif("yes", "no") }} + """ + if value is None and if_none is not _SENTINEL: + return if_none + if bool(value): + return if_true + return if_false + + @contextmanager def set_template(template_str: str, action: str) -> Generator: """Store template being parsed or rendered in a Contextvar to aid error handling.""" @@ -1877,6 +1896,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.filters["int"] = forgiving_int_filter self.filters["relative_time"] = relative_time self.filters["slugify"] = slugify + self.filters["iif"] = iif self.globals["log"] = logarithm self.globals["sin"] = sine self.globals["cos"] = cosine @@ -1906,6 +1926,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.globals["pack"] = struct_pack self.globals["unpack"] = struct_unpack self.globals["slugify"] = slugify + self.globals["iif"] = iif self.tests["match"] = regex_match self.tests["search"] = regex_search diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 42a725eaff5..471a5b2c486 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -21,6 +21,7 @@ from homeassistant.const import ( TEMP_CELSIUS, VOLUME_LITERS, ) +from homeassistant.core import HomeAssistant from homeassistant.exceptions import TemplateError from homeassistant.helpers import device_registry as dr, entity, template from homeassistant.helpers.entity_platform import EntityPlatform @@ -3101,6 +3102,38 @@ def test_urlencode(hass): assert tpl.async_render() == "the%20quick%20brown%20fox%20%3D%20true" +def test_iif(hass: HomeAssistant) -> None: + """Test the immediate if function/filter.""" + tpl = template.Template("{{ (1 == 1) | iif }}", hass) + assert tpl.async_render() is True + + tpl = template.Template("{{ (1 == 2) | iif }}", hass) + assert tpl.async_render() is False + + tpl = template.Template("{{ (1 == 1) | iif('yes') }}", hass) + assert tpl.async_render() == "yes" + + tpl = template.Template("{{ (1 == 2) | iif('yes') }}", hass) + assert tpl.async_render() is False + + tpl = template.Template("{{ (1 == 2) | iif('yes', 'no') }}", hass) + assert tpl.async_render() == "no" + + tpl = template.Template("{{ not_exists | default(None) | iif('yes', 'no') }}", hass) + assert tpl.async_render() == "no" + + tpl = template.Template( + "{{ not_exists | default(None) | iif('yes', 'no', 'unknown') }}", hass + ) + assert tpl.async_render() == "unknown" + + tpl = template.Template("{{ iif(1 == 1) }}", hass) + assert tpl.async_render() is True + + tpl = template.Template("{{ iif(1 == 2, 'yes', 'no') }}", hass) + assert tpl.async_render() == "no" + + async def test_cache_garbage_collection(): """Test caching a template.""" template_string = ( From eb292fbfbd15d4b33bef73004828c57432cb3d55 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 21 Dec 2021 17:53:42 +0100 Subject: [PATCH 0896/2644] Deprecate BeagleBone Black GPIO integration (ADR-0019) (#62492) --- homeassistant/components/bbb_gpio/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/homeassistant/components/bbb_gpio/__init__.py b/homeassistant/components/bbb_gpio/__init__.py index f7d146e073e..9cc94de0910 100644 --- a/homeassistant/components/bbb_gpio/__init__.py +++ b/homeassistant/components/bbb_gpio/__init__.py @@ -1,13 +1,23 @@ """Support for controlling GPIO pins of a Beaglebone Black.""" +import logging + from Adafruit_BBIO import GPIO # pylint: disable=import-error from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP DOMAIN = "bbb_gpio" +_LOGGER = logging.getLogger(__name__) + def setup(hass, config): """Set up the BeagleBone Black GPIO component.""" + _LOGGER.warning( + "The BeagleBone Black GPIO integration is deprecated and will be removed " + "in Home Assistant Core 2022.4; this integration is removed under " + "Architectural Decision Record 0019, more information can be found here: " + "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" + ) def cleanup_gpio(event): """Stuff to do before stopping.""" From 0540c9455da4ac7bf4770ed114111e3d53c4f015 Mon Sep 17 00:00:00 2001 From: micha91 Date: Tue, 21 Dec 2021 18:56:13 +0100 Subject: [PATCH 0897/2644] Use EntityCategory enum for MusicCast entity types (#62303) --- homeassistant/components/yamaha_musiccast/const.py | 11 +++-------- .../components/yamaha_musiccast/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/yamaha_musiccast/const.py b/homeassistant/components/yamaha_musiccast/const.py index 470b3729c08..21b7de82184 100644 --- a/homeassistant/components/yamaha_musiccast/const.py +++ b/homeassistant/components/yamaha_musiccast/const.py @@ -9,11 +9,7 @@ from homeassistant.components.media_player.const import ( REPEAT_MODE_OFF, REPEAT_MODE_ONE, ) -from homeassistant.const import ( - ENTITY_CATEGORY_CONFIG, - ENTITY_CATEGORY_DIAGNOSTIC, - ENTITY_CATEGORY_SYSTEM, -) +from homeassistant.helpers.entity import EntityCategory DOMAIN = "yamaha_musiccast" @@ -51,10 +47,9 @@ MEDIA_CLASS_MAPPING = { } ENTITY_CATEGORY_MAPPING = { - EntityType.CONFIG: ENTITY_CATEGORY_CONFIG, + EntityType.CONFIG: EntityCategory.CONFIG, EntityType.REGULAR: None, - EntityType.DIAGNOSTIC: ENTITY_CATEGORY_DIAGNOSTIC, - EntityType.SYSTEM: ENTITY_CATEGORY_SYSTEM, + EntityType.DIAGNOSTIC: EntityCategory.DIAGNOSTIC, } DEVICE_CLASS_MAPPING = { diff --git a/homeassistant/components/yamaha_musiccast/manifest.json b/homeassistant/components/yamaha_musiccast/manifest.json index 329fa2354d5..7d07d57fc28 100644 --- a/homeassistant/components/yamaha_musiccast/manifest.json +++ b/homeassistant/components/yamaha_musiccast/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yamaha_musiccast", "requirements": [ - "aiomusiccast==0.14.2" + "aiomusiccast==0.14.3" ], "ssdp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 2734e6309b2..aa4fac79245 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -219,7 +219,7 @@ aiolyric==1.0.8 aiomodernforms==0.1.8 # homeassistant.components.yamaha_musiccast -aiomusiccast==0.14.2 +aiomusiccast==0.14.3 # homeassistant.components.nanoleaf aionanoleaf==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 102bf1d3e5f..38ae0552e51 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aiolyric==1.0.8 aiomodernforms==0.1.8 # homeassistant.components.yamaha_musiccast -aiomusiccast==0.14.2 +aiomusiccast==0.14.3 # homeassistant.components.nanoleaf aionanoleaf==0.1.1 From 1f62371f4575a8236622a4fb608c6327dbfa6dc3 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Tue, 21 Dec 2021 18:02:15 +0000 Subject: [PATCH 0898/2644] Use DeviceClass Enums in iotawatt tests (#62512) --- tests/components/iotawatt/test_sensor.py | 32 +++++++++++------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/tests/components/iotawatt/test_sensor.py b/tests/components/iotawatt/test_sensor.py index 2397338c22c..b025ed13d73 100644 --- a/tests/components/iotawatt/test_sensor.py +++ b/tests/components/iotawatt/test_sensor.py @@ -4,15 +4,13 @@ from datetime import timedelta from homeassistant.components.iotawatt.const import ATTR_LAST_UPDATE from homeassistant.components.sensor import ( ATTR_STATE_CLASS, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL, + SensorDeviceClass, + SensorStateClass, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, - DEVICE_CLASS_POWER, ENERGY_WATT_HOUR, POWER_WATT, ) @@ -47,10 +45,10 @@ async def test_sensor_type_input(hass, mock_iotawatt): state = hass.states.get("sensor.my_sensor") assert state is not None assert state.state == "23" - assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_MEASUREMENT + assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.MEASUREMENT assert state.attributes[ATTR_FRIENDLY_NAME] == "My Sensor" assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == POWER_WATT - assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_POWER + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.POWER assert state.attributes["channel"] == "1" assert state.attributes["type"] == "Input" @@ -74,10 +72,10 @@ async def test_sensor_type_output(hass, mock_iotawatt): state = hass.states.get("sensor.my_watthour_sensor") assert state is not None assert state.state == "243" - assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_TOTAL + assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.TOTAL assert state.attributes[ATTR_FRIENDLY_NAME] == "My WattHour Sensor" assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ENERGY_WATT_HOUR - assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_ENERGY + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENERGY assert state.attributes["type"] == "Output" mock_iotawatt.getSensors.return_value["sensors"].pop("my_watthour_sensor_key") @@ -102,7 +100,7 @@ async def test_sensor_type_accumulated_output(hass, mock_iotawatt): "sensor.my_watthour_accumulated_output_sensor_wh_accumulated", "100.0", { - "device_class": DEVICE_CLASS_ENERGY, + "device_class": SensorDeviceClass.ENERGY, "unit_of_measurement": ENERGY_WATT_HOUR, "last_update": DUMMY_DATE, }, @@ -125,9 +123,9 @@ async def test_sensor_type_accumulated_output(hass, mock_iotawatt): state.attributes[ATTR_FRIENDLY_NAME] == "My WattHour Accumulated Output Sensor.wh Accumulated" ) - assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_TOTAL + assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.TOTAL assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ENERGY_WATT_HOUR - assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_ENERGY + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENERGY assert state.attributes["type"] == "Output" assert state.attributes[ATTR_LAST_UPDATE] is not None assert state.attributes[ATTR_LAST_UPDATE] != DUMMY_DATE @@ -166,9 +164,9 @@ async def test_sensor_type_accumulated_output_error_restore(hass, mock_iotawatt) state.attributes[ATTR_FRIENDLY_NAME] == "My WattHour Accumulated Output Sensor.wh Accumulated" ) - assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_TOTAL + assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.TOTAL assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ENERGY_WATT_HOUR - assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_ENERGY + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENERGY assert state.attributes["type"] == "Output" assert state.attributes[ATTR_LAST_UPDATE] is not None assert state.attributes[ATTR_LAST_UPDATE] != DUMMY_DATE @@ -192,7 +190,7 @@ async def test_sensor_type_multiple_accumulated_output(hass, mock_iotawatt): "sensor.my_watthour_accumulated_output_sensor_wh_accumulated", "100.0", { - "device_class": DEVICE_CLASS_ENERGY, + "device_class": SensorDeviceClass.ENERGY, "unit_of_measurement": ENERGY_WATT_HOUR, "last_update": DUMMY_DATE, }, @@ -201,7 +199,7 @@ async def test_sensor_type_multiple_accumulated_output(hass, mock_iotawatt): "sensor.my_watthour_accumulated_input_sensor_wh_accumulated", "50.0", { - "device_class": DEVICE_CLASS_ENERGY, + "device_class": SensorDeviceClass.ENERGY, "unit_of_measurement": ENERGY_WATT_HOUR, "last_update": DUMMY_DATE, }, @@ -224,9 +222,9 @@ async def test_sensor_type_multiple_accumulated_output(hass, mock_iotawatt): state.attributes[ATTR_FRIENDLY_NAME] == "My WattHour Accumulated Output Sensor.wh Accumulated" ) - assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_TOTAL + assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.TOTAL assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ENERGY_WATT_HOUR - assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_ENERGY + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENERGY assert state.attributes["type"] == "Output" assert state.attributes[ATTR_LAST_UPDATE] is not None assert state.attributes[ATTR_LAST_UPDATE] != DUMMY_DATE From 498720f3c916ccd498911080b84d2f309fbbb48d Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Tue, 21 Dec 2021 18:03:39 +0000 Subject: [PATCH 0899/2644] Use DeviceClass Enums in integration tests (#62511) --- tests/components/integration/test_sensor.py | 24 ++++++++------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/tests/components/integration/test_sensor.py b/tests/components/integration/test_sensor.py index 03a43fd2c66..47d3095a85b 100644 --- a/tests/components/integration/test_sensor.py +++ b/tests/components/integration/test_sensor.py @@ -2,14 +2,8 @@ from datetime import timedelta from unittest.mock import patch -from homeassistant.components.sensor import STATE_CLASS_TOTAL -from homeassistant.const import ( - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - ENERGY_KILO_WATT_HOUR, - POWER_WATT, - TIME_SECONDS, -) +from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass +from homeassistant.const import ENERGY_KILO_WATT_HOUR, POWER_WATT, TIME_SECONDS from homeassistant.core import HomeAssistant, State from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -39,13 +33,13 @@ async def test_state(hass) -> None: state = hass.states.get("sensor.integration") assert state is not None - assert state.attributes.get("state_class") == STATE_CLASS_TOTAL + assert state.attributes.get("state_class") is SensorStateClass.TOTAL assert "device_class" not in state.attributes future_now = dt_util.utcnow() + timedelta(seconds=3600) with patch("homeassistant.util.dt.utcnow", return_value=future_now): hass.states.async_set( - entity_id, 1, {"device_class": DEVICE_CLASS_POWER}, force_update=True + entity_id, 1, {"device_class": SensorDeviceClass.POWER}, force_update=True ) await hass.async_block_till_done() @@ -56,8 +50,8 @@ async def test_state(hass) -> None: assert round(float(state.state), config["sensor"]["round"]) == 1.0 assert state.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR - assert state.attributes.get("device_class") == DEVICE_CLASS_ENERGY - assert state.attributes.get("state_class") == STATE_CLASS_TOTAL + assert state.attributes.get("device_class") == SensorDeviceClass.ENERGY + assert state.attributes.get("state_class") is SensorStateClass.TOTAL async def test_restore_state(hass: HomeAssistant) -> None: @@ -69,7 +63,7 @@ async def test_restore_state(hass: HomeAssistant) -> None: "sensor.integration", "100.0", { - "device_class": DEVICE_CLASS_ENERGY, + "device_class": SensorDeviceClass.ENERGY, "unit_of_measurement": ENERGY_KILO_WATT_HOUR, }, ), @@ -92,7 +86,7 @@ async def test_restore_state(hass: HomeAssistant) -> None: assert state assert state.state == "100.00" assert state.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR - assert state.attributes.get("device_class") == DEVICE_CLASS_ENERGY + assert state.attributes.get("device_class") == SensorDeviceClass.ENERGY async def test_restore_state_failed(hass: HomeAssistant) -> None: @@ -125,7 +119,7 @@ async def test_restore_state_failed(hass: HomeAssistant) -> None: assert state assert state.state == "unknown" assert state.attributes.get("unit_of_measurement") is None - assert state.attributes.get("state_class") == STATE_CLASS_TOTAL + assert state.attributes.get("state_class") is SensorStateClass.TOTAL assert "device_class" not in state.attributes From 9b437ef1466f213d819102238630152dc1b49c43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 21 Dec 2021 20:05:48 +0200 Subject: [PATCH 0900/2644] Remaining DeviceAutomationType bits (#62508) * Use DeviceAutomationType in missed tests/components/* * Tighten device automation type hints --- homeassistant/components/device_automation/__init__.py | 10 ++++++---- tests/common.py | 2 +- .../alarm_control_panel/test_device_action.py | 4 ++-- .../alarm_control_panel/test_device_trigger.py | 2 +- .../components/binary_sensor/test_device_condition.py | 2 +- tests/components/binary_sensor/test_device_trigger.py | 2 +- tests/components/cover/test_device_action.py | 6 +++--- tests/components/cover/test_device_condition.py | 6 +++--- tests/components/cover/test_device_trigger.py | 6 +++--- tests/components/fan/test_device_trigger.py | 2 +- tests/components/light/test_device_action.py | 6 +++--- tests/components/light/test_device_condition.py | 2 +- tests/components/light/test_device_trigger.py | 2 +- tests/components/lock/test_device_trigger.py | 2 +- tests/components/media_player/test_device_trigger.py | 2 +- 15 files changed, 29 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 9d80ce169a9..67ef17dc379 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -228,7 +228,11 @@ async def _async_get_device_automations( return combined_results -async def _async_get_device_automation_capabilities(hass, automation_type, automation): +async def _async_get_device_automation_capabilities( + hass: HomeAssistant, + automation_type: DeviceAutomationType, + automation: Mapping[str, Any], +) -> dict[str, Any]: """List device automations.""" try: platform = await async_get_device_automation_platform( @@ -237,8 +241,6 @@ async def _async_get_device_automation_capabilities(hass, automation_type, autom except InvalidDeviceAutomationConfig: return {} - if isinstance(automation_type, str): # until tests pass DeviceAutomationType - automation_type = DeviceAutomationType[automation_type.upper()] function_name = automation_type.value.get_capabilities_func if not hasattr(platform, function_name): @@ -259,7 +261,7 @@ async def _async_get_device_automation_capabilities(hass, automation_type, autom extra_fields, custom_serializer=cv.custom_serializer ) - return capabilities + return capabilities # type: ignore[no-any-return] def handle_device_errors(func): diff --git a/tests/common.py b/tests/common.py index 327427eda6e..73b67a63ebc 100644 --- a/tests/common.py +++ b/tests/common.py @@ -70,7 +70,7 @@ CLIENT_REDIRECT_URI = "https://example.com/app/callback" async def async_get_device_automations( hass: HomeAssistant, - automation_type: device_automation.DeviceAutomationType | str, + automation_type: device_automation.DeviceAutomationType, device_id: str, ) -> Any: """Get a device automation for a single device id.""" diff --git a/tests/components/alarm_control_panel/test_device_action.py b/tests/components/alarm_control_panel/test_device_action.py index f4b0832ad97..5cbe9f256ba 100644 --- a/tests/components/alarm_control_panel/test_device_action.py +++ b/tests/components/alarm_control_panel/test_device_action.py @@ -170,7 +170,7 @@ async def test_get_action_capabilities( assert {action["type"] for action in actions} == set(expected_capabilities) for action in actions: capabilities = await async_get_device_automation_capabilities( - hass, "action", action + hass, DeviceAutomationType.ACTION, action ) assert capabilities == expected_capabilities[action["type"]] @@ -222,7 +222,7 @@ async def test_get_action_capabilities_arm_code( assert {action["type"] for action in actions} == set(expected_capabilities) for action in actions: capabilities = await async_get_device_automation_capabilities( - hass, "action", action + hass, DeviceAutomationType.ACTION, action ) assert capabilities == expected_capabilities[action["type"]] diff --git a/tests/components/alarm_control_panel/test_device_trigger.py b/tests/components/alarm_control_panel/test_device_trigger.py index e874b50baa0..c8082e415e0 100644 --- a/tests/components/alarm_control_panel/test_device_trigger.py +++ b/tests/components/alarm_control_panel/test_device_trigger.py @@ -151,7 +151,7 @@ async def test_get_trigger_capabilities(hass, device_reg, entity_reg): assert len(triggers) == 6 for trigger in triggers: capabilities = await async_get_device_automation_capabilities( - hass, "trigger", trigger + hass, DeviceAutomationType.TRIGGER, trigger ) assert capabilities == { "extra_fields": [ diff --git a/tests/components/binary_sensor/test_device_condition.py b/tests/components/binary_sensor/test_device_condition.py index 2e23fd799f2..fcef3cfd418 100644 --- a/tests/components/binary_sensor/test_device_condition.py +++ b/tests/components/binary_sensor/test_device_condition.py @@ -137,7 +137,7 @@ async def test_get_condition_capabilities(hass, device_reg, entity_reg): ) for condition in conditions: capabilities = await async_get_device_automation_capabilities( - hass, "condition", condition + hass, DeviceAutomationType.CONDITION, condition ) assert capabilities == expected_capabilities diff --git a/tests/components/binary_sensor/test_device_trigger.py b/tests/components/binary_sensor/test_device_trigger.py index 001af0b1e64..c4cd7df9d91 100644 --- a/tests/components/binary_sensor/test_device_trigger.py +++ b/tests/components/binary_sensor/test_device_trigger.py @@ -140,7 +140,7 @@ async def test_get_trigger_capabilities(hass, device_reg, entity_reg): ) for trigger in triggers: capabilities = await async_get_device_automation_capabilities( - hass, "trigger", trigger + hass, DeviceAutomationType.TRIGGER, trigger ) assert capabilities == expected_capabilities diff --git a/tests/components/cover/test_device_action.py b/tests/components/cover/test_device_action.py index 7954b3389e1..e1595089d2e 100644 --- a/tests/components/cover/test_device_action.py +++ b/tests/components/cover/test_device_action.py @@ -151,7 +151,7 @@ async def test_get_action_capabilities( assert action_types == {"open", "close", "stop", "open_tilt", "close_tilt"} for action in actions: capabilities = await async_get_device_automation_capabilities( - hass, "action", action + hass, DeviceAutomationType.ACTION, action ) assert capabilities == {"extra_fields": []} @@ -197,7 +197,7 @@ async def test_get_action_capabilities_set_pos( assert action_types == {"set_position"} for action in actions: capabilities = await async_get_device_automation_capabilities( - hass, "action", action + hass, DeviceAutomationType.ACTION, action ) if action["type"] == "set_position": assert capabilities == expected_capabilities @@ -246,7 +246,7 @@ async def test_get_action_capabilities_set_tilt_pos( assert action_types == {"open", "close", "set_tilt_position"} for action in actions: capabilities = await async_get_device_automation_capabilities( - hass, "action", action + hass, DeviceAutomationType.ACTION, action ) if action["type"] == "set_tilt_position": assert capabilities == expected_capabilities diff --git a/tests/components/cover/test_device_condition.py b/tests/components/cover/test_device_condition.py index b964ad8bc0d..8d6403f8b52 100644 --- a/tests/components/cover/test_device_condition.py +++ b/tests/components/cover/test_device_condition.py @@ -138,7 +138,7 @@ async def test_get_condition_capabilities( assert len(conditions) == 4 for condition in conditions: capabilities = await async_get_device_automation_capabilities( - hass, "condition", condition + hass, DeviceAutomationType.CONDITION, condition ) assert capabilities == {"extra_fields": []} @@ -189,7 +189,7 @@ async def test_get_condition_capabilities_set_pos( assert len(conditions) == 5 for condition in conditions: capabilities = await async_get_device_automation_capabilities( - hass, "condition", condition + hass, DeviceAutomationType.CONDITION, condition ) if condition["type"] == "is_position": assert capabilities == expected_capabilities @@ -243,7 +243,7 @@ async def test_get_condition_capabilities_set_tilt_pos( assert len(conditions) == 5 for condition in conditions: capabilities = await async_get_device_automation_capabilities( - hass, "condition", condition + hass, DeviceAutomationType.CONDITION, condition ) if condition["type"] == "is_tilt_position": assert capabilities == expected_capabilities diff --git a/tests/components/cover/test_device_trigger.py b/tests/components/cover/test_device_trigger.py index 323394e9fe3..3eac5d29b61 100644 --- a/tests/components/cover/test_device_trigger.py +++ b/tests/components/cover/test_device_trigger.py @@ -158,7 +158,7 @@ async def test_get_trigger_capabilities( assert len(triggers) == 4 for trigger in triggers: capabilities = await async_get_device_automation_capabilities( - hass, "trigger", trigger + hass, DeviceAutomationType.TRIGGER, trigger ) assert capabilities == { "extra_fields": [ @@ -213,7 +213,7 @@ async def test_get_trigger_capabilities_set_pos( assert len(triggers) == 5 for trigger in triggers: capabilities = await async_get_device_automation_capabilities( - hass, "trigger", trigger + hass, DeviceAutomationType.TRIGGER, trigger ) if trigger["type"] == "position": assert capabilities == expected_capabilities @@ -275,7 +275,7 @@ async def test_get_trigger_capabilities_set_tilt_pos( assert len(triggers) == 5 for trigger in triggers: capabilities = await async_get_device_automation_capabilities( - hass, "trigger", trigger + hass, DeviceAutomationType.TRIGGER, trigger ) if trigger["type"] == "tilt_position": assert capabilities == expected_capabilities diff --git a/tests/components/fan/test_device_trigger.py b/tests/components/fan/test_device_trigger.py index c58b9004cde..0d9edaf6fab 100644 --- a/tests/components/fan/test_device_trigger.py +++ b/tests/components/fan/test_device_trigger.py @@ -92,7 +92,7 @@ async def test_get_trigger_capabilities(hass, device_reg, entity_reg): ) for trigger in triggers: capabilities = await async_get_device_automation_capabilities( - hass, "trigger", trigger + hass, DeviceAutomationType.TRIGGER, trigger ) assert capabilities == expected_capabilities diff --git a/tests/components/light/test_device_action.py b/tests/components/light/test_device_action.py index 6d38b4784a7..ec47710a6f6 100644 --- a/tests/components/light/test_device_action.py +++ b/tests/components/light/test_device_action.py @@ -127,7 +127,7 @@ async def test_get_action_capabilities(hass, device_reg, entity_reg): assert action_types == {"turn_on", "toggle", "turn_off"} for action in actions: capabilities = await async_get_device_automation_capabilities( - hass, "action", action + hass, DeviceAutomationType.ACTION, action ) assert capabilities == {"extra_fields": []} @@ -135,7 +135,7 @@ async def test_get_action_capabilities(hass, device_reg, entity_reg): entity_reg.async_remove(entity_id) for action in actions: capabilities = await async_get_device_automation_capabilities( - hass, "action", action + hass, DeviceAutomationType.ACTION, action ) assert capabilities == {"extra_fields": []} @@ -273,7 +273,7 @@ async def test_get_action_capabilities_features( assert action_types == expected_actions for action in actions: capabilities = await async_get_device_automation_capabilities( - hass, "action", action + hass, DeviceAutomationType.ACTION, action ) expected = {"extra_fields": expected_capabilities.get(action["type"], [])} assert capabilities == expected diff --git a/tests/components/light/test_device_condition.py b/tests/components/light/test_device_condition.py index 7afd1272017..ba718b385fb 100644 --- a/tests/components/light/test_device_condition.py +++ b/tests/components/light/test_device_condition.py @@ -91,7 +91,7 @@ async def test_get_condition_capabilities(hass, device_reg, entity_reg): ) for condition in conditions: capabilities = await async_get_device_automation_capabilities( - hass, "condition", condition + hass, DeviceAutomationType.CONDITION, condition ) assert capabilities == expected_capabilities diff --git a/tests/components/light/test_device_trigger.py b/tests/components/light/test_device_trigger.py index 342a761e7c4..d6e906abd74 100644 --- a/tests/components/light/test_device_trigger.py +++ b/tests/components/light/test_device_trigger.py @@ -91,7 +91,7 @@ async def test_get_trigger_capabilities(hass, device_reg, entity_reg): ) for trigger in triggers: capabilities = await async_get_device_automation_capabilities( - hass, "trigger", trigger + hass, DeviceAutomationType.TRIGGER, trigger ) assert capabilities == expected_capabilities diff --git a/tests/components/lock/test_device_trigger.py b/tests/components/lock/test_device_trigger.py index 000ed8b44aa..cf4287a02be 100644 --- a/tests/components/lock/test_device_trigger.py +++ b/tests/components/lock/test_device_trigger.py @@ -116,7 +116,7 @@ async def test_get_trigger_capabilities(hass, device_reg, entity_reg): assert len(triggers) == 5 for trigger in triggers: capabilities = await async_get_device_automation_capabilities( - hass, "trigger", trigger + hass, DeviceAutomationType.TRIGGER, trigger ) assert capabilities == { "extra_fields": [ diff --git a/tests/components/media_player/test_device_trigger.py b/tests/components/media_player/test_device_trigger.py index 6440430e2d2..842184d62ef 100644 --- a/tests/components/media_player/test_device_trigger.py +++ b/tests/components/media_player/test_device_trigger.py @@ -91,7 +91,7 @@ async def test_get_trigger_capabilities(hass, device_reg, entity_reg): assert len(triggers) == 5 for trigger in triggers: capabilities = await async_get_device_automation_capabilities( - hass, "trigger", trigger + hass, DeviceAutomationType.TRIGGER, trigger ) assert capabilities == { "extra_fields": [ From 33bddf275c9d2f33bcff82fa93292b132e85c988 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Tue, 21 Dec 2021 18:17:23 +0000 Subject: [PATCH 0901/2644] Use SensorStateClass Enums in fritzbox tests (#62134) --- tests/components/fritzbox/test_binary_sensor.py | 4 ++-- tests/components/fritzbox/test_sensor.py | 10 +++------- tests/components/fritzbox/test_switch.py | 9 ++++----- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/tests/components/fritzbox/test_binary_sensor.py b/tests/components/fritzbox/test_binary_sensor.py index f010c8499ea..a1ccc2bd0f2 100644 --- a/tests/components/fritzbox/test_binary_sensor.py +++ b/tests/components/fritzbox/test_binary_sensor.py @@ -5,7 +5,7 @@ from unittest.mock import Mock from requests.exceptions import HTTPError -from homeassistant.components.binary_sensor import DOMAIN +from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDeviceClass from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN from homeassistant.components.sensor import ATTR_STATE_CLASS, DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( @@ -39,7 +39,7 @@ async def test_setup(hass: HomeAssistant, fritz: Mock): assert state assert state.state == STATE_ON assert state.attributes[ATTR_FRIENDLY_NAME] == CONF_FAKE_NAME - assert state.attributes[ATTR_DEVICE_CLASS] == "window" + assert state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.WINDOW assert ATTR_STATE_CLASS not in state.attributes state = hass.states.get(f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_battery") diff --git a/tests/components/fritzbox/test_sensor.py b/tests/components/fritzbox/test_sensor.py index 42cf90bba58..07b54765c9a 100644 --- a/tests/components/fritzbox/test_sensor.py +++ b/tests/components/fritzbox/test_sensor.py @@ -9,11 +9,7 @@ from homeassistant.components.fritzbox.const import ( ATTR_STATE_LOCKED, DOMAIN as FB_DOMAIN, ) -from homeassistant.components.sensor import ( - ATTR_STATE_CLASS, - DOMAIN, - STATE_CLASS_MEASUREMENT, -) +from homeassistant.components.sensor import ATTR_STATE_CLASS, DOMAIN, SensorStateClass from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, @@ -47,14 +43,14 @@ async def test_setup(hass: HomeAssistant, fritz: Mock): assert state.attributes[ATTR_STATE_DEVICE_LOCKED] == "fake_locked_device" assert state.attributes[ATTR_STATE_LOCKED] == "fake_locked" assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS - assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_MEASUREMENT + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT state = hass.states.get(f"{ENTITY_ID}_humidity") assert state assert state.state == "42" assert state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Humidity" assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE - assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_MEASUREMENT + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT state = hass.states.get(f"{ENTITY_ID}_battery") assert state diff --git a/tests/components/fritzbox/test_switch.py b/tests/components/fritzbox/test_switch.py index 73bb7a1110b..f6f81d5f24c 100644 --- a/tests/components/fritzbox/test_switch.py +++ b/tests/components/fritzbox/test_switch.py @@ -12,8 +12,7 @@ from homeassistant.components.fritzbox.const import ( from homeassistant.components.sensor import ( ATTR_STATE_CLASS, DOMAIN as SENSOR_DOMAIN, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorStateClass, ) from homeassistant.components.switch import DOMAIN from homeassistant.const import ( @@ -62,7 +61,7 @@ async def test_setup(hass: HomeAssistant, fritz: Mock): assert state.attributes[ATTR_STATE_DEVICE_LOCKED] == "fake_locked_device" assert state.attributes[ATTR_STATE_LOCKED] == "fake_locked" assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS - assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_MEASUREMENT + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT state = hass.states.get(f"{ENTITY_ID}_humidity") assert state is None @@ -72,14 +71,14 @@ async def test_setup(hass: HomeAssistant, fritz: Mock): assert state.state == "5.678" assert state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Power Consumption" assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == POWER_WATT - assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_MEASUREMENT + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT state = hass.states.get(f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_total_energy") assert state assert state.state == "1.234" assert state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Total Energy" assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ENERGY_KILO_WATT_HOUR - assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_TOTAL_INCREASING + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.TOTAL_INCREASING async def test_turn_on(hass: HomeAssistant, fritz: Mock): From cc6228f5c943347f3e11b0859f26ba298d23d479 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 21 Dec 2021 15:14:44 -0500 Subject: [PATCH 0902/2644] Bump soco to 0.25.1 (#62523) --- homeassistant/components/sonos/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 1e31d2004b0..00006ab4e90 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,7 +3,7 @@ "name": "Sonos", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", - "requirements": ["soco==0.25.0"], + "requirements": ["soco==0.25.1"], "dependencies": ["ssdp"], "after_dependencies": ["plex", "zeroconf"], "zeroconf": ["_sonos._tcp.local."], diff --git a/requirements_all.txt b/requirements_all.txt index aa4fac79245..f21b5d5e17d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2196,7 +2196,7 @@ smhi-pkg==1.0.15 snapcast==2.1.3 # homeassistant.components.sonos -soco==0.25.0 +soco==0.25.1 # homeassistant.components.solaredge_local solaredge-local==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 38ae0552e51..df3a01cd66a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1303,7 +1303,7 @@ smarthab==0.21 smhi-pkg==1.0.15 # homeassistant.components.sonos -soco==0.25.0 +soco==0.25.1 # homeassistant.components.solaredge solaredge==0.0.2 From 8166f37830ff43e4788e64ce09e50a9ddd082d81 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Tue, 21 Dec 2021 20:51:10 +0000 Subject: [PATCH 0903/2644] Use new enums in nzbget tests (#62524) --- tests/components/nzbget/test_sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/nzbget/test_sensor.py b/tests/components/nzbget/test_sensor.py index d47e67c96c6..290414ab0ff 100644 --- a/tests/components/nzbget/test_sensor.py +++ b/tests/components/nzbget/test_sensor.py @@ -2,11 +2,11 @@ from datetime import timedelta from unittest.mock import patch +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, DATA_MEGABYTES, DATA_RATE_MEGABYTES_PER_SECOND, - DEVICE_CLASS_TIMESTAMP, ) from homeassistant.helpers import entity_registry as er from homeassistant.util import dt as dt_util @@ -39,7 +39,7 @@ async def test_sensors(hass, nzbget_api) -> None: "post_processing_jobs": ("PostJobCount", "2", "Jobs", None), "post_processing_paused": ("PostPaused", "False", None, None), "queue_size": ("RemainingSizeMB", "512", DATA_MEGABYTES, None), - "uptime": ("UpTimeSec", uptime.isoformat(), None, DEVICE_CLASS_TIMESTAMP), + "uptime": ("UpTimeSec", uptime.isoformat(), None, SensorDeviceClass.TIMESTAMP), } for (sensor_id, data) in sensors.items(): From e0c5cbf1e0594a03ef35bee4e39f1d4b23c287d0 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Tue, 21 Dec 2021 20:54:58 +0000 Subject: [PATCH 0904/2644] Use new enums in nam tests (#62522) --- tests/components/nam/test_sensor.py | 98 +++++++++++++---------------- 1 file changed, 45 insertions(+), 53 deletions(-) diff --git a/tests/components/nam/test_sensor.py b/tests/components/nam/test_sensor.py index 995d825454c..c841c957d97 100644 --- a/tests/components/nam/test_sensor.py +++ b/tests/components/nam/test_sensor.py @@ -8,7 +8,8 @@ from homeassistant.components.nam.const import DOMAIN from homeassistant.components.sensor import ( ATTR_STATE_CLASS, DOMAIN as SENSOR_DOMAIN, - STATE_CLASS_MEASUREMENT, + SensorDeviceClass, + SensorStateClass, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -17,15 +18,6 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_MILLION, - DEVICE_CLASS_CO2, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_PM1, - DEVICE_CLASS_PM10, - DEVICE_CLASS_PM25, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_SIGNAL_STRENGTH, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_TIMESTAMP, PERCENTAGE, PRESSURE_HPA, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, @@ -67,8 +59,8 @@ async def test_sensor(hass): state = hass.states.get("sensor.nettigo_air_monitor_bme280_humidity") assert state assert state.state == "45.7" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_HUMIDITY - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.HUMIDITY + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE entry = registry.async_get("sensor.nettigo_air_monitor_bme280_humidity") @@ -78,8 +70,8 @@ async def test_sensor(hass): state = hass.states.get("sensor.nettigo_air_monitor_bme280_temperature") assert state assert state.state == "7.6" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS entry = registry.async_get("sensor.nettigo_air_monitor_bme280_temperature") @@ -89,8 +81,8 @@ async def test_sensor(hass): state = hass.states.get("sensor.nettigo_air_monitor_bme280_pressure") assert state assert state.state == "1011" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PRESSURE - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_HPA entry = registry.async_get("sensor.nettigo_air_monitor_bme280_pressure") @@ -100,8 +92,8 @@ async def test_sensor(hass): state = hass.states.get("sensor.nettigo_air_monitor_bmp180_temperature") assert state assert state.state == "7.6" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS entry = registry.async_get("sensor.nettigo_air_monitor_bmp180_temperature") @@ -111,8 +103,8 @@ async def test_sensor(hass): state = hass.states.get("sensor.nettigo_air_monitor_bmp180_pressure") assert state assert state.state == "1032" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PRESSURE - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_HPA entry = registry.async_get("sensor.nettigo_air_monitor_bmp180_pressure") @@ -122,8 +114,8 @@ async def test_sensor(hass): state = hass.states.get("sensor.nettigo_air_monitor_bmp280_temperature") assert state assert state.state == "5.6" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS entry = registry.async_get("sensor.nettigo_air_monitor_bmp280_temperature") @@ -133,8 +125,8 @@ async def test_sensor(hass): state = hass.states.get("sensor.nettigo_air_monitor_bmp280_pressure") assert state assert state.state == "1022" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PRESSURE - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_HPA entry = registry.async_get("sensor.nettigo_air_monitor_bmp280_pressure") @@ -144,8 +136,8 @@ async def test_sensor(hass): state = hass.states.get("sensor.nettigo_air_monitor_sht3x_humidity") assert state assert state.state == "34.7" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_HUMIDITY - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.HUMIDITY + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE entry = registry.async_get("sensor.nettigo_air_monitor_sht3x_humidity") @@ -155,8 +147,8 @@ async def test_sensor(hass): state = hass.states.get("sensor.nettigo_air_monitor_sht3x_temperature") assert state assert state.state == "6.3" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS entry = registry.async_get("sensor.nettigo_air_monitor_sht3x_temperature") @@ -166,8 +158,8 @@ async def test_sensor(hass): state = hass.states.get("sensor.nettigo_air_monitor_dht22_humidity") assert state assert state.state == "46.2" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_HUMIDITY - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.HUMIDITY + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE entry = registry.async_get("sensor.nettigo_air_monitor_dht22_humidity") @@ -177,8 +169,8 @@ async def test_sensor(hass): state = hass.states.get("sensor.nettigo_air_monitor_dht22_temperature") assert state assert state.state == "6.3" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS entry = registry.async_get("sensor.nettigo_air_monitor_dht22_temperature") @@ -188,8 +180,8 @@ async def test_sensor(hass): state = hass.states.get("sensor.nettigo_air_monitor_heca_humidity") assert state assert state.state == "50.0" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_HUMIDITY - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.HUMIDITY + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE entry = registry.async_get("sensor.nettigo_air_monitor_heca_humidity") @@ -199,8 +191,8 @@ async def test_sensor(hass): state = hass.states.get("sensor.nettigo_air_monitor_heca_temperature") assert state assert state.state == "8.0" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS entry = registry.async_get("sensor.nettigo_air_monitor_heca_temperature") @@ -210,8 +202,8 @@ async def test_sensor(hass): state = hass.states.get("sensor.nettigo_air_monitor_signal_strength") assert state assert state.state == "-72" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_SIGNAL_STRENGTH - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.SIGNAL_STRENGTH + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SIGNAL_STRENGTH_DECIBELS_MILLIWATT @@ -227,7 +219,7 @@ async def test_sensor(hass): state.state == (utcnow() - timedelta(seconds=456987)).replace(microsecond=0).isoformat() ) - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TIMESTAMP + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP assert state.attributes.get(ATTR_STATE_CLASS) is None entry = registry.async_get("sensor.nettigo_air_monitor_uptime") @@ -237,8 +229,8 @@ async def test_sensor(hass): state = hass.states.get("sensor.nettigo_air_monitor_sds011_particulate_matter_10") assert state assert state.state == "19" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PM10 - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM10 + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER @@ -253,8 +245,8 @@ async def test_sensor(hass): state = hass.states.get("sensor.nettigo_air_monitor_sds011_particulate_matter_2_5") assert state assert state.state == "11" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PM25 - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM25 + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER @@ -269,8 +261,8 @@ async def test_sensor(hass): state = hass.states.get("sensor.nettigo_air_monitor_sps30_particulate_matter_1_0") assert state assert state.state == "31" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PM1 - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM1 + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER @@ -285,8 +277,8 @@ async def test_sensor(hass): state = hass.states.get("sensor.nettigo_air_monitor_sps30_particulate_matter_10") assert state assert state.state == "21" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PM10 - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM10 + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER @@ -299,8 +291,8 @@ async def test_sensor(hass): state = hass.states.get("sensor.nettigo_air_monitor_sps30_particulate_matter_2_5") assert state assert state.state == "34" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PM25 - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM25 + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER @@ -315,7 +307,7 @@ async def test_sensor(hass): state = hass.states.get("sensor.nettigo_air_monitor_sps30_particulate_matter_4_0") assert state assert state.state == "25" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER @@ -331,8 +323,8 @@ async def test_sensor(hass): state = hass.states.get("sensor.nettigo_air_monitor_mh_z14a_carbon_dioxide") assert state assert state.state == "865" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_CO2 - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.CO2 + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_PARTS_PER_MILLION @@ -369,7 +361,7 @@ async def test_incompleta_data_after_device_restart(hass): state = hass.states.get("sensor.nettigo_air_monitor_heca_temperature") assert state assert state.state == "8.0" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS future = utcnow() + timedelta(minutes=6) From 03054bc43014eba6e2fe9d5dc3bad52234b0aa06 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Tue, 21 Dec 2021 20:57:55 +0000 Subject: [PATCH 0905/2644] Use new enums in mysensors tests (#62521) --- tests/components/mysensors/test_sensor.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/tests/components/mysensors/test_sensor.py b/tests/components/mysensors/test_sensor.py index d648aebdefd..119d3c4eb42 100644 --- a/tests/components/mysensors/test_sensor.py +++ b/tests/components/mysensors/test_sensor.py @@ -8,16 +8,13 @@ import pytest from homeassistant.components.sensor import ( ATTR_STATE_CLASS, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, + SensorStateClass, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_TEMPERATURE, ENERGY_KILO_WATT_HOUR, POWER_WATT, TEMP_CELSIUS, @@ -71,9 +68,9 @@ async def test_power_sensor( assert state assert state.state == "1200" - assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_POWER + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.POWER assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == POWER_WATT - assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_MEASUREMENT + assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.MEASUREMENT async def test_energy_sensor( @@ -88,9 +85,9 @@ async def test_energy_sensor( assert state assert state.state == "18000" - assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_ENERGY + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENERGY assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ENERGY_KILO_WATT_HOUR - assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_TOTAL_INCREASING + assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.TOTAL_INCREASING async def test_sound_sensor( @@ -153,6 +150,6 @@ async def test_temperature_sensor( assert state assert state.state == temperature - assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_TEMPERATURE + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == unit - assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_MEASUREMENT + assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.MEASUREMENT From 043a4b06d0b356f3111a7ea3a19b72a627d66db4 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Tue, 21 Dec 2021 21:04:32 +0000 Subject: [PATCH 0906/2644] Use new enums in mqtt tests (#62520) --- tests/components/mqtt/test_sensor.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 680bafb3b2b..106845dbd51 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -83,27 +83,27 @@ async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock): @pytest.mark.parametrize( "device_class,native_value,state_value,log", [ - (sensor.DEVICE_CLASS_DATE, "2021-11-18", "2021-11-18", False), - (sensor.DEVICE_CLASS_DATE, "invalid", STATE_UNKNOWN, True), + (sensor.SensorDeviceClass.DATE, "2021-11-18", "2021-11-18", False), + (sensor.SensorDeviceClass.DATE, "invalid", STATE_UNKNOWN, True), ( - sensor.DEVICE_CLASS_TIMESTAMP, + sensor.SensorDeviceClass.TIMESTAMP, "2021-11-18T20:25:00+00:00", "2021-11-18T20:25:00+00:00", False, ), ( - sensor.DEVICE_CLASS_TIMESTAMP, + sensor.SensorDeviceClass.TIMESTAMP, "2021-11-18 20:25:00+00:00", "2021-11-18T20:25:00+00:00", False, ), ( - sensor.DEVICE_CLASS_TIMESTAMP, + sensor.SensorDeviceClass.TIMESTAMP, "2021-11-18 20:25:00+01:00", "2021-11-18T19:25:00+00:00", False, ), - (sensor.DEVICE_CLASS_TIMESTAMP, "invalid", STATE_UNKNOWN, True), + (sensor.SensorDeviceClass.TIMESTAMP, "invalid", STATE_UNKNOWN, True), ], ) async def test_setting_sensor_native_value_handling_via_mqtt_message( From 600db0794d692a457eca1d913ba8f63b0645f2ec Mon Sep 17 00:00:00 2001 From: Angelo Gagliano <25516409+TheGardenMonkey@users.noreply.github.com> Date: Tue, 21 Dec 2021 16:09:28 -0500 Subject: [PATCH 0907/2644] Require RPi.GPIO and bump adafruit-circuitpython-dht to 3.7.0 in dht (#61751) --- homeassistant/components/dht/manifest.json | 11 ++++++++--- requirements_all.txt | 3 ++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/dht/manifest.json b/homeassistant/components/dht/manifest.json index 9067b930f0a..3eb3cfd202c 100644 --- a/homeassistant/components/dht/manifest.json +++ b/homeassistant/components/dht/manifest.json @@ -2,7 +2,12 @@ "domain": "dht", "name": "DHT Sensor", "documentation": "https://www.home-assistant.io/integrations/dht", - "requirements": ["adafruit-circuitpython-dht==3.6.0"], - "codeowners": ["@thegardenmonkey"], + "requirements": [ + "adafruit-circuitpython-dht==3.7.0", + "RPi.GPIO==0.7.1a4" + ], + "codeowners": [ + "@thegardenmonkey" + ], "iot_class": "local_polling" -} +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index f21b5d5e17d..52ef54a7ab7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -64,6 +64,7 @@ PyViCare==2.13.1 PyXiaomiGateway==0.13.4 # homeassistant.components.bmp280 +# homeassistant.components.dht # homeassistant.components.mcp23017 # homeassistant.components.rpi_gpio # homeassistant.components.rpi_rf @@ -97,7 +98,7 @@ accuweather==0.3.0 adafruit-circuitpython-bmp280==3.1.1 # homeassistant.components.dht -adafruit-circuitpython-dht==3.6.0 +adafruit-circuitpython-dht==3.7.0 # homeassistant.components.mcp23017 adafruit-circuitpython-mcp230xx==2.2.2 From fddbecd2bf7c09aabb2dbd3ef8abcdb9ca9c35eb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Dec 2021 15:14:44 -0600 Subject: [PATCH 0908/2644] Fix backwards vol.Coerce order in flux_led (#62509) * Fix backwards vol.Coerce order in flux_led - reference: https://github.com/home-assistant/core/pull/62429#discussion_r773045507 * fix a few more after updating head * cleanup extra comma --- homeassistant/components/flux_led/light.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index 5d8da698a9a..a3c54365071 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -96,7 +96,7 @@ CUSTOM_EFFECT_DICT: Final = { [vol.All(vol.Coerce(tuple), vol.ExactSequence((cv.byte, cv.byte, cv.byte)))], ), vol.Optional(CONF_SPEED_PCT, default=50): vol.All( - vol.Range(min=0, max=100), vol.Coerce(int) + vol.Coerce(int), vol.Range(min=0, max=100) ), vol.Optional(CONF_TRANSITION, default=TRANSITION_GRADUAL): vol.All( cv.string, vol.In([TRANSITION_GRADUAL, TRANSITION_JUMP, TRANSITION_STROBE]) @@ -105,13 +105,13 @@ CUSTOM_EFFECT_DICT: Final = { SET_MUSIC_MODE_DICT: Final = { vol.Optional(ATTR_SENSITIVITY, default=100): vol.All( - vol.Range(min=0, max=100), vol.Coerce(int) + vol.Coerce(int), vol.Range(min=0, max=100) ), vol.Optional(ATTR_BRIGHTNESS, default=100): vol.All( - vol.Range(min=0, max=100), vol.Coerce(int) + vol.Coerce(int), vol.Range(min=0, max=100) ), vol.Optional(ATTR_EFFECT, default=1): vol.All( - vol.Range(min=1, max=16), vol.Coerce(int) + vol.Coerce(int), vol.Range(min=1, max=16) ), vol.Optional(ATTR_LIGHT_SCREEN, default=False): bool, vol.Optional(ATTR_FOREGROUND_COLOR): vol.All( @@ -129,7 +129,7 @@ SET_ZONES_DICT: Final = { [vol.All(vol.Coerce(tuple), vol.ExactSequence((cv.byte, cv.byte, cv.byte)))], ), vol.Optional(CONF_SPEED_PCT, default=50): vol.All( - vol.Range(min=0, max=100), vol.Coerce(int) + vol.Coerce(int), vol.Range(min=0, max=100) ), vol.Optional(CONF_EFFECT, default=MultiColorEffects.STATIC.name.lower()): vol.All( cv.string, vol.In([effect.name.lower() for effect in MultiColorEffects]) From d82e8b6cc0d62b8d3185815b38b2cefbe0d9b155 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Tue, 21 Dec 2021 21:30:18 +0000 Subject: [PATCH 0909/2644] Use new enums in mobile_app tests (#62517) --- tests/components/mobile_app/test_sensor.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/components/mobile_app/test_sensor.py b/tests/components/mobile_app/test_sensor.py index 295e37ee7d9..7eb99df8d8f 100644 --- a/tests/components/mobile_app/test_sensor.py +++ b/tests/components/mobile_app/test_sensor.py @@ -3,7 +3,7 @@ from http import HTTPStatus import pytest -from homeassistant.components.sensor import DEVICE_CLASS_DATE, DEVICE_CLASS_TIMESTAMP +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import PERCENTAGE, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -284,24 +284,24 @@ async def test_update_sensor_no_state(hass, create_registrations, webhook_client @pytest.mark.parametrize( "device_class,native_value,state_value", [ - (DEVICE_CLASS_DATE, "2021-11-18", "2021-11-18"), + (SensorDeviceClass.DATE, "2021-11-18", "2021-11-18"), ( - DEVICE_CLASS_TIMESTAMP, + SensorDeviceClass.TIMESTAMP, "2021-11-18T20:25:00+00:00", "2021-11-18T20:25:00+00:00", ), ( - DEVICE_CLASS_TIMESTAMP, + SensorDeviceClass.TIMESTAMP, "2021-11-18 20:25:00+01:00", "2021-11-18T19:25:00+00:00", ), ( - DEVICE_CLASS_TIMESTAMP, + SensorDeviceClass.TIMESTAMP, "unavailable", STATE_UNAVAILABLE, ), ( - DEVICE_CLASS_TIMESTAMP, + SensorDeviceClass.TIMESTAMP, "unknown", STATE_UNKNOWN, ), From 82013e68fb6c02b75050410ad4565b099eff6567 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 21 Dec 2021 22:51:47 +0100 Subject: [PATCH 0910/2644] Implement DataUpdateCoordinator in luftdaten (#62313) * Implement DataUpdateCoordinator in luftdaten * Typing additions/fixes * Update homeassistant/components/luftdaten/sensor.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- .../components/luftdaten/__init__.py | 160 ++++----------- homeassistant/components/luftdaten/sensor.py | 189 ++++++++++-------- 2 files changed, 144 insertions(+), 205 deletions(-) diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py index a2d7631552d..f131a2dc017 100644 --- a/homeassistant/components/luftdaten/__init__.py +++ b/homeassistant/components/luftdaten/__init__.py @@ -2,159 +2,69 @@ from __future__ import annotations import logging +from typing import Any from luftdaten import Luftdaten from luftdaten.exceptions import LuftdatenError -from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription -from homeassistant.const import ( - CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - CONF_MONITORED_CONDITIONS, - CONF_SCAN_INTERVAL, - CONF_SENSORS, - PERCENTAGE, - PRESSURE_PA, - TEMP_CELSIUS, - Platform, -) -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import CONF_SENSOR_ID, DEFAULT_SCAN_INTERVAL, DOMAIN _LOGGER = logging.getLogger(__name__) -DATA_LUFTDATEN = "luftdaten" -DATA_LUFTDATEN_CLIENT = "data_luftdaten_client" -DATA_LUFTDATEN_LISTENER = "data_luftdaten_listener" - PLATFORMS = [Platform.SENSOR] -TOPIC_UPDATE = f"{DOMAIN}_data_update" - -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( - key="temperature", - name="Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - ), - SensorEntityDescription( - key="humidity", - name="Humidity", - icon="mdi:water-percent", - native_unit_of_measurement=PERCENTAGE, - device_class=SensorDeviceClass.HUMIDITY, - ), - SensorEntityDescription( - key="pressure", - name="Pressure", - icon="mdi:arrow-down-bold", - native_unit_of_measurement=PRESSURE_PA, - device_class=SensorDeviceClass.PRESSURE, - ), - SensorEntityDescription( - key="pressure_at_sealevel", - name="Pressure at sealevel", - icon="mdi:download", - native_unit_of_measurement=PRESSURE_PA, - device_class=SensorDeviceClass.PRESSURE, - ), - SensorEntityDescription( - key="P1", - name="PM10", - icon="mdi:thought-bubble", - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - ), - SensorEntityDescription( - key="P2", - name="PM2.5", - icon="mdi:thought-bubble-outline", - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - ), -) -SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] - CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) -async def async_setup_entry(hass, config_entry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Luftdaten as config entry.""" - hass.data.setdefault( - DOMAIN, - { - DATA_LUFTDATEN_CLIENT: {}, - DATA_LUFTDATEN_LISTENER: {}, - }, - ) # For backwards compat, set unique ID - if config_entry.unique_id is None: + if entry.unique_id is None: hass.config_entries.async_update_entry( - config_entry, unique_id=config_entry.data[CONF_SENSOR_ID] + entry, unique_id=entry.data[CONF_SENSOR_ID] ) - try: - luftdaten = LuftDatenData( - Luftdaten(config_entry.data[CONF_SENSOR_ID]), - config_entry.data.get(CONF_SENSORS, {}).get( - CONF_MONITORED_CONDITIONS, SENSOR_KEYS - ), - ) - await luftdaten.async_update() - hass.data[DOMAIN][DATA_LUFTDATEN_CLIENT][config_entry.entry_id] = luftdaten - except LuftdatenError as err: - raise ConfigEntryNotReady from err + luftdaten = Luftdaten(entry.data[CONF_SENSOR_ID]) - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + async def async_update() -> dict[Any, Any]: + """Update sensor/binary sensor data.""" + try: + await luftdaten.get_data() + except LuftdatenError as err: + raise UpdateFailed("Unable to retrieve data from luftdaten.info") from err - async def refresh_sensors(event_time): - """Refresh Luftdaten data.""" - await luftdaten.async_update() - async_dispatcher_send(hass, TOPIC_UPDATE) + if not luftdaten.values: + raise UpdateFailed("Did not receive sensor data from luftdaten.info") - hass.data[DOMAIN][DATA_LUFTDATEN_LISTENER][ - config_entry.entry_id - ] = async_track_time_interval( + data = luftdaten.values + data.update(luftdaten.meta) + return data + + coordinator: DataUpdateCoordinator[dict[Any, Any]] = DataUpdateCoordinator( hass, - refresh_sensors, - hass.data[DOMAIN].get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL), + _LOGGER, + name=f"{DOMAIN}_{luftdaten.sensor_id}", + update_interval=DEFAULT_SCAN_INTERVAL, + update_method=async_update, ) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -async def async_unload_entry(hass, config_entry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload an Luftdaten config entry.""" - remove_listener = hass.data[DOMAIN][DATA_LUFTDATEN_LISTENER].pop( - config_entry.entry_id - ) - remove_listener() - - hass.data[DOMAIN][DATA_LUFTDATEN_CLIENT].pop(config_entry.entry_id) - - return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) - - -class LuftDatenData: - """Define a generic Luftdaten object.""" - - def __init__(self, client, sensor_conditions): - """Initialize the Luftdata object.""" - self.client = client - self.data = {} - self.sensor_conditions = sensor_conditions - - async def async_update(self): - """Update sensor/binary sensor data.""" - try: - await self.client.get_data() - - if self.client.values: - self.data[DATA_LUFTDATEN] = self.client.values - self.data[DATA_LUFTDATEN].update(self.client.meta) - - except LuftdatenError: - _LOGGER.error("Unable to retrieve data from luftdaten.info") + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + del hass.data[DOMAIN][entry.entry_id] + return unload_ok diff --git a/homeassistant/components/luftdaten/sensor.py b/homeassistant/components/luftdaten/sensor.py index 5b02869c23d..c0d4dbaf619 100644 --- a/homeassistant/components/luftdaten/sensor.py +++ b/homeassistant/components/luftdaten/sensor.py @@ -1,98 +1,127 @@ """Support for Luftdaten sensors.""" -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription -from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, CONF_SHOW_ON_MAP -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from __future__ import annotations -from . import DATA_LUFTDATEN, DATA_LUFTDATEN_CLIENT, DOMAIN, SENSOR_TYPES, TOPIC_UPDATE -from .const import ATTR_SENSOR_ID +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_LATITUDE, + ATTR_LONGITUDE, + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONF_SHOW_ON_MAP, + PERCENTAGE, + PRESSURE_PA, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from . import DOMAIN +from .const import ATTR_SENSOR_ID, CONF_SENSOR_ID + +SENSORS: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="temperature", + name="Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + ), + SensorEntityDescription( + key="humidity", + name="Humidity", + icon="mdi:water-percent", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.HUMIDITY, + ), + SensorEntityDescription( + key="pressure", + name="Pressure", + icon="mdi:arrow-down-bold", + native_unit_of_measurement=PRESSURE_PA, + device_class=SensorDeviceClass.PRESSURE, + ), + SensorEntityDescription( + key="pressure_at_sealevel", + name="Pressure at sealevel", + icon="mdi:download", + native_unit_of_measurement=PRESSURE_PA, + device_class=SensorDeviceClass.PRESSURE, + ), + SensorEntityDescription( + key="P1", + name="PM10", + icon="mdi:thought-bubble", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ), + SensorEntityDescription( + key="P2", + name="PM2.5", + icon="mdi:thought-bubble-outline", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ), +) -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up a Luftdaten sensor based on a config entry.""" - luftdaten = hass.data[DOMAIN][DATA_LUFTDATEN_CLIENT][entry.entry_id] + coordinator = hass.data[DOMAIN][entry.entry_id] - entities = [ - LuftdatenSensor(luftdaten, description, entry.data[CONF_SHOW_ON_MAP]) - for description in SENSOR_TYPES - if description.key in luftdaten.sensor_conditions - ] - - async_add_entities(entities, True) + async_add_entities( + LuftdatenSensor( + coordinator=coordinator, + description=description, + sensor_id=entry.data[CONF_SENSOR_ID], + show_on_map=entry.data[CONF_SHOW_ON_MAP], + ) + for description in SENSORS + if description.key in coordinator.data + ) -class LuftdatenSensor(SensorEntity): +class LuftdatenSensor(CoordinatorEntity, SensorEntity): """Implementation of a Luftdaten sensor.""" _attr_attribution = "Data provided by luftdaten.info" _attr_should_poll = False - def __init__(self, luftdaten, description: SensorEntityDescription, show): + def __init__( + self, + *, + coordinator: DataUpdateCoordinator, + description: SensorEntityDescription, + sensor_id: int, + show_on_map: bool, + ) -> None: """Initialize the Luftdaten sensor.""" + super().__init__(coordinator=coordinator) self.entity_description = description - self._async_unsub_dispatcher_connect = None - self.luftdaten = luftdaten - self._data = None - self._show_on_map = show - self._attrs = {} + self._attr_unique_id = f"{sensor_id}_{description.key}" + self._attr_extra_state_attributes = { + ATTR_SENSOR_ID: sensor_id, + } + if show_on_map: + self._attr_extra_state_attributes[ATTR_LONGITUDE] = coordinator.data[ + "longitude" + ] + self._attr_extra_state_attributes[ATTR_LATITUDE] = coordinator.data[ + "latitude" + ] @property - def native_value(self): + def native_value(self) -> float | None: """Return the state of the device.""" - if self._data is not None: - try: - return self._data[self.entity_description.key] - except KeyError: - return None - - @property - def unique_id(self) -> str: - """Return a unique, friendly identifier for this entity.""" - if self._data is not None: - try: - return f"{self._data['sensor_id']}_{self.entity_description.key}" - except KeyError: - return None - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - if self._data is not None: - try: - self._attrs[ATTR_SENSOR_ID] = self._data["sensor_id"] - except KeyError: - return None - - on_map = ATTR_LATITUDE, ATTR_LONGITUDE - no_map = "lat", "long" - lat_format, lon_format = on_map if self._show_on_map else no_map - try: - self._attrs[lon_format] = self._data["longitude"] - self._attrs[lat_format] = self._data["latitude"] - return self._attrs - except KeyError: - return - - async def async_added_to_hass(self): - """Register callbacks.""" - - @callback - def update(): - """Update the state.""" - self.async_schedule_update_ha_state(True) - - self._async_unsub_dispatcher_connect = async_dispatcher_connect( - self.hass, TOPIC_UPDATE, update - ) - - async def async_will_remove_from_hass(self): - """Disconnect dispatcher listener when removed.""" - if self._async_unsub_dispatcher_connect: - self._async_unsub_dispatcher_connect() - - async def async_update(self): - """Get the latest data and update the state.""" - try: - self._data = self.luftdaten.data[DATA_LUFTDATEN] - except KeyError: - return + if ( + not self.coordinator.data + or (value := self.coordinator.data.get(self.entity_description.key)) is None + ): + return None + return value From 550fe1860329bd79ea561ae01c57770a90d83ab4 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 21 Dec 2021 17:01:07 -0500 Subject: [PATCH 0911/2644] Use enums in tasmota tests (#62150) * Use enums in tasmota tests * platform enums --- .../components/tasmota/test_binary_sensor.py | 20 +++++------ tests/components/tasmota/test_cover.py | 22 ++++++------ tests/components/tasmota/test_fan.py | 20 +++++------ tests/components/tasmota/test_light.py | 29 +++++++-------- tests/components/tasmota/test_sensor.py | 36 +++++++++---------- tests/components/tasmota/test_switch.py | 23 ++++++------ 6 files changed, 73 insertions(+), 77 deletions(-) diff --git a/tests/components/tasmota/test_binary_sensor.py b/tests/components/tasmota/test_binary_sensor.py index 2ee40428293..5b19e46337a 100644 --- a/tests/components/tasmota/test_binary_sensor.py +++ b/tests/components/tasmota/test_binary_sensor.py @@ -11,13 +11,13 @@ from hatasmota.utils import ( get_topic_tele_will, ) -from homeassistant.components import binary_sensor from homeassistant.components.tasmota.const import DEFAULT_PREFIX from homeassistant.const import ( ATTR_ASSUMED_STATE, EVENT_STATE_CHANGED, STATE_OFF, STATE_ON, + Platform, ) import homeassistant.core as ha import homeassistant.util.dt as dt_util @@ -292,7 +292,7 @@ async def test_availability_when_connection_lost( config["swc"][0] = 1 config["swn"][0] = "Test" await help_test_availability_when_connection_lost( - hass, mqtt_client_mock, mqtt_mock, binary_sensor.DOMAIN, config + hass, mqtt_client_mock, mqtt_mock, Platform.BINARY_SENSOR, config ) @@ -301,7 +301,7 @@ async def test_availability(hass, mqtt_mock, setup_tasmota): config = copy.deepcopy(DEFAULT_CONFIG) config["swc"][0] = 1 config["swn"][0] = "Test" - await help_test_availability(hass, mqtt_mock, binary_sensor.DOMAIN, config) + await help_test_availability(hass, mqtt_mock, Platform.BINARY_SENSOR, config) async def test_availability_discovery_update(hass, mqtt_mock, setup_tasmota): @@ -310,7 +310,7 @@ async def test_availability_discovery_update(hass, mqtt_mock, setup_tasmota): config["swc"][0] = 1 config["swn"][0] = "Test" await help_test_availability_discovery_update( - hass, mqtt_mock, binary_sensor.DOMAIN, config + hass, mqtt_mock, Platform.BINARY_SENSOR, config ) @@ -326,7 +326,7 @@ async def test_availability_poll_state( hass, mqtt_client_mock, mqtt_mock, - binary_sensor.DOMAIN, + Platform.BINARY_SENSOR, config, poll_topic, "10", @@ -343,7 +343,7 @@ async def test_discovery_removal_binary_sensor(hass, mqtt_mock, caplog, setup_ta config2["swn"][0] = "Test" await help_test_discovery_removal( - hass, mqtt_mock, caplog, binary_sensor.DOMAIN, config1, config2 + hass, mqtt_mock, caplog, Platform.BINARY_SENSOR, config1, config2 ) @@ -358,7 +358,7 @@ async def test_discovery_update_unchanged_binary_sensor( "homeassistant.components.tasmota.binary_sensor.TasmotaBinarySensor.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, binary_sensor.DOMAIN, config, discovery_update + hass, mqtt_mock, caplog, Platform.BINARY_SENSOR, config, discovery_update ) @@ -368,7 +368,7 @@ async def test_discovery_device_remove(hass, mqtt_mock, setup_tasmota): config["swc"][0] = 1 unique_id = f"{DEFAULT_CONFIG['mac']}_binary_sensor_switch_0" await help_test_discovery_device_remove( - hass, mqtt_mock, binary_sensor.DOMAIN, unique_id, config + hass, mqtt_mock, Platform.BINARY_SENSOR, unique_id, config ) @@ -384,7 +384,7 @@ async def test_entity_id_update_subscriptions(hass, mqtt_mock, setup_tasmota): get_topic_tele_will(config), ] await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, binary_sensor.DOMAIN, config, topics + hass, mqtt_mock, Platform.BINARY_SENSOR, config, topics ) @@ -394,5 +394,5 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock, setup_tasmota) config["swc"][0] = 1 config["swn"][0] = "Test" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, binary_sensor.DOMAIN, config + hass, mqtt_mock, Platform.BINARY_SENSOR, config ) diff --git a/tests/components/tasmota/test_cover.py b/tests/components/tasmota/test_cover.py index c036f490f6d..80bf14943a9 100644 --- a/tests/components/tasmota/test_cover.py +++ b/tests/components/tasmota/test_cover.py @@ -13,7 +13,7 @@ import pytest from homeassistant.components import cover from homeassistant.components.tasmota.const import DEFAULT_PREFIX -from homeassistant.const import ATTR_ASSUMED_STATE, STATE_UNKNOWN +from homeassistant.const import ATTR_ASSUMED_STATE, STATE_UNKNOWN, Platform from .test_common import ( DEFAULT_CONFIG, @@ -392,7 +392,7 @@ async def test_controlling_state_via_mqtt_inverted(hass, mqtt_mock, setup_tasmot async def call_service(hass, entity_id, service, **kwargs): """Call a fan service.""" await hass.services.async_call( - cover.DOMAIN, + Platform.COVER, service, {"entity_id": entity_id, **kwargs}, blocking=True, @@ -538,7 +538,7 @@ async def test_availability_when_connection_lost( hass, mqtt_client_mock, mqtt_mock, - cover.DOMAIN, + Platform.COVER, config, entity_id="test_cover_1", ) @@ -551,7 +551,7 @@ async def test_availability(hass, mqtt_mock, setup_tasmota): config["rl"][0] = 3 config["rl"][1] = 3 await help_test_availability( - hass, mqtt_mock, cover.DOMAIN, config, entity_id="test_cover_1" + hass, mqtt_mock, Platform.COVER, config, entity_id="test_cover_1" ) @@ -562,7 +562,7 @@ async def test_availability_discovery_update(hass, mqtt_mock, setup_tasmota): config["rl"][0] = 3 config["rl"][1] = 3 await help_test_availability_discovery_update( - hass, mqtt_mock, cover.DOMAIN, config, entity_id="test_cover_1" + hass, mqtt_mock, Platform.COVER, config, entity_id="test_cover_1" ) @@ -575,7 +575,7 @@ async def test_availability_poll_state( config["rl"][1] = 3 poll_topic = "tasmota_49A3BC/cmnd/STATUS" await help_test_availability_poll_state( - hass, mqtt_client_mock, mqtt_mock, cover.DOMAIN, config, poll_topic, "10" + hass, mqtt_client_mock, mqtt_mock, Platform.COVER, config, poll_topic, "10" ) @@ -594,7 +594,7 @@ async def test_discovery_removal_cover(hass, mqtt_mock, caplog, setup_tasmota): hass, mqtt_mock, caplog, - cover.DOMAIN, + Platform.COVER, config1, config2, entity_id="test_cover_1", @@ -615,7 +615,7 @@ async def test_discovery_update_unchanged_cover(hass, mqtt_mock, caplog, setup_t hass, mqtt_mock, caplog, - cover.DOMAIN, + Platform.COVER, config, discovery_update, entity_id="test_cover_1", @@ -631,7 +631,7 @@ async def test_discovery_device_remove(hass, mqtt_mock, setup_tasmota): config["rl"][1] = 3 unique_id = f"{DEFAULT_CONFIG['mac']}_cover_shutter_0" await help_test_discovery_device_remove( - hass, mqtt_mock, cover.DOMAIN, unique_id, config + hass, mqtt_mock, Platform.COVER, unique_id, config ) @@ -648,7 +648,7 @@ async def test_entity_id_update_subscriptions(hass, mqtt_mock, setup_tasmota): get_topic_tele_will(config), ] await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, cover.DOMAIN, config, topics, entity_id="test_cover_1" + hass, mqtt_mock, Platform.COVER, config, topics, entity_id="test_cover_1" ) @@ -659,5 +659,5 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock, setup_tasmota) config["rl"][0] = 3 config["rl"][1] = 3 await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, cover.DOMAIN, config, entity_id="test_cover_1" + hass, mqtt_mock, Platform.COVER, config, entity_id="test_cover_1" ) diff --git a/tests/components/tasmota/test_fan.py b/tests/components/tasmota/test_fan.py index bb2610d466d..8c54e913f7c 100644 --- a/tests/components/tasmota/test_fan.py +++ b/tests/components/tasmota/test_fan.py @@ -13,7 +13,7 @@ from voluptuous import MultipleInvalid from homeassistant.components import fan from homeassistant.components.tasmota.const import DEFAULT_PREFIX -from homeassistant.const import ATTR_ASSUMED_STATE, STATE_OFF, STATE_ON +from homeassistant.const import ATTR_ASSUMED_STATE, STATE_OFF, STATE_ON, Platform from .test_common import ( DEFAULT_CONFIG, @@ -191,7 +191,7 @@ async def test_availability_when_connection_lost( config["dn"] = "Test" config["if"] = 1 await help_test_availability_when_connection_lost( - hass, mqtt_client_mock, mqtt_mock, fan.DOMAIN, config + hass, mqtt_client_mock, mqtt_mock, Platform.FAN, config ) @@ -200,7 +200,7 @@ async def test_availability(hass, mqtt_mock, setup_tasmota): config = copy.deepcopy(DEFAULT_CONFIG) config["dn"] = "Test" config["if"] = 1 - await help_test_availability(hass, mqtt_mock, fan.DOMAIN, config) + await help_test_availability(hass, mqtt_mock, Platform.FAN, config) async def test_availability_discovery_update(hass, mqtt_mock, setup_tasmota): @@ -208,7 +208,7 @@ async def test_availability_discovery_update(hass, mqtt_mock, setup_tasmota): config = copy.deepcopy(DEFAULT_CONFIG) config["dn"] = "Test" config["if"] = 1 - await help_test_availability_discovery_update(hass, mqtt_mock, fan.DOMAIN, config) + await help_test_availability_discovery_update(hass, mqtt_mock, Platform.FAN, config) async def test_availability_poll_state( @@ -219,7 +219,7 @@ async def test_availability_poll_state( config["if"] = 1 poll_topic = "tasmota_49A3BC/cmnd/STATE" await help_test_availability_poll_state( - hass, mqtt_client_mock, mqtt_mock, fan.DOMAIN, config, poll_topic, "" + hass, mqtt_client_mock, mqtt_mock, Platform.FAN, config, poll_topic, "" ) @@ -233,7 +233,7 @@ async def test_discovery_removal_fan(hass, mqtt_mock, caplog, setup_tasmota): config2["if"] = 0 await help_test_discovery_removal( - hass, mqtt_mock, caplog, fan.DOMAIN, config1, config2 + hass, mqtt_mock, caplog, Platform.FAN, config1, config2 ) @@ -246,7 +246,7 @@ async def test_discovery_update_unchanged_fan(hass, mqtt_mock, caplog, setup_tas "homeassistant.components.tasmota.fan.TasmotaFan.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, fan.DOMAIN, config, discovery_update + hass, mqtt_mock, caplog, Platform.FAN, config, discovery_update ) @@ -257,7 +257,7 @@ async def test_discovery_device_remove(hass, mqtt_mock, setup_tasmota): config["if"] = 1 unique_id = f"{DEFAULT_CONFIG['mac']}_fan_fan_ifan" await help_test_discovery_device_remove( - hass, mqtt_mock, fan.DOMAIN, unique_id, config + hass, mqtt_mock, Platform.FAN, unique_id, config ) @@ -272,7 +272,7 @@ async def test_entity_id_update_subscriptions(hass, mqtt_mock, setup_tasmota): get_topic_tele_will(config), ] await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, fan.DOMAIN, config, topics + hass, mqtt_mock, Platform.FAN, config, topics ) @@ -282,5 +282,5 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock, setup_tasmota) config["dn"] = "Test" config["if"] = 1 await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, fan.DOMAIN, config + hass, mqtt_mock, Platform.FAN, config ) diff --git a/tests/components/tasmota/test_light.py b/tests/components/tasmota/test_light.py index f85cf0d3c5b..bdad1f8ceef 100644 --- a/tests/components/tasmota/test_light.py +++ b/tests/components/tasmota/test_light.py @@ -10,10 +10,9 @@ from hatasmota.utils import ( get_topic_tele_will, ) -from homeassistant.components import light from homeassistant.components.light import SUPPORT_EFFECT, SUPPORT_TRANSITION from homeassistant.components.tasmota.const import DEFAULT_PREFIX -from homeassistant.const import ATTR_ASSUMED_STATE, STATE_OFF, STATE_ON +from homeassistant.const import ATTR_ASSUMED_STATE, STATE_OFF, STATE_ON, Platform from .test_common import ( DEFAULT_CONFIG, @@ -1620,7 +1619,7 @@ async def test_availability_when_connection_lost( config["rl"][0] = 2 config["lt_st"] = 1 # 1 channel light (Dimmer) await help_test_availability_when_connection_lost( - hass, mqtt_client_mock, mqtt_mock, light.DOMAIN, config + hass, mqtt_client_mock, mqtt_mock, Platform.LIGHT, config ) @@ -1629,7 +1628,7 @@ async def test_availability(hass, mqtt_mock, setup_tasmota): config = copy.deepcopy(DEFAULT_CONFIG) config["rl"][0] = 2 config["lt_st"] = 1 # 1 channel light (Dimmer) - await help_test_availability(hass, mqtt_mock, light.DOMAIN, config) + await help_test_availability(hass, mqtt_mock, Platform.LIGHT, config) async def test_availability_discovery_update(hass, mqtt_mock, setup_tasmota): @@ -1637,7 +1636,9 @@ async def test_availability_discovery_update(hass, mqtt_mock, setup_tasmota): config = copy.deepcopy(DEFAULT_CONFIG) config["rl"][0] = 2 config["lt_st"] = 1 # 1 channel light (Dimmer) - await help_test_availability_discovery_update(hass, mqtt_mock, light.DOMAIN, config) + await help_test_availability_discovery_update( + hass, mqtt_mock, Platform.LIGHT, config + ) async def test_availability_poll_state( @@ -1649,7 +1650,7 @@ async def test_availability_poll_state( config["lt_st"] = 1 # 1 channel light (Dimmer) poll_topic = "tasmota_49A3BC/cmnd/STATE" await help_test_availability_poll_state( - hass, mqtt_client_mock, mqtt_mock, light.DOMAIN, config, poll_topic, "" + hass, mqtt_client_mock, mqtt_mock, Platform.LIGHT, config, poll_topic, "" ) @@ -1663,7 +1664,7 @@ async def test_discovery_removal_light(hass, mqtt_mock, caplog, setup_tasmota): config2["lt_st"] = 0 await help_test_discovery_removal( - hass, mqtt_mock, caplog, light.DOMAIN, config1, config2 + hass, mqtt_mock, caplog, Platform.LIGHT, config1, config2 ) @@ -1677,7 +1678,7 @@ async def test_discovery_removal_relay_as_light(hass, mqtt_mock, caplog, setup_t config2["so"]["30"] = 0 # Disable Home Assistant auto-discovery as light await help_test_discovery_removal( - hass, mqtt_mock, caplog, light.DOMAIN, config1, config2 + hass, mqtt_mock, caplog, Platform.LIGHT, config1, config2 ) @@ -1693,7 +1694,7 @@ async def test_discovery_removal_relay_as_light2( config2["so"]["30"] = 0 # Disable Home Assistant auto-discovery as light await help_test_discovery_removal( - hass, mqtt_mock, caplog, light.DOMAIN, config1, config2 + hass, mqtt_mock, caplog, Platform.LIGHT, config1, config2 ) @@ -1706,7 +1707,7 @@ async def test_discovery_update_unchanged_light(hass, mqtt_mock, caplog, setup_t "homeassistant.components.tasmota.light.TasmotaLight.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, light.DOMAIN, config, discovery_update + hass, mqtt_mock, caplog, Platform.LIGHT, config, discovery_update ) @@ -1717,7 +1718,7 @@ async def test_discovery_device_remove(hass, mqtt_mock, setup_tasmota): config["lt_st"] = 1 # 1 channel light (Dimmer) unique_id = f"{DEFAULT_CONFIG['mac']}_light_light_0" await help_test_discovery_device_remove( - hass, mqtt_mock, light.DOMAIN, unique_id, config + hass, mqtt_mock, Platform.LIGHT, unique_id, config ) @@ -1728,7 +1729,7 @@ async def test_discovery_device_remove_relay_as_light(hass, mqtt_mock, setup_tas config["so"]["30"] = 1 # Enforce Home Assistant auto-discovery as light unique_id = f"{DEFAULT_CONFIG['mac']}_light_relay_0" await help_test_discovery_device_remove( - hass, mqtt_mock, light.DOMAIN, unique_id, config + hass, mqtt_mock, Platform.LIGHT, unique_id, config ) @@ -1743,7 +1744,7 @@ async def test_entity_id_update_subscriptions(hass, mqtt_mock, setup_tasmota): get_topic_tele_will(config), ] await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, light.DOMAIN, config, topics + hass, mqtt_mock, Platform.LIGHT, config, topics ) @@ -1753,5 +1754,5 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock, setup_tasmota) config["rl"][0] = 2 config["lt_st"] = 1 # 1 channel light (Dimmer) await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, light.DOMAIN, config + hass, mqtt_mock, Platform.LIGHT, config ) diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py index f3c4622c03d..e2f5f1111e1 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -14,9 +14,9 @@ from hatasmota.utils import ( import pytest from homeassistant import config_entries -from homeassistant.components import sensor +from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorStateClass from homeassistant.components.tasmota.const import DEFAULT_PREFIX -from homeassistant.const import ATTR_ASSUMED_STATE, STATE_UNKNOWN +from homeassistant.const import ATTR_ASSUMED_STATE, STATE_UNKNOWN, Platform from homeassistant.helpers import entity_registry as er from homeassistant.util import dt @@ -291,9 +291,7 @@ async def test_indexed_sensor_state_via_mqtt2(hass, mqtt_mock, setup_tasmota): state = hass.states.get("sensor.tasmota_energy_total") assert state.state == "unavailable" assert not state.attributes.get(ATTR_ASSUMED_STATE) - assert ( - state.attributes[sensor.ATTR_STATE_CLASS] == sensor.STATE_CLASS_TOTAL_INCREASING - ) + assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.TOTAL_INCREASING async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() @@ -342,9 +340,7 @@ async def test_indexed_sensor_state_via_mqtt3(hass, mqtt_mock, setup_tasmota): state = hass.states.get("sensor.tasmota_energy_total_1") assert state.state == "unavailable" assert not state.attributes.get(ATTR_ASSUMED_STATE) - assert ( - state.attributes[sensor.ATTR_STATE_CLASS] == sensor.STATE_CLASS_TOTAL_INCREASING - ) + assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.TOTAL_INCREASING async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() @@ -490,7 +486,7 @@ async def test_status_sensor_state_via_mqtt(hass, mqtt_mock, setup_tasmota): # Pre-enable the status sensor entity_reg.async_get_or_create( - sensor.DOMAIN, + Platform.SENSOR, "tasmota", "00000049A3BC_status_sensor_status_sensor_status_signal", suggested_object_id="tasmota_status", @@ -550,7 +546,7 @@ async def test_single_shot_status_sensor_state_via_mqtt(hass, mqtt_mock, setup_t # Pre-enable the status sensor entity_reg.async_get_or_create( - sensor.DOMAIN, + Platform.SENSOR, "tasmota", "00000049A3BC_status_sensor_status_sensor_status_restart_reason", suggested_object_id="tasmota_status", @@ -635,7 +631,7 @@ async def test_restart_time_status_sensor_state_via_mqtt( # Pre-enable the status sensor entity_reg.async_get_or_create( - sensor.DOMAIN, + Platform.SENSOR, "tasmota", "00000049A3BC_status_sensor_status_sensor_last_restart_time", suggested_object_id="tasmota_status", @@ -888,7 +884,7 @@ async def test_availability_when_connection_lost( hass, mqtt_client_mock, mqtt_mock, - sensor.DOMAIN, + Platform.SENSOR, config, sensor_config, "tasmota_dht11_temperature", @@ -902,7 +898,7 @@ async def test_availability(hass, mqtt_mock, setup_tasmota): await help_test_availability( hass, mqtt_mock, - sensor.DOMAIN, + Platform.SENSOR, config, sensor_config, "tasmota_dht11_temperature", @@ -916,7 +912,7 @@ async def test_availability_discovery_update(hass, mqtt_mock, setup_tasmota): await help_test_availability_discovery_update( hass, mqtt_mock, - sensor.DOMAIN, + Platform.SENSOR, config, sensor_config, "tasmota_dht11_temperature", @@ -934,7 +930,7 @@ async def test_availability_poll_state( hass, mqtt_client_mock, mqtt_mock, - sensor.DOMAIN, + Platform.SENSOR, config, poll_topic, "10", @@ -951,7 +947,7 @@ async def test_discovery_removal_sensor(hass, mqtt_mock, caplog, setup_tasmota): hass, mqtt_mock, caplog, - sensor.DOMAIN, + Platform.SENSOR, config, config, sensor_config1, @@ -974,7 +970,7 @@ async def test_discovery_update_unchanged_sensor( hass, mqtt_mock, caplog, - sensor.DOMAIN, + Platform.SENSOR, config, discovery_update, sensor_config, @@ -989,7 +985,7 @@ async def test_discovery_device_remove(hass, mqtt_mock, setup_tasmota): sensor_config = copy.deepcopy(DEFAULT_SENSOR_CONFIG) unique_id = f"{DEFAULT_CONFIG['mac']}_sensor_sensor_DHT11_Temperature" await help_test_discovery_device_remove( - hass, mqtt_mock, sensor.DOMAIN, unique_id, config, sensor_config + hass, mqtt_mock, Platform.SENSOR, unique_id, config, sensor_config ) @@ -1005,7 +1001,7 @@ async def test_entity_id_update_subscriptions(hass, mqtt_mock, setup_tasmota): await help_test_entity_id_update_subscriptions( hass, mqtt_mock, - sensor.DOMAIN, + Platform.SENSOR, config, topics, sensor_config, @@ -1020,7 +1016,7 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock, setup_tasmota) await help_test_entity_id_update_discovery_update( hass, mqtt_mock, - sensor.DOMAIN, + Platform.SENSOR, config, sensor_config, "tasmota_dht11_temperature", diff --git a/tests/components/tasmota/test_switch.py b/tests/components/tasmota/test_switch.py index 7c5bf66db45..aa619083171 100644 --- a/tests/components/tasmota/test_switch.py +++ b/tests/components/tasmota/test_switch.py @@ -9,9 +9,8 @@ from hatasmota.utils import ( get_topic_tele_will, ) -from homeassistant.components import switch from homeassistant.components.tasmota.const import DEFAULT_PREFIX -from homeassistant.const import ATTR_ASSUMED_STATE, STATE_OFF, STATE_ON +from homeassistant.const import ATTR_ASSUMED_STATE, STATE_OFF, STATE_ON, Platform from .test_common import ( DEFAULT_CONFIG, @@ -143,7 +142,7 @@ async def test_availability_when_connection_lost( config = copy.deepcopy(DEFAULT_CONFIG) config["rl"][0] = 1 await help_test_availability_when_connection_lost( - hass, mqtt_client_mock, mqtt_mock, switch.DOMAIN, config + hass, mqtt_client_mock, mqtt_mock, Platform.SWITCH, config ) @@ -151,7 +150,7 @@ async def test_availability(hass, mqtt_mock, setup_tasmota): """Test availability.""" config = copy.deepcopy(DEFAULT_CONFIG) config["rl"][0] = 1 - await help_test_availability(hass, mqtt_mock, switch.DOMAIN, config) + await help_test_availability(hass, mqtt_mock, Platform.SWITCH, config) async def test_availability_discovery_update(hass, mqtt_mock, setup_tasmota): @@ -159,7 +158,7 @@ async def test_availability_discovery_update(hass, mqtt_mock, setup_tasmota): config = copy.deepcopy(DEFAULT_CONFIG) config["rl"][0] = 1 await help_test_availability_discovery_update( - hass, mqtt_mock, switch.DOMAIN, config + hass, mqtt_mock, Platform.SWITCH, config ) @@ -171,7 +170,7 @@ async def test_availability_poll_state( config["rl"][0] = 1 poll_topic = "tasmota_49A3BC/cmnd/STATE" await help_test_availability_poll_state( - hass, mqtt_client_mock, mqtt_mock, switch.DOMAIN, config, poll_topic, "" + hass, mqtt_client_mock, mqtt_mock, Platform.SWITCH, config, poll_topic, "" ) @@ -183,7 +182,7 @@ async def test_discovery_removal_switch(hass, mqtt_mock, caplog, setup_tasmota): config2["rl"][0] = 0 await help_test_discovery_removal( - hass, mqtt_mock, caplog, switch.DOMAIN, config1, config2 + hass, mqtt_mock, caplog, Platform.SWITCH, config1, config2 ) @@ -197,7 +196,7 @@ async def test_discovery_removal_relay_as_light(hass, mqtt_mock, caplog, setup_t config2["so"]["30"] = 1 # Enforce Home Assistant auto-discovery as light await help_test_discovery_removal( - hass, mqtt_mock, caplog, switch.DOMAIN, config1, config2 + hass, mqtt_mock, caplog, Platform.SWITCH, config1, config2 ) @@ -211,7 +210,7 @@ async def test_discovery_update_unchanged_switch( "homeassistant.components.tasmota.switch.TasmotaSwitch.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, switch.DOMAIN, config, discovery_update + hass, mqtt_mock, caplog, Platform.SWITCH, config, discovery_update ) @@ -221,7 +220,7 @@ async def test_discovery_device_remove(hass, mqtt_mock, setup_tasmota): config["rl"][0] = 1 unique_id = f"{DEFAULT_CONFIG['mac']}_switch_relay_0" await help_test_discovery_device_remove( - hass, mqtt_mock, switch.DOMAIN, unique_id, config + hass, mqtt_mock, Platform.SWITCH, unique_id, config ) @@ -235,7 +234,7 @@ async def test_entity_id_update_subscriptions(hass, mqtt_mock, setup_tasmota): get_topic_tele_will(config), ] await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, switch.DOMAIN, config, topics + hass, mqtt_mock, Platform.SWITCH, config, topics ) @@ -244,5 +243,5 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock, setup_tasmota) config = copy.deepcopy(DEFAULT_CONFIG) config["rl"][0] = 1 await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, switch.DOMAIN, config + hass, mqtt_mock, Platform.SWITCH, config ) From 1279592a98a3076998a478f062dabab8af2ad4d5 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 21 Dec 2021 17:37:46 -0500 Subject: [PATCH 0912/2644] Remove deprecated yaml config from vlc_telnet (#62542) --- .../components/vlc_telnet/config_flow.py | 4 -- .../components/vlc_telnet/media_player.py | 45 ++----------------- .../components/vlc_telnet/test_config_flow.py | 37 +-------------- 3 files changed, 6 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/vlc_telnet/config_flow.py b/homeassistant/components/vlc_telnet/config_flow.py index bb503229ebb..d714057e6f7 100644 --- a/homeassistant/components/vlc_telnet/config_flow.py +++ b/homeassistant/components/vlc_telnet/config_flow.py @@ -104,10 +104,6 @@ class VLCTelnetConfigFlow(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=user_form_schema(user_input), errors=errors ) - async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: - """Handle the import step.""" - return await self.async_step_user(user_input) - async def async_step_reauth(self, data: dict[str, Any]) -> FlowResult: """Handle reauth flow.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) diff --git a/homeassistant/components/vlc_telnet/media_player.py b/homeassistant/components/vlc_telnet/media_player.py index 0bb157ab2ae..1aa612ade25 100644 --- a/homeassistant/components/vlc_telnet/media_player.py +++ b/homeassistant/components/vlc_telnet/media_player.py @@ -7,9 +7,8 @@ from typing import Any, Callable, TypeVar, cast from aiovlc.client import Client from aiovlc.exceptions import AuthError, CommandError, ConnectError -import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity +from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_CLEAR_PLAYLIST, @@ -24,25 +23,15 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PASSWORD, - CONF_PORT, - STATE_IDLE, - STATE_PAUSED, - STATE_PLAYING, -) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_NAME, STATE_IDLE, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util -from .const import DATA_AVAILABLE, DATA_VLC, DEFAULT_NAME, DEFAULT_PORT, DOMAIN, LOGGER +from .const import DATA_AVAILABLE, DATA_VLC, DEFAULT_NAME, DOMAIN, LOGGER MAX_VOLUME = 500 @@ -59,36 +48,10 @@ SUPPORT_VLC = ( | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.positive_int, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - } -) Func = TypeVar("Func", bound=Callable[..., Any]) -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the vlc platform.""" - LOGGER.warning( - "Loading VLC media player Telnet integration via platform setup is deprecated; " - "Please remove it from your configuration" - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config - ) - ) - - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: diff --git a/tests/components/vlc_telnet/test_config_flow.py b/tests/components/vlc_telnet/test_config_flow.py index 6a0659b6360..f86644b3447 100644 --- a/tests/components/vlc_telnet/test_config_flow.py +++ b/tests/components/vlc_telnet/test_config_flow.py @@ -79,38 +79,7 @@ async def test_user_flow( assert len(mock_setup_entry.mock_calls) == 1 -async def test_import_flow(hass: HomeAssistant) -> None: - """Test successful import flow.""" - test_data = { - "password": "test-password", - "host": "1.1.1.1", - "port": 8888, - "name": "custom name", - } - with patch("homeassistant.components.vlc_telnet.config_flow.Client.connect"), patch( - "homeassistant.components.vlc_telnet.config_flow.Client.login" - ), patch( - "homeassistant.components.vlc_telnet.config_flow.Client.disconnect" - ), patch( - "homeassistant.components.vlc_telnet.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=test_data, - ) - await hass.async_block_till_done() - - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["title"] == test_data["name"] - assert result["data"] == test_data - assert len(mock_setup_entry.mock_calls) == 1 - - -@pytest.mark.parametrize( - "source", [config_entries.SOURCE_USER, config_entries.SOURCE_IMPORT] -) +@pytest.mark.parametrize("source", [config_entries.SOURCE_USER]) async def test_abort_already_configured(hass: HomeAssistant, source: str) -> None: """Test we handle already configured host.""" entry_data = { @@ -133,9 +102,7 @@ async def test_abort_already_configured(hass: HomeAssistant, source: str) -> Non assert result["reason"] == "already_configured" -@pytest.mark.parametrize( - "source", [config_entries.SOURCE_USER, config_entries.SOURCE_IMPORT] -) +@pytest.mark.parametrize("source", [config_entries.SOURCE_USER]) @pytest.mark.parametrize( "error, connect_side_effect, login_side_effect", [ From cceedf766aa38e53ea47d08f7e47300935227ef6 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 22 Dec 2021 00:14:51 +0000 Subject: [PATCH 0913/2644] [ci skip] Translation update --- .../components/acmeda/translations/it.json | 2 +- .../components/adguard/translations/it.json | 2 +- .../components/agent_dvr/translations/it.json | 2 +- .../components/airly/translations/it.json | 2 +- .../components/airvisual/translations/it.json | 12 ++-- .../alarm_control_panel/translations/it.json | 18 ++--- .../alarmdecoder/translations/it.json | 16 ++--- .../ambiclimate/translations/it.json | 2 +- .../components/androidtv/translations/bg.json | 21 ++++++ .../components/androidtv/translations/he.json | 66 +++++++++++++++++++ .../components/androidtv/translations/hu.json | 66 +++++++++++++++++++ .../components/androidtv/translations/it.json | 66 +++++++++++++++++++ .../components/androidtv/translations/ja.json | 66 +++++++++++++++++++ .../components/androidtv/translations/no.json | 38 +++++++++++ .../components/androidtv/translations/ru.json | 66 +++++++++++++++++++ .../androidtv/translations/zh-Hant.json | 66 +++++++++++++++++++ .../components/apple_tv/translations/it.json | 2 +- .../components/auth/translations/it.json | 12 ++-- .../components/axis/translations/it.json | 2 +- .../azure_devops/translations/it.json | 2 +- .../azure_event_hub/translations/it.json | 4 +- .../azure_event_hub/translations/no.json | 42 ++++++++++++ .../components/blink/translations/it.json | 2 +- .../bmw_connected_drive/translations/it.json | 2 +- .../components/bond/translations/it.json | 2 +- .../components/bosch_shc/translations/it.json | 2 +- .../components/braviatv/translations/it.json | 6 +- .../components/broadlink/translations/it.json | 6 +- .../components/brother/translations/it.json | 2 +- .../cert_expiry/translations/it.json | 2 +- .../cloudflare/translations/it.json | 4 +- .../components/coinbase/translations/it.json | 2 +- .../configurator/translations/it.json | 2 +- .../coolmaster/translations/it.json | 2 +- .../coronavirus/translations/it.json | 2 +- .../components/daikin/translations/it.json | 2 +- .../components/deconz/translations/it.json | 6 +- .../components/demo/translations/it.json | 2 +- .../components/denonavr/translations/it.json | 14 ++-- .../components/dexcom/translations/it.json | 2 +- .../dialogflow/translations/it.json | 2 +- .../components/doorbird/translations/it.json | 2 +- .../components/dsmr/translations/it.json | 2 +- .../components/dunehd/translations/it.json | 2 +- .../components/elkm1/translations/it.json | 2 +- .../components/elmax/translations/it.json | 2 +- .../emulated_roku/translations/it.json | 2 +- .../components/esphome/translations/it.json | 4 +- .../evil_genius_labs/translations/no.json | 1 + .../components/ezviz/translations/it.json | 2 +- .../forked_daapd/translations/it.json | 10 +-- .../components/freebox/translations/it.json | 2 +- .../components/fritzbox/translations/it.json | 2 +- .../components/fronius/translations/it.json | 2 +- .../components/geofency/translations/it.json | 2 +- .../components/gios/translations/it.json | 2 +- .../components/glances/translations/it.json | 4 +- .../components/goalzero/translations/it.json | 2 +- .../google_travel_time/translations/it.json | 2 +- .../components/gpslogger/translations/it.json | 2 +- .../components/guardian/translations/it.json | 2 +- .../components/hangouts/translations/it.json | 2 +- .../components/harmony/translations/it.json | 4 +- .../components/hassio/translations/it.json | 2 +- .../hisense_aehw4a1/translations/it.json | 2 +- .../homeassistant/translations/it.json | 4 +- .../homeassistant/translations/ja.json | 10 +-- .../components/homekit/translations/it.json | 2 +- .../homekit_controller/translations/it.json | 4 +- .../homematicip_cloud/translations/it.json | 4 +- .../humidifier/translations/it.json | 4 +- .../components/hyperion/translations/it.json | 2 +- .../components/icloud/translations/it.json | 4 +- .../components/insteon/translations/it.json | 16 ++--- .../components/ipp/translations/it.json | 10 +-- .../islamic_prayer_times/translations/it.json | 4 +- .../components/isy994/translations/it.json | 2 +- .../keenetic_ndms2/translations/it.json | 4 +- .../components/kmtronic/translations/it.json | 2 +- .../components/knx/translations/it.json | 10 +-- .../components/konnected/translations/it.json | 6 +- .../components/locative/translations/it.json | 2 +- .../lutron_caseta/translations/it.json | 2 +- .../components/mailgun/translations/it.json | 2 +- .../meteo_france/translations/it.json | 2 +- .../components/metoffice/translations/it.json | 2 +- .../components/mikrotik/translations/it.json | 6 +- .../minecraft_server/translations/it.json | 4 +- .../mobile_app/translations/it.json | 4 +- .../mobile_app/translations/ja.json | 2 +- .../motion_blinds/translations/it.json | 2 +- .../components/motioneye/translations/it.json | 2 +- .../components/mullvad/translations/it.json | 2 +- .../components/mysensors/translations/it.json | 4 +- .../components/netatmo/translations/it.json | 2 +- .../components/netgear/translations/it.json | 2 +- .../nfandroidtv/translations/he.json | 4 +- .../components/nina/translations/it.json | 2 +- .../components/nws/translations/it.json | 2 +- .../components/nzbget/translations/it.json | 2 +- .../components/octoprint/translations/it.json | 2 +- .../components/onewire/translations/it.json | 2 +- .../components/onvif/translations/it.json | 12 ++-- .../open_meteo/translations/no.json | 12 ++++ .../opengarage/translations/it.json | 2 +- .../components/pi_hole/translations/it.json | 2 +- .../components/plex/translations/it.json | 8 +-- .../components/point/translations/it.json | 2 +- .../components/powerwall/translations/it.json | 2 +- .../progettihwsw/translations/it.json | 4 +- .../components/ps4/translations/it.json | 10 +-- .../components/remote/translations/it.json | 2 +- .../components/rfxtrx/translations/it.json | 16 ++--- .../components/risco/translations/it.json | 20 +++--- .../components/roon/translations/it.json | 4 +- .../components/select/translations/it.json | 2 +- .../components/sentry/translations/it.json | 6 +- .../components/sia/translations/it.json | 6 +- .../simplisafe/translations/it.json | 4 +- .../components/sma/translations/it.json | 4 +- .../components/smarthab/translations/it.json | 2 +- .../smartthings/translations/it.json | 8 +-- .../components/solaredge/translations/it.json | 2 +- .../somfy_mylink/translations/it.json | 8 +-- .../components/sonarr/translations/it.json | 2 +- .../speedtestdotnet/translations/it.json | 4 +- .../squeezebox/translations/it.json | 2 +- .../components/starline/translations/ja.json | 2 +- .../components/switch/translations/it.json | 6 +- .../components/switchbot/translations/it.json | 6 +- .../components/syncthing/translations/it.json | 2 +- .../components/syncthru/translations/it.json | 2 +- .../synology_dsm/translations/it.json | 8 +-- .../components/tado/translations/it.json | 4 +- .../tellduslive/translations/it.json | 2 +- .../components/tile/translations/bg.json | 8 ++- .../components/tile/translations/et.json | 9 ++- .../components/tile/translations/he.json | 8 ++- .../components/tile/translations/hu.json | 9 ++- .../components/tile/translations/it.json | 9 ++- .../components/tile/translations/ja.json | 9 ++- .../components/tile/translations/no.json | 3 +- .../components/tile/translations/ru.json | 9 ++- .../components/tile/translations/zh-Hant.json | 9 ++- .../components/toon/translations/it.json | 2 +- .../components/traccar/translations/it.json | 2 +- .../components/tuya/translations/it.json | 2 +- .../twentemilieu/translations/it.json | 2 +- .../components/twilio/translations/it.json | 2 +- .../components/unifi/translations/it.json | 12 ++-- .../components/upb/translations/it.json | 4 +- .../components/upnp/translations/it.json | 4 +- .../components/vicare/translations/no.json | 10 +++ .../components/vilfo/translations/it.json | 2 +- .../components/vizio/translations/it.json | 4 +- .../waze_travel_time/translations/it.json | 4 +- .../components/wiffi/translations/it.json | 2 +- .../components/withings/translations/ja.json | 2 +- .../components/wolflink/translations/it.json | 2 +- .../xiaomi_aqara/translations/it.json | 4 +- .../xiaomi_miio/translations/it.json | 6 +- .../components/yeelight/translations/it.json | 2 +- .../components/zha/translations/it.json | 8 +-- .../zoneminder/translations/it.json | 4 +- .../components/zwave/translations/it.json | 2 +- .../components/zwave_js/translations/it.json | 4 +- 166 files changed, 878 insertions(+), 301 deletions(-) create mode 100644 homeassistant/components/androidtv/translations/bg.json create mode 100644 homeassistant/components/androidtv/translations/he.json create mode 100644 homeassistant/components/androidtv/translations/hu.json create mode 100644 homeassistant/components/androidtv/translations/it.json create mode 100644 homeassistant/components/androidtv/translations/ja.json create mode 100644 homeassistant/components/androidtv/translations/ru.json create mode 100644 homeassistant/components/androidtv/translations/zh-Hant.json create mode 100644 homeassistant/components/open_meteo/translations/no.json diff --git a/homeassistant/components/acmeda/translations/it.json b/homeassistant/components/acmeda/translations/it.json index 8592e6cc8da..8b5ea51230e 100644 --- a/homeassistant/components/acmeda/translations/it.json +++ b/homeassistant/components/acmeda/translations/it.json @@ -8,7 +8,7 @@ "data": { "id": "ID host" }, - "title": "Scegliere un hub da aggiungere" + "title": "Scegli un hub da aggiungere" } } } diff --git a/homeassistant/components/adguard/translations/it.json b/homeassistant/components/adguard/translations/it.json index 5f8bc33997d..e2653ffd915 100644 --- a/homeassistant/components/adguard/translations/it.json +++ b/homeassistant/components/adguard/translations/it.json @@ -19,7 +19,7 @@ "port": "Porta", "ssl": "Utilizza un certificato SSL", "username": "Nome utente", - "verify_ssl": "Verificare il certificato SSL" + "verify_ssl": "Verifica il certificato SSL" }, "description": "Configura l'istanza di AdGuard Home per consentire il monitoraggio e il controllo." } diff --git a/homeassistant/components/agent_dvr/translations/it.json b/homeassistant/components/agent_dvr/translations/it.json index 8c33cfcc63b..92983fd9073 100644 --- a/homeassistant/components/agent_dvr/translations/it.json +++ b/homeassistant/components/agent_dvr/translations/it.json @@ -13,7 +13,7 @@ "host": "Host", "port": "Porta" }, - "title": "Configurare Agent DVR" + "title": "Configura Agent DVR" } } } diff --git a/homeassistant/components/airly/translations/it.json b/homeassistant/components/airly/translations/it.json index 385b8117437..170455ffb15 100644 --- a/homeassistant/components/airly/translations/it.json +++ b/homeassistant/components/airly/translations/it.json @@ -15,7 +15,7 @@ "longitude": "Logitudine", "name": "Nome" }, - "description": "Configurazione dell'integrazione della qualit\u00e0 dell'aria Airly. Per generare la chiave API andare su https://developer.airly.eu/register", + "description": "Configurazione dell'integrazione della qualit\u00e0 dell'aria Airly. Per generare la chiave API vai su https://developer.airly.eu/register", "title": "Airly" } } diff --git a/homeassistant/components/airvisual/translations/it.json b/homeassistant/components/airvisual/translations/it.json index 581a1256c15..34605fdcaf7 100644 --- a/homeassistant/components/airvisual/translations/it.json +++ b/homeassistant/components/airvisual/translations/it.json @@ -18,7 +18,7 @@ "longitude": "Logitudine" }, "description": "Usa l'API cloud di AirVisual per monitorare una latitudine/longitudine.", - "title": "Configurare un'area geografica" + "title": "Configura un'area geografica" }, "geography_by_name": { "data": { @@ -28,15 +28,15 @@ "state": "Stato" }, "description": "Usa l'API cloud di AirVisual per monitorare una citt\u00e0/stato/paese.", - "title": "Configurare un'area geografica" + "title": "Configura un'area geografica" }, "node_pro": { "data": { "ip_address": "Host", "password": "Password" }, - "description": "Monitorare un'unit\u00e0 AirVisual personale. La password pu\u00f2 essere recuperata dall'interfaccia utente dell'unit\u00e0.", - "title": "Configurare un AirVisual Node/Pro" + "description": "Monitora un'unit\u00e0 AirVisual personale. La password pu\u00f2 essere recuperata dall'interfaccia utente dell'unit\u00e0.", + "title": "Configura un AirVisual Node/Pro" }, "reauth_confirm": { "data": { @@ -45,7 +45,7 @@ "title": "Autentica nuovamente AirVisual" }, "user": { - "description": "Scegliere il tipo di dati AirVisual che si desidera monitorare.", + "description": "Scegli il tipo di dati AirVisual che desideri monitorare.", "title": "Configura AirVisual" } } @@ -56,7 +56,7 @@ "data": { "show_on_map": "Mostra l'area geografica monitorata sulla mappa" }, - "title": "Configurare AirVisual" + "title": "Configura AirVisual" } } } diff --git a/homeassistant/components/alarm_control_panel/translations/it.json b/homeassistant/components/alarm_control_panel/translations/it.json index 2015dc0e09d..ac07c28da62 100644 --- a/homeassistant/components/alarm_control_panel/translations/it.json +++ b/homeassistant/components/alarm_control_panel/translations/it.json @@ -1,11 +1,11 @@ { "device_automation": { "action_type": { - "arm_away": "Armare {entity_name} uscito", - "arm_home": "Armare {entity_name} casa", - "arm_night": "Armare {entity_name} notte", - "arm_vacation": "Armare {entity_name} vacanza", - "disarm": "Disarmare {entity_name}", + "arm_away": "Attiva {entity_name} fuori casa", + "arm_home": "Attiva {entity_name} casa", + "arm_night": "Attiva {entity_name} notte", + "arm_vacation": "Attiva {entity_name} vacanza", + "disarm": "Disattiva {entity_name}", "trigger": "Attivazione {entity_name}" }, "condition_type": { @@ -32,13 +32,13 @@ "armed_custom_bypass": "Attivo con bypass personalizzato", "armed_home": "Attivo in casa", "armed_night": "Attivo Notte", - "armed_vacation": "Attivo Vacanza", - "arming": "In Attivazione", + "armed_vacation": "Attivo in vacanza", + "arming": "In attivazione", "disarmed": "Disattivo", - "disarming": "In Disattivazione", + "disarming": "In disattivazione", "pending": "In sospeso", "triggered": "Attivato" } }, - "title": "Pannello di Controllo degli Allarmi" + "title": "Pannello di controllo degli allarmi" } \ No newline at end of file diff --git a/homeassistant/components/alarmdecoder/translations/it.json b/homeassistant/components/alarmdecoder/translations/it.json index 70be8d733fe..83de29ca190 100644 --- a/homeassistant/components/alarmdecoder/translations/it.json +++ b/homeassistant/components/alarmdecoder/translations/it.json @@ -17,13 +17,13 @@ "host": "Host", "port": "Porta" }, - "title": "Configurare le impostazioni di connessione" + "title": "Configura le impostazioni di connessione" }, "user": { "data": { "protocol": "Protocollo" }, - "title": "Scegliere il protocollo AlarmDecoder" + "title": "Scegli il protocollo AlarmDecoder" } } }, @@ -41,14 +41,14 @@ "auto_bypass": "Bypass automatico all'attivazione", "code_arm_required": "Codice richiesto per l'attivazione" }, - "title": "Configurare AlarmDecoder" + "title": "Configura AlarmDecoder" }, "init": { "data": { "edit_select": "Modifica" }, "description": "Cosa vorresti modificare?", - "title": "Configurare AlarmDecoder" + "title": "Configura AlarmDecoder" }, "zone_details": { "data": { @@ -59,15 +59,15 @@ "zone_rfid": "Seriale RF", "zone_type": "Tipo di zona" }, - "description": "Immettere i dettagli per la zona {zone_number}. Per eliminare la zona {zone_number}, lasciare vuoto il campo Nome zona.", - "title": "Configurare AlarmDecoder" + "description": "Immetti i dettagli per la zona {zone_number}. Per eliminare la zona {zone_number}, lascia vuoto il campo Nome zona.", + "title": "Configura AlarmDecoder" }, "zone_select": { "data": { "zone_number": "Numero di zona" }, - "description": "Immettere il numero di zona che si desidera aggiungere, modificare o rimuovere.", - "title": "Configurare AlarmDecoder" + "description": "Immettere il numero di zona che desideri aggiungere, modificare o rimuovere.", + "title": "Configura AlarmDecoder" } } } diff --git a/homeassistant/components/ambiclimate/translations/it.json b/homeassistant/components/ambiclimate/translations/it.json index 618d4cbe7ca..ccf0836ee1d 100644 --- a/homeassistant/components/ambiclimate/translations/it.json +++ b/homeassistant/components/ambiclimate/translations/it.json @@ -15,7 +15,7 @@ "step": { "auth": { "description": "Segui questo [link]({authorization_url}) e **Consenti** l'accesso al tuo account Ambiclimate, quindi torna indietro e premi **Invia** qui sotto. \n(Assicurati che l'URL di richiamata specificato sia {cb_url})", - "title": "Autenticare Ambiclimate" + "title": "Autentica Ambiclimate" } } } diff --git a/homeassistant/components/androidtv/translations/bg.json b/homeassistant/components/androidtv/translations/bg.json new file mode 100644 index 00000000000..9dd3bde62ad --- /dev/null +++ b/homeassistant/components/androidtv/translations/bg.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_host": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0438\u043c\u0435 \u043d\u0430 \u0445\u043e\u0441\u0442 \u0438\u043b\u0438 IP \u0430\u0434\u0440\u0435\u0441", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "title": "Android TV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/androidtv/translations/he.json b/homeassistant/components/androidtv/translations/he.json new file mode 100644 index 00000000000..298416bccc5 --- /dev/null +++ b/homeassistant/components/androidtv/translations/he.json @@ -0,0 +1,66 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "invalid_unique_id": "\u05d1\u05dc\u05ea\u05d9 \u05d0\u05e4\u05e9\u05e8\u05d9 \u05dc\u05e7\u05d1\u05d5\u05e2 \u05de\u05d6\u05d4\u05d4 \u05d9\u05d9\u05d7\u05d5\u05d3\u05d9 \u05d7\u05d5\u05e7\u05d9 \u05e2\u05d1\u05d5\u05e8 \u05d4\u05d4\u05ea\u05e7\u05df" + }, + "error": { + "adbkey_not_file": "\u05e7\u05d5\u05d1\u05e5 \u05d4\u05de\u05e4\u05ea\u05d7 \u05e9\u05dc ADB \u05dc\u05d0 \u05e0\u05de\u05e6\u05d0", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_host": "\u05e9\u05dd \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea IP \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd", + "key_and_server": "\u05e1\u05e4\u05e7 \u05e8\u05e7 \u05de\u05e4\u05ea\u05d7 ADB \u05d0\u05d5 \u05e9\u05e8\u05ea ADB", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "adb_server_ip": "\u05db\u05ea\u05d5\u05d1\u05ea IP \u05e9\u05dc \u05e9\u05e8\u05ea ADB (\u05d4\u05e9\u05d0\u05e8 \u05e8\u05d9\u05e7 \u05db\u05d3\u05d9 \u05dc\u05d0 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9)", + "adb_server_port": "\u05d9\u05e6\u05d9\u05d0\u05d4 \u05e9\u05dc \u05e9\u05e8\u05ea ADB", + "adbkey": "\u05e0\u05ea\u05d9\u05d1 \u05dc\u05e7\u05d5\u05d1\u05e5 \u05d4\u05de\u05e4\u05ea\u05d7 \u05e9\u05dc ADB (\u05d4\u05e9\u05d0\u05e8 \u05e8\u05d9\u05e7 \u05dc\u05d9\u05e6\u05d9\u05e8\u05d4 \u05d0\u05d5\u05d8\u05d5\u05de\u05d8\u05d9\u05ea)", + "device_class": "\u05e1\u05d5\u05d2 \u05d4\u05d4\u05ea\u05e7\u05df", + "host": "\u05de\u05d0\u05e8\u05d7", + "port": "\u05e4\u05ea\u05d7\u05d4" + }, + "description": "\u05d4\u05d2\u05d3\u05e8\u05ea \u05d4\u05e4\u05e8\u05de\u05d8\u05e8\u05d9\u05dd \u05d4\u05e0\u05d3\u05e8\u05e9\u05d9\u05dd \u05db\u05d3\u05d9 \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d8\u05dc\u05d5\u05d5\u05d9\u05d6\u05d9\u05d9\u05ea \u05d4\u05d0\u05e0\u05d3\u05e8\u05d5\u05d0\u05d9\u05d3 \u05e9\u05dc\u05da", + "title": "\u05d8\u05dc\u05d5\u05d5\u05d9\u05d6\u05d9\u05ea \u05d0\u05e0\u05d3\u05e8\u05d5\u05d0\u05d9\u05d3" + } + } + }, + "options": { + "error": { + "invalid_det_rules": "\u05db\u05dc\u05dc\u05d9 \u05d6\u05d9\u05d4\u05d5\u05d9 \u05de\u05e6\u05d1 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd" + }, + "step": { + "apps": { + "data": { + "app_delete": "\u05e1\u05de\u05df \u05db\u05d3\u05d9 \u05dc\u05de\u05d7\u05d5\u05e7 \u05d9\u05d9\u05e9\u05d5\u05dd \u05d6\u05d4", + "app_id": "\u05de\u05d6\u05d4\u05d4 \u05d9\u05d9\u05e9\u05d5\u05dd", + "app_name": "\u05e9\u05dd \u05d9\u05d9\u05e9\u05d5\u05dd" + }, + "description": "\u05e7\u05d1\u05d9\u05e2\u05ea \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05de\u05d6\u05d4\u05d4 \u05d4\u05d9\u05d9\u05e9\u05d5\u05dd {app_id}", + "title": "\u05e7\u05d1\u05d9\u05e2\u05ea \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05d9\u05d9\u05e9\u05d5\u05de\u05d9 \u05d8\u05dc\u05d5\u05d5\u05d9\u05d6\u05d9\u05ea \u05d0\u05e0\u05d3\u05e8\u05d5\u05d0\u05d9\u05d3" + }, + "init": { + "data": { + "apps": "\u05e7\u05d1\u05d9\u05e2\u05ea \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05e8\u05e9\u05d9\u05de\u05ea \u05d9\u05d9\u05e9\u05d5\u05de\u05d9\u05dd", + "exclude_unnamed_apps": "\u05d0\u05dc \u05ea\u05db\u05dc\u05d5\u05dc \u05d9\u05d9\u05e9\u05d5\u05dd \u05e2\u05dd \u05e9\u05dd \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2", + "get_sources": "\u05d4\u05d0\u05dd \u05dc\u05d0\u05d7\u05d6\u05e8 \u05d0\u05ea \u05d4\u05d9\u05d9\u05e9\u05d5\u05de\u05d9\u05dd \u05d4\u05e4\u05d5\u05e2\u05dc\u05d9\u05dd \u05db\u05e8\u05e9\u05d9\u05de\u05ea \u05d4\u05de\u05e7\u05d5\u05e8\u05d5\u05ea", + "screencap": "\u05e7\u05d5\u05d1\u05e2 \u05d0\u05dd \u05d9\u05e9 \u05dc\u05de\u05e9\u05d5\u05da \u05d0\u05ea \u05ea\u05de\u05d5\u05e0\u05ea \u05d4\u05d0\u05dc\u05d1\u05d5\u05dd \u05de\u05de\u05d4 \u05e9\u05de\u05d5\u05e6\u05d2 \u05e2\u05dc \u05d4\u05de\u05e1\u05da", + "state_detection_rules": "\u05e7\u05d1\u05d9\u05e2\u05ea \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05db\u05dc\u05dc\u05d9 \u05d6\u05d9\u05d4\u05d5\u05d9 \u05de\u05e6\u05d1\u05d9\u05dd", + "turn_off_command": "\u05e4\u05e7\u05d5\u05d3\u05ea \u05de\u05e2\u05d8\u05e4\u05ea ADB \u05db\u05d3\u05d9 \u05dc\u05e2\u05e7\u05d5\u05e3 \u05d0\u05ea \u05e4\u05e7\u05d5\u05d3\u05ea \u05d1\u05e8\u05d9\u05e8\u05ea \u05d4\u05de\u05d7\u05d3\u05dc turn_off", + "turn_on_command": "\u05e4\u05e7\u05d5\u05d3\u05ea \u05de\u05e2\u05d8\u05e4\u05ea ADB \u05db\u05d3\u05d9 \u05dc\u05e2\u05e7\u05d5\u05e3 \u05d0\u05ea \u05e4\u05e7\u05d5\u05d3\u05ea \u05d1\u05e8\u05d9\u05e8\u05ea \u05d4\u05de\u05d7\u05d3\u05dc turn_on" + }, + "title": "\u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05d8\u05dc\u05d5\u05d5\u05d9\u05d6\u05d9\u05ea \u05d0\u05e0\u05d3\u05e8\u05d5\u05d0\u05d9\u05d3" + }, + "rules": { + "data": { + "rule_delete": "\u05e1\u05de\u05df \u05db\u05d3\u05d9 \u05dc\u05de\u05d7\u05d5\u05e7 \u05db\u05dc\u05dc \u05d6\u05d4", + "rule_id": "\u05de\u05d6\u05d4\u05d4 \u05d9\u05d9\u05e9\u05d5\u05dd", + "rule_values": "\u05e8\u05e9\u05d9\u05de\u05ea \u05db\u05dc\u05dc\u05d9 \u05d6\u05d9\u05d4\u05d5\u05d9 \u05de\u05e6\u05d1\u05d9\u05dd (\u05e8\u05d0\u05d4 \u05ea\u05d9\u05e2\u05d5\u05d3)" + }, + "description": "\u05e7\u05d1\u05d9\u05e2\u05ea \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05db\u05dc\u05dc \u05d6\u05d9\u05d4\u05d5\u05d9 \u05e2\u05d1\u05d5\u05e8 \u05de\u05d6\u05d4\u05d4 \u05d4\u05d9\u05d9\u05e9\u05d5\u05dd {rule_id}", + "title": "\u05e7\u05d1\u05d9\u05e2\u05ea \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05db\u05dc\u05dc\u05d9 \u05d6\u05d9\u05d4\u05d5\u05d9 \u05de\u05e6\u05d1 \u05d8\u05dc\u05d5\u05d5\u05d9\u05d6\u05d9\u05ea \u05d0\u05e0\u05d3\u05e8\u05d5\u05d0\u05d9\u05d3" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/androidtv/translations/hu.json b/homeassistant/components/androidtv/translations/hu.json new file mode 100644 index 00000000000..c8d8c0fd97b --- /dev/null +++ b/homeassistant/components/androidtv/translations/hu.json @@ -0,0 +1,66 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "invalid_unique_id": "Lehetetlen \u00e9rv\u00e9nyes egyedi azonos\u00edt\u00f3t meghat\u00e1rozni az eszk\u00f6zh\u00f6z" + }, + "error": { + "adbkey_not_file": "ADB kulcsf\u00e1jl nem tal\u00e1lhat\u00f3", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_host": "\u00c9rv\u00e9nytelen hosztn\u00e9v vagy IP-c\u00edm", + "key_and_server": "Csak ADB-kulcsot vagy ADB-kiszolg\u00e1l\u00f3t adjon meg", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "adb_server_ip": "Az ADB szerver IP-c\u00edme (ha nem szeretn\u00e9 haszn\u00e1lni, hagyja \u00fcresen)", + "adb_server_port": "Az ADB szerver portja", + "adbkey": "Az ADB-kulcsf\u00e1jl el\u00e9r\u00e9si \u00fatja (az automatikus gener\u00e1l\u00e1shoz hagyja \u00fcresen)", + "device_class": "Az eszk\u00f6z t\u00edpusa", + "host": "C\u00edm", + "port": "Port" + }, + "description": "Az Android TV k\u00e9sz\u00fcl\u00e9khez val\u00f3 csatlakoz\u00e1shoz sz\u00fcks\u00e9ges param\u00e9terek be\u00e1ll\u00edt\u00e1sa", + "title": "Android TV" + } + } + }, + "options": { + "error": { + "invalid_det_rules": "\u00c9rv\u00e9nytelen \u00e1llapotfelismer\u00e9si szab\u00e1lyok" + }, + "step": { + "apps": { + "data": { + "app_delete": "Jel\u00f6lje be az alkalmaz\u00e1s t\u00f6rl\u00e9s\u00e9hez", + "app_id": "Alkalmaz\u00e1s azonos\u00edt\u00f3ja", + "app_name": "Alkalmaz\u00e1s neve" + }, + "description": "Alkalmaz\u00e1sazonos\u00edt\u00f3 konfigur\u00e1l\u00e1sa: {app_id}", + "title": "Android TV alkalmaz\u00e1sok konfigur\u00e1l\u00e1sa" + }, + "init": { + "data": { + "apps": "Alkalmaz\u00e1slista konfigur\u00e1l\u00e1sa", + "exclude_unnamed_apps": "Ismeretlen nev\u0171 alkalmaz\u00e1s kiz\u00e1r\u00e1sa", + "get_sources": "A fut\u00f3 alkalmaz\u00e1sok forr\u00e1slist\u00e1jak\u00e9nt val\u00f3 megjelen\u00edt\u00e9se", + "screencap": "A k\u00e9perny\u0151n megjelen\u0151 k\u00e9p legyen-e az albumbor\u00edt\u00f3", + "state_detection_rules": "\u00c1llapotfelismer\u00e9si szab\u00e1lyok konfigur\u00e1l\u00e1sa", + "turn_off_command": "ADB shell parancs az alap\u00e9rtelmezett kikapcsol\u00e1si (turn_off) parancs fel\u00fcl\u00edr\u00e1s\u00e1ra", + "turn_on_command": "ADB shell parancs az alap\u00e9rtelmezett bekapcsol\u00e1si (turn_on) parancs fel\u00fcl\u00edr\u00e1s\u00e1ra" + }, + "title": "Android TV be\u00e1ll\u00edt\u00e1sok" + }, + "rules": { + "data": { + "rule_delete": "Jel\u00f6lje be a szab\u00e1ly t\u00f6rl\u00e9s\u00e9hez", + "rule_id": "Alkalmaz\u00e1s azonos\u00edt\u00f3ja", + "rule_values": "Az \u00e1llapotfelismer\u0151 szab\u00e1lyok list\u00e1ja (l\u00e1sd a dokument\u00e1ci\u00f3t)" + }, + "description": "\u00c1llapotfelismer\u00e9si szab\u00e1ly konfigur\u00e1l\u00e1sa: {rule_id}", + "title": "Az Android TV \u00e1llapotfelismer\u00e9si szab\u00e1lyainak konfigur\u00e1l\u00e1sa" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/androidtv/translations/it.json b/homeassistant/components/androidtv/translations/it.json new file mode 100644 index 00000000000..c1bee374a79 --- /dev/null +++ b/homeassistant/components/androidtv/translations/it.json @@ -0,0 +1,66 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "invalid_unique_id": "Impossibile determinare un ID univoco valido per il dispositivo" + }, + "error": { + "adbkey_not_file": "File della chiave ADB non trovato", + "cannot_connect": "Impossibile connettersi", + "invalid_host": "Nome host o indirizzo IP non valido", + "key_and_server": "Fornisci solo chiave ADB o il server ADB", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "adb_server_ip": "Indirizzo IP del server ADB (lascia vuoto per non utilizzarlo)", + "adb_server_port": "Porta del server ADB", + "adbkey": "Percorso del file chiave ADB (lascia vuoto per generare automaticamente)", + "device_class": "Il tipo di dispositivo", + "host": "Host", + "port": "Porta" + }, + "description": "Imposta i parametri richiesti per connetterti al tuo dispositivo Android TV", + "title": "Android TV" + } + } + }, + "options": { + "error": { + "invalid_det_rules": "Regole di rilevamento dello stato non valide" + }, + "step": { + "apps": { + "data": { + "app_delete": "Marca per eliminare questa applicazione", + "app_id": "ID applicazione", + "app_name": "Nome applicazione" + }, + "description": "Configura ID applicazione {app_id}", + "title": "Configura le applicazioni Android TV" + }, + "init": { + "data": { + "apps": "Configura l'elenco delle applicazioni", + "exclude_unnamed_apps": "Escludi app con nome sconosciuto", + "get_sources": "Decidi se recuperare o meno le app in esecuzione come elenco di fonti", + "screencap": "Determina se la copertina dell'album deve essere estratta da ci\u00f2 che viene mostrato sullo schermo", + "state_detection_rules": "Configura le regole di rilevamento dello stato", + "turn_off_command": "Comando shell ADB per sovrascrivere il comando turn_off predefinito", + "turn_on_command": "Comando shell ADB per sovrascrivere il comando turn_on predefinito" + }, + "title": "Opzioni Android TV" + }, + "rules": { + "data": { + "rule_delete": "Marca per eliminare questa regola", + "rule_id": "ID applicazione", + "rule_values": "Elenco delle regole di rilevamento dello stato (vedi documentazione)" + }, + "description": "Configura la regola di rilevamento per l'ID applicazione {rule_id}", + "title": "Configura le regole di rilevamento dello stato di Android TV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/androidtv/translations/ja.json b/homeassistant/components/androidtv/translations/ja.json new file mode 100644 index 00000000000..f3139df9819 --- /dev/null +++ b/homeassistant/components/androidtv/translations/ja.json @@ -0,0 +1,66 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "invalid_unique_id": "\u30c7\u30d0\u30a4\u30b9\u306e\u6709\u52b9\u306a\u30e6\u30cb\u30fc\u30af(\u4e00\u610f)ID\u3092\u6c7a\u5b9a\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u305b\u3093" + }, + "error": { + "adbkey_not_file": "ADB\u30ad\u30fc\u30d5\u30a1\u30a4\u30eb\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_host": "\u7121\u52b9\u306a\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9", + "key_and_server": "ADB\u30ad\u30fc\u307e\u305f\u306fADB\u30b5\u30fc\u30d0\u30fc\u306e\u307f\u3092\u63d0\u4f9b\u3059\u308b", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "user": { + "data": { + "adb_server_ip": "ADB\u30b5\u30fc\u30d0\u30fc\u306eIP\u30a2\u30c9\u30ec\u30b9(\u4f7f\u7528\u3057\u306a\u3044\u5834\u5408\u306f\u7a7a\u306e\u307e\u307e\u306b\u3057\u3066\u304f\u3060\u3055\u3044)", + "adb_server_port": "ADB\u30b5\u30fc\u30d0\u30fc\u306e\u30dd\u30fc\u30c8", + "adbkey": "ADB\u30ad\u30fc\u30d5\u30a1\u30a4\u30eb\u3078\u306e\u30d1\u30b9(\u7a7a\u306b\u3059\u308b\u3068\u81ea\u52d5\u751f\u6210\u3055\u308c\u307e\u3059)", + "device_class": "\u30c7\u30d0\u30a4\u30b9\u306e\u7a2e\u985e", + "host": "\u30db\u30b9\u30c8", + "port": "\u30dd\u30fc\u30c8" + }, + "description": "Android TV\u30c7\u30d0\u30a4\u30b9\u306b\u63a5\u7d9a\u3059\u308b\u305f\u3081\u306b\u5fc5\u8981\u306a\u30d1\u30e9\u30e1\u30fc\u30bf\u30fc\u3092\u8a2d\u5b9a\u3057\u307e\u3059", + "title": "Android TV" + } + } + }, + "options": { + "error": { + "invalid_det_rules": "\u7121\u52b9\u306a\u72b6\u614b\u691c\u51fa\u30eb\u30fc\u30eb" + }, + "step": { + "apps": { + "data": { + "app_delete": "\u3053\u306e\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u524a\u9664\u3059\u308b\u306b\u306f\u3001\u30c1\u30a7\u30c3\u30af\u3092\u5165\u308c\u307e\u3059", + "app_id": "\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3ID", + "app_name": "\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u540d" + }, + "description": "\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3ID {app_id} \u306e\u8a2d\u5b9a", + "title": "Android TV\u30a2\u30d7\u30ea\u306e\u8a2d\u5b9a" + }, + "init": { + "data": { + "apps": "\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u30ea\u30b9\u30c8\u306e\u8a2d\u5b9a", + "exclude_unnamed_apps": "\u540d\u524d\u304c\u4e0d\u660e\u306a\u30a2\u30d7\u30ea\u3092\u9664\u5916\u3059\u308b", + "get_sources": "\u5b9f\u884c\u4e2d\u306e\u30a2\u30d7\u30ea\u3092\u30bd\u30fc\u30b9\u306e\u30ea\u30b9\u30c8\u3068\u3057\u3066\u53d6\u5f97\u3059\u308b\u304b\u3069\u3046\u304b", + "screencap": "\u753b\u9762\u306b\u8868\u793a\u4e2d\u306e\u3082\u306e\u304b\u3089\u3001\u30a2\u30eb\u30d0\u30e0\u30a2\u30fc\u30c8\u3092\u62bd\u51fa\u3059\u308b\u304b\u3069\u3046\u304b\u3092\u6c7a\u5b9a\u3057\u307e\u3059", + "state_detection_rules": "\u72b6\u614b\u691c\u51fa\u30eb\u30fc\u30eb\u3092\u8a2d\u5b9a", + "turn_off_command": "\u30c7\u30d5\u30a9\u30eb\u30c8\u306eturn_off\u30b3\u30de\u30f3\u30c9\u3092\u4e0a\u66f8\u304d\u3059\u308bADB\u30b7\u30a7\u30eb\u30b3\u30de\u30f3\u30c9", + "turn_on_command": "\u30c7\u30d5\u30a9\u30eb\u30c8\u306eturn_on\u30b3\u30de\u30f3\u30c9\u3092\u4e0a\u66f8\u304d\u3059\u308bADB\u30b7\u30a7\u30eb\u30b3\u30de\u30f3\u30c9" + }, + "title": "Android TV\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" + }, + "rules": { + "data": { + "rule_delete": "\u3053\u306e\u30eb\u30fc\u30eb\u3092\u524a\u9664\u3059\u308b\u306b\u306f\u3001\u30c1\u30a7\u30c3\u30af\u3092\u5165\u308c\u307e\u3059", + "rule_id": "\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3ID", + "rule_values": "\u72b6\u614b\u691c\u51fa\u30eb\u30fc\u30eb\u306e\u30ea\u30b9\u30c8(\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u53c2\u7167)" + }, + "description": "\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3ID {rule_id} \u306e\u691c\u51fa\u30eb\u30fc\u30eb\u3092\u69cb\u6210\u3057\u307e\u3059", + "title": "Android TV\u306e\u72b6\u614b\u691c\u51fa\u30eb\u30fc\u30eb\u3092\u8a2d\u5b9a" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/androidtv/translations/no.json b/homeassistant/components/androidtv/translations/no.json index 830ee10f756..a70de366fca 100644 --- a/homeassistant/components/androidtv/translations/no.json +++ b/homeassistant/components/androidtv/translations/no.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Enheten er allerede konfigurert", "invalid_unique_id": "Umulig \u00e5 bestemme en gyldig unik ID for enheten" }, "error": { @@ -24,5 +25,42 @@ "title": "" } } + }, + "options": { + "error": { + "invalid_det_rules": "Ugyldige regler for tilstandsgjenkjenning" + }, + "step": { + "apps": { + "data": { + "app_delete": "Merk av for \u00e5 slette denne applikasjonen", + "app_id": "Applikasjons-ID", + "app_name": "Navn p\u00e5 applikasjon" + }, + "description": "Konfigurer app-ID {app_id}", + "title": "Konfigurer Android TV-apper" + }, + "init": { + "data": { + "apps": "Konfigurer applikasjonsliste", + "exclude_unnamed_apps": "Ekskluder app med ukjent navn", + "get_sources": "Om de kj\u00f8rende appene skal hentes eller ikke som kildeliste", + "screencap": "Avgj\u00f8r om albumgrafikk skal hentes fra det som vises p\u00e5 skjermen", + "state_detection_rules": "Konfigurere regler for tilstandsgjenkjenning", + "turn_off_command": "ADB-skallkommando for \u00e5 overstyre standard turn_off-kommando", + "turn_on_command": "ADB-skallkommando for \u00e5 overstyre standard turn_on-kommando" + }, + "title": "Android TV-alternativer" + }, + "rules": { + "data": { + "rule_delete": "Merk av for \u00e5 slette denne regelen", + "rule_id": "Applikasjons-ID", + "rule_values": "Liste over regler for tilstandsgjenkjenning (se dokumentasjon)" + }, + "description": "Konfigurer gjenkjenningsregel for app-ID {rule_id}", + "title": "Konfigurer Android TV-statusgjenkjenningsregler" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/androidtv/translations/ru.json b/homeassistant/components/androidtv/translations/ru.json new file mode 100644 index 00000000000..31acebe5f30 --- /dev/null +++ b/homeassistant/components/androidtv/translations/ru.json @@ -0,0 +1,66 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "invalid_unique_id": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439 \u0443\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u044b\u0439 ID \u0434\u043b\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430." + }, + "error": { + "adbkey_not_file": "\u0424\u0430\u0439\u043b \u043a\u043b\u044e\u0447\u0430 ADB \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.", + "key_and_server": "\u041d\u0443\u0436\u043d\u043e \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043a\u043b\u044e\u0447 ADB \u0438\u043b\u0438 \u0441\u0435\u0440\u0432\u0435\u0440 ADB.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "adb_server_ip": "IP-\u0430\u0434\u0440\u0435\u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 ADB (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c, \u0447\u0442\u043e\u0431\u044b \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c)", + "adb_server_port": "\u041f\u043e\u0440\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 ADB", + "adbkey": "\u041f\u0443\u0442\u044c \u043a \u0444\u0430\u0439\u043b\u0443 \u043a\u043b\u044e\u0447\u0430 ADB (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0433\u043e \u0437\u0430\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f)", + "device_class": "\u0422\u0438\u043f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 Android TV.", + "title": "Android TV" + } + } + }, + "options": { + "error": { + "invalid_det_rules": "\u041f\u0440\u0430\u0432\u0438\u043b\u0430 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0439" + }, + "step": { + "apps": { + "data": { + "app_delete": "\u041e\u0442\u043c\u0435\u0442\u044c\u0442\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0434\u0430\u043b\u0438\u0442\u044c \u044d\u0442\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435", + "app_id": "ID \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f", + "app_name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f {app_id}", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439 Android TV" + }, + "init": { + "data": { + "apps": "\u041d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439", + "exclude_unnamed_apps": "\u0418\u0441\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0441 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u043c \u0438\u043c\u0435\u043d\u0435\u043c", + "get_sources": "\u0421\u043b\u0435\u0434\u0443\u0435\u0442 \u043b\u0438 \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c \u0437\u0430\u043f\u0443\u0449\u0435\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0432 \u0432\u0438\u0434\u0435 \u0441\u043f\u0438\u0441\u043a\u0430 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u0432", + "screencap": "\u041d\u0443\u0436\u043d\u043e \u043b\u0438 \u0438\u0437\u0432\u043b\u0435\u043a\u0430\u0442\u044c \u043e\u0431\u043b\u043e\u0436\u043a\u0443 \u0430\u043b\u044c\u0431\u043e\u043c\u0430 \u0438\u0437 \u0442\u043e\u0433\u043e, \u0447\u0442\u043e \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u0442\u0441\u044f \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435", + "state_detection_rules": "\u041d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u0440\u0430\u0432\u0438\u043b\u0430 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0439", + "turn_off_command": "\u041a\u043e\u043c\u0430\u043d\u0434\u0430 \u043e\u0431\u043e\u043b\u043e\u0447\u043a\u0438 ADB \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u043a\u043e\u043c\u0430\u043d\u0434\u044b turn_off \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e", + "turn_on_command": "\u041a\u043e\u043c\u0430\u043d\u0434\u0430 \u043e\u0431\u043e\u043b\u043e\u0447\u043a\u0438 ADB \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u043a\u043e\u043c\u0430\u043d\u0434\u044b turn_on \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Android TV" + }, + "rules": { + "data": { + "rule_delete": "\u041e\u0442\u043c\u0435\u0442\u044c\u0442\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0434\u0430\u043b\u0438\u0442\u044c \u044d\u0442\u043e \u043f\u0440\u0430\u0432\u0438\u043b\u043e", + "rule_id": "ID \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f", + "rule_values": "\u0421\u043f\u0438\u0441\u043e\u043a \u043f\u0440\u0430\u0432\u0438\u043b \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f (\u0441\u043c. \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044e)" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0440\u0430\u0432\u0438\u043b\u0430 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u0434\u043b\u044f ID \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f {rule_id}", + "title": "\u041f\u0440\u0430\u0432\u0438\u043b\u0430 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f Android TV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/androidtv/translations/zh-Hant.json b/homeassistant/components/androidtv/translations/zh-Hant.json new file mode 100644 index 00000000000..7e113048a0c --- /dev/null +++ b/homeassistant/components/androidtv/translations/zh-Hant.json @@ -0,0 +1,66 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "invalid_unique_id": "\u7121\u6cd5\u78ba\u8a8d\u88dd\u7f6e\u6709\u6548\u552f\u4e00 ID" + }, + "error": { + "adbkey_not_file": "\u627e\u4e0d\u5230 ADB \u5bc6\u9470\u6a94\u6848", + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_host": "\u7121\u6548\u4e3b\u6a5f\u540d\u7a31\u6216 IP \u4f4d\u5740", + "key_and_server": "\u50c5\u63d0\u4f9b ADB \u5bc6\u9470\u6216 ADB \u4f3a\u670d\u5668", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "adb_server_ip": "ADB \u4f3a\u670d\u5668 IP \u4f4d\u5740\uff08\u4fdd\u7559\u7a7a\u767d\u70ba\u4e0d\u4f7f\u7528\uff09", + "adb_server_port": "ADB \u4f3a\u670d\u5668\u901a\u8a0a\u57e0", + "adbkey": "ADB \u5bc6\u9470\u6a94\u6848\u8def\u5f91\uff08\u4fdd\u7559\u7a7a\u767d\u5c07\u6703\u81ea\u52d5\u7522\u751f\uff09", + "device_class": "\u88dd\u7f6e\u985e\u578b", + "host": "\u4e3b\u6a5f\u7aef", + "port": "\u901a\u8a0a\u57e0" + }, + "description": "\u8a2d\u5b9a\u9023\u7dda\u81f3 Android TV \u88dd\u7f6e\u6240\u9700\u53c3\u6578", + "title": "Android TV" + } + } + }, + "options": { + "error": { + "invalid_det_rules": "\u72c0\u614b\u5075\u6e2c\u898f\u5247\u7121\u6548" + }, + "step": { + "apps": { + "data": { + "app_delete": "\u6aa2\u67e5\u4ee5\u522a\u9664\u6b64\u61c9\u7528\u7a0b\u5f0f", + "app_id": "\u61c9\u7528\u7a0b\u5f0f ID", + "app_name": "\u61c9\u7528\u7a0b\u5f0f\u540d\u7a31" + }, + "description": "\u8a2d\u5b9a\u61c9\u7528\u7a0b\u5f0f ID {app_id}", + "title": "\u8a2d\u5b9a Android TV App" + }, + "init": { + "data": { + "apps": "\u8a2d\u5b9a\u61c9\u7528\u7a0b\u5f0f\u5217\u8868", + "exclude_unnamed_apps": "\u6392\u9664\u672a\u77e5\u540d\u7a31\u61c9\u7528\u7a0b\u5f0f", + "get_sources": "\u662f\u5426\u7531\u4f86\u6e90\u5217\u8868\u53d6\u5f97\u57f7\u884c\u7a0b\u5f0f\u5217\u8868", + "screencap": "\u6c7a\u5b9a\u662f\u5426\u7531\u756b\u9762\u4e0a\u986f\u793a\u5167\u5bb9\u64f7\u53d6\u5c08\u8f2f\u5c01\u9762", + "state_detection_rules": "\u8a2d\u5b9a\u72c0\u614b\u5075\u6e2c\u898f\u5247", + "turn_off_command": "ADB shell \u6307\u4ee4\u4ee5\u8986\u5beb\u9810\u8a2d turn_off \u6307\u4ee4", + "turn_on_command": "ADB shell \u6307\u4ee4\u4ee5\u8986\u5beb\u9810\u8a2d turn_on \u6307\u4ee4" + }, + "title": "Android TV \u9078\u9805" + }, + "rules": { + "data": { + "rule_delete": "\u6aa2\u67e5\u4ee5\u522a\u9664\u6b64\u898f\u5247", + "rule_id": "\u61c9\u7528\u7a0b\u5f0f ID", + "rule_values": "\u72c0\u614b\u5075\u6e2c\u898f\u5247\u5217\u8868\uff08\u8acb\u53c3\u95b1\u6587\u4ef6\uff09" + }, + "description": "\u8a2d\u5b9a\u61c9\u7528\u7a0b\u5f0f ID {rule_id} \u5075\u6e2c\u898f\u5247", + "title": "\u8a2d\u5b9a Android TV \u72c0\u614b\u5075\u6e2c\u898f\u5247" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/it.json b/homeassistant/components/apple_tv/translations/it.json index efee1a1395f..47bd8612265 100644 --- a/homeassistant/components/apple_tv/translations/it.json +++ b/homeassistant/components/apple_tv/translations/it.json @@ -69,7 +69,7 @@ "data": { "start_off": "Non accendere il dispositivo all'avvio di Home Assistant" }, - "description": "Configurare le impostazioni generali del dispositivo" + "description": "Configura le impostazioni generali del dispositivo" } } }, diff --git a/homeassistant/components/auth/translations/it.json b/homeassistant/components/auth/translations/it.json index 34d404f0bc6..e8091faf757 100644 --- a/homeassistant/components/auth/translations/it.json +++ b/homeassistant/components/auth/translations/it.json @@ -5,27 +5,27 @@ "no_available_service": "Nessun servizio di notifica disponibile." }, "error": { - "invalid_code": "Codice non valido, per favore riprovare." + "invalid_code": "Codice non valido, riprova." }, "step": { "init": { - "description": "Selezionare uno dei servizi di notifica:", + "description": "Seleziona uno dei servizi di notifica:", "title": "Imposta la password monouso fornita dal componente di notifica" }, "setup": { - "description": "\u00c8 stata inviata una password monouso tramite **notify.{notify_service}**. Per favore, inseriscila qui sotto:", + "description": "\u00c8 stata inviata una password monouso tramite **notify.{notify_service}**. Inseriscila qui sotto:", "title": "Verifica l'installazione" } }, - "title": "Notifica la Password monouso" + "title": "Notifica la password monouso" }, "totp": { "error": { - "invalid_code": "Codice non valido, per favore riprovare. Se riscontri spesso questo errore, assicurati che l'orologio del sistema Home Assistant sia accurato." + "invalid_code": "Codice non valido, riprova. Se riscontri spesso questo errore, assicurati che l'orologio del sistema Home Assistant sia accurato." }, "step": { "init": { - "description": "Per attivare l'autenticazione a due fattori utilizzando le password monouso basate sul tempo, eseguire la scansione del codice QR con l'app di autenticazione. Se non ne hai uno, ti consigliamo [Google Authenticator](https://support.google.com/accounts/answer/1066447) o [Authy](https://authy.com/). \n\n {qr_code} \n \nDopo la scansione, inserisci il codice a sei cifre dalla tua app per verificare la configurazione. Se riscontri problemi con la scansione del codice QR, esegui una configurazione manuale con il codice **`{code}`**.", + "description": "Per attivare l'autenticazione a due fattori utilizzando le password monouso basate sul tempo, esegui la scansione del codice QR con l'app di autenticazione. Se non ne hai uno, ti consigliamo [Google Authenticator](https://support.google.com/accounts/answer/1066447) o [Authy](https://authy.com/). \n\n {qr_code} \n \nDopo la scansione, inserisci il codice a sei cifre dalla tua app per verificare la configurazione. Se riscontri problemi con la scansione del codice QR, esegui una configurazione manuale con il codice **`{code}`**.", "title": "Imposta l'autenticazione a due fattori usando TOTP" } }, diff --git a/homeassistant/components/axis/translations/it.json b/homeassistant/components/axis/translations/it.json index 7e7aeb1d1b2..6bf0c73d403 100644 --- a/homeassistant/components/axis/translations/it.json +++ b/homeassistant/components/axis/translations/it.json @@ -28,7 +28,7 @@ "step": { "configure_stream": { "data": { - "stream_profile": "Selezionare il profilo di flusso da utilizzare" + "stream_profile": "Seleziona il profilo di flusso da utilizzare" }, "title": "Opzioni del flusso video del dispositivo Axis" } diff --git a/homeassistant/components/azure_devops/translations/it.json b/homeassistant/components/azure_devops/translations/it.json index 673eaca5475..a39cad3ce8e 100644 --- a/homeassistant/components/azure_devops/translations/it.json +++ b/homeassistant/components/azure_devops/translations/it.json @@ -24,7 +24,7 @@ "personal_access_token": "Token di Accesso Personale (PAT)", "project": "Progetto" }, - "description": "Configurare un'istanza di DevOps di Azure per accedere al progetto. Un Token di Accesso Personale (PAT) \u00e8 richiesto solo per un progetto privato.", + "description": "Configura un'istanza di DevOps di Azure per accedere al progetto. Un Token di Accesso Personale (PAT) \u00e8 richiesto solo per un progetto privato.", "title": "Aggiungere un progetto Azure DevOps" } } diff --git a/homeassistant/components/azure_event_hub/translations/it.json b/homeassistant/components/azure_event_hub/translations/it.json index edf1770ef92..d0459ffadd2 100644 --- a/homeassistant/components/azure_event_hub/translations/it.json +++ b/homeassistant/components/azure_event_hub/translations/it.json @@ -2,9 +2,9 @@ "config": { "abort": { "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", - "cannot_connect": "Connessione con le credenziali da configuration.yaml non riuscita, rimuovere da yaml e utilizzare il flusso di configurazione.", + "cannot_connect": "Connessione con le credenziali da configuration.yaml non riuscita, rimuovi da yaml e utilizza il flusso di configurazione.", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione.", - "unknown": "La connessione con le credenziali da configuration.yaml non \u00e8 riuscita con un errore sconosciuto, rimuovere da yaml e utilizzare il flusso di configurazione." + "unknown": "La connessione con le credenziali da configuration.yaml non \u00e8 riuscita con un errore sconosciuto, rimuovi da yaml e utilizza il flusso di configurazione." }, "error": { "cannot_connect": "Impossibile connettersi", diff --git a/homeassistant/components/azure_event_hub/translations/no.json b/homeassistant/components/azure_event_hub/translations/no.json index a22f7eef3d6..4c767a46076 100644 --- a/homeassistant/components/azure_event_hub/translations/no.json +++ b/homeassistant/components/azure_event_hub/translations/no.json @@ -1,7 +1,49 @@ { "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert", + "cannot_connect": "Tilkobling med p\u00e5loggingsinformasjonen fra configuration.yaml mislyktes, vennligst fjern fra yaml og bruk konfigurasjonsflyten.", + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.", + "unknown": "Tilkobling med p\u00e5loggingsinformasjonen fra configuration.yaml mislyktes med en ukjent feil, vennligst fjern fra yaml og bruk konfigurasjonsflyten." + }, "error": { + "cannot_connect": "Tilkobling mislyktes", "unknown": "Uventet feil" + }, + "step": { + "conn_string": { + "data": { + "event_hub_connection_string": "Event Hub Connection String" + }, + "description": "Vennligst skriv inn tilkoblingsstrengen for: {event_hub_instance_name}", + "title": "Metode for tilkoblingsstreng" + }, + "sas": { + "data": { + "event_hub_namespace": "Event Hub-navneomr\u00e5de", + "event_hub_sas_key": "Event Hub SAS-n\u00f8kkel", + "event_hub_sas_policy": "Event Hub SAS-policy" + }, + "description": "Vennligst skriv inn SAS-legitimasjonen (delt tilgangssignatur) for: {event_hub_instance_name}", + "title": "SAS Credentials-metoden" + }, + "user": { + "data": { + "event_hub_instance_name": "Event Hub-forekomstnavn", + "use_connection_string": "Bruk tilkoblingsstreng" + }, + "title": "Konfigurer din Azure Event Hub-integrasjon" + } + } + }, + "options": { + "step": { + "options": { + "data": { + "send_interval": "Intervall mellom sending av batcher til huben." + }, + "title": "Alternativer for Azure Event Hub." + } } } } \ No newline at end of file diff --git a/homeassistant/components/blink/translations/it.json b/homeassistant/components/blink/translations/it.json index 6dee5d9c02f..5e052c2d95e 100644 --- a/homeassistant/components/blink/translations/it.json +++ b/homeassistant/components/blink/translations/it.json @@ -32,7 +32,7 @@ "data": { "scan_interval": "Intervallo di scansione (secondi)" }, - "description": "Configurare l'integrazione di Blink", + "description": "Configura l'integrazione di Blink", "title": "Opzioni di Blink" } } diff --git a/homeassistant/components/bmw_connected_drive/translations/it.json b/homeassistant/components/bmw_connected_drive/translations/it.json index 277ed189c43..eeca1909ea7 100644 --- a/homeassistant/components/bmw_connected_drive/translations/it.json +++ b/homeassistant/components/bmw_connected_drive/translations/it.json @@ -22,7 +22,7 @@ "account_options": { "data": { "read_only": "Sola lettura (solo sensori e notifica, nessuna esecuzione di servizi, nessun blocco)", - "use_location": "Usa la posizione di Home Assistant per richieste sulla posizione dell'auto (richiesto per veicoli non i3/i8 prodotti prima del 7/2014)" + "use_location": "Usa la posizione di Home Assistant per le richieste di posizione dell'auto (richiesto per veicoli non i3/i8 prodotti prima del 7/2014)" } } } diff --git a/homeassistant/components/bond/translations/it.json b/homeassistant/components/bond/translations/it.json index b6c628abf63..843aa577654 100644 --- a/homeassistant/components/bond/translations/it.json +++ b/homeassistant/components/bond/translations/it.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida", - "old_firmware": "Firmware precedente non supportato sul dispositivo Bond - si prega di aggiornare prima di continuare", + "old_firmware": "Firmware precedente non supportato sul dispositivo Bond - aggiorna prima di continuare", "unknown": "Errore imprevisto" }, "flow_title": "{name} ({host})", diff --git a/homeassistant/components/bosch_shc/translations/it.json b/homeassistant/components/bosch_shc/translations/it.json index c96ea617bf8..80acf92440d 100644 --- a/homeassistant/components/bosch_shc/translations/it.json +++ b/homeassistant/components/bosch_shc/translations/it.json @@ -14,7 +14,7 @@ "flow_title": "Bosch SHC: {name}", "step": { "confirm_discovery": { - "description": "Premere il pulsante sul lato anteriore del controller Bosch Smart Home finch\u00e9 il LED non inizia a lampeggiare.\nPronto per continuare a configurare {model} @ {host} con Home Assistant?" + "description": "Premi il pulsante sul lato anteriore del controller Bosch Smart Home finch\u00e9 il LED non inizia a lampeggiare.\nSei pronto per continuare a configurare {model} @ {host} con Home Assistant?" }, "credentials": { "data": { diff --git a/homeassistant/components/braviatv/translations/it.json b/homeassistant/components/braviatv/translations/it.json index 95fcfc7622b..3e5f0a7aa07 100644 --- a/homeassistant/components/braviatv/translations/it.json +++ b/homeassistant/components/braviatv/translations/it.json @@ -14,14 +14,14 @@ "data": { "pin": "Codice PIN" }, - "description": "Immettere il codice PIN visualizzato sul Sony Bravia TV. \n\nSe il codice PIN non viene visualizzato, \u00e8 necessario annullare la registrazione di Home Assistant sul televisore, andare su: Impostazioni - > Rete - > Impostazioni dispositivo remoto - > Annulla registrazione dispositivo remoto.", - "title": "Autorizzare Sony Bravia TV" + "description": "Immettere il codice PIN visualizzato sul Sony Bravia TV. \n\nSe il codice PIN non viene visualizzato, \u00e8 necessario annullare la registrazione di Home Assistant sul televisore, vai su: Impostazioni - > Rete - > Impostazioni dispositivo remoto - > Annulla registrazione dispositivo remoto.", + "title": "Autorizza Sony Bravia TV" }, "user": { "data": { "host": "Host" }, - "description": "Configurare l'integrazione TV di Sony Bravia. In caso di problemi con la configurazione visitare: https://www.home-assistant.io/integrations/braviatv\n\nAssicurarsi che il televisore sia acceso.", + "description": "Configura l'integrazione TV di Sony Bravia. In caso di problemi con la configurazione visitare: https://www.home-assistant.io/integrations/braviatv\n\nAssicurarsi che il televisore sia acceso.", "title": "Sony Bravia TV" } } diff --git a/homeassistant/components/broadlink/translations/it.json b/homeassistant/components/broadlink/translations/it.json index 3dcc07db87c..fb3d7b8583e 100644 --- a/homeassistant/components/broadlink/translations/it.json +++ b/homeassistant/components/broadlink/translations/it.json @@ -16,7 +16,7 @@ "flow_title": "{name} ({model} presso {host})", "step": { "auth": { - "title": "Eseguire l'autenticazione al dispositivo" + "title": "Esegui l'autenticazione al dispositivo" }, "finish": { "data": { @@ -25,8 +25,8 @@ "title": "Scegliere un nome per il dispositivo" }, "reset": { - "description": "{name} ( {model} su {host} ) \u00e8 bloccato. \u00c8 necessario sbloccare il dispositivo per autenticarsi e completare la configurazione. Istruzioni:\n 1. Apri l'app Broadlink.\n 2. Fare clic sul dispositivo.\n 3. Fare clic su \"...\" in alto a destra.\n 4. Scorri fino in fondo alla pagina.\n 5. Disabilitare il blocco.", - "title": "Sbloccare il dispositivo" + "description": "{name} ( {model} su {host} ) \u00e8 bloccato. Devi sbloccare il dispositivo per autenticarti e completare la configurazione. Istruzioni:\n 1. Apri l'app Broadlink.\n 2. Fai clic sul dispositivo.\n 3. Fai clic su \"...\" in alto a destra.\n 4. Scorri fino in fondo alla pagina.\n 5. Disabilita il blocco.", + "title": "Sblocca il dispositivo" }, "unlock": { "data": { diff --git a/homeassistant/components/brother/translations/it.json b/homeassistant/components/brother/translations/it.json index 0d4af015524..29059723159 100644 --- a/homeassistant/components/brother/translations/it.json +++ b/homeassistant/components/brother/translations/it.json @@ -16,7 +16,7 @@ "host": "Host", "type": "Tipo di stampante" }, - "description": "Configurare l'integrazione della stampante Brother. In caso di problemi con la configurazione, visitare: https://www.home-assistant.io/integrations/brother" + "description": "Configura l'integrazione della stampante Brother. In caso di problemi con la configurazione, visita: https://www.home-assistant.io/integrations/brother" }, "zeroconf_confirm": { "data": { diff --git a/homeassistant/components/cert_expiry/translations/it.json b/homeassistant/components/cert_expiry/translations/it.json index 232ee010221..4904f7afe3a 100644 --- a/homeassistant/components/cert_expiry/translations/it.json +++ b/homeassistant/components/cert_expiry/translations/it.json @@ -16,7 +16,7 @@ "name": "Il nome del certificato", "port": "Porta" }, - "title": "Definire il certificato da testare" + "title": "Definire il certificato da provare" } } }, diff --git a/homeassistant/components/cloudflare/translations/it.json b/homeassistant/components/cloudflare/translations/it.json index db478015dfa..c567b1c8978 100644 --- a/homeassistant/components/cloudflare/translations/it.json +++ b/homeassistant/components/cloudflare/translations/it.json @@ -22,7 +22,7 @@ "data": { "records": "Record" }, - "title": "Scegliere i record da aggiornare" + "title": "Scegli i record da aggiornare" }, "user": { "data": { @@ -35,7 +35,7 @@ "data": { "zone": "Zona" }, - "title": "Scegliere la zona da aggiornare" + "title": "Scegli la zona da aggiornare" } } } diff --git a/homeassistant/components/coinbase/translations/it.json b/homeassistant/components/coinbase/translations/it.json index 117521b1933..dcbb2317f37 100644 --- a/homeassistant/components/coinbase/translations/it.json +++ b/homeassistant/components/coinbase/translations/it.json @@ -34,7 +34,7 @@ "exchange_base": "Valuta di base per i sensori di tasso di cambio.", "exchange_rate_currencies": "Tassi di cambio da segnalare." }, - "description": "Regolare le opzioni di Coinbase" + "description": "Regola le opzioni di Coinbase" } } } diff --git a/homeassistant/components/configurator/translations/it.json b/homeassistant/components/configurator/translations/it.json index b8610b76d9d..3e17f84d1c8 100644 --- a/homeassistant/components/configurator/translations/it.json +++ b/homeassistant/components/configurator/translations/it.json @@ -1,7 +1,7 @@ { "state": { "_": { - "configure": "Configurare", + "configure": "Configura", "configured": "Configurato" } }, diff --git a/homeassistant/components/coolmaster/translations/it.json b/homeassistant/components/coolmaster/translations/it.json index 9e1342257fb..2a0cf07f5ed 100644 --- a/homeassistant/components/coolmaster/translations/it.json +++ b/homeassistant/components/coolmaster/translations/it.json @@ -15,7 +15,7 @@ "host": "Host", "off": "Pu\u00f2 essere spento" }, - "title": "Impostare i dettagli della connessione CoolMasterNet." + "title": "Imposta i dettagli della connessione CoolMasterNet." } } } diff --git a/homeassistant/components/coronavirus/translations/it.json b/homeassistant/components/coronavirus/translations/it.json index fb682589334..c429a070e34 100644 --- a/homeassistant/components/coronavirus/translations/it.json +++ b/homeassistant/components/coronavirus/translations/it.json @@ -9,7 +9,7 @@ "data": { "country": "Nazione" }, - "title": "Scegliere una Nazione da monitorare" + "title": "Scegli una nazione da monitorare" } } } diff --git a/homeassistant/components/daikin/translations/it.json b/homeassistant/components/daikin/translations/it.json index 39b3451467f..8dfa0380d81 100644 --- a/homeassistant/components/daikin/translations/it.json +++ b/homeassistant/components/daikin/translations/it.json @@ -17,7 +17,7 @@ "host": "Host", "password": "Password" }, - "description": "Inserisci l'Indirizzo IP del tuo condizionatore d'aria Daikin.\n\nNota che solo la Chiave API e la Password sono rispettivamente usati dai dispositivi BRP072Cxx e SKYFi.", + "description": "Inserisci l'indirizzo IP del tuo condizionatore d'aria Daikin.\n\nNota che solo la chiave API e la password sono rispettivamente usati dai dispositivi BRP072Cxx e SKYFi.", "title": "Configura Daikin AC" } } diff --git a/homeassistant/components/deconz/translations/it.json b/homeassistant/components/deconz/translations/it.json index 09f93e35911..e6c423c18b4 100644 --- a/homeassistant/components/deconz/translations/it.json +++ b/homeassistant/components/deconz/translations/it.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Il Bridge \u00e8 gi\u00e0 configurato", + "already_configured": "Il bridge \u00e8 gi\u00e0 configurato", "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", "no_bridges": "Nessun bridge deCONZ rilevato", "no_hardware_available": "Nessun hardware radio collegato a deCONZ", @@ -48,7 +48,7 @@ "button_8": "Ottavo pulsante", "close": "Chiudere", "dim_down": "Diminuire luminosit\u00e0", - "dim_up": "Aumentare luminosit\u00e0", + "dim_up": "Aumenta luminosit\u00e0", "left": "Sinistra", "open": "Aperto", "right": "Destra", @@ -101,7 +101,7 @@ "allow_deconz_groups": "Consentire gruppi luce deCONZ", "allow_new_devices": "Consentire l'aggiunta automatica di nuovi dispositivi" }, - "description": "Configurare la visibilit\u00e0 dei tipi di dispositivi deCONZ", + "description": "Configura la visibilit\u00e0 dei tipi di dispositivi deCONZ", "title": "Opzioni deCONZ" } } diff --git a/homeassistant/components/demo/translations/it.json b/homeassistant/components/demo/translations/it.json index 27a068a1918..0fb3f52b916 100644 --- a/homeassistant/components/demo/translations/it.json +++ b/homeassistant/components/demo/translations/it.json @@ -17,7 +17,7 @@ "options_2": { "data": { "multi": "Selezione multipla", - "select": "Selezionare un'opzione", + "select": "Seleziona un'opzione", "string": "Valore stringa" } } diff --git a/homeassistant/components/denonavr/translations/it.json b/homeassistant/components/denonavr/translations/it.json index 86e5bb309b9..76d0d627ca3 100644 --- a/homeassistant/components/denonavr/translations/it.json +++ b/homeassistant/components/denonavr/translations/it.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", - "cannot_connect": "Impossibile connettersi, riprovare, scollegare l'alimentazione della rete elettrica e i cavi ethernet e ricollegarli potrebbe essere d'aiuto", + "cannot_connect": "Impossibile connettersi, riprova, scollega l'alimentazione della rete elettrica e i cavi ethernet e ricollegali potrebbe essere d'aiuto", "not_denonavr_manufacturer": "Non \u00e8 un ricevitore di rete Denon AVR, il produttore rilevato non corrisponde", "not_denonavr_missing": "Non \u00e8 un ricevitore di rete Denon AVR, le informazioni di rilevamento non sono complete" }, @@ -13,21 +13,21 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Si prega di confermare l'aggiunta del ricevitore", + "description": "Conferma l'aggiunta del ricevitore", "title": "Ricevitori di rete Denon AVR" }, "select": { "data": { "select_host": "Indirizzo IP del ricevitore" }, - "description": "Eseguire nuovamente il setup se si desidera collegare altri ricevitori", - "title": "Selezionare il ricevitore che si desidera collegare" + "description": "Esegui nuovamente la configurazione se desideri collegare altri ricevitori", + "title": "Seleziona il ricevitore che desideri collegare" }, "user": { "data": { "host": "Indirizzo IP" }, - "description": "Collegare il ricevitore, se l'indirizzo IP non \u00e8 impostato, sar\u00e0 utilizzato il rilevamento automatico", + "description": "Collega il ricevitore, se l'indirizzo IP non \u00e8 impostato, sar\u00e0 utilizzato il rilevamento automatico", "title": "Ricevitori di rete Denon AVR" } } @@ -38,8 +38,8 @@ "data": { "show_all_sources": "Mostra tutte le fonti", "update_audyssey": "Aggiorna le impostazioni di Audyssey", - "zone2": "Imposta la Zona 2", - "zone3": "Imposta la Zona 3" + "zone2": "Configura la zona 2", + "zone3": "Configura la zona 3" }, "description": "Specificare le impostazioni opzionali", "title": "Ricevitori di rete Denon AVR" diff --git a/homeassistant/components/dexcom/translations/it.json b/homeassistant/components/dexcom/translations/it.json index b196f9f87ed..21904e36378 100644 --- a/homeassistant/components/dexcom/translations/it.json +++ b/homeassistant/components/dexcom/translations/it.json @@ -16,7 +16,7 @@ "username": "Nome utente" }, "description": "Inserisci le credenziali di Dexcom Share", - "title": "Configurare l'integrazione di Dexcom" + "title": "Configura l'integrazione di Dexcom" } } }, diff --git a/homeassistant/components/dialogflow/translations/it.json b/homeassistant/components/dialogflow/translations/it.json index 8df625d2cc3..053cef1d954 100644 --- a/homeassistant/components/dialogflow/translations/it.json +++ b/homeassistant/components/dialogflow/translations/it.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "L'istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi webhook." }, "create_entry": { - "default": "Per inviare eventi a Home Assistant, dovrai configurare [l'integrazione webhook di Dialogflow]({dialogflow_url})\n\n Compila le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json \n\n Vedi [la documentazione]({docs_url}) for ulteriori dettagli." + "default": "Per inviare eventi a Home Assistant, dovrai configurare [l'integrazione webhook di Dialogflow]({dialogflow_url})\n\n Compila le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json \n\n Vedi [la documentazione]({docs_url}) per ulteriori dettagli." }, "step": { "user": { diff --git a/homeassistant/components/doorbird/translations/it.json b/homeassistant/components/doorbird/translations/it.json index 99d721fe491..4a39ef36f3c 100644 --- a/homeassistant/components/doorbird/translations/it.json +++ b/homeassistant/components/doorbird/translations/it.json @@ -29,7 +29,7 @@ "data": { "events": "Elenco di eventi separati da virgole." }, - "description": "Aggiungere un nome di evento separato da virgola per ogni evento che si desidera monitorare. Dopo averli inseriti qui, usa l'applicazione DoorBird per assegnarli a un evento specifico. Consultare la documentazione su https://www.home-assistant.io/integrations/doorbird/#events. Esempio: qualcuno_premuto_il_pulsante, movimento" + "description": "Aggiungere un nome di evento separato da virgola per ogni evento che desideri monitorare. Dopo averli inseriti qui, usa l'applicazione DoorBird per assegnarli a un evento specifico. Consulta la documentazione su https://www.home-assistant.io/integrations/doorbird/#events. Esempio: qualcuno_premuto_il_pulsante, movimento" } } } diff --git a/homeassistant/components/dsmr/translations/it.json b/homeassistant/components/dsmr/translations/it.json index e1287c4af39..9b50979d016 100644 --- a/homeassistant/components/dsmr/translations/it.json +++ b/homeassistant/components/dsmr/translations/it.json @@ -40,7 +40,7 @@ "data": { "type": "Tipo di connessione" }, - "title": "Selezionare il tipo di connessione" + "title": "Seleziona il tipo di connessione" } } }, diff --git a/homeassistant/components/dunehd/translations/it.json b/homeassistant/components/dunehd/translations/it.json index 57f94a2753f..e296a59e474 100644 --- a/homeassistant/components/dunehd/translations/it.json +++ b/homeassistant/components/dunehd/translations/it.json @@ -13,7 +13,7 @@ "data": { "host": "Host" }, - "description": "Configurare l'integrazione Dune HD. In caso di problemi con la configurazione, visitare: https://www.home-assistant.io/integrations/dunehd \n\n Assicurati che il tuo lettore sia acceso.", + "description": "Configura l'integrazione Dune HD. In caso di problemi con la configurazione, visita: https://www.home-assistant.io/integrations/dunehd \n\n Assicurati che il tuo lettore sia acceso.", "title": "Dune HD" } } diff --git a/homeassistant/components/elkm1/translations/it.json b/homeassistant/components/elkm1/translations/it.json index 18e53997937..31b250e33fd 100644 --- a/homeassistant/components/elkm1/translations/it.json +++ b/homeassistant/components/elkm1/translations/it.json @@ -14,7 +14,7 @@ "data": { "address": "L'indirizzo IP o il dominio o la porta seriale se ci si connette tramite seriale.", "password": "Password", - "prefix": "Un prefisso univoco (lasciare vuoto se si dispone di un solo ElkM1).", + "prefix": "Un prefisso univoco (lascia vuoto se disponi di un solo ElkM1).", "protocol": "Protocollo", "temperature_unit": "L'unit\u00e0 di temperatura utilizzata da ElkM1.", "username": "Nome utente" diff --git a/homeassistant/components/elmax/translations/it.json b/homeassistant/components/elmax/translations/it.json index ffcd23629b2..ef9d152e543 100644 --- a/homeassistant/components/elmax/translations/it.json +++ b/homeassistant/components/elmax/translations/it.json @@ -17,7 +17,7 @@ "panel_name": "Nome del pannello", "panel_pin": "Codice PIN" }, - "description": "Seleziona quale pannello vuoi controllare con questa integrazione. Si prega di notare che il pannello deve essere acceso per essere configurato.", + "description": "Seleziona quale pannello vuoi controllare con questa integrazione. Nota che il pannello deve essere acceso per essere configurato.", "title": "Selezione del pannello" }, "user": { diff --git a/homeassistant/components/emulated_roku/translations/it.json b/homeassistant/components/emulated_roku/translations/it.json index 6d734e7b1ba..69699b8f210 100644 --- a/homeassistant/components/emulated_roku/translations/it.json +++ b/homeassistant/components/emulated_roku/translations/it.json @@ -17,5 +17,5 @@ } } }, - "title": "Emulated Roku" + "title": "Roku emulato" } \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/it.json b/homeassistant/components/esphome/translations/it.json index a67710bf844..a65b1e8a033 100644 --- a/homeassistant/components/esphome/translations/it.json +++ b/homeassistant/components/esphome/translations/it.json @@ -6,10 +6,10 @@ "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { - "connection_error": "Impossibile connettersi ad ESP. Assicurati che il tuo file YAML contenga una riga \"api:\".", + "connection_error": "Impossibile connettersi a ESP. Assicurati che il tuo file YAML contenga una riga \"api:\".", "invalid_auth": "Autenticazione non valida", "invalid_psk": "La chiave di crittografia del trasporto non \u00e8 valida. Assicurati che corrisponda a ci\u00f2 che hai nella tua configurazione", - "resolve_error": "Impossibile risolvere l'indirizzo dell'ESP. Se questo errore persiste, impostare un indirizzo IP statico: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "resolve_error": "Impossibile risolvere l'indirizzo dell'ESP. Se questo errore persiste, imposta un indirizzo IP statico: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/evil_genius_labs/translations/no.json b/homeassistant/components/evil_genius_labs/translations/no.json index 17c1c3b70dc..814ac7f1080 100644 --- a/homeassistant/components/evil_genius_labs/translations/no.json +++ b/homeassistant/components/evil_genius_labs/translations/no.json @@ -2,6 +2,7 @@ "config": { "error": { "cannot_connect": "Tilkobling mislyktes", + "timeout": "Tidsavbrudd oppretter forbindelse", "unknown": "Uventet feil" }, "step": { diff --git a/homeassistant/components/ezviz/translations/it.json b/homeassistant/components/ezviz/translations/it.json index 84e7811a4a5..0c6ce669ba3 100644 --- a/homeassistant/components/ezviz/translations/it.json +++ b/homeassistant/components/ezviz/translations/it.json @@ -34,7 +34,7 @@ "url": "URL", "username": "Nome utente" }, - "description": "Specificare manualmente l'URL dell'area geografica", + "description": "Specifica manualmente l'URL dell'area geografica", "title": "Connettiti all'URL personalizzato di Ezviz" } } diff --git a/homeassistant/components/forked_daapd/translations/it.json b/homeassistant/components/forked_daapd/translations/it.json index f33f5b7cc2b..a5a1d092281 100644 --- a/homeassistant/components/forked_daapd/translations/it.json +++ b/homeassistant/components/forked_daapd/translations/it.json @@ -5,10 +5,10 @@ "not_forked_daapd": "Il dispositivo non \u00e8 un server forked-daapd." }, "error": { - "forbidden": "Impossibile connettersi. Si prega di controllare i permessi di rete forked-daapd.", + "forbidden": "Impossibile connettersi. Controlla i permessi di rete forked-daapd.", "unknown_error": "Errore imprevisto", "websocket_not_enabled": "websocket del server forked-daapd non abilitato.", - "wrong_host_or_port": "Impossibile connettersi. Si prega di controllare host e porta.", + "wrong_host_or_port": "Impossibile connettersi. Controlla host e porta.", "wrong_password": "Password errata", "wrong_server_type": "L'integrazione forked-daapd richiede un server forked-daapd con versione >= 27.0." }, @@ -18,10 +18,10 @@ "data": { "host": "Host", "name": "Nome descrittivo", - "password": "Password API (lasciare vuota se non c'\u00e8 password)", + "password": "Password API (lascia vuota se non c'\u00e8 password)", "port": "Porta API" }, - "title": "Configurare il dispositivo forked-daapd" + "title": "Configura il dispositivo forked-daapd" } } }, @@ -34,7 +34,7 @@ "tts_pause_time": "Secondi di pausa prima e dopo il TTS", "tts_volume": "Volume TTS (variabile nell'intervallo [0,1])" }, - "description": "Impostare le varie opzioni per l'integrazione forked-daapd.", + "description": "Imposta le varie opzioni per l'integrazione forked-daapd.", "title": "Configura le opzioni forked-daapd" } } diff --git a/homeassistant/components/freebox/translations/it.json b/homeassistant/components/freebox/translations/it.json index 7dd5e279a87..4eb49d5fdfb 100644 --- a/homeassistant/components/freebox/translations/it.json +++ b/homeassistant/components/freebox/translations/it.json @@ -10,7 +10,7 @@ }, "step": { "link": { - "description": "Fare clic su \"Invia\", quindi toccare la freccia destra sul router per registrare Freebox con Home Assistant.\n\n![Posizione del pulsante sul router](/static/images/config_freebox.png)", + "description": "Fai clic su \"Invia\", quindi tocca la freccia destra sul router per registrare Freebox con Home Assistant.\n\n![Posizione del pulsante sul router](/static/images/config_freebox.png)", "title": "Collega il router Freebox" }, "user": { diff --git a/homeassistant/components/fritzbox/translations/it.json b/homeassistant/components/fritzbox/translations/it.json index bf94dd476a1..68cbe08b1b9 100644 --- a/homeassistant/components/fritzbox/translations/it.json +++ b/homeassistant/components/fritzbox/translations/it.json @@ -4,7 +4,7 @@ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", "no_devices_found": "Nessun dispositivo trovato sulla rete", - "not_supported": "Collegato a AVM FRITZ!Box ma non \u00e8 in grado di controllare i dispositivi Smart Home.", + "not_supported": "Collegato a AVM FRITZ!Box, ma non \u00e8 in grado di controllare i dispositivi Smart Home.", "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { diff --git a/homeassistant/components/fronius/translations/it.json b/homeassistant/components/fronius/translations/it.json index 271754bfc29..82d851bdee8 100644 --- a/homeassistant/components/fronius/translations/it.json +++ b/homeassistant/components/fronius/translations/it.json @@ -17,7 +17,7 @@ "data": { "host": "Host" }, - "description": "Configurare l'indirizzo IP o il nome host locale del proprio dispositivo Fronius.", + "description": "Configura l'indirizzo IP o il nome host locale del proprio dispositivo Fronius.", "title": "Fronius SolarNet" } } diff --git a/homeassistant/components/geofency/translations/it.json b/homeassistant/components/geofency/translations/it.json index 48f0456a2bc..f4049846f56 100644 --- a/homeassistant/components/geofency/translations/it.json +++ b/homeassistant/components/geofency/translations/it.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "L'istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi webhook." }, "create_entry": { - "default": "Per inviare eventi a Home Assistant, dovrai configurare la funzionalit\u00e0 webhook in Geofency.\n\n Compila le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Method: POST \n\n Vedi [la documentazione]({docs_url}) for ulteriori dettagli." + "default": "Per inviare eventi a Home Assistant, dovrai configurare la funzionalit\u00e0 webhook in Geofency.\n\n Compila le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Method: POST \n\n Vedi [la documentazione]({docs_url}) per ulteriori dettagli." }, "step": { "user": { diff --git a/homeassistant/components/gios/translations/it.json b/homeassistant/components/gios/translations/it.json index 5d1e99d17f4..cb1f1cea6b0 100644 --- a/homeassistant/components/gios/translations/it.json +++ b/homeassistant/components/gios/translations/it.json @@ -14,7 +14,7 @@ "name": "Nome", "station_id": "ID della stazione di misura" }, - "description": "Impostare l'integrazione della qualit\u00e0 dell'aria GIO\u015a (Ispettorato capo polacco di protezione ambientale). Se hai bisogno di aiuto con la configurazione dai un'occhiata qui: https://www.home-assistant.io/integrations/gios", + "description": "Imposta l'integrazione della qualit\u00e0 dell'aria GIO\u015a (Ispettorato capo polacco di protezione ambientale). Se hai bisogno di aiuto con la configurazione dai un'occhiata qui: https://www.home-assistant.io/integrations/gios", "title": "GIO\u015a (Ispettorato capo polacco di protezione ambientale)" } } diff --git a/homeassistant/components/glances/translations/it.json b/homeassistant/components/glances/translations/it.json index f4806c5d952..f546a43db89 100644 --- a/homeassistant/components/glances/translations/it.json +++ b/homeassistant/components/glances/translations/it.json @@ -16,10 +16,10 @@ "port": "Porta", "ssl": "Utilizza un certificato SSL", "username": "Nome utente", - "verify_ssl": "Verificare il certificato SSL", + "verify_ssl": "Verifica il certificato SSL", "version": "Glances API Version (2 o 3)" }, - "title": "Impostare Glances" + "title": "Imposta Glances" } } }, diff --git a/homeassistant/components/goalzero/translations/it.json b/homeassistant/components/goalzero/translations/it.json index 541cf7e01e1..ad5042cc602 100644 --- a/homeassistant/components/goalzero/translations/it.json +++ b/homeassistant/components/goalzero/translations/it.json @@ -12,7 +12,7 @@ }, "step": { "confirm_discovery": { - "description": "Si consiglia la prenotazione DHCP sul router. Se non configurato, il dispositivo potrebbe non essere disponibile fino a quando Home Assistant non rileva il nuovo indirizzo IP. Fare riferimento al manuale utente del router.", + "description": "Si consiglia la prenotazione DHCP sul router. Se non configurato, il dispositivo potrebbe non essere disponibile fino a quando Home Assistant non rileva il nuovo indirizzo IP. Fai riferimento al manuale utente del router.", "title": "Goal Zero Yeti" }, "user": { diff --git a/homeassistant/components/google_travel_time/translations/it.json b/homeassistant/components/google_travel_time/translations/it.json index aa109708738..71c0bc442e9 100644 --- a/homeassistant/components/google_travel_time/translations/it.json +++ b/homeassistant/components/google_travel_time/translations/it.json @@ -22,7 +22,7 @@ "step": { "init": { "data": { - "avoid": "Evitare", + "avoid": "Evita", "language": "Lingua", "mode": "Modalit\u00e0 di viaggio", "time": "Ora", diff --git a/homeassistant/components/gpslogger/translations/it.json b/homeassistant/components/gpslogger/translations/it.json index 1ad8589583f..99768a36ff5 100644 --- a/homeassistant/components/gpslogger/translations/it.json +++ b/homeassistant/components/gpslogger/translations/it.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "L'istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi webhook." }, "create_entry": { - "default": "Per inviare eventi a Home Assistant, dovrai configurare la funzionalit\u00e0 webhook in GPSLogger.\n\n Compila le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Method: POST \n\n Vedi [la documentazione]({docs_url}) for ulteriori dettagli." + "default": "Per inviare eventi a Home Assistant, dovrai configurare la funzionalit\u00e0 webhook in GPSLogger.\n\n Compila le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Method: POST \n\n Vedi [la documentazione]({docs_url}) per ulteriori dettagli." }, "step": { "user": { diff --git a/homeassistant/components/guardian/translations/it.json b/homeassistant/components/guardian/translations/it.json index 0f3aacf43bf..5db0956d80f 100644 --- a/homeassistant/components/guardian/translations/it.json +++ b/homeassistant/components/guardian/translations/it.json @@ -14,7 +14,7 @@ "ip_address": "Indirizzo IP", "port": "Porta" }, - "description": "Configurare un dispositivo Elexa Guardian locale." + "description": "Configura un dispositivo Elexa Guardian locale." }, "zeroconf_confirm": { "description": "Vuoi configurare questo dispositivo Guardian?" diff --git a/homeassistant/components/hangouts/translations/it.json b/homeassistant/components/hangouts/translations/it.json index df3b1cf4dee..3d9322b1152 100644 --- a/homeassistant/components/hangouts/translations/it.json +++ b/homeassistant/components/hangouts/translations/it.json @@ -5,7 +5,7 @@ "unknown": "Errore imprevisto" }, "error": { - "invalid_2fa": "Autenticazione a 2 fattori non valida, riprovare.", + "invalid_2fa": "Autenticazione a 2 fattori non valida, riprova.", "invalid_2fa_method": "Metodo 2FA non valido (verifica sul telefono).", "invalid_login": "Accesso non valido, riprova." }, diff --git a/homeassistant/components/harmony/translations/it.json b/homeassistant/components/harmony/translations/it.json index 1edaa9edc15..9f90460f785 100644 --- a/homeassistant/components/harmony/translations/it.json +++ b/homeassistant/components/harmony/translations/it.json @@ -18,7 +18,7 @@ "host": "Host", "name": "Nome Hub" }, - "title": "Configurare Logitech Harmony Hub" + "title": "Configura Logitech Harmony Hub" } } }, @@ -29,7 +29,7 @@ "activity": "L'attivit\u00e0 predefinita da eseguire quando nessuna \u00e8 specificata.", "delay_secs": "Il ritardo tra l'invio dei comandi." }, - "description": "Regolare le opzioni di Harmony Hub" + "description": "Regola le opzioni di Harmony Hub" } } } diff --git a/homeassistant/components/hassio/translations/it.json b/homeassistant/components/hassio/translations/it.json index 44499d3f002..2fb36ce622d 100644 --- a/homeassistant/components/hassio/translations/it.json +++ b/homeassistant/components/hassio/translations/it.json @@ -6,7 +6,7 @@ "disk_used": "Disco utilizzato", "docker_version": "Versione Docker", "healthy": "Integrit\u00e0", - "host_os": "Sistema Operativo Host", + "host_os": "Sistema operativo dell'host", "installed_addons": "Componenti aggiuntivi installati", "supervisor_api": "API Supervisor", "supervisor_version": "Versione Supervisor", diff --git a/homeassistant/components/hisense_aehw4a1/translations/it.json b/homeassistant/components/hisense_aehw4a1/translations/it.json index b6951f4d647..b8a12743cde 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/it.json +++ b/homeassistant/components/hisense_aehw4a1/translations/it.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Voui configurare Hisense AEH-W4A1" + "description": "Vuoi configurare Hisense AEH-W4A1" } } } diff --git a/homeassistant/components/homeassistant/translations/it.json b/homeassistant/components/homeassistant/translations/it.json index 3052a536338..0b797a3e84e 100644 --- a/homeassistant/components/homeassistant/translations/it.json +++ b/homeassistant/components/homeassistant/translations/it.json @@ -6,8 +6,8 @@ "docker": "Docker", "hassio": "Supervisor", "installation_type": "Tipo di installazione", - "os_name": "Famiglia del Sistema Operativo", - "os_version": "Versione del Sistema Operativo", + "os_name": "Famiglia del sistema operativo", + "os_version": "Versione del sistema operativo", "python_version": "Versione Python", "timezone": "Fuso orario", "user": "Utente", diff --git a/homeassistant/components/homeassistant/translations/ja.json b/homeassistant/components/homeassistant/translations/ja.json index 453478aee37..f0c5aaa197b 100644 --- a/homeassistant/components/homeassistant/translations/ja.json +++ b/homeassistant/components/homeassistant/translations/ja.json @@ -1,14 +1,14 @@ { "system_health": { "info": { - "arch": "CPU \u30a2\u30fc\u30ad\u30c6\u30af\u30c1\u30e3", - "dev": "\u958b\u767a", + "arch": "CPU\u30a2\u30fc\u30ad\u30c6\u30af\u30c1\u30e3", + "dev": "\u30c7\u30a3\u30d9\u30ed\u30c3\u30d7\u30e1\u30f3\u30c8", "docker": "Docker", "hassio": "Supervisor", - "installation_type": "\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u306e\u7a2e\u985e", + "installation_type": "\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u7a2e\u5225", "os_name": "\u30aa\u30da\u30ec\u30fc\u30c6\u30a3\u30f3\u30b0\u30b7\u30b9\u30c6\u30e0\u30d5\u30a1\u30df\u30ea\u30fc", - "os_version": "\u30aa\u30da\u30ec\u30fc\u30c6\u30a3\u30f3\u30b0 \u30b7\u30b9\u30c6\u30e0\u306e\u30d0\u30fc\u30b8\u30e7\u30f3", - "python_version": "Python \u30d0\u30fc\u30b8\u30e7\u30f3", + "os_version": "\u30aa\u30da\u30ec\u30fc\u30c6\u30a3\u30f3\u30b0\u30b7\u30b9\u30c6\u30e0\u306e\u30d0\u30fc\u30b8\u30e7\u30f3", + "python_version": "Python\u30d0\u30fc\u30b8\u30e7\u30f3", "timezone": "\u30bf\u30a4\u30e0\u30be\u30fc\u30f3", "user": "\u30e6\u30fc\u30b6\u30fc", "version": "\u30d0\u30fc\u30b8\u30e7\u30f3", diff --git a/homeassistant/components/homekit/translations/it.json b/homeassistant/components/homekit/translations/it.json index 8e7ead91ac1..acb86847a4d 100644 --- a/homeassistant/components/homekit/translations/it.json +++ b/homeassistant/components/homekit/translations/it.json @@ -21,7 +21,7 @@ "step": { "advanced": { "data": { - "auto_start": "Avvio automatico (disabilitare se stai chiamando manualmente il servizio homekit.start)", + "auto_start": "Avvio automatico (disabilita se stai chiamando manualmente il servizio homekit.start)", "devices": "Dispositivi (Attivatori)" }, "description": "Gli interruttori programmabili vengono creati per ogni dispositivo selezionato. Quando si attiva un trigger del dispositivo, HomeKit pu\u00f2 essere configurato per eseguire un'automazione o una scena.", diff --git a/homeassistant/components/homekit_controller/translations/it.json b/homeassistant/components/homekit_controller/translations/it.json index e5c54a6297e..7fcd1039cc0 100644 --- a/homeassistant/components/homekit_controller/translations/it.json +++ b/homeassistant/components/homekit_controller/translations/it.json @@ -4,7 +4,7 @@ "accessory_not_found_error": "Impossibile aggiungere l'abbinamento in quanto non \u00e8 pi\u00f9 possibile trovare il dispositivo.", "already_configured": "L'accessorio \u00e8 gi\u00e0 configurato con questo controller.", "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", - "already_paired": "Questo accessorio \u00e8 gi\u00e0 associato a un altro dispositivo. Si prega di resettare l'accessorio e riprovare.", + "already_paired": "Questo accessorio \u00e8 gi\u00e0 associato a un altro dispositivo. Ripristina l'accessorio e riprova.", "ignored_model": "Il supporto di HomeKit per questo modello \u00e8 bloccato poich\u00e9 \u00e8 disponibile un'integrazione nativa con pi\u00f9 funzionalit\u00e0.", "invalid_config_entry": "Questo dispositivo viene visualizzato come pronto per l'associazione, ma c'\u00e8 gi\u00e0 una voce di configurazione in conflitto in Home Assistant che prima deve essere rimossa.", "invalid_properties": "Propriet\u00e0 non valide annunciate dal dispositivo.", @@ -15,7 +15,7 @@ "insecure_setup_code": "Il codice di installazione richiesto non \u00e8 sicuro a causa della sua natura banale. Questo accessorio non soddisfa i requisiti di sicurezza di base.", "max_peers_error": "Il dispositivo ha rifiutato di aggiungere l'abbinamento in quanto non dispone di una memoria libera per esso.", "pairing_failed": "Si \u00e8 verificato un errore non gestito durante il tentativo di abbinamento con questo dispositivo. Potrebbe trattarsi di un errore temporaneo o il dispositivo potrebbe non essere attualmente supportato.", - "unable_to_pair": "Impossibile abbinare, per favore riprova.", + "unable_to_pair": "Impossibile abbinare, riprova.", "unknown_error": "Il dispositivo ha riportato un errore sconosciuto. L'abbinamento non \u00e8 riuscito." }, "flow_title": "{name}", diff --git a/homeassistant/components/homematicip_cloud/translations/it.json b/homeassistant/components/homematicip_cloud/translations/it.json index 80c766becf1..55c26532fb5 100644 --- a/homeassistant/components/homematicip_cloud/translations/it.json +++ b/homeassistant/components/homematicip_cloud/translations/it.json @@ -7,8 +7,8 @@ }, "error": { "invalid_sgtin_or_pin": "SGTIN o Codice PIN non valido, si prega di riprovare.", - "press_the_button": "Si prega di premere il pulsante blu.", - "register_failed": "Registrazione fallita, si prega di riprovare.", + "press_the_button": "Premi il pulsante blu.", + "register_failed": "Registrazione non riuscita, riprova.", "timeout_button": "Timeout della pressione del pulsante blu, riprovare." }, "step": { diff --git a/homeassistant/components/humidifier/translations/it.json b/homeassistant/components/humidifier/translations/it.json index 2d2caf1a9d7..73205575144 100644 --- a/homeassistant/components/humidifier/translations/it.json +++ b/homeassistant/components/humidifier/translations/it.json @@ -4,8 +4,8 @@ "set_humidity": "Impostare l'umidit\u00e0 per {entity_name}", "set_mode": "Cambiare la modalit\u00e0 di {entity_name}", "toggle": "Commuta {entity_name}", - "turn_off": "Disattivare {entity_name}", - "turn_on": "Attivare {entity_name}" + "turn_off": "Disattiva {entity_name}", + "turn_on": "Attiva {entity_name}" }, "condition_type": { "is_mode": "{entity_name} \u00e8 impostato su una modalit\u00e0 specifica", diff --git a/homeassistant/components/hyperion/translations/it.json b/homeassistant/components/hyperion/translations/it.json index 81f049125ed..0d699085057 100644 --- a/homeassistant/components/hyperion/translations/it.json +++ b/homeassistant/components/hyperion/translations/it.json @@ -27,7 +27,7 @@ "title": "Conferma l'aggiunta del servizio Hyperion Ambilight" }, "create_token": { - "description": "Scegli **Invia** di seguito per richiedere un nuovo token di autenticazione. Verrai reindirizzato all'interfaccia utente di Hyperion per approvare la richiesta. Verifica che l'ID visualizzato sia \"{auth_id}\"", + "description": "Scegli **Invia** di seguito per richiedere un nuovo token di autenticazione. Sarai reindirizzato all'interfaccia utente di Hyperion per approvare la richiesta. Verifica che l'ID visualizzato sia \"{auth_id}\"", "title": "Crea automaticamente un nuovo token di autenticazione" }, "create_token_external": { diff --git a/homeassistant/components/icloud/translations/it.json b/homeassistant/components/icloud/translations/it.json index 0661519b714..777498d6340 100644 --- a/homeassistant/components/icloud/translations/it.json +++ b/homeassistant/components/icloud/translations/it.json @@ -8,7 +8,7 @@ "error": { "invalid_auth": "Autenticazione non valida", "send_verification_code": "Impossibile inviare il codice di verifica", - "validate_verification_code": "Impossibile verificare il codice di verifica, riprovare" + "validate_verification_code": "Impossibile verificare il codice di verifica, riprova" }, "step": { "reauth": { @@ -22,7 +22,7 @@ "data": { "trusted_device": "Dispositivo attendibile" }, - "description": "Selezionare il dispositivo attendibile", + "description": "Seleziona il dispositivo attendibile", "title": "Dispositivo attendibile iCloud" }, "user": { diff --git a/homeassistant/components/insteon/translations/it.json b/homeassistant/components/insteon/translations/it.json index f328c5a5f7e..83a4e87d6f6 100644 --- a/homeassistant/components/insteon/translations/it.json +++ b/homeassistant/components/insteon/translations/it.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "Impossibile connettersi", - "select_single": "Selezionare un'opzione." + "select_single": "Seleziona un'opzione." }, "step": { "hubv1": { @@ -14,7 +14,7 @@ "host": "Indirizzo IP", "port": "Porta" }, - "description": "Configurare la versione 1 di Insteon Hub (precedente al 2014).", + "description": "Configura la versione 1 di Insteon Hub (precedente al 2014).", "title": "Insteon Hub Versione 1" }, "hubv2": { @@ -47,7 +47,7 @@ "error": { "cannot_connect": "Impossibile connettersi", "input_error": "Voci non valide, si prega di controllare i valori.", - "select_single": "Selezionare un'opzione." + "select_single": "Seleziona un'opzione." }, "step": { "add_override": { @@ -76,30 +76,30 @@ "port": "Porta", "username": "Nome utente" }, - "description": "Modificare le informazioni di connessione di Insteon Hub. \u00c8 necessario riavviare Home Assistant dopo aver apportato questa modifica. Ci\u00f2 non modifica la configurazione dell'Hub stesso. Per modificare la configurazione nell'Hub, utilizzare l'app Hub.", + "description": "Modifica le informazioni di connessione di Insteon Hub. \u00c8 necessario riavviare Home Assistant dopo aver apportato questa modifica. Ci\u00f2 non modifica la configurazione dell'Hub stesso. Per modificare la configurazione nell'Hub, utilizza l'app Hub.", "title": "Insteon" }, "init": { "data": { "add_override": "Aggiungi una sostituzione del dispositivo.", "add_x10": "Aggiungi un dispositivo X10.", - "change_hub_config": "Modificare la configurazione dell'hub.", + "change_hub_config": "Modifica la configurazione dell'hub.", "remove_override": "Rimuovere una sostituzione del dispositivo.", "remove_x10": "Rimuovi un dispositivo X10." }, - "description": "Selezionare un'opzione da configurare.", + "description": "Seleziona un'opzione da configurare.", "title": "Insteon" }, "remove_override": { "data": { - "address": "Selezionare un indirizzo del dispositivo da rimuovere" + "address": "Seleziona un indirizzo del dispositivo da rimuovere" }, "description": "Rimuovere una sostituzione del dispositivo", "title": "Insteon" }, "remove_x10": { "data": { - "address": "Selezionare un indirizzo del dispositivo da rimuovere" + "address": "Seleziona un indirizzo del dispositivo da rimuovere" }, "description": "Rimuovi un dispositivo X10", "title": "Insteon" diff --git a/homeassistant/components/ipp/translations/it.json b/homeassistant/components/ipp/translations/it.json index 32dfb7fab19..c5a2a613eeb 100644 --- a/homeassistant/components/ipp/translations/it.json +++ b/homeassistant/components/ipp/translations/it.json @@ -6,12 +6,12 @@ "connection_upgrade": "Impossibile connettersi alla stampante a causa della necessit\u00e0 dell'aggiornamento della connessione.", "ipp_error": "Si \u00e8 verificato un errore IPP.", "ipp_version_error": "Versione IPP non supportata dalla stampante.", - "parse_error": "Impossibile analizzare la risposta dalla stampante.", + "parse_error": "Impossibile analizza la risposta dalla stampante.", "unique_id_required": "Identificazione univoca del dispositivo mancante necessaria per l'individuazione." }, "error": { "cannot_connect": "Impossibile connettersi", - "connection_upgrade": "Impossibile connettersi alla stampante. Riprovare selezionando l'opzione SSL/TLS." + "connection_upgrade": "Impossibile connettersi alla stampante. Riprova selezionando l'opzione SSL/TLS." }, "flow_title": "{name}", "step": { @@ -21,10 +21,10 @@ "host": "Host", "port": "Porta", "ssl": "Utilizza un certificato SSL", - "verify_ssl": "Verificare il certificato SSL" + "verify_ssl": "Verifica il certificato SSL" }, - "description": "Configurare la stampante tramite Internet Printing Protocol (IPP) per l'integrazione con Home Assistant.", - "title": "Collegare la stampante" + "description": "Configura la stampante tramite Internet Printing Protocol (IPP) per l'integrazione con Home Assistant.", + "title": "Collega la stampante" }, "zeroconf_confirm": { "description": "Vuoi configurare {name}?", diff --git a/homeassistant/components/islamic_prayer_times/translations/it.json b/homeassistant/components/islamic_prayer_times/translations/it.json index 256746291d8..109f260c364 100644 --- a/homeassistant/components/islamic_prayer_times/translations/it.json +++ b/homeassistant/components/islamic_prayer_times/translations/it.json @@ -5,8 +5,8 @@ }, "step": { "user": { - "description": "Vuoi impostare gli Orari di Preghiera Islamici?", - "title": "Impostare gli Orari di Preghiera Islamici" + "description": "Vuoi configurare gli orari di preghiera islamici?", + "title": "Imposta gli orari di preghiera islamici" } } }, diff --git a/homeassistant/components/isy994/translations/it.json b/homeassistant/components/isy994/translations/it.json index 1522642e62f..b59e312717a 100644 --- a/homeassistant/components/isy994/translations/it.json +++ b/homeassistant/components/isy994/translations/it.json @@ -32,7 +32,7 @@ "sensor_string": "Stringa Nodo Sensore", "variable_sensor_string": "Stringa Variabile Sensore" }, - "description": "Impostare le opzioni per l'integrazione ISY: \n \u2022 Stringa Nodo Sensore: qualsiasi dispositivo o cartella che contiene \"Stringa Nodo Sensore\" nel nome verr\u00e0 trattato come un sensore o un sensore binario. \n \u2022 Ignora Stringa: qualsiasi dispositivo con \"Ignora Stringa\" nel nome verr\u00e0 ignorato. \n \u2022 Stringa Variabile Sensore: qualsiasi variabile che contiene \"Stringa Variabile Sensore\" verr\u00e0 aggiunta come sensore. \n \u2022 Ripristina Luminosit\u00e0 Luce: se abilitato, verr\u00e0 ripristinata la luminosit\u00e0 precedente quando si accende una luce al posto del livello incorporato nel dispositivo.", + "description": "Imposta le opzioni per l'integrazione ISY: \n \u2022 Stringa Nodo Sensore: qualsiasi dispositivo o cartella che contiene \"Stringa Nodo Sensore\" nel nome verr\u00e0 trattato come un sensore o un sensore binario. \n \u2022 Ignora Stringa: qualsiasi dispositivo con \"Ignora Stringa\" nel nome verr\u00e0 ignorato. \n \u2022 Stringa Variabile Sensore: qualsiasi variabile che contiene \"Stringa Variabile Sensore\" verr\u00e0 aggiunta come sensore. \n \u2022 Ripristina Luminosit\u00e0 Luce: se abilitato, verr\u00e0 ripristinata la luminosit\u00e0 precedente quando si accende una luce al posto del livello incorporato nel dispositivo.", "title": "Opzioni ISY994" } } diff --git a/homeassistant/components/keenetic_ndms2/translations/it.json b/homeassistant/components/keenetic_ndms2/translations/it.json index 8ce00bbdd81..caa3eca2377 100644 --- a/homeassistant/components/keenetic_ndms2/translations/it.json +++ b/homeassistant/components/keenetic_ndms2/translations/it.json @@ -17,7 +17,7 @@ "port": "Porta", "username": "Nome utente" }, - "title": "Configurare il router Keenetic NDMS2" + "title": "Configura il router Keenetic NDMS2" } } }, @@ -25,7 +25,7 @@ "step": { "user": { "data": { - "consider_home": "Considerare in casa nell'intervallo di", + "consider_home": "Considera in casa nell'intervallo di", "include_arp": "Usa i dati ARP (ignorati se vengono utilizzati i dati dell'hotspot)", "include_associated": "Usa i dati delle associazioni WiFi AP (ignorati se si usano i dati dell'hotspot)", "interfaces": "Scegli le interfacce da scansionare", diff --git a/homeassistant/components/kmtronic/translations/it.json b/homeassistant/components/kmtronic/translations/it.json index 59aeaa25f6f..785e37244e7 100644 --- a/homeassistant/components/kmtronic/translations/it.json +++ b/homeassistant/components/kmtronic/translations/it.json @@ -22,7 +22,7 @@ "step": { "init": { "data": { - "reverse": "Invertire la logica dell'interruttore (usare NC)" + "reverse": "Inverti la logica dell'interruttore (usa NC)" } } } diff --git a/homeassistant/components/knx/translations/it.json b/homeassistant/components/knx/translations/it.json index b41d06e5dae..27053f6c62f 100644 --- a/homeassistant/components/knx/translations/it.json +++ b/homeassistant/components/knx/translations/it.json @@ -12,7 +12,7 @@ "data": { "host": "Host", "individual_address": "Indirizzo individuale per la connessione", - "local_ip": "IP locale (lasciare vuoto se non si \u00e8 sicuri)", + "local_ip": "IP locale (lascia vuoto se non sei sicuro)", "port": "Porta", "route_back": "Torna indietro / Modalit\u00e0 NAT" }, @@ -21,11 +21,11 @@ "routing": { "data": { "individual_address": "Indirizzo individuale per la connessione di routing", - "local_ip": "IP locale (lasciare vuoto se non si \u00e8 sicuri)", + "local_ip": "IP locale (lascia vuoto se non sei sicuro)", "multicast_group": "Il gruppo multicast utilizzato per il routing", "multicast_port": "La porta multicast usata per il routing" }, - "description": "Si prega di configurare le opzioni di routing." + "description": "Configura le opzioni di routing." }, "tunnel": { "data": { @@ -47,7 +47,7 @@ "data": { "connection_type": "Tipo di connessione KNX", "individual_address": "Indirizzo individuale predefinito", - "local_ip": "IP locale (lasciare vuoto se non si \u00e8 sicuri)", + "local_ip": "IP locale (lascia vuoto se non sei sicuro)", "multicast_group": "Gruppo multicast utilizzato per il routing e il rilevamento", "multicast_port": "Porta multicast utilizzata per il routing e il rilevamento", "rate_limit": "Numero massimo di telegrammi in uscita al secondo", @@ -57,7 +57,7 @@ "tunnel": { "data": { "host": "Host", - "local_ip": "IP locale (lasciare vuoto se non si \u00e8 sicuri)", + "local_ip": "IP locale (lascia vuoto se non sei sicuro)", "port": "Porta", "route_back": "Torna indietro / Modalit\u00e0 NAT" } diff --git a/homeassistant/components/konnected/translations/it.json b/homeassistant/components/konnected/translations/it.json index 138ccb9a7c5..7692465b2fe 100644 --- a/homeassistant/components/konnected/translations/it.json +++ b/homeassistant/components/konnected/translations/it.json @@ -12,7 +12,7 @@ }, "step": { "confirm": { - "description": "Modello: {model}\nID: {id}\nHost: {host}\nPorta: {port}\n\n\u00c8 possibile configurare il comportamento di I/O e del pannello nelle impostazioni del Pannello Allarmi di Konnected.", + "description": "Modello: {model}\nID: {id}\nHost: {host}\nPorta: {port}\n\nPuoi configurare il comportamento di I/O e del pannello nelle impostazioni del pannello Allarmi di Konnected.", "title": "Dispositivo Konnected pronto" }, "import_confirm": { @@ -67,7 +67,7 @@ "7": "Zona 7", "out": "OUT" }, - "description": "Rilevato un {model} su {host}. Selezionare la configurazione di base di ciascun I/O di seguito: a seconda dell'I/O, essa pu\u00f2 consentire sensori binari (contatti aperti/chiusi), sensori digitali (DHT e DS18B20) o uscite commutabili. Sarai in grado di configurare le opzioni dettagliate nei prossimi passi.", + "description": "Rilevato un {model} su {host}. Seleziona la configurazione di base di ciascun I/O di seguito: a seconda dell'I/O, essa pu\u00f2 consentire sensori binari (contatti aperti/chiusi), sensori digitali (DHT e DS18B20) o uscite commutabili. Sarai in grado di configurare le opzioni dettagliate nei prossimi passi.", "title": "Configura I/O" }, "options_io_ext": { @@ -98,7 +98,7 @@ "data": { "activation": "Uscita quando acceso", "momentary": "Durata impulso (ms) (opzionale)", - "more_states": "Configurare stati aggiuntivi per questa zona", + "more_states": "Configura stati aggiuntivi per questa zona", "name": "Nome (opzionale)", "pause": "Pausa tra gli impulsi (ms) (opzionale)", "repeat": "Numero di volte da ripetere (-1 = infinito) (opzionale)" diff --git a/homeassistant/components/locative/translations/it.json b/homeassistant/components/locative/translations/it.json index 8e39c244555..43ae3ad5412 100644 --- a/homeassistant/components/locative/translations/it.json +++ b/homeassistant/components/locative/translations/it.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "L'istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi webhook." }, "create_entry": { - "default": "Per inviare localit\u00e0 a Home Assistant, dovrai configurare la funzionalit\u00e0 Webhook nell'app Locative.\n\n Compila le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Method: POST \n\n Vedi [la documentazione]({docs_url}) for ulteriori dettagli." + "default": "Per inviare localit\u00e0 a Home Assistant, dovrai configurare la funzionalit\u00e0 Webhook nell'app Locative.\n\n Compila le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Method: POST \n\n Vedi [la documentazione]({docs_url}) per ulteriori dettagli." }, "step": { "user": { diff --git a/homeassistant/components/lutron_caseta/translations/it.json b/homeassistant/components/lutron_caseta/translations/it.json index 4d895ad8438..8b412cfa40f 100644 --- a/homeassistant/components/lutron_caseta/translations/it.json +++ b/homeassistant/components/lutron_caseta/translations/it.json @@ -66,7 +66,7 @@ "stop_2": "Ferma 2", "stop_3": "Ferma 3", "stop_4": "Ferma 4", - "stop_all": "Fermare tutti" + "stop_all": "Ferma tutti" }, "trigger_type": { "press": "\"{subtype}\" premuto", diff --git a/homeassistant/components/mailgun/translations/it.json b/homeassistant/components/mailgun/translations/it.json index 31909cb44d2..70eefc4b858 100644 --- a/homeassistant/components/mailgun/translations/it.json +++ b/homeassistant/components/mailgun/translations/it.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "L'istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi webhook." }, "create_entry": { - "default": "Per inviare eventi a Home Assistant, dovrai configurare [Webhooks con Mailgun]({mailgun_url})\n\n Compila le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json\n\n Vedi [la documentazione]({docs_url}) su come configurare le automazioni per gestire i dati in arrivo." + "default": "Per inviare eventi a Home Assistant, dovrai configurare [Webhook con Mailgun]({mailgun_url})\n\n Compila le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json\n\n Vedi [la documentazione]({docs_url}) su come configurare le automazioni per gestire i dati in arrivo." }, "step": { "user": { diff --git a/homeassistant/components/meteo_france/translations/it.json b/homeassistant/components/meteo_france/translations/it.json index a8a538ddba3..37588fb4a32 100644 --- a/homeassistant/components/meteo_france/translations/it.json +++ b/homeassistant/components/meteo_france/translations/it.json @@ -5,7 +5,7 @@ "unknown": "Errore imprevisto" }, "error": { - "empty": "Nessun risultato nella ricerca della citt\u00e0: si prega di controllare il campo citt\u00e0" + "empty": "Nessun risultato nella ricerca della citt\u00e0: controlla il campo citt\u00e0" }, "step": { "cities": { diff --git a/homeassistant/components/metoffice/translations/it.json b/homeassistant/components/metoffice/translations/it.json index 5b1e885c743..eb4bd12733e 100644 --- a/homeassistant/components/metoffice/translations/it.json +++ b/homeassistant/components/metoffice/translations/it.json @@ -14,7 +14,7 @@ "latitude": "Latitudine", "longitude": "Logitudine" }, - "description": "La latitudine e la longitudine verranno utilizzate per trovare la stazione meteorologica pi\u00f9 vicina.", + "description": "La latitudine e la longitudine saranno utilizzate per trovare la stazione meteorologica pi\u00f9 vicina.", "title": "Connettiti al Met Office del Regno Unito" } } diff --git a/homeassistant/components/mikrotik/translations/it.json b/homeassistant/components/mikrotik/translations/it.json index e4cafa914d4..203b13ff2d3 100644 --- a/homeassistant/components/mikrotik/translations/it.json +++ b/homeassistant/components/mikrotik/translations/it.json @@ -18,7 +18,7 @@ "username": "Nome utente", "verify_ssl": "Usa SSL" }, - "title": "Configurare il router Mikrotik" + "title": "Configura il router Mikrotik" } } }, @@ -26,8 +26,8 @@ "step": { "device_tracker": { "data": { - "arp_ping": "Attivare il ping ARP", - "detection_time": "Considerare in casa nell'intervallo di", + "arp_ping": "Attiva il ping ARP", + "detection_time": "Considera in casa nell'intervallo di", "force_dhcp": "Scansione forzata con DHCP" } } diff --git a/homeassistant/components/minecraft_server/translations/it.json b/homeassistant/components/minecraft_server/translations/it.json index 8aaee8d8b31..6895c5c9931 100644 --- a/homeassistant/components/minecraft_server/translations/it.json +++ b/homeassistant/components/minecraft_server/translations/it.json @@ -4,7 +4,7 @@ "already_configured": "Il servizio \u00e8 gi\u00e0 configurato" }, "error": { - "cannot_connect": "Impossibile connettersi al server. Controllare l'host e la porta e riprovare. Assicurarsi inoltre che si esegue almeno Minecraft versione 1.7 sul server.", + "cannot_connect": "Impossibile connettersi al server. Controlla l'host e la porta e riprova. Assicurati inoltre di eseguire almeno Minecraft versione 1.7 sul server.", "invalid_ip": "L'indirizzo IP non \u00e8 valido (non \u00e8 stato possibile determinare l'indirizzo MAC). Correggilo e riprova.", "invalid_port": "La porta deve essere compresa tra 1024 e 65535. Correggila e riprova." }, @@ -14,7 +14,7 @@ "host": "Host", "name": "Nome" }, - "description": "Configurare l'istanza del Server Minecraft per consentire il monitoraggio.", + "description": "Configura l'istanza del Server Minecraft per consentire il monitoraggio.", "title": "Collega il tuo Server Minecraft" } } diff --git a/homeassistant/components/mobile_app/translations/it.json b/homeassistant/components/mobile_app/translations/it.json index 3784a158930..9f0a9229d6a 100644 --- a/homeassistant/components/mobile_app/translations/it.json +++ b/homeassistant/components/mobile_app/translations/it.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "install_app": "Apri l'App per dispositivi mobili per configurare l'integrazione con Home Assistant. Vedi [i documenti]({apps_url}) per un elenco di app compatibili." + "install_app": "Apri l'app per dispositivi mobili per configurare l'integrazione con Home Assistant. Vedi [i documenti]({apps_url}) per un elenco di app compatibili." }, "step": { "confirm": { - "description": "Si desidera configurare il componente App per dispositivi mobili?" + "description": "Vuoi configurare il componente App per dispositivi mobili?" } } }, diff --git a/homeassistant/components/mobile_app/translations/ja.json b/homeassistant/components/mobile_app/translations/ja.json index 6a5de20ec8d..1f848fa1988 100644 --- a/homeassistant/components/mobile_app/translations/ja.json +++ b/homeassistant/components/mobile_app/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "install_app": "Mobile app\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u958b\u3044\u3066\u3001Home Assistant\u3068\u306e\u8a2d\u5b9a\u3057\u307e\u3059\u3002\u4e92\u63db\u6027\u306e\u3042\u308b\u30a2\u30d7\u30ea\u306e\u4e00\u89a7\u306f\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({apps_url}) \u3092\u3054\u89a7\u304f\u3060\u3055\u3044\u3002" + "install_app": "Mobile app\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u958b\u3044\u3066\u3001Home Assistant\u3068\u306e\u8a2d\u5b9a\u3092\u3057\u307e\u3059\u3002\u4e92\u63db\u6027\u306e\u3042\u308b\u30a2\u30d7\u30ea\u306e\u4e00\u89a7\u306f\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({apps_url})\u3092\u3054\u89a7\u304f\u3060\u3055\u3044\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/motion_blinds/translations/it.json b/homeassistant/components/motion_blinds/translations/it.json index 29b7ac8e950..6a9cef8a0af 100644 --- a/homeassistant/components/motion_blinds/translations/it.json +++ b/homeassistant/components/motion_blinds/translations/it.json @@ -42,7 +42,7 @@ "data": { "wait_for_push": "Attendi il push multicast all'aggiornamento" }, - "description": "Specificare le impostazioni opzionali", + "description": "Specifica le impostazioni opzionali", "title": "Motion Blinds" } } diff --git a/homeassistant/components/motioneye/translations/it.json b/homeassistant/components/motioneye/translations/it.json index 114fdfc6052..4bc75878b2b 100644 --- a/homeassistant/components/motioneye/translations/it.json +++ b/homeassistant/components/motioneye/translations/it.json @@ -31,7 +31,7 @@ "init": { "data": { "stream_url_template": "Modello URL streaming", - "webhook_set": "Configura i webhooks di motionEye per segnalare gli eventi a Home Assistant", + "webhook_set": "Configura i webhook di motionEye per segnalare gli eventi a Home Assistant", "webhook_set_overwrite": "Sovrascrivi webhook non riconosciuti" } } diff --git a/homeassistant/components/mullvad/translations/it.json b/homeassistant/components/mullvad/translations/it.json index 7b1941a5ee8..4de79cdd7cb 100644 --- a/homeassistant/components/mullvad/translations/it.json +++ b/homeassistant/components/mullvad/translations/it.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Configurare l'integrazione VPN Mullvad?" + "description": "Vuoi configurare l'integrazione VPN Mullvad?" } } } diff --git a/homeassistant/components/mysensors/translations/it.json b/homeassistant/components/mysensors/translations/it.json index 8b139120151..65638dad15e 100644 --- a/homeassistant/components/mysensors/translations/it.json +++ b/homeassistant/components/mysensors/translations/it.json @@ -42,7 +42,7 @@ "step": { "gw_mqtt": { "data": { - "persistence_file": "file di persistenza (lasciare vuoto per generare automaticamente)", + "persistence_file": "file di persistenza (lascia vuoto per generare automaticamente)", "retain": "mqtt conserva", "topic_in_prefix": "prefisso per argomenti di input (topic_in_prefix)", "topic_out_prefix": "prefisso per argomenti di output (topic_out_prefix)", @@ -54,7 +54,7 @@ "data": { "baud_rate": "velocit\u00e0 di trasmissione", "device": "Porta seriale", - "persistence_file": "file di persistenza (lasciare vuoto per generare automaticamente)", + "persistence_file": "file di persistenza (lascia vuoto per generare automaticamente)", "version": "Versione MySensors" }, "description": "Configurazione del gateway seriale" diff --git a/homeassistant/components/netatmo/translations/it.json b/homeassistant/components/netatmo/translations/it.json index 279bbe73a97..c1bfbca3330 100644 --- a/homeassistant/components/netatmo/translations/it.json +++ b/homeassistant/components/netatmo/translations/it.json @@ -54,7 +54,7 @@ "mode": "Calcolo", "show_on_map": "Mostra sulla mappa" }, - "description": "Configurare un sensore meteorologico pubblico per un'area.", + "description": "Configura un sensore meteorologico pubblico per un'area.", "title": "Sensore meteorologico pubblico Netatmo" }, "public_weather_areas": { diff --git a/homeassistant/components/netgear/translations/it.json b/homeassistant/components/netgear/translations/it.json index 72feece850a..8235fcc0ce9 100644 --- a/homeassistant/components/netgear/translations/it.json +++ b/homeassistant/components/netgear/translations/it.json @@ -26,7 +26,7 @@ "data": { "consider_home": "Considera il tempo in casa (secondi)" }, - "description": "Specificare le impostazioni opzionali", + "description": "Specifica le impostazioni opzionali", "title": "Netgear" } } diff --git a/homeassistant/components/nfandroidtv/translations/he.json b/homeassistant/components/nfandroidtv/translations/he.json index 70ced66b0a5..dc4d71c70ca 100644 --- a/homeassistant/components/nfandroidtv/translations/he.json +++ b/homeassistant/components/nfandroidtv/translations/he.json @@ -12,7 +12,9 @@ "data": { "host": "\u05de\u05d0\u05e8\u05d7", "name": "\u05e9\u05dd" - } + }, + "description": "\u05e9\u05d9\u05dc\u05d5\u05d1 \u05d6\u05d4 \u05d3\u05d5\u05e8\u05e9 \u05d0\u05ea \u05d4\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d9\u05d9\u05e9\u05d5\u05dd \u05d4\u05ea\u05e8\u05d0\u05d5\u05ea \u05e2\u05d1\u05d5\u05e8 \u05d8\u05dc\u05d5\u05d5\u05d9\u05d6\u05d9\u05ea \u05d0\u05e0\u05d3\u05e8\u05d5\u05d0\u05d9\u05d3.\n\n\u05e2\u05d1\u05d5\u05e8 \u05d8\u05dc\u05d5\u05d5\u05d9\u05d6\u05d9\u05d9\u05ea \u05d0\u05e0\u05d3\u05e8\u05d5\u05d0\u05d9\u05d3: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\n\u05e2\u05d1\u05d5\u05d3 \u05d8\u05dc\u05d5\u05d5\u05d9\u05d6\u05d9\u05ea \u05d0\u05de\u05d6\u05d5\u05df: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK\n\n\u05e2\u05dc\u05d9\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d4\u05d6\u05de\u05e0\u05ea DHCP \u05d1\u05e0\u05ea\u05d1 \u05e9\u05dc\u05da (\u05e2\u05d9\u05d9\u05df \u05d1\u05de\u05d3\u05e8\u05d9\u05da \u05dc\u05de\u05e9\u05ea\u05de\u05e9 \u05e9\u05dc \u05d4\u05e0\u05ea\u05d1) \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea IP \u05e1\u05d8\u05d8\u05d9\u05ea \u05d1\u05de\u05db\u05e9\u05d9\u05e8. \u05d0\u05dd \u05dc\u05d0, \u05d4\u05d4\u05ea\u05e7\u05df \u05d9\u05d4\u05e4\u05d5\u05da \u05d1\u05e1\u05d5\u05e4\u05d5 \u05e9\u05dc \u05d3\u05d1\u05e8 \u05dc\u05dc\u05d0 \u05d6\u05de\u05d9\u05df.", + "title": "\u05d4\u05ea\u05e8\u05d0\u05d5\u05ea \u05e2\u05d1\u05d5\u05e8 \u05d8\u05dc\u05d5\u05d5\u05d9\u05d6\u05d9\u05d9\u05ea \u05d0\u05e0\u05d3\u05e8\u05d5\u05d0\u05d9\u05d3 / \u05d8\u05dc\u05d5\u05d5\u05d9\u05d6\u05d9\u05d9\u05ea \u05d0\u05de\u05d6\u05d5\u05df" } } } diff --git a/homeassistant/components/nina/translations/it.json b/homeassistant/components/nina/translations/it.json index ee7668cf9ce..de54f8f9c1d 100644 --- a/homeassistant/components/nina/translations/it.json +++ b/homeassistant/components/nina/translations/it.json @@ -17,7 +17,7 @@ "_m_to_q": "Citt\u00e0/provincia (M-Q)", "_r_to_u": "Citt\u00e0/provincia (R-U)", "_v_to_z": "Citt\u00e0/provincia (V-Z)", - "corona_filter": "Rimuovere gli avvisi Corona", + "corona_filter": "Rimuovi gli avvisi Corona", "slots": "Numero massimo di avvisi per citt\u00e0/provincia" }, "title": "Seleziona citt\u00e0/provincia" diff --git a/homeassistant/components/nws/translations/it.json b/homeassistant/components/nws/translations/it.json index c05a2ec28b7..5aee4261847 100644 --- a/homeassistant/components/nws/translations/it.json +++ b/homeassistant/components/nws/translations/it.json @@ -16,7 +16,7 @@ "station": "Codice stazione METAR" }, "description": "Se non \u00e8 specificato un codice stazione METAR, la latitudine e la longitudine saranno utilizzate per trovare la stazione pi\u00f9 vicina. Per ora, una chiave API pu\u00f2 essere qualsiasi cosa. Si consiglia di utilizzare un indirizzo email valido.", - "title": "Collegati al Servizio Meteorologico Nazionale" + "title": "Collegati al servizio meteorologico nazionale" } } } diff --git a/homeassistant/components/nzbget/translations/it.json b/homeassistant/components/nzbget/translations/it.json index 17d7d93a6d6..99fee7fd412 100644 --- a/homeassistant/components/nzbget/translations/it.json +++ b/homeassistant/components/nzbget/translations/it.json @@ -17,7 +17,7 @@ "port": "Porta", "ssl": "Utilizza un certificato SSL", "username": "Nome utente", - "verify_ssl": "Verificare il certificato SSL" + "verify_ssl": "Verifica il certificato SSL" }, "title": "Connettiti a NZBGet" } diff --git a/homeassistant/components/octoprint/translations/it.json b/homeassistant/components/octoprint/translations/it.json index 084307b6323..f4da099a163 100644 --- a/homeassistant/components/octoprint/translations/it.json +++ b/homeassistant/components/octoprint/translations/it.json @@ -20,7 +20,7 @@ "host": "Host", "path": "Percorso dell'applicazione", "port": "Numero porta", - "ssl": "Utilizzare SSL", + "ssl": "Utilizza SSL", "username": "Nome utente" } } diff --git a/homeassistant/components/onewire/translations/it.json b/homeassistant/components/onewire/translations/it.json index 108cadc635e..118cee0de4a 100644 --- a/homeassistant/components/onewire/translations/it.json +++ b/homeassistant/components/onewire/translations/it.json @@ -13,7 +13,7 @@ "host": "Host", "port": "Porta" }, - "title": "Impostare i dettagli dell'owserver" + "title": "Imposta i dettagli dell'owserver" }, "user": { "data": { diff --git a/homeassistant/components/onvif/translations/it.json b/homeassistant/components/onvif/translations/it.json index eea178f990a..3590025a6ad 100644 --- a/homeassistant/components/onvif/translations/it.json +++ b/homeassistant/components/onvif/translations/it.json @@ -5,7 +5,7 @@ "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", "no_h264": "Non c'erano flussi H264 disponibili. Controllare la configurazione del profilo sul dispositivo.", "no_mac": "Impossibile configurare l'ID univoco per il dispositivo ONVIF.", - "onvif_error": "Errore durante la configurazione del dispositivo ONVIF. Controllare i registri per ulteriori informazioni." + "onvif_error": "Errore durante la configurazione del dispositivo ONVIF. Controlla i registri per ulteriori informazioni." }, "error": { "cannot_connect": "Impossibile connettersi" @@ -16,7 +16,7 @@ "password": "Password", "username": "Nome utente" }, - "title": "Configurare l'autenticazione" + "title": "Configura l'autenticazione" }, "configure": { "data": { @@ -32,14 +32,14 @@ "data": { "include": "Crea entit\u00e0 telecamera" }, - "description": "Creare un'entit\u00e0 telecamera per {profile} alla risoluzione {resolution}?", - "title": "Configurare i Profili" + "description": "Crea un'entit\u00e0 telecamera per {profile} alla risoluzione {resolution}?", + "title": "Configura i profili" }, "device": { "data": { "host": "Seleziona il dispositivo ONVIF rilevato" }, - "title": "Selezionare il dispositivo ONVIF" + "title": "Seleziona il dispositivo ONVIF" }, "manual_input": { "data": { @@ -47,7 +47,7 @@ "name": "Nome", "port": "Porta" }, - "title": "Configurare il dispositivo ONVIF" + "title": "Configura il dispositivo ONVIF" }, "user": { "data": { diff --git a/homeassistant/components/open_meteo/translations/no.json b/homeassistant/components/open_meteo/translations/no.json new file mode 100644 index 00000000000..8952abdfef6 --- /dev/null +++ b/homeassistant/components/open_meteo/translations/no.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "zone": "Sone" + }, + "description": "Velg lokasjon som skal brukes til v\u00e6rvarsling" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opengarage/translations/it.json b/homeassistant/components/opengarage/translations/it.json index 0bd8adf23e0..da2389962e0 100644 --- a/homeassistant/components/opengarage/translations/it.json +++ b/homeassistant/components/opengarage/translations/it.json @@ -14,7 +14,7 @@ "device_key": "Chiave del dispositivo", "host": "Host", "port": "Porta", - "verify_ssl": "Verificare il certificato SSL" + "verify_ssl": "Verifica il certificato SSL" } } } diff --git a/homeassistant/components/pi_hole/translations/it.json b/homeassistant/components/pi_hole/translations/it.json index 7d355caf985..c25f7546c62 100644 --- a/homeassistant/components/pi_hole/translations/it.json +++ b/homeassistant/components/pi_hole/translations/it.json @@ -21,7 +21,7 @@ "port": "Porta", "ssl": "Utilizza un certificato SSL", "statistics_only": "Solo Statistiche", - "verify_ssl": "Verificare il certificato SSL" + "verify_ssl": "Verifica il certificato SSL" } } } diff --git a/homeassistant/components/plex/translations/it.json b/homeassistant/components/plex/translations/it.json index f1ec23e2736..8fb0603a8ee 100644 --- a/homeassistant/components/plex/translations/it.json +++ b/homeassistant/components/plex/translations/it.json @@ -9,7 +9,7 @@ "unknown": "Errore imprevisto" }, "error": { - "faulty_credentials": "Autorizzazione non riuscita, verificare il Token", + "faulty_credentials": "Autorizzazione non riuscita, verifica il token", "host_or_token": "Si deve fornire almeno un Host o un Token", "no_servers": "Nessun server collegato all'account Plex", "not_found": "Server Plex non trovato", @@ -23,7 +23,7 @@ "port": "Porta", "ssl": "Utilizza un certificato SSL", "token": "Token (opzionale)", - "verify_ssl": "Verificare il certificato SSL" + "verify_ssl": "Verifica il certificato SSL" }, "title": "Configurazione manuale Plex" }, @@ -32,10 +32,10 @@ "server": "Server" }, "description": "Sono disponibili pi\u00f9 server, selezionarne uno:", - "title": "Selezionare il server Plex" + "title": "Seleziona il server Plex" }, "user": { - "description": "Continuare su [plex.tv](https://plex.tv) per collegare un server Plex.", + "description": "Continua su [plex.tv](https://plex.tv) per collegare un server Plex.", "title": "Plex Media Server" }, "user_advanced": { diff --git a/homeassistant/components/point/translations/it.json b/homeassistant/components/point/translations/it.json index 76aae0f4403..831104ba9dc 100644 --- a/homeassistant/components/point/translations/it.json +++ b/homeassistant/components/point/translations/it.json @@ -17,7 +17,7 @@ "step": { "auth": { "description": "Segui il link qui sotto e **Accetta** l'accesso al tuo account Minut, quindi torna indietro e premi **Invia** qui sotto. \n\n [Link]({authorization_url})", - "title": "Autenticare Point" + "title": "Autenticate Point" }, "user": { "data": { diff --git a/homeassistant/components/powerwall/translations/it.json b/homeassistant/components/powerwall/translations/it.json index 85edd1656f8..15a76c31f48 100644 --- a/homeassistant/components/powerwall/translations/it.json +++ b/homeassistant/components/powerwall/translations/it.json @@ -8,7 +8,7 @@ "cannot_connect": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida", "unknown": "Errore imprevisto", - "wrong_version": "Il tuo powerwall utilizza una versione del software non supportata. Si prega di considerare l'aggiornamento o la segnalazione di questo problema in modo che possa essere risolto." + "wrong_version": "Il tuo powerwall utilizza una versione del software non supportata. Considera l'aggiornamento o la segnalazione di questo problema in modo che possa essere risolto." }, "flow_title": "{ip_address}", "step": { diff --git a/homeassistant/components/progettihwsw/translations/it.json b/homeassistant/components/progettihwsw/translations/it.json index 955bcba692a..5d8425648e8 100644 --- a/homeassistant/components/progettihwsw/translations/it.json +++ b/homeassistant/components/progettihwsw/translations/it.json @@ -27,14 +27,14 @@ "relay_8": "Rel\u00e8 8", "relay_9": "Rel\u00e8 9" }, - "title": "Configurare i rel\u00e8" + "title": "Configura i rel\u00e8" }, "user": { "data": { "host": "Host", "port": "Porta" }, - "title": "Impostare la scheda" + "title": "Configura la scheda" } } } diff --git a/homeassistant/components/ps4/translations/it.json b/homeassistant/components/ps4/translations/it.json index 8ada9acadb3..fb48c7585cc 100644 --- a/homeassistant/components/ps4/translations/it.json +++ b/homeassistant/components/ps4/translations/it.json @@ -4,8 +4,8 @@ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "credential_error": "Errore nel recupero delle credenziali.", "no_devices_found": "Nessun dispositivo trovato sulla rete", - "port_987_bind_error": "Impossibile collegarsi alla porta 987. Fare riferimento alla [documentazione](https://www.home-assistant.io/components/ps4/) per ulteriori informazioni.", - "port_997_bind_error": "Impossibile collegarsi alla porta 997. Consultare la [documentazione](https://www.home-assistant.io/components/ps4/) per ulteriori informazioni." + "port_987_bind_error": "Impossibile collegarsi alla porta 987. Fai riferimento alla [documentazione](https://www.home-assistant.io/components/ps4/) per ulteriori informazioni.", + "port_997_bind_error": "Impossibile collegarsi alla porta 997. Consulta la [documentazione](https://www.home-assistant.io/components/ps4/) per ulteriori informazioni." }, "error": { "cannot_connect": "Impossibile connettersi", @@ -25,15 +25,15 @@ "name": "Nome", "region": "Area geografica" }, - "description": "Inserisci le informazioni per la tua PlayStation 4. Per il Codice PIN, vai a \"Impostazioni\" sulla PlayStation 4. Quindi vai a 'Impostazioni di connessione Mobile App' e seleziona 'Aggiungi dispositivo'. Immettere il Codice PIN visualizzato. Fare riferimento alla [documentazione](https://www.home-assistant.io/components/ps4/) per ulteriori informazioni.", + "description": "Inserisci le informazioni per la tua PlayStation 4. Per il Codice PIN, vai a \"Impostazioni\" sulla PlayStation 4. Quindi vai a 'Impostazioni di connessione Mobile App' e seleziona 'Aggiungi dispositivo'. Immettere il Codice PIN visualizzato. Fai riferimento alla [documentazione](https://www.home-assistant.io/components/ps4/) per ulteriori informazioni.", "title": "PlayStation 4" }, "mode": { "data": { - "ip_address": "Indirizzo IP (Lasciare vuoto se si sta usando l'Auto-Rilevamento).", + "ip_address": "Indirizzo IP (Lascia vuoto se stai usando il rilevamento automatico).", "mode": "Modalit\u00e0 di configurazione" }, - "description": "Selezionare la modalit\u00e0 per la configurazione. Il campo per l'Indirizzo IP pu\u00f2 essere lasciato vuoto se si seleziona Auto-Rilevamento, poich\u00e9 i dispositivi saranno automaticamente individuati.", + "description": "Seleziona la modalit\u00e0 per la configurazione. Il campo per l'indiriizzo IP pu\u00f2 essere lasciato vuoto se si seleziona il rilevamento automatico, poich\u00e9 i dispositivi saranno automaticamente individuati.", "title": "PlayStation 4" } } diff --git a/homeassistant/components/remote/translations/it.json b/homeassistant/components/remote/translations/it.json index e770712be19..f61ac3887cd 100644 --- a/homeassistant/components/remote/translations/it.json +++ b/homeassistant/components/remote/translations/it.json @@ -3,7 +3,7 @@ "action_type": { "toggle": "Commuta {entity_name}", "turn_off": "Disattivare {entity_name}", - "turn_on": "Attivare {entity_name}" + "turn_on": "Attiva {entity_name}" }, "condition_type": { "is_off": "{entity_name} \u00e8 spento", diff --git a/homeassistant/components/rfxtrx/translations/it.json b/homeassistant/components/rfxtrx/translations/it.json index 5e05af403ae..b811985349a 100644 --- a/homeassistant/components/rfxtrx/translations/it.json +++ b/homeassistant/components/rfxtrx/translations/it.json @@ -17,11 +17,11 @@ "host": "Host", "port": "Porta" }, - "title": "Selezionare l'indirizzo di connessione" + "title": "Seleziona l'indirizzo di connessione" }, "setup_serial": { "data": { - "device": "Selezionare il dispositivo" + "device": "Seleziona il dispositivo" }, "title": "Dispositivo" }, @@ -35,7 +35,7 @@ "data": { "type": "Tipo di connessione" }, - "title": "Selezionare il tipo di connessione" + "title": "Seleziona il tipo di connessione" } } }, @@ -63,8 +63,8 @@ "prompt_options": { "data": { "automatic_add": "Attivare l'aggiunta automatica", - "debug": "Attivare il debug", - "device": "Selezionare il dispositivo da configurare", + "debug": "Attiva il debug", + "device": "Seleziona il dispositivo da configurare", "event_code": "Inserire il codice dell'evento da aggiungere", "remove_device": "Seleziona il dispositivo da eliminare" }, @@ -77,12 +77,12 @@ "data_bit": "Numero di bit di dati", "fire_event": "Abilita evento dispositivo", "off_delay": "Ritardo di spegnimento", - "off_delay_enabled": "Attivare il ritardo di spegnimento", - "replace_device": "Selezionare il dispositivo da sostituire", + "off_delay_enabled": "Attiva il ritardo di spegnimento", + "replace_device": "Seleziona il dispositivo da sostituire", "signal_repetitions": "Numero di ripetizioni del segnale", "venetian_blind_mode": "Modalit\u00e0 veneziana" }, - "title": "Configurare le opzioni del dispositivo" + "title": "Configura le opzioni del dispositivo" } } }, diff --git a/homeassistant/components/risco/translations/it.json b/homeassistant/components/risco/translations/it.json index 790a6574433..503dca18db4 100644 --- a/homeassistant/components/risco/translations/it.json +++ b/homeassistant/components/risco/translations/it.json @@ -22,18 +22,18 @@ "step": { "ha_to_risco": { "data": { - "armed_away": "Attivo Fuori Casa", - "armed_custom_bypass": "Attivo con Bypass Personalizzato", - "armed_home": "Attivo In Casa", + "armed_away": "Fuori casa attivato", + "armed_custom_bypass": "Attivo con bypass personalizzato", + "armed_home": "Attivo In casa", "armed_night": "Attivo Notte" }, - "description": "Selezionare lo stato in cui impostare l'allarme Risco quando si attiva l'allarme Home Assistant", - "title": "Mappare gli stati di Home Assistant negli stati di Risco" + "description": "Seleziona lo stato in cui impostare l'allarme Risco quando attivi l'allarme di Home Assistant", + "title": "Mappa gli stati di Home Assistant negli stati di Risco" }, "init": { "data": { - "code_arm_required": "Richiedi il Codice PIN per armare", - "code_disarm_required": "Richiedi il Codice PIN per disarmare", + "code_arm_required": "Richiedi il codice PIN per attivare", + "code_disarm_required": "Richiedi il codice PIN per disattivare", "scan_interval": "Con che frequenza interrogare Risco (in secondi)" }, "title": "Configura le opzioni" @@ -44,11 +44,11 @@ "B": "Gruppo B", "C": "Gruppo C", "D": "Gruppo D", - "arm": "Attivo (Fuori Casa)", - "partial_arm": "Parzialmente attivo (IN CASA)" + "arm": "Attivo (Fuori casa)", + "partial_arm": "Parzialmente attivo (In casa)" }, "description": "Seleziona lo stato che verr\u00e0 segnalato dall'allarme Home Assistant per ogni stato segnalato da Risco", - "title": "Mappare gli stati di Risco agli stati di Home Assistant" + "title": "Mappa gli stati di Risco agli stati di Home Assistant" } } } diff --git a/homeassistant/components/roon/translations/it.json b/homeassistant/components/roon/translations/it.json index e21a74aae43..ac5922e1e56 100644 --- a/homeassistant/components/roon/translations/it.json +++ b/homeassistant/components/roon/translations/it.json @@ -9,8 +9,8 @@ }, "step": { "link": { - "description": "\u00c8 necessario autorizzare l'Assistente Home in Roon. Dopo aver fatto clic su Invia, passare all'applicazione Roon Core, aprire Impostazioni e abilitare HomeAssistant nella scheda Estensioni.", - "title": "Autorizzare HomeAssistant in Roon" + "description": "Devi autorizzare l'Assistente Home in Roon. Dopo aver fatto clic su Invia, passa all'applicazione Roon Core, apri Impostazioni e abilita HomeAssistant nella scheda Estensioni.", + "title": "Autorizza HomeAssistant in Roon" }, "user": { "data": { diff --git a/homeassistant/components/select/translations/it.json b/homeassistant/components/select/translations/it.json index 06a3f47ce7d..8aae40f85fa 100644 --- a/homeassistant/components/select/translations/it.json +++ b/homeassistant/components/select/translations/it.json @@ -10,5 +10,5 @@ "current_option_changed": "Opzione {entity_name} modificata" } }, - "title": "Selezionare" + "title": "Seleziona" } \ No newline at end of file diff --git a/homeassistant/components/sentry/translations/it.json b/homeassistant/components/sentry/translations/it.json index 8386f78afa2..9e3e6974883 100644 --- a/homeassistant/components/sentry/translations/it.json +++ b/homeassistant/components/sentry/translations/it.json @@ -22,12 +22,12 @@ "init": { "data": { "environment": "Nome opzionale dell'ambiente.", - "event_custom_components": "Inviare eventi da componenti personalizzati", + "event_custom_components": "Invia eventi da componenti personalizzati", "event_handled": "Inviare eventi gestiti", - "event_third_party_packages": "Inviare eventi da pacchetti di terze parti", + "event_third_party_packages": "Invia eventi da pacchetti di terze parti", "logging_event_level": "Il livello di registro Sentry registrer\u00e0 un evento per", "logging_level": "Il livello di registro Sentry registrer\u00e0 i registri cos\u00ec granulari per", - "tracing": "Attivare il tracciamento delle prestazioni", + "tracing": "Attiva il tracciamento delle prestazioni", "tracing_sample_rate": "Frequenza di campionamento del tracciamento; tra 0,0 e 1,0 (1,0 = 100%)" } } diff --git a/homeassistant/components/sia/translations/it.json b/homeassistant/components/sia/translations/it.json index 7806cd0bed0..977b316c9a5 100644 --- a/homeassistant/components/sia/translations/it.json +++ b/homeassistant/components/sia/translations/it.json @@ -14,7 +14,7 @@ "data": { "account": "ID account", "additional_account": "Account aggiuntivi", - "encryption_key": "Chiave di crittografia", + "encryption_key": "Chiave di cifratura", "ping_interval": "Intervallo ping (min)", "zones": "Numero di zone per l'account" }, @@ -24,13 +24,13 @@ "data": { "account": "ID account", "additional_account": "Account aggiuntivi", - "encryption_key": "Chiave di crittografia", + "encryption_key": "Chiave di cifratura", "ping_interval": "Intervallo ping (min)", "port": "Porta", "protocol": "Protocollo", "zones": "Numero di zone per l'account" }, - "title": "Creare una connessione per i sistemi di allarme basati su SIA." + "title": "Crea una connessione per i sistemi di allarme basati su SIA." } } }, diff --git a/homeassistant/components/simplisafe/translations/it.json b/homeassistant/components/simplisafe/translations/it.json index 9cc1d676bbf..0221536698c 100644 --- a/homeassistant/components/simplisafe/translations/it.json +++ b/homeassistant/components/simplisafe/translations/it.json @@ -20,7 +20,7 @@ "title": "Completa l'autorizzazione" }, "mfa": { - "description": "Controlla la tua email per trovare un link da SimpliSafe. Dopo aver verificato il link, torna qui per completare l'installazione dell'integrazione.", + "description": "Controlla la tua email per trovare un collegamento da SimpliSafe. Dopo aver verificato il collegamento, torna qui per completare l'installazione dell'integrazione.", "title": "Autenticazione a pi\u00f9 fattori (MFA) SimpliSafe " }, "reauth_confirm": { @@ -48,7 +48,7 @@ "data": { "code": "Codice (utilizzato nell'Interfaccia Utente di Home Assistant)" }, - "title": "Configurare SimpliSafe" + "title": "Configura SimpliSafe" } } } diff --git a/homeassistant/components/sma/translations/it.json b/homeassistant/components/sma/translations/it.json index 0c61a0065fa..db26fc5fe84 100644 --- a/homeassistant/components/sma/translations/it.json +++ b/homeassistant/components/sma/translations/it.json @@ -17,10 +17,10 @@ "host": "Host", "password": "Password", "ssl": "Utilizza un certificato SSL", - "verify_ssl": "Verificare il certificato SSL" + "verify_ssl": "Verifica il certificato SSL" }, "description": "Inserisci le informazioni sul tuo dispositivo SMA.", - "title": "Configurare SMA Solar" + "title": "Configura SMA Solar" } } } diff --git a/homeassistant/components/smarthab/translations/it.json b/homeassistant/components/smarthab/translations/it.json index ffd1ff6d9c6..b74da607f77 100644 --- a/homeassistant/components/smarthab/translations/it.json +++ b/homeassistant/components/smarthab/translations/it.json @@ -2,7 +2,7 @@ "config": { "error": { "invalid_auth": "Autenticazione non valida", - "service": "Errore durante il tentativo di raggiungere SmartHab. Il servizio potrebbe non essere attivo. Controllare la connessione.", + "service": "Errore durante il tentativo di raggiungere SmartHab. Il servizio potrebbe non essere attivo. Controlla la connessione.", "unknown": "Errore imprevisto" }, "step": { diff --git a/homeassistant/components/smartthings/translations/it.json b/homeassistant/components/smartthings/translations/it.json index e73bf6d4e55..0f7a508e977 100644 --- a/homeassistant/components/smartthings/translations/it.json +++ b/homeassistant/components/smartthings/translations/it.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "invalid_webhook_url": "Home Assistant non \u00e8 configurato correttamente per ricevere gli aggiornamenti da SmartThings. L'URL del webhook non \u00e8 valido:\n> {webhook_url}\n\nSi prega di aggiornare la configurazione secondo le [istruzioni]({component_url}), riavviare Home Assistant e riprovare.", + "invalid_webhook_url": "Home Assistant non \u00e8 configurato correttamente per ricevere gli aggiornamenti da SmartThings. L'URL del webhook non \u00e8 valido:\n> {webhook_url}\n\nAggiorna la configurazione secondo le [istruzioni]({component_url}), riavvia Home Assistant e riprova.", "no_available_locations": "Non ci sono posizioni SmartThings disponibili da configurare in Home Assistant." }, "error": { - "app_setup_error": "Impossibile configurare SmartApp. Riprovare.", + "app_setup_error": "Impossibile configurare SmartApp. Riprova.", "token_forbidden": "Il token non dispone degli ambiti OAuth necessari.", "token_invalid_format": "Il token deve essere nel formato UID/GUID", "token_unauthorized": "Il token non \u00e8 valido o non \u00e8 pi\u00f9 autorizzato.", @@ -19,7 +19,7 @@ "data": { "access_token": "Token di accesso" }, - "description": "Si prega di inserire un SmartThings [Personal Access Token]({token_url}) che \u00e8 stato creato secondo le [istruzioni]({component_url}). Questo verr\u00e0 utilizzato per creare l'integrazione Home Assistant all'interno del vostro account SmartThings.", + "description": "Inserisci un SmartThings [Personal Access Token]({token_url}) che \u00e8 stato creato secondo le [istruzioni]({component_url}). Questo sar\u00e0 utilizzato per creare l'integrazione Home Assistant all'interno del tuo account SmartThings.", "title": "Inserisci il Token di Accesso Personale" }, "select_location": { @@ -31,7 +31,7 @@ }, "user": { "description": "SmartThings sar\u00e0 configurato per inviare aggiornamenti push a Home Assistant su: \n > {webhook_url} \n\nSe ci\u00f2 non fosse corretto, aggiornare la configurazione, riavviare Home Assistant e riprovare.", - "title": "Confermare l'URL di richiamo" + "title": "Conferma l'URL di richiamo" } } } diff --git a/homeassistant/components/solaredge/translations/it.json b/homeassistant/components/solaredge/translations/it.json index 2a80e48b24a..6d41904836a 100644 --- a/homeassistant/components/solaredge/translations/it.json +++ b/homeassistant/components/solaredge/translations/it.json @@ -16,7 +16,7 @@ "name": "Il nome di questa installazione", "site_id": "Il sito-id di SolarEdge" }, - "title": "Definire i parametri API per questa installazione" + "title": "Definisci i parametri API per questa installazione" } } } diff --git a/homeassistant/components/somfy_mylink/translations/it.json b/homeassistant/components/somfy_mylink/translations/it.json index e519a4edd72..cfd3041d248 100644 --- a/homeassistant/components/somfy_mylink/translations/it.json +++ b/homeassistant/components/somfy_mylink/translations/it.json @@ -27,14 +27,14 @@ "step": { "entity_config": { "data": { - "reverse": "La tapparella \u00e8 invertita" + "reverse": "La serranda \u00e8 invertita" }, "description": "Configura le opzioni per `{entity_id}`", "title": "Configura entit\u00e0" }, "init": { "data": { - "default_reverse": "Stato d'inversione predefinito per le tapparelle non configurate", + "default_reverse": "Stato d'inversione predefinito per le serrande non configurate", "entity_id": "Configura un'entit\u00e0 specifica.", "target_id": "Configura opzioni per una tapparella" }, @@ -42,10 +42,10 @@ }, "target_config": { "data": { - "reverse": "La tapparella \u00e8 invertita" + "reverse": "La serranda \u00e8 invertita" }, "description": "Configura le opzioni per `{target_name}`", - "title": "Configura tapparelle MyLink" + "title": "Configura serranda MyLink" } } }, diff --git a/homeassistant/components/sonarr/translations/it.json b/homeassistant/components/sonarr/translations/it.json index f912fcd9f23..9dc6582c150 100644 --- a/homeassistant/components/sonarr/translations/it.json +++ b/homeassistant/components/sonarr/translations/it.json @@ -22,7 +22,7 @@ "host": "Host", "port": "Porta", "ssl": "Utilizza un certificato SSL", - "verify_ssl": "Verificare il certificato SSL" + "verify_ssl": "Verifica il certificato SSL" } } } diff --git a/homeassistant/components/speedtestdotnet/translations/it.json b/homeassistant/components/speedtestdotnet/translations/it.json index a538c34e96d..d0c44329313 100644 --- a/homeassistant/components/speedtestdotnet/translations/it.json +++ b/homeassistant/components/speedtestdotnet/translations/it.json @@ -14,9 +14,9 @@ "step": { "init": { "data": { - "manual": "Disabilitare l'aggiornamento automatico", + "manual": "Disabilita l'aggiornamento automatico", "scan_interval": "Frequenza di aggiornamento (minuti)", - "server_name": "Selezionare il server di prova" + "server_name": "Seleziona il server di prova" } } } diff --git a/homeassistant/components/squeezebox/translations/it.json b/homeassistant/components/squeezebox/translations/it.json index 166c69e21d6..2687f0ec619 100644 --- a/homeassistant/components/squeezebox/translations/it.json +++ b/homeassistant/components/squeezebox/translations/it.json @@ -19,7 +19,7 @@ "port": "Porta", "username": "Nome utente" }, - "title": "Modificare le informazioni di connessione" + "title": "Modifica le informazioni di connessione" }, "user": { "data": { diff --git a/homeassistant/components/starline/translations/ja.json b/homeassistant/components/starline/translations/ja.json index dc64e26ac64..0afa9c47347 100644 --- a/homeassistant/components/starline/translations/ja.json +++ b/homeassistant/components/starline/translations/ja.json @@ -11,7 +11,7 @@ "app_id": "App ID", "app_secret": "\u30b7\u30fc\u30af\u30ec\u30c3\u30c8" }, - "description": "[StarLine\u958b\u767a\u8005\u30a2\u30ab\u30a6\u30f3\u30c8](https://my.starline.ru/developer)\u306e\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3ID\u3068\u30b7\u30fc\u30af\u30ec\u30c3\u30c8\u30b3\u30fc\u30c9", + "description": "[StarLine developer account](https://my.starline.ru/developer)\u306e\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3ID\u3068\u30b7\u30fc\u30af\u30ec\u30c3\u30c8\u30b3\u30fc\u30c9", "title": "\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u8cc7\u683c\u60c5\u5831" }, "auth_captcha": { diff --git a/homeassistant/components/switch/translations/it.json b/homeassistant/components/switch/translations/it.json index 4ffd50e538f..6f1b1994e5e 100644 --- a/homeassistant/components/switch/translations/it.json +++ b/homeassistant/components/switch/translations/it.json @@ -1,9 +1,9 @@ { "device_automation": { "action_type": { - "toggle": "Attivare / Disattivare {entity_name}", - "turn_off": "Disattivare {entity_name}", - "turn_on": "Attivare {entity_name}" + "toggle": "Attiva/Disattiva {entity_name}", + "turn_off": "Disattiva {entity_name}", + "turn_on": "Attiva {entity_name}" }, "condition_type": { "is_off": "{entity_name} \u00e8 disattivato", diff --git a/homeassistant/components/switchbot/translations/it.json b/homeassistant/components/switchbot/translations/it.json index 9529450232b..68071ec94e5 100644 --- a/homeassistant/components/switchbot/translations/it.json +++ b/homeassistant/components/switchbot/translations/it.json @@ -9,8 +9,8 @@ }, "error": { "cannot_connect": "Impossibile connettersi", - "one": "Vuoto", - "other": "Vuoti" + "one": "Pi\u00f9", + "other": "Altri" }, "flow_title": "{name}", "step": { @@ -20,7 +20,7 @@ "name": "Nome", "password": "Password" }, - "title": "Impostare il dispositivo Switchbot" + "title": "Imposta il dispositivo Switchbot" } } }, diff --git a/homeassistant/components/syncthing/translations/it.json b/homeassistant/components/syncthing/translations/it.json index 2333b09093a..0973a3ccf92 100644 --- a/homeassistant/components/syncthing/translations/it.json +++ b/homeassistant/components/syncthing/translations/it.json @@ -13,7 +13,7 @@ "title": "Configurazione integrazione Syncthing", "token": "Token", "url": "URL", - "verify_ssl": "Verificare il certificato SSL" + "verify_ssl": "Verifica il certificato SSL" } } } diff --git a/homeassistant/components/syncthru/translations/it.json b/homeassistant/components/syncthru/translations/it.json index d04af9a2a5b..e9335c73a40 100644 --- a/homeassistant/components/syncthru/translations/it.json +++ b/homeassistant/components/syncthru/translations/it.json @@ -6,7 +6,7 @@ "error": { "invalid_url": "URL non valido", "syncthru_not_supported": "Il dispositivo non supporta SyncThru", - "unknown_state": "Stato della stampante sconosciuto, verificare l'URL e la connettivit\u00e0 di rete" + "unknown_state": "Stato della stampante sconosciuto, verifica l'URL e la connettivit\u00e0 di rete" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/synology_dsm/translations/it.json b/homeassistant/components/synology_dsm/translations/it.json index eb4a7f6df8d..036a48cd022 100644 --- a/homeassistant/components/synology_dsm/translations/it.json +++ b/homeassistant/components/synology_dsm/translations/it.json @@ -8,8 +8,8 @@ "error": { "cannot_connect": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida", - "missing_data": "Dati mancanti: si prega di riprovare pi\u00f9 tardi o un'altra configurazione", - "otp_failed": "Autenticazione in due fasi fallita, riprovare con un nuovo codice di accesso", + "missing_data": "Dati mancanti: riprova pi\u00f9 tardi o un'altra configurazione", + "otp_failed": "Autenticazione in due fasi non riuscita, riprova con un nuovo codice di accesso", "unknown": "Errore imprevisto" }, "flow_title": "{name} ({host})", @@ -26,7 +26,7 @@ "port": "Porta", "ssl": "Utilizza un certificato SSL", "username": "Nome utente", - "verify_ssl": "Verificare il certificato SSL" + "verify_ssl": "Verifica il certificato SSL" }, "description": "Vuoi impostare {name} ({host})?", "title": "Synology DSM" @@ -53,7 +53,7 @@ "port": "Porta", "ssl": "Utilizza un certificato SSL", "username": "Nome utente", - "verify_ssl": "Verificare il certificato SSL" + "verify_ssl": "Verifica il certificato SSL" }, "title": "Synology DSM" } diff --git a/homeassistant/components/tado/translations/it.json b/homeassistant/components/tado/translations/it.json index 2c09ca74312..8527d440710 100644 --- a/homeassistant/components/tado/translations/it.json +++ b/homeassistant/components/tado/translations/it.json @@ -23,10 +23,10 @@ "step": { "init": { "data": { - "fallback": "Abilitare la modalit\u00e0 di fallback." + "fallback": "Abilita la modalit\u00e0 di ripiego." }, "description": "La modalit\u00e0 di fallback passer\u00e0 a Smart Schedule al prossimo cambio di programma dopo aver regolato manualmente una zona.", - "title": "Regolare le opzioni di Tado." + "title": "Regola le opzioni di Tado." } } } diff --git a/homeassistant/components/tellduslive/translations/it.json b/homeassistant/components/tellduslive/translations/it.json index 8c879798a45..0d941b371b3 100644 --- a/homeassistant/components/tellduslive/translations/it.json +++ b/homeassistant/components/tellduslive/translations/it.json @@ -11,7 +11,7 @@ }, "step": { "auth": { - "description": "Per collegare il tuo account TelldusLive:\n 1. Clicca sul link sottostante\n 2. Accedi a Telldus Live\n 3. Autorizzare **{app_name}** (cliccare **S\u00ec**).\n 4. Torna qui e clicca su **SUBMIT**.\n\n [Link per account TelldusLive]({auth_url})", + "description": "Per collegare il tuo account TelldusLive:\n 1. Fai clic sul collegamento sottostante\n 2. Accedi a Telldus Live\n 3. Autorizzare **{app_name}** (fai clic su **S\u00ec**).\n 4. Torna qui e fai clic su **INVIA**.\n\n [Collegamento per account TelldusLive]({auth_url})", "title": "Autenticati con TelldusLive" }, "user": { diff --git a/homeassistant/components/tile/translations/bg.json b/homeassistant/components/tile/translations/bg.json index debdbdaaf87..a1394d94305 100644 --- a/homeassistant/components/tile/translations/bg.json +++ b/homeassistant/components/tile/translations/bg.json @@ -1,12 +1,18 @@ { "config": { "abort": { - "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" }, "error": { "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + } + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", diff --git a/homeassistant/components/tile/translations/et.json b/homeassistant/components/tile/translations/et.json index de0f91daece..99622e0426d 100644 --- a/homeassistant/components/tile/translations/et.json +++ b/homeassistant/components/tile/translations/et.json @@ -1,12 +1,19 @@ { "config": { "abort": { - "already_configured": "Konto on juba seadistatud" + "already_configured": "Konto on juba seadistatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "invalid_auth": "Tuvastamise viga" }, "step": { + "reauth_confirm": { + "data": { + "password": "Salas\u00f5na" + }, + "title": "Taastuvasta Tile" + }, "user": { "data": { "password": "Salas\u00f5na", diff --git a/homeassistant/components/tile/translations/he.json b/homeassistant/components/tile/translations/he.json index adb5e510107..78cceae2cdd 100644 --- a/homeassistant/components/tile/translations/he.json +++ b/homeassistant/components/tile/translations/he.json @@ -1,12 +1,18 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "error": { "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + } + }, "user": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", diff --git a/homeassistant/components/tile/translations/hu.json b/homeassistant/components/tile/translations/hu.json index c4a6e63030c..12332a1f67f 100644 --- a/homeassistant/components/tile/translations/hu.json +++ b/homeassistant/components/tile/translations/hu.json @@ -1,12 +1,19 @@ { "config": { "abort": { - "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, "step": { + "reauth_confirm": { + "data": { + "password": "Jelsz\u00f3" + }, + "title": "Csempe (Tile) \u00fajrahiteles\u00edt\u00e9se" + }, "user": { "data": { "password": "Jelsz\u00f3", diff --git a/homeassistant/components/tile/translations/it.json b/homeassistant/components/tile/translations/it.json index 2b4cbacaa39..89112e61f2a 100644 --- a/homeassistant/components/tile/translations/it.json +++ b/homeassistant/components/tile/translations/it.json @@ -1,12 +1,19 @@ { "config": { "abort": { - "already_configured": "L'account \u00e8 gi\u00e0 configurato" + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "invalid_auth": "Autenticazione non valida" }, "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "title": "Autentica nuovamente il riquadro" + }, "user": { "data": { "password": "Password", diff --git a/homeassistant/components/tile/translations/ja.json b/homeassistant/components/tile/translations/ja.json index 9dd693379bd..5c8f74c71eb 100644 --- a/homeassistant/components/tile/translations/ja.json +++ b/homeassistant/components/tile/translations/ja.json @@ -1,12 +1,19 @@ { "config": { "abort": { - "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, + "title": "\u30bf\u30a4\u30eb\u306e\u518d\u8a8d\u8a3c" + }, "user": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", diff --git a/homeassistant/components/tile/translations/no.json b/homeassistant/components/tile/translations/no.json index 182e2eb654a..c449beb2382 100644 --- a/homeassistant/components/tile/translations/no.json +++ b/homeassistant/components/tile/translations/no.json @@ -11,7 +11,8 @@ "reauth_confirm": { "data": { "password": "Passord" - } + }, + "title": "Autentiser Tile p\u00e5 nytt" }, "user": { "data": { diff --git a/homeassistant/components/tile/translations/ru.json b/homeassistant/components/tile/translations/ru.json index f42a4d631b0..490e5f28252 100644 --- a/homeassistant/components/tile/translations/ru.json +++ b/homeassistant/components/tile/translations/ru.json @@ -1,12 +1,19 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "error": { "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b\u044f" + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", diff --git a/homeassistant/components/tile/translations/zh-Hant.json b/homeassistant/components/tile/translations/zh-Hant.json index b44a8a1381f..3ee6ef671fd 100644 --- a/homeassistant/components/tile/translations/zh-Hant.json +++ b/homeassistant/components/tile/translations/zh-Hant.json @@ -1,12 +1,19 @@ { "config": { "abort": { - "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u5bc6\u78bc" + }, + "title": "\u91cd\u65b0\u8a8d\u8b49 Tile" + }, "user": { "data": { "password": "\u5bc6\u78bc", diff --git a/homeassistant/components/toon/translations/it.json b/homeassistant/components/toon/translations/it.json index 72564e03674..af492ab500c 100644 --- a/homeassistant/components/toon/translations/it.json +++ b/homeassistant/components/toon/translations/it.json @@ -13,7 +13,7 @@ "data": { "agreement": "Accordo" }, - "description": "Selezionare l'indirizzo del contratto che si desidera aggiungere.", + "description": "Seleziona l'indirizzo del contratto che si desidera aggiungere.", "title": "Seleziona il tuo contratto" }, "pick_implementation": { diff --git a/homeassistant/components/traccar/translations/it.json b/homeassistant/components/traccar/translations/it.json index 8c95b3cd022..a1f4b812806 100644 --- a/homeassistant/components/traccar/translations/it.json +++ b/homeassistant/components/traccar/translations/it.json @@ -10,7 +10,7 @@ "step": { "user": { "description": "Sei sicuro di voler configurare Traccar?", - "title": "Imposta Traccar" + "title": "Configura Traccar" } } } diff --git a/homeassistant/components/tuya/translations/it.json b/homeassistant/components/tuya/translations/it.json index 8ffb263be36..96d7d3e997b 100644 --- a/homeassistant/components/tuya/translations/it.json +++ b/homeassistant/components/tuya/translations/it.json @@ -71,7 +71,7 @@ "init": { "data": { "discovery_interval": "Intervallo di scansione di rilevamento dispositivo in secondi", - "list_devices": "Selezionare i dispositivi da configurare o lasciare vuoto per salvare la configurazione", + "list_devices": "Seleziona i dispositivi da configurare o lascia vuoto per salvare la configurazione", "query_device": "Selezionare il dispositivo che utilizzer\u00e0 il metodo di interrogazione per un pi\u00f9 rapido aggiornamento dello stato", "query_interval": "Intervallo di scansione di interrogazione dispositivo in secondi" }, diff --git a/homeassistant/components/twentemilieu/translations/it.json b/homeassistant/components/twentemilieu/translations/it.json index 886f0dd2ffd..d8d9570d8ca 100644 --- a/homeassistant/components/twentemilieu/translations/it.json +++ b/homeassistant/components/twentemilieu/translations/it.json @@ -12,7 +12,7 @@ "data": { "house_letter": "Edificio, Scala, Interno, ecc. / Informazioni aggiuntive", "house_number": "Numero civico", - "post_code": "Codice di Avviamento Postale" + "post_code": "CAP" }, "description": "Imposta Twente Milieu fornendo le informazioni sulla raccolta dei rifiuti al tuo indirizzo.", "title": "Twente Milieu" diff --git a/homeassistant/components/twilio/translations/it.json b/homeassistant/components/twilio/translations/it.json index a4fe3efcaa2..981e1964b20 100644 --- a/homeassistant/components/twilio/translations/it.json +++ b/homeassistant/components/twilio/translations/it.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "L'istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi webhook." }, "create_entry": { - "default": "Per inviare eventi a Home Assistant, dovrai configurare [Webhooks con Twilio]({twilio_url})\n\n Compila le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/x-www-form-urlencoded\n\n Vedi [la documentazione]({docs_url}) su come configurare le automazioni per gestire i dati in arrivo." + "default": "Per inviare eventi a Home Assistant, dovrai configurare [Webhook con Twilio]({twilio_url})\n\n Compila le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/x-www-form-urlencoded\n\n Vedi [la documentazione]({docs_url}) su come configurare le automazioni per gestire i dati in arrivo." }, "step": { "user": { diff --git a/homeassistant/components/unifi/translations/it.json b/homeassistant/components/unifi/translations/it.json index 389b98a26d9..eeda8a8c9bb 100644 --- a/homeassistant/components/unifi/translations/it.json +++ b/homeassistant/components/unifi/translations/it.json @@ -19,7 +19,7 @@ "port": "Porta", "site": "ID del sito", "username": "Nome utente", - "verify_ssl": "Verificare il certificato SSL" + "verify_ssl": "Verifica il certificato SSL" }, "title": "Configura la rete UniFi" } @@ -33,19 +33,19 @@ "dpi_restrictions": "Consenti il controllo dei gruppi di restrizione DPI", "poe_clients": "Consentire il controllo POE dei client" }, - "description": "Configurare i controlli client \n\nCreare interruttori per i numeri di serie dei quali si desidera controllare l'accesso alla rete.", + "description": "Configura i controlli client \n\nCrear interruttori per i numeri di serie dei quali si desidera controllare l'accesso alla rete.", "title": "Opzioni di rete UniFi 2/3" }, "device_tracker": { "data": { "detection_time": "Tempo in secondi dall'ultima volta che viene visto fino a quando non \u00e8 considerato lontano", "ignore_wired_bug": "Disabilita la logica dei bug cablati di rete UniFi", - "ssid_filter": "Selezionare gli SSID su cui tracciare i client wireless", + "ssid_filter": "Seleziona gli SSID su cui tracciare i client wireless", "track_clients": "Traccia i client di rete", - "track_devices": "Tracciare i dispositivi di rete (dispositivi Ubiquiti)", + "track_devices": "Traccia i dispositivi di rete (dispositivi Ubiquiti)", "track_wired_clients": "Includi i client di rete cablata" }, - "description": "Configurare il tracciamento del dispositivo", + "description": "Configura il tracciamento del dispositivo", "title": "Opzioni di rete UniFi 1/3" }, "init": { @@ -58,7 +58,7 @@ "data": { "block_client": "Client controllati per l'accesso alla rete", "track_clients": "Traccia i client di rete", - "track_devices": "Tracciare i dispositivi di rete (dispositivi Ubiquiti)" + "track_devices": "Traccia i dispositivi di rete (dispositivi Ubiquiti)" }, "description": "Configura l'integrazione della rete UniFi" }, diff --git a/homeassistant/components/upb/translations/it.json b/homeassistant/components/upb/translations/it.json index 8811ebadc18..8264efdbcf2 100644 --- a/homeassistant/components/upb/translations/it.json +++ b/homeassistant/components/upb/translations/it.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "Impossibile connettersi", - "invalid_upb_file": "File di esportazione UPStart UPB mancante o non valido, controllare il nome e il percorso del file.", + "invalid_upb_file": "File di esportazione UPStart UPB mancante o non valido, controlla il nome e il percorso del file.", "unknown": "Errore imprevisto" }, "step": { @@ -15,7 +15,7 @@ "file_path": "Percorso e nome del file di esportazione UPStart UPB.", "protocol": "Protocollo" }, - "description": "Collegare un Modulo Interfaccia Powerline del Bus Universale Powerline (UPB PIM). La stringa dell'indirizzo deve essere nel formato 'address[:port]' per 'tcp'. La porta \u00e8 facoltativa e il valore predefinito \u00e8 2101. Esempio: '192.168.1.42'. Per il protocollo seriale, l'indirizzo deve essere nella forma 'tty[:baud]'. Baud \u00e8 opzionale e il valore predefinito \u00e8 4800. Esempio: '/dev/ttyS1'.", + "description": "Collega un Modulo Interfaccia Powerline del Bus Universale Powerline (UPB PIM). La stringa dell'indirizzo deve essere nel formato 'address[:port]' per 'tcp'. La porta \u00e8 facoltativa e il valore predefinito \u00e8 2101. Esempio: '192.168.1.42'. Per il protocollo seriale, l'indirizzo deve essere nella forma 'tty[:baud]'. Baud \u00e8 opzionale e il valore predefinito \u00e8 4800. Esempio: '/dev/ttyS1'.", "title": "Collegamento a UPB PIM" } } diff --git a/homeassistant/components/upnp/translations/it.json b/homeassistant/components/upnp/translations/it.json index 0e00d002422..ce637bbad08 100644 --- a/homeassistant/components/upnp/translations/it.json +++ b/homeassistant/components/upnp/translations/it.json @@ -6,8 +6,8 @@ "no_devices_found": "Nessun dispositivo trovato sulla rete" }, "error": { - "one": "Vuoto", - "other": "Vuoto" + "one": "Pi\u00f9", + "other": "Altri" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/vicare/translations/no.json b/homeassistant/components/vicare/translations/no.json index 0f88d6b219d..6f2cde73484 100644 --- a/homeassistant/components/vicare/translations/no.json +++ b/homeassistant/components/vicare/translations/no.json @@ -1,14 +1,24 @@ { "config": { + "abort": { + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.", + "unknown": "Uventet feil" + }, "error": { "invalid_auth": "Ugyldig godkjenning" }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { + "client_id": "API-n\u00f8kkel", + "heating_type": "Oppvarmingstype", + "name": "Navn", "password": "Passord", + "scan_interval": "Skanneintervall (sekunder)", "username": "E-post" }, + "description": "Sett opp ViCare-integrasjon. Hvis du vil generere API-n\u00f8kkel, g\u00e5r du til https://developer.viessmann.com", "title": "" } } diff --git a/homeassistant/components/vilfo/translations/it.json b/homeassistant/components/vilfo/translations/it.json index ad15381b1db..7d30b0017fb 100644 --- a/homeassistant/components/vilfo/translations/it.json +++ b/homeassistant/components/vilfo/translations/it.json @@ -14,7 +14,7 @@ "access_token": "Token di accesso", "host": "Host" }, - "description": "Configurare l'integrazione del Vilfo Router. \u00c8 necessario il vostro hostname/IP del Vilfo Router e un token di accesso API. Per ulteriori informazioni su questa integrazione e su come ottenere tali dettagli, visitare il sito: https://www.home-assistant.io/integrations/vilfo", + "description": "Configura l'integrazione del Vilfo Router. Hai bisogno del tuo nome host/IP del Vilfo Router e un token di accesso API. Per ulteriori informazioni su questa integrazione e su come ottenere tali dettagli, visita il sito: https://www.home-assistant.io/integrations/vilfo", "title": "Collegamento al Vilfo Router" } } diff --git a/homeassistant/components/vizio/translations/it.json b/homeassistant/components/vizio/translations/it.json index 6dad0225765..b0bd1de7077 100644 --- a/homeassistant/components/vizio/translations/it.json +++ b/homeassistant/components/vizio/translations/it.json @@ -7,7 +7,7 @@ }, "error": { "cannot_connect": "Impossibile connettersi", - "complete_pairing_failed": "Impossibile completare l'associazione. Assicurarsi che il PIN fornito sia corretto e che la TV sia ancora accesa e collegata alla rete prima di inviare nuovamente.", + "complete_pairing_failed": "Impossibile completare l'associazione. Assicurati che il PIN fornito sia corretto e che la TV sia ancora accesa e collegata alla rete prima di inviare nuovamente.", "existing_config_entry_found": "\u00c8 gi\u00e0 stata configurata una voce di configurazione esistente Dispositivo SmartCast VIZIO con lo stesso numero di serie. Devi eliminare la voce esistente per configurare questa." }, "step": { @@ -33,7 +33,7 @@ "host": "Host", "name": "Nome" }, - "description": "Un Token di accesso \u00e8 necessario solo per i televisori. Se si sta configurando un televisore e non si dispone ancora di un Token di accesso, lasciarlo vuoto per passare attraverso un processo di associazione.", + "description": "Un token di accesso \u00e8 necessario solo per i televisori. Se stai configurando un televisore e non disponi ancora di un token di accesso, lascialo vuoto per passare attraverso un processo di associazione.", "title": "Dispositivo SmartCast VIZIO" } } diff --git a/homeassistant/components/waze_travel_time/translations/it.json b/homeassistant/components/waze_travel_time/translations/it.json index bfbe94c2a23..95ff6a1d30b 100644 --- a/homeassistant/components/waze_travel_time/translations/it.json +++ b/homeassistant/components/waze_travel_time/translations/it.json @@ -22,8 +22,8 @@ "step": { "init": { "data": { - "avoid_ferries": "Evitare i traghetti?", - "avoid_subscription_roads": "Evitare le strade che richiedono una vignetta/abbonamento?", + "avoid_ferries": "Vuoi evitare i traghetti?", + "avoid_subscription_roads": "Vuoi evitare le strade che richiedono una vignetta/abbonamento?", "avoid_toll_roads": "Evitare le strade a pedaggio?", "excl_filter": "Sottostringa NON nella descrizione del percorso selezionato", "incl_filter": "Sottostringa nella descrizione del percorso selezionato", diff --git a/homeassistant/components/wiffi/translations/it.json b/homeassistant/components/wiffi/translations/it.json index 054bcbc9862..bab9e839310 100644 --- a/homeassistant/components/wiffi/translations/it.json +++ b/homeassistant/components/wiffi/translations/it.json @@ -9,7 +9,7 @@ "data": { "port": "Porta" }, - "title": "Configurare il server TCP per i dispositivi WIFFI" + "title": "Configura il server TCP per i dispositivi WIFFI" } } }, diff --git a/homeassistant/components/withings/translations/ja.json b/homeassistant/components/withings/translations/ja.json index c409579fbc8..bc70bf0c746 100644 --- a/homeassistant/components/withings/translations/ja.json +++ b/homeassistant/components/withings/translations/ja.json @@ -21,7 +21,7 @@ "data": { "profile": "\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u540d" }, - "description": "\u3053\u306e\u30c7\u30fc\u30bf\u306b\u56fa\u6709\u306e\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u540d\u3092\u6307\u5b9a\u3057\u307e\u3059\u3002\u901a\u5e38\u3001\u3053\u308c\u306f\u524d\u306e\u624b\u9806\u3067\u9078\u629e\u3057\u305f\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u306e\u540d\u524d\u3067\u3059\u3002", + "description": "\u3053\u306e\u30c7\u30fc\u30bf\u306b\u30e6\u30cb\u30fc\u30af(\u4e00\u610f)\u306a\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u540d\u3092\u6307\u5b9a\u3057\u307e\u3059\u3002\u901a\u5e38\u3001\u3053\u308c\u306f\u524d\u306e\u624b\u9806\u3067\u9078\u629e\u3057\u305f\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u306e\u540d\u524d\u3067\u3059\u3002", "title": "\u30e6\u30fc\u30b6\u30fc\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u3002" }, "reauth": { diff --git a/homeassistant/components/wolflink/translations/it.json b/homeassistant/components/wolflink/translations/it.json index 6e88a6b3e29..7d41f47cd2d 100644 --- a/homeassistant/components/wolflink/translations/it.json +++ b/homeassistant/components/wolflink/translations/it.json @@ -13,7 +13,7 @@ "data": { "device_name": "Dispositivo" }, - "title": "Selezionare il dispositivo WOLF" + "title": "Seleziona il dispositivo WOLF" }, "user": { "data": { diff --git a/homeassistant/components/xiaomi_aqara/translations/it.json b/homeassistant/components/xiaomi_aqara/translations/it.json index a730a2e24d9..fa1999fc877 100644 --- a/homeassistant/components/xiaomi_aqara/translations/it.json +++ b/homeassistant/components/xiaomi_aqara/translations/it.json @@ -6,7 +6,7 @@ "not_xiaomi_aqara": "Non \u00e8 un Gateway Xiaomi Aqara, il dispositivo scoperto non corrisponde ai gateway noti" }, "error": { - "discovery_error": "Impossibile individuare un gateway Xiaomi Aqara, provare a utilizzare l'IP del dispositivo che esegue HomeAssistant come interfaccia", + "discovery_error": "Impossibile individuare un gateway Xiaomi Aqara, prova a utilizzare l'IP del dispositivo che esegue HomeAssistant come interfaccia", "invalid_host": "Nome host o indirizzo IP non valido, vedere https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", "invalid_interface": "Interfaccia di rete non valida", "invalid_key": "Chiave gateway non valida", @@ -19,7 +19,7 @@ "select_ip": "Indirizzo IP" }, "description": "Esegui di nuovo la configurazione se desideri connettere gateway aggiuntivi", - "title": "Selezionare il Gateway Xiaomi Aqara che si desidera collegare" + "title": "Seleziona il Gateway Xiaomi Aqara che si desidera collegare" }, "settings": { "data": { diff --git a/homeassistant/components/xiaomi_miio/translations/it.json b/homeassistant/components/xiaomi_miio/translations/it.json index 56198ab443a..5e32a09fb3b 100644 --- a/homeassistant/components/xiaomi_miio/translations/it.json +++ b/homeassistant/components/xiaomi_miio/translations/it.json @@ -12,7 +12,7 @@ "cloud_credentials_incomplete": "Credenziali cloud incomplete, inserisci nome utente, password e paese", "cloud_login_error": "Impossibile accedere a Xioami Miio Cloud, controlla le credenziali.", "cloud_no_devices": "Nessun dispositivo trovato in questo account cloud Xiaomi Miio.", - "no_device_selected": "Nessun dispositivo selezionato, selezionare un dispositivo.", + "no_device_selected": "Nessun dispositivo selezionato, seleziona un dispositivo.", "unknown_device": "Il modello del dispositivo non \u00e8 noto, non \u00e8 possibile configurare il dispositivo utilizzando il flusso di configurazione.", "wrong_token": "Errore del codice di controllo, token errato" }, @@ -77,7 +77,7 @@ "data": { "gateway": "Connettiti a un Xiaomi Gateway" }, - "description": "Selezionare a quale dispositivo si desidera collegare.", + "description": "Seleziona a quale dispositivo desideri collegarti.", "title": "Xiaomi Miio" } } @@ -91,7 +91,7 @@ "data": { "cloud_subdevices": "Usa il cloud per connettere i sottodispositivi" }, - "description": "Specificare le impostazioni opzionali", + "description": "Specifica le impostazioni opzionali", "title": "Xiaomi Miio" } } diff --git a/homeassistant/components/yeelight/translations/it.json b/homeassistant/components/yeelight/translations/it.json index ce34523bb61..ca200674871 100644 --- a/homeassistant/components/yeelight/translations/it.json +++ b/homeassistant/components/yeelight/translations/it.json @@ -21,7 +21,7 @@ "data": { "host": "Host" }, - "description": "Se lasci l'host vuoto, il rilevamento verr\u00e0 utilizzato per trovare i dispositivi." + "description": "Se lasci l'host vuoto, il rilevamento sar\u00e0 utilizzato per trovare i dispositivi." } } }, diff --git a/homeassistant/components/zha/translations/it.json b/homeassistant/components/zha/translations/it.json index ba2427ebd4c..884c6429cad 100644 --- a/homeassistant/components/zha/translations/it.json +++ b/homeassistant/components/zha/translations/it.json @@ -33,15 +33,15 @@ "data": { "path": "Percorso del dispositivo seriale" }, - "description": "Selezionare la porta seriale per la radio Zigbee", + "description": "Seleziona la porta seriale per la radio Zigbee", "title": "ZHA" } } }, "config_panel": { "zha_alarm_options": { - "alarm_arm_requires_code": "Codice necessario per le azioni di armamento", - "alarm_failed_tries": "Il numero di inserimenti consecutivi di codici falliti per attivare un allarme", + "alarm_arm_requires_code": "Codice necessario per le azioni di attivazione", + "alarm_failed_tries": "Il numero di inserimenti consecutivi di codici non validi per attivare un allarme", "alarm_master_code": "Codice principale per i pannelli di controllo degli allarmi", "title": "Opzioni del pannello di controllo degli allarmi" }, @@ -68,7 +68,7 @@ "button_6": "Sesto pulsante", "close": "Chiudere", "dim_down": "Diminuire luminosit\u00e0", - "dim_up": "Aumentare luminosit\u00e0", + "dim_up": "Aumenta luminosit\u00e0", "face_1": "con faccia 1 attivata", "face_2": "con faccia 2 attivata", "face_3": "con faccia 3 attivata", diff --git a/homeassistant/components/zoneminder/translations/it.json b/homeassistant/components/zoneminder/translations/it.json index cf2a3a63553..7078747dc97 100644 --- a/homeassistant/components/zoneminder/translations/it.json +++ b/homeassistant/components/zoneminder/translations/it.json @@ -25,9 +25,9 @@ "path_zms": "Percorso ZMS", "ssl": "Utilizza un certificato SSL", "username": "Nome utente", - "verify_ssl": "Verificare il certificato SSL" + "verify_ssl": "Verifica il certificato SSL" }, - "title": "Aggiungi Server ZoneMinder." + "title": "Aggiungi server ZoneMinder." } } } diff --git a/homeassistant/components/zwave/translations/it.json b/homeassistant/components/zwave/translations/it.json index a99cc241633..17207c23b50 100644 --- a/homeassistant/components/zwave/translations/it.json +++ b/homeassistant/components/zwave/translations/it.json @@ -5,7 +5,7 @@ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." }, "error": { - "option_error": "Convalida Z-Wave fallita. Il percorso della chiavetta USB \u00e8 corretto?" + "option_error": "Convalida Z-Wave non riuscita. Il percorso della chiavetta USB \u00e8 corretto?" }, "step": { "user": { diff --git a/homeassistant/components/zwave_js/translations/it.json b/homeassistant/components/zwave_js/translations/it.json index 310aa56fcc4..70c7062bc57 100644 --- a/homeassistant/components/zwave_js/translations/it.json +++ b/homeassistant/components/zwave_js/translations/it.json @@ -67,7 +67,7 @@ "clear_lock_usercode": "Cancella codice utente su {entity_name}", "ping": "Dispositivo ping", "refresh_value": "Aggiorna il/i valore/i per {entity_name}", - "reset_meter": "Azzerare i contatori su {subtype}", + "reset_meter": "Azzera i contatori su {subtype}", "set_config_parameter": "Imposta il valore del parametro di configurazione {subtype}", "set_lock_usercode": "Imposta un codice utente su {entity_name}", "set_value": "Imposta un valore Z-Wave" @@ -111,7 +111,7 @@ "step": { "configure_addon": { "data": { - "emulate_hardware": "Emulare l'hardware", + "emulate_hardware": "Emula l'hardware", "log_level": "Livello di registro", "network_key": "Chiave di rete", "s0_legacy_key": "Chiave S0 (Obsoleta)", From 4475e88707139184d220a9c97dc643e2a259be7f Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 21 Dec 2021 23:36:12 -0500 Subject: [PATCH 0914/2644] Fix Sonos updating when entities are disabled (#62456) Co-authored-by: J. Nick Koston --- .../components/sonos/binary_sensor.py | 4 +++ homeassistant/components/sonos/const.py | 1 - homeassistant/components/sonos/entity.py | 9 +----- .../components/sonos/media_player.py | 1 + homeassistant/components/sonos/number.py | 7 +++++ homeassistant/components/sonos/sensor.py | 6 ++++ homeassistant/components/sonos/speaker.py | 28 ++++--------------- 7 files changed, 24 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/sonos/binary_sensor.py b/homeassistant/components/sonos/binary_sensor.py index bab153175e8..e3545552bee 100644 --- a/homeassistant/components/sonos/binary_sensor.py +++ b/homeassistant/components/sonos/binary_sensor.py @@ -1,6 +1,7 @@ """Entity representing a Sonos power sensor.""" from __future__ import annotations +import logging from typing import Any from homeassistant.components.binary_sensor import ( @@ -16,11 +17,14 @@ from .speaker import SonosSpeaker ATTR_BATTERY_POWER_SOURCE = "power_source" +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Sonos from a config entry.""" async def _async_create_entity(speaker: SonosSpeaker) -> None: + _LOGGER.debug("Creating battery binary_sensor on %s", speaker.zone_name) entity = SonosPowerEntity(speaker) async_add_entities([entity]) diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py index 523ac9f561b..bffc6425928 100644 --- a/homeassistant/components/sonos/const.py +++ b/homeassistant/components/sonos/const.py @@ -149,7 +149,6 @@ SONOS_CREATE_BATTERY = "sonos_create_battery" SONOS_CREATE_SWITCHES = "sonos_create_switches" SONOS_CREATE_LEVELS = "sonos_create_levels" SONOS_CREATE_MEDIA_PLAYER = "sonos_create_media_player" -SONOS_ENTITY_CREATED = "sonos_entity_created" SONOS_POLL_UPDATE = "sonos_poll_update" SONOS_ALARMS_UPDATED = "sonos_alarms_updated" SONOS_FAVORITES_UPDATED = "sonos_favorites_updated" diff --git a/homeassistant/components/sonos/entity.py b/homeassistant/components/sonos/entity.py index d8196ffdfa6..65f73eaf3f0 100644 --- a/homeassistant/components/sonos/entity.py +++ b/homeassistant/components/sonos/entity.py @@ -10,15 +10,11 @@ from soco.core import SoCo from soco.exceptions import SoCoException import homeassistant.helpers.device_registry as dr -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, Entity from .const import ( DOMAIN, - SONOS_ENTITY_CREATED, SONOS_FAVORITES_UPDATED, SONOS_POLL_UPDATE, SONOS_STATE_UPDATED, @@ -60,9 +56,6 @@ class SonosEntity(Entity): self.async_write_ha_state, ) ) - async_dispatcher_send( - self.hass, f"{SONOS_ENTITY_CREATED}-{self.soco.uid}", self.platform.domain - ) async def async_poll(self, now: datetime.datetime) -> None: """Poll the entity if subscriptions fail.""" diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 90c33d7a4e6..f4dbf8f87d0 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -132,6 +132,7 @@ async def async_setup_entry( @callback def async_create_entities(speaker: SonosSpeaker) -> None: """Handle device discovery and create entities.""" + _LOGGER.debug("Creating media_player on %s", speaker.zone_name) async_add_entities([SonosMediaPlayerEntity(speaker)]) @service.verify_domain_control(hass, SONOS_DOMAIN) diff --git a/homeassistant/components/sonos/number.py b/homeassistant/components/sonos/number.py index c1b0ffcfd3b..afe7effed53 100644 --- a/homeassistant/components/sonos/number.py +++ b/homeassistant/components/sonos/number.py @@ -1,6 +1,8 @@ """Entity representing a Sonos number control.""" from __future__ import annotations +import logging + from homeassistant.components.number import NumberEntity from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -13,6 +15,8 @@ from .speaker import SonosSpeaker LEVEL_TYPES = ("bass", "treble") +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Sonos number platform from a config entry.""" @@ -21,6 +25,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): def _async_create_entities(speaker: SonosSpeaker) -> None: entities = [] for level_type in LEVEL_TYPES: + _LOGGER.debug( + "Creating %s number control on %s", level_type, speaker.zone_name + ) entities.append(SonosLevelEntity(speaker, level_type)) async_add_entities(entities) diff --git a/homeassistant/components/sonos/sensor.py b/homeassistant/components/sonos/sensor.py index 6a1162a6aa6..d735a7be1e3 100644 --- a/homeassistant/components/sonos/sensor.py +++ b/homeassistant/components/sonos/sensor.py @@ -1,6 +1,8 @@ """Entity representing a Sonos battery level.""" from __future__ import annotations +import logging + from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.const import PERCENTAGE from homeassistant.core import callback @@ -11,6 +13,8 @@ from .const import SONOS_CREATE_AUDIO_FORMAT_SENSOR, SONOS_CREATE_BATTERY from .entity import SonosEntity from .speaker import SonosSpeaker +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Sonos from a config entry.""" @@ -19,11 +23,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): def _async_create_audio_format_entity( speaker: SonosSpeaker, audio_format: str ) -> None: + _LOGGER.debug("Creating audio input format sensor on %s", speaker.zone_name) entity = SonosAudioInputFormatSensorEntity(speaker, audio_format) async_add_entities([entity]) @callback def _async_create_battery_sensor(speaker: SonosSpeaker) -> None: + _LOGGER.debug("Creating battery level sensor on %s", speaker.zone_name) entity = SonosBatteryEntity(speaker) async_add_entities([entity]) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 30b240c9fd7..8cd2abcf2b2 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -20,10 +20,7 @@ from soco.music_library import MusicLibrary from soco.plugins.sharelink import ShareLinkPlugin from soco.snapshot import Snapshot -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.media_player import DOMAIN as MP_DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError @@ -42,7 +39,6 @@ from .const import ( BATTERY_SCAN_INTERVAL, DATA_SONOS, DOMAIN, - PLATFORMS, SCAN_INTERVAL, SONOS_CHECK_ACTIVITY, SONOS_CREATE_ALARM, @@ -51,7 +47,6 @@ from .const import ( SONOS_CREATE_LEVELS, SONOS_CREATE_MEDIA_PLAYER, SONOS_CREATE_SWITCHES, - SONOS_ENTITY_CREATED, SONOS_POLL_UPDATE, SONOS_REBOOTED, SONOS_SPEAKER_ACTIVITY, @@ -161,9 +156,6 @@ class SonosSpeaker: self._share_link_plugin: ShareLinkPlugin | None = None self.available = True - # Synchronization helpers - self._platforms_ready: set[str] = set() - # Subscriptions and events self.subscriptions_failed: bool = False self._subscriptions: list[SubscriptionBase] = [] @@ -217,7 +209,6 @@ class SonosSpeaker: dispatch_pairs = ( (SONOS_CHECK_ACTIVITY, self.async_check_activity), (SONOS_SPEAKER_ADDED, self.update_group_for_uid), - (f"{SONOS_ENTITY_CREATED}-{self.soco.uid}", self.async_handle_new_entity), (f"{SONOS_REBOOTED}-{self.soco.uid}", self.async_rebooted), (f"{SONOS_SPEAKER_ACTIVITY}-{self.soco.uid}", self.speaker_activity), ) @@ -253,15 +244,11 @@ class SonosSpeaker: self.hass, self.async_poll_battery, BATTERY_SCAN_INTERVAL ) dispatcher_send(self.hass, SONOS_CREATE_BATTERY, self) - else: - self._platforms_ready.update({BINARY_SENSOR_DOMAIN, SENSOR_DOMAIN}) if new_alarms := [ alarm.alarm_id for alarm in self.alarms if alarm.zone.uid == self.soco.uid ]: dispatcher_send(self.hass, SONOS_CREATE_ALARM, self, new_alarms) - else: - self._platforms_ready.add(SWITCH_DOMAIN) dispatcher_send(self.hass, SONOS_CREATE_SWITCHES, self) @@ -277,19 +264,11 @@ class SonosSpeaker: dispatcher_send(self.hass, SONOS_CREATE_MEDIA_PLAYER, self) dispatcher_send(self.hass, SONOS_SPEAKER_ADDED, self.soco.uid) + self.hass.create_task(self.async_subscribe()) + # # Entity management # - async def async_handle_new_entity(self, entity_type: str) -> None: - """Listen to new entities to trigger first subscription.""" - if self._platforms_ready == PLATFORMS: - return - - self._platforms_ready.add(entity_type) - if self._platforms_ready == PLATFORMS: - self._resubscription_lock = asyncio.Lock() - await self.async_subscribe() - def write_entity_states(self) -> None: """Write states for associated SonosEntity instances.""" dispatcher_send(self.hass, f"{SONOS_STATE_UPDATED}-{self.soco.uid}") @@ -405,6 +384,9 @@ class SonosSpeaker: async def async_resubscribe(self, exception: Exception) -> None: """Attempt to resubscribe when a renewal failure is detected.""" + if not self._resubscription_lock: + self._resubscription_lock = asyncio.Lock() + async with self._resubscription_lock: if not self.available: return From 42c7f1dd1f9fa97859292d1f173b968a245eae9e Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Wed, 22 Dec 2021 05:36:27 +0100 Subject: [PATCH 0915/2644] Update xknx to version 0.18.15 (#62557) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 21ac4ce9ea4..a0250a0cc92 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/knx", "requirements": [ - "xknx==0.18.14" + "xknx==0.18.15" ], "codeowners": [ "@Julius2342", diff --git a/requirements_all.txt b/requirements_all.txt index 52ef54a7ab7..f1b5382cfbd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2461,7 +2461,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.knx -xknx==0.18.14 +xknx==0.18.15 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index df3a01cd66a..ceba6d19dbd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1462,7 +1462,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.knx -xknx==0.18.14 +xknx==0.18.15 # homeassistant.components.bluesound # homeassistant.components.fritz From a2be1a4402b937cc2c1ca979f0d8ca98e6013fb8 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 21 Dec 2021 21:36:37 -0700 Subject: [PATCH 0916/2644] Bump pytile to 2021.12.0 (#62559) --- homeassistant/components/tile/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tile/manifest.json b/homeassistant/components/tile/manifest.json index 4e9913615a9..8702f3b62bf 100644 --- a/homeassistant/components/tile/manifest.json +++ b/homeassistant/components/tile/manifest.json @@ -3,7 +3,7 @@ "name": "Tile", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tile", - "requirements": ["pytile==5.2.4"], + "requirements": ["pytile==2021.12.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index f1b5382cfbd..ed35c50a1e7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1974,7 +1974,7 @@ python_opendata_transport==0.2.1 pythonegardia==1.0.40 # homeassistant.components.tile -pytile==5.2.4 +pytile==2021.12.0 # homeassistant.components.touchline pytouchline==0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ceba6d19dbd..8cc174d9127 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1184,7 +1184,7 @@ python-twitch-client==0.6.0 python_awair==0.2.1 # homeassistant.components.tile -pytile==5.2.4 +pytile==2021.12.0 # homeassistant.components.traccar pytraccar==0.10.0 From 0c82a3c7b0973b524c2d79730000e4b6722bd51c Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 21 Dec 2021 23:37:17 -0500 Subject: [PATCH 0917/2644] Use platform enums in withings tests (#62551) --- tests/components/withings/test_sensor.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/components/withings/test_sensor.py b/tests/components/withings/test_sensor.py index 7e337da8afb..fbe9c6304bb 100644 --- a/tests/components/withings/test_sensor.py +++ b/tests/components/withings/test_sensor.py @@ -17,7 +17,6 @@ from withings_api.common import ( SleepModel, ) -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.withings.common import ( WITHINGS_MEASUREMENTS_MAP, WithingsAttribute, @@ -25,6 +24,7 @@ from homeassistant.components.withings.common import ( get_platform_attributes, ) from homeassistant.components.withings.const import Measurement +from homeassistant.const import Platform from homeassistant.core import HomeAssistant, State from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_registry import EntityRegistry @@ -310,14 +310,14 @@ async def test_sensor_default_enabled_entities( await component_factory.configure_component(profile_configs=(PERSON0,)) # Assert entities should not exist yet. - for attribute in get_platform_attributes(SENSOR_DOMAIN): + for attribute in get_platform_attributes(Platform.SENSOR): assert not await async_get_entity_id(hass, attribute, PERSON0.user_id) # person 0 await component_factory.setup_profile(PERSON0.user_id) # Assert entities should exist. - for attribute in get_platform_attributes(SENSOR_DOMAIN): + for attribute in get_platform_attributes(Platform.SENSOR): entity_id = await async_get_entity_id(hass, attribute, PERSON0.user_id) assert entity_id assert entity_registry.async_is_registered(entity_id) @@ -356,14 +356,14 @@ async def test_all_entities( await component_factory.configure_component(profile_configs=(PERSON0,)) # Assert entities should not exist yet. - for attribute in get_platform_attributes(SENSOR_DOMAIN): + for attribute in get_platform_attributes(Platform.SENSOR): assert not await async_get_entity_id(hass, attribute, PERSON0.user_id) # person 0 await component_factory.setup_profile(PERSON0.user_id) # Assert entities should exist. - for attribute in get_platform_attributes(SENSOR_DOMAIN): + for attribute in get_platform_attributes(Platform.SENSOR): entity_id = await async_get_entity_id(hass, attribute, PERSON0.user_id) assert entity_id assert entity_registry.async_is_registered(entity_id) From bf108b9d0d22764acfb8f013a4ef8d481d6f0ef0 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 21 Dec 2021 20:38:25 -0800 Subject: [PATCH 0918/2644] Update nest mac addresses based on newer generation hardware (#62525) Add nest mac addresses for cameras and doorbell devices added in the last few years. This is in perparation for improving nest discovery, which currently does not work great because it requires configuration.yaml --- homeassistant/components/nest/manifest.json | 7 ++++--- homeassistant/generated/dhcp.py | 12 ++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index bca37ce41c6..f943293775a 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -8,9 +8,10 @@ "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ - { - "macaddress": "18B430*" - } + { "macaddress": "18B430*" }, + { "macaddress": "641666*" }, + { "macaddress": "D8EB46*" }, + { "macaddress": "1C53F9*" } ], "iot_class": "cloud_push" } diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 3fef7f71d53..7f36fd509a1 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -175,6 +175,18 @@ DHCP = [ "domain": "nest", "macaddress": "18B430*" }, + { + "domain": "nest", + "macaddress": "641666*" + }, + { + "domain": "nest", + "macaddress": "D8EB46*" + }, + { + "domain": "nest", + "macaddress": "1C53F9*" + }, { "domain": "nexia", "hostname": "xl857-*", From ce9abdb520a0c191b26b1a36d28ea285ef06aa42 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 22 Dec 2021 00:01:01 -0500 Subject: [PATCH 0919/2644] Use platform enums in ring tests (#62565) --- tests/components/ring/test_light.py | 12 ++++++------ tests/components/ring/test_switch.py | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/components/ring/test_light.py b/tests/components/ring/test_light.py index 1b8150364bc..86c6033f480 100644 --- a/tests/components/ring/test_light.py +++ b/tests/components/ring/test_light.py @@ -1,5 +1,5 @@ """The tests for the Ring light platform.""" -from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.const import Platform from homeassistant.helpers import entity_registry as er from .common import setup_platform @@ -9,7 +9,7 @@ from tests.common import load_fixture async def test_entity_registry(hass, requests_mock): """Tests that the devices are registered in the entity registry.""" - await setup_platform(hass, LIGHT_DOMAIN) + await setup_platform(hass, Platform.LIGHT) entity_registry = er.async_get(hass) entry = entity_registry.async_get("light.front_light") @@ -21,7 +21,7 @@ async def test_entity_registry(hass, requests_mock): async def test_light_off_reports_correctly(hass, requests_mock): """Tests that the initial state of a device that should be off is correct.""" - await setup_platform(hass, LIGHT_DOMAIN) + await setup_platform(hass, Platform.LIGHT) state = hass.states.get("light.front_light") assert state.state == "off" @@ -30,7 +30,7 @@ async def test_light_off_reports_correctly(hass, requests_mock): async def test_light_on_reports_correctly(hass, requests_mock): """Tests that the initial state of a device that should be on is correct.""" - await setup_platform(hass, LIGHT_DOMAIN) + await setup_platform(hass, Platform.LIGHT) state = hass.states.get("light.internal_light") assert state.state == "on" @@ -39,7 +39,7 @@ async def test_light_on_reports_correctly(hass, requests_mock): async def test_light_can_be_turned_on(hass, requests_mock): """Tests the light turns on correctly.""" - await setup_platform(hass, LIGHT_DOMAIN) + await setup_platform(hass, Platform.LIGHT) # Mocks the response for turning a light on requests_mock.put( @@ -61,7 +61,7 @@ async def test_light_can_be_turned_on(hass, requests_mock): async def test_updates_work(hass, requests_mock): """Tests the update service works correctly.""" - await setup_platform(hass, LIGHT_DOMAIN) + await setup_platform(hass, Platform.LIGHT) state = hass.states.get("light.front_light") assert state.state == "off" # Changes the return to indicate that the light is now on. diff --git a/tests/components/ring/test_switch.py b/tests/components/ring/test_switch.py index ed4e9024292..14d0a8b213f 100644 --- a/tests/components/ring/test_switch.py +++ b/tests/components/ring/test_switch.py @@ -1,5 +1,5 @@ """The tests for the Ring switch platform.""" -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import Platform from homeassistant.helpers import entity_registry as er from .common import setup_platform @@ -9,7 +9,7 @@ from tests.common import load_fixture async def test_entity_registry(hass, requests_mock): """Tests that the devices are registered in the entity registry.""" - await setup_platform(hass, SWITCH_DOMAIN) + await setup_platform(hass, Platform.SWITCH) entity_registry = er.async_get(hass) entry = entity_registry.async_get("switch.front_siren") @@ -21,7 +21,7 @@ async def test_entity_registry(hass, requests_mock): async def test_siren_off_reports_correctly(hass, requests_mock): """Tests that the initial state of a device that should be off is correct.""" - await setup_platform(hass, SWITCH_DOMAIN) + await setup_platform(hass, Platform.SWITCH) state = hass.states.get("switch.front_siren") assert state.state == "off" @@ -30,7 +30,7 @@ async def test_siren_off_reports_correctly(hass, requests_mock): async def test_siren_on_reports_correctly(hass, requests_mock): """Tests that the initial state of a device that should be on is correct.""" - await setup_platform(hass, SWITCH_DOMAIN) + await setup_platform(hass, Platform.SWITCH) state = hass.states.get("switch.internal_siren") assert state.state == "on" @@ -40,7 +40,7 @@ async def test_siren_on_reports_correctly(hass, requests_mock): async def test_siren_can_be_turned_on(hass, requests_mock): """Tests the siren turns on correctly.""" - await setup_platform(hass, SWITCH_DOMAIN) + await setup_platform(hass, Platform.SWITCH) # Mocks the response for turning a siren on requests_mock.put( @@ -62,7 +62,7 @@ async def test_siren_can_be_turned_on(hass, requests_mock): async def test_updates_work(hass, requests_mock): """Tests the update service works correctly.""" - await setup_platform(hass, SWITCH_DOMAIN) + await setup_platform(hass, Platform.SWITCH) state = hass.states.get("switch.front_siren") assert state.state == "off" # Changes the return to indicate that the siren is now on. From 14e4216e290d27057c8580cba9076fd87c040fb3 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 21 Dec 2021 23:40:43 -0800 Subject: [PATCH 0920/2644] Put access to ffmpeg hass.data behind a method (#62570) * Put access to ffmpeg hass.data behind a method Move all callers of `hass.data[DATA_FFMPEG]` to a new function that returns the FFMpegManager. * Update homeassistant/components/ffmpeg/__init__.py Co-authored-by: Martin Hjelmare * Remove unnecessary async_ prefix Co-authored-by: Martin Hjelmare --- homeassistant/components/amcrest/camera.py | 4 ++-- homeassistant/components/arlo/camera.py | 4 ++-- homeassistant/components/canary/camera.py | 4 ++-- homeassistant/components/ezviz/camera.py | 4 ++-- homeassistant/components/ffmpeg/__init__.py | 8 ++++++++ homeassistant/components/ffmpeg_motion/binary_sensor.py | 4 ++-- homeassistant/components/ffmpeg_noise/binary_sensor.py | 4 ++-- homeassistant/components/homekit/type_cameras.py | 4 ++-- homeassistant/components/logi_circle/camera.py | 4 ++-- homeassistant/components/onvif/camera.py | 4 ++-- homeassistant/components/xiaomi/camera.py | 4 ++-- homeassistant/components/yi/camera.py | 4 ++-- 12 files changed, 30 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py index e250b8ef59b..28485796420 100644 --- a/homeassistant/components/amcrest/camera.py +++ b/homeassistant/components/amcrest/camera.py @@ -15,7 +15,7 @@ import voluptuous as vol from homeassistant.components.camera import SUPPORT_ON_OFF, SUPPORT_STREAM, Camera from homeassistant.components.camera.const import DOMAIN as CAMERA_DOMAIN -from homeassistant.components.ffmpeg import DATA_FFMPEG, FFmpegManager +from homeassistant.components.ffmpeg import FFmpegManager, get_ffmpeg_manager from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry @@ -136,7 +136,7 @@ async def async_setup_platform( name = discovery_info[CONF_NAME] device = hass.data[DATA_AMCREST][DEVICES][name] - entity = AmcrestCam(name, device, hass.data[DATA_FFMPEG]) + entity = AmcrestCam(name, device, get_ffmpeg_manager(hass)) # 2021.9.0 introduced unique id's for the camera entity, but these were not # unique for different resolution streams. If any cameras were configured diff --git a/homeassistant/components/arlo/camera.py b/homeassistant/components/arlo/camera.py index 146970a8610..d60a82db770 100644 --- a/homeassistant/components/arlo/camera.py +++ b/homeassistant/components/arlo/camera.py @@ -7,7 +7,7 @@ from haffmpeg.camera import CameraMjpeg import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, Camera -from homeassistant.components.ffmpeg import DATA_FFMPEG +from homeassistant.components.ffmpeg import get_ffmpeg_manager from homeassistant.const import ATTR_BATTERY_LEVEL from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream import homeassistant.helpers.config_validation as cv @@ -59,7 +59,7 @@ class ArloCam(Camera): self._camera = camera self._attr_name = camera.name self._motion_status = False - self._ffmpeg = hass.data[DATA_FFMPEG] + self._ffmpeg = get_ffmpeg_manager(hass) self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS) self._last_refresh = None self.attrs = {} diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index bbdaaf97d0d..a4c5a5ac837 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -15,7 +15,7 @@ from homeassistant.components.camera import ( PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, Camera, ) -from homeassistant.components.ffmpeg import DATA_FFMPEG, FFmpegManager +from homeassistant.components.ffmpeg import FFmpegManager, get_ffmpeg_manager from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv @@ -94,7 +94,7 @@ class CanaryCamera(CoordinatorEntity, Camera): """Initialize a Canary security camera.""" super().__init__(coordinator) Camera.__init__(self) - self._ffmpeg: FFmpegManager = hass.data[DATA_FFMPEG] + self._ffmpeg: FFmpegManager = get_ffmpeg_manager(hass) self._ffmpeg_arguments = ffmpeg_args self._location_id = location_id self._device = device diff --git a/homeassistant/components/ezviz/camera.py b/homeassistant/components/ezviz/camera.py index 89023b8902d..95e51ab8b61 100644 --- a/homeassistant/components/ezviz/camera.py +++ b/homeassistant/components/ezviz/camera.py @@ -8,7 +8,7 @@ import voluptuous as vol from homeassistant.components import ffmpeg from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera -from homeassistant.components.ffmpeg import DATA_FFMPEG +from homeassistant.components.ffmpeg import get_ffmpeg_manager from homeassistant.config_entries import ( SOURCE_DISCOVERY, SOURCE_IGNORE, @@ -249,7 +249,7 @@ class EzvizCamera(EzvizEntity, Camera): self._rtsp_stream = camera_rtsp_stream self._local_rtsp_port = local_rtsp_port self._ffmpeg_arguments = ffmpeg_arguments - self._ffmpeg = hass.data[DATA_FFMPEG] + self._ffmpeg = get_ffmpeg_manager(hass) self._attr_unique_id = serial self._attr_name = self.data["name"] diff --git a/homeassistant/components/ffmpeg/__init__.py b/homeassistant/components/ffmpeg/__init__.py index 74c826f47d6..e23be94f0ce 100644 --- a/homeassistant/components/ffmpeg/__init__.py +++ b/homeassistant/components/ffmpeg/__init__.py @@ -90,6 +90,14 @@ async def async_setup(hass, config): return True +@bind_hass +def get_ffmpeg_manager(hass: HomeAssistant) -> FFmpegManager: + """Return the FFmpegManager.""" + if DATA_FFMPEG not in hass.data: + raise ValueError("ffmpeg component not initialized") + return hass.data[DATA_FFMPEG] + + @bind_hass async def async_get_image( hass: HomeAssistant, diff --git a/homeassistant/components/ffmpeg_motion/binary_sensor.py b/homeassistant/components/ffmpeg_motion/binary_sensor.py index 292ec35fbff..bacc692c13d 100644 --- a/homeassistant/components/ffmpeg_motion/binary_sensor.py +++ b/homeassistant/components/ffmpeg_motion/binary_sensor.py @@ -11,8 +11,8 @@ from homeassistant.components.ffmpeg import ( CONF_EXTRA_ARGUMENTS, CONF_INITIAL_STATE, CONF_INPUT, - DATA_FFMPEG, FFmpegBase, + get_ffmpeg_manager, ) from homeassistant.const import CONF_NAME, CONF_REPEAT from homeassistant.core import callback @@ -49,7 +49,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the FFmpeg binary motion sensor.""" - manager = hass.data[DATA_FFMPEG] + manager = get_ffmpeg_manager(hass) entity = FFmpegMotion(hass, manager, config) async_add_entities([entity]) diff --git a/homeassistant/components/ffmpeg_noise/binary_sensor.py b/homeassistant/components/ffmpeg_noise/binary_sensor.py index 7745d6fae2a..cd2b6457988 100644 --- a/homeassistant/components/ffmpeg_noise/binary_sensor.py +++ b/homeassistant/components/ffmpeg_noise/binary_sensor.py @@ -11,7 +11,7 @@ from homeassistant.components.ffmpeg import ( CONF_INITIAL_STATE, CONF_INPUT, CONF_OUTPUT, - DATA_FFMPEG, + get_ffmpeg_manager, ) from homeassistant.components.ffmpeg_motion.binary_sensor import FFmpegBinarySensor from homeassistant.const import CONF_NAME @@ -44,7 +44,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the FFmpeg noise binary sensor.""" - manager = hass.data[DATA_FFMPEG] + manager = get_ffmpeg_manager(hass) entity = FFmpegNoise(hass, manager, config) async_add_entities([entity]) diff --git a/homeassistant/components/homekit/type_cameras.py b/homeassistant/components/homekit/type_cameras.py index 14d065dc9bf..9d1821ec724 100644 --- a/homeassistant/components/homekit/type_cameras.py +++ b/homeassistant/components/homekit/type_cameras.py @@ -11,7 +11,7 @@ from pyhap.camera import ( ) from pyhap.const import CATEGORY_CAMERA -from homeassistant.components.ffmpeg import DATA_FFMPEG +from homeassistant.components.ffmpeg import get_ffmpeg_manager from homeassistant.const import STATE_ON from homeassistant.core import callback from homeassistant.helpers.event import ( @@ -139,7 +139,7 @@ class Camera(HomeAccessory, PyhapCamera): def __init__(self, hass, driver, name, entity_id, aid, config): """Initialize a Camera accessory object.""" - self._ffmpeg = hass.data[DATA_FFMPEG] + self._ffmpeg = get_ffmpeg_manager(hass) for config_key, conf in CONFIG_DEFAULTS.items(): if config_key not in config: config[config_key] = conf diff --git a/homeassistant/components/logi_circle/camera.py b/homeassistant/components/logi_circle/camera.py index 5146ffca69f..30ac1ced473 100644 --- a/homeassistant/components/logi_circle/camera.py +++ b/homeassistant/components/logi_circle/camera.py @@ -5,7 +5,7 @@ from datetime import timedelta import logging from homeassistant.components.camera import ATTR_ENTITY_ID, SUPPORT_ON_OFF, Camera -from homeassistant.components.ffmpeg import DATA_FFMPEG +from homeassistant.components.ffmpeg import get_ffmpeg_manager from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_BATTERY_CHARGING, @@ -40,7 +40,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, entry, async_add_entities): """Set up a Logi Circle Camera based on a config entry.""" devices = await hass.data[LOGI_CIRCLE_DOMAIN].cameras - ffmpeg = hass.data[DATA_FFMPEG] + ffmpeg = get_ffmpeg_manager(hass) cameras = [LogiCam(device, entry, ffmpeg) for device in devices] diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index bb7cffa86f9..46558d661f4 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -8,7 +8,7 @@ from yarl import URL from homeassistant.components import ffmpeg from homeassistant.components.camera import SUPPORT_STREAM, Camera -from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS, DATA_FFMPEG +from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS, get_ffmpeg_manager from homeassistant.const import HTTP_BASIC_AUTHENTICATION from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream @@ -153,7 +153,7 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera): """Generate an HTTP MJPEG stream from the camera.""" LOGGER.debug("Handling mjpeg stream from camera '%s'", self.device.name) - ffmpeg_manager = self.hass.data[DATA_FFMPEG] + ffmpeg_manager = get_ffmpeg_manager(self.hass) stream = CameraMjpeg(ffmpeg_manager.binary) await stream.open_camera( diff --git a/homeassistant/components/xiaomi/camera.py b/homeassistant/components/xiaomi/camera.py index 016fe7dd2ba..4cdd5cc6e00 100644 --- a/homeassistant/components/xiaomi/camera.py +++ b/homeassistant/components/xiaomi/camera.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components import ffmpeg from homeassistant.components.camera import PLATFORM_SCHEMA, Camera -from homeassistant.components.ffmpeg import DATA_FFMPEG +from homeassistant.components.ffmpeg import get_ffmpeg_manager from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -65,7 +65,7 @@ class XiaomiCamera(Camera): self._extra_arguments = config.get(CONF_FFMPEG_ARGUMENTS) self._last_image = None self._last_url = None - self._manager = hass.data[DATA_FFMPEG] + self._manager = get_ffmpeg_manager(hass) self._name = config[CONF_NAME] self.host = config[CONF_HOST] self.host.hass = hass diff --git a/homeassistant/components/yi/camera.py b/homeassistant/components/yi/camera.py index 91dfaab38bf..bceb9b999aa 100644 --- a/homeassistant/components/yi/camera.py +++ b/homeassistant/components/yi/camera.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components import ffmpeg from homeassistant.components.camera import PLATFORM_SCHEMA, Camera -from homeassistant.components.ffmpeg import DATA_FFMPEG +from homeassistant.components.ffmpeg import get_ffmpeg_manager from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -60,7 +60,7 @@ class YiCamera(Camera): self._extra_arguments = config.get(CONF_FFMPEG_ARGUMENTS) self._last_image = None self._last_url = None - self._manager = hass.data[DATA_FFMPEG] + self._manager = get_ffmpeg_manager(hass) self._name = config[CONF_NAME] self._is_on = True self.host = config[CONF_HOST] From 5580e872534681b527ec80c910598cabda8c45a1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 22 Dec 2021 00:09:31 -0800 Subject: [PATCH 0921/2644] Add correct callback annotation in configurator (#62569) --- homeassistant/components/configurator/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/configurator/__init__.py b/homeassistant/components/configurator/__init__.py index fde0cdc590d..b94483122d0 100644 --- a/homeassistant/components/configurator/__init__.py +++ b/homeassistant/components/configurator/__init__.py @@ -205,6 +205,7 @@ class Configurator: # it shortly after so that it is deleted when the client updates. self.hass.states.async_set(entity_id, STATE_CONFIGURED) + @async_callback def deferred_remove(event: Event): """Remove the request state.""" self.hass.states.async_remove(entity_id, context=event.context) From 3663e0af4103524c0723d940f6b2efa020dd6639 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Wed, 22 Dec 2021 08:16:37 +0000 Subject: [PATCH 0922/2644] Use enums for ozw tests (#62547) --- tests/components/ozw/test_binary_sensor.py | 4 ++-- tests/components/ozw/test_sensor.py | 17 ++++++----------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/tests/components/ozw/test_binary_sensor.py b/tests/components/ozw/test_binary_sensor.py index 6053cc43457..d0852a5caf0 100644 --- a/tests/components/ozw/test_binary_sensor.py +++ b/tests/components/ozw/test_binary_sensor.py @@ -1,7 +1,7 @@ """Test Z-Wave Sensors.""" from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_MOTION, DOMAIN as BINARY_SENSOR_DOMAIN, + BinarySensorDeviceClass, ) from homeassistant.components.ozw.const import DOMAIN from homeassistant.const import ATTR_DEVICE_CLASS @@ -35,7 +35,7 @@ async def test_binary_sensor(hass, generic_data, binary_sensor_msg): state = hass.states.get("binary_sensor.trisensor_home_security_motion_detected") assert state assert state.state == "off" - assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_MOTION + assert state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.MOTION # Test incoming state change receive_msg(binary_sensor_msg) diff --git a/tests/components/ozw/test_sensor.py b/tests/components/ozw/test_sensor.py index 01bdb7b51a2..2eddb9722e5 100644 --- a/tests/components/ozw/test_sensor.py +++ b/tests/components/ozw/test_sensor.py @@ -1,11 +1,6 @@ """Test Z-Wave Sensors.""" from homeassistant.components.ozw.const import DOMAIN -from homeassistant.components.sensor import ( - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_PRESSURE, - DOMAIN as SENSOR_DOMAIN, -) +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass from homeassistant.const import ATTR_DEVICE_CLASS from homeassistant.helpers import entity_registry as er @@ -24,15 +19,15 @@ async def test_sensor(hass, generic_data): # Test device classes state = hass.states.get("sensor.trisensor_relative_humidity") - assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_HUMIDITY + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.HUMIDITY state = hass.states.get("sensor.trisensor_pressure") - assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_PRESSURE + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.PRESSURE state = hass.states.get("sensor.trisensor_fake_power") - assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_POWER + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.POWER state = hass.states.get("sensor.trisensor_fake_energy") - assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_POWER + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.POWER state = hass.states.get("sensor.trisensor_fake_electric") - assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_POWER + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.POWER # Test ZWaveListSensor disabled by default registry = er.async_get(hass) From f135d77a27447169fdade12070c354916d6fcbf3 Mon Sep 17 00:00:00 2001 From: schmyd Date: Wed, 22 Dec 2021 09:29:54 +0100 Subject: [PATCH 0923/2644] Fix deconz light service parameter handling (#62128) * Only check presence of values, not their content * Add tests * Revert "Only check presence of values, not their content" This reverts commit 046f0ed5fd631cbac0d26e4d3869ad2c6254c0f9. * Validate existence of keys, not their values * Properly handle cases of missing keys --- homeassistant/components/deconz/light.py | 14 +++++------ tests/components/deconz/test_light.py | 30 ++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index e287d574633..5330fdb3226 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -199,7 +199,7 @@ class DeconzBaseLight(DeconzDevice, LightEntity): """Turn on light.""" data: dict[str, bool | float | int | str | tuple[float, float]] = {"on": True} - if attr_brightness := kwargs.get(ATTR_BRIGHTNESS): + if (attr_brightness := kwargs.get(ATTR_BRIGHTNESS)) is not None: data["brightness"] = attr_brightness if attr_color_temp := kwargs.get(ATTR_COLOR_TEMP): @@ -215,16 +215,16 @@ class DeconzBaseLight(DeconzDevice, LightEntity): if ATTR_XY_COLOR in kwargs: data["xy"] = kwargs[ATTR_XY_COLOR] - if attr_transition := kwargs.get(ATTR_TRANSITION): + if (attr_transition := kwargs.get(ATTR_TRANSITION)) is not None: data["transition_time"] = int(attr_transition * 10) elif "IKEA" in self._device.manufacturer: data["transition_time"] = 0 - if (alert := FLASH_TO_DECONZ.get(kwargs.get(ATTR_FLASH, ""))) is not None: + if (alert := FLASH_TO_DECONZ.get(kwargs.get(ATTR_FLASH))) is not None: data["alert"] = alert del data["on"] - if (effect := EFFECT_TO_DECONZ.get(kwargs.get(ATTR_EFFECT, ""))) is not None: + if (effect := EFFECT_TO_DECONZ.get(kwargs.get(ATTR_EFFECT))) is not None: data["effect"] = effect await self._device.set_state(**data) @@ -236,11 +236,11 @@ class DeconzBaseLight(DeconzDevice, LightEntity): data: dict[str, bool | int | str] = {"on": False} - if ATTR_TRANSITION in kwargs: + if (attr_transition := kwargs.get(ATTR_TRANSITION)) is not None: data["brightness"] = 0 - data["transition_time"] = int(kwargs[ATTR_TRANSITION] * 10) + data["transition_time"] = int(attr_transition * 10) - if (alert := FLASH_TO_DECONZ.get(kwargs.get(ATTR_FLASH, ""))) is not None: + if (alert := FLASH_TO_DECONZ.get(kwargs.get(ATTR_FLASH))) is not None: data["alert"] = alert del data["on"] diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index c2b12651fc0..2405ed159e6 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -399,6 +399,20 @@ async def test_light_state_change(hass, aioclient_mock, mock_deconz_websocket): "xy": (0.411, 0.351), }, ), + ( # Turn on light without transition time + { + "light_on": True, + "service": SERVICE_TURN_ON, + "call": { + ATTR_ENTITY_ID: "light.hue_go", + ATTR_TRANSITION: 0, + }, + }, + { + "on": True, + "transitiontime": 0, + }, + ), ( # Turn on light with short color loop { "light_on": False, @@ -453,6 +467,22 @@ async def test_light_state_change(hass, aioclient_mock, mock_deconz_websocket): "alert": "select", }, ), + ( # Turn off light without transition time + { + "light_on": True, + "service": SERVICE_TURN_OFF, + "call": { + ATTR_ENTITY_ID: "light.hue_go", + ATTR_TRANSITION: 0, + ATTR_FLASH: FLASH_SHORT, + }, + }, + { + "bri": 0, + "transitiontime": 0, + "alert": "select", + }, + ), ( # Turn off light with long flashing { "light_on": True, From c7b910ca33bb6fdb90078d1adf1bda02b673ac84 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Wed, 22 Dec 2021 08:31:23 +0000 Subject: [PATCH 0924/2644] Use new enums in knx tests (#62513) * Use new enums in knx tests * Code review: Swap == for is --- tests/components/knx/test_binary_sensor.py | 13 ++++--------- tests/components/knx/test_scene.py | 11 ++++------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/tests/components/knx/test_binary_sensor.py b/tests/components/knx/test_binary_sensor.py index 5513cefcbb4..25a223e76c8 100644 --- a/tests/components/knx/test_binary_sensor.py +++ b/tests/components/knx/test_binary_sensor.py @@ -4,14 +4,9 @@ from unittest.mock import patch from homeassistant.components.knx.const import CONF_STATE_ADDRESS, CONF_SYNC_STATE from homeassistant.components.knx.schema import BinarySensorSchema -from homeassistant.const import ( - CONF_ENTITY_CATEGORY, - CONF_NAME, - ENTITY_CATEGORY_DIAGNOSTIC, - STATE_OFF, - STATE_ON, -) +from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant, State +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_registry import ( async_get_registry as async_get_entity_registry, ) @@ -30,7 +25,7 @@ async def test_binary_sensor_entity_category(hass: HomeAssistant, knx: KNXTestKi { CONF_NAME: "test_normal", CONF_STATE_ADDRESS: "1/1/1", - CONF_ENTITY_CATEGORY: ENTITY_CATEGORY_DIAGNOSTIC, + CONF_ENTITY_CATEGORY: EntityCategory.DIAGNOSTIC, }, ] } @@ -42,7 +37,7 @@ async def test_binary_sensor_entity_category(hass: HomeAssistant, knx: KNXTestKi registry = await async_get_entity_registry(hass) entity = registry.async_get("binary_sensor.test_normal") - assert entity.entity_category == ENTITY_CATEGORY_DIAGNOSTIC + assert entity.entity_category is EntityCategory.DIAGNOSTIC async def test_binary_sensor(hass: HomeAssistant, knx: KNXTestKit): diff --git a/tests/components/knx/test_scene.py b/tests/components/knx/test_scene.py index c2f15df6f6c..37e4ac12728 100644 --- a/tests/components/knx/test_scene.py +++ b/tests/components/knx/test_scene.py @@ -2,12 +2,9 @@ from homeassistant.components.knx.const import KNX_ADDRESS from homeassistant.components.knx.schema import SceneSchema -from homeassistant.const import ( - CONF_ENTITY_CATEGORY, - CONF_NAME, - ENTITY_CATEGORY_DIAGNOSTIC, -) +from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_registry import ( async_get_registry as async_get_entity_registry, ) @@ -24,7 +21,7 @@ async def test_activate_knx_scene(hass: HomeAssistant, knx: KNXTestKit): CONF_NAME: "test", SceneSchema.CONF_SCENE_NUMBER: 24, KNX_ADDRESS: "1/1/1", - CONF_ENTITY_CATEGORY: ENTITY_CATEGORY_DIAGNOSTIC, + CONF_ENTITY_CATEGORY: EntityCategory.DIAGNOSTIC, }, ] } @@ -33,7 +30,7 @@ async def test_activate_knx_scene(hass: HomeAssistant, knx: KNXTestKit): registry = await async_get_entity_registry(hass) entity = registry.async_get("scene.test") - assert entity.entity_category == ENTITY_CATEGORY_DIAGNOSTIC + assert entity.entity_category is EntityCategory.DIAGNOSTIC assert entity.unique_id == "1/1/1_24" await hass.services.async_call( From 6806b8b1165099e4a4606e5899d33cdf7ca06c91 Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Wed, 22 Dec 2021 09:39:59 +0100 Subject: [PATCH 0925/2644] Change Brunt cover device class (#62578) --- homeassistant/components/brunt/cover.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/brunt/cover.py b/homeassistant/components/brunt/cover.py index 08b7eade612..d3efdce0a5b 100644 --- a/homeassistant/components/brunt/cover.py +++ b/homeassistant/components/brunt/cover.py @@ -101,7 +101,7 @@ class BruntDevice(CoordinatorEntity, CoverEntity): self._remove_update_listener = None self._attr_name = self._thing.name - self._attr_device_class = CoverDeviceClass.SHADE + self._attr_device_class = CoverDeviceClass.BLIND self._attr_supported_features = COVER_FEATURES self._attr_attribution = ATTRIBUTION self._attr_device_info = DeviceInfo( From d3d6965ba0481132f89ad45c4a096eb484e726ff Mon Sep 17 00:00:00 2001 From: Jc2k Date: Wed, 22 Dec 2021 08:43:17 +0000 Subject: [PATCH 0926/2644] Support setting Aqara Hub Volume via homekit_controller (#62538) --- .../components/homekit_controller/const.py | 1 + .../homekit_controller/manifest.json | 2 +- .../components/homekit_controller/number.py | 23 +++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../specific_devices/test_aqara_gateway.py | 97 +++++++++++-------- .../specific_devices/test_eve_degree.py | 4 +- .../test_vocolinc_flowerbud.py | 13 ++- .../homekit_controller/test_number.py | 8 +- 9 files changed, 101 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index eee244c8cf1..2de7156482e 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -47,6 +47,7 @@ HOMEKIT_ACCESSORY_DISPATCH = { } CHARACTERISTIC_PLATFORMS = { + CharacteristicsTypes.Vendor.AQARA_GATEWAY_VOLUME: "number", CharacteristicsTypes.Vendor.EVE_ENERGY_WATT: "sensor", CharacteristicsTypes.Vendor.EVE_DEGREE_AIR_PRESSURE: "sensor", CharacteristicsTypes.Vendor.EVE_DEGREE_ELEVATION: "number", diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index d9645d22a2d..4b4b971b3b6 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==0.6.4"], + "requirements": ["aiohomekit==0.6.10"], "zeroconf": ["_hap._tcp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], diff --git a/homeassistant/components/homekit_controller/number.py b/homeassistant/components/homekit_controller/number.py index beb0ffa7fa5..f646072425b 100644 --- a/homeassistant/components/homekit_controller/number.py +++ b/homeassistant/components/homekit_controller/number.py @@ -10,6 +10,7 @@ from aiohomekit.model.characteristics import Characteristic, CharacteristicsType from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.core import callback +from homeassistant.helpers.entity import EntityCategory from . import KNOWN_DEVICES, CharacteristicEntity @@ -18,11 +19,25 @@ NUMBER_ENTITIES: dict[str, NumberEntityDescription] = { key=CharacteristicsTypes.Vendor.VOCOLINC_HUMIDIFIER_SPRAY_LEVEL, name="Spray Quantity", icon="mdi:water", + entity_category=EntityCategory.CONFIG, ), CharacteristicsTypes.Vendor.EVE_DEGREE_ELEVATION: NumberEntityDescription( key=CharacteristicsTypes.Vendor.EVE_DEGREE_ELEVATION, name="Elevation", icon="mdi:elevation-rise", + entity_category=EntityCategory.CONFIG, + ), + CharacteristicsTypes.Vendor.AQARA_GATEWAY_VOLUME: NumberEntityDescription( + key=CharacteristicsTypes.Vendor.AQARA_GATEWAY_VOLUME, + name="Volume", + icon="mdi:volume-high", + entity_category=EntityCategory.CONFIG, + ), + CharacteristicsTypes.Vendor.AQARA_E1_GATEWAY_VOLUME: NumberEntityDescription( + key=CharacteristicsTypes.Vendor.AQARA_E1_GATEWAY_VOLUME, + name="Volume", + icon="mdi:volume-high", + entity_category=EntityCategory.CONFIG, ), } @@ -57,6 +72,14 @@ class HomeKitNumber(CharacteristicEntity, NumberEntity): self.entity_description = description super().__init__(conn, info, char) + @property + def name(self) -> str: + """Return the name of the device if any.""" + prefix = "" + if name := super().name: + prefix = f"{name} -" + return f"{prefix} {self.entity_description.name}" + def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" return [self._char.type] diff --git a/requirements_all.txt b/requirements_all.txt index ed35c50a1e7..7e7fd6f7b95 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -186,7 +186,7 @@ aioguardian==2021.11.0 aioharmony==0.2.8 # homeassistant.components.homekit_controller -aiohomekit==0.6.4 +aiohomekit==0.6.10 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8cc174d9127..1eb99cfec45 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -130,7 +130,7 @@ aioguardian==2021.11.0 aioharmony==0.2.8 # homeassistant.components.homekit_controller -aiohomekit==0.6.4 +aiohomekit==0.6.10 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py b/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py index b4437a7a9b5..08c9e7b3976 100644 --- a/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py +++ b/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py @@ -3,9 +3,14 @@ Regression tests for Aqara Gateway V3. https://github.com/home-assistant/core/issues/20957 """ - +from homeassistant.components.alarm_control_panel import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) from homeassistant.components.light import SUPPORT_BRIGHTNESS, SUPPORT_COLOR from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entity import EntityCategory from tests.components.homekit_controller.common import ( Helper, @@ -20,44 +25,58 @@ async def test_aqara_gateway_setup(hass): config_entry, pairing = await setup_test_accessories(hass, accessories) entity_registry = er.async_get(hass) - - # Check that the light is correctly found and set up - alarm_id = "alarm_control_panel.aqara_hub_1563" - alarm = entity_registry.async_get(alarm_id) - assert alarm.unique_id == "homekit-0000000123456789-66304" - - alarm_helper = Helper( - hass, - "alarm_control_panel.aqara_hub_1563", - pairing, - accessories[0], - config_entry, - ) - alarm_state = await alarm_helper.poll_and_get_state() - assert alarm_state.attributes["friendly_name"] == "Aqara Hub-1563" - - # Check that the light is correctly found and set up - light = entity_registry.async_get("light.aqara_hub_1563") - assert light.unique_id == "homekit-0000000123456789-65792" - - light_helper = Helper( - hass, "light.aqara_hub_1563", pairing, accessories[0], config_entry - ) - light_state = await light_helper.poll_and_get_state() - assert light_state.attributes["friendly_name"] == "Aqara Hub-1563" - assert light_state.attributes["supported_features"] == ( - SUPPORT_BRIGHTNESS | SUPPORT_COLOR - ) - device_registry = dr.async_get(hass) - # All the entities are services of the same accessory - # So it looks at the protocol like a single physical device - assert alarm.device_id == light.device_id + sensors = [ + ( + "alarm_control_panel.aqara_hub_1563", + "homekit-0000000123456789-66304", + "Aqara Hub-1563", + SUPPORT_ALARM_ARM_NIGHT | SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY, + None, + ), + ( + "light.aqara_hub_1563", + "homekit-0000000123456789-65792", + "Aqara Hub-1563", + SUPPORT_BRIGHTNESS | SUPPORT_COLOR, + None, + ), + ( + "number.aqara_hub_1563_volume", + "homekit-0000000123456789-aid:1-sid:65536-cid:65541", + "Aqara Hub-1563 - Volume", + None, + EntityCategory.CONFIG, + ), + ] - device = device_registry.async_get(light.device_id) - assert device.manufacturer == "Aqara" - assert device.name == "Aqara Hub-1563" - assert device.model == "ZHWA11LM" - assert device.sw_version == "1.4.7" - assert device.via_device_id is None + device_ids = set() + + for (entity_id, unique_id, friendly_name, supported_features, category) in sensors: + entry = entity_registry.async_get(entity_id) + assert entry.unique_id == unique_id + assert entry.entity_category == category + + helper = Helper( + hass, + entity_id, + pairing, + accessories[0], + config_entry, + ) + state = await helper.poll_and_get_state() + assert state.attributes["friendly_name"] == friendly_name + assert state.attributes.get("supported_features") == supported_features + + device = device_registry.async_get(entry.device_id) + assert device.manufacturer == "Aqara" + assert device.name == "Aqara Hub-1563" + assert device.model == "ZHWA11LM" + assert device.sw_version == "1.4.7" + assert device.via_device_id is None + + device_ids.add(entry.device_id) + + # All entities should be part of same device + assert len(device_ids) == 1 diff --git a/tests/components/homekit_controller/specific_devices/test_eve_degree.py b/tests/components/homekit_controller/specific_devices/test_eve_degree.py index e419b140e94..043920fadec 100644 --- a/tests/components/homekit_controller/specific_devices/test_eve_degree.py +++ b/tests/components/homekit_controller/specific_devices/test_eve_degree.py @@ -39,9 +39,9 @@ async def test_eve_degree_setup(hass): "Eve Degree AA11 Battery", ), ( - "number.eve_degree_aa11", + "number.eve_degree_aa11_elevation", "homekit-AA00A0A00000-aid:1-sid:30-cid:33", - "Eve Degree AA11", + "Eve Degree AA11 - Elevation", ), ] diff --git a/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py b/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py index 391ac1c8f39..fe570ff0b73 100644 --- a/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py +++ b/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py @@ -19,14 +19,21 @@ async def test_vocolinc_flowerbud_setup(hass): # Check that the switch entity is handled correctly - entry = entity_registry.async_get("number.vocolinc_flowerbud_0d324b") + entry = entity_registry.async_get("number.vocolinc_flowerbud_0d324b_spray_quantity") assert entry.unique_id == "homekit-AM01121849000327-aid:1-sid:30-cid:38" helper = Helper( - hass, "number.vocolinc_flowerbud_0d324b", pairing, accessories[0], config_entry + hass, + "number.vocolinc_flowerbud_0d324b_spray_quantity", + pairing, + accessories[0], + config_entry, ) state = await helper.poll_and_get_state() - assert state.attributes["friendly_name"] == "VOCOlinc-Flowerbud-0d324b" + assert ( + state.attributes["friendly_name"] + == "VOCOlinc-Flowerbud-0d324b - Spray Quantity" + ) device = device_registry.async_get(entry.device_id) assert device.manufacturer == "VOCOlinc" diff --git a/tests/components/homekit_controller/test_number.py b/tests/components/homekit_controller/test_number.py index 490b69b1a80..8eebcbda8f5 100644 --- a/tests/components/homekit_controller/test_number.py +++ b/tests/components/homekit_controller/test_number.py @@ -33,7 +33,7 @@ async def test_read_number(hass, utcnow): # Helper will be for the primary entity, which is the outlet. Make a helper for the sensor. energy_helper = Helper( hass, - "number.testdevice", + "number.testdevice_spray_quantity", helper.pairing, helper.accessory, helper.config_entry, @@ -61,7 +61,7 @@ async def test_write_number(hass, utcnow): # Helper will be for the primary entity, which is the outlet. Make a helper for the sensor. energy_helper = Helper( hass, - "number.testdevice", + "number.testdevice_spray_quantity", helper.pairing, helper.accessory, helper.config_entry, @@ -73,7 +73,7 @@ async def test_write_number(hass, utcnow): await hass.services.async_call( "number", "set_value", - {"entity_id": "number.testdevice", "value": 5}, + {"entity_id": "number.testdevice_spray_quantity", "value": 5}, blocking=True, ) assert spray_level.value == 5 @@ -81,7 +81,7 @@ async def test_write_number(hass, utcnow): await hass.services.async_call( "number", "set_value", - {"entity_id": "number.testdevice", "value": 3}, + {"entity_id": "number.testdevice_spray_quantity", "value": 3}, blocking=True, ) assert spray_level.value == 3 From a7ef983a3141b1e2147c318e72328c2ca08e123c Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Wed, 22 Dec 2021 09:00:24 +0000 Subject: [PATCH 0927/2644] Use new enums in litterrobot tests (#62515) * Use new enums in litterrobot tests * Code review: swap == for is --- tests/components/litterrobot/test_button.py | 10 +++------- tests/components/litterrobot/test_select.py | 5 +++-- tests/components/litterrobot/test_sensor.py | 6 +++--- tests/components/litterrobot/test_switch.py | 10 +++------- 4 files changed, 12 insertions(+), 19 deletions(-) diff --git a/tests/components/litterrobot/test_button.py b/tests/components/litterrobot/test_button.py index 0ca74da5d02..3f802d0e6b2 100644 --- a/tests/components/litterrobot/test_button.py +++ b/tests/components/litterrobot/test_button.py @@ -4,14 +4,10 @@ from unittest.mock import MagicMock from freezegun import freeze_time from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS -from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_ICON, - ENTITY_CATEGORY_CONFIG, - STATE_UNKNOWN, -) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_ICON, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity import EntityCategory from .conftest import setup_integration @@ -31,7 +27,7 @@ async def test_button(hass: HomeAssistant, mock_account: MagicMock) -> None: entry = entity_registry.async_get(BUTTON_ENTITY) assert entry - assert entry.entity_category == ENTITY_CATEGORY_CONFIG + assert entry.entity_category is EntityCategory.CONFIG await hass.services.async_call( BUTTON_DOMAIN, diff --git a/tests/components/litterrobot/test_select.py b/tests/components/litterrobot/test_select.py index dfb1b0e639e..e3e9782423e 100644 --- a/tests/components/litterrobot/test_select.py +++ b/tests/components/litterrobot/test_select.py @@ -10,9 +10,10 @@ from homeassistant.components.select import ( DOMAIN as PLATFORM_DOMAIN, SERVICE_SELECT_OPTION, ) -from homeassistant.const import ATTR_ENTITY_ID, ENTITY_CATEGORY_CONFIG +from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry +from homeassistant.helpers.entity import EntityCategory from homeassistant.util.dt import utcnow from .conftest import setup_integration @@ -32,7 +33,7 @@ async def test_wait_time_select(hass: HomeAssistant, mock_account): ent_reg = entity_registry.async_get(hass) entity_entry = ent_reg.async_get(SELECT_ENTITY_ID) assert entity_entry - assert entity_entry.entity_category == ENTITY_CATEGORY_CONFIG + assert entity_entry.entity_category is EntityCategory.CONFIG data = {ATTR_ENTITY_ID: SELECT_ENTITY_ID} diff --git a/tests/components/litterrobot/test_sensor.py b/tests/components/litterrobot/test_sensor.py index dbc8c39790c..e3e62d5f5e4 100644 --- a/tests/components/litterrobot/test_sensor.py +++ b/tests/components/litterrobot/test_sensor.py @@ -2,8 +2,8 @@ from unittest.mock import Mock from homeassistant.components.litterrobot.sensor import LitterRobotSleepTimeSensor -from homeassistant.components.sensor import DOMAIN as PLATFORM_DOMAIN -from homeassistant.const import DEVICE_CLASS_TIMESTAMP, PERCENTAGE +from homeassistant.components.sensor import DOMAIN as PLATFORM_DOMAIN, SensorDeviceClass +from homeassistant.const import PERCENTAGE from .conftest import create_mock_robot, setup_integration @@ -30,7 +30,7 @@ async def test_sleep_time_sensor_with_none_state(hass): assert sensor assert sensor.state is None - assert sensor.device_class == DEVICE_CLASS_TIMESTAMP + assert sensor.device_class is SensorDeviceClass.TIMESTAMP async def test_gauge_icon(): diff --git a/tests/components/litterrobot/test_switch.py b/tests/components/litterrobot/test_switch.py index 99c34e4273f..540a1c92810 100644 --- a/tests/components/litterrobot/test_switch.py +++ b/tests/components/litterrobot/test_switch.py @@ -10,14 +10,10 @@ from homeassistant.components.switch import ( SERVICE_TURN_OFF, SERVICE_TURN_ON, ) -from homeassistant.const import ( - ATTR_ENTITY_ID, - ENTITY_CATEGORY_CONFIG, - STATE_OFF, - STATE_ON, -) +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry +from homeassistant.helpers.entity import EntityCategory from homeassistant.util.dt import utcnow from .conftest import setup_integration @@ -39,7 +35,7 @@ async def test_switch(hass: HomeAssistant, mock_account: MagicMock): ent_reg = entity_registry.async_get(hass) entity_entry = ent_reg.async_get(NIGHT_LIGHT_MODE_ENTITY_ID) assert entity_entry - assert entity_entry.entity_category == ENTITY_CATEGORY_CONFIG + assert entity_entry.entity_category is EntityCategory.CONFIG @pytest.mark.parametrize( From f2ae7c0b929e51a3789fccc4a993c4a30f5d89ee Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Wed, 22 Dec 2021 09:01:00 +0000 Subject: [PATCH 0928/2644] Use new enums in mfi tests (#62516) * Use new enums in mfi tests * Code review: swap == for is --- tests/components/mfi/test_sensor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/components/mfi/test_sensor.py b/tests/components/mfi/test_sensor.py index 17d8df958ef..ea67ab6f427 100644 --- a/tests/components/mfi/test_sensor.py +++ b/tests/components/mfi/test_sensor.py @@ -8,7 +8,8 @@ import requests import homeassistant.components.mfi.sensor as mfi import homeassistant.components.sensor as sensor_component -from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS +from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.const import TEMP_CELSIUS from homeassistant.setup import async_setup_component PLATFORM = mfi @@ -134,7 +135,7 @@ async def test_uom_temp(port, sensor): """Test the UOM temperature.""" port.tag = "temperature" assert sensor.unit_of_measurement == TEMP_CELSIUS - assert sensor.device_class == DEVICE_CLASS_TEMPERATURE + assert sensor.device_class is SensorDeviceClass.TEMPERATURE async def test_uom_power(port, sensor): From e64352a7e0df57655e0d0676a528e4f85bc9ad54 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Wed, 22 Dec 2021 09:03:11 +0000 Subject: [PATCH 0929/2644] Use new enums in modbus tests (#62518) --- tests/components/modbus/test_sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index bf3e8711922..0edd9bdc945 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -19,7 +19,7 @@ from homeassistant.components.modbus.const import ( from homeassistant.components.sensor import ( CONF_STATE_CLASS, DOMAIN as SENSOR_DOMAIN, - STATE_CLASS_MEASUREMENT, + SensorStateClass, ) from homeassistant.const import ( CONF_ADDRESS, @@ -62,7 +62,7 @@ ENTITY_ID = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}" CONF_PRECISION: 0, CONF_SCALE: 1, CONF_OFFSET: 0, - CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT, + CONF_STATE_CLASS: SensorStateClass.MEASUREMENT, CONF_LAZY_ERROR: 10, CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, CONF_DEVICE_CLASS: "battery", From 563e6b3e80ab17957384def8e48396e005172ccc Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Wed, 22 Dec 2021 09:04:12 +0000 Subject: [PATCH 0930/2644] Use new enums in modern_forms tests (#62519) --- tests/components/modern_forms/test_sensor.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/components/modern_forms/test_sensor.py b/tests/components/modern_forms/test_sensor.py index d18793f51c2..638b3ddbacf 100644 --- a/tests/components/modern_forms/test_sensor.py +++ b/tests/components/modern_forms/test_sensor.py @@ -1,7 +1,8 @@ """Tests for the Modern Forms sensor platform.""" from datetime import datetime -from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ICON, DEVICE_CLASS_TIMESTAMP +from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ICON from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -22,14 +23,14 @@ async def test_sensors( state = hass.states.get("sensor.modernformsfan_light_sleep_time") assert state assert state.attributes.get(ATTR_ICON) == "mdi:timer-outline" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TIMESTAMP + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP assert state.state == "unknown" # Fan timer remaining time state = hass.states.get("sensor.modernformsfan_fan_sleep_time") assert state assert state.attributes.get(ATTR_ICON) == "mdi:timer-outline" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TIMESTAMP + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP assert state.state == "unknown" @@ -46,12 +47,12 @@ async def test_active_sensors( state = hass.states.get("sensor.modernformsfan_light_sleep_time") assert state assert state.attributes.get(ATTR_ICON) == "mdi:timer-outline" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TIMESTAMP + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP datetime.fromisoformat(state.state) # Fan timer remaining time state = hass.states.get("sensor.modernformsfan_fan_sleep_time") assert state assert state.attributes.get(ATTR_ICON) == "mdi:timer-outline" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TIMESTAMP + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP datetime.fromisoformat(state.state) From 5e25df91b232798cb43a08cc9baa27edfa0b286e Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 22 Dec 2021 04:43:33 -0500 Subject: [PATCH 0931/2644] Use platform enums in utility_meter tests (#62553) --- tests/components/utility_meter/test_init.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/utility_meter/test_init.py b/tests/components/utility_meter/test_init.py index aa6de34f611..61e6fc4dae8 100644 --- a/tests/components/utility_meter/test_init.py +++ b/tests/components/utility_meter/test_init.py @@ -2,7 +2,6 @@ from datetime import timedelta from unittest.mock import patch -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.utility_meter.const import ( ATTR_TARIFF, DOMAIN, @@ -17,6 +16,7 @@ from homeassistant.const import ( CONF_PLATFORM, ENERGY_KILO_WATT_HOUR, EVENT_HOMEASSISTANT_START, + Platform, ) from homeassistant.core import State from homeassistant.setup import async_setup_component @@ -46,7 +46,7 @@ async def test_restore_state(hass): ) assert await async_setup_component(hass, DOMAIN, config) - assert await async_setup_component(hass, SENSOR_DOMAIN, config) + assert await async_setup_component(hass, Platform.SENSOR, config) await hass.async_block_till_done() # restore from cache @@ -67,7 +67,7 @@ async def test_services(hass): } assert await async_setup_component(hass, DOMAIN, config) - assert await async_setup_component(hass, SENSOR_DOMAIN, config) + assert await async_setup_component(hass, Platform.SENSOR, config) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_START) From ee878513a762e825537ecb94161da51efa8985b1 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 22 Dec 2021 06:03:31 -0500 Subject: [PATCH 0932/2644] Remove deprecated yaml config from squeezebox (#62537) --- .../components/squeezebox/config_flow.py | 7 -- .../components/squeezebox/media_player.py | 34 +--------- .../components/squeezebox/test_config_flow.py | 66 ------------------- 3 files changed, 2 insertions(+), 105 deletions(-) diff --git a/homeassistant/components/squeezebox/config_flow.py b/homeassistant/components/squeezebox/config_flow.py index 03c5f45e357..a8a1797f3bc 100644 --- a/homeassistant/components/squeezebox/config_flow.py +++ b/homeassistant/components/squeezebox/config_flow.py @@ -158,13 +158,6 @@ class SqueezeboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="edit", data_schema=self.data_schema, errors=errors ) - async def async_step_import(self, config): - """Import a config flow from configuration.""" - error = await self._validate_input(config) - if error: - return self.async_abort(reason=error) - return self.async_create_entry(title=config[CONF_HOST], data=config) - async def async_step_integration_discovery(self, discovery_info): """Handle discovery of a server.""" _LOGGER.debug("Reached server discovery flow with info: %s", discovery_info) diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index ef54f18bc9c..91857fb3ff8 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -6,8 +6,7 @@ import logging from pysqueezebox import Server, async_discover import voluptuous as vol -from homeassistant import config_entries -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity +from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( ATTR_MEDIA_ENQUEUE, MEDIA_TYPE_MUSIC, @@ -51,13 +50,7 @@ from homeassistant.helpers.dispatcher import ( from homeassistant.util.dt import utcnow from .browse_media import build_item_response, generate_playlist, library_payload -from .const import ( - DEFAULT_PORT, - DISCOVERY_TASK, - DOMAIN, - KNOWN_PLAYERS, - PLAYER_DISCOVERY_UNSUB, -) +from .const import DISCOVERY_TASK, DOMAIN, KNOWN_PLAYERS, PLAYER_DISCOVERY_UNSUB SERVICE_CALL_METHOD = "call_method" SERVICE_CALL_QUERY = "call_query" @@ -90,21 +83,6 @@ SUPPORT_SQUEEZEBOX = ( | SUPPORT_STOP ) -PLATFORM_SCHEMA = vol.All( - cv.deprecated(CONF_HOST), - cv.deprecated(CONF_PORT), - cv.deprecated(CONF_PASSWORD), - cv.deprecated(CONF_USERNAME), - PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_USERNAME): cv.string, - } - ), -) - KNOWN_SERVERS = "known_servers" ATTR_PARAMETERS = "parameters" ATTR_OTHER_PLAYER = "other_player" @@ -145,14 +123,6 @@ async def start_server_discovery(hass): ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up squeezebox platform from platform entry in configuration.yaml (deprecated).""" - if config: - await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config - ) - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up an LMS Server from a config entry.""" config = config_entry.data diff --git a/tests/components/squeezebox/test_config_flow.py b/tests/components/squeezebox/test_config_flow.py index 70f5f1233d7..22181d73fd3 100644 --- a/tests/components/squeezebox/test_config_flow.py +++ b/tests/components/squeezebox/test_config_flow.py @@ -246,69 +246,3 @@ async def test_dhcp_discovery_existing_player(hass): ), ) assert result["type"] == RESULT_TYPE_ABORT - - -async def test_import(hass): - """Test handling of configuration imported.""" - with patch("pysqueezebox.Server.async_query", return_value={"uuid": UUID},), patch( - "homeassistant.components.squeezebox.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={CONF_HOST: HOST, CONF_PORT: PORT}, - ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - - await hass.async_block_till_done() - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_import_bad_host(hass): - """Test handling of configuration imported with bad host.""" - with patch("pysqueezebox.Server.async_query", return_value=False): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={CONF_HOST: HOST, CONF_PORT: PORT}, - ) - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "cannot_connect" - - -async def test_import_bad_auth(hass): - """Test handling of configuration import with bad authentication.""" - with patch("pysqueezebox.Server.async_query", new=patch_async_query_unauthorized): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_HOST: HOST, - CONF_PORT: PORT, - CONF_USERNAME: "test", - CONF_PASSWORD: "bad", - }, - ) - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "invalid_auth" - - -async def test_import_existing(hass): - """Test handling of configuration import of existing server.""" - with patch( - "homeassistant.components.squeezebox.async_setup_entry", - return_value=True, - ), patch( - "pysqueezebox.Server.async_query", - return_value={"ip": HOST, "uuid": UUID}, - ): - entry = MockConfigEntry(domain=DOMAIN, unique_id=UUID) - await hass.config_entries.async_add(entry) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={CONF_HOST: HOST, CONF_PORT: PORT}, - ) - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" From 66185f8247b14364430ff0655eb0ff3a8b381338 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 22 Dec 2021 12:04:31 +0100 Subject: [PATCH 0933/2644] Add state class support to Luftdaten (#62585) --- homeassistant/components/luftdaten/sensor.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/luftdaten/sensor.py b/homeassistant/components/luftdaten/sensor.py index c0d4dbaf619..975acf30266 100644 --- a/homeassistant/components/luftdaten/sensor.py +++ b/homeassistant/components/luftdaten/sensor.py @@ -5,6 +5,7 @@ from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -32,6 +33,7 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="humidity", @@ -39,6 +41,7 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( icon="mdi:water-percent", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="pressure", @@ -46,6 +49,7 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( icon="mdi:arrow-down-bold", native_unit_of_measurement=PRESSURE_PA, device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="pressure_at_sealevel", @@ -53,18 +57,21 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( icon="mdi:download", native_unit_of_measurement=PRESSURE_PA, device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="P1", name="PM10", icon="mdi:thought-bubble", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="P2", name="PM2.5", icon="mdi:thought-bubble-outline", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, ), ) From 3323263c94ed6b659878baa2a658866f5e92398b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 22 Dec 2021 12:14:45 +0100 Subject: [PATCH 0934/2644] Flip behavior for grouped locks (#62580) --- homeassistant/components/lock/group.py | 2 +- tests/components/group/test_init.py | 23 ++++++++++++++++------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/lock/group.py b/homeassistant/components/lock/group.py index d463f72242b..9cf460dc019 100644 --- a/homeassistant/components/lock/group.py +++ b/homeassistant/components/lock/group.py @@ -11,4 +11,4 @@ def async_describe_on_off_states( hass: HomeAssistant, registry: GroupIntegrationRegistry ) -> None: """Describe group on off states.""" - registry.on_off_states({STATE_LOCKED}, STATE_UNLOCKED) + registry.on_off_states({STATE_UNLOCKED}, STATE_LOCKED) diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index fff1526b711..4eb0e455422 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -3,6 +3,8 @@ from collections import OrderedDict from unittest.mock import patch +import pytest + import homeassistant.components.group as group from homeassistant.const import ( ATTR_ASSUMED_STATE, @@ -713,7 +715,7 @@ async def test_group_persons_and_device_trackers(hass): async def test_group_mixed_domains_on(hass): """Test group of mixed domains that is on.""" - hass.states.async_set("lock.alexander_garage_exit_door", "locked") + hass.states.async_set("lock.alexander_garage_exit_door", "unlocked") hass.states.async_set("binary_sensor.alexander_garage_side_door_open", "on") hass.states.async_set("cover.small_garage_door", "open") @@ -738,7 +740,7 @@ async def test_group_mixed_domains_on(hass): async def test_group_mixed_domains_off(hass): """Test group of mixed domains that is off.""" - hass.states.async_set("lock.alexander_garage_exit_door", "unlocked") + hass.states.async_set("lock.alexander_garage_exit_door", "locked") hass.states.async_set("binary_sensor.alexander_garage_side_door_open", "off") hass.states.async_set("cover.small_garage_door", "closed") @@ -761,11 +763,18 @@ async def test_group_mixed_domains_off(hass): assert hass.states.get("group.group_zero").state == "off" -async def test_group_locks(hass): +@pytest.mark.parametrize( + "states,group_state", + [ + (("locked", "locked", "unlocked"), "unlocked"), + (("locked", "locked", "locked"), "locked"), + ], +) +async def test_group_locks(hass, states, group_state): """Test group of locks.""" - hass.states.async_set("lock.one", "locked") - hass.states.async_set("lock.two", "locked") - hass.states.async_set("lock.three", "unlocked") + hass.states.async_set("lock.one", states[0]) + hass.states.async_set("lock.two", states[1]) + hass.states.async_set("lock.three", states[2]) assert await async_setup_component(hass, "lock", {}) assert await async_setup_component( @@ -779,7 +788,7 @@ async def test_group_locks(hass): ) await hass.async_block_till_done() - assert hass.states.get("group.group_zero").state == "locked" + assert hass.states.get("group.group_zero").state == group_state async def test_group_sensors(hass): From 4805b6730053e824be8b28ca457f29bd63fc8ba5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 22 Dec 2021 12:20:35 +0100 Subject: [PATCH 0935/2644] Add strict typing to Luftdaten (#62588) --- .strict-typing | 1 + .../components/luftdaten/__init__.py | 4 ++-- .../components/luftdaten/config_flow.py | 24 ++++++++++++------- homeassistant/components/luftdaten/sensor.py | 4 +++- mypy.ini | 14 ++++++++--- script/hassfest/mypy_config.py | 1 - 6 files changed, 33 insertions(+), 15 deletions(-) diff --git a/.strict-typing b/.strict-typing index f9baa9ac01a..cfccd601ac1 100644 --- a/.strict-typing +++ b/.strict-typing @@ -78,6 +78,7 @@ homeassistant.components.light.* homeassistant.components.local_ip.* homeassistant.components.lock.* homeassistant.components.lookin.* +homeassistant.components.luftdaten.* homeassistant.components.mailbox.* homeassistant.components.media_player.* homeassistant.components.modbus.* diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py index f131a2dc017..33ddcf67a1e 100644 --- a/homeassistant/components/luftdaten/__init__.py +++ b/homeassistant/components/luftdaten/__init__.py @@ -33,7 +33,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: luftdaten = Luftdaten(entry.data[CONF_SENSOR_ID]) - async def async_update() -> dict[Any, Any]: + async def async_update() -> dict[str, float | int]: """Update sensor/binary sensor data.""" try: await luftdaten.get_data() @@ -43,7 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not luftdaten.values: raise UpdateFailed("Did not receive sensor data from luftdaten.info") - data = luftdaten.values + data: dict[str, float | int] = luftdaten.values data.update(luftdaten.meta) return data diff --git a/homeassistant/components/luftdaten/config_flow.py b/homeassistant/components/luftdaten/config_flow.py index febc42e28fd..31f518dcbcf 100644 --- a/homeassistant/components/luftdaten/config_flow.py +++ b/homeassistant/components/luftdaten/config_flow.py @@ -1,5 +1,7 @@ """Config flow to configure the Luftdaten component.""" -from collections import OrderedDict +from __future__ import annotations + +from typing import Any from luftdaten import Luftdaten from luftdaten.exceptions import LuftdatenConnectionError @@ -13,6 +15,7 @@ from homeassistant.const import ( CONF_SHOW_ON_MAP, ) from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from .const import CONF_SENSOR_ID, DEFAULT_SCAN_INTERVAL, DOMAIN @@ -24,17 +27,22 @@ class LuftDatenFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 @callback - def _show_form(self, errors=None): + def _show_form(self, errors: dict[str, str] | None = None) -> FlowResult: """Show the form to the user.""" - data_schema = OrderedDict() - data_schema[vol.Required(CONF_SENSOR_ID)] = cv.positive_int - data_schema[vol.Optional(CONF_SHOW_ON_MAP, default=False)] = bool - return self.async_show_form( - step_id="user", data_schema=vol.Schema(data_schema), errors=errors or {} + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_SENSOR_ID): cv.positive_int, + vol.Optional(CONF_SHOW_ON_MAP, default=False): bool, + } + ), + errors=errors or {}, ) - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the start of the config flow.""" if not user_input: diff --git a/homeassistant/components/luftdaten/sensor.py b/homeassistant/components/luftdaten/sensor.py index 975acf30266..e0bc64bc918 100644 --- a/homeassistant/components/luftdaten/sensor.py +++ b/homeassistant/components/luftdaten/sensor.py @@ -1,6 +1,8 @@ """Support for Luftdaten sensors.""" from __future__ import annotations +from typing import cast + from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -131,4 +133,4 @@ class LuftdatenSensor(CoordinatorEntity, SensorEntity): or (value := self.coordinator.data.get(self.entity_description.key)) is None ): return None - return value + return cast(float, value) diff --git a/mypy.ini b/mypy.ini index 417e3d39d1c..6458bb83932 100644 --- a/mypy.ini +++ b/mypy.ini @@ -869,6 +869,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.luftdaten.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.mailbox.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1919,9 +1930,6 @@ ignore_errors = true [mypy-homeassistant.components.lovelace.*] ignore_errors = true -[mypy-homeassistant.components.luftdaten.*] -ignore_errors = true - [mypy-homeassistant.components.lutron_caseta.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 220837fd10b..9878fb891e7 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -67,7 +67,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.litejet.*", "homeassistant.components.litterrobot.*", "homeassistant.components.lovelace.*", - "homeassistant.components.luftdaten.*", "homeassistant.components.lutron_caseta.*", "homeassistant.components.lyric.*", "homeassistant.components.melcloud.*", From 60b2cdd069b5d23b975f9a84773da9452429bfaa Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 22 Dec 2021 12:24:29 +0100 Subject: [PATCH 0936/2644] Allow binary sensor state to be None (#60193) --- .../components/binary_sensor/__init__.py | 10 ++++++---- tests/components/binary_sensor/test_init.py | 2 +- tests/components/group/test_binary_sensor.py | 12 +++++++++--- .../homematicip_cloud/test_binary_sensor.py | 12 ++++++------ .../mobile_app/test_binary_sensor.py | 8 ++++---- tests/components/modbus/test_binary_sensor.py | 3 ++- tests/components/mqtt/test_binary_sensor.py | 17 +++++++++-------- tests/components/rflink/test_binary_sensor.py | 7 ++++--- tests/components/rfxtrx/test_binary_sensor.py | 19 ++++++++++--------- tests/components/rfxtrx/test_config_flow.py | 7 ++++--- .../components/tasmota/test_binary_sensor.py | 15 ++++++++------- .../components/template/test_binary_sensor.py | 15 +++++++++------ tests/components/trend/test_binary_sensor.py | 6 +++--- 13 files changed, 75 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index e3c6ed707e2..dc2d302012f 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -4,7 +4,7 @@ from __future__ import annotations from dataclasses import dataclass from datetime import timedelta import logging -from typing import final +from typing import Literal, final import voluptuous as vol @@ -18,7 +18,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 ) from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.typing import ConfigType, StateType +from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) @@ -200,6 +200,8 @@ class BinarySensorEntity(Entity): @final @property - def state(self) -> StateType: + def state(self) -> Literal["on", "off"] | None: """Return the state of the binary sensor.""" - return STATE_ON if self.is_on else STATE_OFF + if (is_on := self.is_on) is None: + return None + return STATE_ON if is_on else STATE_OFF diff --git a/tests/components/binary_sensor/test_init.py b/tests/components/binary_sensor/test_init.py index 18b42136ea2..5472df3f6e5 100644 --- a/tests/components/binary_sensor/test_init.py +++ b/tests/components/binary_sensor/test_init.py @@ -8,7 +8,7 @@ from homeassistant.const import STATE_OFF, STATE_ON def test_state(): """Test binary sensor state.""" sensor = binary_sensor.BinarySensorEntity() - assert sensor.state == STATE_OFF + assert sensor.state is None with mock.patch( "homeassistant.components.binary_sensor.BinarySensorEntity.is_on", new=False, diff --git a/tests/components/group/test_binary_sensor.py b/tests/components/group/test_binary_sensor.py index 9da54be2ab4..0a85c793aaa 100644 --- a/tests/components/group/test_binary_sensor.py +++ b/tests/components/group/test_binary_sensor.py @@ -1,7 +1,13 @@ """The tests for the Group Binary Sensor platform.""" from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.group import DOMAIN -from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -65,7 +71,7 @@ async def test_state_reporting_all(hass): hass.states.async_set("binary_sensor.test1", STATE_ON) hass.states.async_set("binary_sensor.test2", STATE_UNAVAILABLE) await hass.async_block_till_done() - assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_OFF + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNKNOWN hass.states.async_set("binary_sensor.test1", STATE_ON) hass.states.async_set("binary_sensor.test2", STATE_OFF) @@ -114,7 +120,7 @@ async def test_state_reporting_any(hass): hass.states.async_set("binary_sensor.test1", STATE_ON) hass.states.async_set("binary_sensor.test2", STATE_UNAVAILABLE) await hass.async_block_till_done() - assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_OFF + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNKNOWN hass.states.async_set("binary_sensor.test1", STATE_ON) hass.states.async_set("binary_sensor.test2", STATE_OFF) diff --git a/tests/components/homematicip_cloud/test_binary_sensor.py b/tests/components/homematicip_cloud/test_binary_sensor.py index c3922666665..c74cda43209 100644 --- a/tests/components/homematicip_cloud/test_binary_sensor.py +++ b/tests/components/homematicip_cloud/test_binary_sensor.py @@ -22,7 +22,7 @@ from homeassistant.components.homematicip_cloud.generic_entity import ( ATTR_RSSI_DEVICE, ATTR_SABOTAGE, ) -from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN from homeassistant.setup import async_setup_component from .helper import async_manipulate_test_data, get_and_check_entity_basics @@ -152,7 +152,7 @@ async def test_hmip_contact_interface(hass, default_mock_hap_factory): await async_manipulate_test_data(hass, hmip_device, "windowState", None) ha_state = hass.states.get(entity_id) - assert ha_state.state == STATE_OFF + assert ha_state.state == STATE_UNKNOWN async def test_hmip_shutter_contact(hass, default_mock_hap_factory): @@ -185,7 +185,7 @@ async def test_hmip_shutter_contact(hass, default_mock_hap_factory): await async_manipulate_test_data(hass, hmip_device, "windowState", None) ha_state = hass.states.get(entity_id) - assert ha_state.state == STATE_OFF + assert ha_state.state == STATE_UNKNOWN # test common attributes assert ha_state.attributes[ATTR_RSSI_DEVICE] == -54 @@ -215,7 +215,7 @@ async def test_hmip_shutter_contact_optical(hass, default_mock_hap_factory): await async_manipulate_test_data(hass, hmip_device, "windowState", None) ha_state = hass.states.get(entity_id) - assert ha_state.state == STATE_OFF + assert ha_state.state == STATE_UNKNOWN # test common attributes assert ha_state.attributes[ATTR_RSSI_DEVICE] == -72 @@ -562,7 +562,7 @@ async def test_hmip_multi_contact_interface(hass, default_mock_hap_factory): await async_manipulate_test_data(hass, hmip_device, "windowState", None, channel=5) ha_state = hass.states.get(entity_id) - assert ha_state.state == STATE_OFF + assert ha_state.state == STATE_UNKNOWN ha_state, hmip_device = get_and_check_entity_basics( hass, @@ -572,4 +572,4 @@ async def test_hmip_multi_contact_interface(hass, default_mock_hap_factory): "HmIP-FCI6", ) - assert ha_state.state == STATE_OFF + assert ha_state.state == STATE_UNKNOWN diff --git a/tests/components/mobile_app/test_binary_sensor.py b/tests/components/mobile_app/test_binary_sensor.py index e379603e079..ce1c017b4bf 100644 --- a/tests/components/mobile_app/test_binary_sensor.py +++ b/tests/components/mobile_app/test_binary_sensor.py @@ -1,7 +1,7 @@ """Entity tests for mobile_app.""" from http import HTTPStatus -from homeassistant.const import STATE_OFF +from homeassistant.const import STATE_UNKNOWN from homeassistant.helpers import device_registry as dr @@ -198,7 +198,7 @@ async def test_register_sensor_no_state(hass, create_registrations, webhook_clie assert entity.domain == "binary_sensor" assert entity.name == "Test 1 Is Charging" - assert entity.state == STATE_OFF # Binary sensor defaults to off + assert entity.state == STATE_UNKNOWN reg_resp = await webhook_client.post( webhook_url, @@ -223,7 +223,7 @@ async def test_register_sensor_no_state(hass, create_registrations, webhook_clie assert entity.domain == "binary_sensor" assert entity.name == "Test 1 Backup Is Charging" - assert entity.state == STATE_OFF # Binary sensor defaults to off + assert entity.state == STATE_UNKNOWN async def test_update_sensor_no_state(hass, create_registrations, webhook_client): @@ -270,4 +270,4 @@ async def test_update_sensor_no_state(hass, create_registrations, webhook_client assert json == {"is_charging": {"success": True}} updated_entity = hass.states.get("binary_sensor.test_1_is_charging") - assert updated_entity.state == STATE_OFF # Binary sensor defaults to off + assert updated_entity.state == STATE_UNKNOWN diff --git a/tests/components/modbus/test_binary_sensor.py b/tests/components/modbus/test_binary_sensor.py index d36a11e3eab..5127bd55ad1 100644 --- a/tests/components/modbus/test_binary_sensor.py +++ b/tests/components/modbus/test_binary_sensor.py @@ -18,6 +18,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import State @@ -141,7 +142,7 @@ async def test_all_binary_sensor(hass, expected, mock_do_cycle): ( [0x00], True, - STATE_OFF, + STATE_UNKNOWN, STATE_UNAVAILABLE, ), ], diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index aebdc8b692e..1a30836d074 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -12,6 +12,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNAVAILABLE, + STATE_UNKNOWN, ) import homeassistant.core as ha from homeassistant.setup import async_setup_component @@ -256,7 +257,7 @@ async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock): state = hass.states.get("binary_sensor.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN async_fire_mqtt_message(hass, "test-topic", "ON") state = hass.states.get("binary_sensor.test") @@ -286,11 +287,11 @@ async def test_invalid_sensor_value_via_mqtt_message(hass, mqtt_mock, caplog): state = hass.states.get("binary_sensor.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN async_fire_mqtt_message(hass, "test-topic", "0N") state = hass.states.get("binary_sensor.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert "No matching payload found for entity" in caplog.text caplog.clear() assert "No matching payload found for entity" not in caplog.text @@ -325,7 +326,7 @@ async def test_setting_sensor_value_via_mqtt_message_and_template(hass, mqtt_moc await hass.async_block_till_done() state = hass.states.get("binary_sensor.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN async_fire_mqtt_message(hass, "test-topic", "") state = hass.states.get("binary_sensor.test") @@ -357,7 +358,7 @@ async def test_setting_sensor_value_via_mqtt_message_and_template2( await hass.async_block_till_done() state = hass.states.get("binary_sensor.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN async_fire_mqtt_message(hass, "test-topic", "on") state = hass.states.get("binary_sensor.test") @@ -395,7 +396,7 @@ async def test_setting_sensor_value_via_mqtt_message_and_template_and_raw_state_ await hass.async_block_till_done() state = hass.states.get("binary_sensor.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN async_fire_mqtt_message(hass, "test-topic", b"\x01") state = hass.states.get("binary_sensor.test") @@ -427,11 +428,11 @@ async def test_setting_sensor_value_via_mqtt_message_empty_template( await hass.async_block_till_done() state = hass.states.get("binary_sensor.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN async_fire_mqtt_message(hass, "test-topic", "DEF") state = hass.states.get("binary_sensor.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert "Empty template output" in caplog.text async_fire_mqtt_message(hass, "test-topic", "ABC") diff --git a/tests/components/rflink/test_binary_sensor.py b/tests/components/rflink/test_binary_sensor.py index 118a3689fc7..4b6acfb4394 100644 --- a/tests/components/rflink/test_binary_sensor.py +++ b/tests/components/rflink/test_binary_sensor.py @@ -13,6 +13,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNAVAILABLE, + STATE_UNKNOWN, ) import homeassistant.core as ha import homeassistant.util.dt as dt_util @@ -53,7 +54,7 @@ async def test_default_setup(hass, monkeypatch): # test default state of sensor loaded from config config_sensor = hass.states.get("binary_sensor.test") assert config_sensor - assert config_sensor.state == STATE_OFF + assert config_sensor.state == STATE_UNKNOWN assert config_sensor.attributes["device_class"] == "door" # test on event for config sensor @@ -95,7 +96,7 @@ async def test_entity_availability(hass, monkeypatch): ) # Entities are available by default - assert hass.states.get("binary_sensor.test").state == STATE_OFF + assert hass.states.get("binary_sensor.test").state == STATE_UNKNOWN # Mock a disconnect of the Rflink device disconnect_callback() @@ -113,7 +114,7 @@ async def test_entity_availability(hass, monkeypatch): await hass.async_block_till_done() # Entities should be available again - assert hass.states.get("binary_sensor.test").state == STATE_OFF + assert hass.states.get("binary_sensor.test").state == STATE_UNKNOWN async def test_off_delay(hass, legacy_patchable_time, monkeypatch): diff --git a/tests/components/rfxtrx/test_binary_sensor.py b/tests/components/rfxtrx/test_binary_sensor.py index 5b76c6287a3..96368d166d7 100644 --- a/tests/components/rfxtrx/test_binary_sensor.py +++ b/tests/components/rfxtrx/test_binary_sensor.py @@ -3,6 +3,7 @@ import pytest from homeassistant.components.rfxtrx import DOMAIN from homeassistant.components.rfxtrx.const import ATTR_EVENT +from homeassistant.const import STATE_UNKNOWN from homeassistant.core import State from tests.common import MockConfigEntry, mock_restore_cache @@ -32,7 +33,7 @@ async def test_one(hass, rfxtrx): state = hass.states.get("binary_sensor.ac_213c7f2_48") assert state - assert state.state == "off" + assert state.state == STATE_UNKNOWN assert state.attributes.get("friendly_name") == "AC 213c7f2:48" @@ -57,7 +58,7 @@ async def test_one_pt2262(hass, rfxtrx): state = hass.states.get("binary_sensor.pt2262_22670e") assert state - assert state.state == "off" # probably aught to be unknown + assert state.state == STATE_UNKNOWN assert state.attributes.get("friendly_name") == "PT2262 22670e" await rfxtrx.signal("0913000022670e013970") @@ -84,12 +85,12 @@ async def test_pt2262_unconfigured(hass, rfxtrx): state = hass.states.get("binary_sensor.pt2262_22670e") assert state - assert state.state == "off" # probably aught to be unknown + assert state.state == STATE_UNKNOWN assert state.attributes.get("friendly_name") == "PT2262 22670e" state = hass.states.get("binary_sensor.pt2262_226707") assert state - assert state.state == "off" # probably aught to be unknown + assert state.state == STATE_UNKNOWN assert state.attributes.get("friendly_name") == "PT2262 226707" @@ -133,17 +134,17 @@ async def test_several(hass, rfxtrx): state = hass.states.get("binary_sensor.ac_213c7f2_48") assert state - assert state.state == "off" + assert state.state == STATE_UNKNOWN assert state.attributes.get("friendly_name") == "AC 213c7f2:48" state = hass.states.get("binary_sensor.ac_118cdea_2") assert state - assert state.state == "off" + assert state.state == STATE_UNKNOWN assert state.attributes.get("friendly_name") == "AC 118cdea:2" state = hass.states.get("binary_sensor.ac_118cdea_3") assert state - assert state.state == "off" + assert state.state == STATE_UNKNOWN assert state.attributes.get("friendly_name") == "AC 118cdea:3" # "2: Group on" @@ -214,7 +215,7 @@ async def test_off_delay(hass, rfxtrx, timestep): state = hass.states.get("binary_sensor.ac_118cdea_2") assert state - assert state.state == "off" + assert state.state == STATE_UNKNOWN await rfxtrx.signal("0b1100100118cdea02010f70") state = hass.states.get("binary_sensor.ac_118cdea_2") @@ -317,5 +318,5 @@ async def test_pt2262_duplicate_id(hass, rfxtrx): state = hass.states.get("binary_sensor.pt2262_22670e") assert state - assert state.state == "off" # probably aught to be unknown + assert state.state == STATE_UNKNOWN assert state.attributes.get("friendly_name") == "PT2262 22670e" diff --git a/tests/components/rfxtrx/test_config_flow.py b/tests/components/rfxtrx/test_config_flow.py index 10c9ca12022..cd87f02c893 100644 --- a/tests/components/rfxtrx/test_config_flow.py +++ b/tests/components/rfxtrx/test_config_flow.py @@ -6,6 +6,7 @@ import serial.tools.list_ports from homeassistant import config_entries, data_entry_flow from homeassistant.components.rfxtrx import DOMAIN, config_flow +from homeassistant.const import STATE_UNKNOWN from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.common import MockConfigEntry @@ -367,7 +368,7 @@ async def test_options_add_device(hass): state = hass.states.get("binary_sensor.ac_213c7f2_48") assert state - assert state.state == "off" + assert state.state == STATE_UNKNOWN assert state.attributes.get("friendly_name") == "AC 213c7f2:48" @@ -456,7 +457,7 @@ async def test_options_add_remove_device(hass): state = hass.states.get("binary_sensor.ac_213c7f2_48") assert state - assert state.state == "off" + assert state.state == STATE_UNKNOWN assert state.attributes.get("friendly_name") == "AC 213c7f2:48" device_registry = dr.async_get(hass) @@ -900,7 +901,7 @@ async def test_options_add_and_configure_device(hass): state = hass.states.get("binary_sensor.pt2262_22670e") assert state - assert state.state == "off" + assert state.state == STATE_UNKNOWN assert state.attributes.get("friendly_name") == "PT2262 22670e" device_registry = dr.async_get(hass) diff --git a/tests/components/tasmota/test_binary_sensor.py b/tests/components/tasmota/test_binary_sensor.py index 5b19e46337a..94963234074 100644 --- a/tests/components/tasmota/test_binary_sensor.py +++ b/tests/components/tasmota/test_binary_sensor.py @@ -17,6 +17,7 @@ from homeassistant.const import ( EVENT_STATE_CHANGED, STATE_OFF, STATE_ON, + STATE_UNKNOWN, Platform, ) import homeassistant.core as ha @@ -58,7 +59,7 @@ async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota): async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() state = hass.states.get("binary_sensor.tasmota_binary_sensor_1") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert not state.attributes.get(ATTR_ASSUMED_STATE) # Test normal state update @@ -124,7 +125,7 @@ async def test_controlling_state_via_mqtt_switchname(hass, mqtt_mock, setup_tasm async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() state = hass.states.get("binary_sensor.custom_name") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert not state.attributes.get(ATTR_ASSUMED_STATE) # Test normal state update @@ -183,7 +184,7 @@ async def test_pushon_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota) async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() state = hass.states.get("binary_sensor.tasmota_binary_sensor_1") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert not state.attributes.get(ATTR_ASSUMED_STATE) # Test normal state update @@ -260,14 +261,14 @@ async def test_off_delay(hass, mqtt_mock, setup_tasmota): async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() - assert events == ["off"] + assert events == ["unknown"] async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/RESULT", '{"Switch1":{"Action":"ON"}}' ) await hass.async_block_till_done() state = hass.states.get("binary_sensor.tasmota_binary_sensor_1") assert state.state == STATE_ON - assert events == ["off", "on"] + assert events == ["unknown", "on"] async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/RESULT", '{"Switch1":{"Action":"ON"}}' @@ -275,13 +276,13 @@ async def test_off_delay(hass, mqtt_mock, setup_tasmota): await hass.async_block_till_done() state = hass.states.get("binary_sensor.tasmota_binary_sensor_1") assert state.state == STATE_ON - assert events == ["off", "on", "on"] + assert events == ["unknown", "on", "on"] async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=1)) await hass.async_block_till_done() state = hass.states.get("binary_sensor.tasmota_binary_sensor_1") assert state.state == STATE_OFF - assert events == ["off", "on", "on", "off"] + assert events == ["unknown", "on", "on", "off"] async def test_availability_when_connection_lost( diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index 0605759b16b..4ed4d59c124 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -13,6 +13,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import Context, CoreState from homeassistant.helpers import entity_registry @@ -499,8 +500,10 @@ async def test_event(hass, start_ha): ) async def test_template_delay_on_off(hass, start_ha): """Test binary sensor template delay on.""" - assert hass.states.get("binary_sensor.test_on").state == OFF - assert hass.states.get("binary_sensor.test_off").state == OFF + # Ensure the initial state is not on + assert hass.states.get("binary_sensor.test_on").state != ON + assert hass.states.get("binary_sensor.test_off").state != ON + hass.states.async_set("input_number.delay", 5) hass.states.async_set("sensor.test_state", ON) await hass.async_block_till_done() @@ -722,10 +725,10 @@ async def test_no_update_template_match_all(hass, caplog): hass.states.async_set("binary_sensor.test_sensor", "true") assert len(hass.states.async_all()) == 5 - assert hass.states.get("binary_sensor.all_state").state == OFF - assert hass.states.get("binary_sensor.all_icon").state == OFF - assert hass.states.get("binary_sensor.all_entity_picture").state == OFF - assert hass.states.get("binary_sensor.all_attribute").state == OFF + assert hass.states.get("binary_sensor.all_state").state == STATE_UNKNOWN + assert hass.states.get("binary_sensor.all_icon").state == STATE_UNKNOWN + assert hass.states.get("binary_sensor.all_entity_picture").state == STATE_UNKNOWN + assert hass.states.get("binary_sensor.all_attribute").state == STATE_UNKNOWN hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() diff --git a/tests/components/trend/test_binary_sensor.py b/tests/components/trend/test_binary_sensor.py index 4716762d2e7..f7ccee97b29 100644 --- a/tests/components/trend/test_binary_sensor.py +++ b/tests/components/trend/test_binary_sensor.py @@ -4,7 +4,7 @@ from unittest.mock import patch from homeassistant import config as hass_config, setup from homeassistant.components.trend import DOMAIN -from homeassistant.const import SERVICE_RELOAD +from homeassistant.const import SERVICE_RELOAD, STATE_UNKNOWN import homeassistant.util.dt as dt_util from tests.common import ( @@ -307,7 +307,7 @@ class TestTrendBinarySensor: self.hass.states.set("sensor.test_state", "Numeric") self.hass.block_till_done() state = self.hass.states.get("binary_sensor.test_trend_sensor") - assert state.state == "off" + assert state.state == STATE_UNKNOWN def test_missing_attribute(self): """Test attribute down trend.""" @@ -333,7 +333,7 @@ class TestTrendBinarySensor: self.hass.states.set("sensor.test_state", "State", {"attr": "1"}) self.hass.block_till_done() state = self.hass.states.get("binary_sensor.test_trend_sensor") - assert state.state == "off" + assert state.state == STATE_UNKNOWN def test_invalid_name_does_not_create(self): """Test invalid name.""" From 89526fe86cef1f35a48b24ec4a294dfeca45809d Mon Sep 17 00:00:00 2001 From: ShadowBr0ther <38364191+ShadowBr0ther@users.noreply.github.com> Date: Wed, 22 Dec 2021 12:26:23 +0100 Subject: [PATCH 0937/2644] Fix repetier crash when printer is offline (#62490) --- CODEOWNERS | 2 +- homeassistant/components/repetier/__init__.py | 2 +- homeassistant/components/repetier/manifest.json | 4 ++-- requirements_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index bb926fa8b5b..aedfcea7e96 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -748,7 +748,7 @@ tests/components/recorder/* @home-assistant/core homeassistant/components/rejseplanen/* @DarkFox homeassistant/components/renault/* @epenet tests/components/renault/* @epenet -homeassistant/components/repetier/* @MTrab +homeassistant/components/repetier/* @MTrab @ShadowBr0ther homeassistant/components/rflink/* @javicalle tests/components/rflink/* @javicalle homeassistant/components/rfxtrx/* @danielhiversen @elupus @RobBie1221 diff --git a/homeassistant/components/repetier/__init__.py b/homeassistant/components/repetier/__init__.py index 54acbc07449..0c88d49eb22 100644 --- a/homeassistant/components/repetier/__init__.py +++ b/homeassistant/components/repetier/__init__.py @@ -5,7 +5,7 @@ from dataclasses import dataclass from datetime import timedelta import logging -import pyrepetier +import pyrepetierng as pyrepetier import voluptuous as vol from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription diff --git a/homeassistant/components/repetier/manifest.json b/homeassistant/components/repetier/manifest.json index 0fd3d904987..463c42c3a64 100644 --- a/homeassistant/components/repetier/manifest.json +++ b/homeassistant/components/repetier/manifest.json @@ -2,7 +2,7 @@ "domain": "repetier", "name": "Repetier-Server", "documentation": "https://www.home-assistant.io/integrations/repetier", - "requirements": ["pyrepetier==3.0.5"], - "codeowners": ["@MTrab"], + "requirements": ["pyrepetierng==0.1.0"], + "codeowners": ["@MTrab", "@ShadowBr0ther"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 7e7fd6f7b95..ef1b59a40a0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1766,7 +1766,7 @@ pyrainbird==0.4.3 pyrecswitch==1.0.2 # homeassistant.components.repetier -pyrepetier==3.0.5 +pyrepetierng==0.1.0 # homeassistant.components.risco pyrisco==0.3.1 From f40870b4d74efd3274b009eaa05172f73873bb97 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Wed, 22 Dec 2021 12:27:36 +0100 Subject: [PATCH 0938/2644] Round imperial values to two decimals in bmw_connected_drive (#62531) Co-authored-by: rikroe --- homeassistant/components/bmw_connected_drive/sensor.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 241ee3a83b6..1ef7e33497c 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -73,7 +73,7 @@ SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = { unit_metric=LENGTH_KILOMETERS, unit_imperial=LENGTH_MILES, value=lambda x, hass: round( - hass.config.units.length(x[0], UNIT_MAP.get(x[1], x[1])) + hass.config.units.length(x[0], UNIT_MAP.get(x[1], x[1])), 2 ), ), "remaining_range_total": BMWSensorEntityDescription( @@ -82,7 +82,7 @@ SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = { unit_metric=LENGTH_KILOMETERS, unit_imperial=LENGTH_MILES, value=lambda x, hass: round( - hass.config.units.length(x[0], UNIT_MAP.get(x[1], x[1])) + hass.config.units.length(x[0], UNIT_MAP.get(x[1], x[1])), 2 ), ), "remaining_range_electric": BMWSensorEntityDescription( @@ -91,7 +91,7 @@ SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = { unit_metric=LENGTH_KILOMETERS, unit_imperial=LENGTH_MILES, value=lambda x, hass: round( - hass.config.units.length(x[0], UNIT_MAP.get(x[1], x[1])) + hass.config.units.length(x[0], UNIT_MAP.get(x[1], x[1])), 2 ), ), "remaining_range_fuel": BMWSensorEntityDescription( @@ -100,7 +100,7 @@ SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = { unit_metric=LENGTH_KILOMETERS, unit_imperial=LENGTH_MILES, value=lambda x, hass: round( - hass.config.units.length(x[0], UNIT_MAP.get(x[1], x[1])) + hass.config.units.length(x[0], UNIT_MAP.get(x[1], x[1])), 2 ), ), "remaining_fuel": BMWSensorEntityDescription( @@ -109,7 +109,7 @@ SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = { unit_metric=VOLUME_LITERS, unit_imperial=VOLUME_GALLONS, value=lambda x, hass: round( - hass.config.units.volume(x[0], UNIT_MAP.get(x[1], x[1])) + hass.config.units.volume(x[0], UNIT_MAP.get(x[1], x[1])), 2 ), ), "fuel_percent": BMWSensorEntityDescription( From 986b60e527c02d42ce44f3bc9d98cb43b7e54dec Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Wed, 22 Dec 2021 12:52:01 +0100 Subject: [PATCH 0939/2644] Replace charging_time_remaining with charging_end_time in bmw_connected_drive (#60942) Co-authored-by: rikroe --- .../components/bmw_connected_drive/sensor.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 1ef7e33497c..f21c1b851ca 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -19,7 +19,6 @@ from homeassistant.const import ( LENGTH_KILOMETERS, LENGTH_MILES, PERCENTAGE, - TIME_HOURS, VOLUME_GALLONS, VOLUME_LITERS, ) @@ -49,11 +48,18 @@ class BMWSensorEntityDescription(SensorEntityDescription): SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = { # --- Generic --- - "charging_time_remaining": BMWSensorEntityDescription( - key="charging_time_remaining", - icon="mdi:update", - unit_metric=TIME_HOURS, - unit_imperial=TIME_HOURS, + "charging_start_time": BMWSensorEntityDescription( + key="charging_start_time", + device_class=SensorDeviceClass.TIMESTAMP, + entity_registry_enabled_default=False, + ), + "charging_end_time": BMWSensorEntityDescription( + key="charging_end_time", + device_class=SensorDeviceClass.TIMESTAMP, + ), + "charging_time_label": BMWSensorEntityDescription( + key="charging_time_label", + entity_registry_enabled_default=False, ), "charging_status": BMWSensorEntityDescription( key="charging_status", From 925e4998b42fd7c5a5e4c6a2ddd1e0edc40c7901 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 22 Dec 2021 12:59:54 +0100 Subject: [PATCH 0940/2644] Fix missing object assignment for Fritz (#62575) --- homeassistant/components/fritz/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fritz/config_flow.py b/homeassistant/components/fritz/config_flow.py index 3a0cc2b1301..3b6089d3272 100644 --- a/homeassistant/components/fritz/config_flow.py +++ b/homeassistant/components/fritz/config_flow.py @@ -117,7 +117,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): self._port = ssdp_location.port self._name = ( discovery_info.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME) - or self.fritz_tools.model + or discovery_info.upnp[ssdp.ATTR_UPNP_MODEL_NAME] ) self.context[CONF_HOST] = self._host From 496165711d1d9507fdc18ec5f8e8e8f0d59d1309 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 22 Dec 2021 13:00:51 +0100 Subject: [PATCH 0941/2644] Improve config flow Luftdaten (#62589) Co-authored-by: Martin Hjelmare --- .../components/luftdaten/config_flow.py | 24 ++---------- .../components/luftdaten/test_config_flow.py | 38 +++++++++++++++++-- 2 files changed, 37 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/luftdaten/config_flow.py b/homeassistant/components/luftdaten/config_flow.py index 31f518dcbcf..d40eb3295c7 100644 --- a/homeassistant/components/luftdaten/config_flow.py +++ b/homeassistant/components/luftdaten/config_flow.py @@ -8,17 +8,12 @@ from luftdaten.exceptions import LuftdatenConnectionError import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import ( - CONF_MONITORED_CONDITIONS, - CONF_SCAN_INTERVAL, - CONF_SENSORS, - CONF_SHOW_ON_MAP, -) +from homeassistant.const import CONF_SHOW_ON_MAP from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv -from .const import CONF_SENSOR_ID, DEFAULT_SCAN_INTERVAL, DOMAIN +from .const import CONF_SENSOR_ID, DOMAIN class LuftDatenFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -44,8 +39,7 @@ class LuftDatenFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle the start of the config flow.""" - - if not user_input: + if user_input is None: return self._show_form() await self.async_set_unique_id(str(user_input[CONF_SENSOR_ID])) @@ -61,18 +55,6 @@ class LuftDatenFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if not valid: return self._show_form({CONF_SENSOR_ID: "invalid_sensor"}) - available_sensors = [ - x for x, x_values in luftdaten.values.items() if x_values is not None - ] - - if available_sensors: - user_input.update( - {CONF_SENSORS: {CONF_MONITORED_CONDITIONS: available_sensors}} - ) - - scan_interval = user_input.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) - user_input.update({CONF_SCAN_INTERVAL: scan_interval.total_seconds()}) - return self.async_create_entry( title=str(user_input[CONF_SENSOR_ID]), data=user_input ) diff --git a/tests/components/luftdaten/test_config_flow.py b/tests/components/luftdaten/test_config_flow.py index 9b9aa139e02..21589381066 100644 --- a/tests/components/luftdaten/test_config_flow.py +++ b/tests/components/luftdaten/test_config_flow.py @@ -6,7 +6,7 @@ from luftdaten.exceptions import LuftdatenConnectionError from homeassistant.components.luftdaten import DOMAIN from homeassistant.components.luftdaten.const import CONF_SENSOR_ID from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_SCAN_INTERVAL, CONF_SHOW_ON_MAP +from homeassistant.const import CONF_SHOW_ON_MAP from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, @@ -60,6 +60,21 @@ async def test_communication_error(hass: HomeAssistant) -> None: assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {CONF_SENSOR_ID: "cannot_connect"} + with patch("luftdaten.Luftdaten.get_data"), patch( + "luftdaten.Luftdaten.validate_sensor", return_value=True + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + user_input={CONF_SENSOR_ID: 12345}, + ) + + assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("title") == "12345" + assert result3.get("data") == { + CONF_SENSOR_ID: 12345, + CONF_SHOW_ON_MAP: False, + } + async def test_invalid_sensor(hass: HomeAssistant) -> None: """Test that an invalid sensor throws an error.""" @@ -82,6 +97,22 @@ async def test_invalid_sensor(hass: HomeAssistant) -> None: assert result2.get("type") == RESULT_TYPE_FORM assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {CONF_SENSOR_ID: "invalid_sensor"} + assert "flow_id" in result2 + + with patch("luftdaten.Luftdaten.get_data"), patch( + "luftdaten.Luftdaten.validate_sensor", return_value=True + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + user_input={CONF_SENSOR_ID: 12345}, + ) + + assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("title") == "12345" + assert result3.get("data") == { + CONF_SENSOR_ID: 12345, + CONF_SHOW_ON_MAP: False, + } async def test_step_user(hass: HomeAssistant, mock_setup_entry: MagicMock) -> None: @@ -101,7 +132,7 @@ async def test_step_user(hass: HomeAssistant, mock_setup_entry: MagicMock) -> No result["flow_id"], user_input={ CONF_SENSOR_ID: 12345, - CONF_SHOW_ON_MAP: False, + CONF_SHOW_ON_MAP: True, }, ) @@ -109,6 +140,5 @@ async def test_step_user(hass: HomeAssistant, mock_setup_entry: MagicMock) -> No assert result2.get("title") == "12345" assert result2.get("data") == { CONF_SENSOR_ID: 12345, - CONF_SHOW_ON_MAP: False, - CONF_SCAN_INTERVAL: 600.0, + CONF_SHOW_ON_MAP: True, } From d9788c244708ad5207a143a8b216739c6c9f1a3d Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 22 Dec 2021 07:31:55 -0500 Subject: [PATCH 0942/2644] Use platform enums in rest tests (#62564) --- tests/components/rest/test_binary_sensor.py | 19 ++++++------ tests/components/rest/test_switch.py | 33 +++++++++++---------- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/tests/components/rest/test_binary_sensor.py b/tests/components/rest/test_binary_sensor.py index 72fc46c56be..8383d53b51f 100644 --- a/tests/components/rest/test_binary_sensor.py +++ b/tests/components/rest/test_binary_sensor.py @@ -8,7 +8,7 @@ import httpx import respx from homeassistant import config as hass_config -from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDeviceClass +from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, @@ -17,6 +17,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNAVAILABLE, + Platform, ) from homeassistant.setup import async_setup_component @@ -26,7 +27,7 @@ from tests.common import get_fixture_path async def test_setup_missing_basic_config(hass): """Test setup with configuration missing required entries.""" assert await async_setup_component( - hass, DOMAIN, {"binary_sensor": {"platform": "rest"}} + hass, Platform.BINARY_SENSOR, {"binary_sensor": {"platform": "rest"}} ) await hass.async_block_till_done() assert len(hass.states.async_all("binary_sensor")) == 0 @@ -36,7 +37,7 @@ async def test_setup_missing_config(hass): """Test setup with configuration missing required entries.""" assert await async_setup_component( hass, - DOMAIN, + Platform.BINARY_SENSOR, { "binary_sensor": { "platform": "rest", @@ -58,7 +59,7 @@ async def test_setup_failed_connect(hass, caplog): ) assert await async_setup_component( hass, - DOMAIN, + Platform.BINARY_SENSOR, { "binary_sensor": { "platform": "rest", @@ -78,7 +79,7 @@ async def test_setup_timeout(hass): respx.get("http://localhost").mock(side_effect=asyncio.TimeoutError()) assert await async_setup_component( hass, - DOMAIN, + Platform.BINARY_SENSOR, { "binary_sensor": { "platform": "rest", @@ -97,7 +98,7 @@ async def test_setup_minimum(hass): respx.get("http://localhost") % HTTPStatus.OK assert await async_setup_component( hass, - DOMAIN, + Platform.BINARY_SENSOR, { "binary_sensor": { "platform": "rest", @@ -116,7 +117,7 @@ async def test_setup_minimum_resource_template(hass): respx.get("http://localhost") % HTTPStatus.OK assert await async_setup_component( hass, - DOMAIN, + Platform.BINARY_SENSOR, { "binary_sensor": { "platform": "rest", @@ -134,7 +135,7 @@ async def test_setup_duplicate_resource_template(hass): respx.get("http://localhost") % HTTPStatus.OK assert await async_setup_component( hass, - DOMAIN, + Platform.BINARY_SENSOR, { "binary_sensor": { "platform": "rest", @@ -418,7 +419,7 @@ async def test_setup_query_params(hass): respx.get("http://localhost", params={"search": "something"}) % HTTPStatus.OK assert await async_setup_component( hass, - DOMAIN, + Platform.BINARY_SENSOR, { "binary_sensor": { "platform": "rest", diff --git a/tests/components/rest/test_switch.py b/tests/components/rest/test_switch.py index d9a5019f2cb..c422bfc841d 100644 --- a/tests/components/rest/test_switch.py +++ b/tests/components/rest/test_switch.py @@ -6,7 +6,7 @@ import aiohttp from homeassistant.components.rest import DOMAIN import homeassistant.components.rest.switch as rest -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SwitchDeviceClass +from homeassistant.components.switch import SwitchDeviceClass from homeassistant.const import ( CONF_HEADERS, CONF_NAME, @@ -14,6 +14,7 @@ from homeassistant.const import ( CONF_PLATFORM, CONF_RESOURCE, CONTENT_TYPE_JSON, + Platform, ) from homeassistant.helpers.template import Template from homeassistant.setup import async_setup_component @@ -68,12 +69,12 @@ async def test_setup_timeout(hass, aioclient_mock): async def test_setup_minimum(hass, aioclient_mock): """Test setup with minimum configuration.""" aioclient_mock.get("http://localhost", status=HTTPStatus.OK) - with assert_setup_component(1, SWITCH_DOMAIN): + with assert_setup_component(1, Platform.SWITCH): assert await async_setup_component( hass, - SWITCH_DOMAIN, + Platform.SWITCH, { - SWITCH_DOMAIN: { + Platform.SWITCH: { CONF_PLATFORM: DOMAIN, CONF_RESOURCE: "http://localhost", } @@ -86,12 +87,12 @@ async def test_setup_minimum(hass, aioclient_mock): async def test_setup_query_params(hass, aioclient_mock): """Test setup with query params.""" aioclient_mock.get("http://localhost/?search=something", status=HTTPStatus.OK) - with assert_setup_component(1, SWITCH_DOMAIN): + with assert_setup_component(1, Platform.SWITCH): assert await async_setup_component( hass, - SWITCH_DOMAIN, + Platform.SWITCH, { - SWITCH_DOMAIN: { + Platform.SWITCH: { CONF_PLATFORM: DOMAIN, CONF_RESOURCE: "http://localhost", CONF_PARAMS: {"search": "something"}, @@ -109,9 +110,9 @@ async def test_setup(hass, aioclient_mock): aioclient_mock.get("http://localhost", status=HTTPStatus.OK) assert await async_setup_component( hass, - SWITCH_DOMAIN, + Platform.SWITCH, { - SWITCH_DOMAIN: { + Platform.SWITCH: { CONF_PLATFORM: DOMAIN, CONF_NAME: "foo", CONF_RESOURCE: "http://localhost", @@ -123,7 +124,7 @@ async def test_setup(hass, aioclient_mock): ) await hass.async_block_till_done() assert aioclient_mock.call_count == 1 - assert_setup_component(1, SWITCH_DOMAIN) + assert_setup_component(1, Platform.SWITCH) async def test_setup_with_state_resource(hass, aioclient_mock): @@ -132,9 +133,9 @@ async def test_setup_with_state_resource(hass, aioclient_mock): aioclient_mock.get("http://localhost/state", status=HTTPStatus.OK) assert await async_setup_component( hass, - SWITCH_DOMAIN, + Platform.SWITCH, { - SWITCH_DOMAIN: { + Platform.SWITCH: { CONF_PLATFORM: DOMAIN, CONF_NAME: "foo", CONF_RESOURCE: "http://localhost", @@ -147,7 +148,7 @@ async def test_setup_with_state_resource(hass, aioclient_mock): ) await hass.async_block_till_done() assert aioclient_mock.call_count == 1 - assert_setup_component(1, SWITCH_DOMAIN) + assert_setup_component(1, Platform.SWITCH) async def test_setup_with_templated_headers_params(hass, aioclient_mock): @@ -155,9 +156,9 @@ async def test_setup_with_templated_headers_params(hass, aioclient_mock): aioclient_mock.get("http://localhost", status=HTTPStatus.OK) assert await async_setup_component( hass, - SWITCH_DOMAIN, + Platform.SWITCH, { - SWITCH_DOMAIN: { + Platform.SWITCH: { CONF_PLATFORM: DOMAIN, CONF_NAME: "foo", CONF_RESOURCE: "http://localhost", @@ -178,7 +179,7 @@ async def test_setup_with_templated_headers_params(hass, aioclient_mock): assert aioclient_mock.mock_calls[-1][3].get("User-Agent") == "Mozilla/5.0" assert aioclient_mock.mock_calls[-1][1].query["start"] == "0" assert aioclient_mock.mock_calls[-1][1].query["end"] == "5" - assert_setup_component(1, SWITCH_DOMAIN) + assert_setup_component(1, Platform.SWITCH) """Tests for REST switch platform.""" From 432d48a4d7dfddbdcb7910aed042178b78f31a11 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 22 Dec 2021 07:33:13 -0500 Subject: [PATCH 0943/2644] Use platform enums in vizio tests (#62555) --- tests/components/vizio/test_init.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/tests/components/vizio/test_init.py b/tests/components/vizio/test_init.py index ccda9253ec7..288dd2c6ac0 100644 --- a/tests/components/vizio/test_init.py +++ b/tests/components/vizio/test_init.py @@ -3,9 +3,8 @@ from datetime import timedelta import pytest -from homeassistant.components.media_player.const import DOMAIN as MP_DOMAIN from homeassistant.components.vizio.const import DOMAIN -from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.const import STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -25,7 +24,7 @@ async def test_setup_component( hass, DOMAIN, {DOMAIN: MOCK_USER_VALID_TV_CONFIG} ) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 1 + assert len(hass.states.async_entity_ids(Platform.MEDIA_PLAYER)) == 1 async def test_tv_load_and_unload( @@ -40,12 +39,12 @@ async def test_tv_load_and_unload( config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 1 + assert len(hass.states.async_entity_ids(Platform.MEDIA_PLAYER)) == 1 assert DOMAIN in hass.data assert await config_entry.async_unload(hass) await hass.async_block_till_done() - entities = hass.states.async_entity_ids(MP_DOMAIN) + entities = hass.states.async_entity_ids(Platform.MEDIA_PLAYER) assert len(entities) == 1 for entity in entities: assert hass.states.get(entity).state == STATE_UNAVAILABLE @@ -64,12 +63,12 @@ async def test_speaker_load_and_unload( config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 1 + assert len(hass.states.async_entity_ids(Platform.MEDIA_PLAYER)) == 1 assert DOMAIN in hass.data assert await config_entry.async_unload(hass) await hass.async_block_till_done() - entities = hass.states.async_entity_ids(MP_DOMAIN) + entities = hass.states.async_entity_ids(Platform.MEDIA_PLAYER) assert len(entities) == 1 for entity in entities: assert hass.states.get(entity).state == STATE_UNAVAILABLE @@ -91,7 +90,7 @@ async def test_coordinator_update_failure( config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 1 + assert len(hass.states.async_entity_ids(Platform.MEDIA_PLAYER)) == 1 assert DOMAIN in hass.data # Failing 25 days in a row should result in a single log message From 75e8a2ec77dc61bd7c1cc2a28daf1a005cd0f2e4 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 22 Dec 2021 07:59:54 -0500 Subject: [PATCH 0944/2644] Use platform enums in plex tests (#62561) --- tests/components/plex/const.py | 4 +- tests/components/plex/test_config_flow.py | 10 +++-- tests/components/plex/test_device_handling.py | 10 ++--- tests/components/plex/test_media_search.py | 41 +++++++++---------- tests/components/plex/test_server.py | 10 ++--- 5 files changed, 38 insertions(+), 37 deletions(-) diff --git a/tests/components/plex/const.py b/tests/components/plex/const.py index 9e376d19cac..ff9d3c0e9b3 100644 --- a/tests/components/plex/const.py +++ b/tests/components/plex/const.py @@ -1,5 +1,4 @@ """Constants used by Plex tests.""" -from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.plex import const from homeassistant.const import ( CONF_CLIENT_ID, @@ -8,6 +7,7 @@ from homeassistant.const import ( CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL, + Platform, ) MOCK_SERVERS = [ @@ -55,7 +55,7 @@ SECONDARY_DATA = { } DEFAULT_OPTIONS = { - MP_DOMAIN: { + Platform.MEDIA_PLAYER: { const.CONF_IGNORE_NEW_SHARED_USERS: False, const.CONF_MONITORED_USERS: MOCK_USERS, const.CONF_USE_EPISODE_ART: False, diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index c18d38fdba1..43df5092435 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -8,7 +8,6 @@ import plexapi.exceptions import pytest import requests.exceptions -from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.plex import config_flow from homeassistant.components.plex.const import ( AUTOMATIC_SETUP_STRING, @@ -36,6 +35,7 @@ from homeassistant.const import ( CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL, + Platform, ) from .const import DEFAULT_OPTIONS, MOCK_SERVERS, MOCK_TOKEN, PLEX_DIRECT_URL @@ -414,7 +414,7 @@ async def test_option_flow(hass, entry, mock_plex_server): ) assert result["type"] == "create_entry" assert result["data"] == { - MP_DOMAIN: { + Platform.MEDIA_PLAYER: { CONF_USE_EPISODE_ART: True, CONF_IGNORE_NEW_SHARED_USERS: True, CONF_MONITORED_USERS: { @@ -446,7 +446,7 @@ async def test_missing_option_flow(hass, entry, mock_plex_server): ) assert result["type"] == "create_entry" assert result["data"] == { - MP_DOMAIN: { + Platform.MEDIA_PLAYER: { CONF_USE_EPISODE_ART: True, CONF_IGNORE_NEW_SHARED_USERS: True, CONF_MONITORED_USERS: { @@ -460,7 +460,9 @@ async def test_missing_option_flow(hass, entry, mock_plex_server): async def test_option_flow_new_users_available(hass, entry, setup_plex_server): """Test config options multiselect defaults when new Plex users are seen.""" OPTIONS_OWNER_ONLY = copy.deepcopy(DEFAULT_OPTIONS) - OPTIONS_OWNER_ONLY[MP_DOMAIN][CONF_MONITORED_USERS] = {"User 1": {"enabled": True}} + OPTIONS_OWNER_ONLY[Platform.MEDIA_PLAYER][CONF_MONITORED_USERS] = { + "User 1": {"enabled": True} + } entry.options = OPTIONS_OWNER_ONLY mock_plex_server = await setup_plex_server(config_entry=entry) diff --git a/tests/components/plex/test_device_handling.py b/tests/components/plex/test_device_handling.py index f36e5dc3641..2c23ee7cb09 100644 --- a/tests/components/plex/test_device_handling.py +++ b/tests/components/plex/test_device_handling.py @@ -1,7 +1,7 @@ """Tests for handling the device registry.""" -from homeassistant.components.media_player.const import DOMAIN as MP_DOMAIN from homeassistant.components.plex.const import DOMAIN +from homeassistant.const import Platform async def test_cleanup_orphaned_devices(hass, entry, setup_plex_server): @@ -18,7 +18,7 @@ async def test_cleanup_orphaned_devices(hass, entry, setup_plex_server): assert test_device is not None test_entity = entity_registry.async_get_or_create( - MP_DOMAIN, DOMAIN, "entity_unique_id_123", device_id=test_device.id + Platform.MEDIA_PLAYER, DOMAIN, "entity_unique_id_123", device_id=test_device.id ) assert test_entity is not None @@ -53,9 +53,9 @@ async def test_migrate_transient_devices( identifiers=plexweb_device_id, model="Plex Web", ) - # plexweb_entity = entity_registry.async_get_or_create(MP_DOMAIN, DOMAIN, "unique_id_123:plexweb_id", suggested_object_id="plex_plex_web_chrome", device_id=plexweb_device.id) + entity_registry.async_get_or_create( - MP_DOMAIN, + Platform.MEDIA_PLAYER, DOMAIN, "unique_id_123:plexweb_id", suggested_object_id="plex_plex_web_chrome", @@ -68,7 +68,7 @@ async def test_migrate_transient_devices( model="Plex for Android (TV)", ) entity_registry.async_get_or_create( - MP_DOMAIN, + Platform.MEDIA_PLAYER, DOMAIN, "unique_id_123:1234567890123456-com-plexapp-android", suggested_object_id="plex_plex_for_android_tv_shield_android_tv", diff --git a/tests/components/plex/test_media_search.py b/tests/components/plex/test_media_search.py index 467ab3555f5..adfdff2d1dc 100644 --- a/tests/components/plex/test_media_search.py +++ b/tests/components/plex/test_media_search.py @@ -4,7 +4,6 @@ from unittest.mock import patch from plexapi.exceptions import BadRequest, NotFound import pytest -from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, @@ -16,7 +15,7 @@ from homeassistant.components.media_player.const import ( SERVICE_PLAY_MEDIA, ) from homeassistant.components.plex.const import DOMAIN -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.exceptions import HomeAssistantError @@ -30,7 +29,7 @@ async def test_media_lookups( requests_mock.get("/player/playback/playMedia", status_code=200) assert await hass.services.async_call( - MP_DOMAIN, + Platform.MEDIA_PLAYER, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, @@ -42,7 +41,7 @@ async def test_media_lookups( with pytest.raises(HomeAssistantError) as excinfo: with patch("plexapi.server.PlexServer.fetchItem", side_effect=NotFound): assert await hass.services.async_call( - MP_DOMAIN, + Platform.MEDIA_PLAYER, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, @@ -57,7 +56,7 @@ async def test_media_lookups( with pytest.raises(HomeAssistantError) as excinfo: payload = '{"library_name": "Not a Library", "show_name": "TV Show"}' assert await hass.services.async_call( - MP_DOMAIN, + Platform.MEDIA_PLAYER, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, @@ -70,7 +69,7 @@ async def test_media_lookups( with patch("plexapi.library.LibrarySection.search") as search: assert await hass.services.async_call( - MP_DOMAIN, + Platform.MEDIA_PLAYER, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, @@ -82,7 +81,7 @@ async def test_media_lookups( search.assert_called_with(**{"show.title": "TV Show", "libtype": "show"}) assert await hass.services.async_call( - MP_DOMAIN, + Platform.MEDIA_PLAYER, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, @@ -96,7 +95,7 @@ async def test_media_lookups( ) assert await hass.services.async_call( - MP_DOMAIN, + Platform.MEDIA_PLAYER, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, @@ -110,7 +109,7 @@ async def test_media_lookups( ) assert await hass.services.async_call( - MP_DOMAIN, + Platform.MEDIA_PLAYER, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, @@ -129,7 +128,7 @@ async def test_media_lookups( ) assert await hass.services.async_call( - MP_DOMAIN, + Platform.MEDIA_PLAYER, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, @@ -141,7 +140,7 @@ async def test_media_lookups( search.assert_called_with(**{"artist.title": "Artist", "libtype": "artist"}) assert await hass.services.async_call( - MP_DOMAIN, + Platform.MEDIA_PLAYER, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, @@ -153,7 +152,7 @@ async def test_media_lookups( search.assert_called_with(**{"album.title": "Album", "libtype": "album"}) assert await hass.services.async_call( - MP_DOMAIN, + Platform.MEDIA_PLAYER, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, @@ -167,7 +166,7 @@ async def test_media_lookups( ) assert await hass.services.async_call( - MP_DOMAIN, + Platform.MEDIA_PLAYER, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, @@ -181,7 +180,7 @@ async def test_media_lookups( ) assert await hass.services.async_call( - MP_DOMAIN, + Platform.MEDIA_PLAYER, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, @@ -200,7 +199,7 @@ async def test_media_lookups( ) assert await hass.services.async_call( - MP_DOMAIN, + Platform.MEDIA_PLAYER, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, @@ -220,7 +219,7 @@ async def test_media_lookups( # Movie searches assert await hass.services.async_call( - MP_DOMAIN, + Platform.MEDIA_PLAYER, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, @@ -232,7 +231,7 @@ async def test_media_lookups( search.assert_called_with(**{"movie.title": "Movie 1", "libtype": None}) assert await hass.services.async_call( - MP_DOMAIN, + Platform.MEDIA_PLAYER, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, @@ -248,7 +247,7 @@ async def test_media_lookups( payload = '{"library_name": "Movies", "title": "Not a Movie"}' with patch("plexapi.library.LibrarySection.search", side_effect=BadRequest): assert await hass.services.async_call( - MP_DOMAIN, + Platform.MEDIA_PLAYER, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, @@ -262,7 +261,7 @@ async def test_media_lookups( # Playlist searches assert await hass.services.async_call( - MP_DOMAIN, + Platform.MEDIA_PLAYER, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, @@ -275,7 +274,7 @@ async def test_media_lookups( with pytest.raises(HomeAssistantError) as excinfo: payload = '{"playlist_name": "Not a Playlist"}' assert await hass.services.async_call( - MP_DOMAIN, + Platform.MEDIA_PLAYER, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, @@ -290,7 +289,7 @@ async def test_media_lookups( with pytest.raises(HomeAssistantError) as excinfo: payload = "{}" assert await hass.services.async_call( - MP_DOMAIN, + Platform.MEDIA_PLAYER, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, diff --git a/tests/components/plex/test_server.py b/tests/components/plex/test_server.py index 724b34cf729..68032868b1a 100644 --- a/tests/components/plex/test_server.py +++ b/tests/components/plex/test_server.py @@ -4,7 +4,6 @@ from unittest.mock import patch from requests.exceptions import ConnectionError, RequestException -from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.plex.const import ( CONF_IGNORE_NEW_SHARED_USERS, CONF_IGNORE_PLEX_WEB_CLIENTS, @@ -13,6 +12,7 @@ from homeassistant.components.plex.const import ( DOMAIN, SERVERS, ) +from homeassistant.const import Platform from .const import DEFAULT_DATA, DEFAULT_OPTIONS from .helpers import trigger_plex_update, wait_for_debouncer @@ -22,7 +22,7 @@ async def test_new_users_available(hass, entry, setup_plex_server): """Test setting up when new users available on Plex server.""" MONITORED_USERS = {"User 1": {"enabled": True}} OPTIONS_WITH_USERS = copy.deepcopy(DEFAULT_OPTIONS) - OPTIONS_WITH_USERS[MP_DOMAIN][CONF_MONITORED_USERS] = MONITORED_USERS + OPTIONS_WITH_USERS[Platform.MEDIA_PLAYER][CONF_MONITORED_USERS] = MONITORED_USERS entry.options = OPTIONS_WITH_USERS mock_plex_server = await setup_plex_server(config_entry=entry) @@ -48,8 +48,8 @@ async def test_new_ignored_users_available( """Test setting up when new users available on Plex server but are ignored.""" MONITORED_USERS = {"User 1": {"enabled": True}} OPTIONS_WITH_USERS = copy.deepcopy(DEFAULT_OPTIONS) - OPTIONS_WITH_USERS[MP_DOMAIN][CONF_MONITORED_USERS] = MONITORED_USERS - OPTIONS_WITH_USERS[MP_DOMAIN][CONF_IGNORE_NEW_SHARED_USERS] = True + OPTIONS_WITH_USERS[Platform.MEDIA_PLAYER][CONF_MONITORED_USERS] = MONITORED_USERS + OPTIONS_WITH_USERS[Platform.MEDIA_PLAYER][CONF_IGNORE_NEW_SHARED_USERS] = True entry.options = OPTIONS_WITH_USERS mock_plex_server = await setup_plex_server(config_entry=entry) @@ -151,7 +151,7 @@ async def test_mark_sessions_idle( async def test_ignore_plex_web_client(hass, entry, setup_plex_server): """Test option to ignore Plex Web clients.""" OPTIONS = copy.deepcopy(DEFAULT_OPTIONS) - OPTIONS[MP_DOMAIN][CONF_IGNORE_PLEX_WEB_CLIENTS] = True + OPTIONS[Platform.MEDIA_PLAYER][CONF_IGNORE_PLEX_WEB_CLIENTS] = True entry.options = OPTIONS mock_plex_server = await setup_plex_server( From 2f8e65a9b02b082f491a9de559b92bc6d340f085 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 22 Dec 2021 14:27:56 +0100 Subject: [PATCH 0945/2644] Store deleted duplicated statistics in .storage (#62574) --- homeassistant/components/recorder/statistics.py | 5 +++-- tests/components/recorder/test_statistics.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 54ef5f2be67..319431171f0 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -30,6 +30,7 @@ from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry from homeassistant.helpers.json import JSONEncoder +from homeassistant.helpers.storage import STORAGE_DIR import homeassistant.util.dt as dt_util import homeassistant.util.pressure as pressure_util import homeassistant.util.temperature as temperature_util @@ -368,8 +369,8 @@ def delete_duplicates(instance: Recorder, session: scoped_session) -> None: if non_identical_duplicates: isotime = dt_util.utcnow().isoformat() - backup_file_name = f".deleted_statistics/deleted_statistics.{isotime}.json" - backup_path = instance.hass.config.path(backup_file_name) + backup_file_name = f"deleted_statistics.{isotime}.json" + backup_path = instance.hass.config.path(STORAGE_DIR, backup_file_name) os.makedirs(os.path.dirname(backup_path), exist_ok=True) with open(backup_path, "w", encoding="utf8") as backup_file: diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 42581cf8adf..296409d984f 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -993,7 +993,7 @@ def test_delete_duplicates_non_identical(caplog, tmpdir): assert "Found duplicated" not in caplog.text isotime = dt_util.utcnow().isoformat() - backup_file_name = f".deleted_statistics/deleted_statistics.{isotime}.json" + backup_file_name = f".storage/deleted_statistics.{isotime}.json" with open(hass.config.path(backup_file_name)) as backup_file: backup = json.load(backup_file) From eb10ff47df8c8d2561efef3b76981f40a52216ec Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Wed, 22 Dec 2021 13:28:22 +0000 Subject: [PATCH 0946/2644] Use new enums in p1_monitor tests (#62548) --- tests/components/p1_monitor/test_sensor.py | 32 ++++++++++------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/tests/components/p1_monitor/test_sensor.py b/tests/components/p1_monitor/test_sensor.py index 9cb01109c66..faaafca5ab8 100644 --- a/tests/components/p1_monitor/test_sensor.py +++ b/tests/components/p1_monitor/test_sensor.py @@ -4,8 +4,8 @@ import pytest from homeassistant.components.p1_monitor.const import DOMAIN from homeassistant.components.sensor import ( ATTR_STATE_CLASS, - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, + SensorStateClass, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -13,10 +13,6 @@ from homeassistant.const import ( ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, CURRENCY_EURO, - DEVICE_CLASS_CURRENT, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_VOLTAGE, ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, @@ -44,9 +40,9 @@ async def test_smartmeter( assert entry.unique_id == f"{entry_id}_smartmeter_power_consumption" assert state.state == "877" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Power Consumption" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER assert ATTR_ICON not in state.attributes state = hass.states.get("sensor.monitor_energy_consumption_high") @@ -58,9 +54,9 @@ async def test_smartmeter( assert ( state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy Consumption - High Tariff" ) - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL_INCREASING assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert ATTR_ICON not in state.attributes state = hass.states.get("sensor.monitor_energy_tariff_period") @@ -101,9 +97,9 @@ async def test_phases( assert entry.unique_id == f"{entry_id}_phases_voltage_phase_l1" assert state.state == "233.6" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Voltage Phase L1" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ELECTRIC_POTENTIAL_VOLT - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_VOLTAGE + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.VOLTAGE assert ATTR_ICON not in state.attributes state = hass.states.get("sensor.monitor_current_phase_l1") @@ -113,9 +109,9 @@ async def test_phases( assert entry.unique_id == f"{entry_id}_phases_current_phase_l1" assert state.state == "1.6" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Current Phase L1" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ELECTRIC_CURRENT_AMPERE - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_CURRENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.CURRENT assert ATTR_ICON not in state.attributes state = hass.states.get("sensor.monitor_power_consumed_phase_l1") @@ -125,9 +121,9 @@ async def test_phases( assert entry.unique_id == f"{entry_id}_phases_power_consumed_phase_l1" assert state.state == "315" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Power Consumed Phase L1" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER assert ATTR_ICON not in state.attributes assert entry.device_id @@ -157,7 +153,7 @@ async def test_settings( assert entry.unique_id == f"{entry_id}_settings_energy_consumption_price_low" assert state.state == "0.20522" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy Consumption Price - Low" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}" @@ -170,7 +166,7 @@ async def test_settings( assert entry.unique_id == f"{entry_id}_settings_energy_production_price_low" assert state.state == "0.20522" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy Production Price - Low" - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}" From d7de3fbfce11619189ab4ded0653f461f41f4a2f Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Wed, 22 Dec 2021 13:29:55 +0000 Subject: [PATCH 0947/2644] Use new enums in picnic tests (#62549) --- tests/components/picnic/test_sensor.py | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/tests/components/picnic/test_sensor.py b/tests/components/picnic/test_sensor.py index b0d12f1f080..4773206f5cf 100644 --- a/tests/components/picnic/test_sensor.py +++ b/tests/components/picnic/test_sensor.py @@ -10,12 +10,8 @@ import requests from homeassistant import config_entries from homeassistant.components.picnic import const from homeassistant.components.picnic.const import CONF_COUNTRY_CODE, SENSOR_TYPES -from homeassistant.const import ( - CONF_ACCESS_TOKEN, - CURRENCY_EURO, - DEVICE_CLASS_TIMESTAMP, - STATE_UNAVAILABLE, -) +from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.const import CONF_ACCESS_TOKEN, CURRENCY_EURO, STATE_UNAVAILABLE from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.util import dt @@ -212,44 +208,44 @@ class TestPicnicSensor(unittest.IsolatedAsyncioTestCase): self._assert_sensor( "sensor.picnic_selected_slot_start", "2021-03-03T13:45:00+00:00", - cls=DEVICE_CLASS_TIMESTAMP, + cls=SensorDeviceClass.TIMESTAMP, ) self._assert_sensor( "sensor.picnic_selected_slot_end", "2021-03-03T14:45:00+00:00", - cls=DEVICE_CLASS_TIMESTAMP, + cls=SensorDeviceClass.TIMESTAMP, ) self._assert_sensor( "sensor.picnic_selected_slot_max_order_time", "2021-03-02T21:00:00+00:00", - cls=DEVICE_CLASS_TIMESTAMP, + cls=SensorDeviceClass.TIMESTAMP, ) self._assert_sensor("sensor.picnic_selected_slot_min_order_value", "35.0") self._assert_sensor( "sensor.picnic_last_order_slot_start", "2021-02-26T19:15:00+00:00", - cls=DEVICE_CLASS_TIMESTAMP, + cls=SensorDeviceClass.TIMESTAMP, ) self._assert_sensor( "sensor.picnic_last_order_slot_end", "2021-02-26T20:15:00+00:00", - cls=DEVICE_CLASS_TIMESTAMP, + cls=SensorDeviceClass.TIMESTAMP, ) self._assert_sensor("sensor.picnic_last_order_status", "COMPLETED") self._assert_sensor( "sensor.picnic_last_order_eta_start", "2021-02-26T19:54:00+00:00", - cls=DEVICE_CLASS_TIMESTAMP, + cls=SensorDeviceClass.TIMESTAMP, ) self._assert_sensor( "sensor.picnic_last_order_eta_end", "2021-02-26T20:14:00+00:00", - cls=DEVICE_CLASS_TIMESTAMP, + cls=SensorDeviceClass.TIMESTAMP, ) self._assert_sensor( "sensor.picnic_last_order_delivery_time", "2021-02-26T19:54:05+00:00", - cls=DEVICE_CLASS_TIMESTAMP, + cls=SensorDeviceClass.TIMESTAMP, ) self._assert_sensor( "sensor.picnic_last_order_total_price", "41.33", unit=CURRENCY_EURO From 38e95ca663b4ec33e461575560110a7ed37fd08b Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Wed, 22 Dec 2021 13:31:20 +0000 Subject: [PATCH 0948/2644] Use new enums for prometheus tests (#62550) --- tests/components/prometheus/test_init.py | 35 ++++++++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index b625642d12e..7ad945d2d22 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -13,12 +13,11 @@ from homeassistant.components.demo.light import DemoLight from homeassistant.components.demo.number import DemoNumber from homeassistant.components.demo.sensor import DemoSensor import homeassistant.components.prometheus as prometheus +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONTENT_TYPE_TEXT_PLAIN, DEGREE, - DEVICE_CLASS_POWER, - DEVICE_CLASS_TEMPERATURE, ENERGY_KILO_WATT_HOUR, EVENT_STATE_CHANGED, TEMP_FAHRENHEIT, @@ -75,7 +74,13 @@ async def test_view_empty_namespace(hass, hass_client): client = await setup_prometheus_client(hass, hass_client, "") sensor2 = DemoSensor( - None, "Radio Energy", 14, DEVICE_CLASS_POWER, None, ENERGY_KILO_WATT_HOUR, None + None, + "Radio Energy", + 14, + SensorDeviceClass.POWER, + None, + ENERGY_KILO_WATT_HOUR, + None, ) sensor2.hass = hass sensor2.entity_id = "sensor.radio_energy" @@ -149,7 +154,13 @@ async def test_sensor_unit(hass, hass_client): await sensor1.async_update_ha_state() sensor2 = DemoSensor( - None, "Radio Energy", 14, DEVICE_CLASS_POWER, None, ENERGY_KILO_WATT_HOUR, None + None, + "Radio Energy", + 14, + SensorDeviceClass.POWER, + None, + ENERGY_KILO_WATT_HOUR, + None, ) sensor2.hass = hass sensor2.entity_id = "sensor.radio_energy" @@ -272,14 +283,26 @@ async def test_sensor_device_class(hass, hass_client): await hass.async_block_till_done() sensor1 = DemoSensor( - None, "Fahrenheit", 50, DEVICE_CLASS_TEMPERATURE, None, TEMP_FAHRENHEIT, None + None, + "Fahrenheit", + 50, + SensorDeviceClass.TEMPERATURE, + None, + TEMP_FAHRENHEIT, + None, ) sensor1.hass = hass sensor1.entity_id = "sensor.fahrenheit" await sensor1.async_update_ha_state() sensor2 = DemoSensor( - None, "Radio Energy", 14, DEVICE_CLASS_POWER, None, ENERGY_KILO_WATT_HOUR, None + None, + "Radio Energy", + 14, + SensorDeviceClass.POWER, + None, + ENERGY_KILO_WATT_HOUR, + None, ) sensor2.hass = hass sensor2.entity_id = "sensor.radio_energy" From c2b44c5fdcdd3ea754ca67e48aa96381db3d8c86 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Dec 2021 06:35:48 -0700 Subject: [PATCH 0949/2644] Bump flux_led to 0.27.12 to fix legacy cct controllers (#62573) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index ab1e78bc1ed..53192b96b89 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["network"], "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.27.10"], + "requirements": ["flux_led==0.27.12"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index ef1b59a40a0..a8696e0c04c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -665,7 +665,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.27.10 +flux_led==0.27.12 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1eb99cfec45..9c4303a3343 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -405,7 +405,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.27.10 +flux_led==0.27.12 # homeassistant.components.homekit fnvhash==0.1.0 From eda9291ca1ec966c7a5855db8bcd816baf01866c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 22 Dec 2021 14:54:36 +0100 Subject: [PATCH 0950/2644] Improve google cast state reporting (#62587) --- homeassistant/components/cast/media_player.py | 7 +++- tests/components/cast/test_media_player.py | 34 +++++++++++++++---- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 8160c1f5bf0..f3cc9c32661 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -76,6 +76,8 @@ from .helpers import CastStatusListener, ChromecastInfo, ChromeCastZeroconf _LOGGER = logging.getLogger(__name__) +APP_IDS_UNRELIABLE_MEDIA_INFO = ("Netflix",) + CAST_SPLASH = "https://www.home-assistant.io/images/cast/splash.png" SUPPORT_CAST = SUPPORT_PLAY_MEDIA | SUPPORT_TURN_OFF @@ -564,7 +566,10 @@ class CastDevice(MediaPlayerEntity): if media_status.player_is_idle: return STATE_IDLE if self.app_id is not None and self.app_id != pychromecast.IDLE_APP_ID: - return STATE_PLAYING + if self.app_id in APP_IDS_UNRELIABLE_MEDIA_INFO: + # Some apps don't report media status, show the player as playing + return STATE_PLAYING + return STATE_IDLE if self._chromecast is not None and self._chromecast.is_idle: return STATE_OFF return None diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 3c5d4705713..9bf1175c1b9 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -1082,7 +1082,12 @@ async def test_entity_control(hass: HomeAssistant): chromecast.media_controller.seek.assert_called_once_with(123) -async def test_entity_media_states(hass: HomeAssistant): +# Some smart TV's with Google TV report "Netflix", not the Netflix app's ID +@pytest.mark.parametrize( + "app_id, state_no_media", + [(pychromecast.APP_YOUTUBE, "idle"), ("Netflix", "playing")], +) +async def test_entity_media_states(hass: HomeAssistant, app_id, state_no_media): """Test various entity media states.""" entity_id = "media_player.speaker" reg = er.async_get(hass) @@ -1090,7 +1095,7 @@ async def test_entity_media_states(hass: HomeAssistant): info = get_fake_chromecast_info() chromecast, _ = await async_setup_media_player_cast(hass, info) - _, conn_status_cb, media_status_cb = get_status_callbacks(chromecast) + cast_status_cb, conn_status_cb, media_status_cb = get_status_callbacks(chromecast) connection_status = MagicMock() connection_status.status = "CONNECTED" @@ -1103,6 +1108,15 @@ async def test_entity_media_states(hass: HomeAssistant): assert state.state == "off" assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid)) + # App id updated, but no media status + chromecast.app_id = app_id + cast_status = MagicMock() + cast_status_cb(cast_status) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == state_no_media + + # Got media status media_status = MagicMock(images=None) media_status.player_is_playing = True media_status_cb(media_status) @@ -1124,15 +1138,23 @@ async def test_entity_media_states(hass: HomeAssistant): state = hass.states.get(entity_id) assert state.state == "idle" - media_status.player_is_idle = False - chromecast.is_idle = True - media_status_cb(media_status) + # No media status, app is still running + media_status_cb(None) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == state_no_media + + # App no longer running + chromecast.app_id = pychromecast.IDLE_APP_ID + cast_status = MagicMock() + cast_status_cb(cast_status) await hass.async_block_till_done() state = hass.states.get(entity_id) assert state.state == "off" + # No cast status chromecast.is_idle = False - media_status_cb(media_status) + cast_status_cb(None) await hass.async_block_till_done() state = hass.states.get(entity_id) assert state.state == "unknown" From 6e13605cada2dea7aa55d57f55ee50546a23727f Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Thu, 23 Dec 2021 00:24:53 +0800 Subject: [PATCH 0951/2644] Add get_image method to Stream (#61918) * Add get_image method to Stream * Add KeyFrameConverter class --- homeassistant/components/stream/__init__.py | 15 +++- homeassistant/components/stream/core.py | 85 +++++++++++++++++++ homeassistant/components/stream/manifest.json | 2 +- homeassistant/components/stream/worker.py | 9 +- requirements_all.txt | 1 + requirements_test_all.txt | 1 + tests/components/camera/common.py | 1 + tests/components/stream/test_worker.py | 45 +++++++++- 8 files changed, 153 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 0556bc2c7a9..fec9731136f 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -50,7 +50,7 @@ from .const import ( STREAM_RESTART_RESET_TIME, TARGET_SEGMENT_DURATION_NON_LL_HLS, ) -from .core import PROVIDERS, IdleTimer, StreamOutput, StreamSettings +from .core import PROVIDERS, IdleTimer, KeyFrameConverter, StreamOutput, StreamSettings from .hls import HlsStreamOutput, async_setup_hls _LOGGER = logging.getLogger(__name__) @@ -137,6 +137,8 @@ def filter_libav_logging() -> None: # Set log level to error for libav.mp4 logging.getLogger("libav.mp4").setLevel(logging.ERROR) + # Suppress "deprecated pixel format" WARNING + logging.getLogger("libav.swscaler").setLevel(logging.ERROR) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -214,6 +216,7 @@ class Stream: self._thread_quit = threading.Event() self._outputs: dict[str, StreamOutput] = {} self._fast_restart_once = False + self._keyframe_converter = KeyFrameConverter(hass) self._available: bool = True self._update_callback: Callable[[], None] | None = None self._logger = ( @@ -327,6 +330,7 @@ class Stream: self.source, self.options, stream_state, + self._keyframe_converter, self._thread_quit, ) except StreamWorkerError as err: @@ -419,3 +423,12 @@ class Stream: # Wait for latest segment, then add the lookback await hls.recv() recorder.prepend(list(hls.get_segments())[-num_segments:]) + + async def get_image( + self, + width: int | None = None, + height: int | None = None, + ) -> bytes | None: + """Wrap get_image from KeyFrameConverter.""" + + return await self._keyframe_converter.get_image(width=width, height=height) diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index b51c953e915..08397fb6876 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -19,6 +19,8 @@ from homeassistant.util.decorator import Registry from .const import ATTR_STREAMS, DOMAIN if TYPE_CHECKING: + from av import CodecContext, Packet + from . import Stream PROVIDERS = Registry() @@ -356,3 +358,86 @@ class StreamView(HomeAssistantView): ) -> web.StreamResponse: """Handle the stream request.""" raise NotImplementedError() + + +class KeyFrameConverter: + """ + Generate and hold the keyframe as a jpeg. + + An overview of the thread and state interaction: + the worker thread sets a packet + at any time, main loop can run a get_image call + _generate_image will try to create an image from the packet + Running _generate_image will clear the packet, so there will only + be one attempt per packet + If successful, _image will be updated and returned by get_image + If unsuccessful, get_image will return the previous image + """ + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize.""" + + # Keep import here so that we can import stream integration without installing reqs + # pylint: disable=import-outside-toplevel + from homeassistant.components.camera.img_util import TurboJPEGSingleton + + self.packet: Packet = None + self._hass = hass + self._image: bytes | None = None + self._turbojpeg = TurboJPEGSingleton.instance() + self._lock = asyncio.Lock() + self._codec_context: CodecContext | None = None + + def create_codec_context(self, codec_context: CodecContext) -> None: + """ + Create a codec context to be used for decoding the keyframes. + + This is run by the worker thread and will only be called once per worker. + """ + + # Keep import here so that we can import stream integration without installing reqs + # pylint: disable=import-outside-toplevel + from av import CodecContext + + self._codec_context = CodecContext.create(codec_context.name, "r") + self._codec_context.extradata = codec_context.extradata + self._codec_context.skip_frame = "NONKEY" + self._codec_context.thread_type = "NONE" + + def _generate_image(self, width: int | None, height: int | None) -> None: + """ + Generate the keyframe image. + + This is run in an executor thread, but since it is called within an + the asyncio lock from the main thread, there will only be one entry + at a time per instance. + """ + + if not (self._turbojpeg and self.packet and self._codec_context): + return + packet = self.packet + self.packet = None + # decode packet (flush afterwards) + frames = self._codec_context.decode(packet) + for _i in range(2): + if frames: + break + frames = self._codec_context.decode(None) + if frames: + frame = frames[0] + if width and height: + frame = frame.reformat(width=width, height=height) + bgr_array = frame.to_ndarray(format="bgr24") + self._image = bytes(self._turbojpeg.encode(bgr_array)) + + async def get_image( + self, + width: int | None = None, + height: int | None = None, + ) -> bytes | None: + """Fetch an image from the Stream and return it as a jpeg in bytes.""" + + # Use a lock to ensure only one thread is working on the keyframe at a time + async with self._lock: + await self._hass.async_add_executor_job(self._generate_image, width, height) + return self._image diff --git a/homeassistant/components/stream/manifest.json b/homeassistant/components/stream/manifest.json index d5115754e2e..60d4a6e66eb 100644 --- a/homeassistant/components/stream/manifest.json +++ b/homeassistant/components/stream/manifest.json @@ -2,7 +2,7 @@ "domain": "stream", "name": "Stream", "documentation": "https://www.home-assistant.io/integrations/stream", - "requirements": ["ha-av==8.0.4-rc.1"], + "requirements": ["ha-av==8.0.4-rc.1", "PyTurboJPEG==1.6.3"], "dependencies": ["http"], "codeowners": ["@hunterjm", "@uvjustin", "@allenporter"], "quality_scale": "internal", diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index b1d79e52800..e633d146444 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -14,7 +14,7 @@ import av from homeassistant.core import HomeAssistant -from . import redact_credentials +from . import KeyFrameConverter, redact_credentials from .const import ( ATTR_SETTINGS, AUDIO_CODECS, @@ -439,6 +439,7 @@ def stream_worker( source: str, options: dict[str, str], stream_state: StreamState, + keyframe_converter: KeyFrameConverter, quit_event: Event, ) -> None: """Handle consuming streams.""" @@ -453,6 +454,7 @@ def stream_worker( video_stream = container.streams.video[0] except (KeyError, IndexError) as ex: raise StreamWorkerError("Stream has no video") from ex + keyframe_converter.create_codec_context(codec_context=video_stream.codec_context) try: audio_stream = container.streams.audio[0] except (KeyError, IndexError): @@ -474,7 +476,7 @@ def stream_worker( def is_video(packet: av.Packet) -> Any: """Return true if the packet is for the video stream.""" - return packet.stream == video_stream + return packet.stream.type == "video" # Have to work around two problems with RTSP feeds in ffmpeg # 1 - first frame has bad pts/dts https://trac.ffmpeg.org/ticket/5018 @@ -535,3 +537,6 @@ def stream_worker( raise StreamWorkerError("Error demuxing stream: %s" % str(ex)) from ex muxer.mux_packet(packet) + + if packet.is_keyframe and is_video(packet): + keyframe_converter.packet = packet diff --git a/requirements_all.txt b/requirements_all.txt index a8696e0c04c..8c4e685de62 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -55,6 +55,7 @@ PySocks==1.7.1 PyTransportNSW==0.1.1 # homeassistant.components.camera +# homeassistant.components.stream PyTurboJPEG==1.6.3 # homeassistant.components.vicare diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9c4303a3343..3a03271aa22 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,6 +33,7 @@ PyRMVtransport==0.3.3 PyTransportNSW==0.1.1 # homeassistant.components.camera +# homeassistant.components.stream PyTurboJPEG==1.6.3 # homeassistant.components.vicare diff --git a/tests/components/camera/common.py b/tests/components/camera/common.py index 756a553f3c7..bd3841cc4e8 100644 --- a/tests/components/camera/common.py +++ b/tests/components/camera/common.py @@ -29,4 +29,5 @@ def mock_turbo_jpeg( (second_width, second_height, 0, 0), ] mocked_turbo_jpeg.scale_with_quality.return_value = EMPTY_8_6_JPEG + mocked_turbo_jpeg.encode.return_value = EMPTY_8_6_JPEG return mocked_turbo_jpeg diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index f05b2ece829..eb50e76a80a 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -23,7 +23,7 @@ from unittest.mock import patch import av import pytest -from homeassistant.components.stream import Stream, create_stream +from homeassistant.components.stream import KeyFrameConverter, Stream, create_stream from homeassistant.components.stream.const import ( ATTR_SETTINGS, CONF_LL_HLS, @@ -45,6 +45,7 @@ from homeassistant.components.stream.worker import ( ) from homeassistant.setup import async_setup_component +from tests.components.camera.common import EMPTY_8_6_JPEG, mock_turbo_jpeg from tests.components.stream.common import generate_h264_video, generate_h265_video from tests.components.stream.test_ll_hls import TEST_PART_DURATION @@ -97,6 +98,17 @@ class FakeAvInputStream: self.codec = FakeCodec() + class FakeCodecContext: + name = "h264" + extradata = None + + self.codec_context = FakeCodecContext() + + @property + def type(self): + """Return packet type.""" + return "video" if self.name == VIDEO_STREAM_FORMAT else "audio" + def __str__(self) -> str: """Return a stream name for debugging.""" return f"FakePyAvStream<{self.name}, {self.time_base}>" @@ -195,6 +207,7 @@ class FakePyAvBuffer: class FakeAvOutputStream: def __init__(self, capture_packets): self.capture_packets = capture_packets + self.type = "ignored-type" def close(self): return @@ -258,7 +271,9 @@ class MockPyAv: def run_worker(hass, stream, stream_source): """Run the stream worker under test.""" stream_state = StreamState(hass, stream.outputs) - stream_worker(stream_source, {}, stream_state, threading.Event()) + stream_worker( + stream_source, {}, stream_state, KeyFrameConverter(hass), threading.Event() + ) async def async_decode_stream(hass, packets, py_av=None): @@ -854,3 +869,29 @@ async def test_h265_video_is_hvc1(hass, record_worker_sync): await record_worker_sync.join() stream.stop() + + +async def test_get_image(hass, record_worker_sync): + """Test that the has_keyframe metadata matches the media.""" + await async_setup_component(hass, "stream", {"stream": {}}) + + source = generate_h264_video() + + # Since libjpeg-turbo is not installed on the CI runner, we use a mock + with patch( + "homeassistant.components.camera.img_util.TurboJPEGSingleton" + ) as mock_turbo_jpeg_singleton: + mock_turbo_jpeg_singleton.instance.return_value = mock_turbo_jpeg() + stream = create_stream(hass, source, {}) + + # use record_worker_sync to grab output segments + with patch.object(hass.config, "is_allowed_path", return_value=True): + await stream.async_record("/example/path") + + assert stream._keyframe_converter._image is None + + await record_worker_sync.join() + + assert await stream.get_image() == EMPTY_8_6_JPEG + + stream.stop() From 06eec7adfcefeba7267abf83d9145116c8b814c2 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Wed, 22 Dec 2021 18:49:58 +0000 Subject: [PATCH 0952/2644] Allow adding new devices to an Aqara hub via homekit_controller (#62600) --- .../components/homekit_controller/const.py | 3 + .../components/homekit_controller/number.py | 7 +- .../components/homekit_controller/switch.py | 90 ++- .../homekit_controller/fixtures/aqara_e1.json | 646 ++++++++++++++++++ .../specific_devices/test_aqara_gateway.py | 72 +- .../specific_devices/test_eve_degree.py | 2 +- .../test_vocolinc_flowerbud.py | 3 +- .../homekit_controller/test_switch.py | 56 ++ 8 files changed, 869 insertions(+), 10 deletions(-) create mode 100644 tests/components/homekit_controller/fixtures/aqara_e1.json diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index 2de7156482e..7fab8222ae2 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -48,6 +48,9 @@ HOMEKIT_ACCESSORY_DISPATCH = { CHARACTERISTIC_PLATFORMS = { CharacteristicsTypes.Vendor.AQARA_GATEWAY_VOLUME: "number", + CharacteristicsTypes.Vendor.AQARA_E1_GATEWAY_VOLUME: "number", + CharacteristicsTypes.Vendor.AQARA_PAIRING_MODE: "switch", + CharacteristicsTypes.Vendor.AQARA_E1_PAIRING_MODE: "switch", CharacteristicsTypes.Vendor.EVE_ENERGY_WATT: "sensor", CharacteristicsTypes.Vendor.EVE_DEGREE_AIR_PRESSURE: "sensor", CharacteristicsTypes.Vendor.EVE_DEGREE_ELEVATION: "number", diff --git a/homeassistant/components/homekit_controller/number.py b/homeassistant/components/homekit_controller/number.py index f646072425b..26ec7e6b9e0 100644 --- a/homeassistant/components/homekit_controller/number.py +++ b/homeassistant/components/homekit_controller/number.py @@ -75,10 +75,9 @@ class HomeKitNumber(CharacteristicEntity, NumberEntity): @property def name(self) -> str: """Return the name of the device if any.""" - prefix = "" - if name := super().name: - prefix = f"{name} -" - return f"{prefix} {self.entity_description.name}" + if prefix := super().name: + return f"{prefix} {self.entity_description.name}" + return self.entity_description.name def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" diff --git a/homeassistant/components/homekit_controller/switch.py b/homeassistant/components/homekit_controller/switch.py index 4ae9ed5a5f0..1f128bd4d26 100644 --- a/homeassistant/components/homekit_controller/switch.py +++ b/homeassistant/components/homekit_controller/switch.py @@ -1,15 +1,21 @@ """Support for Homekit switches.""" +from __future__ import annotations + +from dataclasses import dataclass + from aiohomekit.model.characteristics import ( + Characteristic, CharacteristicsTypes, InUseValues, IsConfiguredValues, ) from aiohomekit.model.services import ServicesTypes -from homeassistant.components.switch import SwitchEntity +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.core import callback +from homeassistant.helpers.entity import EntityCategory -from . import KNOWN_DEVICES, HomeKitEntity +from . import KNOWN_DEVICES, CharacteristicEntity, HomeKitEntity OUTLET_IN_USE = "outlet_in_use" @@ -18,6 +24,30 @@ ATTR_IS_CONFIGURED = "is_configured" ATTR_REMAINING_DURATION = "remaining_duration" +@dataclass +class DeclarativeSwitchEntityDescription(SwitchEntityDescription): + """Describes Homekit button.""" + + true_value: bool = True + false_value: bool = False + + +SWITCH_ENTITIES: dict[str, DeclarativeSwitchEntityDescription] = { + CharacteristicsTypes.Vendor.AQARA_PAIRING_MODE: DeclarativeSwitchEntityDescription( + key=CharacteristicsTypes.Vendor.AQARA_PAIRING_MODE, + name="Pairing Mode", + icon="mdi:lock-open", + entity_category=EntityCategory.CONFIG, + ), + CharacteristicsTypes.Vendor.AQARA_E1_PAIRING_MODE: DeclarativeSwitchEntityDescription( + key=CharacteristicsTypes.Vendor.AQARA_E1_PAIRING_MODE, + name="Pairing Mode", + icon="mdi:lock-open", + entity_category=EntityCategory.CONFIG, + ), +} + + class HomeKitSwitch(HomeKitEntity, SwitchEntity): """Representation of a Homekit switch.""" @@ -96,6 +126,49 @@ class HomeKitValve(HomeKitEntity, SwitchEntity): return attrs +class DeclarativeCharacteristicSwitch(CharacteristicEntity, SwitchEntity): + """Representation of a Homekit switch backed by a single characteristic.""" + + def __init__( + self, + conn, + info, + char, + description: DeclarativeSwitchEntityDescription, + ): + """Initialise a HomeKit switch.""" + self.entity_description = description + super().__init__(conn, info, char) + + @property + def name(self) -> str: + """Return the name of the device if any.""" + if prefix := super().name: + return f"{prefix} {self.entity_description.name}" + return self.entity_description.name + + def get_characteristic_types(self): + """Define the homekit characteristics the entity cares about.""" + return [self._char.type] + + @property + def is_on(self): + """Return true if device is on.""" + return self._char.value == self.entity_description.true_value + + async def async_turn_on(self, **kwargs): + """Turn the specified switch on.""" + await self.async_put_characteristics( + {self._char.type: self.entity_description.true_value} + ) + + async def async_turn_off(self, **kwargs): + """Turn the specified switch off.""" + await self.async_put_characteristics( + {self._char.type: self.entity_description.false_value} + ) + + ENTITY_TYPES = { ServicesTypes.SWITCH: HomeKitSwitch, ServicesTypes.OUTLET: HomeKitSwitch, @@ -117,3 +190,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities): return True conn.add_listener(async_add_service) + + @callback + def async_add_characteristic(char: Characteristic): + if not (description := SWITCH_ENTITIES.get(char.type)): + return False + + info = {"aid": char.service.accessory.aid, "iid": char.service.iid} + async_add_entities( + [DeclarativeCharacteristicSwitch(conn, info, char, description)], True + ) + return True + + conn.add_char_factory(async_add_characteristic) diff --git a/tests/components/homekit_controller/fixtures/aqara_e1.json b/tests/components/homekit_controller/fixtures/aqara_e1.json new file mode 100644 index 00000000000..8c8ff326bd6 --- /dev/null +++ b/tests/components/homekit_controller/fixtures/aqara_e1.json @@ -0,0 +1,646 @@ +[ + { + "aid": 1, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "characteristics": [ + { + "iid": 65537, + "type": "00000014-0000-1000-8000-0026BB765291", + "format": "bool", + "perms": [ + "pw" + ] + }, + { + "iid": 65538, + "type": "00000020-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Aqara", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 65539, + "type": "00000021-0000-1000-8000-0026BB765291", + "format": "string", + "value": "HE1-G01", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 65540, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Aqara-Hub-E1-00A0", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 65541, + "type": "00000030-0000-1000-8000-0026BB765291", + "format": "string", + "value": "00aa00000a0", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 65542, + "type": "00000052-0000-1000-8000-0026BB765291", + "format": "string", + "value": "3.3.0", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 65543, + "type": "00000053-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 65544, + "type": "34AB8811-AC7F-4340-BAC3-FD6A85F9943B", + "format": "string", + "value": "5.0;dfeceb3a", + "perms": [ + "pr", + "hd" + ], + "ev": false + }, + { + "iid": 65545, + "type": "220", + "format": "data", + "value": "xDsGO4QdTEA=", + "perms": [ + "pr" + ], + "ev": false, + "maxDataLen": 8 + } + ] + }, + { + "iid": 2, + "type": "000000A2-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "characteristics": [ + { + "iid": 131074, + "type": "00000037-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.1.0", + "perms": [ + "pr" + ], + "ev": false + } + ] + }, + { + "iid": 4, + "type": "22A", + "primary": false, + "hidden": false, + "characteristics": [ + { + "iid": 262145, + "type": "22B", + "format": "bool", + "value": 1, + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 262146, + "type": "22C", + "format": "uint32", + "value": 9, + "perms": [ + "pr" + ], + "ev": false, + "minValue": 0, + "maxValue": 15, + "minStep": 1 + }, + { + "iid": 262147, + "type": "22D", + "format": "tlv8", + "value": "", + "perms": [ + "pr", + "pw", + "ev", + "tw", + "wr" + ], + "ev": false + } + ] + }, + { + "iid": 16, + "type": "0000007E-0000-1000-8000-0026BB765291", + "primary": true, + "hidden": false, + "characteristics": [ + { + "iid": 1048578, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Security System", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 1048579, + "type": "00000066-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 3, + "perms": [ + "pr", + "ev" + ], + "ev": true, + "minValue": 0, + "maxValue": 4, + "minStep": 1, + "valid-values": [ + 0, + 1, + 2, + 3, + 4 + ] + }, + { + "iid": 1048580, + "type": "00000067-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 3, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": true, + "minValue": 0, + "maxValue": 3, + "minStep": 1, + "valid-values": [ + 0, + 1, + 2, + 3 + ] + }, + { + "iid": 1048581, + "type": "60CDDE6C-42B6-4C72-9719-AB2740EABE2A", + "format": "tlv8", + "value": "AAA=", + "perms": [ + "pr", + "pw" + ], + "ev": false, + "description": "Stay Arm Trigger Devices" + }, + { + "iid": 1048582, + "type": "4AB2460A-41E4-4F05-97C3-CCFDAE1BE324", + "format": "tlv8", + "value": "AAA=", + "perms": [ + "pr", + "pw" + ], + "ev": false, + "description": "Alarm Trigger Devices" + }, + { + "iid": 1048583, + "type": "F8296386-5A30-4AA7-838C-ED0DA9D807DF", + "format": "tlv8", + "value": "AAA=", + "perms": [ + "pr", + "pw" + ], + "ev": false, + "description": "Night Arm Trigger Devices" + } + ] + }, + { + "iid": 17, + "type": "9715BF53-AB63-4449-8DC7-2785D617390A", + "primary": false, + "hidden": true, + "characteristics": [ + { + "iid": 1114114, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Gateway", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 1114115, + "type": "4CB28907-66DF-4D9C-962C-9971ABF30EDC", + "format": "string", + "value": "1970-01-01 21:01:22+8", + "perms": [ + "pr", + "pw", + "hd" + ], + "ev": false, + "description": "Date and Time" + }, + { + "iid": 1114116, + "type": "EE56B186-B0D3-488E-8C79-C21FC9BCF437", + "format": "int", + "value": 40, + "perms": [ + "pr", + "pw", + "ev", + "hd" + ], + "ev": false, + "description": "Gateway Volume", + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 1114117, + "type": "B1C09E4C-E202-4827-B863-B0F32F727CFF", + "format": "bool", + "value": 0, + "perms": [ + "pr", + "pw", + "ev", + "hd" + ], + "ev": false, + "description": "New Accessory Permission" + }, + { + "iid": 1114118, + "type": "2CB22739-1E4C-4798-A761-BC2FAF51AFC3", + "format": "string", + "value": "", + "perms": [ + "pr", + "ev", + "hd" + ], + "ev": false, + "description": "Accessory Joined" + }, + { + "iid": 1114119, + "type": "75D19FA9-218B-4943-997E-341E5D1C60CC", + "format": "string", + "perms": [ + "pw", + "hd" + ], + "description": "Remove Accessory" + }, + { + "iid": 1114120, + "type": "7D943F6A-E052-4E96-A176-D17BF00E32CB", + "format": "int", + "value": -1, + "perms": [ + "pr", + "ev", + "hd" + ], + "ev": false, + "description": "Firmware Update Status", + "minValue": -65535, + "maxValue": 65535, + "minStep": 1 + }, + { + "iid": 1114121, + "type": "A45EFD52-0DB5-4C1A-9727-513FBCD8185F", + "format": "string", + "perms": [ + "pw", + "hd" + ], + "description": "Firmware Update URL", + "maxLen": 256 + }, + { + "iid": 1114122, + "type": "40F0124A-579D-40E4-865E-0EF6740EA64B", + "format": "string", + "perms": [ + "pw", + "hd" + ], + "description": "Firmware Update Checksum" + }, + { + "iid": 1114123, + "type": "E1C20B22-E3A7-4B92-8BA3-C16E778648A7", + "format": "string", + "value": "", + "perms": [ + "pr", + "ev", + "hd" + ], + "ev": false, + "description": "Identify Accessory" + }, + { + "iid": 1114124, + "type": "4CF1436A-755C-4377-BDB8-30BE29EB8620", + "format": "string", + "value": "Chinese", + "perms": [ + "pr", + "pw", + "ev", + "hd" + ], + "ev": false, + "description": "Language" + }, + { + "iid": 1114125, + "type": "25D889CB-7135-4A29-B5B4-C1FFD6D2DD5C", + "format": "string", + "value": "", + "perms": [ + "pr", + "pw", + "hd" + ], + "ev": false, + "description": "Country Domain" + }, + { + "iid": 1114126, + "type": "C7EECAA7-91D9-40EB-AD0C-FFDDE3143CB9", + "format": "string", + "value": "lumi1.00aa00000a0", + "perms": [ + "pr", + "hd" + ], + "ev": false, + "description": "Lumi Did" + }, + { + "iid": 1114127, + "type": "80FA747E-CB45-45A4-B7BE-AA7D9964859E", + "format": "string", + "perms": [ + "pw", + "hd" + ], + "description": "Lumi Bindkey" + }, + { + "iid": 1114128, + "type": "C3B8A329-EF0C-4739-B773-E5B7AEA52C71", + "format": "bool", + "value": 0, + "perms": [ + "pr", + "hd" + ], + "ev": false, + "description": "Lumi Bindstate" + } + ] + } + ] + }, + { + "aid": 33, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "characteristics": [ + { + "iid": 65537, + "type": "00000014-0000-1000-8000-0026BB765291", + "format": "bool", + "perms": [ + "pw" + ] + }, + { + "iid": 65538, + "type": "00000020-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Aqara", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 65539, + "type": "00000021-0000-1000-8000-0026BB765291", + "format": "string", + "value": "AS006", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 65540, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Contact Sensor", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 65541, + "type": "00000030-0000-1000-8000-0026BB765291", + "format": "string", + "value": "158d0007c59c6a", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 65542, + "type": "00000052-0000-1000-8000-0026BB765291", + "format": "string", + "value": "0", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 65543, + "type": "00000053-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0", + "perms": [ + "pr" + ], + "ev": false + } + ] + }, + { + "iid": 4, + "type": "00000080-0000-1000-8000-0026BB765291", + "primary": true, + "hidden": false, + "characteristics": [ + { + "iid": 262146, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Contact Sensor", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 262147, + "type": "0000006A-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": true, + "minValue": 0, + "maxValue": 1, + "minStep": 1, + "valid-values": [ + 0, + 1 + ] + } + ] + }, + { + "iid": 5, + "type": "00000096-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "characteristics": [ + { + "iid": 327682, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "Battery Sensor", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 327683, + "type": "00000068-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 100, + "perms": [ + "pr", + "ev" + ], + "ev": true, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 327685, + "type": "00000079-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": true, + "minValue": 0, + "maxValue": 1, + "minStep": 1, + "valid-values": [ + 0, + 1 + ] + }, + { + "iid": 327684, + "type": "0000008F-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 2, + "perms": [ + "pr", + "ev" + ], + "ev": true, + "minValue": 2, + "maxValue": 2, + "minStep": 1, + "valid-values": [ + 2 + ] + } + ] + } + ] + } +] diff --git a/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py b/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py index 08c9e7b3976..fd9ef96752a 100644 --- a/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py +++ b/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py @@ -45,7 +45,14 @@ async def test_aqara_gateway_setup(hass): ( "number.aqara_hub_1563_volume", "homekit-0000000123456789-aid:1-sid:65536-cid:65541", - "Aqara Hub-1563 - Volume", + "Aqara Hub-1563 Volume", + None, + EntityCategory.CONFIG, + ), + ( + "switch.aqara_hub_1563_pairing_mode", + "homekit-0000000123456789-aid:1-sid:65536-cid:65538", + "Aqara Hub-1563 Pairing Mode", None, EntityCategory.CONFIG, ), @@ -80,3 +87,66 @@ async def test_aqara_gateway_setup(hass): # All entities should be part of same device assert len(device_ids) == 1 + + +async def test_aqara_gateway_e1_setup(hass): + """Test that an Aqara E1 Gateway can be correctly setup in HA.""" + accessories = await setup_accessories_from_file(hass, "aqara_e1.json") + config_entry, pairing = await setup_test_accessories(hass, accessories) + + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + sensors = [ + ( + "alarm_control_panel.aqara_hub_e1_00a0", + "homekit-00aa00000a0-16", + "Aqara-Hub-E1-00A0", + SUPPORT_ALARM_ARM_NIGHT | SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY, + None, + ), + ( + "number.aqara_hub_e1_00a0_volume", + "homekit-00aa00000a0-aid:1-sid:17-cid:1114116", + "Aqara-Hub-E1-00A0 Volume", + None, + EntityCategory.CONFIG, + ), + ( + "switch.aqara_hub_e1_00a0_pairing_mode", + "homekit-00aa00000a0-aid:1-sid:17-cid:1114117", + "Aqara-Hub-E1-00A0 Pairing Mode", + None, + EntityCategory.CONFIG, + ), + ] + + device_ids = set() + + for (entity_id, unique_id, friendly_name, supported_features, category) in sensors: + entry = entity_registry.async_get(entity_id) + assert entry.unique_id == unique_id + assert entry.entity_category == category + + helper = Helper( + hass, + entity_id, + pairing, + accessories[0], + config_entry, + ) + state = await helper.poll_and_get_state() + assert state.attributes["friendly_name"] == friendly_name + assert state.attributes.get("supported_features") == supported_features + + device = device_registry.async_get(entry.device_id) + assert device.manufacturer == "Aqara" + assert device.name == "Aqara-Hub-E1-00A0" + assert device.model == "HE1-G01" + assert device.sw_version == "3.3.0" + assert device.via_device_id is None + + device_ids.add(entry.device_id) + + # All entities should be part of same device + assert len(device_ids) == 1 diff --git a/tests/components/homekit_controller/specific_devices/test_eve_degree.py b/tests/components/homekit_controller/specific_devices/test_eve_degree.py index 043920fadec..312316e8870 100644 --- a/tests/components/homekit_controller/specific_devices/test_eve_degree.py +++ b/tests/components/homekit_controller/specific_devices/test_eve_degree.py @@ -41,7 +41,7 @@ async def test_eve_degree_setup(hass): ( "number.eve_degree_aa11_elevation", "homekit-AA00A0A00000-aid:1-sid:30-cid:33", - "Eve Degree AA11 - Elevation", + "Eve Degree AA11 Elevation", ), ] diff --git a/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py b/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py index fe570ff0b73..a58f306a241 100644 --- a/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py +++ b/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py @@ -31,8 +31,7 @@ async def test_vocolinc_flowerbud_setup(hass): ) state = await helper.poll_and_get_state() assert ( - state.attributes["friendly_name"] - == "VOCOlinc-Flowerbud-0d324b - Spray Quantity" + state.attributes["friendly_name"] == "VOCOlinc-Flowerbud-0d324b Spray Quantity" ) device = device_registry.async_get(entry.device_id) diff --git a/tests/components/homekit_controller/test_switch.py b/tests/components/homekit_controller/test_switch.py index c53d20891b1..5c737e63edc 100644 --- a/tests/components/homekit_controller/test_switch.py +++ b/tests/components/homekit_controller/test_switch.py @@ -38,6 +38,14 @@ def create_valve_service(accessory): remaining.value = 99 +def create_char_switch_service(accessory): + """Define swtch characteristics.""" + service = accessory.add_service(ServicesTypes.OUTLET) + + on_char = service.add_char(CharacteristicsTypes.Vendor.AQARA_PAIRING_MODE) + on_char.value = False + + async def test_switch_change_outlet_state(hass, utcnow): """Test that we can turn a HomeKit outlet on and off again.""" helper = await setup_test_component(hass, create_switch_service) @@ -122,3 +130,51 @@ async def test_valve_read_state(hass, utcnow): helper.characteristics[("valve", "in-use")].value = InUseValues.NOT_IN_USE switch_1 = await helper.poll_and_get_state() assert switch_1.attributes["in_use"] is False + + +async def test_char_switch_change_state(hass, utcnow): + """Test that we can turn a characteristic on and off again.""" + helper = await setup_test_component( + hass, create_char_switch_service, suffix="pairing_mode" + ) + svc = helper.accessory.services.first(service_type=ServicesTypes.OUTLET) + pairing_mode = svc[CharacteristicsTypes.Vendor.AQARA_PAIRING_MODE] + + await hass.services.async_call( + "switch", + "turn_on", + {"entity_id": "switch.testdevice_pairing_mode"}, + blocking=True, + ) + assert pairing_mode.value is True + + await hass.services.async_call( + "switch", + "turn_off", + {"entity_id": "switch.testdevice_pairing_mode"}, + blocking=True, + ) + assert pairing_mode.value is False + + +async def test_char_switch_read_state(hass, utcnow): + """Test that we can read the state of a HomeKit characteristic switch.""" + helper = await setup_test_component( + hass, create_char_switch_service, suffix="pairing_mode" + ) + svc = helper.accessory.services.first(service_type=ServicesTypes.OUTLET) + pairing_mode = svc[CharacteristicsTypes.Vendor.AQARA_PAIRING_MODE] + + # Initial state is that the switch is off + switch_1 = await helper.poll_and_get_state() + assert switch_1.state == "off" + + # Simulate that someone switched on the device in the real world not via HA + pairing_mode.set_value(True) + switch_1 = await helper.poll_and_get_state() + assert switch_1.state == "on" + + # Simulate that device switched off in the real world not via HA + pairing_mode.set_value(False) + switch_1 = await helper.poll_and_get_state() + assert switch_1.state == "off" From d1c0e60bf10117fe9776f2347cd4a088834619cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Klomp?= Date: Wed, 22 Dec 2021 20:57:35 +0100 Subject: [PATCH 0953/2644] Bump pysma to 0.6.10 (#62599) --- homeassistant/components/sma/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sma/manifest.json b/homeassistant/components/sma/manifest.json index 4b48ce6aef1..d667ab7ea37 100644 --- a/homeassistant/components/sma/manifest.json +++ b/homeassistant/components/sma/manifest.json @@ -3,7 +3,7 @@ "name": "SMA Solar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sma", - "requirements": ["pysma==0.6.9"], + "requirements": ["pysma==0.6.10"], "codeowners": ["@kellerza", "@rklomp"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 8c4e685de62..0a4f7bfc6a7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1816,7 +1816,7 @@ pysignalclirestapi==0.3.4 pyskyqhub==0.1.3 # homeassistant.components.sma -pysma==0.6.9 +pysma==0.6.10 # homeassistant.components.smappee pysmappee==0.2.29 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3a03271aa22..f68c247f3fd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1119,7 +1119,7 @@ pysiaalarm==3.0.2 pysignalclirestapi==0.3.4 # homeassistant.components.sma -pysma==0.6.9 +pysma==0.6.10 # homeassistant.components.smappee pysmappee==0.2.29 From 51a49f3d3997ea3cf71d46af12ba0f7223ce9eb4 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 22 Dec 2021 20:59:56 +0100 Subject: [PATCH 0954/2644] Fix missing exception handling from upstream lib in Fritz (#62617) * Fix missing exception handling from upstream lib * isort --- homeassistant/components/fritz/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/fritz/__init__.py b/homeassistant/components/fritz/__init__.py index 2f53bc540a0..568364c45ac 100644 --- a/homeassistant/components/fritz/__init__.py +++ b/homeassistant/components/fritz/__init__.py @@ -3,6 +3,7 @@ import logging from fritzconnection.core.exceptions import FritzConnectionException, FritzSecurityError from fritzconnection.core.logger import fritzlogger +from requests import exceptions from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME @@ -38,7 +39,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await fritz_tools.async_setup(entry.options) except FritzSecurityError as ex: raise ConfigEntryAuthFailed from ex - except FritzConnectionException as ex: + except (FritzConnectionException, exceptions.ConnectionError) as ex: raise ConfigEntryNotReady from ex hass.data.setdefault(DOMAIN, {}) From e3f7d9a8039d2286a1f0df1100ed30d7cf3bd68b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 22 Dec 2021 21:17:23 +0100 Subject: [PATCH 0955/2644] Extract PVOutput logic into PyPi package (#62625) --- CODEOWNERS | 2 +- .../components/pvoutput/manifest.json | 4 +- homeassistant/components/pvoutput/sensor.py | 86 +++++-------------- requirements_all.txt | 3 + 4 files changed, 29 insertions(+), 66 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index aedfcea7e96..28ce29ec29a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -717,7 +717,7 @@ homeassistant/components/ps4/* @ktnrg45 tests/components/ps4/* @ktnrg45 homeassistant/components/push/* @dgomes tests/components/push/* @dgomes -homeassistant/components/pvoutput/* @fabaff +homeassistant/components/pvoutput/* @fabaff @frenck homeassistant/components/pvpc_hourly_pricing/* @azogue tests/components/pvpc_hourly_pricing/* @azogue homeassistant/components/qbittorrent/* @geoffreylagaisse diff --git a/homeassistant/components/pvoutput/manifest.json b/homeassistant/components/pvoutput/manifest.json index af40cf7eca4..d349aa33849 100644 --- a/homeassistant/components/pvoutput/manifest.json +++ b/homeassistant/components/pvoutput/manifest.json @@ -2,7 +2,7 @@ "domain": "pvoutput", "name": "PVOutput", "documentation": "https://www.home-assistant.io/integrations/pvoutput", - "after_dependencies": ["rest"], - "codeowners": ["@fabaff"], + "codeowners": ["@fabaff", "@frenck"], + "requirements": ["pvo==0.1.0"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/pvoutput/sensor.py b/homeassistant/components/pvoutput/sensor.py index 4f745666020..a18c5cc40b6 100644 --- a/homeassistant/components/pvoutput/sensor.py +++ b/homeassistant/components/pvoutput/sensor.py @@ -1,13 +1,12 @@ """Support for getting collected information from PVOutput.""" from __future__ import annotations -from collections import namedtuple from datetime import timedelta import logging +from pvo import PVOutput, PVOutputError import voluptuous as vol -from homeassistant.components.rest.data import RestData from homeassistant.components.sensor import ( PLATFORM_SCHEMA, SensorDeviceClass, @@ -15,19 +14,15 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.const import ( - ATTR_DATE, ATTR_TEMPERATURE, - ATTR_TIME, ATTR_VOLTAGE, CONF_API_KEY, CONF_NAME, ENERGY_WATT_HOUR, ) -from homeassistant.core import callback import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -_ENDPOINT = "https://pvoutput.org/service/r2/getstatus.jsp" ATTR_ENERGY_GENERATION = "energy_generation" ATTR_POWER_GENERATION = "power_generation" @@ -38,7 +33,6 @@ ATTR_EFFICIENCY = "efficiency" CONF_SYSTEM_ID = "system_id" DEFAULT_NAME = "PVOutput" -DEFAULT_VERIFY_SSL = True SCAN_INTERVAL = timedelta(minutes=2) @@ -54,21 +48,19 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the PVOutput sensor.""" name = config.get(CONF_NAME) - api_key = config.get(CONF_API_KEY) - system_id = config.get(CONF_SYSTEM_ID) - method = "GET" - payload = auth = None - verify_ssl = DEFAULT_VERIFY_SSL - headers = {"X-Pvoutput-Apikey": api_key, "X-Pvoutput-SystemId": system_id} - rest = RestData(hass, method, _ENDPOINT, auth, headers, None, payload, verify_ssl) - await rest.async_update() + pvoutput = PVOutput( + api_key=config[CONF_API_KEY], + system_id=config[CONF_SYSTEM_ID], + ) - if rest.data is None: + try: + status = await pvoutput.status() + except PVOutputError: _LOGGER.error("Unable to fetch data from PVOutput") return False - async_add_entities([PvoutputSensor(rest, name)]) + async_add_entities([PvoutputSensor(pvoutput, status, name)]) class PvoutputSensor(SensorEntity): @@ -78,62 +70,30 @@ class PvoutputSensor(SensorEntity): _attr_device_class = SensorDeviceClass.ENERGY _attr_native_unit_of_measurement = ENERGY_WATT_HOUR - def __init__(self, rest, name): + def __init__(self, pvoutput, status, name): """Initialize a PVOutput sensor.""" - self.rest = rest self._attr_name = name - self.pvcoutput = None - self.status = namedtuple( - "status", - [ - ATTR_DATE, - ATTR_TIME, - ATTR_ENERGY_GENERATION, - ATTR_POWER_GENERATION, - ATTR_ENERGY_CONSUMPTION, - ATTR_POWER_CONSUMPTION, - ATTR_EFFICIENCY, - ATTR_TEMPERATURE, - ATTR_VOLTAGE, - ], - ) + self.pvoutput = pvoutput + self.status = status @property def native_value(self): """Return the state of the device.""" - if self.pvcoutput is not None: - return self.pvcoutput.energy_generation - return None + return self.status.energy_generation @property def extra_state_attributes(self): """Return the state attributes of the monitored installation.""" - if self.pvcoutput is not None: - return { - ATTR_ENERGY_GENERATION: self.pvcoutput.energy_generation, - ATTR_POWER_GENERATION: self.pvcoutput.power_generation, - ATTR_ENERGY_CONSUMPTION: self.pvcoutput.energy_consumption, - ATTR_POWER_CONSUMPTION: self.pvcoutput.power_consumption, - ATTR_EFFICIENCY: self.pvcoutput.efficiency, - ATTR_TEMPERATURE: self.pvcoutput.temperature, - ATTR_VOLTAGE: self.pvcoutput.voltage, - } + return { + ATTR_ENERGY_GENERATION: self.status.energy_generation, + ATTR_POWER_GENERATION: self.status.power_generation, + ATTR_ENERGY_CONSUMPTION: self.status.energy_consumption, + ATTR_POWER_CONSUMPTION: self.status.power_consumption, + ATTR_EFFICIENCY: self.status.normalized_ouput, + ATTR_TEMPERATURE: self.status.temperature, + ATTR_VOLTAGE: self.status.voltage, + } async def async_update(self): """Get the latest data from the PVOutput API and updates the state.""" - await self.rest.async_update() - self._async_update_from_rest_data() - - async def async_added_to_hass(self): - """Ensure the data from the initial update is reflected in the state.""" - self._async_update_from_rest_data() - - @callback - def _async_update_from_rest_data(self): - """Update state from the rest data.""" - try: - # https://pvoutput.org/help/api_specification.html#get-status-service - self.pvcoutput = self.status._make(self.rest.data.split(",")) - except TypeError: - self.pvcoutput = None - _LOGGER.error("Unable to fetch data from PVOutput. %s", self.rest.data) + self.status = await self.pvoutput.status() diff --git a/requirements_all.txt b/requirements_all.txt index 0a4f7bfc6a7..de80c1b96d8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1291,6 +1291,9 @@ pushbullet.py==0.11.0 # homeassistant.components.pushover pushover_complete==1.1.1 +# homeassistant.components.pvoutput +pvo==0.1.0 + # homeassistant.components.rpi_gpio_pwm pwmled==1.6.7 From 4954f3c73f8d596ee67965c69821a212c0af0c55 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Dec 2021 13:18:52 -0700 Subject: [PATCH 0956/2644] Bump flux_led to 0.27.13 to fix discovery of legacy devices (#62613) - The 2013/2014 devices have yet another format for the version --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 53192b96b89..8360f36aa9a 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["network"], "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.27.12"], + "requirements": ["flux_led==0.27.13"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index de80c1b96d8..35e1dac37c3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -666,7 +666,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.27.12 +flux_led==0.27.13 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f68c247f3fd..88f43c270b1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -406,7 +406,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.27.12 +flux_led==0.27.13 # homeassistant.components.homekit fnvhash==0.1.0 From a49aa065b7b76e5bfd898412f1afdfca411e3e61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 22 Dec 2021 22:21:05 +0200 Subject: [PATCH 0957/2644] Derive mypy python_version from REQUIRED_PYTHON_VER (#62616) --- script/hassfest/mypy_config.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 9878fb891e7..80e2022243d 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -7,6 +7,8 @@ import os from pathlib import Path from typing import Final +from homeassistant.const import REQUIRED_PYTHON_VER + from .model import Config, Integration # Modules which have type hints which known to be broken. @@ -143,7 +145,7 @@ HEADER: Final = """ """.lstrip() GENERAL_SETTINGS: Final[dict[str, str]] = { - "python_version": "3.8", + "python_version": ".".join(str(x) for x in REQUIRED_PYTHON_VER[:2]), "show_error_codes": "true", "follow_imports": "silent", # Enable some checks globally. From 0b8cf4761364904106d7f2f484ebb6a8714d2a6d Mon Sep 17 00:00:00 2001 From: Thomas Dietrich Date: Wed, 22 Dec 2021 21:22:57 +0100 Subject: [PATCH 0958/2644] Add secondary codeowner to statistics integration (#62622) --- CODEOWNERS | 4 ++-- homeassistant/components/statistics/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 28ce29ec29a..b38a168d850 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -871,8 +871,8 @@ homeassistant/components/srp_energy/* @briglx tests/components/srp_energy/* @briglx homeassistant/components/starline/* @anonym-tsk tests/components/starline/* @anonym-tsk -homeassistant/components/statistics/* @fabaff -tests/components/statistics/* @fabaff +homeassistant/components/statistics/* @fabaff @ThomDietrich +tests/components/statistics/* @fabaff @ThomDietrich homeassistant/components/stiebel_eltron/* @fucm homeassistant/components/stookalert/* @fwestenberg @frenck tests/components/stookalert/* @fwestenberg @frenck diff --git a/homeassistant/components/statistics/manifest.json b/homeassistant/components/statistics/manifest.json index 936f8b60849..442bdf2ca6c 100644 --- a/homeassistant/components/statistics/manifest.json +++ b/homeassistant/components/statistics/manifest.json @@ -3,7 +3,7 @@ "name": "Statistics", "documentation": "https://www.home-assistant.io/integrations/statistics", "after_dependencies": ["recorder"], - "codeowners": ["@fabaff"], + "codeowners": ["@fabaff", "@ThomDietrich"], "quality_scale": "internal", "iot_class": "local_polling" } From 0e48a658f378ed8c424654cb30f6d8918bb64b6e Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 22 Dec 2021 21:28:18 +0100 Subject: [PATCH 0959/2644] Fix timezone trafikverket_train (#62582) * Bugfix trafikverket train * Change from pytz to hass function * Fix datetime in extra attributes * Fix time timezone * Reset changes extra attributes --- .../components/trafikverket_train/sensor.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/trafikverket_train/sensor.py b/homeassistant/components/trafikverket_train/sensor.py index 08ebb32abcf..57d02e95582 100644 --- a/homeassistant/components/trafikverket_train/sensor.py +++ b/homeassistant/components/trafikverket_train/sensor.py @@ -14,6 +14,7 @@ from homeassistant.components.sensor import ( from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_WEEKDAY, WEEKDAYS from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv +from homeassistant.util.dt import get_time_zone _LOGGER = logging.getLogger(__name__) @@ -129,12 +130,15 @@ class TrainSensor(SensorEntity): self._state = None self._departure_state = None self._delay_in_minutes = None + self._timezone = get_time_zone("Europe/Stockholm") async def async_update(self): """Retrieve latest state.""" if self._time is not None: departure_day = next_departuredate(self._weekday) - when = datetime.combine(departure_day, self._time) + when = datetime.combine(departure_day, self._time).astimezone( + self._timezone + ) try: self._state = await self._train_api.async_get_train_stop( self._from_station, self._to_station, when @@ -191,8 +195,8 @@ class TrainSensor(SensorEntity): """Return the departure state.""" if (state := self._state) is not None: if state.time_at_location is not None: - return state.time_at_location + return state.time_at_location.astimezone(self._timezone) if state.estimated_time_at_location is not None: - return state.estimated_time_at_location - return state.advertised_time_at_location + return state.estimated_time_at_location.astimezone(self._timezone) + return state.advertised_time_at_location.astimezone(self._timezone) return None From 566f6319337c9162d36941001e47cd5146fca851 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 22 Dec 2021 21:32:50 +0100 Subject: [PATCH 0960/2644] Implement config_Flow for Sensibo (#60900) Co-authored-by: J. Nick Koston --- .coveragerc | 1 + CODEOWNERS | 3 +- homeassistant/components/sensibo/__init__.py | 61 +++++++ homeassistant/components/sensibo/climate.py | 84 +++++----- .../components/sensibo/config_flow.py | 91 ++++++++++ homeassistant/components/sensibo/const.py | 15 ++ .../components/sensibo/manifest.json | 8 +- homeassistant/components/sensibo/strings.json | 18 ++ .../components/sensibo/translations/en.json | 18 ++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/zeroconf.py | 1 + requirements_test_all.txt | 3 + tests/components/sensibo/__init__.py | 1 + tests/components/sensibo/test_config_flow.py | 155 ++++++++++++++++++ 14 files changed, 416 insertions(+), 44 deletions(-) create mode 100644 homeassistant/components/sensibo/config_flow.py create mode 100644 homeassistant/components/sensibo/strings.json create mode 100644 homeassistant/components/sensibo/translations/en.json create mode 100644 tests/components/sensibo/__init__.py create mode 100644 tests/components/sensibo/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 0c48b0ebb25..26f05a7816a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -935,6 +935,7 @@ omit = homeassistant/components/sense/sensor.py homeassistant/components/sensehat/light.py homeassistant/components/sensehat/sensor.py + homeassistant/components/sensibo/__init__.py homeassistant/components/sensibo/climate.py homeassistant/components/serial/sensor.py homeassistant/components/serial_pm/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index b38a168d850..75149c273e6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -793,7 +793,8 @@ homeassistant/components/select/* @home-assistant/core tests/components/select/* @home-assistant/core homeassistant/components/sense/* @kbickar tests/components/sense/* @kbickar -homeassistant/components/sensibo/* @andrey-git +homeassistant/components/sensibo/* @andrey-git @gjohansson-ST +tests/components/sensibo/* @andrey-git @gjohansson-ST homeassistant/components/sentry/* @dcramer @frenck tests/components/sentry/* @dcramer @frenck homeassistant/components/serial/* @fabaff diff --git a/homeassistant/components/sensibo/__init__.py b/homeassistant/components/sensibo/__init__.py index 41959bdc9b2..c384c826859 100644 --- a/homeassistant/components/sensibo/__init__.py +++ b/homeassistant/components/sensibo/__init__.py @@ -1 +1,62 @@ """The sensibo component.""" +from __future__ import annotations + +import asyncio +import logging + +import aiohttp +import async_timeout +import pysensibo + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import _INITIAL_FETCH_FIELDS, DOMAIN, PLATFORMS, TIMEOUT + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Sensibo from a config entry.""" + client = pysensibo.SensiboClient( + entry.data[CONF_API_KEY], session=async_get_clientsession(hass), timeout=TIMEOUT + ) + devicelist = [] + try: + async with async_timeout.timeout(TIMEOUT): + for dev in await client.async_get_devices(_INITIAL_FETCH_FIELDS): + devicelist.append(dev) + except ( + aiohttp.client_exceptions.ClientConnectorError, + asyncio.TimeoutError, + pysensibo.SensiboError, + ) as err: + raise ConfigEntryNotReady( + f"Failed to get devices from Sensibo servers: {err}" + ) from err + + if not devicelist: + return False + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { + "devices": devicelist, + "client": client, + } + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + _LOGGER.debug("Loaded entry for %s", entry.title) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload Sensibo config entry.""" + if await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + del hass.data[DOMAIN][entry.entry_id] + if not hass.data[DOMAIN]: + del hass.data[DOMAIN] + _LOGGER.debug("Unloaded entry for %s", entry.title) + return True + return False diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index 7104d9ebff7..60ea483865f 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -8,7 +8,10 @@ import async_timeout import pysensibo import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity +from homeassistant.components.climate import ( + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + ClimateEntity, +) from homeassistant.components.climate.const import ( HVAC_MODE_COOL, HVAC_MODE_DRY, @@ -20,6 +23,7 @@ from homeassistant.components.climate.const import ( SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_STATE, @@ -30,21 +34,22 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, ) -from homeassistant.exceptions import PlatformNotReady +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity_platform import ( + AddEntitiesCallback, + ConfigType, + DiscoveryInfoType, +) from homeassistant.util.temperature import convert as convert_temperature -from .const import DOMAIN as SENSIBO_DOMAIN +from .const import _FETCH_FIELDS, ALL, DOMAIN, TIMEOUT _LOGGER = logging.getLogger(__name__) -ALL = ["all"] -TIMEOUT = 8 - SERVICE_ASSUME_STATE = "assume_state" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_ID, default=ALL): vol.All(cv.ensure_list, [cv.string]), @@ -55,18 +60,6 @@ ASSUME_STATE_SCHEMA = vol.Schema( {vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Required(ATTR_STATE): cv.string} ) -_FETCH_FIELDS = ",".join( - [ - "room{name}", - "measurements", - "remoteCapabilities", - "acState", - "connectionStatus{isAlive}", - "temperatureUnit", - ] -) -_INITIAL_FETCH_FIELDS = f"id,{_FETCH_FIELDS}" - FIELD_TO_FLAG = { "fanLevel": SUPPORT_FAN_MODE, "swing": SUPPORT_SWING_MODE, @@ -84,29 +77,38 @@ SENSIBO_TO_HA = { HA_TO_SENSIBO = {value: key for key, value in SENSIBO_TO_HA.items()} -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType = None, +): """Set up Sensibo devices.""" - client = pysensibo.SensiboClient( - config[CONF_API_KEY], session=async_get_clientsession(hass), timeout=TIMEOUT + _LOGGER.warning( + "Loading Sensibo via platform setup is deprecated; Please remove it from your configuration" + ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) ) - devices = [] - try: - async with async_timeout.timeout(TIMEOUT): - for dev in await client.async_get_devices(_INITIAL_FETCH_FIELDS): - if config[CONF_ID] == ALL or dev["id"] in config[CONF_ID]: - devices.append( - SensiboClimate(client, dev, hass.config.units.temperature_unit) - ) - except ( - aiohttp.client_exceptions.ClientConnectorError, - asyncio.TimeoutError, - pysensibo.SensiboError, - ) as err: - _LOGGER.error("Failed to get devices from Sensibo servers") - raise PlatformNotReady from err - if not devices: - return + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the Sensibo climate entry.""" + + data = hass.data[DOMAIN][entry.entry_id] + client = data["client"] + devicelist = data["devices"] + + devices = [ + SensiboClimate(client, dev, hass.config.units.temperature_unit) + for dev in devicelist + ] async_add_entities(devices) @@ -128,7 +130,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= await asyncio.wait(update_tasks) hass.services.async_register( - SENSIBO_DOMAIN, + DOMAIN, SERVICE_ASSUME_STATE, async_assume_state, schema=ASSUME_STATE_SCHEMA, diff --git a/homeassistant/components/sensibo/config_flow.py b/homeassistant/components/sensibo/config_flow.py new file mode 100644 index 00000000000..0d9e7880f38 --- /dev/null +++ b/homeassistant/components/sensibo/config_flow.py @@ -0,0 +1,91 @@ +"""Adds config flow for Sensibo integration.""" +from __future__ import annotations + +import asyncio +import logging + +import aiohttp +import async_timeout +from pysensibo import SensiboClient, SensiboError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY, CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv + +from .const import _INITIAL_FETCH_FIELDS, DEFAULT_NAME, DOMAIN, TIMEOUT + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) + + +async def async_validate_api(hass: HomeAssistant, api_key: str) -> bool: + """Get data from API.""" + client = SensiboClient( + api_key, + session=async_get_clientsession(hass), + timeout=TIMEOUT, + ) + + try: + async with async_timeout.timeout(TIMEOUT): + if await client.async_get_devices(_INITIAL_FETCH_FIELDS): + return True + except ( + aiohttp.ClientConnectionError, + asyncio.TimeoutError, + SensiboError, + ) as err: + _LOGGER.error("Failed to get devices from Sensibo servers %s", err) + return False + + +class SensiboConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Sensibo integration.""" + + VERSION = 1 + + async def async_step_import(self, config: dict): + """Import a configuration from config.yaml.""" + + self.context.update( + {"title_placeholders": {"Sensibo": f"YAML import {DOMAIN}"}} + ) + if CONF_NAME not in config: + config[CONF_NAME] = DEFAULT_NAME + return await self.async_step_user(user_input=config) + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + + errors: dict[str, str] = {} + + if user_input is not None: + + api_key = user_input[CONF_API_KEY] + name = user_input[CONF_NAME] + + await self.async_set_unique_id(api_key) + self._abort_if_unique_id_configured() + + validate = await async_validate_api(self.hass, api_key) + if validate: + return self.async_create_entry( + title=name, + data={CONF_NAME: name, CONF_API_KEY: api_key}, + ) + errors["base"] = "cannot_connect" + + return self.async_show_form( + step_id="user", + data_schema=DATA_SCHEMA, + errors=errors, + ) diff --git a/homeassistant/components/sensibo/const.py b/homeassistant/components/sensibo/const.py index 383eca59f47..45d53df2d80 100644 --- a/homeassistant/components/sensibo/const.py +++ b/homeassistant/components/sensibo/const.py @@ -1,3 +1,18 @@ """Constants for Sensibo.""" DOMAIN = "sensibo" +PLATFORMS = ["climate"] +ALL = ["all"] +DEFAULT_NAME = "Sensibo@Home" +TIMEOUT = 8 +_FETCH_FIELDS = ",".join( + [ + "room{name}", + "measurements", + "remoteCapabilities", + "acState", + "connectionStatus{isAlive}", + "temperatureUnit", + ] +) +_INITIAL_FETCH_FIELDS = f"id,{_FETCH_FIELDS}" diff --git a/homeassistant/components/sensibo/manifest.json b/homeassistant/components/sensibo/manifest.json index 3cea31c5d5e..bf0142628b4 100644 --- a/homeassistant/components/sensibo/manifest.json +++ b/homeassistant/components/sensibo/manifest.json @@ -3,6 +3,10 @@ "name": "Sensibo", "documentation": "https://www.home-assistant.io/integrations/sensibo", "requirements": ["pysensibo==1.0.3"], - "codeowners": ["@andrey-git"], - "iot_class": "cloud_polling" + "config_flow": true, + "codeowners": ["@andrey-git", "@gjohansson-ST"], + "iot_class": "cloud_polling", + "homekit": { + "models": ["Sensibo"] + } } diff --git a/homeassistant/components/sensibo/strings.json b/homeassistant/components/sensibo/strings.json new file mode 100644 index 00000000000..22751964999 --- /dev/null +++ b/homeassistant/components/sensibo/strings.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + }, + "error":{ + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "step": { + "user": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]", + "name": "[%key:common::config_flow::data::name%]" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/en.json b/homeassistant/components/sensibo/translations/en.json new file mode 100644 index 00000000000..4d07dadc086 --- /dev/null +++ b/homeassistant/components/sensibo/translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Account is already configured" + }, + "error":{ + "cannot_connect": "Failed to connect" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "name": "Name" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index f1474f415d0..473caaa44c8 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -263,6 +263,7 @@ FLOWS = [ "samsungtv", "screenlogic", "sense", + "sensibo", "sentry", "sharkiq", "shelly", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index d5b8839bd77..bc4a83f3261 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -369,6 +369,7 @@ HOMEKIT = { "Presence": "netatmo", "Rachio": "rachio", "SPK5": "rainmachine", + "Sensibo": "sensibo", "Smart Bridge": "lutron_caseta", "Socket": "wemo", "TRADFRI": "tradfri", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 88f43c270b1..4aa16511c71 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1102,6 +1102,9 @@ pyrituals==0.0.6 # homeassistant.components.ruckus_unleashed pyruckus==0.12 +# homeassistant.components.sensibo +pysensibo==1.0.3 + # homeassistant.components.serial # homeassistant.components.zha pyserial-asyncio==0.5 diff --git a/tests/components/sensibo/__init__.py b/tests/components/sensibo/__init__.py new file mode 100644 index 00000000000..8dd2ed661bc --- /dev/null +++ b/tests/components/sensibo/__init__.py @@ -0,0 +1 @@ +"""Tests for the Sensibo integration.""" diff --git a/tests/components/sensibo/test_config_flow.py b/tests/components/sensibo/test_config_flow.py new file mode 100644 index 00000000000..b277ed80e96 --- /dev/null +++ b/tests/components/sensibo/test_config_flow.py @@ -0,0 +1,155 @@ +"""Test the Sensibo config flow.""" +from __future__ import annotations + +import asyncio +from unittest.mock import patch + +import aiohttp +from pysensibo import SensiboError +import pytest + +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY, CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from tests.common import MockConfigEntry + +DOMAIN = "sensibo" + + +def devices(): + """Return list of test devices.""" + return (yield from [{"id": "xyzxyz"}, {"id": "abcabc"}]) + + +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["step_id"] == "user" + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.sensibo.config_flow.SensiboClient.async_get_devices", + return_value=devices(), + ), patch( + "homeassistant.components.sensibo.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_NAME: "Sensibo@Home", + CONF_API_KEY: "1234567890", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["data"] == { + "name": "Sensibo@Home", + "api_key": "1234567890", + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_flow_success(hass: HomeAssistant) -> None: + """Test a successful import of yaml.""" + + with patch( + "homeassistant.components.sensibo.config_flow.SensiboClient.async_get_devices", + return_value=devices(), + ), patch( + "homeassistant.components.sensibo.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: "1234567890", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "Sensibo@Home" + assert result2["data"] == { + "name": "Sensibo@Home", + "api_key": "1234567890", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_flow_already_exist(hass: HomeAssistant) -> None: + """Test import of yaml already exist.""" + + MockConfigEntry( + domain=DOMAIN, + data={ + CONF_NAME: "Sensibo@Home", + CONF_API_KEY: "1234567890", + }, + unique_id="1234567890", + ).add_to_hass(hass) + + with patch( + "homeassistant.components.sensibo.async_setup_entry", + return_value=True, + ), patch( + "homeassistant.components.sensibo.config_flow.SensiboClient.async_get_devices", + return_value=devices(), + ): + result3 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: "1234567890", + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == RESULT_TYPE_ABORT + assert result3["reason"] == "already_configured" + + +@pytest.mark.parametrize( + "error_message", + [ + (aiohttp.ClientConnectionError), + (asyncio.TimeoutError), + (SensiboError), + ], +) +async def test_flow_fails(hass: HomeAssistant, error_message) -> None: + """Test config flow errors.""" + + result4 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result4["type"] == RESULT_TYPE_FORM + assert result4["step_id"] == config_entries.SOURCE_USER + + with patch( + "homeassistant.components.sensibo.config_flow.SensiboClient.async_get_devices", + side_effect=error_message, + ): + result4 = await hass.config_entries.flow.async_configure( + result4["flow_id"], + user_input={ + CONF_NAME: "Sensibo@Home", + CONF_API_KEY: "1234567890", + }, + ) + + assert result4["errors"] == {"base": "cannot_connect"} From 8750fd14ccaa15b14508a65ae34a883c14b77903 Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Wed, 22 Dec 2021 15:44:37 -0500 Subject: [PATCH 0961/2644] Improve Insteon responsiveness (#62612) --- homeassistant/components/insteon/__init__.py | 2 ++ homeassistant/components/insteon/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index 223448953b9..e9be0d22cb8 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -39,6 +39,8 @@ async def async_get_device_config(hass, config_entry): # Make a copy of addresses due to edge case where the list of devices could change during status update # Cannot be done concurrently due to issues with the underlying protocol. for address in list(devices): + if devices[address].is_battery: + continue with suppress(AttributeError): await devices[address].async_status() diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index c17f441a159..e00a85a9823 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -3,7 +3,7 @@ "name": "Insteon", "documentation": "https://www.home-assistant.io/integrations/insteon", "requirements": [ - "pyinsteon==1.0.13" + "pyinsteon==1.0.14" ], "codeowners": [ "@teharris1" diff --git a/requirements_all.txt b/requirements_all.txt index 35e1dac37c3..fef25ac0673 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1561,7 +1561,7 @@ pyialarm==1.9.0 pyicloud==0.10.2 # homeassistant.components.insteon -pyinsteon==1.0.13 +pyinsteon==1.0.14 # homeassistant.components.intesishome pyintesishome==1.7.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4aa16511c71..d2f8504a6a2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -951,7 +951,7 @@ pyialarm==1.9.0 pyicloud==0.10.2 # homeassistant.components.insteon -pyinsteon==1.0.13 +pyinsteon==1.0.14 # homeassistant.components.ipma pyipma==2.0.5 From e593377fba1a517d06a3d6fb496a8247f5c3e2f6 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 22 Dec 2021 16:17:53 -0500 Subject: [PATCH 0962/2644] Add deprecation warning to switchbot yaml config (#62583) Co-authored-by: Franck Nijhof --- homeassistant/components/switchbot/switch.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index 76353559756..20e8a58b6ee 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -49,6 +49,12 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Import yaml config and initiates config flow for Switchbot devices.""" + _LOGGER.warning( + "Configuration of the Switchbot switch platform in YAML is deprecated and " + "will be removed in Home Assistant 2022.4; Your existing configuration " + "has been imported into the UI automatically and can be safely removed " + "from your configuration.yaml file" + ) # Check if entry config exists and skips import if it does. if hass.config_entries.async_entries(DOMAIN): From 91a8b1e7b3d90d50ba42f2df3973fcb3dfbb97e9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Dec 2021 14:27:03 -0700 Subject: [PATCH 0963/2644] Speed up connecting to legacy flux_led devices (#62614) --- homeassistant/components/flux_led/__init__.py | 17 ++++++++++------- .../components/flux_led/config_flow.py | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/flux_led/__init__.py b/homeassistant/components/flux_led/__init__.py index 0630f49de84..81fb9a1ce24 100644 --- a/homeassistant/components/flux_led/__init__.py +++ b/homeassistant/components/flux_led/__init__.py @@ -8,6 +8,7 @@ from typing import Any, Final, cast from flux_led import DeviceType from flux_led.aio import AIOWifiLedBulb from flux_led.const import ATTR_ID +from flux_led.scanner import FluxLEDDiscovery from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STARTED, Platform @@ -52,9 +53,11 @@ REQUEST_REFRESH_DELAY: Final = 1.5 @callback -def async_wifi_bulb_for_host(host: str) -> AIOWifiLedBulb: +def async_wifi_bulb_for_host( + host: str, discovery: FluxLEDDiscovery | None +) -> AIOWifiLedBulb: """Create a AIOWifiLedBulb from a host.""" - return AIOWifiLedBulb(host) + return AIOWifiLedBulb(host, discovery=discovery) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -78,7 +81,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Flux LED/MagicLight from a config entry.""" host = entry.data[CONF_HOST] - device: AIOWifiLedBulb = async_wifi_bulb_for_host(host) + directed_discovery = None + if discovery := async_get_discovery(hass, host): + directed_discovery = False + device: AIOWifiLedBulb = async_wifi_bulb_for_host(host, discovery=discovery) signal = SIGNAL_STATE_UPDATED.format(device.ipaddr) @callback @@ -94,10 +100,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) from ex # UDP probe after successful connect only - directed_discovery = None - if discovery := async_get_discovery(hass, host): - directed_discovery = False - elif discovery := await async_discover_device(hass, host): + if not discovery and (discovery := await async_discover_device(hass, host)): directed_discovery = True if discovery: diff --git a/homeassistant/components/flux_led/config_flow.py b/homeassistant/components/flux_led/config_flow.py index 942ba840212..101e3a55d17 100644 --- a/homeassistant/components/flux_led/config_flow.py +++ b/homeassistant/components/flux_led/config_flow.py @@ -225,7 +225,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # identifying the device as the chip model number # AKA `HF-LPB100-ZJ200` return device - bulb = async_wifi_bulb_for_host(host) + bulb = async_wifi_bulb_for_host(host, discovery=device) try: await bulb.async_setup(lambda: None) finally: From bda1f02371a70289c62576dcc7f18108c95cceb7 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 22 Dec 2021 22:38:55 +0100 Subject: [PATCH 0964/2644] Reduce boilerplate code in entry init of rfxtrx (#58844) * Reduce boilerplate code for rfxtrx * Use rfxtrx built in to construct event * Fixup mypy after rebase * Also fix callable import --- homeassistant/components/rfxtrx/__init__.py | 110 ++++++++++++------ .../components/rfxtrx/binary_sensor.py | 107 ++++------------- homeassistant/components/rfxtrx/const.py | 2 - homeassistant/components/rfxtrx/cover.py | 77 ++++-------- homeassistant/components/rfxtrx/light.py | 81 ++++--------- homeassistant/components/rfxtrx/sensor.py | 80 ++++--------- homeassistant/components/rfxtrx/switch.py | 81 ++++--------- 7 files changed, 180 insertions(+), 358 deletions(-) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index e380e1734e6..c93e686701f 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio import binascii +from collections.abc import Callable import copy import functools import logging @@ -23,10 +24,11 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, Platform, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceRegistry -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from .const import ( @@ -35,8 +37,6 @@ from .const import ( CONF_AUTOMATIC_ADD, CONF_DATA_BITS, CONF_REMOVE_DEVICE, - DATA_CLEANUP_CALLBACKS, - DATA_LISTENER, DATA_RFXOBJECT, DEVICE_PACKET_TYPE_LIGHTING4, EVENT_RFXTRX_EVENT, @@ -85,8 +85,6 @@ async def async_setup_entry(hass, entry: config_entries.ConfigEntry): """Set up the RFXtrx component.""" hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][DATA_CLEANUP_CALLBACKS] = [] - try: await async_setup_internal(hass, entry) except asyncio.TimeoutError: @@ -108,12 +106,6 @@ async def async_unload_entry(hass, entry: config_entries.ConfigEntry): hass.services.async_remove(DOMAIN, SERVICE_SEND) - for cleanup_callback in hass.data[DOMAIN][DATA_CLEANUP_CALLBACKS]: - cleanup_callback() - - listener = hass.data[DOMAIN][DATA_LISTENER] - listener() - rfx_object = hass.data[DOMAIN][DATA_RFXOBJECT] await hass.async_add_executor_job(rfx_object.close_connection) @@ -160,6 +152,7 @@ async def async_setup_internal(hass, entry: config_entries.ConfigEntry): # Setup some per device config devices = _get_device_lookup(config[CONF_DEVICES]) + pt2262_devices: list[str] = [] device_registry: DeviceRegistry = ( await hass.helpers.device_registry.async_get_registry() @@ -193,6 +186,10 @@ async def async_setup_internal(hass, entry: config_entries.ConfigEntry): else: return + if event.device.packettype == DEVICE_PACKET_TYPE_LIGHTING4: + find_possible_pt2262_device(pt2262_devices, event.device.id_string) + pt2262_devices.append(event.device.id_string) + device_entry = device_registry.async_get_device( identifiers={(DOMAIN, *device_id)}, ) @@ -211,6 +208,14 @@ async def async_setup_internal(hass, entry: config_entries.ConfigEntry): config = {} config[CONF_DEVICE_ID] = device_id + _LOGGER.info( + "Added device (Device ID: %s Class: %s Sub: %s, Event: %s)", + event.device.id_string.lower(), + event.device.__class__.__name__, + event.device.subtype, + "".join(f"{x:02x}" for x in event.data), + ) + data = entry.data.copy() data[CONF_DEVICES] = copy.deepcopy(entry.data[CONF_DEVICES]) event_code = binascii.hexlify(event.data).decode("ASCII") @@ -222,9 +227,9 @@ async def async_setup_internal(hass, entry: config_entries.ConfigEntry): """Close connection with RFXtrx.""" rfx_object.close_connection() - listener = hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown_rfxtrx) - - hass.data[DOMAIN][DATA_LISTENER] = listener + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown_rfxtrx) + ) hass.data[DOMAIN][DATA_RFXOBJECT] = rfx_object rfx_object.event_callback = lambda event: hass.add_job(async_handle_receive, event) @@ -236,25 +241,66 @@ async def async_setup_internal(hass, entry: config_entries.ConfigEntry): hass.services.async_register(DOMAIN, SERVICE_SEND, send, schema=SERVICE_SEND_SCHEMA) +async def async_setup_platform_entry( + hass: HomeAssistant, + config_entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, + supported: Callable[[rfxtrxmod.RFXtrxEvent], bool], + constructor: Callable[ + [rfxtrxmod.RFXtrxEvent, rfxtrxmod.RFXtrxEvent | None, DeviceTuple, dict], + list[Entity], + ], +): + """Set up config entry.""" + entry_data = config_entry.data + device_ids: set[DeviceTuple] = set() + + # Add entities from config + entities = [] + for packet_id, entity_info in entry_data[CONF_DEVICES].items(): + if (event := get_rfx_object(packet_id)) is None: + _LOGGER.error("Invalid device: %s", packet_id) + continue + if not supported(event): + continue + + device_id = get_device_id( + event.device, data_bits=entity_info.get(CONF_DATA_BITS) + ) + if device_id in device_ids: + continue + device_ids.add(device_id) + + entities.extend(constructor(event, None, device_id, entity_info)) + + async_add_entities(entities) + + # If automatic add is on, hookup listener + if entry_data[CONF_AUTOMATIC_ADD]: + + @callback + def _update(event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple): + """Handle light updates from the RFXtrx gateway.""" + if not supported(event): + return + + if device_id in device_ids: + return + device_ids.add(device_id) + async_add_entities(constructor(event, event, device_id, {})) + + config_entry.async_on_unload( + hass.helpers.dispatcher.async_dispatcher_connect(SIGNAL_EVENT, _update) + ) + + def get_rfx_object(packetid: str) -> rfxtrxmod.RFXtrxEvent | None: """Return the RFXObject with the packetid.""" try: binarypacket = bytearray.fromhex(packetid) except ValueError: return None - - pkt = rfxtrxmod.lowlevel.parse(binarypacket) - if pkt is None: - return None - if isinstance(pkt, rfxtrxmod.lowlevel.SensorPacket): - obj = rfxtrxmod.SensorEvent(pkt) - elif isinstance(pkt, rfxtrxmod.lowlevel.Status): - obj = rfxtrxmod.StatusEvent(pkt) - else: - obj = rfxtrxmod.ControlEvent(pkt) - - obj.data = binarypacket - return obj + return rfxtrxmod.RFXtrxTransport.parse(binarypacket) def get_pt2262_deviceid(device_id: str, nb_data_bits: int | None) -> bytes | None: @@ -341,14 +387,6 @@ def get_device_id( return DeviceTuple(f"{device.packettype:x}", f"{device.subtype:x}", id_string) -def connect_auto_add(hass, entry_data, callback_fun): - """Connect to dispatcher for automatic add.""" - if entry_data[CONF_AUTOMATIC_ADD]: - hass.data[DOMAIN][DATA_CLEANUP_CALLBACKS].append( - hass.helpers.dispatcher.async_dispatcher_connect(SIGNAL_EVENT, callback_fun) - ) - - class RfxtrxEntity(RestoreEntity): """Represents a Rfxtrx device. diff --git a/homeassistant/components/rfxtrx/binary_sensor.py b/homeassistant/components/rfxtrx/binary_sensor.py index 10342fd826a..dfc8d25ff5e 100644 --- a/homeassistant/components/rfxtrx/binary_sensor.py +++ b/homeassistant/components/rfxtrx/binary_sensor.py @@ -10,24 +10,11 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.const import ( - CONF_COMMAND_OFF, - CONF_COMMAND_ON, - CONF_DEVICES, - STATE_ON, -) +from homeassistant.const import CONF_COMMAND_OFF, CONF_COMMAND_ON, STATE_ON from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.helpers import event as evt -from . import ( - DeviceTuple, - RfxtrxEntity, - connect_auto_add, - find_possible_pt2262_device, - get_device_id, - get_pt2262_cmd, - get_rfx_object, -) +from . import DeviceTuple, RfxtrxEntity, async_setup_platform_entry, get_pt2262_cmd from .const import ( COMMAND_OFF_LIST, COMMAND_ON_LIST, @@ -100,82 +87,36 @@ async def async_setup_entry( config_entry, async_add_entities, ): - """Set up platform.""" - sensors = [] - - device_ids: set[DeviceTuple] = set() - pt2262_devices: list[str] = [] - - discovery_info = config_entry.data + """Set up config entry.""" def get_sensor_description(type_string: str): if (description := SENSOR_TYPES_DICT.get(type_string)) is None: return BinarySensorEntityDescription(key=type_string) return description - for packet_id, entity_info in discovery_info[CONF_DEVICES].items(): - if (event := get_rfx_object(packet_id)) is None: - _LOGGER.error("Invalid device: %s", packet_id) - continue - if not supported(event): - continue + def _constructor( + event: rfxtrxmod.RFXtrxEvent, + auto: bool, + device_id: DeviceTuple, + entity_info: dict, + ): - device_id = get_device_id( - event.device, data_bits=entity_info.get(CONF_DATA_BITS) - ) - if device_id in device_ids: - continue - device_ids.add(device_id) + return [ + RfxtrxBinarySensor( + event.device, + device_id, + get_sensor_description(event.device.type_string), + entity_info.get(CONF_OFF_DELAY), + entity_info.get(CONF_DATA_BITS), + entity_info.get(CONF_COMMAND_ON), + entity_info.get(CONF_COMMAND_OFF), + event=event if auto else None, + ) + ] - device: rfxtrxmod.RFXtrxDevice = event.device - - if device.packettype == DEVICE_PACKET_TYPE_LIGHTING4: - find_possible_pt2262_device(pt2262_devices, device.id_string) - pt2262_devices.append(device.id_string) - - entity = RfxtrxBinarySensor( - device, - device_id, - get_sensor_description(device.type_string), - entity_info.get(CONF_OFF_DELAY), - entity_info.get(CONF_DATA_BITS), - entity_info.get(CONF_COMMAND_ON), - entity_info.get(CONF_COMMAND_OFF), - ) - sensors.append(entity) - - async_add_entities(sensors) - - @callback - def binary_sensor_update( - event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple - ) -> None: - """Call for control updates from the RFXtrx gateway.""" - if not supported(event): - return - - if device_id in device_ids: - return - device_ids.add(device_id) - - _LOGGER.info( - "Added binary sensor (Device ID: %s Class: %s Sub: %s Event: %s)", - event.device.id_string.lower(), - event.device.__class__.__name__, - event.device.subtype, - "".join(f"{x:02x}" for x in event.data), - ) - - sensor = RfxtrxBinarySensor( - event.device, - device_id, - event=event, - entity_description=get_sensor_description(event.device.type_string), - ) - async_add_entities([sensor]) - - # Subscribe to main RFXtrx events - connect_auto_add(hass, discovery_info, binary_sensor_update) + await async_setup_platform_entry( + hass, config_entry, async_add_entities, supported, _constructor + ) class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): diff --git a/homeassistant/components/rfxtrx/const.py b/homeassistant/components/rfxtrx/const.py index 20f6fd75dc2..b7cb52df984 100644 --- a/homeassistant/components/rfxtrx/const.py +++ b/homeassistant/components/rfxtrx/const.py @@ -45,5 +45,3 @@ DEVICE_PACKET_TYPE_LIGHTING4 = 0x13 EVENT_RFXTRX_EVENT = "rfxtrx_event" DATA_RFXOBJECT = "rfxobject" -DATA_LISTENER = "ha_stop" -DATA_CLEANUP_CALLBACKS = "cleanup_callbacks" diff --git a/homeassistant/components/rfxtrx/cover.py b/homeassistant/components/rfxtrx/cover.py index c1c009c930c..c8920ccf97b 100644 --- a/homeassistant/components/rfxtrx/cover.py +++ b/homeassistant/components/rfxtrx/cover.py @@ -14,21 +14,18 @@ from homeassistant.components.cover import ( SUPPORT_STOP_TILT, CoverEntity, ) -from homeassistant.const import CONF_DEVICES, STATE_OPEN +from homeassistant.const import STATE_OPEN from homeassistant.core import callback from . import ( DEFAULT_SIGNAL_REPETITIONS, DeviceTuple, RfxtrxCommandEntity, - connect_auto_add, - get_device_id, - get_rfx_object, + async_setup_platform_entry, ) from .const import ( COMMAND_OFF_LIST, COMMAND_ON_LIST, - CONF_DATA_BITS, CONF_SIGNAL_REPETITIONS, CONF_VENETIAN_BLIND_MODE, CONST_VENETIAN_BLIND_MODE_EU, @@ -49,60 +46,26 @@ async def async_setup_entry( async_add_entities, ): """Set up config entry.""" - discovery_info = config_entry.data - device_ids: set[DeviceTuple] = set() - entities = [] - for packet_id, entity_info in discovery_info[CONF_DEVICES].items(): - if (event := get_rfx_object(packet_id)) is None: - _LOGGER.error("Invalid device: %s", packet_id) - continue - if not supported(event): - continue + def _constructor( + event: rfxtrxmod.RFXtrxEvent, + auto: bool, + device_id: DeviceTuple, + entity_info: dict, + ): + return [ + RfxtrxCover( + event.device, + device_id, + entity_info.get(CONF_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS), + venetian_blind_mode=entity_info.get(CONF_VENETIAN_BLIND_MODE), + event=event if auto else None, + ) + ] - device_id = get_device_id( - event.device, data_bits=entity_info.get(CONF_DATA_BITS) - ) - if device_id in device_ids: - continue - device_ids.add(device_id) - - entity = RfxtrxCover( - event.device, - device_id, - signal_repetitions=entity_info.get(CONF_SIGNAL_REPETITIONS, 1), - venetian_blind_mode=entity_info.get(CONF_VENETIAN_BLIND_MODE), - ) - entities.append(entity) - - async_add_entities(entities) - - @callback - def cover_update(event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple) -> None: - """Handle cover updates from the RFXtrx gateway.""" - if not supported(event): - return - - if device_id in device_ids: - return - device_ids.add(device_id) - device: rfxtrxmod.RFXtrxDevice = event.device - - _LOGGER.info( - "Added cover (Device ID: %s Class: %s Sub: %s, Event: %s)", - device.id_string.lower(), - device.__class__.__name__, - device.subtype, - "".join(f"{x:02x}" for x in event.data), - ) - - entity = RfxtrxCover( - event.device, device_id, DEFAULT_SIGNAL_REPETITIONS, event=event - ) - async_add_entities([entity]) - - # Subscribe to main RFXtrx events - connect_auto_add(hass, discovery_info, cover_update) + await async_setup_platform_entry( + hass, config_entry, async_add_entities, supported, _constructor + ) class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): diff --git a/homeassistant/components/rfxtrx/light.py b/homeassistant/components/rfxtrx/light.py index bf88ff86368..981dca37e57 100644 --- a/homeassistant/components/rfxtrx/light.py +++ b/homeassistant/components/rfxtrx/light.py @@ -10,23 +10,16 @@ from homeassistant.components.light import ( SUPPORT_BRIGHTNESS, LightEntity, ) -from homeassistant.const import CONF_DEVICES, STATE_ON +from homeassistant.const import STATE_ON from homeassistant.core import callback from . import ( DEFAULT_SIGNAL_REPETITIONS, DeviceTuple, RfxtrxCommandEntity, - connect_auto_add, - get_device_id, - get_rfx_object, -) -from .const import ( - COMMAND_OFF_LIST, - COMMAND_ON_LIST, - CONF_DATA_BITS, - CONF_SIGNAL_REPETITIONS, + async_setup_platform_entry, ) +from .const import COMMAND_OFF_LIST, COMMAND_ON_LIST, CONF_SIGNAL_REPETITIONS _LOGGER = logging.getLogger(__name__) @@ -47,59 +40,25 @@ async def async_setup_entry( async_add_entities, ): """Set up config entry.""" - discovery_info = config_entry.data - device_ids: set[DeviceTuple] = set() - # Add switch from config file - entities = [] - for packet_id, entity_info in discovery_info[CONF_DEVICES].items(): - if (event := get_rfx_object(packet_id)) is None: - _LOGGER.error("Invalid device: %s", packet_id) - continue - if not supported(event): - continue + def _constructor( + event: rfxtrxmod.RFXtrxEvent, + auto: bool, + device_id: DeviceTuple, + entity_info: dict, + ): + return [ + RfxtrxLight( + event.device, + device_id, + entity_info.get(CONF_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS), + event=event if auto else None, + ) + ] - device_id = get_device_id( - event.device, data_bits=entity_info.get(CONF_DATA_BITS) - ) - if device_id in device_ids: - continue - device_ids.add(device_id) - - entity = RfxtrxLight( - event.device, device_id, entity_info.get(CONF_SIGNAL_REPETITIONS, 1) - ) - - entities.append(entity) - - async_add_entities(entities) - - @callback - def light_update(event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple): - """Handle light updates from the RFXtrx gateway.""" - if not supported(event): - return - - if device_id in device_ids: - return - device_ids.add(device_id) - - _LOGGER.info( - "Added light (Device ID: %s Class: %s Sub: %s, Event: %s)", - event.device.id_string.lower(), - event.device.__class__.__name__, - event.device.subtype, - "".join(f"{x:02x}" for x in event.data), - ) - - entity = RfxtrxLight( - event.device, device_id, DEFAULT_SIGNAL_REPETITIONS, event=event - ) - - async_add_entities([entity]) - - # Subscribe to main RFXtrx events - connect_auto_add(hass, discovery_info, light_update) + await async_setup_platform_entry( + hass, config_entry, async_add_entities, supported, _constructor + ) class RfxtrxLight(RfxtrxCommandEntity, LightEntity): diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index bf292adeee8..623730f1c91 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -5,7 +5,7 @@ from collections.abc import Callable from dataclasses import dataclass import logging -from RFXtrx import ControlEvent, SensorEvent +from RFXtrx import ControlEvent, RFXtrxEvent, SensorEvent from homeassistant.components.sensor import ( SensorDeviceClass, @@ -14,7 +14,6 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.const import ( - CONF_DEVICES, DEGREE, ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, @@ -32,13 +31,7 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.helpers.entity import EntityCategory -from . import ( - CONF_DATA_BITS, - RfxtrxEntity, - connect_auto_add, - get_device_id, - get_rfx_object, -) +from . import DeviceTuple, RfxtrxEntity, async_setup_platform_entry, get_rfx_object from .const import ATTR_EVENT _LOGGER = logging.getLogger(__name__) @@ -219,62 +212,33 @@ async def async_setup_entry( config_entry, async_add_entities, ): - """Set up platform.""" - discovery_info = config_entry.data - data_ids = set() + """Set up config entry.""" - def supported(event): + def _supported(event): return isinstance(event, (ControlEvent, SensorEvent)) - entities = [] - for packet_id, entity_info in discovery_info[CONF_DEVICES].items(): - if (event := get_rfx_object(packet_id)) is None: - _LOGGER.error("Invalid device: %s", packet_id) - continue - if not supported(event): - continue - - device_id = get_device_id( - event.device, data_bits=entity_info.get(CONF_DATA_BITS) - ) + def _constructor( + event: RFXtrxEvent, + auto: bool, + device_id: DeviceTuple, + entity_info: dict, + ): + entities: list[RfxtrxSensor] = [] for data_type in set(event.values) & set(SENSOR_TYPES_DICT): - data_id = (*device_id, str(data_type)) - if data_id in data_ids: - continue - data_ids.add(data_id) - - entity = RfxtrxSensor(event.device, device_id, SENSOR_TYPES_DICT[data_type]) - entities.append(entity) - - async_add_entities(entities) - - @callback - def sensor_update(event, device_id): - """Handle sensor updates from the RFXtrx gateway.""" - if not supported(event): - return - - for data_type in set(event.values) & set(SENSOR_TYPES_DICT): - data_id = (*device_id, data_type) - if data_id in data_ids: - continue - data_ids.add(data_id) - - _LOGGER.info( - "Added sensor (Device ID: %s Class: %s Sub: %s, Event: %s)", - event.device.id_string.lower(), - event.device.__class__.__name__, - event.device.subtype, - "".join(f"{x:02x}" for x in event.data), + entities.append( + RfxtrxSensor( + event.device, + device_id, + SENSOR_TYPES_DICT[data_type], + event=event if auto else None, + ) ) - entity = RfxtrxSensor( - event.device, device_id, SENSOR_TYPES_DICT[data_type], event=event - ) - async_add_entities([entity]) + return entities - # Subscribe to main RFXtrx events - connect_auto_add(hass, discovery_info, sensor_update) + await async_setup_platform_entry( + hass, config_entry, async_add_entities, _supported, _constructor + ) class RfxtrxSensor(RfxtrxEntity, SensorEntity): diff --git a/homeassistant/components/rfxtrx/switch.py b/homeassistant/components/rfxtrx/switch.py index d0ec83f3c66..b62164a0cf3 100644 --- a/homeassistant/components/rfxtrx/switch.py +++ b/homeassistant/components/rfxtrx/switch.py @@ -6,7 +6,7 @@ import logging import RFXtrx as rfxtrxmod from homeassistant.components.switch import SwitchEntity -from homeassistant.const import CONF_DEVICES, STATE_ON +from homeassistant.const import STATE_ON from homeassistant.core import callback from . import ( @@ -14,16 +14,9 @@ from . import ( DOMAIN, DeviceTuple, RfxtrxCommandEntity, - connect_auto_add, - get_device_id, - get_rfx_object, -) -from .const import ( - COMMAND_OFF_LIST, - COMMAND_ON_LIST, - CONF_DATA_BITS, - CONF_SIGNAL_REPETITIONS, + async_setup_platform_entry, ) +from .const import COMMAND_OFF_LIST, COMMAND_ON_LIST, CONF_SIGNAL_REPETITIONS DATA_SWITCH = f"{DOMAIN}_switch" @@ -46,59 +39,25 @@ async def async_setup_entry( async_add_entities, ): """Set up config entry.""" - discovery_info = config_entry.data - device_ids: set[DeviceTuple] = set() - # Add switch from config file - entities = [] - for packet_id, entity_info in discovery_info[CONF_DEVICES].items(): - if (event := get_rfx_object(packet_id)) is None: - _LOGGER.error("Invalid device: %s", packet_id) - continue - if not supported(event): - continue + def _constructor( + event: rfxtrxmod.RFXtrxEvent, + auto: bool, + device_id: DeviceTuple, + entity_info: dict, + ): + return [ + RfxtrxSwitch( + event.device, + device_id, + entity_info.get(CONF_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS), + event=event if auto else None, + ) + ] - device_id = get_device_id( - event.device, data_bits=entity_info.get(CONF_DATA_BITS) - ) - if device_id in device_ids: - continue - device_ids.add(device_id) - - entity = RfxtrxSwitch( - event.device, device_id, entity_info.get(CONF_SIGNAL_REPETITIONS, 1) - ) - entities.append(entity) - - async_add_entities(entities) - - @callback - def switch_update(event, device_id): - """Handle sensor updates from the RFXtrx gateway.""" - if not supported(event): - return - - if device_id in device_ids: - return - device_ids.add(device_id) - - device: rfxtrxmod.RFXtrxDevice = event.device - - _LOGGER.info( - "Added switch (Device ID: %s Class: %s Sub: %s, Event: %s)", - device.id_string.lower(), - device.__class__.__name__, - device.subtype, - "".join(f"{x:02x}" for x in event.data), - ) - - entity = RfxtrxSwitch( - device, device_id, DEFAULT_SIGNAL_REPETITIONS, event=event - ) - async_add_entities([entity]) - - # Subscribe to main RFXtrx events - connect_auto_add(hass, discovery_info, switch_update) + await async_setup_platform_entry( + hass, config_entry, async_add_entities, supported, _constructor + ) class RfxtrxSwitch(RfxtrxCommandEntity, SwitchEntity): From cd452552af1f69d3ae8adaf018fb6882cc2c791c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 23 Dec 2021 00:22:15 +0100 Subject: [PATCH 0965/2644] Use relative imports in sensor (#62638) --- homeassistant/components/sensor/device_condition.py | 3 +-- homeassistant/components/sensor/device_trigger.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index 32fa06eb507..7ba3e907c60 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -6,7 +6,6 @@ import voluptuous as vol from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) -from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import CONF_ABOVE, CONF_BELOW, CONF_ENTITY_ID, CONF_TYPE from homeassistant.core import HomeAssistant, HomeAssistantError, callback from homeassistant.helpers import condition, config_validation as cv @@ -17,7 +16,7 @@ from homeassistant.helpers.entity_registry import ( ) from homeassistant.helpers.typing import ConfigType -from . import DOMAIN +from . import DOMAIN, SensorDeviceClass # mypy: allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index e605ed3b797..1970e8df65b 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -8,7 +8,6 @@ from homeassistant.components.device_automation.exceptions import ( from homeassistant.components.homeassistant.triggers import ( numeric_state as numeric_state_trigger, ) -from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( CONF_ABOVE, CONF_BELOW, @@ -21,7 +20,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import get_device_class, get_unit_of_measurement from homeassistant.helpers.entity_registry import async_entries_for_device -from . import DOMAIN +from . import DOMAIN, SensorDeviceClass # mypy: allow-untyped-defs, no-check-untyped-defs From cb82169e92290213b85f43bcaab7bd2f73bd9399 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Thu, 23 Dec 2021 00:23:22 +0100 Subject: [PATCH 0966/2644] Bump async-upnp-client to 0.23.2 (#62634) --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/components/yeelight/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index ecc3cd4256d..8ddd63ef753 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -3,7 +3,7 @@ "name": "DLNA Digital Media Renderer", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", - "requirements": ["async-upnp-client==0.23.1"], + "requirements": ["async-upnp-client==0.23.2"], "dependencies": ["ssdp"], "ssdp": [ { diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index e95c0e13887..28d41ea8986 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -2,7 +2,7 @@ "domain": "ssdp", "name": "Simple Service Discovery Protocol (SSDP)", "documentation": "https://www.home-assistant.io/integrations/ssdp", - "requirements": ["async-upnp-client==0.23.1"], + "requirements": ["async-upnp-client==0.23.2"], "dependencies": ["network"], "after_dependencies": ["zeroconf"], "codeowners": [], diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 2644a91b20f..045022ccb36 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -3,7 +3,7 @@ "name": "UPnP/IGD", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/upnp", - "requirements": ["async-upnp-client==0.23.1"], + "requirements": ["async-upnp-client==0.23.2"], "dependencies": ["network", "ssdp"], "codeowners": ["@StevenLooman","@ehendrix23"], "ssdp": [ diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 7cd1fe09dba..447a4ae3177 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -2,7 +2,7 @@ "domain": "yeelight", "name": "Yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight", - "requirements": ["yeelight==0.7.8", "async-upnp-client==0.23.1"], + "requirements": ["yeelight==0.7.8", "async-upnp-client==0.23.2"], "codeowners": ["@zewelor", "@shenxn", "@starkillerOG"], "config_flow": true, "dependencies": ["network"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index dcd9d257474..b035cec441a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodiscover==1.4.5 aiohttp==3.8.1 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.23.1 +async-upnp-client==0.23.2 async_timeout==4.0.0 atomicwrites==1.4.0 attrs==21.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index fef25ac0673..b57284a1857 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -341,7 +341,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.23.1 +async-upnp-client==0.23.2 # homeassistant.components.supla asyncpysupla==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d2f8504a6a2..0a9e244bd32 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -240,7 +240,7 @@ arcam-fmj==0.12.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.23.1 +async-upnp-client==0.23.2 # homeassistant.components.aurora auroranoaa==0.0.2 From c5d62ccc7edf99b8da58a3a0f3057c153fee4bf8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 23 Dec 2021 00:23:57 +0100 Subject: [PATCH 0967/2644] Add input_button support to HomeKit (#62590) --- homeassistant/components/homekit/accessories.py | 1 + homeassistant/components/homekit/config_flow.py | 1 + homeassistant/components/homekit/type_switches.py | 6 ++++-- tests/components/homekit/test_get_accessories.py | 1 + tests/components/homekit/test_type_switches.py | 11 +++++++---- 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 6af4b4a8d89..23f546910f1 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -198,6 +198,7 @@ def get_accessory(hass, driver, state, aid, config): # noqa: C901 "automation", "button", "input_boolean", + "input_button", "remote", "scene", "script", diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index f47ecdf5dbb..8d2f17a3878 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -85,6 +85,7 @@ SUPPORTED_DOMAINS = [ "fan", "humidifier", "input_boolean", + "input_button", "input_select", "light", "lock", diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index ec6813a82f1..cd0d4243726 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -12,7 +12,7 @@ from pyhap.const import ( CATEGORY_SWITCH, ) -from homeassistant.components import button +from homeassistant.components import button, input_button from homeassistant.components.input_select import ATTR_OPTIONS, SERVICE_SELECT_OPTION from homeassistant.components.switch import DOMAIN from homeassistant.components.vacuum import ( @@ -70,7 +70,7 @@ VALVE_TYPE: dict[str, ValveInfo] = { } -ACTIVATE_ONLY_SWITCH_DOMAINS = {"button", "scene", "script"} +ACTIVATE_ONLY_SWITCH_DOMAINS = {"button", "input_button", "scene", "script"} ACTIVATE_ONLY_RESET_SECONDS = 10 @@ -152,6 +152,8 @@ class Switch(HomeAccessory): params = {} elif self._domain == button.DOMAIN: service = button.SERVICE_PRESS + elif self._domain == input_button.DOMAIN: + service = input_button.SERVICE_PRESS else: service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 0f843c387ab..31f7b0f3bcc 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -272,6 +272,7 @@ def test_type_sensors(type_name, entity_id, state, attrs): ("Switch", "automation.test", "on", {}, {}), ("Switch", "button.test", STATE_UNKNOWN, {}, {}), ("Switch", "input_boolean.test", "on", {}, {}), + ("Switch", "input_button.test", STATE_UNKNOWN, {}, {}), ("Switch", "remote.test", "on", {}, {}), ("Switch", "scene.test", "on", {}, {}), ("Switch", "script.test", "on", {}, {}), diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index 54eae42ca1d..c1340e1d34e 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -451,10 +451,13 @@ async def test_input_select_switch(hass, hk_driver, events, domain): assert acc.select_chars["option3"].value is False -async def test_button_switch(hass, hk_driver, events): - """Test switch accessory from a button entity.""" - domain = "button" - entity_id = "button.test" +@pytest.mark.parametrize( + "domain", + ["button", "input_button"], +) +async def test_button_switch(hass, hk_driver, events, domain): + """Test switch accessory from a (input) button entity.""" + entity_id = f"{domain}.test" hass.states.async_set(entity_id, None) await hass.async_block_till_done() From 87d4420a720da2b9e0dbeb260858d183b81ba49b Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 23 Dec 2021 00:14:21 +0000 Subject: [PATCH 0968/2644] [ci skip] Translation update --- .../components/adax/translations/it.json | 2 +- .../components/androidtv/translations/de.json | 66 +++++++++++++++++++ .../components/androidtv/translations/id.json | 4 +- .../components/androidtv/translations/tr.json | 66 +++++++++++++++++++ .../aurora_abb_powerone/translations/it.json | 2 +- .../azure_devops/translations/it.json | 2 +- .../components/bosch_shc/translations/it.json | 2 +- .../components/braviatv/translations/it.json | 4 +- .../components/broadlink/translations/it.json | 2 +- .../crownstone/translations/it.json | 2 +- .../components/deconz/translations/it.json | 2 +- .../dialogflow/translations/it.json | 2 +- .../components/elkm1/translations/it.json | 2 +- .../evil_genius_labs/translations/id.json | 1 + .../evil_genius_labs/translations/tr.json | 1 + .../forecast_solar/translations/it.json | 2 +- .../components/geofency/translations/it.json | 2 +- .../components/glances/translations/it.json | 2 +- .../google_travel_time/translations/it.json | 2 +- .../components/gpslogger/translations/it.json | 2 +- .../components/hive/translations/it.json | 2 +- .../components/homekit/translations/it.json | 4 +- .../homekit_controller/translations/it.json | 2 +- .../components/knx/translations/it.json | 4 +- .../components/konnected/translations/it.json | 2 +- .../components/locative/translations/it.json | 2 +- .../components/mailgun/translations/it.json | 2 +- .../components/nut/translations/it.json | 6 +- .../components/plaato/translations/it.json | 2 +- .../components/plugwise/translations/it.json | 2 +- .../components/point/translations/it.json | 2 +- .../components/ps4/translations/it.json | 6 +- .../screenlogic/translations/it.json | 2 +- .../components/sensibo/translations/en.json | 28 ++++---- .../components/sensibo/translations/et.json | 18 +++++ .../components/smappee/translations/it.json | 4 +- .../smartthings/translations/it.json | 2 +- .../system_bridge/translations/it.json | 2 +- .../components/tado/translations/it.json | 2 +- .../tellduslive/translations/it.json | 2 +- .../components/tile/translations/de.json | 9 ++- .../components/tile/translations/tr.json | 9 ++- .../components/toon/translations/it.json | 2 +- .../components/traccar/translations/it.json | 2 +- .../transmission/translations/it.json | 2 +- .../components/tuya/translations/it.json | 2 +- .../components/twilio/translations/it.json | 2 +- .../components/upb/translations/it.json | 2 +- .../components/vera/translations/it.json | 2 +- .../components/vizio/translations/it.json | 2 +- .../xiaomi_aqara/translations/it.json | 2 +- .../components/yeelight/translations/it.json | 2 +- 52 files changed, 236 insertions(+), 68 deletions(-) create mode 100644 homeassistant/components/androidtv/translations/de.json create mode 100644 homeassistant/components/androidtv/translations/tr.json create mode 100644 homeassistant/components/sensibo/translations/et.json diff --git a/homeassistant/components/adax/translations/it.json b/homeassistant/components/adax/translations/it.json index 0ec4566e73b..17095fab12b 100644 --- a/homeassistant/components/adax/translations/it.json +++ b/homeassistant/components/adax/translations/it.json @@ -22,7 +22,7 @@ "wifi_pswd": "Password Wi-Fi", "wifi_ssid": "SSID Wi-Fi" }, - "description": "Ripristina il riscaldatore premendo + e OK finch\u00e9 il display non mostra 'Reset'. Quindi premere e tenere premuto il pulsante OK sul riscaldatore fino a quando il led blu inizia a lampeggiare prima di premere Invia. La configurazione del riscaldatore potrebbe richiedere alcuni minuti." + "description": "Ripristina il riscaldatore premendo + e OK finch\u00e9 il display non mostra 'Reset'. Quindi premi e tieni premuto il pulsante OK sul riscaldatore fino a quando il led blu inizia a lampeggiare prima di premere Invia. La configurazione del riscaldatore potrebbe richiedere alcuni minuti." }, "user": { "data": { diff --git a/homeassistant/components/androidtv/translations/de.json b/homeassistant/components/androidtv/translations/de.json new file mode 100644 index 00000000000..bf07b6fee80 --- /dev/null +++ b/homeassistant/components/androidtv/translations/de.json @@ -0,0 +1,66 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "invalid_unique_id": "Unm\u00f6glich, eine g\u00fcltige eindeutige Kennung f\u00fcr das Ger\u00e4t zu ermitteln" + }, + "error": { + "adbkey_not_file": "ADB-Schl\u00fcsseldatei nicht gefunden", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_host": "Ung\u00fcltiger Hostname oder IP-Adresse", + "key_and_server": "Nur ADB-Schl\u00fcssel oder ADB-Server bereitstellen", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "adb_server_ip": "IP-Adresse des ADB-Servers (leer lassen, um sie nicht zu verwenden)", + "adb_server_port": "Port des ADB-Servers", + "adbkey": "Pfad zu deiner ADB-Schl\u00fcsseldatei (zum automatischen Generieren leer lassen)", + "device_class": "Der Typ des Ger\u00e4ts", + "host": "Host", + "port": "Port" + }, + "description": "Stelle die erforderlichen Parameter f\u00fcr die Verbindung mit deinem Android TV-Ger\u00e4t ein", + "title": "Android TV" + } + } + }, + "options": { + "error": { + "invalid_det_rules": "Ung\u00fcltige Statuserkennungsregeln" + }, + "step": { + "apps": { + "data": { + "app_delete": "Aktiviere diese Option, um diese Anwendung zu l\u00f6schen", + "app_id": "Anwendungs-ID", + "app_name": "Anwendungsname" + }, + "description": "Anwendungs-ID {app_id} konfigurieren", + "title": "Android TV-Apps konfigurieren" + }, + "init": { + "data": { + "apps": "Anwendungsliste konfigurieren", + "exclude_unnamed_apps": "App mit unbekanntem Namen ausschlie\u00dfen", + "get_sources": "Ob die laufenden Apps als Liste der Quellen abgerufen werden sollen oder nicht", + "screencap": "Legt fest, ob Albumcover von der Bildschirmanzeige \u00fcbernommen werden sollen", + "state_detection_rules": "Regeln zur Statuserkennung konfigurieren", + "turn_off_command": "ADB-Shell-Befehl zum \u00dcberschreiben des Standardbefehls turn_off", + "turn_on_command": "ADB-Shell-Befehl zum \u00dcberschreiben des Standardbefehls turn_on" + }, + "title": "Android TV-Optionen" + }, + "rules": { + "data": { + "rule_delete": "Aktiviere diese Option, um diese Regel zu l\u00f6schen", + "rule_id": "Anwendungs-ID", + "rule_values": "Liste der Statuserkennungsregeln (siehe Dokumentation)" + }, + "description": "Erkennungsregel f\u00fcr Anwendungs-ID {rule_id}", + "title": "Regeln f\u00fcr die Android TV-Zustandserkennung konfigurieren" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/androidtv/translations/id.json b/homeassistant/components/androidtv/translations/id.json index d85048243ae..86b34f3a3e5 100644 --- a/homeassistant/components/androidtv/translations/id.json +++ b/homeassistant/components/androidtv/translations/id.json @@ -1,9 +1,11 @@ { "config": { "abort": { - "already_configured": "Perangkat sudah dikonfigurasi" + "already_configured": "Perangkat sudah dikonfigurasi", + "invalid_unique_id": "Penentuan ID unik yang valid untuk perangkat tidak dimungkinkan" }, "error": { + "adbkey_not_file": "File kunci ADB tidak ditemukan", "cannot_connect": "Gagal terhubung", "invalid_host": "Nama host atau alamat IP tidak valid", "unknown": "Kesalahan yang tidak diharapkan" diff --git a/homeassistant/components/androidtv/translations/tr.json b/homeassistant/components/androidtv/translations/tr.json new file mode 100644 index 00000000000..f139082ce36 --- /dev/null +++ b/homeassistant/components/androidtv/translations/tr.json @@ -0,0 +1,66 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "invalid_unique_id": "Cihaz i\u00e7in ge\u00e7erli bir benzersiz kimlik belirlemek imkans\u0131z" + }, + "error": { + "adbkey_not_file": "ADB anahtar dosyas\u0131 bulunamad\u0131", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_host": "Ge\u00e7ersiz ana bilgisayar ad\u0131 veya IP adresi", + "key_and_server": "Yaln\u0131zca ADB Anahtar\u0131 veya ADB Sunucusu sa\u011flay\u0131n", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "adb_server_ip": "ADB sunucusunun IP adresi (kullanmamak i\u00e7in bo\u015f b\u0131rak\u0131n)", + "adb_server_port": "ADB sunucusunun ba\u011flant\u0131 noktas\u0131", + "adbkey": "ADB anahtar dosyan\u0131z\u0131n yolu (otomatik olu\u015fturmak i\u00e7in bo\u015f b\u0131rak\u0131n)", + "device_class": "Cihaz\u0131n t\u00fcr\u00fc", + "host": "Sunucu", + "port": "Port" + }, + "description": "Android TV cihaz\u0131n\u0131za ba\u011flanmak i\u00e7in gerekli parametreleri ayarlay\u0131n", + "title": "Android TV" + } + } + }, + "options": { + "error": { + "invalid_det_rules": "Ge\u00e7ersiz durum alg\u0131lama kurallar\u0131" + }, + "step": { + "apps": { + "data": { + "app_delete": "Bu uygulamay\u0131 silmek i\u00e7in i\u015faretleyin", + "app_id": "Uygulama Kimli\u011fi", + "app_name": "Uygulama Ad\u0131" + }, + "description": "{app_id} uygulama kimli\u011fini yap\u0131land\u0131r\u0131n", + "title": "Android TV Uygulamalar\u0131n\u0131 Yap\u0131land\u0131r\u0131n" + }, + "init": { + "data": { + "apps": "Uygulamalar listesini yap\u0131land\u0131r", + "exclude_unnamed_apps": "Bilinmeyen ada sahip uygulamay\u0131 hari\u00e7 tut", + "get_sources": "\u00c7al\u0131\u015fan uygulamalar\u0131n kaynak listesi olarak al\u0131n\u0131p al\u0131nmayaca\u011f\u0131", + "screencap": "Alb\u00fcm resminin ekranda g\u00f6sterilenden \u00e7ekilmesi gerekip gerekmedi\u011fini belirler", + "state_detection_rules": "Durum alg\u0131lama kurallar\u0131n\u0131 yap\u0131land\u0131r\u0131n", + "turn_off_command": "Varsay\u0131lan turn_off komutunu ge\u00e7ersiz k\u0131lmak i\u00e7in ADB kabuk komutu", + "turn_on_command": "Varsay\u0131lan turn_on komutunu ge\u00e7ersiz k\u0131lmak i\u00e7in ADB kabuk komutu" + }, + "title": "Android TV Se\u00e7enekleri" + }, + "rules": { + "data": { + "rule_delete": "Bu kural\u0131 silmek i\u00e7in i\u015faretleyin", + "rule_id": "Uygulama Kimli\u011fi", + "rule_values": "Durum alg\u0131lama kurallar\u0131n\u0131n listesi (belgelere bak\u0131n)" + }, + "description": "{rule_id} uygulama kimli\u011fi i\u00e7in alg\u0131lama kural\u0131n\u0131 yap\u0131land\u0131r\u0131n", + "title": "Android TV durum alg\u0131lama kurallar\u0131n\u0131 yap\u0131land\u0131r\u0131n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aurora_abb_powerone/translations/it.json b/homeassistant/components/aurora_abb_powerone/translations/it.json index a16c655d282..bb534d079cc 100644 --- a/homeassistant/components/aurora_abb_powerone/translations/it.json +++ b/homeassistant/components/aurora_abb_powerone/translations/it.json @@ -16,7 +16,7 @@ "address": "Indirizzo dell'inverter", "port": "Porta adattatore RS485 o USB-RS485" }, - "description": "L'inverter deve essere collegato tramite un adattatore RS485, selezionare la porta seriale e l'indirizzo dell'inverter come configurato sul pannello LCD" + "description": "L'inverter deve essere collegato tramite un adattatore RS485, seleziona la porta seriale e l'indirizzo dell'inverter come configurato sul pannello LCD" } } } diff --git a/homeassistant/components/azure_devops/translations/it.json b/homeassistant/components/azure_devops/translations/it.json index a39cad3ce8e..232264d1026 100644 --- a/homeassistant/components/azure_devops/translations/it.json +++ b/homeassistant/components/azure_devops/translations/it.json @@ -15,7 +15,7 @@ "data": { "personal_access_token": "Token di accesso personale (PAT)" }, - "description": "Autenticazione non riuscita per {project_url}. Si prega di inserire le proprie credenziali attuali.", + "description": "Autenticazione non riuscita per {project_url}. Digita le tue credenziali attuali.", "title": "Nuova autenticazione" }, "user": { diff --git a/homeassistant/components/bosch_shc/translations/it.json b/homeassistant/components/bosch_shc/translations/it.json index 80acf92440d..2c64beee59c 100644 --- a/homeassistant/components/bosch_shc/translations/it.json +++ b/homeassistant/components/bosch_shc/translations/it.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida", - "pairing_failed": "Associazione fallita; verificare che il controller Bosch Smart Home sia in modalit\u00e0 di associazione (LED lampeggiante) e che la password sia corretta.", + "pairing_failed": "Associazione non riuscita; verifica che il controller Bosch Smart Home sia in modalit\u00e0 di associazione (LED lampeggiante) e che la password sia corretta.", "session_error": "Errore di sessione: l'API restituisce il risultato Non-OK.", "unknown": "Errore imprevisto" }, diff --git a/homeassistant/components/braviatv/translations/it.json b/homeassistant/components/braviatv/translations/it.json index 3e5f0a7aa07..bbd02157496 100644 --- a/homeassistant/components/braviatv/translations/it.json +++ b/homeassistant/components/braviatv/translations/it.json @@ -14,14 +14,14 @@ "data": { "pin": "Codice PIN" }, - "description": "Immettere il codice PIN visualizzato sul Sony Bravia TV. \n\nSe il codice PIN non viene visualizzato, \u00e8 necessario annullare la registrazione di Home Assistant sul televisore, vai su: Impostazioni - > Rete - > Impostazioni dispositivo remoto - > Annulla registrazione dispositivo remoto.", + "description": "Immetti il codice PIN visualizzato sul Sony Bravia TV. \n\nSe il codice PIN non viene visualizzato, devi annullare la registrazione di Home Assistant sul televisore, vai su: Impostazioni - > Rete - > Impostazioni dispositivo remoto - > Annulla registrazione dispositivo remoto.", "title": "Autorizza Sony Bravia TV" }, "user": { "data": { "host": "Host" }, - "description": "Configura l'integrazione TV di Sony Bravia. In caso di problemi con la configurazione visitare: https://www.home-assistant.io/integrations/braviatv\n\nAssicurarsi che il televisore sia acceso.", + "description": "Configura l'integrazione TV di Sony Bravia. In caso di problemi con la configurazione visita: https://www.home-assistant.io/integrations/braviatv\n\nAssicurati che il televisore sia acceso.", "title": "Sony Bravia TV" } } diff --git a/homeassistant/components/broadlink/translations/it.json b/homeassistant/components/broadlink/translations/it.json index fb3d7b8583e..e0056efceb7 100644 --- a/homeassistant/components/broadlink/translations/it.json +++ b/homeassistant/components/broadlink/translations/it.json @@ -22,7 +22,7 @@ "data": { "name": "Nome" }, - "title": "Scegliere un nome per il dispositivo" + "title": "Scegli un nome per il dispositivo" }, "reset": { "description": "{name} ( {model} su {host} ) \u00e8 bloccato. Devi sbloccare il dispositivo per autenticarti e completare la configurazione. Istruzioni:\n 1. Apri l'app Broadlink.\n 2. Fai clic sul dispositivo.\n 3. Fai clic su \"...\" in alto a destra.\n 4. Scorri fino in fondo alla pagina.\n 5. Disabilita il blocco.", diff --git a/homeassistant/components/crownstone/translations/it.json b/homeassistant/components/crownstone/translations/it.json index ee267a46004..062f77c9349 100644 --- a/homeassistant/components/crownstone/translations/it.json +++ b/homeassistant/components/crownstone/translations/it.json @@ -74,7 +74,7 @@ "data": { "usb_manual_path": "Percorso del dispositivo USB" }, - "description": "Immettere manualmente il percorso di una chiavetta USB Crownstone.", + "description": "Immetti manualmente il percorso di una chiavetta USB Crownstone.", "title": "Percorso manuale della chiavetta USB Crownstone" }, "usb_sphere_config": { diff --git a/homeassistant/components/deconz/translations/it.json b/homeassistant/components/deconz/translations/it.json index e6c423c18b4..61e5e3b5e96 100644 --- a/homeassistant/components/deconz/translations/it.json +++ b/homeassistant/components/deconz/translations/it.json @@ -18,7 +18,7 @@ "title": "Gateway deCONZ Zigbee tramite il componente aggiuntivo di Home Assistant" }, "link": { - "description": "Sblocca il tuo gateway deCONZ per registrarti con Home Assistant.\n\n1. Vai a Impostazioni deCONZ -> Gateway -> Avanzate\n2. Premere il pulsante \"Autentica app\"", + "description": "Sblocca il tuo gateway deCONZ per registrarti con Home Assistant.\n\n1. Vai a Impostazioni deCONZ -> Gateway -> Avanzate\n2. Premi il pulsante \"Autentica app\"", "title": "Collega con deCONZ" }, "manual_input": { diff --git a/homeassistant/components/dialogflow/translations/it.json b/homeassistant/components/dialogflow/translations/it.json index 053cef1d954..b7b04c78863 100644 --- a/homeassistant/components/dialogflow/translations/it.json +++ b/homeassistant/components/dialogflow/translations/it.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "L'istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi webhook." }, "create_entry": { - "default": "Per inviare eventi a Home Assistant, dovrai configurare [l'integrazione webhook di Dialogflow]({dialogflow_url})\n\n Compila le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json \n\n Vedi [la documentazione]({docs_url}) per ulteriori dettagli." + "default": "Per inviare eventi a Home Assistant, dovrai configurare [l'integrazione webhook di Dialogflow]({dialogflow_url})\n\n Compila le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Metodo: POST \n - Tipo di contenuto: application/json \n\n Vedi [la documentazione]({docs_url}) per ulteriori dettagli." }, "step": { "user": { diff --git a/homeassistant/components/elkm1/translations/it.json b/homeassistant/components/elkm1/translations/it.json index 31b250e33fd..b22ba8c8528 100644 --- a/homeassistant/components/elkm1/translations/it.json +++ b/homeassistant/components/elkm1/translations/it.json @@ -19,7 +19,7 @@ "temperature_unit": "L'unit\u00e0 di temperatura utilizzata da ElkM1.", "username": "Nome utente" }, - "description": "La stringa di indirizzi deve essere nella forma \"address[:port]\" per \"secure\" e \"non secure\". Esempio: '192.168.1.1.1'. La porta \u00e8 facoltativa e il valore predefinito \u00e8 2101 per 'non sicuro' e 2601 per 'sicuro'. Per il protocollo seriale, l'indirizzo deve essere nella forma 'tty[:baud]'. Esempio: '/dev/ttyS1'. Il baud \u00e8 opzionale e il valore predefinito \u00e8 115200.", + "description": "La stringa di indirizzi deve essere nella forma 'indirizzo[:porta]' per 'sicuro' e 'non sicuro'. Esempio: '192.168.1.1.1'. La porta \u00e8 facoltativa e il valore predefinito \u00e8 2101 per 'non sicuro' e 2601 per 'sicuro'. Per il protocollo seriale, l'indirizzo deve essere nella forma 'tty[:baud]'. Esempio: '/dev/ttyS1'. Il baud \u00e8 opzionale e il valore predefinito \u00e8 115200.", "title": "Collegamento al controllo Elk-M1" } } diff --git a/homeassistant/components/evil_genius_labs/translations/id.json b/homeassistant/components/evil_genius_labs/translations/id.json index 66c930e348b..8d550418ce2 100644 --- a/homeassistant/components/evil_genius_labs/translations/id.json +++ b/homeassistant/components/evil_genius_labs/translations/id.json @@ -2,6 +2,7 @@ "config": { "error": { "cannot_connect": "Gagal terhubung", + "timeout": "Tenggang waktu membuat koneksi habis", "unknown": "Kesalahan yang tidak diharapkan" }, "step": { diff --git a/homeassistant/components/evil_genius_labs/translations/tr.json b/homeassistant/components/evil_genius_labs/translations/tr.json index 4fc3d5ccab2..46c39ff6b8e 100644 --- a/homeassistant/components/evil_genius_labs/translations/tr.json +++ b/homeassistant/components/evil_genius_labs/translations/tr.json @@ -2,6 +2,7 @@ "config": { "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", + "timeout": "Ba\u011flant\u0131 kurulurken zaman a\u015f\u0131m\u0131", "unknown": "Beklenmeyen hata" }, "step": { diff --git a/homeassistant/components/forecast_solar/translations/it.json b/homeassistant/components/forecast_solar/translations/it.json index 1f7f7677888..7920eee43eb 100644 --- a/homeassistant/components/forecast_solar/translations/it.json +++ b/homeassistant/components/forecast_solar/translations/it.json @@ -24,7 +24,7 @@ "declination": "Declinazione (0 = Orizzontale, 90 = Verticale)", "modules power": "Potenza di picco totale in Watt dei tuoi moduli solari" }, - "description": "Questi valori consentono di modificare il risultato di Solar.Forecast. Fare riferimento alla documentazione se un campo non \u00e8 chiaro." + "description": "Questi valori consentono di modificare il risultato di Solar.Forecast. Fai riferimento alla documentazione se un campo non \u00e8 chiaro." } } } diff --git a/homeassistant/components/geofency/translations/it.json b/homeassistant/components/geofency/translations/it.json index f4049846f56..a72adf25c50 100644 --- a/homeassistant/components/geofency/translations/it.json +++ b/homeassistant/components/geofency/translations/it.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "L'istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi webhook." }, "create_entry": { - "default": "Per inviare eventi a Home Assistant, dovrai configurare la funzionalit\u00e0 webhook in Geofency.\n\n Compila le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Method: POST \n\n Vedi [la documentazione]({docs_url}) per ulteriori dettagli." + "default": "Per inviare eventi a Home Assistant, dovrai configurare la funzionalit\u00e0 webhook in Geofency.\n\n Compila le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Metodo: POST \n\n Vedi [la documentazione]({docs_url}) per ulteriori dettagli." }, "step": { "user": { diff --git a/homeassistant/components/glances/translations/it.json b/homeassistant/components/glances/translations/it.json index f546a43db89..f7af778e17d 100644 --- a/homeassistant/components/glances/translations/it.json +++ b/homeassistant/components/glances/translations/it.json @@ -19,7 +19,7 @@ "verify_ssl": "Verifica il certificato SSL", "version": "Glances API Version (2 o 3)" }, - "title": "Imposta Glances" + "title": "Configura Glances" } } }, diff --git a/homeassistant/components/google_travel_time/translations/it.json b/homeassistant/components/google_travel_time/translations/it.json index 71c0bc442e9..484bed6099e 100644 --- a/homeassistant/components/google_travel_time/translations/it.json +++ b/homeassistant/components/google_travel_time/translations/it.json @@ -31,7 +31,7 @@ "transit_routing_preference": "Preferenza percorso di transito", "units": "Unit\u00e0" }, - "description": "Facoltativamente, \u00e8 possibile specificare un orario di partenza o un orario di arrivo. Se si specifica un orario di partenza, \u00e8 possibile inserire \"now\", un timestamp Unix o una stringa di 24 ore come \"08: 00: 00\". Se si specifica un'ora di arrivo, \u00e8 possibile utilizzare un timestamp Unix o una stringa di 24 ore come \"08: 00: 00\"" + "description": "Facoltativamente, puoi specificare un orario di partenza o un orario di arrivo. Se specifichi un orario di partenza, puoi inserire \"now\", un timestamp Unix o una stringa di 24 ore come \"08: 00: 00\". Se specifichi un'ora di arrivo, puoi utilizzare un timestamp Unix o una stringa di 24 ore come \"08: 00: 00\"" } } }, diff --git a/homeassistant/components/gpslogger/translations/it.json b/homeassistant/components/gpslogger/translations/it.json index 99768a36ff5..7db05dfb8ee 100644 --- a/homeassistant/components/gpslogger/translations/it.json +++ b/homeassistant/components/gpslogger/translations/it.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "L'istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi webhook." }, "create_entry": { - "default": "Per inviare eventi a Home Assistant, dovrai configurare la funzionalit\u00e0 webhook in GPSLogger.\n\n Compila le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Method: POST \n\n Vedi [la documentazione]({docs_url}) per ulteriori dettagli." + "default": "Per inviare eventi a Home Assistant, dovrai configurare la funzionalit\u00e0 webhook in GPSLogger.\n\n Compila le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Metodo: POST \n\n Vedi [la documentazione]({docs_url}) per ulteriori dettagli." }, "step": { "user": { diff --git a/homeassistant/components/hive/translations/it.json b/homeassistant/components/hive/translations/it.json index fd79ca35b79..38edac70cb1 100644 --- a/homeassistant/components/hive/translations/it.json +++ b/homeassistant/components/hive/translations/it.json @@ -34,7 +34,7 @@ "scan_interval": "Intervallo di scansione (secondi)", "username": "Nome utente" }, - "description": "Immettere le informazioni di accesso e la configurazione di Hive.", + "description": "Inserisci le informazioni di accesso e la configurazione di Hive.", "title": "Accesso Hive" } } diff --git a/homeassistant/components/homekit/translations/it.json b/homeassistant/components/homekit/translations/it.json index acb86847a4d..44d274e32e5 100644 --- a/homeassistant/components/homekit/translations/it.json +++ b/homeassistant/components/homekit/translations/it.json @@ -32,7 +32,7 @@ "camera_audio": "Telecamere che supportano l'audio", "camera_copy": "Telecamere che supportano flussi H.264 nativi" }, - "description": "Controllare tutte le telecamere che supportano i flussi H.264 nativi. Se la videocamera non emette uno stream H.264, il sistema provveder\u00e0 a transcodificare il video in H.264 per HomeKit. La transcodifica richiede una CPU performante ed \u00e8 improbabile che funzioni su computer a scheda singola.", + "description": "Controllare tutte le telecamere che supportano i flussi H.264 nativi. Se la videocamera non emette un flusso H.264, il sistema provveder\u00e0 a transcodificare il video in H.264 per HomeKit. La transcodifica richiede una CPU performante ed \u00e8 improbabile che funzioni su computer a scheda singola.", "title": "Configurazione della telecamera" }, "include_exclude": { @@ -40,7 +40,7 @@ "entities": "Entit\u00e0", "mode": "Modalit\u00e0" }, - "description": "Scegliere le entit\u00e0 da includere. In modalit\u00e0 accessorio, \u00e8 inclusa una sola entit\u00e0. In modalit\u00e0 di inclusione bridge, tutte le entit\u00e0 nel dominio saranno incluse, a meno che non siano selezionate entit\u00e0 specifiche. In modalit\u00e0 di esclusione bridge, tutte le entit\u00e0 nel dominio saranno incluse, ad eccezione delle entit\u00e0 escluse. Per prestazioni ottimali, sar\u00e0 creata una HomeKit separata accessoria per ogni lettore multimediale TV, telecomando basato sulle attivit\u00e0, serratura e videocamera.", + "description": "Scegli le entit\u00e0 da includere. In modalit\u00e0 accessorio, \u00e8 inclusa una sola entit\u00e0. In modalit\u00e0 di inclusione bridge, tutte le entit\u00e0 nel dominio saranno incluse, a meno che non siano selezionate entit\u00e0 specifiche. In modalit\u00e0 di esclusione bridge, tutte le entit\u00e0 nel dominio saranno incluse, a eccezione delle entit\u00e0 escluse. Per prestazioni ottimali, sar\u00e0 creata una HomeKit separata accessoria per ogni lettore multimediale TV, telecomando basato sulle attivit\u00e0, serratura e videocamera.", "title": "Seleziona le entit\u00e0 da includere" }, "init": { diff --git a/homeassistant/components/homekit_controller/translations/it.json b/homeassistant/components/homekit_controller/translations/it.json index 7fcd1039cc0..7a50e07126e 100644 --- a/homeassistant/components/homekit_controller/translations/it.json +++ b/homeassistant/components/homekit_controller/translations/it.json @@ -21,7 +21,7 @@ "flow_title": "{name}", "step": { "busy_error": { - "description": "Interrompere l'associazione su tutti i controller o provare a riavviare il dispositivo, quindi continuare a riprendere l'associazione.", + "description": "Interrompi l'associazione su tutti i controller o provare a riavviare il dispositivo, quindi continua a riprendere l'associazione.", "title": "Il dispositivo \u00e8 gi\u00e0 associato a un altro controller" }, "max_tries_error": { diff --git a/homeassistant/components/knx/translations/it.json b/homeassistant/components/knx/translations/it.json index 27053f6c62f..750b3a183fb 100644 --- a/homeassistant/components/knx/translations/it.json +++ b/homeassistant/components/knx/translations/it.json @@ -25,7 +25,7 @@ "multicast_group": "Il gruppo multicast utilizzato per il routing", "multicast_port": "La porta multicast usata per il routing" }, - "description": "Configura le opzioni di routing." + "description": "Configura le opzioni di instradamento." }, "tunnel": { "data": { @@ -37,7 +37,7 @@ "data": { "connection_type": "Tipo di connessione KNX" }, - "description": "Inserisci il tipo di connessione che dovremmo usare per la tua connessione KNX.\n AUTOMATICO - L'integrazione si occupa della connettivit\u00e0 al tuo Bus KNX eseguendo una scansione del gateway.\n TUNNELING - L'integrazione si collegher\u00e0 al bus KNX tramite tunneling.\n ROUTING - L'integrazione si collegher\u00e0 al bus KNX tramite routing." + "description": "Inserisci il tipo di connessione che dovremmo usare per la tua connessione KNX.\n AUTOMATICO - L'integrazione si occupa della connettivit\u00e0 al tuo Bus KNX eseguendo una scansione del gateway.\n TUNNELING - L'integrazione si collegher\u00e0 al bus KNX tramite tunnel.\n ROUTING - L'integrazione si collegher\u00e0 al bus KNX tramite instradamento." } } }, diff --git a/homeassistant/components/konnected/translations/it.json b/homeassistant/components/konnected/translations/it.json index 7692465b2fe..78190aff5b3 100644 --- a/homeassistant/components/konnected/translations/it.json +++ b/homeassistant/components/konnected/translations/it.json @@ -24,7 +24,7 @@ "host": "Indirizzo IP", "port": "Porta" }, - "description": "Si prega di inserire le informazioni dell'host per il tuo Pannello Konnected." + "description": "Inserisci le informazioni dell'host per il tuo pannello Konnected." } } }, diff --git a/homeassistant/components/locative/translations/it.json b/homeassistant/components/locative/translations/it.json index 43ae3ad5412..a9215ea6553 100644 --- a/homeassistant/components/locative/translations/it.json +++ b/homeassistant/components/locative/translations/it.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "L'istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi webhook." }, "create_entry": { - "default": "Per inviare localit\u00e0 a Home Assistant, dovrai configurare la funzionalit\u00e0 Webhook nell'app Locative.\n\n Compila le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Method: POST \n\n Vedi [la documentazione]({docs_url}) per ulteriori dettagli." + "default": "Per inviare localit\u00e0 a Home Assistant, dovrai configurare la funzionalit\u00e0 webhook nell'app Locative.\n\n Compila le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Metodo: POST \n\n Vedi [la documentazione]({docs_url}) per ulteriori dettagli." }, "step": { "user": { diff --git a/homeassistant/components/mailgun/translations/it.json b/homeassistant/components/mailgun/translations/it.json index 70eefc4b858..fdefe8992e8 100644 --- a/homeassistant/components/mailgun/translations/it.json +++ b/homeassistant/components/mailgun/translations/it.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "L'istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi webhook." }, "create_entry": { - "default": "Per inviare eventi a Home Assistant, dovrai configurare [Webhook con Mailgun]({mailgun_url})\n\n Compila le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json\n\n Vedi [la documentazione]({docs_url}) su come configurare le automazioni per gestire i dati in arrivo." + "default": "Per inviare eventi a Home Assistant, dovrai configurare [Webhook con Mailgun]({mailgun_url})\n\n Compila le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Metodo: POST \n - Tipo di contenuto: application/json\n\n Vedi [la documentazione]({docs_url}) su come configurare le automazioni per gestire i dati in arrivo." }, "step": { "user": { diff --git a/homeassistant/components/nut/translations/it.json b/homeassistant/components/nut/translations/it.json index cb8949fca64..bb4d9f9907e 100644 --- a/homeassistant/components/nut/translations/it.json +++ b/homeassistant/components/nut/translations/it.json @@ -12,14 +12,14 @@ "data": { "resources": "Risorse" }, - "title": "Scegliere le risorse da monitorare" + "title": "Scegli le risorse da monitorare" }, "ups": { "data": { "alias": "Alias", "resources": "Risorse" }, - "title": "Scegliere l'UPS da monitorare" + "title": "Scegli l'UPS da monitorare" }, "user": { "data": { @@ -43,7 +43,7 @@ "resources": "Risorse", "scan_interval": "Intervallo di scansione (secondi)" }, - "description": "Scegliere le Risorse del Sensore." + "description": "Scegli le risorse del sensore." } } } diff --git a/homeassistant/components/plaato/translations/it.json b/homeassistant/components/plaato/translations/it.json index acd2fcfa3f4..722d1c5c34c 100644 --- a/homeassistant/components/plaato/translations/it.json +++ b/homeassistant/components/plaato/translations/it.json @@ -31,7 +31,7 @@ "title": "Imposta i dispositivi Plaato" }, "webhook": { - "description": "Per inviare eventi a Home Assistant, dovrai configurare la funzione webhook in Plaato Airlock. \n\n Compila le seguenti informazioni: \n\n - URL: \"{webhook_url}\"\n - Metodo: POST \n\n Vedere [la documentazione] ({docs_url}) per ulteriori dettagli.", + "description": "Per inviare eventi a Home Assistant, dovrai configurare la funzione webhook in Plaato Airlock. \n\n Compila le seguenti informazioni: \n\n - URL: \"{webhook_url}\"\n - Metodo: POST \n\n Vedi [la documentazione] ({docs_url}) per ulteriori dettagli.", "title": "Webhook da utilizzare" } } diff --git a/homeassistant/components/plugwise/translations/it.json b/homeassistant/components/plugwise/translations/it.json index 316d733121b..e60b91a106b 100644 --- a/homeassistant/components/plugwise/translations/it.json +++ b/homeassistant/components/plugwise/translations/it.json @@ -24,7 +24,7 @@ "port": "Porta", "username": "Nome utente Smile" }, - "description": "Si prega di inserire", + "description": "Inserisci", "title": "Connettiti allo Smile" } } diff --git a/homeassistant/components/point/translations/it.json b/homeassistant/components/point/translations/it.json index 831104ba9dc..8a35f7acdf3 100644 --- a/homeassistant/components/point/translations/it.json +++ b/homeassistant/components/point/translations/it.json @@ -16,7 +16,7 @@ }, "step": { "auth": { - "description": "Segui il link qui sotto e **Accetta** l'accesso al tuo account Minut, quindi torna indietro e premi **Invia** qui sotto. \n\n [Link]({authorization_url})", + "description": "Segui il collegamento qui sotto e **Accetta** l'accesso al tuo account Minut, quindi torna indietro e premi **Invia** qui sotto. \n\n [Collegamento]({authorization_url})", "title": "Autenticate Point" }, "user": { diff --git a/homeassistant/components/ps4/translations/it.json b/homeassistant/components/ps4/translations/it.json index fb48c7585cc..4d2390d89c5 100644 --- a/homeassistant/components/ps4/translations/it.json +++ b/homeassistant/components/ps4/translations/it.json @@ -10,8 +10,8 @@ "error": { "cannot_connect": "Impossibile connettersi", "credential_timeout": "Servizio credenziali scaduto. Premi Invia per riavviare.", - "login_failed": "Impossibile eseguire l'associazione a PlayStation 4. Verificare che il Codice PIN sia corretto.", - "no_ipaddress": "Inserire l'Indirizzo IP della PlayStation 4 che desideri configurare." + "login_failed": "Impossibile eseguire l'associazione a PlayStation 4. Verificare che il codice PIN sia corretto.", + "no_ipaddress": "Inserisci l'indirizzo IP della PlayStation 4 che desideri configurare." }, "step": { "creds": { @@ -25,7 +25,7 @@ "name": "Nome", "region": "Area geografica" }, - "description": "Inserisci le informazioni per la tua PlayStation 4. Per il Codice PIN, vai a \"Impostazioni\" sulla PlayStation 4. Quindi vai a 'Impostazioni di connessione Mobile App' e seleziona 'Aggiungi dispositivo'. Immettere il Codice PIN visualizzato. Fai riferimento alla [documentazione](https://www.home-assistant.io/components/ps4/) per ulteriori informazioni.", + "description": "Inserisci le informazioni per la tua PlayStation 4. Per il codice PIN, vai a \"Impostazioni\" sulla PlayStation 4. Quindi vai a 'Impostazioni di connessione Mobile App' e seleziona 'Aggiungi dispositivo'. Immettere il codice PIN visualizzato. Fai riferimento alla [documentazione](https://www.home-assistant.io/components/ps4/) per ulteriori informazioni.", "title": "PlayStation 4" }, "mode": { diff --git a/homeassistant/components/screenlogic/translations/it.json b/homeassistant/components/screenlogic/translations/it.json index 778f69fc530..46b14f07776 100644 --- a/homeassistant/components/screenlogic/translations/it.json +++ b/homeassistant/components/screenlogic/translations/it.json @@ -20,7 +20,7 @@ "data": { "selected_gateway": "Gateway" }, - "description": "Sono stati individuati i gateway ScreenLogic seguenti. Selezionarne uno da configurare oppure scegliere di configurare manualmente un gateway ScreenLogic.", + "description": "Sono stati individuati i gateway ScreenLogic seguenti. Selezionane uno da configurare oppure scegli di configurare manualmente un gateway ScreenLogic.", "title": "ScreenLogic" } } diff --git a/homeassistant/components/sensibo/translations/en.json b/homeassistant/components/sensibo/translations/en.json index 4d07dadc086..d3ee9fb1336 100644 --- a/homeassistant/components/sensibo/translations/en.json +++ b/homeassistant/components/sensibo/translations/en.json @@ -1,18 +1,18 @@ { - "config": { - "abort": { - "already_configured": "Account is already configured" - }, - "error":{ - "cannot_connect": "Failed to connect" - }, - "step": { - "user": { - "data": { - "api_key": "API Key", - "name": "Name" + "config": { + "abort": { + "already_configured": "Account is already configured" + }, + "error": { + "cannot_connect": "Failed to connect" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "name": "Name" + } + } } - } } - } } \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/et.json b/homeassistant/components/sensibo/translations/et.json new file mode 100644 index 00000000000..ccee8f1c41d --- /dev/null +++ b/homeassistant/components/sensibo/translations/et.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Konto on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "step": { + "user": { + "data": { + "api_key": "API v\u00f5ti", + "name": "Nimi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smappee/translations/it.json b/homeassistant/components/smappee/translations/it.json index cc67aab0237..3a493371b16 100644 --- a/homeassistant/components/smappee/translations/it.json +++ b/homeassistant/components/smappee/translations/it.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured_device": "Il dispositivo \u00e8 gi\u00e0 configurato", - "already_configured_local_device": "L'apparecchio o gli apparecchi locali sono gi\u00e0 configurati. Si prega di rimuoverli prima di configurare un dispositivo cloud.", + "already_configured_local_device": "L'apparecchio o gli apparecchi locali sono gi\u00e0 configurati. Rimuovili prima di configurare un dispositivo cloud.", "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", "cannot_connect": "Impossibile connettersi", "invalid_mdns": "Dispositivo non supportato per l'integrazione Smappee.", @@ -21,7 +21,7 @@ "data": { "host": "Host" }, - "description": "Immettere l'host per avviare l'integrazione locale di Smappee" + "description": "Immetti l'host per avviare l'integrazione locale di Smappee" }, "pick_implementation": { "title": "Scegli il metodo di autenticazione" diff --git a/homeassistant/components/smartthings/translations/it.json b/homeassistant/components/smartthings/translations/it.json index 0f7a508e977..4f2006e52c3 100644 --- a/homeassistant/components/smartthings/translations/it.json +++ b/homeassistant/components/smartthings/translations/it.json @@ -30,7 +30,7 @@ "title": "Seleziona posizione" }, "user": { - "description": "SmartThings sar\u00e0 configurato per inviare aggiornamenti push a Home Assistant su: \n > {webhook_url} \n\nSe ci\u00f2 non fosse corretto, aggiornare la configurazione, riavviare Home Assistant e riprovare.", + "description": "SmartThings sar\u00e0 configurato per inviare aggiornamenti push a Home Assistant su: \n > {webhook_url} \n\nSe ci\u00f2 non fosse corretto, aggiorna la configurazione, riavvia Home Assistant e riprova.", "title": "Conferma l'URL di richiamo" } } diff --git a/homeassistant/components/system_bridge/translations/it.json b/homeassistant/components/system_bridge/translations/it.json index 0fe7f0103b8..2b6885edc44 100644 --- a/homeassistant/components/system_bridge/translations/it.json +++ b/homeassistant/components/system_bridge/translations/it.json @@ -16,7 +16,7 @@ "data": { "api_key": "Chiave API" }, - "description": "Immettere la chiave API impostata nella configurazione per {name} ." + "description": "Immetti la chiave API impostata nella configurazione per {name} ." }, "user": { "data": { diff --git a/homeassistant/components/tado/translations/it.json b/homeassistant/components/tado/translations/it.json index 8527d440710..34c9dfe597d 100644 --- a/homeassistant/components/tado/translations/it.json +++ b/homeassistant/components/tado/translations/it.json @@ -25,7 +25,7 @@ "data": { "fallback": "Abilita la modalit\u00e0 di ripiego." }, - "description": "La modalit\u00e0 di fallback passer\u00e0 a Smart Schedule al prossimo cambio di programma dopo aver regolato manualmente una zona.", + "description": "La modalit\u00e0 di ripiego passer\u00e0 a Smart Schedule al prossimo cambio di programma dopo aver regolato manualmente una zona.", "title": "Regola le opzioni di Tado." } } diff --git a/homeassistant/components/tellduslive/translations/it.json b/homeassistant/components/tellduslive/translations/it.json index 0d941b371b3..e0faeffb227 100644 --- a/homeassistant/components/tellduslive/translations/it.json +++ b/homeassistant/components/tellduslive/translations/it.json @@ -11,7 +11,7 @@ }, "step": { "auth": { - "description": "Per collegare il tuo account TelldusLive:\n 1. Fai clic sul collegamento sottostante\n 2. Accedi a Telldus Live\n 3. Autorizzare **{app_name}** (fai clic su **S\u00ec**).\n 4. Torna qui e fai clic su **INVIA**.\n\n [Collegamento per account TelldusLive]({auth_url})", + "description": "Per collegare il tuo account TelldusLive:\n 1. Fai clic sul collegamento sottostante\n 2. Accedi a Telldus Live\n 3. Autorizza **{app_name}** (fai clic su **S\u00ec**).\n 4. Torna qui e fai clic su **INVIA**.\n\n [Collegamento per account TelldusLive]({auth_url})", "title": "Autenticati con TelldusLive" }, "user": { diff --git a/homeassistant/components/tile/translations/de.json b/homeassistant/components/tile/translations/de.json index 5866a1e0f5b..a16a0bb34bb 100644 --- a/homeassistant/components/tile/translations/de.json +++ b/homeassistant/components/tile/translations/de.json @@ -1,12 +1,19 @@ { "config": { "abort": { - "already_configured": "Konto wurde bereits konfiguriert" + "already_configured": "Konto wurde bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "step": { + "reauth_confirm": { + "data": { + "password": "Passwort" + }, + "title": "Kachel erneut authentifizieren" + }, "user": { "data": { "password": "Passwort", diff --git a/homeassistant/components/tile/translations/tr.json b/homeassistant/components/tile/translations/tr.json index 793f7fd5fd7..0f6bd7cf6c8 100644 --- a/homeassistant/components/tile/translations/tr.json +++ b/homeassistant/components/tile/translations/tr.json @@ -1,12 +1,19 @@ { "config": { "abort": { - "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" }, "step": { + "reauth_confirm": { + "data": { + "password": "Parola" + }, + "title": "Kutucu\u011fu Yeniden Do\u011frula" + }, "user": { "data": { "password": "Parola", diff --git a/homeassistant/components/toon/translations/it.json b/homeassistant/components/toon/translations/it.json index af492ab500c..724c61dba3c 100644 --- a/homeassistant/components/toon/translations/it.json +++ b/homeassistant/components/toon/translations/it.json @@ -17,7 +17,7 @@ "title": "Seleziona il tuo contratto" }, "pick_implementation": { - "title": "Scegliere il tenant con cui eseguire l'autenticazione" + "title": "Scegli il tenant con cui eseguire l'autenticazione" } } } diff --git a/homeassistant/components/traccar/translations/it.json b/homeassistant/components/traccar/translations/it.json index a1f4b812806..cee10e5f9fe 100644 --- a/homeassistant/components/traccar/translations/it.json +++ b/homeassistant/components/traccar/translations/it.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "L'istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi webhook." }, "create_entry": { - "default": "Per inviare eventi a Home Assistant, \u00e8 necessario impostare la funzione webhook in Traccar.\n\nUsa il seguente URL: `{webhook_url}`.\n\nVedi [la documentazione]({docs_url}) per ulteriori dettagli." + "default": "Per inviare eventi a Home Assistant, devi impostare la funzione webhook in Traccar.\n\nUsa il seguente URL: `{webhook_url}`.\n\nVedi [la documentazione]({docs_url}) per ulteriori dettagli." }, "step": { "user": { diff --git a/homeassistant/components/transmission/translations/it.json b/homeassistant/components/transmission/translations/it.json index 0e1cc009824..18edfb17351 100644 --- a/homeassistant/components/transmission/translations/it.json +++ b/homeassistant/components/transmission/translations/it.json @@ -29,7 +29,7 @@ "order": "Ordine", "scan_interval": "Frequenza di aggiornamento" }, - "title": "Configurare le opzioni per Transmission" + "title": "Configura le opzioni per Transmission" } } } diff --git a/homeassistant/components/tuya/translations/it.json b/homeassistant/components/tuya/translations/it.json index 96d7d3e997b..0ab73ab3b60 100644 --- a/homeassistant/components/tuya/translations/it.json +++ b/homeassistant/components/tuya/translations/it.json @@ -72,7 +72,7 @@ "data": { "discovery_interval": "Intervallo di scansione di rilevamento dispositivo in secondi", "list_devices": "Seleziona i dispositivi da configurare o lascia vuoto per salvare la configurazione", - "query_device": "Selezionare il dispositivo che utilizzer\u00e0 il metodo di interrogazione per un pi\u00f9 rapido aggiornamento dello stato", + "query_device": "Seleziona il dispositivo che utilizzer\u00e0 il metodo di interrogazione per un pi\u00f9 rapido aggiornamento dello stato", "query_interval": "Intervallo di scansione di interrogazione dispositivo in secondi" }, "description": "Non impostare valori dell'intervallo di scansione troppo bassi o le chiamate non riusciranno a generare un messaggio di errore nel registro", diff --git a/homeassistant/components/twilio/translations/it.json b/homeassistant/components/twilio/translations/it.json index 981e1964b20..591618bfb60 100644 --- a/homeassistant/components/twilio/translations/it.json +++ b/homeassistant/components/twilio/translations/it.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "L'istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi webhook." }, "create_entry": { - "default": "Per inviare eventi a Home Assistant, dovrai configurare [Webhook con Twilio]({twilio_url})\n\n Compila le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/x-www-form-urlencoded\n\n Vedi [la documentazione]({docs_url}) su come configurare le automazioni per gestire i dati in arrivo." + "default": "Per inviare eventi a Home Assistant, dovrai configurare [Webhook con Twilio]({twilio_url})\n\n Compila le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Metodo: POST \n - Tipo di contenuto: application/x-www-form-urlencoded\n\n Vedi [la documentazione]({docs_url}) su come configurare le automazioni per gestire i dati in arrivo." }, "step": { "user": { diff --git a/homeassistant/components/upb/translations/it.json b/homeassistant/components/upb/translations/it.json index 8264efdbcf2..1de3cdddabd 100644 --- a/homeassistant/components/upb/translations/it.json +++ b/homeassistant/components/upb/translations/it.json @@ -15,7 +15,7 @@ "file_path": "Percorso e nome del file di esportazione UPStart UPB.", "protocol": "Protocollo" }, - "description": "Collega un Modulo Interfaccia Powerline del Bus Universale Powerline (UPB PIM). La stringa dell'indirizzo deve essere nel formato 'address[:port]' per 'tcp'. La porta \u00e8 facoltativa e il valore predefinito \u00e8 2101. Esempio: '192.168.1.42'. Per il protocollo seriale, l'indirizzo deve essere nella forma 'tty[:baud]'. Baud \u00e8 opzionale e il valore predefinito \u00e8 4800. Esempio: '/dev/ttyS1'.", + "description": "Collega un Modulo Interfaccia Powerline del Bus Universale Powerline (UPB PIM). La stringa dell'indirizzo deve essere nel formato 'indirizzo[:porta]' per 'tcp'. La porta \u00e8 facoltativa e il valore predefinito \u00e8 2101. Esempio: '192.168.1.42'. Per il protocollo seriale, l'indirizzo deve essere nella forma 'tty[:baud]'. Baud \u00e8 opzionale e il valore predefinito \u00e8 4800. Esempio: '/dev/ttyS1'.", "title": "Collegamento a UPB PIM" } } diff --git a/homeassistant/components/vera/translations/it.json b/homeassistant/components/vera/translations/it.json index 3ec026beaac..a3832914ac6 100644 --- a/homeassistant/components/vera/translations/it.json +++ b/homeassistant/components/vera/translations/it.json @@ -22,7 +22,7 @@ "exclude": "ID dispositivo Vera da escludere da Home Assistant.", "lights": "Gli ID dei dispositivi switch Vera da trattare come luci in Home Assistant." }, - "description": "Consultare la documentazione di vera per i dettagli sui parametri opzionali: https://www.home-assistant.io/integrations/vera/. Nota: qualsiasi modifica qui effettuata necessita del riavvio del server di Home Assistant. Per cancellare i valori, inserire uno spazio.", + "description": "Consulta la documentazione di vera per i dettagli sui parametri opzionali: https://www.home-assistant.io/integrations/vera/. Nota: qualsiasi modifica qui effettuata necessita del riavvio del server di Home Assistant. Per cancellare i valori, inserisci uno spazio.", "title": "Opzioni controller Vera" } } diff --git a/homeassistant/components/vizio/translations/it.json b/homeassistant/components/vizio/translations/it.json index b0bd1de7077..3dc3b44c5f3 100644 --- a/homeassistant/components/vizio/translations/it.json +++ b/homeassistant/components/vizio/translations/it.json @@ -46,7 +46,7 @@ "include_or_exclude": "Includere o escludere app?", "volume_step": "Dimensione del passo del volume" }, - "description": "Se si dispone di una Smart TV, \u00e8 possibile filtrare l'elenco di origine scegliendo le app da includere o escludere in esso.", + "description": "Se disponi di una Smart TV, puoi filtrare l'elenco di origine scegliendo le app da includere o escludere in esso.", "title": "Aggiornamento delle Opzioni del Dispositivo SmartCast VIZIO" } } diff --git a/homeassistant/components/xiaomi_aqara/translations/it.json b/homeassistant/components/xiaomi_aqara/translations/it.json index fa1999fc877..27330c11242 100644 --- a/homeassistant/components/xiaomi_aqara/translations/it.json +++ b/homeassistant/components/xiaomi_aqara/translations/it.json @@ -35,7 +35,7 @@ "interface": "L'interfaccia di rete da utilizzare", "mac": "Indirizzo Mac (opzionale)" }, - "description": "Connettiti al tuo Xiaomi Aqara Gateway, se gli indirizzi IP e MAC sono lasciati vuoti, verr\u00e0 utilizzato il rilevamento automatico", + "description": "Connettiti al tuo Xiaomi Aqara Gateway, se gli indirizzi IP e MAC sono lasciati vuoti, sar\u00e0 utilizzato il rilevamento automatico", "title": "Xiaomi Aqara Gateway" } } diff --git a/homeassistant/components/yeelight/translations/it.json b/homeassistant/components/yeelight/translations/it.json index ca200674871..7022a016ce8 100644 --- a/homeassistant/components/yeelight/translations/it.json +++ b/homeassistant/components/yeelight/translations/it.json @@ -35,7 +35,7 @@ "transition": "Tempo di transizione (ms)", "use_music_mode": "Abilita la modalit\u00e0 musica" }, - "description": "Se lasci il modello vuoto, verr\u00e0 rilevato automaticamente." + "description": "Se lasci il modello vuoto, sar\u00e0 rilevato automaticamente." } } } From 36c7521508ec8da4cc7f01e6b2c6e996adbb90f6 Mon Sep 17 00:00:00 2001 From: shbatm Date: Wed, 22 Dec 2021 22:22:27 -0600 Subject: [PATCH 0969/2644] Bump PyISY to v3.0.1 (#62646) --- homeassistant/components/isy994/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/isy994/manifest.json b/homeassistant/components/isy994/manifest.json index 84f13ae4fc4..792629f801c 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -2,7 +2,7 @@ "domain": "isy994", "name": "Universal Devices ISY994", "documentation": "https://www.home-assistant.io/integrations/isy994", - "requirements": ["pyisy==3.0.0"], + "requirements": ["pyisy==3.0.1"], "codeowners": ["@bdraco", "@shbatm"], "config_flow": true, "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index b57284a1857..0743ff415a1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1582,7 +1582,7 @@ pyirishrail==0.0.2 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.0.0 +pyisy==3.0.1 # homeassistant.components.itach pyitachip2ir==0.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0a9e244bd32..088ba49f44a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -963,7 +963,7 @@ pyipp==0.11.0 pyiqvia==2021.11.0 # homeassistant.components.isy994 -pyisy==3.0.0 +pyisy==3.0.1 # homeassistant.components.kira pykira==0.1.1 From 79627526c7bf7bb4195598aa206355b601d71b1c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 23 Dec 2021 06:25:05 +0100 Subject: [PATCH 0970/2644] Add strict typing to PVOutput (#62628) --- .strict-typing | 1 + homeassistant/components/pvoutput/sensor.py | 26 +++++++++++++-------- mypy.ini | 11 +++++++++ 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/.strict-typing b/.strict-typing index cfccd601ac1..66fc72efc77 100644 --- a/.strict-typing +++ b/.strict-typing @@ -102,6 +102,7 @@ homeassistant.components.openuv.* homeassistant.components.persistent_notification.* homeassistant.components.pi_hole.* homeassistant.components.proximity.* +homeassistant.components.pvoutput.* homeassistant.components.rainmachine.* homeassistant.components.rdw.* homeassistant.components.recollect_waste.* diff --git a/homeassistant/components/pvoutput/sensor.py b/homeassistant/components/pvoutput/sensor.py index a18c5cc40b6..8341e45579d 100644 --- a/homeassistant/components/pvoutput/sensor.py +++ b/homeassistant/components/pvoutput/sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import timedelta import logging -from pvo import PVOutput, PVOutputError +from pvo import PVOutput, PVOutputError, Status import voluptuous as vol from homeassistant.components.sensor import ( @@ -20,7 +20,10 @@ from homeassistant.const import ( CONF_NAME, ENERGY_WATT_HOUR, ) +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) @@ -45,10 +48,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Set up the PVOutput sensor.""" - name = config.get(CONF_NAME) - pvoutput = PVOutput( api_key=config[CONF_API_KEY], system_id=config[CONF_SYSTEM_ID], @@ -58,9 +64,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= status = await pvoutput.status() except PVOutputError: _LOGGER.error("Unable to fetch data from PVOutput") - return False + return - async_add_entities([PvoutputSensor(pvoutput, status, name)]) + async_add_entities([PvoutputSensor(pvoutput, status, config[CONF_NAME])]) class PvoutputSensor(SensorEntity): @@ -70,19 +76,19 @@ class PvoutputSensor(SensorEntity): _attr_device_class = SensorDeviceClass.ENERGY _attr_native_unit_of_measurement = ENERGY_WATT_HOUR - def __init__(self, pvoutput, status, name): + def __init__(self, pvoutput: PVOutput, status: Status, name: str) -> None: """Initialize a PVOutput sensor.""" self._attr_name = name self.pvoutput = pvoutput self.status = status @property - def native_value(self): + def native_value(self) -> int | None: """Return the state of the device.""" return self.status.energy_generation @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, int | float | None]: """Return the state attributes of the monitored installation.""" return { ATTR_ENERGY_GENERATION: self.status.energy_generation, @@ -94,6 +100,6 @@ class PvoutputSensor(SensorEntity): ATTR_VOLTAGE: self.status.voltage, } - async def async_update(self): + async def async_update(self) -> None: """Get the latest data from the PVOutput API and updates the state.""" self.status = await self.pvoutput.status() diff --git a/mypy.ini b/mypy.ini index 6458bb83932..6ccb0b8106c 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1133,6 +1133,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.pvoutput.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.rainmachine.*] check_untyped_defs = true disallow_incomplete_defs = true From ef5e5c3f9675db4602a1291609c02ce2a2c4f45e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Dec 2021 19:40:36 -1000 Subject: [PATCH 0971/2644] Dismiss existing discoveries when a HomeKit device is paired (#62632) --- .../homekit_controller/config_flow.py | 18 +++++-- .../homekit_controller/test_config_flow.py | 50 +++++++++++++++---- 2 files changed, 56 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 0728048ec84..26055b964f8 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult +from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.helpers.device_registry import ( CONNECTION_NETWORK_MAC, async_get_registry as async_get_device_registry, @@ -223,6 +223,8 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # The hkid is a unique random number that looks like a pairing code. # It changes if a device is factory reset. hkid = properties[zeroconf.ATTR_PROPERTIES_ID] + normalized_hkid = normalize_hkid(hkid) + model = properties["md"] name = discovery_info.name.replace("._hap._tcp.local.", "") status_flags = int(properties["sf"]) @@ -240,7 +242,9 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): config_num = None # Set unique-id and error out if it's already configured - existing_entry = await self.async_set_unique_id(normalize_hkid(hkid)) + existing_entry = await self.async_set_unique_id( + normalized_hkid, raise_on_progress=False + ) updated_ip_port = { "AccessoryIP": discovery_info.host, "AccessoryPort": discovery_info.port, @@ -303,7 +307,15 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # Set unique-id and error out if it's already configured self._abort_if_unique_id_configured(updates=updated_ip_port) - self.context["hkid"] = hkid + for progress in self._async_in_progress(include_uninitialized=True): + if progress["context"].get("unique_id") == normalized_hkid: + if paired: + # If the device gets paired, we want to dismiss + # an existing discovery since we can no longer + # pair with it + self.hass.config_entries.flow.async_abort(progress["flow_id"]) + else: + raise AbortFlow("already_in_progress") if paired: # Device is paired but not to us - ignore it diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index d077bb8eb4e..33b5b15698d 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -206,7 +206,6 @@ async def test_discovery_works(hass, controller, upper_case_props, missing_cshar assert result["type"] == "form" assert result["step_id"] == "pair" assert get_flow_context(hass, result) == { - "hkid": "00:00:00:00:00:00", "source": config_entries.SOURCE_ZEROCONF, "title_placeholders": {"name": "TestDevice"}, "unique_id": "00:00:00:00:00:00", @@ -577,7 +576,6 @@ async def test_pair_form_errors_on_start(hass, controller, exception, expected): ) assert get_flow_context(hass, result) == { - "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, "unique_id": "00:00:00:00:00:00", "source": config_entries.SOURCE_ZEROCONF, @@ -593,7 +591,6 @@ async def test_pair_form_errors_on_start(hass, controller, exception, expected): assert result["errors"]["pairing_code"] == expected assert get_flow_context(hass, result) == { - "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, "unique_id": "00:00:00:00:00:00", "source": config_entries.SOURCE_ZEROCONF, @@ -627,7 +624,6 @@ async def test_pair_abort_errors_on_finish(hass, controller, exception, expected ) assert get_flow_context(hass, result) == { - "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, "unique_id": "00:00:00:00:00:00", "source": config_entries.SOURCE_ZEROCONF, @@ -641,7 +637,6 @@ async def test_pair_abort_errors_on_finish(hass, controller, exception, expected assert result["type"] == "form" assert get_flow_context(hass, result) == { - "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, "unique_id": "00:00:00:00:00:00", "source": config_entries.SOURCE_ZEROCONF, @@ -669,7 +664,6 @@ async def test_pair_form_errors_on_finish(hass, controller, exception, expected) ) assert get_flow_context(hass, result) == { - "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, "unique_id": "00:00:00:00:00:00", "source": config_entries.SOURCE_ZEROCONF, @@ -683,7 +677,6 @@ async def test_pair_form_errors_on_finish(hass, controller, exception, expected) assert result["type"] == "form" assert get_flow_context(hass, result) == { - "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, "unique_id": "00:00:00:00:00:00", "source": config_entries.SOURCE_ZEROCONF, @@ -697,7 +690,6 @@ async def test_pair_form_errors_on_finish(hass, controller, exception, expected) assert result["errors"]["pairing_code"] == expected assert get_flow_context(hass, result) == { - "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, "unique_id": "00:00:00:00:00:00", "source": config_entries.SOURCE_ZEROCONF, @@ -820,7 +812,6 @@ async def test_unignore_works(hass, controller): assert result["type"] == "form" assert result["step_id"] == "pair" assert get_flow_context(hass, result) == { - "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, "unique_id": "00:00:00:00:00:00", "source": config_entries.SOURCE_UNIGNORE, @@ -852,3 +843,44 @@ async def test_unignore_ignores_missing_devices(hass, controller): assert result["type"] == "abort" assert result["reason"] == "no_devices" + + +async def test_discovery_dismiss_existing_flow_on_paired(hass, controller): + """Test that existing flows get dismissed once paired to something else.""" + device = setup_mock_accessory(controller) + discovery_info = get_device_discovery_info(device) + + # Set device as already not paired + discovery_info.properties["sf"] = 0x01 + discovery_info.properties["c#"] = 99999 + discovery_info.properties[zeroconf.ATTR_PROPERTIES_ID] = "AA:BB:CC:DD:EE:FF" + + # Device is discovered + result = await hass.config_entries.flow.async_init( + "homekit_controller", + context={"source": config_entries.SOURCE_ZEROCONF}, + data=discovery_info, + ) + assert result["type"] == "form" + assert result["step_id"] == "pair" + await hass.async_block_till_done() + assert ( + len(hass.config_entries.flow.async_progress_by_handler("homekit_controller")) + == 1 + ) + + # Set device as already paired + discovery_info.properties["sf"] = 0x00 + # Device is discovered again after pairing to someone else + result2 = await hass.config_entries.flow.async_init( + "homekit_controller", + context={"source": config_entries.SOURCE_ZEROCONF}, + data=discovery_info, + ) + assert result2["type"] == "abort" + assert result2["reason"] == "already_paired" + await hass.async_block_till_done() + assert ( + len(hass.config_entries.flow.async_progress_by_handler("homekit_controller")) + == 0 + ) From 6ef7539a31c59c2d86ecac365814152cd2f4beb7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 23 Dec 2021 07:25:26 +0100 Subject: [PATCH 0972/2644] Add base integration tests to Luftdaten (#62594) --- .coveragerc | 1 - .../components/luftdaten/__init__.py | 2 +- tests/components/luftdaten/conftest.py | 58 +++++++++++++- .../components/luftdaten/test_config_flow.py | 80 +++++++++---------- tests/components/luftdaten/test_init.py | 75 +++++++++++++++++ 5 files changed, 170 insertions(+), 46 deletions(-) create mode 100644 tests/components/luftdaten/test_init.py diff --git a/.coveragerc b/.coveragerc index 26f05a7816a..d5876b62b61 100644 --- a/.coveragerc +++ b/.coveragerc @@ -607,7 +607,6 @@ omit = homeassistant/components/lookin/climate.py homeassistant/components/lookin/media_player.py homeassistant/components/luci/device_tracker.py - homeassistant/components/luftdaten/__init__.py homeassistant/components/luftdaten/sensor.py homeassistant/components/lupusec/* homeassistant/components/lutron/* diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py index 33ddcf67a1e..8725c25bf8e 100644 --- a/homeassistant/components/luftdaten/__init__.py +++ b/homeassistant/components/luftdaten/__init__.py @@ -28,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # For backwards compat, set unique ID if entry.unique_id is None: hass.config_entries.async_update_entry( - entry, unique_id=entry.data[CONF_SENSOR_ID] + entry, unique_id=str(entry.data[CONF_SENSOR_ID]) ) luftdaten = Luftdaten(entry.data[CONF_SENSOR_ID]) diff --git a/tests/components/luftdaten/conftest.py b/tests/components/luftdaten/conftest.py index 28cade2c34d..14d8991e41c 100644 --- a/tests/components/luftdaten/conftest.py +++ b/tests/components/luftdaten/conftest.py @@ -2,12 +2,13 @@ from __future__ import annotations from collections.abc import Generator -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest -from homeassistant.components.luftdaten import DOMAIN -from homeassistant.components.luftdaten.const import CONF_SENSOR_ID +from homeassistant.components.luftdaten.const import CONF_SENSOR_ID, DOMAIN +from homeassistant.const import CONF_SHOW_ON_MAP +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -18,7 +19,7 @@ def mock_config_entry() -> MockConfigEntry: return MockConfigEntry( title="12345", domain=DOMAIN, - data={CONF_SENSOR_ID: 123456}, + data={CONF_SENSOR_ID: 12345, CONF_SHOW_ON_MAP: True}, unique_id="12345", ) @@ -30,3 +31,52 @@ def mock_setup_entry() -> Generator[None, None, None]: "homeassistant.components.luftdaten.async_setup_entry", return_value=True ): yield + + +@pytest.fixture +def mock_luftdaten_config_flow() -> Generator[None, MagicMock, None]: + """Return a mocked Luftdaten client.""" + with patch( + "homeassistant.components.luftdaten.config_flow.Luftdaten", autospec=True + ) as luftdaten_mock: + luftdaten = luftdaten_mock.return_value + luftdaten.validate_sensor.return_value = True + yield luftdaten + + +@pytest.fixture +def mock_luftdaten() -> Generator[None, MagicMock, None]: + """Return a mocked Luftdaten client.""" + with patch( + "homeassistant.components.luftdaten.Luftdaten", autospec=True + ) as luftdaten_mock: + luftdaten = luftdaten_mock.return_value + luftdaten.sensor_id = 12345 + luftdaten.meta = { + "altitude": 123.456, + "latitude": 56.789, + "longitude": 12.345, + "sensor_id": 12345, + } + luftdaten.values = { + "humidity": 34.70, + "P1": 8.5, + "P2": 4.07, + "pressure_at_sea_level": 103102.13, + "pressure": 98545.00, + "temperature": 22.30, + } + yield luftdaten + + +@pytest.fixture +async def init_integration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_luftdaten: MagicMock +) -> MockConfigEntry: + """Set up the Luftdaten integration for testing.""" + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + return mock_config_entry diff --git a/tests/components/luftdaten/test_config_flow.py b/tests/components/luftdaten/test_config_flow.py index 21589381066..595d4397200 100644 --- a/tests/components/luftdaten/test_config_flow.py +++ b/tests/components/luftdaten/test_config_flow.py @@ -1,5 +1,5 @@ """Define tests for the Luftdaten config flow.""" -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock from luftdaten.exceptions import LuftdatenConnectionError @@ -40,7 +40,9 @@ async def test_duplicate_error( assert result2.get("reason") == "already_configured" -async def test_communication_error(hass: HomeAssistant) -> None: +async def test_communication_error( + hass: HomeAssistant, mock_luftdaten_config_flow: MagicMock +) -> None: """Test that no sensor is added while unable to communicate with API.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -50,23 +52,22 @@ async def test_communication_error(hass: HomeAssistant) -> None: assert result.get("step_id") == SOURCE_USER assert "flow_id" in result - with patch("luftdaten.Luftdaten.get_data", side_effect=LuftdatenConnectionError): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_SENSOR_ID: 12345}, - ) + mock_luftdaten_config_flow.get_data.side_effect = LuftdatenConnectionError + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_SENSOR_ID: 12345}, + ) assert result2.get("type") == RESULT_TYPE_FORM assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {CONF_SENSOR_ID: "cannot_connect"} + assert "flow_id" in result2 - with patch("luftdaten.Luftdaten.get_data"), patch( - "luftdaten.Luftdaten.validate_sensor", return_value=True - ): - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], - user_input={CONF_SENSOR_ID: 12345}, - ) + mock_luftdaten_config_flow.get_data.side_effect = None + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + user_input={CONF_SENSOR_ID: 12345}, + ) assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY assert result3.get("title") == "12345" @@ -76,7 +77,9 @@ async def test_communication_error(hass: HomeAssistant) -> None: } -async def test_invalid_sensor(hass: HomeAssistant) -> None: +async def test_invalid_sensor( + hass: HomeAssistant, mock_luftdaten_config_flow: MagicMock +) -> None: """Test that an invalid sensor throws an error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -86,26 +89,22 @@ async def test_invalid_sensor(hass: HomeAssistant) -> None: assert result.get("step_id") == SOURCE_USER assert "flow_id" in result - with patch("luftdaten.Luftdaten.get_data", return_value=False), patch( - "luftdaten.Luftdaten.validate_sensor", return_value=False - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_SENSOR_ID: 12345}, - ) + mock_luftdaten_config_flow.validate_sensor.return_value = False + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_SENSOR_ID: 11111}, + ) assert result2.get("type") == RESULT_TYPE_FORM assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {CONF_SENSOR_ID: "invalid_sensor"} assert "flow_id" in result2 - with patch("luftdaten.Luftdaten.get_data"), patch( - "luftdaten.Luftdaten.validate_sensor", return_value=True - ): - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], - user_input={CONF_SENSOR_ID: 12345}, - ) + mock_luftdaten_config_flow.validate_sensor.return_value = True + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + user_input={CONF_SENSOR_ID: 12345}, + ) assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY assert result3.get("title") == "12345" @@ -115,7 +114,11 @@ async def test_invalid_sensor(hass: HomeAssistant) -> None: } -async def test_step_user(hass: HomeAssistant, mock_setup_entry: MagicMock) -> None: +async def test_step_user( + hass: HomeAssistant, + mock_setup_entry: MagicMock, + mock_luftdaten_config_flow: MagicMock, +) -> None: """Test that the user step works.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -125,16 +128,13 @@ async def test_step_user(hass: HomeAssistant, mock_setup_entry: MagicMock) -> No assert result.get("step_id") == SOURCE_USER assert "flow_id" in result - with patch("luftdaten.Luftdaten.get_data", return_value=True), patch( - "luftdaten.Luftdaten.validate_sensor", return_value=True - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_SENSOR_ID: 12345, - CONF_SHOW_ON_MAP: True, - }, - ) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_SENSOR_ID: 12345, + CONF_SHOW_ON_MAP: True, + }, + ) assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY assert result2.get("title") == "12345" diff --git a/tests/components/luftdaten/test_init.py b/tests/components/luftdaten/test_init.py new file mode 100644 index 00000000000..83902b19cd7 --- /dev/null +++ b/tests/components/luftdaten/test_init.py @@ -0,0 +1,75 @@ +"""Tests for the Luftdaten integration.""" +from unittest.mock import AsyncMock, MagicMock, patch + +from luftdaten.exceptions import LuftdatenError + +from homeassistant.components.luftdaten.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_load_unload_config_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_luftdaten: AsyncMock, +) -> None: + """Test the Luftdaten configuration entry loading/unloading.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert not hass.data.get(DOMAIN) + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + +@patch( + "homeassistant.components.luftdaten.Luftdaten.get_data", + side_effect=LuftdatenError, +) +async def test_config_entry_not_ready( + mock_get_data: MagicMock, + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the Luftdaten configuration entry not ready.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_get_data.call_count == 1 + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_config_entry_not_ready_no_data( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_luftdaten: MagicMock, +) -> None: + """Test the Luftdaten configuration entry not ready.""" + mock_luftdaten.values = {} + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + mock_luftdaten.get_data.assert_called() + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_setting_unique_id( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_luftdaten: MagicMock +) -> None: + """Test we set unique ID if not set yet.""" + mock_config_entry.unique_id = None + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.unique_id == "12345" From 23277181ca52891dd661ae3ef4471da164332b17 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 22 Dec 2021 22:31:56 -0800 Subject: [PATCH 0973/2644] Increase test coverage for google calendar (#62648) * Increase test coverage for google calendar Update tests to exercise the API responses, getting test coverage to 97% for calendar.py ----------- coverage: platform linux, python 3.9.6-final-0 ----------- Name Stmts Miss Cover Missing --------------------------------------------------------------------------- homeassistant/components/google/__init__.py 193 84 56% 92, 163-228, 238, 244-247, 254-262, 274, 298-299, 305-347, 387-392, 416-430, 435-437 homeassistant/components/google/calendar.py 122 4 97% 41, 45, 51, 135 --------------------------------------------------------------------------- TOTAL 315 88 72% * Revert conftest changes * Update typing errors found on CI * Update python3.8 typing imports * Remove commented out code --- .coveragerc | 2 +- homeassistant/components/google/calendar.py | 4 +- tests/components/google/test_calendar.py | 173 +++++++++++++++++++- 3 files changed, 170 insertions(+), 9 deletions(-) diff --git a/.coveragerc b/.coveragerc index d5876b62b61..c5b0e6c0923 100644 --- a/.coveragerc +++ b/.coveragerc @@ -399,7 +399,7 @@ omit = homeassistant/components/glances/sensor.py homeassistant/components/gntp/notify.py homeassistant/components/goalfeed/* - homeassistant/components/google/* + homeassistant/components/google/__init__.py homeassistant/components/google_cloud/tts.py homeassistant/components/google_maps/device_tracker.py homeassistant/components/google_pubsub/__init__.py diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index 5c06e0fbb94..7381e37d2aa 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -121,8 +121,8 @@ class GoogleCalendarData: def _prepare_query(self): try: service = self.calendar_service.get() - except ServerNotFoundError: - _LOGGER.error("Unable to connect to Google") + except ServerNotFoundError as err: + _LOGGER.error("Unable to connect to Google: %s", err) return None, None params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS) params["calendarId"] = self.calendar_id diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index ad7b6b12001..3b5aa7365dc 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -1,5 +1,10 @@ """The tests for the google calendar platform.""" + +from __future__ import annotations + import copy +from http import HTTPStatus +from typing import Any, Callable from unittest.mock import Mock, patch import httplib2 @@ -11,10 +16,12 @@ from homeassistant.components.google import ( CONF_CLIENT_SECRET, CONF_DEVICE_ID, CONF_ENTITIES, + CONF_IGNORE_AVAILABILITY, CONF_NAME, CONF_TRACK, DEVICE_SCHEMA, SERVICE_SCAN_CALENDARS, + GoogleCalendarService, do_setup, ) from homeassistant.const import STATE_OFF, STATE_ON @@ -23,6 +30,8 @@ from homeassistant.setup import async_setup_component from homeassistant.util import slugify import homeassistant.util.dt as dt_util +from .conftest import TEST_CALENDAR + from tests.common import async_mock_service GOOGLE_CONFIG = {CONF_CLIENT_ID: "client_id", CONF_CLIENT_SECRET: "client_secret"} @@ -69,6 +78,7 @@ def get_calendar_info(calendar): CONF_TRACK: calendar["track"], CONF_NAME: calendar["summary"], CONF_DEVICE_ID: slugify(calendar["summary"]), + CONF_IGNORE_AVAILABILITY: calendar.get("ignore_availability", True), } ], } @@ -95,12 +105,6 @@ def mock_google_setup(hass, test_calendar): yield -@pytest.fixture(autouse=True) -def mock_http(hass): - """Mock the http component.""" - hass.http = Mock() - - @pytest.fixture(autouse=True) def set_time_zone(): """Set the time zone for the tests.""" @@ -314,3 +318,160 @@ async def test_update_error(hass, google_service): state = hass.states.get(TEST_ENTITY) assert state.name == TEST_ENTITY_NAME assert state.state == "off" + + +async def test_calendars_api(hass, hass_client, google_service): + """Test the Rest API returns the calendar.""" + assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) + await hass.async_block_till_done() + + client = await hass_client() + response = await client.get("/api/calendars") + assert response.status == HTTPStatus.OK + data = await response.json() + assert data == [ + { + "entity_id": TEST_ENTITY, + "name": TEST_ENTITY_NAME, + } + ] + + +async def test_http_event_api_failure(hass, hass_client, google_service): + """Test the Rest API response during a calendar failure.""" + google_service.return_value.get = Mock( + side_effect=httplib2.ServerNotFoundError("unit test") + ) + + assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) + await hass.async_block_till_done() + + start = dt_util.now().isoformat() + end = (dt_util.now() + dt_util.dt.timedelta(minutes=60)).isoformat() + + client = await hass_client() + response = await client.get(f"/api/calendars/{TEST_ENTITY}?start={start}&end={end}") + assert response.status == HTTPStatus.OK + # A failure to talk to the server results in an empty list of events + events = await response.json() + assert events == [] + + +@pytest.fixture +def mock_events_list( + google_service: GoogleCalendarService, +) -> Callable[[dict[str, Any]], None]: + """Fixture to construct a fake event list API response.""" + + def _put_result(response: dict[str, Any]) -> None: + google_service.return_value.get.return_value.events.return_value.list.return_value.execute.return_value = ( + response + ) + return + + return _put_result + + +async def test_http_api_event(hass, hass_client, google_service, mock_events_list): + """Test querying the API and fetching events from the server.""" + now = dt_util.now() + + mock_events_list( + { + "items": [ + { + "summary": "Event title", + "start": {"dateTime": now.isoformat()}, + "end": { + "dateTime": (now + dt_util.dt.timedelta(minutes=5)).isoformat() + }, + } + ], + } + ) + assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) + await hass.async_block_till_done() + + start = (now - dt_util.dt.timedelta(minutes=60)).isoformat() + end = (now + dt_util.dt.timedelta(minutes=60)).isoformat() + + client = await hass_client() + response = await client.get(f"/api/calendars/{TEST_ENTITY}?start={start}&end={end}") + assert response.status == HTTPStatus.OK + events = await response.json() + assert len(events) == 1 + assert "summary" in events[0] + assert events[0]["summary"] == "Event title" + + +def create_ignore_avail_calendar() -> dict[str, Any]: + """Create a calendar with ignore_availability set.""" + calendar = TEST_CALENDAR.copy() + calendar["ignore_availability"] = False + return calendar + + +@pytest.mark.parametrize("test_calendar", [create_ignore_avail_calendar()]) +async def test_opaque_event(hass, hass_client, google_service, mock_events_list): + """Test querying the API and fetching events from the server.""" + now = dt_util.now() + + mock_events_list( + { + "items": [ + { + "summary": "Event title", + "transparency": "opaque", + "start": {"dateTime": now.isoformat()}, + "end": { + "dateTime": (now + dt_util.dt.timedelta(minutes=5)).isoformat() + }, + } + ], + } + ) + assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) + await hass.async_block_till_done() + + start = (now - dt_util.dt.timedelta(minutes=60)).isoformat() + end = (now + dt_util.dt.timedelta(minutes=60)).isoformat() + + client = await hass_client() + response = await client.get(f"/api/calendars/{TEST_ENTITY}?start={start}&end={end}") + assert response.status == HTTPStatus.OK + events = await response.json() + assert len(events) == 1 + assert "summary" in events[0] + assert events[0]["summary"] == "Event title" + + +@pytest.mark.parametrize("test_calendar", [create_ignore_avail_calendar()]) +async def test_transparent_event(hass, hass_client, google_service, mock_events_list): + """Test querying the API and fetching events from the server.""" + now = dt_util.now() + + mock_events_list( + { + "items": [ + { + "summary": "Event title", + "transparency": "transparent", + "start": {"dateTime": now.isoformat()}, + "end": { + "dateTime": (now + dt_util.dt.timedelta(minutes=5)).isoformat() + }, + } + ], + } + ) + assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) + await hass.async_block_till_done() + + start = (now - dt_util.dt.timedelta(minutes=60)).isoformat() + end = (now + dt_util.dt.timedelta(minutes=60)).isoformat() + + client = await hass_client() + response = await client.get(f"/api/calendars/{TEST_ENTITY}?start={start}&end={end}") + assert response.status == HTTPStatus.OK + events = await response.json() + assert events == [] From 99b21613656f4f736d5dc00f300dc4aa71bed5b7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 23 Dec 2021 07:36:45 +0100 Subject: [PATCH 0974/2644] Add input_button support to Alexa (#62592) --- homeassistant/components/alexa/entities.py | 2 ++ homeassistant/components/alexa/handlers.py | 3 +++ tests/components/alexa/test_smart_home.py | 16 ++++++++++++---- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 17cdab18df1..1ab24927bcb 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -16,6 +16,7 @@ from homeassistant.components import ( group, image_processing, input_boolean, + input_button, input_number, light, lock, @@ -426,6 +427,7 @@ class SwitchCapabilities(AlexaEntity): @ENTITY_ADAPTERS.register(button.DOMAIN) +@ENTITY_ADAPTERS.register(input_button.DOMAIN) class ButtonCapabilities(AlexaEntity): """Class to represent Button capabilities.""" diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index edd510f7844..c0b0782f62e 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -9,6 +9,7 @@ from homeassistant.components import ( cover, fan, group, + input_button, input_number, light, media_player, @@ -317,6 +318,8 @@ async def async_api_activate(hass, config, directive, context): service = SERVICE_TURN_ON if domain == button.DOMAIN: service = button.SERVICE_PRESS + elif domain == input_button.DOMAIN: + service = input_button.SERVICE_PRESS await hass.services.async_call( domain, diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 12708c9b55a..d74233c2bd9 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -3939,12 +3939,20 @@ async def test_initialize_camera_stream(hass, mock_camera, mock_stream): ) -async def test_button(hass): +@pytest.mark.parametrize( + "domain", + ["button", "input_button"], +) +async def test_button(hass, domain): """Test button discovery.""" - device = ("button.ring_doorbell", STATE_UNKNOWN, {"friendly_name": "Ring Doorbell"}) + device = ( + f"{domain}.ring_doorbell", + STATE_UNKNOWN, + {"friendly_name": "Ring Doorbell"}, + ) appliance = await discovery_test(device, hass) - assert appliance["endpointId"] == "button#ring_doorbell" + assert appliance["endpointId"] == f"{domain}#ring_doorbell" assert appliance["displayCategories"][0] == "ACTIVITY_TRIGGER" assert appliance["friendlyName"] == "Ring Doorbell" @@ -3955,5 +3963,5 @@ async def test_button(hass): assert scene_capability["supportsDeactivation"] is False await assert_scene_controller_works( - "button#ring_doorbell", "button.press", False, hass + f"{domain}#ring_doorbell", f"{domain}.press", False, hass ) From dc47cbd01b17df7259670fec22eebf611f7375f4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 23 Dec 2021 07:38:35 +0100 Subject: [PATCH 0975/2644] Add input_button support to Google Assistant (#62593) --- .../components/google_assistant/const.py | 2 ++ .../components/google_assistant/trait.py | 13 ++++++++++-- .../components/google_assistant/test_trait.py | 21 ++++++++++++------- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index 37154bf5e4d..efeb62deb8e 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -10,6 +10,7 @@ from homeassistant.components import ( group, humidifier, input_boolean, + input_button, input_select, light, lock, @@ -129,6 +130,7 @@ DOMAIN_TO_GOOGLE_TYPES = { group.DOMAIN: TYPE_SWITCH, humidifier.DOMAIN: TYPE_HUMIDIFIER, input_boolean.DOMAIN: TYPE_SWITCH, + input_button.DOMAIN: TYPE_SCENE, input_select.DOMAIN: TYPE_SENSOR, light.DOMAIN: TYPE_LIGHT, lock.DOMAIN: TYPE_LOCK, diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 4b6593abadb..26cce0bd5a5 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -12,6 +12,7 @@ from homeassistant.components import ( fan, group, input_boolean, + input_button, input_select, light, lock, @@ -514,7 +515,12 @@ class SceneTrait(_Trait): @staticmethod def supported(domain, features, device_class, _): """Test if state is supported.""" - return domain in (button.DOMAIN, scene.DOMAIN, script.DOMAIN) + return domain in ( + button.DOMAIN, + input_button.DOMAIN, + scene.DOMAIN, + script.DOMAIN, + ) def sync_attributes(self): """Return scene attributes for a sync request.""" @@ -530,6 +536,8 @@ class SceneTrait(_Trait): service = SERVICE_TURN_ON if self.state.domain == button.DOMAIN: service = button.SERVICE_PRESS + elif self.state.domain == input_button.DOMAIN: + service = input_button.SERVICE_PRESS # Don't block for scripts or buttons, as they can be slow. await self.hass.services.async_call( @@ -537,7 +545,8 @@ class SceneTrait(_Trait): service, {ATTR_ENTITY_ID: self.state.entity_id}, blocking=(not self.config.should_report_state) - and self.state.domain not in (button.DOMAIN, script.DOMAIN), + and self.state.domain + not in (button.DOMAIN, input_button.DOMAIN, script.DOMAIN), context=data.context, ) diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index e20f08f8702..83fcaa59b19 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -13,6 +13,7 @@ from homeassistant.components import ( fan, group, input_boolean, + input_button, input_select, light, lock, @@ -768,24 +769,30 @@ async def test_light_modes(hass): } -async def test_scene_button(hass): - """Test Scene trait support for the button domain.""" - assert helpers.get_google_type(button.DOMAIN, None) is not None - assert trait.SceneTrait.supported(button.DOMAIN, 0, None, None) +@pytest.mark.parametrize( + "component", + [button, input_button], +) +async def test_scene_button(hass, component): + """Test Scene trait support for the (input) button domain.""" + assert helpers.get_google_type(component.DOMAIN, None) is not None + assert trait.SceneTrait.supported(component.DOMAIN, 0, None, None) - trt = trait.SceneTrait(hass, State("button.bla", STATE_UNKNOWN), BASIC_CONFIG) + trt = trait.SceneTrait( + hass, State(f"{component.DOMAIN}.bla", STATE_UNKNOWN), BASIC_CONFIG + ) assert trt.sync_attributes() == {} assert trt.query_attributes() == {} assert trt.can_execute(trait.COMMAND_ACTIVATE_SCENE, {}) - calls = async_mock_service(hass, button.DOMAIN, button.SERVICE_PRESS) + calls = async_mock_service(hass, component.DOMAIN, component.SERVICE_PRESS) await trt.execute(trait.COMMAND_ACTIVATE_SCENE, BASIC_DATA, {}, {}) # We don't wait till button press is done. await hass.async_block_till_done() assert len(calls) == 1 - assert calls[0].data == {ATTR_ENTITY_ID: "button.bla"} + assert calls[0].data == {ATTR_ENTITY_ID: f"{component.DOMAIN}.bla"} async def test_scene_scene(hass): From e9c69682c7f90ad2b19c1941d83b2de627c83056 Mon Sep 17 00:00:00 2001 From: Andre Richter Date: Thu, 23 Dec 2021 07:48:31 +0100 Subject: [PATCH 0976/2644] Fix broken Vallox integration in 2021.12 (#62308) --- homeassistant/components/vallox/__init__.py | 23 +++++++++++++++++++-- homeassistant/components/vallox/const.py | 1 + 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vallox/__init__.py b/homeassistant/components/vallox/__init__.py index 73dc633834e..9beddaaad66 100644 --- a/homeassistant/components/vallox/__init__.py +++ b/homeassistant/components/vallox/__init__.py @@ -1,6 +1,7 @@ """Support for Vallox ventilation units.""" from __future__ import annotations +import asyncio from dataclasses import dataclass, field import ipaddress import logging @@ -13,7 +14,7 @@ from vallox_websocket_api.vallox import get_uuid as calculate_uuid import voluptuous as vol from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STARTED -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.core import CoreState, HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.typing import ConfigType, StateType @@ -25,6 +26,7 @@ from .const import ( DEFAULT_FAN_SPEED_HOME, DEFAULT_NAME, DOMAIN, + INITIAL_COORDINATOR_UPDATE_RETRY_INTERVAL_SECONDS, METRIC_KEY_PROFILE_FAN_SPEED_AWAY, METRIC_KEY_PROFILE_FAN_SPEED_BOOST, METRIC_KEY_PROFILE_FAN_SPEED_HOME, @@ -171,7 +173,24 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.data[DOMAIN] = {"client": client, "coordinator": coordinator, "name": name} async def _async_load_platform_delayed(*_: Any) -> None: - await coordinator.async_refresh() + # We need a successful update before loading the platforms, because platform init code + # derives the UUIDs from the data the coordinator fetches. + warned_once = False + while hass.state == CoreState.running: + await coordinator.async_refresh() + if coordinator.last_update_success: + break + + if not warned_once: + _LOGGER.warning( + "Vallox integration not ready yet; Retrying in background" + ) + warned_once = True + + await asyncio.sleep(INITIAL_COORDINATOR_UPDATE_RETRY_INTERVAL_SECONDS) + else: + return + hass.async_create_task(async_load_platform(hass, "sensor", DOMAIN, {}, config)) hass.async_create_task(async_load_platform(hass, "fan", DOMAIN, {}, config)) diff --git a/homeassistant/components/vallox/const.py b/homeassistant/components/vallox/const.py index aba10188bde..96767bd0e18 100644 --- a/homeassistant/components/vallox/const.py +++ b/homeassistant/components/vallox/const.py @@ -7,6 +7,7 @@ from vallox_websocket_api import PROFILE as VALLOX_PROFILE DOMAIN = "vallox" DEFAULT_NAME = "Vallox" +INITIAL_COORDINATOR_UPDATE_RETRY_INTERVAL_SECONDS = 5 STATE_SCAN_INTERVAL = timedelta(seconds=60) # Common metric keys and (default) values. From 259e454c3e8f16a01e8e10d455bd135c32fd2a1e Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Thu, 23 Dec 2021 07:52:44 +0100 Subject: [PATCH 0977/2644] Azure Event Hub code improvements (#62584) * code improvements to AEH * moved hub back --- .../components/azure_event_hub/__init__.py | 164 ++++++++---------- .../components/azure_event_hub/const.py | 3 + tests/components/azure_event_hub/test_init.py | 7 +- 3 files changed, 83 insertions(+), 91 deletions(-) diff --git a/homeassistant/components/azure_event_hub/__init__.py b/homeassistant/components/azure_event_hub/__init__.py index adbfe68fe4d..71d4d7c2cc4 100644 --- a/homeassistant/components/azure_event_hub/__init__.py +++ b/homeassistant/components/azure_event_hub/__init__.py @@ -3,23 +3,25 @@ from __future__ import annotations import asyncio from collections.abc import Callable +from datetime import datetime import json import logging -import time from typing import Any from azure.eventhub import EventData, EventDataBatch +from azure.eventhub.aio import EventHubProducerClient from azure.eventhub.exceptions import EventHubError import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry, ConfigEntryNotReady -from homeassistant.const import MATCH_ALL, STATE_UNAVAILABLE, STATE_UNKNOWN -from homeassistant.core import Event, HomeAssistant +from homeassistant.const import MATCH_ALL +from homeassistant.core import Event, HomeAssistant, State import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import FILTER_SCHEMA from homeassistant.helpers.event import async_call_later from homeassistant.helpers.json import JSONEncoder from homeassistant.helpers.typing import ConfigType +from homeassistant.util.dt import utcnow from .client import AzureEventHubClient from .const import ( @@ -35,6 +37,7 @@ from .const import ( DATA_HUB, DEFAULT_MAX_DELAY, DOMAIN, + FILTER_STATES, ) _LOGGER = logging.getLogger(__name__) @@ -91,10 +94,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {DATA_FILTER: FILTER_SCHEMA({})}) hub = AzureEventHub( hass, - AzureEventHubClient.from_input(**entry.data), + entry, hass.data[DOMAIN][DATA_FILTER], - entry.options[CONF_SEND_INTERVAL], - entry.options.get(CONF_MAX_DELAY), ) try: await hub.async_test_connection() @@ -124,139 +125,128 @@ class AzureEventHub: def __init__( self, hass: HomeAssistant, - client: AzureEventHubClient, + entry: ConfigEntry, entities_filter: vol.Schema, - send_interval: int, - max_delay: int | None = None, ) -> None: """Initialize the listener.""" self.hass = hass - self.queue: asyncio.PriorityQueue[ # pylint: disable=unsubscriptable-object - tuple[int, tuple[float, Event | None]] - ] = asyncio.PriorityQueue() - self._client = client + self._entry = entry self._entities_filter = entities_filter - self._send_interval = send_interval - self._max_delay = max_delay if max_delay else DEFAULT_MAX_DELAY + + self._client = AzureEventHubClient.from_input(**self._entry.data) + self._send_interval = self._entry.options[CONF_SEND_INTERVAL] + self._max_delay = self._entry.options.get(CONF_MAX_DELAY, DEFAULT_MAX_DELAY) + + self._shutdown = False + self._queue: asyncio.PriorityQueue[ # pylint: disable=unsubscriptable-object + tuple[int, tuple[datetime, State | None]] + ] = asyncio.PriorityQueue() self._listener_remover: Callable[[], None] | None = None self._next_send_remover: Callable[[], None] | None = None - self.shutdown = False async def async_start(self) -> None: """Start the hub. This suppresses logging and register the listener and schedules the first send. + + Suppress the INFO and below logging on the underlying packages, + they are very verbose, even at INFO. """ - # suppress the INFO and below logging on the underlying packages, - # they are very verbose, even at INFO logging.getLogger("uamqp").setLevel(logging.WARNING) logging.getLogger("azure.eventhub").setLevel(logging.WARNING) - self._listener_remover = self.hass.bus.async_listen( MATCH_ALL, self.async_listen ) - # schedule the first send after 10 seconds to capture startup events, - # after that each send will schedule the next after the interval. - self._next_send_remover = async_call_later( - self.hass, self._send_interval, self.async_send - ) + self._schedule_next_send() async def async_stop(self) -> None: - """Shut down the AEH by queueing None and calling send.""" + """Shut down the AEH by queueing None, calling send, join queue.""" if self._next_send_remover: self._next_send_remover() if self._listener_remover: self._listener_remover() - await self.queue.put((3, (time.monotonic(), None))) + await self._queue.put((3, (utcnow(), None))) await self.async_send(None) + await self._queue.join() + + def update_options(self, new_options: dict[str, Any]) -> None: + """Update options.""" + self._send_interval = new_options[CONF_SEND_INTERVAL] async def async_test_connection(self) -> None: """Test the connection to the event hub.""" await self._client.test_connection() - async def async_listen(self, event: Event) -> None: - """Listen for new messages on the bus and queue them for AEH.""" - await self.queue.put((2, (time.monotonic(), event))) - - async def async_send(self, _) -> None: - """Write preprocessed events to eventhub, with retry.""" - async with self._client.client as client: - while not self.queue.empty(): - data_batch, dequeue_count = await self.fill_batch(client) - _LOGGER.debug( - "Sending %d event(s), out of %d events in the queue", - len(data_batch), - dequeue_count, - ) - if data_batch: - try: - await client.send_batch(data_batch) - except EventHubError as exc: - _LOGGER.error("Error in sending events to Event Hub: %s", exc) - finally: - for _ in range(dequeue_count): - self.queue.task_done() - - if not self.shutdown: + def _schedule_next_send(self) -> None: + """Schedule the next send.""" + if not self._shutdown: self._next_send_remover = async_call_later( self.hass, self._send_interval, self.async_send ) - async def fill_batch(self, client) -> tuple[EventDataBatch, int]: - """Return a batch of events formatted for writing. + async def async_listen(self, event: Event) -> None: + """Listen for new messages on the bus and queue them for AEH.""" + if state := event.data.get("new_state"): + await self._queue.put((2, (event.time_fired, state))) + + async def async_send(self, _) -> None: + """Write preprocessed events to eventhub, with retry.""" + async with self._client.client as client: + while not self._queue.empty(): + if event_batch := await self.fill_batch(client): + _LOGGER.debug("Sending %d event(s)", len(event_batch)) + try: + await client.send_batch(event_batch) + except EventHubError as exc: + _LOGGER.error("Error in sending events to Event Hub: %s", exc) + self._schedule_next_send() + + async def fill_batch(self, client: EventHubProducerClient) -> EventDataBatch: + """Return a batch of events formatted for sending to Event Hub. Uses get_nowait instead of await get, because the functions batches and - doesn't wait for each single event, the send function is called. + doesn't wait for each single event. Throws ValueError on add to batch when the EventDataBatch object reaches max_size. Put the item back in the queue and the next batch will include it. """ event_batch = await client.create_batch() - dequeue_count = 0 dropped = 0 - while not self.shutdown: + while not self._shutdown: try: - _, (timestamp, event) = self.queue.get_nowait() + _, event = self._queue.get_nowait() except asyncio.QueueEmpty: break - dequeue_count += 1 - if not event: - self.shutdown = True - break - event_data = self._event_to_filtered_event_data(event) + event_data, dropped = self._parse_event(*event, dropped) if not event_data: continue - if time.monotonic() - timestamp <= self._max_delay + self._send_interval: - try: - event_batch.add(event_data) - except ValueError: - dequeue_count -= 1 - self.queue.task_done() - self.queue.put_nowait((1, (timestamp, event))) - break - else: - dropped += 1 + try: + event_batch.add(event_data) + except ValueError: + self._queue.put_nowait((1, event)) + break if dropped: _LOGGER.warning( "Dropped %d old events, consider filtering messages", dropped ) + return event_batch - return event_batch, dequeue_count - - def _event_to_filtered_event_data(self, event: Event) -> EventData | None: - """Filter event states and create EventData object.""" - state = event.data.get("new_state") - if ( - state is None - or state.state in (STATE_UNKNOWN, "", STATE_UNAVAILABLE) - or not self._entities_filter(state.entity_id) - ): - return None - return EventData(json.dumps(obj=state, cls=JSONEncoder).encode("utf-8")) - - def update_options(self, new_options: dict[str, Any]) -> None: - """Update options.""" - self._send_interval = new_options[CONF_SEND_INTERVAL] + def _parse_event( + self, time_fired: datetime, state: State | None, dropped: int + ) -> tuple[EventData | None, int]: + """Parse event by checking if it needs to be sent, and format it.""" + self._queue.task_done() + if not state: + self._shutdown = True + return None, dropped + if state.state in FILTER_STATES or not self._entities_filter(state.entity_id): + return None, dropped + if (utcnow() - time_fired).seconds > self._max_delay + self._send_interval: + return None, dropped + 1 + return ( + EventData(json.dumps(obj=state, cls=JSONEncoder).encode("utf-8")), + dropped, + ) diff --git a/homeassistant/components/azure_event_hub/const.py b/homeassistant/components/azure_event_hub/const.py index 3aa0765545a..8c90b5daaa0 100644 --- a/homeassistant/components/azure_event_hub/const.py +++ b/homeassistant/components/azure_event_hub/const.py @@ -3,6 +3,8 @@ from __future__ import annotations from typing import Any +from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN + DOMAIN = "azure_event_hub" CONF_USE_CONN_STRING = "use_connection_string" @@ -27,3 +29,4 @@ DEFAULT_OPTIONS: dict[str, Any] = { } ADDITIONAL_ARGS: dict[str, Any] = {"logging_enable": False} +FILTER_STATES = (STATE_UNKNOWN, STATE_UNAVAILABLE, "") diff --git a/tests/components/azure_event_hub/test_init.py b/tests/components/azure_event_hub/test_init.py index 74985266821..cf7226e20b0 100644 --- a/tests/components/azure_event_hub/test_init.py +++ b/tests/components/azure_event_hub/test_init.py @@ -1,7 +1,6 @@ """Test the init functions for AEH.""" from datetime import timedelta import logging -from time import monotonic from unittest.mock import patch from azure.eventhub.exceptions import EventHubError @@ -96,7 +95,7 @@ async def test_send_batch_error(hass, entry_with_one_event, mock_send_batch): await hass.async_block_till_done() mock_send_batch.assert_called_once() mock_send_batch.reset_mock() - + hass.states.async_set("sensor.test2", STATE_ON) async_fire_time_changed( hass, utcnow() + timedelta(seconds=entry_with_one_event.options[CONF_SEND_INTERVAL]), @@ -108,8 +107,8 @@ async def test_send_batch_error(hass, entry_with_one_event, mock_send_batch): async def test_late_event(hass, entry_with_one_event, mock_create_batch): """Test the check on late events.""" with patch( - f"{AZURE_EVENT_HUB_PATH}.time.monotonic", - return_value=monotonic() + timedelta(hours=1).seconds, + f"{AZURE_EVENT_HUB_PATH}.utcnow", + return_value=utcnow() + timedelta(hours=1), ): async_fire_time_changed( hass, From 1edfa2d4261e0868d2ee830bc76d66d9728a9c57 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 22 Dec 2021 23:17:04 -0800 Subject: [PATCH 0978/2644] Bump aiohue to 3.0.8 (#62651) --- homeassistant/components/hue/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index ba2d97f44a5..788d1eb7bed 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==3.0.7"], + "requirements": ["aiohue==3.0.8"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 0743ff415a1..166f54efe9f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -194,7 +194,7 @@ aiohomekit==0.6.10 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.7 +aiohue==3.0.8 # homeassistant.components.imap aioimaplib==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 088ba49f44a..08ad2ad2c30 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -138,7 +138,7 @@ aiohomekit==0.6.10 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.7 +aiohue==3.0.8 # homeassistant.components.apache_kafka aiokafka==0.6.0 From cb2c2d98c3af3b429a7328cbaf0812e1d550decc Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 23 Dec 2021 09:59:31 +0100 Subject: [PATCH 0979/2644] Remove unnecessary checks before calling os.makedirs (#62576) --- homeassistant/components/camera/__init__.py | 3 +-- homeassistant/components/doods/image_processing.py | 3 +-- homeassistant/components/downloader/__init__.py | 3 +-- homeassistant/components/gtfs/sensor.py | 3 +-- homeassistant/components/stream/recorder.py | 3 +-- homeassistant/components/tensorflow/image_processing.py | 3 +-- homeassistant/helpers/storage.py | 3 +-- homeassistant/scripts/ensure_config.py | 2 +- tests/components/homekit/test_homekit.py | 3 +-- 9 files changed, 9 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 286c7957169..45d0cf9371a 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -849,8 +849,7 @@ async def async_handle_snapshot_service( """Executor helper to write image.""" if image_data is None: return - if not os.path.exists(os.path.dirname(to_file)): - os.makedirs(os.path.dirname(to_file), exist_ok=True) + os.makedirs(os.path.dirname(to_file), exist_ok=True) with open(to_file, "wb") as img_file: img_file.write(image_data) diff --git a/homeassistant/components/doods/image_processing.py b/homeassistant/components/doods/image_processing.py index e4398a756ec..cfc2b7c11c7 100644 --- a/homeassistant/components/doods/image_processing.py +++ b/homeassistant/components/doods/image_processing.py @@ -270,8 +270,7 @@ class Doods(ImageProcessingEntity): for path in paths: _LOGGER.info("Saving results image to %s", path) - if not os.path.exists(os.path.dirname(path)): - os.makedirs(os.path.dirname(path), exist_ok=True) + os.makedirs(os.path.dirname(path), exist_ok=True) img.save(path) def process_image(self, image): diff --git a/homeassistant/components/downloader/__init__.py b/homeassistant/components/downloader/__init__.py index 7cc64a1507b..8753d3e06f1 100644 --- a/homeassistant/components/downloader/__init__.py +++ b/homeassistant/components/downloader/__init__.py @@ -110,8 +110,7 @@ def setup(hass, config): subdir_path = os.path.join(download_path, subdir) # Ensure subdir exist - if not os.path.isdir(subdir_path): - os.makedirs(subdir_path) + os.makedirs(subdir_path, exist_ok=True) final_path = os.path.join(subdir_path, filename) diff --git a/homeassistant/components/gtfs/sensor.py b/homeassistant/components/gtfs/sensor.py index 99bc0ca51ed..f3e1d678d48 100644 --- a/homeassistant/components/gtfs/sensor.py +++ b/homeassistant/components/gtfs/sensor.py @@ -492,8 +492,7 @@ def setup_platform( offset: datetime.timedelta = config[CONF_OFFSET] include_tomorrow = config[CONF_TOMORROW] - if not os.path.exists(gtfs_dir): - os.makedirs(gtfs_dir) + os.makedirs(gtfs_dir, exist_ok=True) if not os.path.exists(os.path.join(gtfs_dir, data)): _LOGGER.error("The given GTFS data file/folder was not found") diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py index 2fa612e631c..64a3208edcb 100644 --- a/homeassistant/components/stream/recorder.py +++ b/homeassistant/components/stream/recorder.py @@ -34,8 +34,7 @@ def recorder_save_worker(file_out: str, segments: deque[Segment]) -> None: _LOGGER.error("Recording failed to capture anything") return - if not os.path.exists(os.path.dirname(file_out)): - os.makedirs(os.path.dirname(file_out), exist_ok=True) + os.makedirs(os.path.dirname(file_out), exist_ok=True) pts_adjuster: dict[str, int | None] = {"video": None, "audio": None} output: OutputContainer | None = None diff --git a/homeassistant/components/tensorflow/image_processing.py b/homeassistant/components/tensorflow/image_processing.py index 323dbcb7882..9205b9ae561 100644 --- a/homeassistant/components/tensorflow/image_processing.py +++ b/homeassistant/components/tensorflow/image_processing.py @@ -327,8 +327,7 @@ class TensorFlowImageProcessor(ImageProcessingEntity): for path in paths: _LOGGER.info("Saving results image to %s", path) - if not os.path.exists(os.path.dirname(path)): - os.makedirs(os.path.dirname(path), exist_ok=True) + os.makedirs(os.path.dirname(path), exist_ok=True) img.save(path) def process_image(self, image): diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 323737d5b98..da9aa06ba4a 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -277,8 +277,7 @@ class Store: def _write_data(self, path: str, data: dict) -> None: """Write the data.""" - if not os.path.isdir(os.path.dirname(path)): - os.makedirs(os.path.dirname(path)) + os.makedirs(os.path.dirname(path), exist_ok=True) _LOGGER.debug("Writing data for %s to %s", self.key, path) json_util.save_json( diff --git a/homeassistant/scripts/ensure_config.py b/homeassistant/scripts/ensure_config.py index b78b38735e1..f25639c7986 100644 --- a/homeassistant/scripts/ensure_config.py +++ b/homeassistant/scripts/ensure_config.py @@ -30,7 +30,7 @@ def run(args): # Test if configuration directory exists if not os.path.isdir(config_dir): print("Creating directory", config_dir) - os.makedirs(config_dir) + os.makedirs(config_dir, exist_ok=True) config_path = asyncio.run(async_run(config_dir)) print("Configuration file:", config_path) diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 2d504ad517a..24b4cf5b373 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -1299,8 +1299,7 @@ async def test_homekit_uses_system_zeroconf(hass, hk_driver, mock_async_zeroconf def _write_data(path: str, data: dict) -> None: """Write the data.""" - if not os.path.isdir(os.path.dirname(path)): - os.makedirs(os.path.dirname(path)) + os.makedirs(os.path.dirname(path), exist_ok=True) json_util.save_json(path, data) From 247c220882b49640604ce1224add700754e54e4b Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 23 Dec 2021 10:14:07 +0100 Subject: [PATCH 0980/2644] Bump aiohue to 3.0.9 (#62658) --- homeassistant/components/hue/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index 788d1eb7bed..a7c04ed422d 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==3.0.8"], + "requirements": ["aiohue==3.0.9"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 166f54efe9f..f3641bd96d3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -194,7 +194,7 @@ aiohomekit==0.6.10 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.8 +aiohue==3.0.9 # homeassistant.components.imap aioimaplib==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 08ad2ad2c30..5e9875c36cd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -138,7 +138,7 @@ aiohomekit==0.6.10 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.8 +aiohue==3.0.9 # homeassistant.components.apache_kafka aiokafka==0.6.0 From 8e8e49d3e728cd273120336cd7de541c622f8e23 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 23 Dec 2021 10:29:38 +0100 Subject: [PATCH 0981/2644] Add sensor device classes for apparent and reactive power (#62010) --- homeassistant/components/sensor/__init__.py | 6 ++++++ homeassistant/components/sensor/device_condition.py | 6 ++++++ homeassistant/components/sensor/device_trigger.py | 6 ++++++ homeassistant/components/sensor/strings.json | 4 ++++ homeassistant/const.py | 7 ++++++- tests/components/sensor/test_device_condition.py | 1 + tests/components/sensor/test_device_trigger.py | 2 +- tests/testing_config/custom_components/test/sensor.py | 6 +++++- 8 files changed, 35 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index b115da28800..3bbbe221fcf 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -72,6 +72,9 @@ SCAN_INTERVAL: Final = timedelta(seconds=30) class SensorDeviceClass(StrEnum): """Device class for sensors.""" + # apparent power (VA) + APPARENT_POWER = "apparent_power" + # Air Quality Index AQI = "aqi" @@ -138,6 +141,9 @@ class SensorDeviceClass(StrEnum): # pressure (hPa/mbar) PRESSURE = "pressure" + # reactive power (var) + REACTIVE_POWER = "reactive_power" + # signal strength (dB/dBm) SIGNAL_STRENGTH = "signal_strength" diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index 7ba3e907c60..229e5069069 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -22,6 +22,7 @@ from . import DOMAIN, SensorDeviceClass DEVICE_CLASS_NONE = "none" +CONF_IS_APPARENT_POWER = "is_apparent_power" CONF_IS_BATTERY_LEVEL = "is_battery_level" CONF_IS_CO = "is_carbon_monoxide" CONF_IS_CO2 = "is_carbon_dioxide" @@ -41,6 +42,7 @@ CONF_IS_PM25 = "is_pm25" CONF_IS_POWER = "is_power" CONF_IS_POWER_FACTOR = "is_power_factor" CONF_IS_PRESSURE = "is_pressure" +CONF_IS_REACTIVE_POWER = "is_reactive_power" CONF_IS_SIGNAL_STRENGTH = "is_signal_strength" CONF_IS_SULPHUR_DIOXIDE = "is_sulphur_dioxide" CONF_IS_TEMPERATURE = "is_temperature" @@ -49,6 +51,7 @@ CONF_IS_VOLTAGE = "is_voltage" CONF_IS_VALUE = "is_value" ENTITY_CONDITIONS = { + SensorDeviceClass.APPARENT_POWER: [{CONF_TYPE: CONF_IS_APPARENT_POWER}], SensorDeviceClass.BATTERY: [{CONF_TYPE: CONF_IS_BATTERY_LEVEL}], SensorDeviceClass.CO: [{CONF_TYPE: CONF_IS_CO}], SensorDeviceClass.CO2: [{CONF_TYPE: CONF_IS_CO2}], @@ -68,6 +71,7 @@ ENTITY_CONDITIONS = { SensorDeviceClass.PM10: [{CONF_TYPE: CONF_IS_PM10}], SensorDeviceClass.PM25: [{CONF_TYPE: CONF_IS_PM25}], SensorDeviceClass.PRESSURE: [{CONF_TYPE: CONF_IS_PRESSURE}], + SensorDeviceClass.REACTIVE_POWER: [{CONF_TYPE: CONF_IS_REACTIVE_POWER}], SensorDeviceClass.SIGNAL_STRENGTH: [{CONF_TYPE: CONF_IS_SIGNAL_STRENGTH}], SensorDeviceClass.SULPHUR_DIOXIDE: [{CONF_TYPE: CONF_IS_SULPHUR_DIOXIDE}], SensorDeviceClass.TEMPERATURE: [{CONF_TYPE: CONF_IS_TEMPERATURE}], @@ -84,6 +88,7 @@ CONDITION_SCHEMA = vol.All( vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In( [ + CONF_IS_APPARENT_POWER, CONF_IS_BATTERY_LEVEL, CONF_IS_CO, CONF_IS_CO2, @@ -103,6 +108,7 @@ CONDITION_SCHEMA = vol.All( CONF_IS_PM10, CONF_IS_PM25, CONF_IS_PRESSURE, + CONF_IS_REACTIVE_POWER, CONF_IS_SIGNAL_STRENGTH, CONF_IS_SULPHUR_DIOXIDE, CONF_IS_TEMPERATURE, diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index 1970e8df65b..17d1230221d 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -26,6 +26,7 @@ from . import DOMAIN, SensorDeviceClass DEVICE_CLASS_NONE = "none" +CONF_APPARENT_POWER = "apparent_power" CONF_BATTERY_LEVEL = "battery_level" CONF_CO = "carbon_monoxide" CONF_CO2 = "carbon_dioxide" @@ -45,6 +46,7 @@ CONF_PM25 = "pm25" CONF_POWER = "power" CONF_POWER_FACTOR = "power_factor" CONF_PRESSURE = "pressure" +CONF_REACTIVE_POWER = "reactive_power" CONF_SIGNAL_STRENGTH = "signal_strength" CONF_SULPHUR_DIOXIDE = "sulphur_dioxide" CONF_TEMPERATURE = "temperature" @@ -53,6 +55,7 @@ CONF_VOLTAGE = "voltage" CONF_VALUE = "value" ENTITY_TRIGGERS = { + SensorDeviceClass.APPARENT_POWER: [{CONF_TYPE: CONF_APPARENT_POWER}], SensorDeviceClass.BATTERY: [{CONF_TYPE: CONF_BATTERY_LEVEL}], SensorDeviceClass.CO: [{CONF_TYPE: CONF_CO}], SensorDeviceClass.CO2: [{CONF_TYPE: CONF_CO2}], @@ -72,6 +75,7 @@ ENTITY_TRIGGERS = { SensorDeviceClass.POWER: [{CONF_TYPE: CONF_POWER}], SensorDeviceClass.POWER_FACTOR: [{CONF_TYPE: CONF_POWER_FACTOR}], SensorDeviceClass.PRESSURE: [{CONF_TYPE: CONF_PRESSURE}], + SensorDeviceClass.REACTIVE_POWER: [{CONF_TYPE: CONF_REACTIVE_POWER}], SensorDeviceClass.SIGNAL_STRENGTH: [{CONF_TYPE: CONF_SIGNAL_STRENGTH}], SensorDeviceClass.SULPHUR_DIOXIDE: [{CONF_TYPE: CONF_SULPHUR_DIOXIDE}], SensorDeviceClass.TEMPERATURE: [{CONF_TYPE: CONF_TEMPERATURE}], @@ -89,6 +93,7 @@ TRIGGER_SCHEMA = vol.All( vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In( [ + CONF_APPARENT_POWER, CONF_BATTERY_LEVEL, CONF_CO, CONF_CO2, @@ -108,6 +113,7 @@ TRIGGER_SCHEMA = vol.All( CONF_POWER, CONF_POWER_FACTOR, CONF_PRESSURE, + CONF_REACTIVE_POWER, CONF_SIGNAL_STRENGTH, CONF_SULPHUR_DIOXIDE, CONF_TEMPERATURE, diff --git a/homeassistant/components/sensor/strings.json b/homeassistant/components/sensor/strings.json index df2dc3560af..f09f2c80db0 100644 --- a/homeassistant/components/sensor/strings.json +++ b/homeassistant/components/sensor/strings.json @@ -2,6 +2,7 @@ "title": "Sensor", "device_automation": { "condition_type": { + "is_apparent_power": "Current {entity_name} apparent power", "is_battery_level": "Current {entity_name} battery level", "is_carbon_monoxide": "Current {entity_name} carbon monoxide concentration level", "is_carbon_dioxide": "Current {entity_name} carbon dioxide concentration level", @@ -17,6 +18,7 @@ "is_pm25": "Current {entity_name} PM2.5 concentration level", "is_power": "Current {entity_name} power", "is_pressure": "Current {entity_name} pressure", + "is_reactive_power": "Current {entity_name} reactive power", "is_signal_strength": "Current {entity_name} signal strength", "is_sulphur_dioxide": "Current {entity_name} sulphur dioxide concentration level", "is_temperature": "Current {entity_name} temperature", @@ -29,6 +31,7 @@ "is_value": "Current {entity_name} value" }, "trigger_type": { + "apparent_power": "{entity_name} apparent power changes", "battery_level": "{entity_name} battery level changes", "carbon_monoxide": "{entity_name} carbon monoxide concentration changes", "carbon_dioxide": "{entity_name} carbon dioxide concentration changes", @@ -44,6 +47,7 @@ "pm25": "{entity_name} PM2.5 concentration changes", "power": "{entity_name} power changes", "pressure": "{entity_name} pressure changes", + "reactive_power": "{entity_name} reactive power changes", "signal_strength": "{entity_name} signal strength changes", "sulphur_dioxide": "{entity_name} sulphur dioxide concentration changes", "temperature": "{entity_name} temperature changes", diff --git a/homeassistant/const.py b/homeassistant/const.py index 2b9302c0fc9..ffe472acec9 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -458,12 +458,17 @@ ATTR_TEMPERATURE: Final = "temperature" # #### UNITS OF MEASUREMENT #### +# Apparent power units +POWER_VOLT_AMPERE: Final = "VA" + # Power units POWER_WATT: Final = "W" POWER_KILO_WATT: Final = "kW" -POWER_VOLT_AMPERE: Final = "VA" POWER_BTU_PER_HOUR: Final = "BTU/h" +# Reactive power units +POWER_VOLT_AMPERE_REACTIVE: Final = "var" + # Energy units ENERGY_WATT_HOUR: Final = "Wh" ENERGY_KILO_WATT_HOUR: Final = "kWh" diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py index ebbeb5ee616..d2be84e1233 100644 --- a/tests/components/sensor/test_device_condition.py +++ b/tests/components/sensor/test_device_condition.py @@ -77,6 +77,7 @@ async def test_get_conditions(hass, device_reg, entity_reg, enable_custom_integr conditions = await async_get_device_automations( hass, DeviceAutomationType.CONDITION, device_entry.id ) + assert len(conditions) == 26 assert conditions == expected_conditions diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index b217016d339..e37b0e9470f 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -81,7 +81,7 @@ async def test_get_triggers(hass, device_reg, entity_reg, enable_custom_integrat triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, device_entry.id ) - assert len(triggers) == 24 + assert len(triggers) == 26 assert triggers == expected_triggers diff --git a/tests/testing_config/custom_components/test/sensor.py b/tests/testing_config/custom_components/test/sensor.py index 436bafc11ff..4ad2580ad8b 100644 --- a/tests/testing_config/custom_components/test/sensor.py +++ b/tests/testing_config/custom_components/test/sensor.py @@ -13,6 +13,8 @@ from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, FREQUENCY_GIGAHERTZ, PERCENTAGE, + POWER_VOLT_AMPERE, + POWER_VOLT_AMPERE_REACTIVE, PRESSURE_HPA, SIGNAL_STRENGTH_DECIBELS, VOLUME_CUBIC_METERS, @@ -23,6 +25,7 @@ from tests.common import MockEntity DEVICE_CLASSES.append("none") UNITS_OF_MEASUREMENT = { + SensorDeviceClass.APPARENT_POWER: POWER_VOLT_AMPERE, # apparent power (VA) SensorDeviceClass.BATTERY: PERCENTAGE, # % of battery that is left SensorDeviceClass.CO: CONCENTRATION_PARTS_PER_MILLION, # ppm of CO concentration SensorDeviceClass.CO2: CONCENTRATION_PARTS_PER_MILLION, # ppm of CO2 concentration @@ -41,9 +44,10 @@ UNITS_OF_MEASUREMENT = { SensorDeviceClass.PRESSURE: PRESSURE_HPA, # pressure (hPa/mbar) SensorDeviceClass.POWER: "kW", # power (W/kW) SensorDeviceClass.CURRENT: "A", # current (A) - SensorDeviceClass.ENERGY: "kWh", # energy (Wh/kWh) + SensorDeviceClass.ENERGY: "kWh", # energy (Wh/kWh/MWh) SensorDeviceClass.FREQUENCY: FREQUENCY_GIGAHERTZ, # energy (Hz/kHz/MHz/GHz) SensorDeviceClass.POWER_FACTOR: PERCENTAGE, # power factor (no unit, min: -1.0, max: 1.0) + SensorDeviceClass.REACTIVE_POWER: POWER_VOLT_AMPERE_REACTIVE, # reactive power (var) SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, # µg/m³ of vocs SensorDeviceClass.VOLTAGE: "V", # voltage (V) SensorDeviceClass.GAS: VOLUME_CUBIC_METERS, # gas (m³) From fa7739937dd53e95a40b0318c59114d2748c0855 Mon Sep 17 00:00:00 2001 From: Tom Brien Date: Thu, 23 Dec 2021 09:52:22 +0000 Subject: [PATCH 0982/2644] Provide clearer feedback in Coinbase when authentication fails (#62627) --- .../components/coinbase/config_flow.py | 21 +++++++ .../components/coinbase/strings.json | 2 + .../components/coinbase/translations/en.json | 6 +- tests/components/coinbase/test_config_flow.py | 60 +++++++++++++++++-- 4 files changed, 81 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/coinbase/config_flow.py b/homeassistant/components/coinbase/config_flow.py index 5901aeeed9a..37311bbd1af 100644 --- a/homeassistant/components/coinbase/config_flow.py +++ b/homeassistant/components/coinbase/config_flow.py @@ -51,6 +51,15 @@ async def validate_api(hass: core.HomeAssistant, data): get_user_from_client, data[CONF_API_KEY], data[CONF_API_TOKEN] ) except AuthenticationError as error: + if "api key" in str(error): + _LOGGER.debug("Coinbase rejected API credentials due to an invalid API key") + raise InvalidKey from error + if "invalid signature" in str(error): + _LOGGER.debug( + "Coinbase rejected API credentials due to an invalid API secret" + ) + raise InvalidSecret from error + _LOGGER.debug("Coinbase rejected API credentials due to an unknown error") raise InvalidAuth from error except ConnectionError as error: raise CannotConnect from error @@ -110,6 +119,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): info = await validate_api(self.hass, user_input) except CannotConnect: errors["base"] = "cannot_connect" + except InvalidKey: + errors["base"] = "invalid_auth_key" + except InvalidSecret: + errors["base"] = "invalid_auth_secret" except InvalidAuth: errors["base"] = "invalid_auth" except Exception: # pylint: disable=broad-except @@ -218,6 +231,14 @@ class InvalidAuth(exceptions.HomeAssistantError): """Error to indicate there is invalid auth.""" +class InvalidSecret(exceptions.HomeAssistantError): + """Error to indicate auth failed due to invalid secret.""" + + +class InvalidKey(exceptions.HomeAssistantError): + """Error to indicate auth failed due to invalid key.""" + + class AlreadyConfigured(exceptions.HomeAssistantError): """Error to indicate Coinbase API Key is already configured.""" diff --git a/homeassistant/components/coinbase/strings.json b/homeassistant/components/coinbase/strings.json index ce80db35918..3e0b986365c 100644 --- a/homeassistant/components/coinbase/strings.json +++ b/homeassistant/components/coinbase/strings.json @@ -13,6 +13,8 @@ "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "invalid_auth_key": "API credentials rejected by Coinbase due to an invalid API Key.", + "invalid_auth_secret": "API credentials rejected by Coinbase due to an invalid API Secret.", "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { diff --git a/homeassistant/components/coinbase/translations/en.json b/homeassistant/components/coinbase/translations/en.json index 0c5b296bce0..fe7370cb016 100644 --- a/homeassistant/components/coinbase/translations/en.json +++ b/homeassistant/components/coinbase/translations/en.json @@ -6,15 +6,15 @@ "error": { "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication", + "invalid_auth_key": "API credentials rejected by Coinbase due to an invalid API Key.", + "invalid_auth_secret": "API credentials rejected by Coinbase due to an invalid API Secret.", "unknown": "Unexpected error" }, "step": { "user": { "data": { "api_key": "API Key", - "api_token": "API Secret", - "currencies": "Account Balance Currencies", - "exchange_rates": "Exchange Rates" + "api_token": "API Secret" }, "description": "Please enter the details of your API key as provided by Coinbase.", "title": "Coinbase API Key Details" diff --git a/tests/components/coinbase/test_config_flow.py b/tests/components/coinbase/test_config_flow.py index e487cb5d837..fff03797fbb 100644 --- a/tests/components/coinbase/test_config_flow.py +++ b/tests/components/coinbase/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Coinbase config flow.""" +import logging from unittest.mock import patch from coinbase.wallet.error import AuthenticationError @@ -63,23 +64,25 @@ async def test_form(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_invalid_auth(hass): +async def test_form_invalid_auth(hass, caplog): """Test we handle invalid auth.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) + caplog.set_level(logging.DEBUG) + response = Response() response.status_code = 401 - api_auth_error = AuthenticationError( + api_auth_error_unknown = AuthenticationError( response, "authentication_error", - "invalid signature", - [{"id": "authentication_error", "message": "invalid signature"}], + "unknown error", + [{"id": "authentication_error", "message": "unknown error"}], ) with patch( "coinbase.wallet.client.Client.get_current_user", - side_effect=api_auth_error, + side_effect=api_auth_error_unknown, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -91,6 +94,53 @@ async def test_form_invalid_auth(hass): assert result2["type"] == "form" assert result2["errors"] == {"base": "invalid_auth"} + assert "Coinbase rejected API credentials due to an unknown error" in caplog.text + + api_auth_error_key = AuthenticationError( + response, + "authentication_error", + "invalid api key", + [{"id": "authentication_error", "message": "invalid api key"}], + ) + with patch( + "coinbase.wallet.client.Client.get_current_user", + side_effect=api_auth_error_key, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_API_KEY: "123456", + CONF_API_TOKEN: "AbCDeF", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth_key"} + assert "Coinbase rejected API credentials due to an invalid API key" in caplog.text + + api_auth_error_secret = AuthenticationError( + response, + "authentication_error", + "invalid signature", + [{"id": "authentication_error", "message": "invalid signature"}], + ) + with patch( + "coinbase.wallet.client.Client.get_current_user", + side_effect=api_auth_error_secret, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_API_KEY: "123456", + CONF_API_TOKEN: "AbCDeF", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth_secret"} + assert ( + "Coinbase rejected API credentials due to an invalid API secret" in caplog.text + ) async def test_form_cannot_connect(hass): From b6682b30890f41e0d0b48527b5db3ef639a5eebc Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 23 Dec 2021 11:35:01 +0100 Subject: [PATCH 0983/2644] Correct extra attributes trafikverket_train (#62636) --- .../components/trafikverket_train/sensor.py | 50 ++++++++++++------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/trafikverket_train/sensor.py b/homeassistant/components/trafikverket_train/sensor.py index 57d02e95582..9fddeead0f7 100644 --- a/homeassistant/components/trafikverket_train/sensor.py +++ b/homeassistant/components/trafikverket_train/sensor.py @@ -14,7 +14,7 @@ from homeassistant.components.sensor import ( from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_WEEKDAY, WEEKDAYS from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.util.dt import get_time_zone +from homeassistant.util.dt import as_utc, get_time_zone _LOGGER = logging.getLogger(__name__) @@ -158,27 +158,39 @@ class TrainSensor(SensorEntity): @property def extra_state_attributes(self): """Return the state attributes.""" - if self._state is None: + if not self._state: return None - state = self._state - other_information = None - if state.other_information is not None: - other_information = ", ".join(state.other_information) - deviations = None - if state.deviations is not None: - deviations = ", ".join(state.deviations) - if self._delay_in_minutes is not None: - self._delay_in_minutes = self._delay_in_minutes.total_seconds() / 60 - return { + attributes = { ATTR_DEPARTURE_STATE: self._departure_state, - ATTR_CANCELED: state.canceled, - ATTR_DELAY_TIME: self._delay_in_minutes, - ATTR_PLANNED_TIME: state.advertised_time_at_location, - ATTR_ESTIMATED_TIME: state.estimated_time_at_location, - ATTR_ACTUAL_TIME: state.time_at_location, - ATTR_OTHER_INFORMATION: other_information, - ATTR_DEVIATIONS: deviations, + ATTR_CANCELED: self._state.canceled, + ATTR_DELAY_TIME: None, + ATTR_PLANNED_TIME: None, + ATTR_ESTIMATED_TIME: None, + ATTR_ACTUAL_TIME: None, + ATTR_OTHER_INFORMATION: None, + ATTR_DEVIATIONS: None, } + if self._state.other_information: + attributes[ATTR_OTHER_INFORMATION] = ", ".join( + self._state.other_information + ) + if self._state.deviations: + attributes[ATTR_DEVIATIONS] = ", ".join(self._state.deviations) + if self._delay_in_minutes: + attributes[ATTR_DELAY_TIME] = self._delay_in_minutes.total_seconds() / 60 + if self._state.advertised_time_at_location: + attributes[ATTR_PLANNED_TIME] = as_utc( + self._state.advertised_time_at_location.astimezone(self._timezone) + ).isoformat() + if self._state.estimated_time_at_location: + attributes[ATTR_ESTIMATED_TIME] = as_utc( + self._state.estimated_time_at_location.astimezone(self._timezone) + ).isoformat() + if self._state.time_at_location: + attributes[ATTR_ACTUAL_TIME] = as_utc( + self._state.time_at_location.astimezone(self._timezone) + ).isoformat() + return attributes @property def name(self): From aa9746808e58e2596d2e54eb55c26f6c49b51783 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 23 Dec 2021 12:18:53 +0100 Subject: [PATCH 0984/2644] Bump aiohue to 3.0.10 (#62664) --- homeassistant/components/hue/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index a7c04ed422d..7a69637fbb1 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==3.0.9"], + "requirements": ["aiohue==3.0.10"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index f3641bd96d3..1cc4e523559 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -194,7 +194,7 @@ aiohomekit==0.6.10 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.9 +aiohue==3.0.10 # homeassistant.components.imap aioimaplib==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5e9875c36cd..22bd5c42d84 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -138,7 +138,7 @@ aiohomekit==0.6.10 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==3.0.9 +aiohue==3.0.10 # homeassistant.components.apache_kafka aiokafka==0.6.0 From 8e759bb2674b006b75d80fcfdfe718ccdd6379d3 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 23 Dec 2021 14:04:10 +0100 Subject: [PATCH 0985/2644] Adjust Hue retry logic to changes in the aiohue library (#62665) --- homeassistant/components/hue/bridge.py | 64 ++++++-------------------- 1 file changed, 15 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index cda639bcf6c..3a529c15cf3 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -3,13 +3,12 @@ from __future__ import annotations import asyncio from collections.abc import Callable -from http import HTTPStatus import logging from typing import Any from aiohttp import client_exceptions from aiohue import HueBridgeV1, HueBridgeV2, LinkButtonNotPressed, Unauthorized -from aiohue.errors import AiohueException +from aiohue.errors import AiohueException, BridgeBusy import async_timeout from homeassistant import core @@ -44,9 +43,6 @@ class HueBridge: self.config_entry = config_entry self.hass = hass self.authorized = False - self.parallel_updates_semaphore = asyncio.Semaphore( - 3 if self.api_version == 1 else 10 - ) # Jobs to be executed when API is reset. self.reset_jobs: list[core.CALLBACK_TYPE] = [] self.sensor_manager: SensorManager | None = None @@ -89,6 +85,7 @@ class HueBridge: client_exceptions.ClientOSError, client_exceptions.ServerDisconnectedError, client_exceptions.ContentTypeError, + BridgeBusy, ) as err: raise ConfigEntryNotReady( f"Error connecting to the Hue bridge at {self.host}" @@ -121,50 +118,19 @@ class HueBridge: async def async_request_call( self, task: Callable, *args, allowed_errors: list[str] | None = None, **kwargs ) -> Any: - """Limit parallel requests to Hue hub. - - The Hue hub can only handle a certain amount of parallel requests, total. - Although we limit our parallel requests, we still will run into issues because - other products are hitting up Hue. - - ClientOSError means hub closed the socket on us. - ContentResponseError means hub raised an error. - Since we don't make bad requests, this is on them. - """ - max_tries = 5 - async with self.parallel_updates_semaphore: - for tries in range(max_tries): - try: - return await task(*args, **kwargs) - except AiohueException as err: - # The new V2 api is a bit more fanatic with throwing errors - # some of which we accept in certain conditions - # handle that here. Note that these errors are strings and do not have - # an identifier or something. - if allowed_errors is not None and str(err) in allowed_errors: - # log only - self.logger.debug( - "Ignored error/warning from Hue API: %s", str(err) - ) - return None - raise err - except ( - client_exceptions.ClientOSError, - client_exceptions.ClientResponseError, - client_exceptions.ServerDisconnectedError, - ) as err: - if tries == max_tries: - self.logger.error("Request failed %s times, giving up", tries) - raise - - # We only retry if it's a server error. So raise on all 4XX errors. - if ( - isinstance(err, client_exceptions.ClientResponseError) - and err.status < HTTPStatus.INTERNAL_SERVER_ERROR - ): - raise - - await asyncio.sleep(HUB_BUSY_SLEEP * tries) + """Send request to the Hue bridge, optionally omitting error(s).""" + try: + return await task(*args, **kwargs) + except AiohueException as err: + # The (new) Hue api can be a bit fanatic with throwing errors + # some of which we accept in certain conditions + # handle that here. Note that these errors are strings and do not have + # an identifier or something. + if allowed_errors is not None and str(err) in allowed_errors: + # log only + self.logger.debug("Ignored error/warning from Hue API: %s", str(err)) + return None + raise err async def async_reset(self) -> bool: """Reset this bridge to default state. From eb37668036519365715b49c2cedace5f2d722b10 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 23 Dec 2021 14:24:37 +0100 Subject: [PATCH 0986/2644] Fix Hue button events (#62669) --- homeassistant/components/hue/v2/hue_event.py | 24 +++++++++++++++++-- .../components/hue/test_device_trigger_v2.py | 2 +- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/v2/hue_event.py b/homeassistant/components/hue/v2/hue_event.py index 86dabc26660..496507aff4d 100644 --- a/homeassistant/components/hue/v2/hue_event.py +++ b/homeassistant/components/hue/v2/hue_event.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING from aiohue.v2 import HueBridgeV2 from aiohue.v2.controllers.events import EventType -from aiohue.v2.models.button import Button +from aiohue.v2.models.button import Button, ButtonEvent from homeassistant.const import CONF_DEVICE_ID, CONF_ID, CONF_TYPE, CONF_UNIQUE_ID from homeassistant.core import callback @@ -27,6 +27,11 @@ async def async_setup_hue_events(bridge: "HueBridge"): api: HueBridgeV2 = bridge.api # to satisfy typing conf_entry = bridge.config_entry dev_reg = device_registry.async_get(hass) + last_state = { + x.id: x.button.last_event + for x in api.sensors.button.items + if x.button is not None + } # at this time the `button` resource is the only source of hue events btn_controller = api.sensors.button @@ -35,6 +40,21 @@ async def async_setup_hue_events(bridge: "HueBridge"): def handle_button_event(evt_type: EventType, hue_resource: Button) -> None: """Handle event from Hue devices controller.""" LOGGER.debug("Received button event: %s", hue_resource) + + # guard for missing button object on the resource + if hue_resource.button is None: + return + + cur_event = hue_resource.button.last_event + last_event = last_state.get(hue_resource.id) + # ignore the event if the last_event value is exactly the same + # this may happen if some other metadata of the button resource is adjusted + if cur_event == last_event: + return + if cur_event != ButtonEvent.REPEAT: + # do not store repeat event + last_state[hue_resource.id] = cur_event + hue_device = btn_controller.get_device(hue_resource.id) device = dev_reg.async_get_device({(DOMAIN, hue_device.id)}) @@ -44,7 +64,7 @@ async def async_setup_hue_events(bridge: "HueBridge"): CONF_ID: slugify(f"{hue_device.metadata.name}: Button"), CONF_DEVICE_ID: device.id, # type: ignore CONF_UNIQUE_ID: hue_resource.id, - CONF_TYPE: hue_resource.button.last_event.value, + CONF_TYPE: cur_event.value, CONF_SUBTYPE: hue_resource.metadata.control_id, } hass.bus.async_fire(ATTR_HUE_EVENT, data) diff --git a/tests/components/hue/test_device_trigger_v2.py b/tests/components/hue/test_device_trigger_v2.py index e6de70d12cb..a5e60b965fd 100644 --- a/tests/components/hue/test_device_trigger_v2.py +++ b/tests/components/hue/test_device_trigger_v2.py @@ -26,7 +26,7 @@ async def test_hue_event(hass, mock_bridge_v2, v2_resources_test_data): # Emit button update event btn_event = { - "button": {"last_event": "short_release"}, + "button": {"last_event": "initial_press"}, "id": "c658d3d8-a013-4b81-8ac6-78b248537e70", "metadata": {"control_id": 1}, "type": "button", From 66fd7de34a1a3fee91de2dbf99d5fd2137d093e5 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 23 Dec 2021 08:31:28 -0500 Subject: [PATCH 0987/2644] Remove deprecated yaml config from Syncthru (#62541) --- .../components/syncthru/config_flow.py | 4 -- homeassistant/components/syncthru/sensor.py | 41 ++----------------- 2 files changed, 3 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/syncthru/config_flow.py b/homeassistant/components/syncthru/config_flow.py index ac7b317faf8..664de1f6d96 100644 --- a/homeassistant/components/syncthru/config_flow.py +++ b/homeassistant/components/syncthru/config_flow.py @@ -30,10 +30,6 @@ class SyncThruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self._async_show_form(step_id="user") return await self._async_check_and_create("user", user_input) - async def async_step_import(self, user_input=None): - """Handle import initiated flow.""" - return await self.async_step_user(user_input=user_input) - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle SSDP initiated flow.""" await self.async_set_unique_id(discovery_info.upnp[ssdp.ATTR_UPNP_UDN]) diff --git a/homeassistant/components/syncthru/sensor.py b/homeassistant/components/syncthru/sensor.py index a3e24f9ffd8..0df0fd54de2 100644 --- a/homeassistant/components/syncthru/sensor.py +++ b/homeassistant/components/syncthru/sensor.py @@ -1,15 +1,10 @@ """Support for Samsung Printers with SyncThru web interface.""" from __future__ import annotations -import logging - from pysyncthru import SyncThru, SyncthruState -import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import CONF_NAME, CONF_RESOURCE, CONF_URL, PERCENTAGE -import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import SensorEntity +from homeassistant.const import CONF_NAME, PERCENTAGE from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -17,9 +12,7 @@ from homeassistant.helpers.update_coordinator import ( ) from . import device_identifiers -from .const import DEFAULT_MODEL, DEFAULT_NAME_TEMPLATE, DOMAIN - -_LOGGER = logging.getLogger(__name__) +from .const import DOMAIN COLORS = ["black", "cyan", "magenta", "yellow"] DRUM_COLORS = COLORS @@ -42,34 +35,6 @@ SYNCTHRU_STATE_HUMAN = { SyncthruState.ERROR: "error", } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_RESOURCE): cv.url, - vol.Optional( - CONF_NAME, default=DEFAULT_NAME_TEMPLATE.format(DEFAULT_MODEL) - ): cv.string, - } -) - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the SyncThru component.""" - _LOGGER.warning( - "Loading syncthru via platform config is deprecated and no longer " - "necessary as of 0.113; Please remove it from your configuration YAML" - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_URL: config.get(CONF_RESOURCE), - CONF_NAME: config.get(CONF_NAME), - }, - ) - ) - return True - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up from config entry.""" From 540ae4d10e01e17af4acac0a6f284c62c16cd1d2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Dec 2021 03:32:57 -1000 Subject: [PATCH 0988/2644] Update flux_led dhcp matching for older devices (#62577) --- homeassistant/components/flux_led/manifest.json | 4 ++-- homeassistant/generated/dhcp.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 8360f36aa9a..ab8b6b64d2d 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -23,7 +23,7 @@ }, { "macaddress": "ACCF23*", - "hostname": "[ba][lk]*" + "hostname": "[hba][flk]*" }, { "macaddress": "B4E842*", @@ -31,7 +31,7 @@ }, { "macaddress": "F0FE6B*", - "hostname": "[ba][lk]*" + "hostname": "[hba][flk]*" }, { "macaddress": "8CCE4E*", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 7f36fd509a1..e68d7883181 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -89,7 +89,7 @@ DHCP = [ { "domain": "flux_led", "macaddress": "ACCF23*", - "hostname": "[ba][lk]*" + "hostname": "[hba][flk]*" }, { "domain": "flux_led", @@ -99,7 +99,7 @@ DHCP = [ { "domain": "flux_led", "macaddress": "F0FE6B*", - "hostname": "[ba][lk]*" + "hostname": "[hba][flk]*" }, { "domain": "flux_led", From 1bbeaa722ccd2889a96a4d4b54149d456b7c6107 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcus=20L=C3=B6nnberg?= Date: Thu, 23 Dec 2021 15:07:23 +0100 Subject: [PATCH 0989/2644] Support Tuya cover with operation mach_operate (#62650) --- homeassistant/components/tuya/const.py | 2 ++ homeassistant/components/tuya/cover.py | 32 +++++++++++++++++++++----- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 4fc2f37f106..f19109e4104 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -206,6 +206,7 @@ class DPCode(str, Enum): LIGHT = "light" # Light LIGHT_MODE = "light_mode" LOCK = "lock" # Lock / Child lock + MACH_OPERATE = "mach_operate" MATERIAL = "material" # Material MODE = "mode" # Working mode / Mode MOTION_RECORD = "motion_record" @@ -221,6 +222,7 @@ class DPCode(str, Enum): PERCENT_STATE = "percent_state" PERCENT_STATE_2 = "percent_state_2" PERCENT_STATE_3 = "percent_state_3" + POSITION = "position" PHASE_A = "phase_a" PHASE_B = "phase_b" PHASE_C = "phase_c" diff --git a/homeassistant/components/tuya/cover.py b/homeassistant/components/tuya/cover.py index 572d440f937..a450c38e2a0 100644 --- a/homeassistant/components/tuya/cover.py +++ b/homeassistant/components/tuya/cover.py @@ -38,6 +38,9 @@ class TuyaCoverEntityDescription(CoverEntityDescription): current_state_inverse: bool = False current_position: DPCode | tuple[DPCode, ...] | None = None set_position: DPCode | None = None + open_instruction_value: str = "open" + close_instruction_value: str = "close" + stop_instruction_value: str = "stop" COVERS: dict[str, tuple[TuyaCoverEntityDescription, ...]] = { @@ -67,6 +70,16 @@ COVERS: dict[str, tuple[TuyaCoverEntityDescription, ...]] = { set_position=DPCode.PERCENT_CONTROL_3, device_class=CoverDeviceClass.CURTAIN, ), + TuyaCoverEntityDescription( + key=DPCode.MACH_OPERATE, + name="Curtain", + current_position=DPCode.POSITION, + set_position=DPCode.POSITION, + device_class=CoverDeviceClass.CURTAIN, + open_instruction_value="FZ", + close_instruction_value="ZZ", + stop_instruction_value="STOP", + ), # switch_1 is an undocumented code that behaves identically to control # It is used by the Kogan Smart Blinds Driver TuyaCoverEntityDescription( @@ -193,11 +206,11 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity): self._attr_supported_features |= SUPPORT_OPEN | SUPPORT_CLOSE elif device.function[description.key].type == "Enum": data_type = EnumTypeData.from_json(device.function[description.key].values) - if "open" in data_type.range: + if description.open_instruction_value in data_type.range: self._attr_supported_features |= SUPPORT_OPEN - if "close" in data_type.range: + if description.close_instruction_value in data_type.range: self._attr_supported_features |= SUPPORT_CLOSE - if "stop" in data_type.range: + if description.stop_instruction_value in data_type.range: self._attr_supported_features |= SUPPORT_STOP # Determine type to use for setting the position @@ -309,7 +322,7 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity): """Open the cover.""" value: bool | str = True if self.device.function[self.entity_description.key].type == "Enum": - value = "open" + value = self.entity_description.open_instruction_value commands: list[dict[str, str | int]] = [ {"code": self.entity_description.key, "value": value} @@ -336,7 +349,7 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity): """Close cover.""" value: bool | str = False if self.device.function[self.entity_description.key].type == "Enum": - value = "close" + value = self.entity_description.close_instruction_value commands: list[dict[str, str | int]] = [ {"code": self.entity_description.key, "value": value} @@ -381,7 +394,14 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity): def stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" - self._send_command([{"code": self.entity_description.key, "value": "stop"}]) + self._send_command( + [ + { + "code": self.entity_description.key, + "value": self.entity_description.stop_instruction_value, + } + ] + ) def set_cover_tilt_position(self, **kwargs): """Move the cover tilt to a specific position.""" From 772428e70f15057c45e19d9e4f33f6e36a8b843b Mon Sep 17 00:00:00 2001 From: Christian Manivong Date: Thu, 23 Dec 2021 15:08:24 +0100 Subject: [PATCH 0990/2644] Round Hue transition to steps of 100ms (#62619) * Adding round() to transition before firing turn_on, turn_off #62608 --- homeassistant/components/hue/scene.py | 8 ++++---- homeassistant/components/hue/v2/group.py | 16 ++++------------ homeassistant/components/hue/v2/helpers.py | 21 +++++++++++++++++++++ homeassistant/components/hue/v2/light.py | 17 +++++------------ tests/components/hue/test_light_v2.py | 18 +++++++++--------- tests/components/hue/test_scene.py | 4 ++-- 6 files changed, 45 insertions(+), 39 deletions(-) create mode 100644 homeassistant/components/hue/v2/helpers.py diff --git a/homeassistant/components/hue/scene.py b/homeassistant/components/hue/scene.py index d67a3b097c7..90c1bddc970 100644 --- a/homeassistant/components/hue/scene.py +++ b/homeassistant/components/hue/scene.py @@ -8,6 +8,7 @@ from aiohue.v2.controllers.events import EventType from aiohue.v2.controllers.scenes import ScenesController from aiohue.v2.models.scene import Scene as HueScene +from homeassistant.components.light import ATTR_TRANSITION from homeassistant.components.scene import Scene as SceneEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -16,6 +17,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .bridge import HueBridge from .const import DOMAIN from .v2.entity import HueBaseEntity +from .v2.helpers import normalize_hue_transition async def async_setup_entry( @@ -94,11 +96,9 @@ class HueSceneEntity(HueBaseEntity, SceneEntity): async def async_activate(self, **kwargs: Any) -> None: """Activate Hue scene.""" - transition = kwargs.get("transition") - if transition is not None: - # hue transition duration is in milliseconds - transition = int(transition * 1000) + transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION)) dynamic = kwargs.get("dynamic", self.is_dynamic) + await self.bridge.async_request_call( self.controller.recall, self.resource.id, diff --git a/homeassistant/components/hue/v2/group.py b/homeassistant/components/hue/v2/group.py index c5f7ae5d926..add3336764d 100644 --- a/homeassistant/components/hue/v2/group.py +++ b/homeassistant/components/hue/v2/group.py @@ -29,6 +29,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from ..bridge import HueBridge from ..const import CONF_ALLOW_HUE_GROUPS, DOMAIN from .entity import HueBaseEntity +from .helpers import normalize_hue_brightness, normalize_hue_transition ALLOWED_ERRORS = [ "device (groupedLight) has communication issues, command (on) may not have effect", @@ -147,17 +148,11 @@ class GroupedHueLight(HueBaseEntity, LightEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" - transition = kwargs.get(ATTR_TRANSITION) + transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION)) xy_color = kwargs.get(ATTR_XY_COLOR) color_temp = kwargs.get(ATTR_COLOR_TEMP) - brightness = kwargs.get(ATTR_BRIGHTNESS) + brightness = normalize_hue_brightness(kwargs.get(ATTR_BRIGHTNESS)) flash = kwargs.get(ATTR_FLASH) - if brightness is not None: - # Hue uses a range of [0, 100] to control brightness. - brightness = float((brightness / 255) * 100) - if transition is not None: - # hue transition duration is in milliseconds - transition = int(transition * 1000) # NOTE: a grouped_light can only handle turn on/off # To set other features, you'll have to control the attached lights @@ -193,10 +188,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" - transition = kwargs.get(ATTR_TRANSITION) - if transition is not None: - # hue transition duration is in milliseconds - transition = int(transition * 1000) + transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION)) # NOTE: a grouped_light can only handle turn on/off # To set other features, you'll have to control the attached lights diff --git a/homeassistant/components/hue/v2/helpers.py b/homeassistant/components/hue/v2/helpers.py new file mode 100644 index 00000000000..1c26652ce25 --- /dev/null +++ b/homeassistant/components/hue/v2/helpers.py @@ -0,0 +1,21 @@ +"""Helper functions for Philips Hue v2.""" + + +def normalize_hue_brightness(brightness): + """Returns calculated brightness values""" + + if brightness is not None: + # Hue uses a range of [0, 100] to control brightness. + brightness = float((brightness / 255) * 100) + + return brightness + + +def normalize_hue_transition(transition): + """Returns rounded transition values""" + + if transition is not None: + # hue transition duration is in milliseconds and round them to 100ms + transition = int(round(transition, 1) * 1000) + + return transition diff --git a/homeassistant/components/hue/v2/light.py b/homeassistant/components/hue/v2/light.py index afb4c3d88bd..d6578c8ef9a 100644 --- a/homeassistant/components/hue/v2/light.py +++ b/homeassistant/components/hue/v2/light.py @@ -30,6 +30,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from ..bridge import HueBridge from ..const import DOMAIN from .entity import HueBaseEntity +from .helpers import normalize_hue_brightness, normalize_hue_transition ALLOWED_ERRORS = [ "device (light) has communication issues, command (on) may not have effect", @@ -155,17 +156,11 @@ class HueLight(HueBaseEntity, LightEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" - transition = kwargs.get(ATTR_TRANSITION) + transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION)) xy_color = kwargs.get(ATTR_XY_COLOR) color_temp = kwargs.get(ATTR_COLOR_TEMP) - brightness = kwargs.get(ATTR_BRIGHTNESS) + brightness = normalize_hue_brightness(kwargs.get(ATTR_BRIGHTNESS)) flash = kwargs.get(ATTR_FLASH) - if brightness is not None: - # Hue uses a range of [0, 100] to control brightness. - brightness = float((brightness / 255) * 100) - if transition is not None: - # hue transition duration is in milliseconds - transition = int(transition * 1000) await self.bridge.async_request_call( self.controller.set_state, @@ -181,11 +176,9 @@ class HueLight(HueBaseEntity, LightEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" - transition = kwargs.get(ATTR_TRANSITION) + transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION)) flash = kwargs.get(ATTR_FLASH) - if transition is not None: - # hue transition duration is in milliseconds - transition = int(transition * 1000) + await self.bridge.async_request_call( self.controller.set_state, id=self.resource.id, diff --git a/tests/components/hue/test_light_v2.py b/tests/components/hue/test_light_v2.py index 95e3324e81a..936da661dd7 100644 --- a/tests/components/hue/test_light_v2.py +++ b/tests/components/hue/test_light_v2.py @@ -110,16 +110,16 @@ async def test_light_turn_on_service(hass, mock_bridge_v2, v2_resources_test_dat assert test_light.attributes["color_mode"] == COLOR_MODE_COLOR_TEMP assert test_light.attributes["brightness"] == 255 - # test again with sending transition + # test again with sending transition with 250ms which should round up to 200ms await hass.services.async_call( "light", "turn_on", - {"entity_id": test_light_id, "brightness_pct": 50, "transition": 6}, + {"entity_id": test_light_id, "brightness_pct": 50, "transition": 0.25}, blocking=True, ) assert len(mock_bridge_v2.mock_requests) == 2 assert mock_bridge_v2.mock_requests[1]["json"]["on"]["on"] is True - assert mock_bridge_v2.mock_requests[1]["json"]["dynamics"]["duration"] == 6000 + assert mock_bridge_v2.mock_requests[1]["json"]["dynamics"]["duration"] == 200 # test again with sending flash/alert await hass.services.async_call( @@ -170,12 +170,12 @@ async def test_light_turn_off_service(hass, mock_bridge_v2, v2_resources_test_da await hass.services.async_call( "light", "turn_off", - {"entity_id": test_light_id, "transition": 6}, + {"entity_id": test_light_id, "transition": 0.25}, blocking=True, ) assert len(mock_bridge_v2.mock_requests) == 2 assert mock_bridge_v2.mock_requests[1]["json"]["on"]["on"] is False - assert mock_bridge_v2.mock_requests[1]["json"]["dynamics"]["duration"] == 6000 + assert mock_bridge_v2.mock_requests[1]["json"]["dynamics"]["duration"] == 200 async def test_light_added(hass, mock_bridge_v2): @@ -310,7 +310,7 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data): "entity_id": test_light_id, "brightness_pct": 100, "xy_color": (0.123, 0.123), - "transition": 6, + "transition": 0.25, }, blocking=True, ) @@ -325,7 +325,7 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data): assert mock_bridge_v2.mock_requests[index]["json"]["color"]["xy"]["x"] == 0.123 assert mock_bridge_v2.mock_requests[index]["json"]["color"]["xy"]["y"] == 0.123 assert ( - mock_bridge_v2.mock_requests[index]["json"]["dynamics"]["duration"] == 6000 + mock_bridge_v2.mock_requests[index]["json"]["dynamics"]["duration"] == 200 ) # Now generate update events by emitting the json we've sent as incoming events @@ -374,7 +374,7 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data): "turn_off", { "entity_id": test_light_id, - "transition": 6, + "transition": 0.25, }, blocking=True, ) @@ -384,5 +384,5 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data): for index in range(0, 3): assert mock_bridge_v2.mock_requests[index]["json"]["on"]["on"] is False assert ( - mock_bridge_v2.mock_requests[index]["json"]["dynamics"]["duration"] == 6000 + mock_bridge_v2.mock_requests[index]["json"]["dynamics"]["duration"] == 200 ) diff --git a/tests/components/hue/test_scene.py b/tests/components/hue/test_scene.py index 1d270706c99..8982b70fbfb 100644 --- a/tests/components/hue/test_scene.py +++ b/tests/components/hue/test_scene.py @@ -77,13 +77,13 @@ async def test_scene_turn_on_service(hass, mock_bridge_v2, v2_resources_test_dat await hass.services.async_call( "scene", "turn_on", - {"entity_id": test_entity_id, "transition": 6}, + {"entity_id": test_entity_id, "transition": 0.25}, blocking=True, ) assert len(mock_bridge_v2.mock_requests) == 2 assert mock_bridge_v2.mock_requests[1]["json"]["recall"] == { "action": "active", - "duration": 6000, + "duration": 200, } From dff9767da5a796795781424798f60ac0cd428a0c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 23 Dec 2021 15:17:40 +0100 Subject: [PATCH 0991/2644] Use SensorDeviceClass in thermoworks_smoke (#62637) --- homeassistant/components/thermoworks_smoke/sensor.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/thermoworks_smoke/sensor.py b/homeassistant/components/thermoworks_smoke/sensor.py index 2e4ef6e56ec..c8865f061f7 100644 --- a/homeassistant/components/thermoworks_smoke/sensor.py +++ b/homeassistant/components/thermoworks_smoke/sensor.py @@ -11,14 +11,17 @@ from stringcase import camelcase, snakecase import thermoworks_smoke import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorDeviceClass, + SensorEntity, +) from homeassistant.const import ( ATTR_BATTERY_LEVEL, CONF_EMAIL, CONF_EXCLUDE, CONF_MONITORED_CONDITIONS, CONF_PASSWORD, - DEVICE_CLASS_TEMPERATURE, TEMP_FAHRENHEIT, ) import homeassistant.helpers.config_validation as cv @@ -106,7 +109,7 @@ class ThermoworksSmokeSensor(SensorEntity): self._unique_id = f"{serial}-{sensor_type}" self.serial = serial self.mgr = mgr - self._attr_device_class = DEVICE_CLASS_TEMPERATURE + self._attr_device_class = SensorDeviceClass.TEMPERATURE self.update_unit() @property From 430cc6194b4ac1e3374e609c2dda142743595ea7 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 23 Dec 2021 15:35:57 +0100 Subject: [PATCH 0992/2644] Never use availability workaround for certified Hue devices (#62676) --- homeassistant/components/hue/v2/entity.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/hue/v2/entity.py b/homeassistant/components/hue/v2/entity.py index 7371efff3bb..69b299d574f 100644 --- a/homeassistant/components/hue/v2/entity.py +++ b/homeassistant/components/hue/v2/entity.py @@ -142,18 +142,21 @@ class HueBaseEntity(Entity): if self._ignore_availability is not None: # already processed return - cur_state = self.resource.on.on - if self._last_state is None: - self._last_state = cur_state - return + if self.device.product_data.certified: + # certified products report their state correctly + self._ignore_availability = False # some (3th party) Hue lights report their connection status incorrectly # causing the zigbee availability to report as disconnected while in fact # it can be controlled. Although this is in fact something the device manufacturer # should fix, we work around it here. If the light is reported unavailable - # by the zigbee connectivity but the state changesm its considered as a + # by the zigbee connectivity but the state changes its considered as a # malfunctioning device and we report it. # while the user should actually fix this issue instead of ignoring it, we # ignore the availability for this light from this point. + cur_state = self.resource.on.on + if self._last_state is None: + self._last_state = cur_state + return if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id): if ( self._last_state != cur_state @@ -163,7 +166,7 @@ class HueBaseEntity(Entity): # while it was reported as not connected! self.logger.warning( "Light %s changed state while reported as disconnected. " - "This is an indicator that routing is not working properly for this device. " + "This might be an indicator that routing is not working for this device. " "Home Assistant will ignore availability for this light from now on. " "Device details: %s - %s (%s) fw: %s", self.name, From 40f1d53475218265f35398658c7487e8152dc4fc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 23 Dec 2021 15:46:17 +0100 Subject: [PATCH 0993/2644] Add sensor tests to Luftdaten (#62663) --- homeassistant/components/luftdaten/sensor.py | 7 +- tests/components/luftdaten/conftest.py | 2 +- tests/components/luftdaten/test_sensor.py | 118 +++++++++++++++++++ 3 files changed, 121 insertions(+), 6 deletions(-) create mode 100644 tests/components/luftdaten/test_sensor.py diff --git a/homeassistant/components/luftdaten/sensor.py b/homeassistant/components/luftdaten/sensor.py index e0bc64bc918..422e5ed7117 100644 --- a/homeassistant/components/luftdaten/sensor.py +++ b/homeassistant/components/luftdaten/sensor.py @@ -40,7 +40,6 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="humidity", name="Humidity", - icon="mdi:water-percent", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, @@ -48,7 +47,6 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="pressure", name="Pressure", - icon="mdi:arrow-down-bold", native_unit_of_measurement=PRESSURE_PA, device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, @@ -56,7 +54,6 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="pressure_at_sealevel", name="Pressure at sealevel", - icon="mdi:download", native_unit_of_measurement=PRESSURE_PA, device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, @@ -64,15 +61,15 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="P1", name="PM10", - icon="mdi:thought-bubble", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.PM10, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="P2", name="PM2.5", - icon="mdi:thought-bubble-outline", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.PM25, state_class=SensorStateClass.MEASUREMENT, ), ) diff --git a/tests/components/luftdaten/conftest.py b/tests/components/luftdaten/conftest.py index 14d8991e41c..248e1344f1b 100644 --- a/tests/components/luftdaten/conftest.py +++ b/tests/components/luftdaten/conftest.py @@ -62,7 +62,7 @@ def mock_luftdaten() -> Generator[None, MagicMock, None]: "humidity": 34.70, "P1": 8.5, "P2": 4.07, - "pressure_at_sea_level": 103102.13, + "pressure_at_sealevel": 103102.13, "pressure": 98545.00, "temperature": 22.30, } diff --git a/tests/components/luftdaten/test_sensor.py b/tests/components/luftdaten/test_sensor.py new file mode 100644 index 00000000000..ae016615047 --- /dev/null +++ b/tests/components/luftdaten/test_sensor.py @@ -0,0 +1,118 @@ +"""Tests for the sensors provided by the Luftdaten integration.""" +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + SensorDeviceClass, + SensorStateClass, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_FRIENDLY_NAME, + ATTR_ICON, + ATTR_UNIT_OF_MEASUREMENT, + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + PERCENTAGE, + PRESSURE_PA, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry + + +async def test_luftdaten_sensors( + hass: HomeAssistant, + init_integration: MockConfigEntry, +) -> None: + """Test the Luftdaten sensors.""" + entity_registry = er.async_get(hass) + + entry = entity_registry.async_get("sensor.temperature") + assert entry + assert not entry.device_id + assert entry.unique_id == "12345_temperature" + + state = hass.states.get("sensor.temperature") + assert state + assert state.state == "22.3" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Temperature" + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert ATTR_ICON not in state.attributes + + entry = entity_registry.async_get("sensor.humidity") + assert entry + assert not entry.device_id + assert entry.unique_id == "12345_humidity" + + state = hass.states.get("sensor.humidity") + assert state + assert state.state == "34.7" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Humidity" + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.HUMIDITY + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + assert ATTR_ICON not in state.attributes + + entry = entity_registry.async_get("sensor.pressure") + assert entry + assert not entry.device_id + assert entry.unique_id == "12345_pressure" + + state = hass.states.get("sensor.pressure") + assert state + assert state.state == "98545.0" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Pressure" + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_PA + assert ATTR_ICON not in state.attributes + + entry = entity_registry.async_get("sensor.pressure_at_sealevel") + assert entry + assert not entry.device_id + assert entry.unique_id == "12345_pressure_at_sealevel" + + state = hass.states.get("sensor.pressure_at_sealevel") + assert state + assert state.state == "103102.13" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Pressure at sealevel" + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_PA + assert ATTR_ICON not in state.attributes + + entry = entity_registry.async_get("sensor.pm10") + assert entry + assert not entry.device_id + assert entry.unique_id == "12345_P1" + + state = hass.states.get("sensor.pm10") + assert state + assert state.state == "8.5" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "PM10" + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM10 + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + assert ATTR_ICON not in state.attributes + + entry = entity_registry.async_get("sensor.pm2_5") + assert entry + assert not entry.device_id + assert entry.unique_id == "12345_P2" + + state = hass.states.get("sensor.pm2_5") + assert state + assert state.state == "4.07" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "PM2.5" + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM25 + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + assert ATTR_ICON not in state.attributes From c3917fc2506a3b35231e28ab4dd523041b0acb20 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Thu, 23 Dec 2021 07:17:51 -0800 Subject: [PATCH 0994/2644] Remove TaHoma integration (#62607) --- .coveragerc | 1 - CODEOWNERS | 1 - homeassistant/components/tahoma/__init__.py | 149 ----------- .../components/tahoma/binary_sensor.py | 99 ------- homeassistant/components/tahoma/cover.py | 248 ------------------ homeassistant/components/tahoma/lock.py | 88 ------- homeassistant/components/tahoma/manifest.json | 8 - homeassistant/components/tahoma/scene.py | 42 --- homeassistant/components/tahoma/sensor.py | 132 ---------- homeassistant/components/tahoma/switch.py | 123 --------- requirements_all.txt | 3 - script/hassfest/coverage.py | 1 - 12 files changed, 895 deletions(-) delete mode 100644 homeassistant/components/tahoma/__init__.py delete mode 100644 homeassistant/components/tahoma/binary_sensor.py delete mode 100644 homeassistant/components/tahoma/cover.py delete mode 100644 homeassistant/components/tahoma/lock.py delete mode 100644 homeassistant/components/tahoma/manifest.json delete mode 100644 homeassistant/components/tahoma/scene.py delete mode 100644 homeassistant/components/tahoma/sensor.py delete mode 100644 homeassistant/components/tahoma/switch.py diff --git a/.coveragerc b/.coveragerc index c5b0e6c0923..cc96377a65d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1072,7 +1072,6 @@ omit = homeassistant/components/system_bridge/sensor.py homeassistant/components/systemmonitor/sensor.py homeassistant/components/tado/* - homeassistant/components/tahoma/* homeassistant/components/tank_utility/sensor.py homeassistant/components/tankerkoenig/* homeassistant/components/tapsaff/binary_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 75149c273e6..bfad230f22c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -910,7 +910,6 @@ homeassistant/components/tado/* @michaelarnauts @noltari tests/components/tado/* @michaelarnauts @noltari homeassistant/components/tag/* @balloob @dmulcahey tests/components/tag/* @balloob @dmulcahey -homeassistant/components/tahoma/* @philklei homeassistant/components/tailscale/* @frenck tests/components/tailscale/* @frenck homeassistant/components/tankerkoenig/* @guillempages diff --git a/homeassistant/components/tahoma/__init__.py b/homeassistant/components/tahoma/__init__.py deleted file mode 100644 index 8db7b23a8ce..00000000000 --- a/homeassistant/components/tahoma/__init__.py +++ /dev/null @@ -1,149 +0,0 @@ -"""Support for Tahoma devices.""" -from collections import defaultdict -import logging - -from requests.exceptions import RequestException -from tahoma_api import Action, TahomaApi -import voluptuous as vol - -from homeassistant.const import CONF_EXCLUDE, CONF_PASSWORD, CONF_USERNAME -from homeassistant.helpers import config_validation as cv, discovery -from homeassistant.helpers.entity import Entity - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "tahoma" - -TAHOMA_ID_FORMAT = "{}_{}" - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_EXCLUDE, default=[]): vol.All( - cv.ensure_list, [cv.string] - ), - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - -PLATFORMS = ["binary_sensor", "cover", "lock", "scene", "sensor", "switch"] - -TAHOMA_TYPES = { - "io:AwningValanceIOComponent": "cover", - "io:ExteriorVenetianBlindIOComponent": "cover", - "io:DiscreteGarageOpenerIOComponent": "cover", - "io:DiscreteGarageOpenerWithPartialPositionIOComponent": "cover", - "io:HorizontalAwningIOComponent": "cover", - "io:GarageOpenerIOComponent": "cover", - "io:LightIOSystemSensor": "sensor", - "io:OnOffIOComponent": "switch", - "io:OnOffLightIOComponent": "switch", - "io:RollerShutterGenericIOComponent": "cover", - "io:RollerShutterUnoIOComponent": "cover", - "io:RollerShutterVeluxIOComponent": "cover", - "io:RollerShutterWithLowSpeedManagementIOComponent": "cover", - "io:SomfyBasicContactIOSystemSensor": "sensor", - "io:SomfyContactIOSystemSensor": "sensor", - "io:TemperatureIOSystemSensor": "sensor", - "io:VerticalExteriorAwningIOComponent": "cover", - "io:VerticalInteriorBlindVeluxIOComponent": "cover", - "io:WindowOpenerVeluxIOComponent": "cover", - "opendoors:OpenDoorsSmartLockComponent": "lock", - "rtds:RTDSContactSensor": "sensor", - "rtds:RTDSMotionSensor": "sensor", - "rtds:RTDSSmokeSensor": "smoke", - "rts:BlindRTSComponent": "cover", - "rts:CurtainRTSComponent": "cover", - "rts:DualCurtainRTSComponent": "cover", - "rts:ExteriorVenetianBlindRTSComponent": "cover", - "rts:GarageDoor4TRTSComponent": "switch", - "rts:LightRTSComponent": "switch", - "rts:RollerShutterRTSComponent": "cover", - "rts:OnOffRTSComponent": "switch", - "rts:VenetianBlindRTSComponent": "cover", - "somfythermostat:SomfyThermostatTemperatureSensor": "sensor", - "somfythermostat:SomfyThermostatHumiditySensor": "sensor", - "zwave:OnOffLightZWaveComponent": "switch", -} - - -def setup(hass, config): - """Set up Tahoma integration.""" - - conf = config[DOMAIN] - username = conf.get(CONF_USERNAME) - password = conf.get(CONF_PASSWORD) - exclude = conf.get(CONF_EXCLUDE) - try: - api = TahomaApi(username, password) - except RequestException: - _LOGGER.exception("Error when trying to log in to the Tahoma API") - return False - - try: - api.get_setup() - devices = api.get_devices() - scenes = api.get_action_groups() - except RequestException: - _LOGGER.exception("Error when getting devices from the Tahoma API") - return False - - hass.data[DOMAIN] = {"controller": api, "devices": defaultdict(list), "scenes": []} - - for device in devices: - _device = api.get_device(device) - if all(ext not in _device.type for ext in exclude): - device_type = map_tahoma_device(_device) - if device_type is None: - _LOGGER.warning( - "Unsupported type %s for Tahoma device %s", - _device.type, - _device.label, - ) - continue - hass.data[DOMAIN]["devices"][device_type].append(_device) - - for scene in scenes: - hass.data[DOMAIN]["scenes"].append(scene) - - for platform in PLATFORMS: - discovery.load_platform(hass, platform, DOMAIN, {}, config) - - return True - - -def map_tahoma_device(tahoma_device): - """Map Tahoma device types to Home Assistant platforms.""" - return TAHOMA_TYPES.get(tahoma_device.type) - - -class TahomaDevice(Entity): - """Representation of a Tahoma device entity.""" - - def __init__(self, tahoma_device, controller): - """Initialize the device.""" - self.tahoma_device = tahoma_device - self.controller = controller - self._name = self.tahoma_device.label - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def extra_state_attributes(self): - """Return the state attributes of the device.""" - return {"tahoma_device_id": self.tahoma_device.url} - - def apply_action(self, cmd_name, *args): - """Apply Action to Device.""" - - action = Action(self.tahoma_device.url) - action.add_command(cmd_name, *args) - self.controller.apply_actions("HomeAssistant", [action]) diff --git a/homeassistant/components/tahoma/binary_sensor.py b/homeassistant/components/tahoma/binary_sensor.py deleted file mode 100644 index 01e2976bda6..00000000000 --- a/homeassistant/components/tahoma/binary_sensor.py +++ /dev/null @@ -1,99 +0,0 @@ -"""Support for Tahoma binary sensors.""" -from datetime import timedelta -import logging - -from homeassistant.components.binary_sensor import ( - BinarySensorDeviceClass, - BinarySensorEntity, -) -from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON - -from . import DOMAIN as TAHOMA_DOMAIN, TahomaDevice - -_LOGGER = logging.getLogger(__name__) - -SCAN_INTERVAL = timedelta(seconds=120) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Tahoma controller devices.""" - if discovery_info is None: - return - _LOGGER.debug("Setup Tahoma Binary sensor platform") - controller = hass.data[TAHOMA_DOMAIN]["controller"] - devices = [] - for device in hass.data[TAHOMA_DOMAIN]["devices"]["smoke"]: - devices.append(TahomaBinarySensor(device, controller)) - add_entities(devices, True) - - -class TahomaBinarySensor(TahomaDevice, BinarySensorEntity): - """Representation of a Tahoma Binary Sensor.""" - - def __init__(self, tahoma_device, controller): - """Initialize the sensor.""" - super().__init__(tahoma_device, controller) - - self._state = None - self._icon = None - self._battery = None - self._available = False - - @property - def is_on(self): - """Return the state of the sensor.""" - return bool(self._state == STATE_ON) - - @property - def device_class(self): - """Return the class of the device.""" - if self.tahoma_device.type == "rtds:RTDSSmokeSensor": - return BinarySensorDeviceClass.SMOKE - return None - - @property - def icon(self): - """Icon for device by its type.""" - return self._icon - - @property - def extra_state_attributes(self): - """Return the device state attributes.""" - attr = {} - if (super_attr := super().extra_state_attributes) is not None: - attr.update(super_attr) - - if self._battery is not None: - attr[ATTR_BATTERY_LEVEL] = self._battery - return attr - - @property - def available(self): - """Return True if entity is available.""" - return self._available - - def update(self): - """Update the state.""" - self.controller.get_states([self.tahoma_device]) - if self.tahoma_device.type == "rtds:RTDSSmokeSensor": - if self.tahoma_device.active_states["core:SmokeState"] == "notDetected": - self._state = STATE_OFF - else: - self._state = STATE_ON - - if "core:SensorDefectState" in self.tahoma_device.active_states: - # 'lowBattery' for low battery warning. 'dead' for not available. - self._battery = self.tahoma_device.active_states["core:SensorDefectState"] - self._available = bool(self._battery != "dead") - else: - self._battery = None - self._available = True - - if self._state == STATE_ON: - self._icon = "mdi:fire" - elif self._battery == "lowBattery": - self._icon = "mdi:battery-alert" - else: - self._icon = None - - _LOGGER.debug("Update %s, state: %s", self._name, self._state) diff --git a/homeassistant/components/tahoma/cover.py b/homeassistant/components/tahoma/cover.py deleted file mode 100644 index 11881cac6a0..00000000000 --- a/homeassistant/components/tahoma/cover.py +++ /dev/null @@ -1,248 +0,0 @@ -"""Support for Tahoma cover - shutters etc.""" -from datetime import timedelta -import logging - -from homeassistant.components.cover import ATTR_POSITION, CoverDeviceClass, CoverEntity -from homeassistant.util.dt import utcnow - -from . import DOMAIN as TAHOMA_DOMAIN, TahomaDevice - -_LOGGER = logging.getLogger(__name__) - -ATTR_MEM_POS = "memorized_position" -ATTR_RSSI_LEVEL = "rssi_level" -ATTR_LOCK_START_TS = "lock_start_ts" -ATTR_LOCK_END_TS = "lock_end_ts" -ATTR_LOCK_LEVEL = "lock_level" -ATTR_LOCK_ORIG = "lock_originator" - -HORIZONTAL_AWNING = "io:HorizontalAwningIOComponent" - -TAHOMA_DEVICE_CLASSES = { - HORIZONTAL_AWNING: CoverDeviceClass.AWNING, - "io:AwningValanceIOComponent": CoverDeviceClass.AWNING, - "io:DiscreteGarageOpenerWithPartialPositionIOComponent": CoverDeviceClass.GARAGE, - "io:DiscreteGarageOpenerIOComponent": CoverDeviceClass.GARAGE, - "io:ExteriorVenetianBlindIOComponent": CoverDeviceClass.BLIND, - "io:GarageOpenerIOComponent": CoverDeviceClass.GARAGE, - "io:RollerShutterGenericIOComponent": CoverDeviceClass.SHUTTER, - "io:RollerShutterUnoIOComponent": CoverDeviceClass.SHUTTER, - "io:RollerShutterVeluxIOComponent": CoverDeviceClass.SHUTTER, - "io:RollerShutterWithLowSpeedManagementIOComponent": CoverDeviceClass.SHUTTER, - "io:VerticalExteriorAwningIOComponent": CoverDeviceClass.AWNING, - "io:VerticalInteriorBlindVeluxIOComponent": CoverDeviceClass.BLIND, - "io:WindowOpenerVeluxIOComponent": CoverDeviceClass.WINDOW, - "rts:BlindRTSComponent": CoverDeviceClass.BLIND, - "rts:CurtainRTSComponent": CoverDeviceClass.CURTAIN, - "rts:DualCurtainRTSComponent": CoverDeviceClass.CURTAIN, - "rts:ExteriorVenetianBlindRTSComponent": CoverDeviceClass.BLIND, - "rts:RollerShutterRTSComponent": CoverDeviceClass.SHUTTER, - "rts:VenetianBlindRTSComponent": CoverDeviceClass.BLIND, -} - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Tahoma covers.""" - if discovery_info is None: - return - controller = hass.data[TAHOMA_DOMAIN]["controller"] - devices = [] - for device in hass.data[TAHOMA_DOMAIN]["devices"]["cover"]: - devices.append(TahomaCover(device, controller)) - add_entities(devices, True) - - -class TahomaCover(TahomaDevice, CoverEntity): - """Representation a Tahoma Cover.""" - - def __init__(self, tahoma_device, controller): - """Initialize the device.""" - super().__init__(tahoma_device, controller) - - self._closure = 0 - # 100 equals open - self._position = 100 - self._closed = False - self._rssi_level = None - self._icon = None - # Can be 0 and bigger - self._lock_timer = 0 - self._lock_start_ts = None - self._lock_end_ts = None - # Can be 'comfortLevel1', 'comfortLevel2', 'comfortLevel3', - # 'comfortLevel4', 'environmentProtection', 'humanProtection', - # 'userLevel1', 'userLevel2' - self._lock_level = None - # Can be 'LSC', 'SAAC', 'SFC', 'UPS', 'externalGateway', 'localUser', - # 'myself', 'rain', 'security', 'temperature', 'timer', 'user', 'wind' - self._lock_originator = None - - def update(self): - """Update method.""" - self.controller.get_states([self.tahoma_device]) - - # For vertical covers - self._closure = self.tahoma_device.active_states.get("core:ClosureState") - # For horizontal covers - if self._closure is None: - self._closure = self.tahoma_device.active_states.get("core:DeploymentState") - - # For all, if available - if "core:PriorityLockTimerState" in self.tahoma_device.active_states: - old_lock_timer = self._lock_timer - self._lock_timer = self.tahoma_device.active_states[ - "core:PriorityLockTimerState" - ] - # Derive timestamps from _lock_timer, only if not already set or - # something has changed - if self._lock_timer > 0: - _LOGGER.debug("Update %s, lock_timer: %d", self._name, self._lock_timer) - if self._lock_start_ts is None: - self._lock_start_ts = utcnow() - if self._lock_end_ts is None or old_lock_timer != self._lock_timer: - self._lock_end_ts = utcnow() + timedelta(seconds=self._lock_timer) - else: - self._lock_start_ts = None - self._lock_end_ts = None - else: - self._lock_timer = 0 - self._lock_start_ts = None - self._lock_end_ts = None - - self._lock_level = self.tahoma_device.active_states.get( - "io:PriorityLockLevelState" - ) - - self._lock_originator = self.tahoma_device.active_states.get( - "io:PriorityLockOriginatorState" - ) - - self._rssi_level = self.tahoma_device.active_states.get("core:RSSILevelState") - - # Define which icon to use - if self._lock_timer > 0: - if self._lock_originator == "wind": - self._icon = "mdi:weather-windy" - else: - self._icon = "mdi:lock-alert" - else: - self._icon = None - - # Define current position. - # _position: 0 is closed, 100 is fully open. - # 'core:ClosureState': 100 is closed, 0 is fully open. - if self._closure is not None: - if self.tahoma_device.type == HORIZONTAL_AWNING: - self._position = self._closure - else: - self._position = 100 - self._closure - if self._position <= 5: - self._position = 0 - if self._position >= 95: - self._position = 100 - self._closed = self._position == 0 - else: - self._position = None - if "core:OpenClosedState" in self.tahoma_device.active_states: - self._closed = ( - self.tahoma_device.active_states["core:OpenClosedState"] == "closed" - ) - if "core:OpenClosedPartialState" in self.tahoma_device.active_states: - self._closed = ( - self.tahoma_device.active_states["core:OpenClosedPartialState"] - == "closed" - ) - else: - self._closed = False - - _LOGGER.debug("Update %s, position: %d", self._name, self._position) - - @property - def current_cover_position(self): - """Return current position of cover.""" - return self._position - - def set_cover_position(self, **kwargs): - """Move the cover to a specific position.""" - if self.tahoma_device.type == "io:WindowOpenerVeluxIOComponent": - command = "setClosure" - else: - command = "setPosition" - - if self.tahoma_device.type == HORIZONTAL_AWNING: - self.apply_action(command, kwargs.get(ATTR_POSITION, 0)) - else: - self.apply_action(command, 100 - kwargs.get(ATTR_POSITION, 0)) - - @property - def is_closed(self): - """Return if the cover is closed.""" - return self._closed - - @property - def device_class(self): - """Return the class of the device.""" - return TAHOMA_DEVICE_CLASSES.get(self.tahoma_device.type) - - @property - def extra_state_attributes(self): - """Return the device state attributes.""" - attr = {} - if (super_attr := super().extra_state_attributes) is not None: - attr.update(super_attr) - - if "core:Memorized1PositionState" in self.tahoma_device.active_states: - attr[ATTR_MEM_POS] = self.tahoma_device.active_states[ - "core:Memorized1PositionState" - ] - if self._rssi_level is not None: - attr[ATTR_RSSI_LEVEL] = self._rssi_level - if self._lock_start_ts is not None: - attr[ATTR_LOCK_START_TS] = self._lock_start_ts.isoformat() - if self._lock_end_ts is not None: - attr[ATTR_LOCK_END_TS] = self._lock_end_ts.isoformat() - if self._lock_level is not None: - attr[ATTR_LOCK_LEVEL] = self._lock_level - if self._lock_originator is not None: - attr[ATTR_LOCK_ORIG] = self._lock_originator - return attr - - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return self._icon - - def open_cover(self, **kwargs): - """Open the cover.""" - self.apply_action("open") - - def close_cover(self, **kwargs): - """Close the cover.""" - self.apply_action("close") - - def stop_cover(self, **kwargs): - """Stop the cover.""" - if ( - self.tahoma_device.type - == "io:RollerShutterWithLowSpeedManagementIOComponent" - ): - self.apply_action("setPosition", "secured") - elif self.tahoma_device.type in { - "io:ExteriorVenetianBlindIOComponent", - "rts:BlindRTSComponent", - "rts:DualCurtainRTSComponent", - "rts:ExteriorVenetianBlindRTSComponent", - "rts:VenetianBlindRTSComponent", - }: - self.apply_action("my") - elif self.tahoma_device.type in { - HORIZONTAL_AWNING, - "io:AwningValanceIOComponent", - "io:RollerShutterGenericIOComponent", - "io:VerticalExteriorAwningIOComponent", - "io:VerticalInteriorBlindVeluxIOComponent", - "io:WindowOpenerVeluxIOComponent", - }: - self.apply_action("stop") - else: - self.apply_action("stopIdentify") diff --git a/homeassistant/components/tahoma/lock.py b/homeassistant/components/tahoma/lock.py deleted file mode 100644 index dde1227621c..00000000000 --- a/homeassistant/components/tahoma/lock.py +++ /dev/null @@ -1,88 +0,0 @@ -"""Support for Tahoma lock.""" -from datetime import timedelta -import logging - -from homeassistant.components.lock import LockEntity -from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_LOCKED, STATE_UNLOCKED - -from . import DOMAIN as TAHOMA_DOMAIN, TahomaDevice - -_LOGGER = logging.getLogger(__name__) - -SCAN_INTERVAL = timedelta(seconds=120) -TAHOMA_STATE_LOCKED = "locked" - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Tahoma lock.""" - if discovery_info is None: - return - controller = hass.data[TAHOMA_DOMAIN]["controller"] - devices = [] - for device in hass.data[TAHOMA_DOMAIN]["devices"]["lock"]: - devices.append(TahomaLock(device, controller)) - add_entities(devices, True) - - -class TahomaLock(TahomaDevice, LockEntity): - """Representation a Tahoma lock.""" - - def __init__(self, tahoma_device, controller): - """Initialize the device.""" - super().__init__(tahoma_device, controller) - self._lock_status = None - self._available = False - self._battery_level = None - self._name = None - - def update(self): - """Update method.""" - self.controller.get_states([self.tahoma_device]) - self._battery_level = self.tahoma_device.active_states["core:BatteryState"] - self._name = self.tahoma_device.active_states["core:NameState"] - if ( - self.tahoma_device.active_states.get("core:LockedUnlockedState") - == TAHOMA_STATE_LOCKED - ): - self._lock_status = STATE_LOCKED - else: - self._lock_status = STATE_UNLOCKED - self._available = ( - self.tahoma_device.active_states.get("core:AvailabilityState") - == "available" - ) - - def unlock(self, **kwargs): - """Unlock method.""" - _LOGGER.debug("Unlocking %s", self._name) - self.apply_action("unlock") - - def lock(self, **kwargs): - """Lock method.""" - _LOGGER.debug("Locking %s", self._name) - self.apply_action("lock") - - @property - def name(self): - """Return the name of the lock.""" - return self._name - - @property - def available(self): - """Return True if the lock is available.""" - return self._available - - @property - def is_locked(self): - """Return True if the lock is locked.""" - return self._lock_status == STATE_LOCKED - - @property - def extra_state_attributes(self): - """Return the lock state attributes.""" - attr = { - ATTR_BATTERY_LEVEL: self._battery_level, - } - if (super_attr := super().extra_state_attributes) is not None: - attr.update(super_attr) - return attr diff --git a/homeassistant/components/tahoma/manifest.json b/homeassistant/components/tahoma/manifest.json deleted file mode 100644 index 44eb2ca7575..00000000000 --- a/homeassistant/components/tahoma/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "tahoma", - "name": "Tahoma", - "documentation": "https://www.home-assistant.io/integrations/tahoma", - "requirements": ["tahoma-api==0.0.16"], - "codeowners": ["@philklei"], - "iot_class": "cloud_polling" -} diff --git a/homeassistant/components/tahoma/scene.py b/homeassistant/components/tahoma/scene.py deleted file mode 100644 index 3cfa26a5f89..00000000000 --- a/homeassistant/components/tahoma/scene.py +++ /dev/null @@ -1,42 +0,0 @@ -"""Support for Tahoma scenes.""" -from typing import Any - -from homeassistant.components.scene import Scene - -from . import DOMAIN as TAHOMA_DOMAIN - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Tahoma scenes.""" - if discovery_info is None: - return - controller = hass.data[TAHOMA_DOMAIN]["controller"] - scenes = [ - TahomaScene(scene, controller) for scene in hass.data[TAHOMA_DOMAIN]["scenes"] - ] - - add_entities(scenes, True) - - -class TahomaScene(Scene): - """Representation of a Tahoma scene entity.""" - - def __init__(self, tahoma_scene, controller): - """Initialize the scene.""" - self.tahoma_scene = tahoma_scene - self.controller = controller - self._name = self.tahoma_scene.name - - def activate(self, **kwargs: Any) -> None: - """Activate the scene.""" - self.controller.launch_action_group(self.tahoma_scene.oid) - - @property - def name(self): - """Return the name of the scene.""" - return self._name - - @property - def extra_state_attributes(self): - """Return the state attributes of the scene.""" - return {"tahoma_scene_oid": self.tahoma_scene.oid} diff --git a/homeassistant/components/tahoma/sensor.py b/homeassistant/components/tahoma/sensor.py deleted file mode 100644 index 6a7847ec06e..00000000000 --- a/homeassistant/components/tahoma/sensor.py +++ /dev/null @@ -1,132 +0,0 @@ -"""Support for Tahoma sensors.""" -from datetime import timedelta -import logging - -from homeassistant.components.sensor import SensorEntity -from homeassistant.const import ATTR_BATTERY_LEVEL, LIGHT_LUX, PERCENTAGE, TEMP_CELSIUS - -from . import DOMAIN as TAHOMA_DOMAIN, TahomaDevice - -_LOGGER = logging.getLogger(__name__) - -SCAN_INTERVAL = timedelta(seconds=60) - -ATTR_RSSI_LEVEL = "rssi_level" - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Tahoma controller devices.""" - if discovery_info is None: - return - controller = hass.data[TAHOMA_DOMAIN]["controller"] - devices = [] - for device in hass.data[TAHOMA_DOMAIN]["devices"]["sensor"]: - devices.append(TahomaSensor(device, controller)) - add_entities(devices, True) - - -class TahomaSensor(TahomaDevice, SensorEntity): - """Representation of a Tahoma Sensor.""" - - def __init__(self, tahoma_device, controller): - """Initialize the sensor.""" - self.current_value = None - self._available = False - super().__init__(tahoma_device, controller) - - @property - def native_value(self): - """Return the name of the sensor.""" - return self.current_value - - @property - def native_unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - if self.tahoma_device.type == "io:TemperatureIOSystemSensor": - return TEMP_CELSIUS - if self.tahoma_device.type == "io:SomfyContactIOSystemSensor": - return None - if self.tahoma_device.type == "io:SomfyBasicContactIOSystemSensor": - return None - if self.tahoma_device.type == "io:LightIOSystemSensor": - return LIGHT_LUX - if self.tahoma_device.type == "Humidity Sensor": - return PERCENTAGE - if self.tahoma_device.type == "rtds:RTDSContactSensor": - return None - if self.tahoma_device.type == "rtds:RTDSMotionSensor": - return None - if ( - self.tahoma_device.type - == "somfythermostat:SomfyThermostatTemperatureSensor" - ): - return TEMP_CELSIUS - if self.tahoma_device.type == "somfythermostat:SomfyThermostatHumiditySensor": - return PERCENTAGE - - def update(self): - """Update the state.""" - self.controller.get_states([self.tahoma_device]) - if self.tahoma_device.type == "io:LightIOSystemSensor": - self.current_value = self.tahoma_device.active_states["core:LuminanceState"] - self._available = bool( - self.tahoma_device.active_states.get("core:StatusState") == "available" - ) - if self.tahoma_device.type == "io:SomfyContactIOSystemSensor": - self.current_value = self.tahoma_device.active_states["core:ContactState"] - self._available = bool( - self.tahoma_device.active_states.get("core:StatusState") == "available" - ) - if self.tahoma_device.type == "io:SomfyBasicContactIOSystemSensor": - self.current_value = self.tahoma_device.active_states["core:ContactState"] - self._available = bool( - self.tahoma_device.active_states.get("core:StatusState") == "available" - ) - if self.tahoma_device.type == "rtds:RTDSContactSensor": - self.current_value = self.tahoma_device.active_states["core:ContactState"] - self._available = True - if self.tahoma_device.type == "rtds:RTDSMotionSensor": - self.current_value = self.tahoma_device.active_states["core:OccupancyState"] - self._available = True - if self.tahoma_device.type == "io:TemperatureIOSystemSensor": - self.current_value = round( - float(self.tahoma_device.active_states["core:TemperatureState"]), 1 - ) - self._available = True - if ( - self.tahoma_device.type - == "somfythermostat:SomfyThermostatTemperatureSensor" - ): - self.current_value = float( - f"{self.tahoma_device.active_states['core:TemperatureState']:.2f}" - ) - self._available = True - if self.tahoma_device.type == "somfythermostat:SomfyThermostatHumiditySensor": - self.current_value = float( - f"{self.tahoma_device.active_states['core:RelativeHumidityState']:.2f}" - ) - self._available = True - - _LOGGER.debug("Update %s, value: %d", self._name, self.current_value) - - @property - def extra_state_attributes(self): - """Return the device state attributes.""" - attr = {} - if (super_attr := super().extra_state_attributes) is not None: - attr.update(super_attr) - - if "core:RSSILevelState" in self.tahoma_device.active_states: - attr[ATTR_RSSI_LEVEL] = self.tahoma_device.active_states[ - "core:RSSILevelState" - ] - if "core:SensorDefectState" in self.tahoma_device.active_states: - attr[ATTR_BATTERY_LEVEL] = self.tahoma_device.active_states[ - "core:SensorDefectState" - ] - return attr - - @property - def available(self): - """Return True if entity is available.""" - return self._available diff --git a/homeassistant/components/tahoma/switch.py b/homeassistant/components/tahoma/switch.py deleted file mode 100644 index e75c72e8d4b..00000000000 --- a/homeassistant/components/tahoma/switch.py +++ /dev/null @@ -1,123 +0,0 @@ -"""Support for Tahoma switches.""" -import logging - -from homeassistant.components.switch import SwitchEntity -from homeassistant.const import STATE_OFF, STATE_ON - -from . import DOMAIN as TAHOMA_DOMAIN, TahomaDevice - -_LOGGER = logging.getLogger(__name__) - -ATTR_RSSI_LEVEL = "rssi_level" - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Tahoma switches.""" - if discovery_info is None: - return - controller = hass.data[TAHOMA_DOMAIN]["controller"] - devices = [] - for switch in hass.data[TAHOMA_DOMAIN]["devices"]["switch"]: - devices.append(TahomaSwitch(switch, controller)) - add_entities(devices, True) - - -class TahomaSwitch(TahomaDevice, SwitchEntity): - """Representation a Tahoma Switch.""" - - def __init__(self, tahoma_device, controller): - """Initialize the switch.""" - super().__init__(tahoma_device, controller) - self._state = STATE_OFF - self._skip_update = False - self._available = False - - def update(self): - """Update method.""" - # Postpone the immediate state check for changes that take time. - if self._skip_update: - self._skip_update = False - return - - self.controller.get_states([self.tahoma_device]) - - if self.tahoma_device.type == "io:OnOffLightIOComponent": - if self.tahoma_device.active_states.get("core:OnOffState") == "on": - self._state = STATE_ON - else: - self._state = STATE_OFF - - if self.tahoma_device.type == "zwave:OnOffLightZWaveComponent": - if self.tahoma_device.active_states.get("core:OnOffState") == "on": - self._state = STATE_ON - else: - self._state = STATE_OFF - - # A RTS power socket doesn't have a feedback channel, - # so we must assume the socket is available. - if self.tahoma_device.type == "rts:OnOffRTSComponent": - self._available = True - elif self.tahoma_device.type == "zwave:OnOffLightZWaveComponent": - self._available = True - else: - self._available = bool( - self.tahoma_device.active_states.get("core:StatusState") == "available" - ) - - _LOGGER.debug("Update %s, state: %s", self._name, self._state) - - @property - def device_class(self): - """Return the class of the device.""" - if self.tahoma_device.type == "rts:GarageDoor4TRTSComponent": - return "garage" - return None - - def turn_on(self, **kwargs): - """Send the on command.""" - _LOGGER.debug("Turn on: %s", self._name) - if self.tahoma_device.type == "rts:GarageDoor4TRTSComponent": - self.toggle() - else: - self.apply_action("on") - self._skip_update = True - self._state = STATE_ON - - def turn_off(self, **kwargs): - """Send the off command.""" - _LOGGER.debug("Turn off: %s", self._name) - if self.tahoma_device.type == "rts:GarageDoor4TRTSComponent": - return - - self.apply_action("off") - self._skip_update = True - self._state = STATE_OFF - - def toggle(self, **kwargs): - """Click the switch.""" - self.apply_action("cycle") - - @property - def is_on(self): - """Get whether the switch is in on state.""" - if self.tahoma_device.type == "rts:GarageDoor4TRTSComponent": - return False - return bool(self._state == STATE_ON) - - @property - def extra_state_attributes(self): - """Return the device state attributes.""" - attr = {} - if (super_attr := super().extra_state_attributes) is not None: - attr.update(super_attr) - - if "core:RSSILevelState" in self.tahoma_device.active_states: - attr[ATTR_RSSI_LEVEL] = self.tahoma_device.active_states[ - "core:RSSILevelState" - ] - return attr - - @property - def available(self): - """Return True if entity is available.""" - return self._available diff --git a/requirements_all.txt b/requirements_all.txt index 1cc4e523559..d2eaae2ede9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2285,9 +2285,6 @@ synology-srm==0.2.0 # homeassistant.components.system_bridge systembridge==2.2.3 -# homeassistant.components.tahoma -tahoma-api==0.0.16 - # homeassistant.components.tailscale tailscale==0.1.6 diff --git a/script/hassfest/coverage.py b/script/hassfest/coverage.py index e4e8058c69b..ec0b437186e 100644 --- a/script/hassfest/coverage.py +++ b/script/hassfest/coverage.py @@ -52,7 +52,6 @@ ALLOWED_IGNORE_VIOLATIONS = { ("spider", "config_flow.py"), ("starline", "config_flow.py"), ("tado", "config_flow.py"), - ("tahoma", "scene.py"), ("totalconnect", "config_flow.py"), ("tradfri", "config_flow.py"), ("tuya", "config_flow.py"), From c79f13429c556fbb95bf16fd073af3fd56d17cc7 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 23 Dec 2021 16:41:22 +0100 Subject: [PATCH 0995/2644] Fix Hue docstring (#62684) --- homeassistant/components/hue/v2/helpers.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/hue/v2/helpers.py b/homeassistant/components/hue/v2/helpers.py index 1c26652ce25..307e7c55e03 100644 --- a/homeassistant/components/hue/v2/helpers.py +++ b/homeassistant/components/hue/v2/helpers.py @@ -2,8 +2,7 @@ def normalize_hue_brightness(brightness): - """Returns calculated brightness values""" - + """Return calculated brightness values.""" if brightness is not None: # Hue uses a range of [0, 100] to control brightness. brightness = float((brightness / 255) * 100) @@ -12,8 +11,7 @@ def normalize_hue_brightness(brightness): def normalize_hue_transition(transition): - """Returns rounded transition values""" - + """Return rounded transition values.""" if transition is not None: # hue transition duration is in milliseconds and round them to 100ms transition = int(round(transition, 1) * 1000) From 08c66f49830327b022ee01982f0931b78fdf24cd Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 23 Dec 2021 16:41:47 +0100 Subject: [PATCH 0996/2644] Add subfolder globbing to partial linters (#62683) Co-authored-by: epenet --- .github/workflows/ci.yaml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fbaba7ba7e2..be21087b50a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -239,7 +239,8 @@ jobs: shell: bash run: | . venv/bin/activate - pre-commit run --hook-stage manual black --files {homeassistant,tests}/components/${{ needs.changes.outputs.integrations_glob }}/* --show-diff-on-failure + shopt -s globstar + pre-commit run --hook-stage manual black --files {homeassistant,tests}/components/${{ needs.changes.outputs.integrations_glob }}/**/* --show-diff-on-failure lint-flake8: name: Check flake8 @@ -291,7 +292,8 @@ jobs: shell: bash run: | . venv/bin/activate - pre-commit run --hook-stage manual flake8 --files {homeassistant,tests}/components/${{ needs.changes.outputs.integrations_glob }}/* + shopt -s globstar + pre-commit run --hook-stage manual flake8 --files {homeassistant,tests}/components/${{ needs.changes.outputs.integrations_glob }}/**/* lint-isort: name: Check isort @@ -381,7 +383,8 @@ jobs: shell: bash run: | . venv/bin/activate - pre-commit run --hook-stage manual pyupgrade --files {homeassistant,tests}/components/${{ needs.changes.outputs.integrations_glob }}/* --show-diff-on-failure + shopt -s globstar + pre-commit run --hook-stage manual pyupgrade --files {homeassistant,tests}/components/${{ needs.changes.outputs.integrations_glob }}/**/* --show-diff-on-failure - name: Register yamllint problem matcher run: | @@ -437,7 +440,8 @@ jobs: shell: bash run: | . venv/bin/activate - pre-commit run --hook-stage manual bandit --files {homeassistant,tests}/components/${{ needs.changes.outputs.integrations_glob }}/* --show-diff-on-failure + shopt -s globstar + pre-commit run --hook-stage manual bandit --files {homeassistant,tests}/components/${{ needs.changes.outputs.integrations_glob }}/**/* --show-diff-on-failure hassfest: name: Check hassfest From c1ada1754f4056b7244872c6a046588479401215 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 23 Dec 2021 17:05:52 +0100 Subject: [PATCH 0997/2644] Reject MQTT cover discovery using unsupported tilt_invert_state (#62680) --- homeassistant/components/mqtt/cover.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 8b8a0764aca..da07218ff77 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -193,6 +193,7 @@ PLATFORM_SCHEMA = vol.All( ) DISCOVERY_SCHEMA = vol.All( + cv.removed("tilt_invert_state"), _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA), validate_options, ) From f422dd418b95ce5ad1459d5296bf43b67d09dfa0 Mon Sep 17 00:00:00 2001 From: Eugenio Panadero Date: Thu, 23 Dec 2021 17:08:40 +0100 Subject: [PATCH 0998/2644] Fix pvpc_hourly_pricing by changing data source and modernise integration (#62591) --- .../pvpc_hourly_pricing/__init__.py | 48 +- .../pvpc_hourly_pricing/manifest.json | 2 +- .../components/pvpc_hourly_pricing/sensor.py | 260 +++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../pvpc_hourly_pricing/conftest.py | 43 +- .../fixtures/PVPC_CURV_DD_2019_10_26.json | 820 ----------------- .../fixtures/PVPC_CURV_DD_2019_10_27.json | 854 ------------------ .../fixtures/PVPC_CURV_DD_2019_10_29.json | 820 ----------------- .../fixtures/PVPC_CURV_DD_2021_06_01.json | 604 ------------- .../fixtures/PVPC_DATA_2021_06_01.json | 154 ++++ .../pvpc_hourly_pricing/test_config_flow.py | 7 +- .../pvpc_hourly_pricing/test_sensor.py | 107 +-- 13 files changed, 388 insertions(+), 3335 deletions(-) delete mode 100644 tests/components/pvpc_hourly_pricing/fixtures/PVPC_CURV_DD_2019_10_26.json delete mode 100644 tests/components/pvpc_hourly_pricing/fixtures/PVPC_CURV_DD_2019_10_27.json delete mode 100644 tests/components/pvpc_hourly_pricing/fixtures/PVPC_CURV_DD_2019_10_29.json delete mode 100644 tests/components/pvpc_hourly_pricing/fixtures/PVPC_CURV_DD_2021_06_01.json create mode 100644 tests/components/pvpc_hourly_pricing/fixtures/PVPC_DATA_2021_06_01.json diff --git a/homeassistant/components/pvpc_hourly_pricing/__init__.py b/homeassistant/components/pvpc_hourly_pricing/__init__.py index e628dfb9813..05e7c5940b7 100644 --- a/homeassistant/components/pvpc_hourly_pricing/__init__.py +++ b/homeassistant/components/pvpc_hourly_pricing/__init__.py @@ -1,12 +1,15 @@ """The pvpc_hourly_pricing integration to collect Spain official electric prices.""" +from datetime import datetime, timedelta import logging +from typing import Mapping -from aiopvpc import DEFAULT_POWER_KW, TARIFFS +from aiopvpc import DEFAULT_POWER_KW, TARIFFS, PVPCData import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_registry import ( EntityRegistry, @@ -14,6 +17,8 @@ from homeassistant.helpers.entity_registry import ( async_migrate_entries, ) from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util import dt as dt_util from .const import ( ATTR_POWER, @@ -99,6 +104,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_remove(entry.entry_id) return False + coordinator = ElecPricesDataUpdateCoordinator(hass, entry) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator hass.config_entries.async_setup_platforms(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_update_options)) return True @@ -119,4 +128,39 @@ async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok + + +class ElecPricesDataUpdateCoordinator(DataUpdateCoordinator[Mapping[datetime, float]]): + """Class to manage fetching Electricity prices data from API.""" + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize.""" + self.api = PVPCData( + session=async_get_clientsession(hass), + tariff=entry.data[ATTR_TARIFF], + local_timezone=hass.config.time_zone, + power=entry.data[ATTR_POWER], + power_valley=entry.data[ATTR_POWER_P3], + ) + super().__init__( + hass, _LOGGER, name=DOMAIN, update_interval=timedelta(minutes=30) + ) + self._entry = entry + + @property + def entry_id(self) -> str: + """Return entry ID.""" + return self._entry.entry_id + + async def _async_update_data(self) -> Mapping[datetime, float]: + """Update electricity prices from the ESIOS API.""" + prices = await self.api.async_update_prices(dt_util.utcnow()) + self.api.process_state_and_attributes(dt_util.utcnow()) + if not prices: + raise UpdateFailed + + return prices diff --git a/homeassistant/components/pvpc_hourly_pricing/manifest.json b/homeassistant/components/pvpc_hourly_pricing/manifest.json index 86696784638..5c9c06776b8 100644 --- a/homeassistant/components/pvpc_hourly_pricing/manifest.json +++ b/homeassistant/components/pvpc_hourly_pricing/manifest.json @@ -3,7 +3,7 @@ "name": "Spain electricity hourly pricing (PVPC)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/pvpc_hourly_pricing", - "requirements": ["aiopvpc==2.2.4"], + "requirements": ["aiopvpc==3.0.0"], "codeowners": ["@azogue"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/homeassistant/components/pvpc_hourly_pricing/sensor.py b/homeassistant/components/pvpc_hourly_pricing/sensor.py index 544c02571c2..c1925fa12ff 100644 --- a/homeassistant/components/pvpc_hourly_pricing/sensor.py +++ b/homeassistant/components/pvpc_hourly_pricing/sensor.py @@ -1,73 +1,160 @@ """Sensor to collect the reference daily prices of electricity ('PVPC') in Spain.""" from __future__ import annotations +from collections.abc import Mapping +from datetime import datetime import logging -from random import randint from typing import Any -from aiopvpc import PVPCData - -from homeassistant.components.sensor import SensorEntity, SensorStateClass +from homeassistant.components.sensor import ( + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, CURRENCY_EURO, ENERGY_KILO_WATT_HOUR from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_call_later, async_track_time_change -from homeassistant.helpers.restore_state import RestoreEntity -import homeassistant.util.dt as dt_util +from homeassistant.helpers.event import async_track_time_change +from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ATTR_POWER, ATTR_POWER_P3, ATTR_TARIFF +from . import ElecPricesDataUpdateCoordinator +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) - -ATTR_PRICE = "price" -ICON = "mdi:currency-eur" -UNIT = f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}" - -_DEFAULT_TIMEOUT = 10 +PARALLEL_UPDATES = 1 +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="PVPC", + icon="mdi:currency-eur", + native_unit_of_measurement=f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}", + state_class=SensorStateClass.MEASUREMENT, + ), +) +_PRICE_SENSOR_ATTRIBUTES_MAP = { + "tariff": "tariff", + "period": "period", + "available_power": "available_power", + "next_period": "next_period", + "hours_to_next_period": "hours_to_next_period", + "next_better_price": "next_better_price", + "hours_to_better_price": "hours_to_better_price", + "num_better_prices_ahead": "num_better_prices_ahead", + "price_position": "price_position", + "price_ratio": "price_ratio", + "max_price": "max_price", + "max_price_at": "max_price_at", + "min_price": "min_price", + "min_price_at": "min_price_at", + "next_best_at": "next_best_at", + "price_00h": "price_00h", + "price_01h": "price_01h", + "price_02h": "price_02h", + "price_02h_d": "price_02h_d", # only on DST day change with 25h + "price_03h": "price_03h", + "price_04h": "price_04h", + "price_05h": "price_05h", + "price_06h": "price_06h", + "price_07h": "price_07h", + "price_08h": "price_08h", + "price_09h": "price_09h", + "price_10h": "price_10h", + "price_11h": "price_11h", + "price_12h": "price_12h", + "price_13h": "price_13h", + "price_14h": "price_14h", + "price_15h": "price_15h", + "price_16h": "price_16h", + "price_17h": "price_17h", + "price_18h": "price_18h", + "price_19h": "price_19h", + "price_20h": "price_20h", + "price_21h": "price_21h", + "price_22h": "price_22h", + "price_23h": "price_23h", + # only seen in the evening + "next_better_price (next day)": "next_better_price (next day)", + "hours_to_better_price (next day)": "hours_to_better_price (next day)", + "num_better_prices_ahead (next day)": "num_better_prices_ahead (next day)", + "price_position (next day)": "price_position (next day)", + "price_ratio (next day)": "price_ratio (next day)", + "max_price (next day)": "max_price (next day)", + "max_price_at (next day)": "max_price_at (next day)", + "min_price (next day)": "min_price (next day)", + "min_price_at (next day)": "min_price_at (next day)", + "next_best_at (next day)": "next_best_at (next day)", + "price_next_day_00h": "price_next_day_00h", + "price_next_day_01h": "price_next_day_01h", + "price_next_day_02h": "price_next_day_02h", + "price_next_day_02h_d": "price_next_day_02h_d", + "price_next_day_03h": "price_next_day_03h", + "price_next_day_04h": "price_next_day_04h", + "price_next_day_05h": "price_next_day_05h", + "price_next_day_06h": "price_next_day_06h", + "price_next_day_07h": "price_next_day_07h", + "price_next_day_08h": "price_next_day_08h", + "price_next_day_09h": "price_next_day_09h", + "price_next_day_10h": "price_next_day_10h", + "price_next_day_11h": "price_next_day_11h", + "price_next_day_12h": "price_next_day_12h", + "price_next_day_13h": "price_next_day_13h", + "price_next_day_14h": "price_next_day_14h", + "price_next_day_15h": "price_next_day_15h", + "price_next_day_16h": "price_next_day_16h", + "price_next_day_17h": "price_next_day_17h", + "price_next_day_18h": "price_next_day_18h", + "price_next_day_19h": "price_next_day_19h", + "price_next_day_20h": "price_next_day_20h", + "price_next_day_21h": "price_next_day_21h", + "price_next_day_22h": "price_next_day_22h", + "price_next_day_23h": "price_next_day_23h", +} async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the electricity price sensor from config_entry.""" - name = config_entry.data[CONF_NAME] - pvpc_data_handler = PVPCData( - tariff=config_entry.data[ATTR_TARIFF], - power=config_entry.data[ATTR_POWER], - power_valley=config_entry.data[ATTR_POWER_P3], - local_timezone=hass.config.time_zone, - websession=async_get_clientsession(hass), - timeout=_DEFAULT_TIMEOUT, - ) + coordinator: ElecPricesDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + name = entry.data[CONF_NAME] async_add_entities( - [ElecPriceSensor(name, config_entry.unique_id, pvpc_data_handler)], False + [ElecPriceSensor(coordinator, SENSOR_TYPES[0], entry.unique_id, name)] ) -class ElecPriceSensor(RestoreEntity, SensorEntity): +class ElecPriceSensor(CoordinatorEntity, SensorEntity): """Class to hold the prices of electricity as a sensor.""" - _attr_icon = ICON - _attr_native_unit_of_measurement = UNIT - _attr_should_poll = False - _attr_state_class = SensorStateClass.MEASUREMENT + coordinator: ElecPricesDataUpdateCoordinator - def __init__(self, name, unique_id, pvpc_data_handler): - """Initialize the sensor object.""" - self._name = name - self._unique_id = unique_id - self._pvpc_data = pvpc_data_handler - self._num_retries = 0 + def __init__( + self, + coordinator: ElecPricesDataUpdateCoordinator, + description: SensorEntityDescription, + unique_id: str | None, + name: str, + ) -> None: + """Initialize ESIOS sensor.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_attribution = coordinator.api.attribution + self._attr_unique_id = unique_id + self._attr_name = name + self._attr_device_info = DeviceInfo( + configuration_url="https://www.ree.es/es/apidatos", + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, coordinator.entry_id)}, + manufacturer="REE", + name="PVPC (REData API)", + ) + self._state: StateType = None + self._attrs: Mapping[str, Any] = {} async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" await super().async_added_to_hass() - if state := await self.async_get_last_state(): - self._pvpc_data.state = state.state # Update 'state' value in hour changes self.async_on_remove( @@ -75,86 +162,31 @@ class ElecPriceSensor(RestoreEntity, SensorEntity): self.hass, self.update_current_price, second=[0], minute=[0] ) ) - # Update prices at random time, 2 times/hour (don't want to upset API) - random_minute = randint(1, 29) - mins_update = [random_minute, random_minute + 30] - self.async_on_remove( - async_track_time_change( - self.hass, self.async_update_prices, second=[0], minute=mins_update - ) - ) _LOGGER.debug( - "Setup of price sensor %s (%s) with tariff '%s', " - "updating prices each hour at %s min", + "Setup of price sensor %s (%s) with tariff '%s'", self.name, self.entity_id, - self._pvpc_data.tariff, - mins_update, + self.coordinator.api.tariff, ) - now = dt_util.utcnow() - await self.async_update_prices(now) - self.update_current_price(now) - - @property - def unique_id(self) -> str | None: - """Return a unique ID.""" - return self._unique_id - - @property - def name(self) -> str: - """Return the name of the sensor.""" - return self._name - - @property - def native_value(self) -> float: - """Return the state of the sensor.""" - return self._pvpc_data.state - - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self._pvpc_data.state_available - - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return the state attributes.""" - return self._pvpc_data.attributes @callback - def update_current_price(self, now): + def update_current_price(self, now: datetime) -> None: """Update the sensor state, by selecting the current price for this hour.""" - self._pvpc_data.process_state_and_attributes(now) + self.coordinator.api.process_state_and_attributes(now) self.async_write_ha_state() - async def async_update_prices(self, now): - """Update electricity prices from the ESIOS API.""" - prices = await self._pvpc_data.async_update_prices(now) - if not prices and self._pvpc_data.source_available: - self._num_retries += 1 - if self._num_retries > 2: - _LOGGER.warning( - "%s: repeated bad data update, mark component as unavailable source", - self.entity_id, - ) - self._pvpc_data.source_available = False - return + @property + def native_value(self) -> StateType: + """Return the state of the sensor.""" + self._state = self.coordinator.api.state + return self._state - retry_delay = 2 * self._num_retries * self._pvpc_data.timeout - _LOGGER.debug( - "%s: Bad update[retry:%d], will try again in %d s", - self.entity_id, - self._num_retries, - retry_delay, - ) - async_call_later(self.hass, retry_delay, self.async_update_prices) - return - - if not prices: - _LOGGER.debug("%s: data source is not yet available", self.entity_id) - return - - self._num_retries = 0 - if not self._pvpc_data.source_available: - self._pvpc_data.source_available = True - _LOGGER.warning("%s: component has recovered data access", self.entity_id) - self.update_current_price(now) + @property + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """Return the state attributes.""" + self._attrs = { + _PRICE_SENSOR_ATTRIBUTES_MAP[key]: value + for key, value in self.coordinator.api.attributes.items() + if key in _PRICE_SENSOR_ATTRIBUTES_MAP + } + return self._attrs diff --git a/requirements_all.txt b/requirements_all.txt index d2eaae2ede9..f2a1aa593a1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -239,7 +239,7 @@ aiopulse==0.4.3 aiopvapi==1.6.19 # homeassistant.components.pvpc_hourly_pricing -aiopvpc==2.2.4 +aiopvpc==3.0.0 # homeassistant.components.webostv aiopylgtv==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 22bd5c42d84..a222c5e968c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -168,7 +168,7 @@ aiopulse==0.4.3 aiopvapi==1.6.19 # homeassistant.components.pvpc_hourly_pricing -aiopvpc==2.2.4 +aiopvpc==3.0.0 # homeassistant.components.webostv aiopylgtv==0.4.0 diff --git a/tests/components/pvpc_hourly_pricing/conftest.py b/tests/components/pvpc_hourly_pricing/conftest.py index 2421c753518..632284774ee 100644 --- a/tests/components/pvpc_hourly_pricing/conftest.py +++ b/tests/components/pvpc_hourly_pricing/conftest.py @@ -1,4 +1,6 @@ """Tests for the pvpc_hourly_pricing integration.""" +from http import HTTPStatus + import pytest from homeassistant.components.pvpc_hourly_pricing import ATTR_TARIFF, DOMAIN @@ -11,10 +13,7 @@ from homeassistant.const import ( from tests.common import load_fixture from tests.test_util.aiohttp import AiohttpClientMocker -FIXTURE_JSON_DATA_2019_10_26 = "PVPC_CURV_DD_2019_10_26.json" -FIXTURE_JSON_DATA_2019_10_27 = "PVPC_CURV_DD_2019_10_27.json" -FIXTURE_JSON_DATA_2019_10_29 = "PVPC_CURV_DD_2019_10_29.json" -FIXTURE_JSON_DATA_2021_06_01 = "PVPC_CURV_DD_2021_06_01.json" +FIXTURE_JSON_DATA_2021_06_01 = "PVPC_DATA_2021_06_01.json" def check_valid_state(state, tariff: str, value=None, key_attr=None): @@ -27,7 +26,7 @@ def check_valid_state(state, tariff: str, value=None, key_attr=None): try: _ = float(state.state) # safety margins for current electricity price (it shouldn't be out of [0, 0.2]) - assert -0.1 < float(state.state) < 0.3 + assert -0.1 < float(state.state) < 0.5 assert state.attributes[ATTR_TARIFF] == tariff except ValueError: pass @@ -43,28 +42,22 @@ def check_valid_state(state, tariff: str, value=None, key_attr=None): @pytest.fixture def pvpc_aioclient_mock(aioclient_mock: AiohttpClientMocker): """Create a mock config entry.""" - aioclient_mock.get( - "https://api.esios.ree.es/archives/70/download_json?locale=es&date=2019-10-26", - text=load_fixture(f"{DOMAIN}/{FIXTURE_JSON_DATA_2019_10_26}"), - ) - aioclient_mock.get( - "https://api.esios.ree.es/archives/70/download_json?locale=es&date=2019-10-27", - text=load_fixture(f"{DOMAIN}/{FIXTURE_JSON_DATA_2019_10_27}"), - ) - # missing day - aioclient_mock.get( - "https://api.esios.ree.es/archives/70/download_json?locale=es&date=2019-10-28", - text='{"message":"No values for specified archive"}', - ) - aioclient_mock.get( - "https://api.esios.ree.es/archives/70/download_json?locale=es&date=2019-10-29", - text=load_fixture(f"{DOMAIN}/{FIXTURE_JSON_DATA_2019_10_29}"), - ) - + mask_url = "https://apidatos.ree.es/es/datos/mercados/precios-mercados-tiempo-real" + mask_url += "?time_trunc=hour&geo_ids={0}&start_date={1}T00:00&end_date={1}T23:59" # new format for prices >= 2021-06-01 + sample_data = load_fixture(f"{DOMAIN}/{FIXTURE_JSON_DATA_2021_06_01}") + + # tariff variant with different geo_ids=8744 + aioclient_mock.get(mask_url.format(8741, "2021-06-01"), text=sample_data) + aioclient_mock.get(mask_url.format(8744, "2021-06-01"), text=sample_data) + # simulate missing day aioclient_mock.get( - "https://api.esios.ree.es/archives/70/download_json?locale=es&date=2021-06-01", - text=load_fixture(f"{DOMAIN}/{FIXTURE_JSON_DATA_2021_06_01}"), + mask_url.format(8741, "2021-06-02"), + status=HTTPStatus.BAD_GATEWAY, + text=( + '{"errors":[{"code":502,"status":"502","title":"Bad response from ESIOS",' + '"detail":"There are no data for the selected filters."}]}' + ), ) return aioclient_mock diff --git a/tests/components/pvpc_hourly_pricing/fixtures/PVPC_CURV_DD_2019_10_26.json b/tests/components/pvpc_hourly_pricing/fixtures/PVPC_CURV_DD_2019_10_26.json deleted file mode 100644 index dd8c73352d9..00000000000 --- a/tests/components/pvpc_hourly_pricing/fixtures/PVPC_CURV_DD_2019_10_26.json +++ /dev/null @@ -1,820 +0,0 @@ -{ - "PVPC": [ - { - "Dia": "26/10/2019", - "Hora": "00-01", - "GEN": "114,20", - "NOC": "65,17", - "VHC": "69,02", - "COFGEN": "0,000087148314000000", - "COFNOC": "0,000135978057000000", - "COFVHC": "0,000151138804000000", - "PMHGEN": "59,56", - "PMHNOC": "57,22", - "PMHVHC": "59,81", - "SAHGEN": "1,96", - "SAHNOC": "1,89", - "SAHVHC": "1,97", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,14", - "INTGEN": "0,93", - "INTNOC": "0,90", - "INTVHC": "0,94", - "PCAPGEN": "5,54", - "PCAPNOC": "0,93", - "PCAPVHC": "1,31", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "2,01", - "CCVNOC": "1,86", - "CCVVHC": "1,95" - }, - { - "Dia": "26/10/2019", - "Hora": "01-02", - "GEN": "111,01", - "NOC": "62,10", - "VHC": "59,03", - "COFGEN": "0,000072922194000000", - "COFNOC": "0,000124822445000000", - "COFVHC": "0,000160597191000000", - "PMHGEN": "56,23", - "PMHNOC": "54,03", - "PMHVHC": "52,62", - "SAHGEN": "2,14", - "SAHNOC": "2,05", - "SAHVHC": "2,00", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,14", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,94", - "INTNOC": "0,90", - "INTVHC": "0,88", - "PCAPGEN": "5,56", - "PCAPNOC": "0,93", - "PCAPVHC": "0,72", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "0,89", - "CCVGEN": "1,96", - "CCVNOC": "1,82", - "CCVVHC": "1,77" - }, - { - "Dia": "26/10/2019", - "Hora": "02-03", - "GEN": "105,17", - "NOC": "56,48", - "VHC": "53,56", - "COFGEN": "0,000064100056000000", - "COFNOC": "0,000117356595000000", - "COFVHC": "0,000158787037000000", - "PMHGEN": "50,26", - "PMHNOC": "48,29", - "PMHVHC": "47,03", - "SAHGEN": "2,35", - "SAHNOC": "2,26", - "SAHVHC": "2,20", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,94", - "INTNOC": "0,90", - "INTVHC": "0,88", - "PCAPGEN": "5,55", - "PCAPNOC": "0,93", - "PCAPVHC": "0,72", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "0,89", - "CCVGEN": "1,87", - "CCVNOC": "1,73", - "CCVVHC": "1,68" - }, - { - "Dia": "26/10/2019", - "Hora": "03-04", - "GEN": "102,45", - "NOC": "53,87", - "VHC": "51,02", - "COFGEN": "0,000059549798000000", - "COFNOC": "0,000113408113000000", - "COFVHC": "0,000152391581000000", - "PMHGEN": "47,42", - "PMHNOC": "45,57", - "PMHVHC": "44,38", - "SAHGEN": "2,51", - "SAHNOC": "2,41", - "SAHVHC": "2,35", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,14", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,94", - "INTNOC": "0,90", - "INTVHC": "0,88", - "PCAPGEN": "5,56", - "PCAPNOC": "0,93", - "PCAPVHC": "0,72", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "0,89", - "CCVGEN": "1,83", - "CCVNOC": "1,69", - "CCVVHC": "1,65" - }, - { - "Dia": "26/10/2019", - "Hora": "04-05", - "GEN": "102,15", - "NOC": "53,58", - "VHC": "50,73", - "COFGEN": "0,000057296575000000", - "COFNOC": "0,000111308472000000", - "COFVHC": "0,000145270809000000", - "PMHGEN": "47,05", - "PMHNOC": "45,21", - "PMHVHC": "44,03", - "SAHGEN": "2,58", - "SAHNOC": "2,48", - "SAHVHC": "2,41", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,14", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,94", - "INTNOC": "0,90", - "INTVHC": "0,88", - "PCAPGEN": "5,56", - "PCAPNOC": "0,93", - "PCAPVHC": "0,72", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "0,89", - "CCVGEN": "1,83", - "CCVNOC": "1,69", - "CCVVHC": "1,64" - }, - { - "Dia": "26/10/2019", - "Hora": "05-06", - "GEN": "101,62", - "NOC": "53,13", - "VHC": "50,34", - "COFGEN": "0,000057285870000000", - "COFNOC": "0,000111061995000000", - "COFVHC": "0,000141535570000000", - "PMHGEN": "46,55", - "PMHNOC": "44,76", - "PMHVHC": "43,63", - "SAHGEN": "2,60", - "SAHNOC": "2,50", - "SAHVHC": "2,43", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,93", - "INTNOC": "0,90", - "INTVHC": "0,87", - "PCAPGEN": "5,54", - "PCAPNOC": "0,93", - "PCAPVHC": "0,72", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "0,89", - "CCVGEN": "1,82", - "CCVNOC": "1,68", - "CCVVHC": "1,64" - }, - { - "Dia": "26/10/2019", - "Hora": "06-07", - "GEN": "102,36", - "NOC": "53,90", - "VHC": "51,08", - "COFGEN": "0,000060011439000000", - "COFNOC": "0,000113191071000000", - "COFVHC": "0,000139395926000000", - "PMHGEN": "46,58", - "PMHNOC": "44,82", - "PMHVHC": "43,69", - "SAHGEN": "3,32", - "SAHNOC": "3,20", - "SAHVHC": "3,12", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,93", - "INTNOC": "0,89", - "INTVHC": "0,87", - "PCAPGEN": "5,51", - "PCAPNOC": "0,92", - "PCAPVHC": "0,72", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "0,89", - "CCVGEN": "1,82", - "CCVNOC": "1,69", - "CCVVHC": "1,64" - }, - { - "Dia": "26/10/2019", - "Hora": "07-08", - "GEN": "106,73", - "NOC": "58,10", - "VHC": "61,55", - "COFGEN": "0,000067624746000000", - "COFNOC": "0,000113073036000000", - "COFVHC": "0,000130165590000000", - "PMHGEN": "50,24", - "PMHNOC": "48,34", - "PMHVHC": "50,45", - "SAHGEN": "3,98", - "SAHNOC": "3,83", - "SAHVHC": "4,00", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,93", - "INTNOC": "0,89", - "INTVHC": "0,93", - "PCAPGEN": "5,50", - "PCAPNOC": "0,92", - "PCAPVHC": "1,30", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "1,89", - "CCVNOC": "1,75", - "CCVVHC": "1,83" - }, - { - "Dia": "26/10/2019", - "Hora": "08-09", - "GEN": "107,75", - "NOC": "59,43", - "VHC": "62,66", - "COFGEN": "0,000083194704000000", - "COFNOC": "0,000083589950000000", - "COFVHC": "0,000069841029000000", - "PMHGEN": "51,74", - "PMHNOC": "50,02", - "PMHVHC": "51,97", - "SAHGEN": "3,62", - "SAHNOC": "3,50", - "SAHVHC": "3,63", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,91", - "INTNOC": "0,88", - "INTVHC": "0,91", - "PCAPGEN": "5,40", - "PCAPNOC": "0,91", - "PCAPVHC": "1,27", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "1,89", - "CCVNOC": "1,76", - "CCVVHC": "1,83" - }, - { - "Dia": "26/10/2019", - "Hora": "09-10", - "GEN": "110,38", - "NOC": "62,09", - "VHC": "65,34", - "COFGEN": "0,000105869478000000", - "COFNOC": "0,000077963480000000", - "COFVHC": "0,000057355982000000", - "PMHGEN": "55,41", - "PMHNOC": "53,64", - "PMHVHC": "55,65", - "SAHGEN": "2,60", - "SAHNOC": "2,52", - "SAHVHC": "2,61", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,90", - "INTNOC": "0,87", - "INTVHC": "0,91", - "PCAPGEN": "5,36", - "PCAPNOC": "0,90", - "PCAPVHC": "1,26", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "1,92", - "CCVNOC": "1,79", - "CCVVHC": "1,86" - }, - { - "Dia": "26/10/2019", - "Hora": "10-11", - "GEN": "108,10", - "NOC": "60,00", - "VHC": "63,02", - "COFGEN": "0,000121833263000000", - "COFNOC": "0,000085468800000000", - "COFVHC": "0,000063770407000000", - "PMHGEN": "53,39", - "PMHNOC": "51,77", - "PMHVHC": "53,58", - "SAHGEN": "2,42", - "SAHNOC": "2,34", - "SAHVHC": "2,42", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,90", - "INTNOC": "0,87", - "INTVHC": "0,90", - "PCAPGEN": "5,32", - "PCAPNOC": "0,90", - "PCAPVHC": "1,25", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "1,88", - "CCVNOC": "1,76", - "CCVVHC": "1,82" - }, - { - "Dia": "26/10/2019", - "Hora": "11-12", - "GEN": "104,11", - "NOC": "56,20", - "VHC": "59,04", - "COFGEN": "0,000125947995000000", - "COFNOC": "0,000085228595000000", - "COFVHC": "0,000064070840000000", - "PMHGEN": "50,02", - "PMHNOC": "48,54", - "PMHVHC": "50,20", - "SAHGEN": "1,89", - "SAHNOC": "1,83", - "SAHVHC": "1,90", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,89", - "INTNOC": "0,87", - "INTVHC": "0,90", - "PCAPGEN": "5,31", - "PCAPNOC": "0,90", - "PCAPVHC": "1,25", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "1,81", - "CCVNOC": "1,70", - "CCVVHC": "1,76" - }, - { - "Dia": "26/10/2019", - "Hora": "12-13", - "GEN": "103,61", - "NOC": "55,65", - "VHC": "58,52", - "COFGEN": "0,000128302145000000", - "COFNOC": "0,000082279443000000", - "COFVHC": "0,000063904657000000", - "PMHGEN": "49,50", - "PMHNOC": "47,99", - "PMHVHC": "49,67", - "SAHGEN": "1,90", - "SAHNOC": "1,84", - "SAHVHC": "1,90", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,90", - "INTNOC": "0,87", - "INTVHC": "0,90", - "PCAPGEN": "5,32", - "PCAPNOC": "0,90", - "PCAPVHC": "1,25", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "1,81", - "CCVNOC": "1,69", - "CCVVHC": "1,75" - }, - { - "Dia": "26/10/2019", - "Hora": "13-14", - "GEN": "104,03", - "NOC": "122,60", - "VHC": "122,60", - "COFGEN": "0,000134270665000000", - "COFNOC": "0,000080726428000000", - "COFVHC": "0,000063976543000000", - "PMHGEN": "49,98", - "PMHNOC": "50,33", - "PMHVHC": "50,33", - "SAHGEN": "1,85", - "SAHNOC": "1,87", - "SAHVHC": "1,87", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,89", - "INTNOC": "0,90", - "INTVHC": "0,90", - "PCAPGEN": "5,30", - "PCAPNOC": "5,50", - "PCAPVHC": "5,50", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "1,81", - "CCVNOC": "1,83", - "CCVVHC": "1,83" - }, - { - "Dia": "26/10/2019", - "Hora": "14-15", - "GEN": "103,44", - "NOC": "122,00", - "VHC": "122,00", - "COFGEN": "0,000130580837000000", - "COFNOC": "0,000079392022000000", - "COFVHC": "0,000064422150000000", - "PMHGEN": "49,25", - "PMHNOC": "49,60", - "PMHVHC": "49,60", - "SAHGEN": "1,97", - "SAHNOC": "1,98", - "SAHVHC": "1,98", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,90", - "INTNOC": "0,90", - "INTVHC": "0,90", - "PCAPGEN": "5,32", - "PCAPNOC": "5,52", - "PCAPVHC": "5,52", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "1,81", - "CCVNOC": "1,82", - "CCVVHC": "1,82" - }, - { - "Dia": "26/10/2019", - "Hora": "15-16", - "GEN": "100,57", - "NOC": "119,16", - "VHC": "119,16", - "COFGEN": "0,000114850139000000", - "COFNOC": "0,000070924506000000", - "COFVHC": "0,000056150579000000", - "PMHGEN": "46,19", - "PMHNOC": "46,55", - "PMHVHC": "46,55", - "SAHGEN": "2,15", - "SAHNOC": "2,17", - "SAHVHC": "2,17", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,90", - "INTNOC": "0,91", - "INTVHC": "0,91", - "PCAPGEN": "5,36", - "PCAPNOC": "5,57", - "PCAPVHC": "5,57", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "1,77", - "CCVNOC": "1,79", - "CCVVHC": "1,79" - }, - { - "Dia": "26/10/2019", - "Hora": "16-17", - "GEN": "99,90", - "NOC": "118,48", - "VHC": "118,48", - "COFGEN": "0,000105915899000000", - "COFNOC": "0,000065274280000000", - "COFVHC": "0,000051268616000000", - "PMHGEN": "45,44", - "PMHNOC": "45,80", - "PMHVHC": "45,80", - "SAHGEN": "2,25", - "SAHNOC": "2,27", - "SAHVHC": "2,27", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,90", - "INTNOC": "0,91", - "INTVHC": "0,91", - "PCAPGEN": "5,35", - "PCAPNOC": "5,56", - "PCAPVHC": "5,56", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "1,76", - "CCVNOC": "1,78", - "CCVVHC": "1,78" - }, - { - "Dia": "26/10/2019", - "Hora": "17-18", - "GEN": "102,97", - "NOC": "121,53", - "VHC": "121,53", - "COFGEN": "0,000104178581000000", - "COFNOC": "0,000063611672000000", - "COFVHC": "0,000049947652000000", - "PMHGEN": "48,62", - "PMHNOC": "48,96", - "PMHVHC": "48,96", - "SAHGEN": "2,14", - "SAHNOC": "2,16", - "SAHVHC": "2,16", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,90", - "INTNOC": "0,90", - "INTVHC": "0,90", - "PCAPGEN": "5,33", - "PCAPNOC": "5,53", - "PCAPVHC": "5,53", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "1,80", - "CCVNOC": "1,82", - "CCVVHC": "1,82" - }, - { - "Dia": "26/10/2019", - "Hora": "18-19", - "GEN": "107,71", - "NOC": "126,30", - "VHC": "126,30", - "COFGEN": "0,000106669089000000", - "COFNOC": "0,000070000350000000", - "COFVHC": "0,000061100876000000", - "PMHGEN": "53,37", - "PMHNOC": "53,74", - "PMHVHC": "53,74", - "SAHGEN": "2,05", - "SAHNOC": "2,06", - "SAHVHC": "2,06", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,90", - "INTNOC": "0,90", - "INTVHC": "0,90", - "PCAPGEN": "5,33", - "PCAPNOC": "5,53", - "PCAPVHC": "5,53", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "1,87", - "CCVNOC": "1,89", - "CCVVHC": "1,89" - }, - { - "Dia": "26/10/2019", - "Hora": "19-20", - "GEN": "118,75", - "NOC": "137,49", - "VHC": "137,49", - "COFGEN": "0,000115010612000000", - "COFNOC": "0,000095780287000000", - "COFVHC": "0,000092687680000000", - "PMHGEN": "64,21", - "PMHNOC": "64,71", - "PMHVHC": "64,71", - "SAHGEN": "2,07", - "SAHNOC": "2,08", - "SAHVHC": "2,08", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,90", - "INTNOC": "0,91", - "INTVHC": "0,91", - "PCAPGEN": "5,35", - "PCAPNOC": "5,55", - "PCAPVHC": "5,55", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "2,04", - "CCVNOC": "2,06", - "CCVVHC": "2,06" - }, - { - "Dia": "26/10/2019", - "Hora": "20-21", - "GEN": "124,00", - "NOC": "142,78", - "VHC": "142,78", - "COFGEN": "0,000129085428000000", - "COFNOC": "0,000144302922000000", - "COFVHC": "0,000185612441000000", - "PMHGEN": "69,13", - "PMHNOC": "69,67", - "PMHVHC": "69,67", - "SAHGEN": "2,30", - "SAHNOC": "2,32", - "SAHVHC": "2,32", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,90", - "INTNOC": "0,91", - "INTVHC": "0,91", - "PCAPGEN": "5,36", - "PCAPNOC": "5,56", - "PCAPVHC": "5,56", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "2,12", - "CCVNOC": "2,14", - "CCVVHC": "2,14" - }, - { - "Dia": "26/10/2019", - "Hora": "21-22", - "GEN": "124,16", - "NOC": "143,00", - "VHC": "143,00", - "COFGEN": "0,000133109692000000", - "COFNOC": "0,000151101318000000", - "COFVHC": "0,000197574745000000", - "PMHGEN": "68,50", - "PMHNOC": "69,09", - "PMHVHC": "69,09", - "SAHGEN": "3,05", - "SAHNOC": "3,07", - "SAHVHC": "3,07", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,91", - "INTNOC": "0,91", - "INTVHC": "0,91", - "PCAPGEN": "5,38", - "PCAPNOC": "5,60", - "PCAPVHC": "5,60", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "2,13", - "CCVNOC": "2,15", - "CCVVHC": "2,15" - }, - { - "Dia": "26/10/2019", - "Hora": "22-23", - "GEN": "120,30", - "NOC": "139,04", - "VHC": "139,04", - "COFGEN": "0,000120157209000000", - "COFNOC": "0,000148137882000000", - "COFVHC": "0,000194906294000000", - "PMHGEN": "64,33", - "PMHNOC": "64,82", - "PMHVHC": "64,82", - "SAHGEN": "3,38", - "SAHNOC": "3,41", - "SAHVHC": "3,41", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,91", - "INTNOC": "0,92", - "INTVHC": "0,92", - "PCAPGEN": "5,42", - "PCAPNOC": "5,63", - "PCAPVHC": "5,63", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "2,08", - "CCVNOC": "2,10", - "CCVVHC": "2,10" - }, - { - "Dia": "26/10/2019", - "Hora": "23-24", - "GEN": "118,05", - "NOC": "69,05", - "VHC": "72,93", - "COFGEN": "0,000103870556000000", - "COFNOC": "0,000146233245000000", - "COFVHC": "0,000182184931000000", - "PMHGEN": "61,54", - "PMHNOC": "59,25", - "PMHVHC": "61,80", - "SAHGEN": "3,85", - "SAHNOC": "3,71", - "SAHVHC": "3,87", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,92", - "INTNOC": "0,89", - "INTVHC": "0,93", - "PCAPGEN": "5,49", - "PCAPNOC": "0,92", - "PCAPVHC": "1,29", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "2,05", - "CCVNOC": "1,91", - "CCVVHC": "2,00" - } - ] -} \ No newline at end of file diff --git a/tests/components/pvpc_hourly_pricing/fixtures/PVPC_CURV_DD_2019_10_27.json b/tests/components/pvpc_hourly_pricing/fixtures/PVPC_CURV_DD_2019_10_27.json deleted file mode 100644 index 66afc5a91d3..00000000000 --- a/tests/components/pvpc_hourly_pricing/fixtures/PVPC_CURV_DD_2019_10_27.json +++ /dev/null @@ -1,854 +0,0 @@ -{ - "PVPC": [ - { - "Dia": "27/10/2019", - "Hora": "00-01", - "GEN": "115,15", - "NOC": "65,95", - "VHC": "69,94", - "COFGEN": "0,000083408754000000", - "COFNOC": "0,000125204015000000", - "COFVHC": "0,000143740251000000", - "PMHGEN": "59,13", - "PMHNOC": "56,72", - "PMHVHC": "59,37", - "SAHGEN": "3,28", - "SAHNOC": "3,14", - "SAHVHC": "3,29", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,14", - "FOSNOC": "0,13", - "FOSVHC": "0,14", - "INTGEN": "0,94", - "INTNOC": "0,90", - "INTVHC": "0,94", - "PCAPGEN": "5,58", - "PCAPNOC": "0,93", - "PCAPVHC": "1,32", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "2,03", - "CCVNOC": "1,88", - "CCVVHC": "1,97" - }, - { - "Dia": "27/10/2019", - "Hora": "01-02", - "GEN": "109,63", - "NOC": "60,60", - "VHC": "57,48", - "COFGEN": "0,000069962863000000", - "COFNOC": "0,000114629494000000", - "COFVHC": "0,000147622130000000", - "PMHGEN": "53,21", - "PMHNOC": "51,01", - "PMHVHC": "49,61", - "SAHGEN": "3,72", - "SAHNOC": "3,57", - "SAHVHC": "3,47", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,14", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,95", - "INTNOC": "0,91", - "INTVHC": "0,88", - "PCAPGEN": "5,61", - "PCAPNOC": "0,94", - "PCAPVHC": "0,73", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "0,89", - "CCVGEN": "1,95", - "CCVNOC": "1,80", - "CCVVHC": "1,75" - }, - { - "Dia": "27/10/2019", - "Hora": "02-03", - "GEN": "108,41", - "NOC": "59,38", - "VHC": "56,29", - "COFGEN": "0,000065978330000000", - "COFNOC": "0,000111216294000000", - "COFVHC": "0,000145651145000000", - "PMHGEN": "52,09", - "PMHNOC": "49,90", - "PMHVHC": "48,53", - "SAHGEN": "3,62", - "SAHNOC": "3,47", - "SAHVHC": "3,37", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,14", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,95", - "INTNOC": "0,91", - "INTVHC": "0,88", - "PCAPGEN": "5,63", - "PCAPNOC": "0,94", - "PCAPVHC": "0,73", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "0,89", - "CCVGEN": "1,93", - "CCVNOC": "1,79", - "CCVVHC": "1,73" - }, - { - "Dia": "27/10/2019", - "Hora": "03-04", - "GEN": "108,22", - "NOC": "59,31", - "VHC": "56,27", - "COFGEN": "0,000061999708000000", - "COFNOC": "0,000107809474000000", - "COFVHC": "0,000143671560000000", - "PMHGEN": "51,88", - "PMHNOC": "49,78", - "PMHVHC": "48,45", - "SAHGEN": "3,68", - "SAHNOC": "3,53", - "SAHVHC": "3,44", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,14", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,94", - "INTNOC": "0,90", - "INTVHC": "0,88", - "PCAPGEN": "5,59", - "PCAPNOC": "0,93", - "PCAPVHC": "0,73", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "0,89", - "CCVGEN": "1,93", - "CCVNOC": "1,78", - "CCVVHC": "1,73" - }, - { - "Dia": "27/10/2019", - "Hora": "04-05", - "GEN": "107,03", - "NOC": "58,16", - "VHC": "55,10", - "COFGEN": "0,000057358428000000", - "COFNOC": "0,000103595831000000", - "COFVHC": "0,000139122535000000", - "PMHGEN": "50,53", - "PMHNOC": "48,48", - "PMHVHC": "47,15", - "SAHGEN": "3,85", - "SAHNOC": "3,69", - "SAHVHC": "3,59", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,14", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,94", - "INTNOC": "0,91", - "INTVHC": "0,88", - "PCAPGEN": "5,60", - "PCAPNOC": "0,93", - "PCAPVHC": "0,73", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "0,89", - "CCVGEN": "1,91", - "CCVNOC": "1,76", - "CCVVHC": "1,71" - }, - { - "Dia": "27/10/2019", - "Hora": "05-06", - "GEN": "104,79", - "NOC": "56,01", - "VHC": "53,06", - "COFGEN": "0,000055060063000000", - "COFNOC": "0,000101732765000000", - "COFVHC": "0,000134441142000000", - "PMHGEN": "48,28", - "PMHNOC": "46,32", - "PMHVHC": "45,08", - "SAHGEN": "3,91", - "SAHNOC": "3,75", - "SAHVHC": "3,65", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,14", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,94", - "INTNOC": "0,90", - "INTVHC": "0,88", - "PCAPGEN": "5,59", - "PCAPNOC": "0,93", - "PCAPVHC": "0,73", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "0,89", - "CCVGEN": "1,87", - "CCVNOC": "1,73", - "CCVVHC": "1,68" - }, - { - "Dia": "27/10/2019", - "Hora": "06-07", - "GEN": "104,56", - "NOC": "55,85", - "VHC": "52,94", - "COFGEN": "0,000054511300000000", - "COFNOC": "0,000101250808000000", - "COFVHC": "0,000131206727000000", - "PMHGEN": "48,10", - "PMHNOC": "46,18", - "PMHVHC": "44,98", - "SAHGEN": "3,90", - "SAHNOC": "3,74", - "SAHVHC": "3,65", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,14", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,94", - "INTNOC": "0,90", - "INTVHC": "0,88", - "PCAPGEN": "5,57", - "PCAPNOC": "0,93", - "PCAPVHC": "0,72", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "0,89", - "CCVGEN": "1,87", - "CCVNOC": "1,73", - "CCVVHC": "1,68" - }, - { - "Dia": "27/10/2019", - "Hora": "07-08", - "GEN": "107,72", - "NOC": "58,93", - "VHC": "55,95", - "COFGEN": "0,000056191283000000", - "COFNOC": "0,000102978398000000", - "COFVHC": "0,000130073563000000", - "PMHGEN": "50,23", - "PMHNOC": "48,26", - "PMHVHC": "47,01", - "SAHGEN": "4,89", - "SAHNOC": "4,70", - "SAHVHC": "4,57", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,14", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,94", - "INTNOC": "0,90", - "INTVHC": "0,88", - "PCAPGEN": "5,56", - "PCAPNOC": "0,93", - "PCAPVHC": "0,72", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "0,89", - "CCVGEN": "1,91", - "CCVNOC": "1,77", - "CCVVHC": "1,72" - }, - { - "Dia": "27/10/2019", - "Hora": "08-09", - "GEN": "107,80", - "NOC": "59,29", - "VHC": "62,70", - "COFGEN": "0,000060083432000000", - "COFNOC": "0,000100348617000000", - "COFVHC": "0,000118460190000000", - "PMHGEN": "50,94", - "PMHNOC": "49,13", - "PMHVHC": "51,20", - "SAHGEN": "4,38", - "SAHNOC": "4,23", - "SAHVHC": "4,40", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,92", - "INTNOC": "0,89", - "INTVHC": "0,93", - "PCAPGEN": "5,47", - "PCAPNOC": "0,92", - "PCAPVHC": "1,29", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "1,90", - "CCVNOC": "1,76", - "CCVVHC": "1,84" - }, - { - "Dia": "27/10/2019", - "Hora": "09-10", - "GEN": "106,74", - "NOC": "58,40", - "VHC": "61,63", - "COFGEN": "0,000070236674000000", - "COFNOC": "0,000071273888000000", - "COFVHC": "0,000062511624000000", - "PMHGEN": "50,00", - "PMHNOC": "48,29", - "PMHVHC": "50,22", - "SAHGEN": "4,34", - "SAHNOC": "4,20", - "SAHVHC": "4,36", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,91", - "INTNOC": "0,88", - "INTVHC": "0,92", - "PCAPGEN": "5,42", - "PCAPNOC": "0,91", - "PCAPVHC": "1,28", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "1,87", - "CCVNOC": "1,74", - "CCVVHC": "1,82" - }, - { - "Dia": "27/10/2019", - "Hora": "10-11", - "GEN": "106,13", - "NOC": "57,81", - "VHC": "61,02", - "COFGEN": "0,000089379429000000", - "COFNOC": "0,000066131351000000", - "COFVHC": "0,000053107930000000", - "PMHGEN": "50,32", - "PMHNOC": "48,60", - "PMHVHC": "50,54", - "SAHGEN": "3,43", - "SAHNOC": "3,31", - "SAHVHC": "3,44", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,91", - "INTNOC": "0,88", - "INTVHC": "0,92", - "PCAPGEN": "5,42", - "PCAPNOC": "0,91", - "PCAPVHC": "1,28", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "1,86", - "CCVNOC": "1,73", - "CCVVHC": "1,81" - }, - { - "Dia": "27/10/2019", - "Hora": "11-12", - "GEN": "105,00", - "NOC": "56,78", - "VHC": "59,91", - "COFGEN": "0,000106229062000000", - "COFNOC": "0,000075658481000000", - "COFVHC": "0,000058816566000000", - "PMHGEN": "50,34", - "PMHNOC": "48,65", - "PMHVHC": "50,56", - "SAHGEN": "2,33", - "SAHNOC": "2,25", - "SAHVHC": "2,34", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,91", - "INTNOC": "0,88", - "INTVHC": "0,91", - "PCAPGEN": "5,39", - "PCAPNOC": "0,91", - "PCAPVHC": "1,27", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "1,84", - "CCVNOC": "1,72", - "CCVVHC": "1,79" - }, - { - "Dia": "27/10/2019", - "Hora": "12-13", - "GEN": "105,07", - "NOC": "56,79", - "VHC": "59,92", - "COFGEN": "0,000113739886000000", - "COFNOC": "0,000079251893000000", - "COFVHC": "0,000061868784000000", - "PMHGEN": "50,41", - "PMHNOC": "48,69", - "PMHVHC": "50,59", - "SAHGEN": "2,31", - "SAHNOC": "2,23", - "SAHVHC": "2,32", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,91", - "INTNOC": "0,88", - "INTVHC": "0,91", - "PCAPGEN": "5,40", - "PCAPNOC": "0,91", - "PCAPVHC": "1,27", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "1,85", - "CCVNOC": "1,72", - "CCVVHC": "1,79" - }, - { - "Dia": "27/10/2019", - "Hora": "13-14", - "GEN": "104,67", - "NOC": "123,29", - "VHC": "59,59", - "COFGEN": "0,000116885572000000", - "COFNOC": "0,000077561607000000", - "COFVHC": "0,000061189779000000", - "PMHGEN": "50,08", - "PMHNOC": "50,47", - "PMHVHC": "50,30", - "SAHGEN": "2,29", - "SAHNOC": "2,31", - "SAHVHC": "2,30", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,90", - "INTNOC": "0,91", - "INTVHC": "0,91", - "PCAPGEN": "5,37", - "PCAPNOC": "5,57", - "PCAPVHC": "1,27", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "2,88", - "CCVGEN": "1,83", - "CCVNOC": "1,85", - "CCVVHC": "1,78" - }, - { - "Dia": "27/10/2019", - "Hora": "14-15", - "GEN": "107,41", - "NOC": "126,05", - "VHC": "126,05", - "COFGEN": "0,000122253070000000", - "COFNOC": "0,000076034460000000", - "COFVHC": "0,000059795888000000", - "PMHGEN": "52,87", - "PMHNOC": "53,28", - "PMHVHC": "53,28", - "SAHGEN": "2,20", - "SAHNOC": "2,22", - "SAHVHC": "2,22", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,90", - "INTNOC": "0,91", - "INTVHC": "0,91", - "PCAPGEN": "5,37", - "PCAPNOC": "5,57", - "PCAPVHC": "5,57", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "1,87", - "CCVNOC": "1,89", - "CCVVHC": "1,89" - }, - { - "Dia": "27/10/2019", - "Hora": "15-16", - "GEN": "108,36", - "NOC": "127,06", - "VHC": "127,06", - "COFGEN": "0,000120316270000000", - "COFNOC": "0,000073732639000000", - "COFVHC": "0,000059483320000000", - "PMHGEN": "53,68", - "PMHNOC": "54,14", - "PMHVHC": "54,14", - "SAHGEN": "2,29", - "SAHNOC": "2,31", - "SAHVHC": "2,31", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,91", - "INTNOC": "0,92", - "INTVHC": "0,92", - "PCAPGEN": "5,39", - "PCAPNOC": "5,60", - "PCAPVHC": "5,60", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "1,89", - "CCVNOC": "1,91", - "CCVVHC": "1,91" - }, - { - "Dia": "27/10/2019", - "Hora": "16-17", - "GEN": "106,15", - "NOC": "124,78", - "VHC": "124,78", - "COFGEN": "0,000106276301000000", - "COFNOC": "0,000065442255000000", - "COFVHC": "0,000053614900000000", - "PMHGEN": "51,38", - "PMHNOC": "51,78", - "PMHVHC": "51,78", - "SAHGEN": "2,40", - "SAHNOC": "2,42", - "SAHVHC": "2,42", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,91", - "INTNOC": "0,92", - "INTVHC": "0,92", - "PCAPGEN": "5,40", - "PCAPNOC": "5,61", - "PCAPVHC": "5,61", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "1,86", - "CCVNOC": "1,88", - "CCVVHC": "1,88" - }, - { - "Dia": "27/10/2019", - "Hora": "17-18", - "GEN": "105,09", - "NOC": "123,72", - "VHC": "123,72", - "COFGEN": "0,000098092024000000", - "COFNOC": "0,000060340481000000", - "COFVHC": "0,000050280869000000", - "PMHGEN": "51,35", - "PMHNOC": "51,75", - "PMHVHC": "51,75", - "SAHGEN": "1,40", - "SAHNOC": "1,41", - "SAHVHC": "1,41", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,91", - "INTNOC": "0,92", - "INTVHC": "0,92", - "PCAPGEN": "5,40", - "PCAPNOC": "5,61", - "PCAPVHC": "5,61", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "1,85", - "CCVNOC": "1,86", - "CCVVHC": "1,86" - }, - { - "Dia": "27/10/2019", - "Hora": "18-19", - "GEN": "108,12", - "NOC": "126,77", - "VHC": "126,77", - "COFGEN": "0,000095857172000000", - "COFNOC": "0,000058545227000000", - "COFVHC": "0,000049936767000000", - "PMHGEN": "54,41", - "PMHNOC": "54,83", - "PMHVHC": "54,83", - "SAHGEN": "1,35", - "SAHNOC": "1,36", - "SAHVHC": "1,36", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,91", - "INTNOC": "0,91", - "INTVHC": "0,91", - "PCAPGEN": "5,38", - "PCAPNOC": "5,59", - "PCAPVHC": "5,59", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "1,89", - "CCVNOC": "1,91", - "CCVVHC": "1,91" - }, - { - "Dia": "27/10/2019", - "Hora": "19-20", - "GEN": "112,76", - "NOC": "131,51", - "VHC": "131,51", - "COFGEN": "0,000099686581000000", - "COFNOC": "0,000063674261000000", - "COFVHC": "0,000057884599000000", - "PMHGEN": "58,53", - "PMHNOC": "59,03", - "PMHVHC": "59,03", - "SAHGEN": "1,77", - "SAHNOC": "1,79", - "SAHVHC": "1,79", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,91", - "INTNOC": "0,92", - "INTVHC": "0,92", - "PCAPGEN": "5,40", - "PCAPNOC": "5,62", - "PCAPVHC": "5,62", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "1,96", - "CCVNOC": "1,98", - "CCVVHC": "1,98" - }, - { - "Dia": "27/10/2019", - "Hora": "20-21", - "GEN": "118,69", - "NOC": "137,48", - "VHC": "137,48", - "COFGEN": "0,000111025948000000", - "COFNOC": "0,000087846097000000", - "COFVHC": "0,000084304207000000", - "PMHGEN": "64,79", - "PMHNOC": "65,35", - "PMHVHC": "65,35", - "SAHGEN": "1,34", - "SAHNOC": "1,35", - "SAHVHC": "1,35", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,91", - "INTNOC": "0,92", - "INTVHC": "0,92", - "PCAPGEN": "5,40", - "PCAPNOC": "5,62", - "PCAPVHC": "5,62", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "2,05", - "CCVNOC": "2,07", - "CCVVHC": "2,07" - }, - { - "Dia": "27/10/2019", - "Hora": "21-22", - "GEN": "121,19", - "NOC": "139,94", - "VHC": "139,94", - "COFGEN": "0,000129356812000000", - "COFNOC": "0,000137580750000000", - "COFVHC": "0,000175068439000000", - "PMHGEN": "66,00", - "PMHNOC": "66,51", - "PMHVHC": "66,51", - "SAHGEN": "2,64", - "SAHNOC": "2,66", - "SAHVHC": "2,66", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,91", - "INTNOC": "0,91", - "INTVHC": "0,91", - "PCAPGEN": "5,38", - "PCAPNOC": "5,58", - "PCAPVHC": "5,58", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "2,08", - "CCVNOC": "2,10", - "CCVVHC": "2,10" - }, - { - "Dia": "27/10/2019", - "Hora": "22-23", - "GEN": "120,21", - "NOC": "138,96", - "VHC": "138,96", - "COFGEN": "0,000132818174000000", - "COFNOC": "0,000143862321000000", - "COFVHC": "0,000185393247000000", - "PMHGEN": "65,72", - "PMHNOC": "66,23", - "PMHVHC": "66,23", - "SAHGEN": "1,94", - "SAHNOC": "1,96", - "SAHVHC": "1,96", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,91", - "INTNOC": "0,91", - "INTVHC": "0,91", - "PCAPGEN": "5,38", - "PCAPNOC": "5,59", - "PCAPVHC": "5,59", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "2,07", - "CCVNOC": "2,09", - "CCVVHC": "2,09" - }, - { - "Dia": "27/10/2019", - "Hora": "23-24", - "GEN": "117,85", - "NOC": "68,93", - "VHC": "136,63", - "COFGEN": "0,000117725347000000", - "COFNOC": "0,000138623638000000", - "COFVHC": "0,000180725170000000", - "PMHGEN": "62,92", - "PMHNOC": "60,64", - "PMHVHC": "63,46", - "SAHGEN": "2,28", - "SAHNOC": "2,20", - "SAHVHC": "2,30", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,92", - "INTNOC": "0,89", - "INTVHC": "0,93", - "PCAPGEN": "5,48", - "PCAPNOC": "0,92", - "PCAPVHC": "5,69", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "62,01", - "CCVGEN": "2,05", - "CCVNOC": "1,91", - "CCVVHC": "2,07" - }, - { - "Dia": "27/10/2019", - "Hora": "24-25", - "GEN": "118,42", - "NOC": "69,35", - "VHC": "73,34", - "COFGEN": "0,000097485259000000", - "COFNOC": "0,000133828173000000", - "COFVHC": "0,000166082424000000", - "PMHGEN": "63,21", - "PMHNOC": "60,82", - "PMHVHC": "63,52", - "SAHGEN": "2,51", - "SAHNOC": "2,41", - "SAHVHC": "2,52", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,93", - "INTNOC": "0,90", - "INTVHC": "0,94", - "PCAPGEN": "5,52", - "PCAPNOC": "0,92", - "PCAPVHC": "1,30", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "2,07", - "CCVNOC": "1,92", - "CCVVHC": "2,01" - } - ] -} \ No newline at end of file diff --git a/tests/components/pvpc_hourly_pricing/fixtures/PVPC_CURV_DD_2019_10_29.json b/tests/components/pvpc_hourly_pricing/fixtures/PVPC_CURV_DD_2019_10_29.json deleted file mode 100644 index 631e77f5e76..00000000000 --- a/tests/components/pvpc_hourly_pricing/fixtures/PVPC_CURV_DD_2019_10_29.json +++ /dev/null @@ -1,820 +0,0 @@ -{ - "PVPC": [ - { - "Dia": "29/10/2019", - "Hora": "00-01", - "GEN": "117,17", - "NOC": "68,21", - "VHC": "72,10", - "COFGEN": "0,000077051997000000", - "COFNOC": "0,000124733002000000", - "COFVHC": "0,000143780107000000", - "PMHGEN": "62,55", - "PMHNOC": "60,23", - "PMHVHC": "62,86", - "SAHGEN": "1,96", - "SAHNOC": "1,89", - "SAHVHC": "1,97", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,93", - "INTNOC": "0,89", - "INTVHC": "0,93", - "PCAPGEN": "5,50", - "PCAPNOC": "0,92", - "PCAPVHC": "1,30", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "2,04", - "CCVNOC": "1,90", - "CCVVHC": "1,99" - }, - { - "Dia": "29/10/2019", - "Hora": "01-02", - "GEN": "115,34", - "NOC": "66,27", - "VHC": "63,14", - "COFGEN": "0,000063669626000000", - "COFNOC": "0,000113703738000000", - "COFVHC": "0,000153709241000000", - "PMHGEN": "60,54", - "PMHNOC": "58,17", - "PMHVHC": "56,70", - "SAHGEN": "2,11", - "SAHNOC": "2,02", - "SAHVHC": "1,97", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,93", - "INTNOC": "0,90", - "INTVHC": "0,87", - "PCAPGEN": "5,54", - "PCAPNOC": "0,93", - "PCAPVHC": "0,72", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "0,89", - "CCVGEN": "2,02", - "CCVNOC": "1,88", - "CCVVHC": "1,83" - }, - { - "Dia": "29/10/2019", - "Hora": "02-03", - "GEN": "112,37", - "NOC": "63,40", - "VHC": "60,25", - "COFGEN": "0,000057299719000000", - "COFNOC": "0,000107847932000000", - "COFVHC": "0,000151346355000000", - "PMHGEN": "57,42", - "PMHNOC": "55,17", - "PMHVHC": "53,69", - "SAHGEN": "2,27", - "SAHNOC": "2,18", - "SAHVHC": "2,12", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,14", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,94", - "INTNOC": "0,90", - "INTVHC": "0,88", - "PCAPGEN": "5,57", - "PCAPNOC": "0,93", - "PCAPVHC": "0,72", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "0,89", - "CCVGEN": "1,98", - "CCVNOC": "1,84", - "CCVVHC": "1,79" - }, - { - "Dia": "29/10/2019", - "Hora": "03-04", - "GEN": "111,32", - "NOC": "62,39", - "VHC": "59,27", - "COFGEN": "0,000054631496000000", - "COFNOC": "0,000105135123000000", - "COFVHC": "0,000145712713000000", - "PMHGEN": "55,92", - "PMHNOC": "53,73", - "PMHVHC": "52,29", - "SAHGEN": "2,74", - "SAHNOC": "2,63", - "SAHVHC": "2,56", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,14", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,94", - "INTNOC": "0,90", - "INTVHC": "0,88", - "PCAPGEN": "5,57", - "PCAPNOC": "0,93", - "PCAPVHC": "0,72", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "0,89", - "CCVGEN": "1,97", - "CCVNOC": "1,82", - "CCVVHC": "1,77" - }, - { - "Dia": "29/10/2019", - "Hora": "04-05", - "GEN": "111,08", - "NOC": "62,17", - "VHC": "59,04", - "COFGEN": "0,000053587732000000", - "COFNOC": "0,000103791403000000", - "COFVHC": "0,000139739507000000", - "PMHGEN": "55,64", - "PMHNOC": "53,46", - "PMHVHC": "52,03", - "SAHGEN": "2,79", - "SAHNOC": "2,68", - "SAHVHC": "2,61", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,14", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,94", - "INTNOC": "0,90", - "INTVHC": "0,88", - "PCAPGEN": "5,56", - "PCAPNOC": "0,93", - "PCAPVHC": "0,72", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "0,89", - "CCVGEN": "1,96", - "CCVNOC": "1,82", - "CCVVHC": "1,77" - }, - { - "Dia": "29/10/2019", - "Hora": "05-06", - "GEN": "113,57", - "NOC": "64,62", - "VHC": "61,53", - "COFGEN": "0,000054978965000000", - "COFNOC": "0,000104220226000000", - "COFVHC": "0,000135044065000000", - "PMHGEN": "58,23", - "PMHNOC": "55,99", - "PMHVHC": "54,57", - "SAHGEN": "2,69", - "SAHNOC": "2,58", - "SAHVHC": "2,52", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,93", - "INTNOC": "0,90", - "INTVHC": "0,87", - "PCAPGEN": "5,53", - "PCAPNOC": "0,92", - "PCAPVHC": "0,72", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "0,89", - "CCVGEN": "2,00", - "CCVNOC": "1,85", - "CCVVHC": "1,80" - }, - { - "Dia": "29/10/2019", - "Hora": "06-07", - "GEN": "113,48", - "NOC": "64,78", - "VHC": "61,84", - "COFGEN": "0,000063808739000000", - "COFNOC": "0,000109956697000000", - "COFVHC": "0,000134904594000000", - "PMHGEN": "58,13", - "PMHNOC": "56,06", - "PMHVHC": "54,78", - "SAHGEN": "2,80", - "SAHNOC": "2,70", - "SAHVHC": "2,64", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,12", - "INTGEN": "0,92", - "INTNOC": "0,89", - "INTVHC": "0,87", - "PCAPGEN": "5,45", - "PCAPNOC": "0,91", - "PCAPVHC": "0,71", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "0,89", - "CCVGEN": "1,98", - "CCVNOC": "1,84", - "CCVVHC": "1,80" - }, - { - "Dia": "29/10/2019", - "Hora": "07-08", - "GEN": "118,40", - "NOC": "69,72", - "VHC": "73,36", - "COFGEN": "0,000086957107000000", - "COFNOC": "0,000119021762000000", - "COFVHC": "0,000131848949000000", - "PMHGEN": "63,73", - "PMHNOC": "61,60", - "PMHVHC": "64,00", - "SAHGEN": "2,12", - "SAHNOC": "2,05", - "SAHVHC": "2,13", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,91", - "INTNOC": "0,88", - "INTVHC": "0,91", - "PCAPGEN": "5,40", - "PCAPNOC": "0,91", - "PCAPVHC": "1,27", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "2,05", - "CCVNOC": "1,91", - "CCVVHC": "1,99" - }, - { - "Dia": "29/10/2019", - "Hora": "08-09", - "GEN": "117,71", - "NOC": "69,47", - "VHC": "72,71", - "COFGEN": "0,000103659543000000", - "COFNOC": "0,000093080441000000", - "COFVHC": "0,000078478538000000", - "PMHGEN": "63,59", - "PMHNOC": "61,75", - "PMHVHC": "63,81", - "SAHGEN": "1,76", - "SAHNOC": "1,71", - "SAHVHC": "1,77", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,12", - "FOSVHC": "0,13", - "INTGEN": "0,89", - "INTNOC": "0,86", - "INTVHC": "0,89", - "PCAPGEN": "5,27", - "PCAPNOC": "0,89", - "PCAPVHC": "1,24", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "2,01", - "CCVNOC": "1,89", - "CCVVHC": "1,96" - }, - { - "Dia": "29/10/2019", - "Hora": "09-10", - "GEN": "115,84", - "NOC": "67,79", - "VHC": "70,80", - "COFGEN": "0,000109607743000000", - "COFNOC": "0,000077907419000000", - "COFVHC": "0,000061476325000000", - "PMHGEN": "62,27", - "PMHNOC": "60,57", - "PMHVHC": "62,44", - "SAHGEN": "1,29", - "SAHNOC": "1,25", - "SAHVHC": "1,29", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,12", - "FOSVHC": "0,13", - "INTGEN": "0,88", - "INTNOC": "0,86", - "INTVHC": "0,88", - "PCAPGEN": "5,23", - "PCAPNOC": "0,88", - "PCAPVHC": "1,23", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "1,98", - "CCVNOC": "1,86", - "CCVVHC": "1,92" - }, - { - "Dia": "29/10/2019", - "Hora": "10-11", - "GEN": "114,70", - "NOC": "66,74", - "VHC": "69,67", - "COFGEN": "0,000115808394000000", - "COFNOC": "0,000078426619000000", - "COFVHC": "0,000062221967000000", - "PMHGEN": "61,05", - "PMHNOC": "59,42", - "PMHVHC": "61,21", - "SAHGEN": "1,41", - "SAHNOC": "1,37", - "SAHVHC": "1,41", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,12", - "FOSVHC": "0,13", - "INTGEN": "0,88", - "INTNOC": "0,86", - "INTVHC": "0,88", - "PCAPGEN": "5,22", - "PCAPNOC": "0,88", - "PCAPVHC": "1,23", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "1,96", - "CCVNOC": "1,84", - "CCVVHC": "1,90" - }, - { - "Dia": "29/10/2019", - "Hora": "11-12", - "GEN": "114,45", - "NOC": "66,51", - "VHC": "69,43", - "COFGEN": "0,000117753360000000", - "COFNOC": "0,000076432674000000", - "COFVHC": "0,000061112533000000", - "PMHGEN": "60,85", - "PMHNOC": "59,23", - "PMHVHC": "61,01", - "SAHGEN": "1,37", - "SAHNOC": "1,34", - "SAHVHC": "1,38", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,12", - "FOSVHC": "0,13", - "INTGEN": "0,88", - "INTNOC": "0,85", - "INTVHC": "0,88", - "PCAPGEN": "5,21", - "PCAPNOC": "0,88", - "PCAPVHC": "1,23", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "1,95", - "CCVNOC": "1,84", - "CCVVHC": "1,90" - }, - { - "Dia": "29/10/2019", - "Hora": "12-13", - "GEN": "114,46", - "NOC": "133,04", - "VHC": "69,45", - "COFGEN": "0,000121492044000000", - "COFNOC": "0,000074703573000000", - "COFVHC": "0,000061457855000000", - "PMHGEN": "60,95", - "PMHNOC": "61,33", - "PMHVHC": "61,11", - "SAHGEN": "1,30", - "SAHNOC": "1,31", - "SAHVHC": "1,31", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,88", - "INTNOC": "0,88", - "INTVHC": "0,88", - "PCAPGEN": "5,20", - "PCAPNOC": "5,39", - "PCAPVHC": "1,22", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "2,88", - "CCVGEN": "1,95", - "CCVNOC": "1,97", - "CCVVHC": "1,90" - }, - { - "Dia": "29/10/2019", - "Hora": "13-14", - "GEN": "113,37", - "NOC": "131,94", - "VHC": "131,94", - "COFGEN": "0,000126490319000000", - "COFNOC": "0,000074777760000000", - "COFVHC": "0,000060760068000000", - "PMHGEN": "59,86", - "PMHNOC": "60,24", - "PMHVHC": "60,24", - "SAHGEN": "1,32", - "SAHNOC": "1,33", - "SAHVHC": "1,33", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,87", - "INTNOC": "0,88", - "INTVHC": "0,88", - "PCAPGEN": "5,19", - "PCAPNOC": "5,38", - "PCAPVHC": "5,38", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "1,93", - "CCVNOC": "1,95", - "CCVVHC": "1,95" - }, - { - "Dia": "29/10/2019", - "Hora": "14-15", - "GEN": "112,88", - "NOC": "131,46", - "VHC": "131,46", - "COFGEN": "0,000120771211000000", - "COFNOC": "0,000072095790000000", - "COFVHC": "0,000058765117000000", - "PMHGEN": "59,31", - "PMHNOC": "59,68", - "PMHVHC": "59,68", - "SAHGEN": "1,37", - "SAHNOC": "1,38", - "SAHVHC": "1,38", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,88", - "INTNOC": "0,88", - "INTVHC": "0,88", - "PCAPGEN": "5,21", - "PCAPNOC": "5,40", - "PCAPVHC": "5,40", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "1,93", - "CCVNOC": "1,94", - "CCVVHC": "1,94" - }, - { - "Dia": "29/10/2019", - "Hora": "15-16", - "GEN": "115,75", - "NOC": "134,41", - "VHC": "134,41", - "COFGEN": "0,000110808247000000", - "COFNOC": "0,000066006577000000", - "COFVHC": "0,000053763013000000", - "PMHGEN": "62,14", - "PMHNOC": "62,58", - "PMHVHC": "62,58", - "SAHGEN": "1,34", - "SAHNOC": "1,35", - "SAHVHC": "1,35", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,88", - "INTNOC": "0,89", - "INTVHC": "0,89", - "PCAPGEN": "5,23", - "PCAPNOC": "5,42", - "PCAPVHC": "5,42", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "1,98", - "CCVNOC": "1,99", - "CCVVHC": "1,99" - }, - { - "Dia": "29/10/2019", - "Hora": "16-17", - "GEN": "118,08", - "NOC": "136,75", - "VHC": "136,75", - "COFGEN": "0,000107924950000000", - "COFNOC": "0,000063090606000000", - "COFVHC": "0,000052115396000000", - "PMHGEN": "64,48", - "PMHNOC": "64,93", - "PMHVHC": "64,93", - "SAHGEN": "1,31", - "SAHNOC": "1,32", - "SAHVHC": "1,32", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,88", - "INTNOC": "0,89", - "INTVHC": "0,89", - "PCAPGEN": "5,22", - "PCAPNOC": "5,42", - "PCAPVHC": "5,42", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "2,01", - "CCVNOC": "2,03", - "CCVVHC": "2,03" - }, - { - "Dia": "29/10/2019", - "Hora": "17-18", - "GEN": "121,32", - "NOC": "139,95", - "VHC": "139,95", - "COFGEN": "0,000111993776000000", - "COFNOC": "0,000063840323000000", - "COFVHC": "0,000053264660000000", - "PMHGEN": "67,88", - "PMHNOC": "68,30", - "PMHVHC": "68,30", - "SAHGEN": "1,10", - "SAHNOC": "1,11", - "SAHVHC": "1,11", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,88", - "INTNOC": "0,88", - "INTVHC": "0,88", - "PCAPGEN": "5,22", - "PCAPNOC": "5,41", - "PCAPVHC": "5,41", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "2,06", - "CCVNOC": "2,07", - "CCVVHC": "2,07" - }, - { - "Dia": "29/10/2019", - "Hora": "18-19", - "GEN": "126,19", - "NOC": "144,85", - "VHC": "144,85", - "COFGEN": "0,000117118878000000", - "COFNOC": "0,000072058416000000", - "COFVHC": "0,000066417528000000", - "PMHGEN": "69,04", - "PMHNOC": "69,47", - "PMHVHC": "69,47", - "SAHGEN": "4,73", - "SAHNOC": "4,76", - "SAHVHC": "4,76", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,88", - "INTNOC": "0,89", - "INTVHC": "0,89", - "PCAPGEN": "5,22", - "PCAPNOC": "5,42", - "PCAPVHC": "5,42", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "2,13", - "CCVNOC": "2,15", - "CCVVHC": "2,15" - }, - { - "Dia": "29/10/2019", - "Hora": "19-20", - "GEN": "125,34", - "NOC": "144,06", - "VHC": "144,06", - "COFGEN": "0,000128443388000000", - "COFNOC": "0,000098772457000000", - "COFVHC": "0,000100678475000000", - "PMHGEN": "68,61", - "PMHNOC": "69,09", - "PMHVHC": "69,09", - "SAHGEN": "4,31", - "SAHNOC": "4,34", - "SAHVHC": "4,34", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,88", - "INTNOC": "0,89", - "INTVHC": "0,89", - "PCAPGEN": "5,24", - "PCAPNOC": "5,43", - "PCAPVHC": "5,43", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "2,12", - "CCVNOC": "2,14", - "CCVVHC": "2,14" - }, - { - "Dia": "29/10/2019", - "Hora": "20-21", - "GEN": "120,62", - "NOC": "139,31", - "VHC": "139,31", - "COFGEN": "0,000144847952000000", - "COFNOC": "0,000148736569000000", - "COFVHC": "0,000192706770000000", - "PMHGEN": "67,11", - "PMHNOC": "67,58", - "PMHVHC": "67,58", - "SAHGEN": "1,12", - "SAHNOC": "1,12", - "SAHVHC": "1,12", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,89", - "INTNOC": "0,89", - "INTVHC": "0,89", - "PCAPGEN": "5,27", - "PCAPNOC": "5,47", - "PCAPVHC": "5,47", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "2,06", - "CCVNOC": "2,07", - "CCVVHC": "2,07" - }, - { - "Dia": "29/10/2019", - "Hora": "21-22", - "GEN": "120,67", - "NOC": "139,36", - "VHC": "139,36", - "COFGEN": "0,000143400205000000", - "COFNOC": "0,000153448551000000", - "COFVHC": "0,000201113372000000", - "PMHGEN": "66,43", - "PMHNOC": "66,90", - "PMHVHC": "66,90", - "SAHGEN": "1,80", - "SAHNOC": "1,81", - "SAHVHC": "1,81", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,89", - "INTNOC": "0,90", - "INTVHC": "0,90", - "PCAPGEN": "5,30", - "PCAPNOC": "5,50", - "PCAPVHC": "5,50", - "TEUGEN": "44,03", - "TEUNOC": "62,01", - "TEUVHC": "62,01", - "CCVGEN": "2,06", - "CCVNOC": "2,08", - "CCVVHC": "2,08" - }, - { - "Dia": "29/10/2019", - "Hora": "22-23", - "GEN": "117,80", - "NOC": "69,35", - "VHC": "136,53", - "COFGEN": "0,000122948482000000", - "COFNOC": "0,000146077289000000", - "COFVHC": "0,000194614149000000", - "PMHGEN": "63,25", - "PMHNOC": "61,28", - "PMHVHC": "63,75", - "SAHGEN": "2,10", - "SAHNOC": "2,03", - "SAHVHC": "2,11", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,90", - "INTNOC": "0,87", - "INTVHC": "0,91", - "PCAPGEN": "5,34", - "PCAPNOC": "0,90", - "PCAPVHC": "5,54", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "62,01", - "CCVGEN": "2,03", - "CCVNOC": "1,90", - "CCVVHC": "2,04" - }, - { - "Dia": "29/10/2019", - "Hora": "23-24", - "GEN": "111,95", - "NOC": "63,48", - "VHC": "66,87", - "COFGEN": "0,000098841799000000", - "COFNOC": "0,000139677463000000", - "COFVHC": "0,000176886301000000", - "PMHGEN": "56,97", - "PMHNOC": "55,07", - "PMHVHC": "57,22", - "SAHGEN": "2,52", - "SAHNOC": "2,44", - "SAHVHC": "2,53", - "FOMGEN": "0,03", - "FOMNOC": "0,03", - "FOMVHC": "0,03", - "FOSGEN": "0,13", - "FOSNOC": "0,13", - "FOSVHC": "0,13", - "INTGEN": "0,91", - "INTNOC": "0,88", - "INTVHC": "0,91", - "PCAPGEN": "5,40", - "PCAPNOC": "0,91", - "PCAPVHC": "1,27", - "TEUGEN": "44,03", - "TEUNOC": "2,22", - "TEUVHC": "2,88", - "CCVGEN": "1,95", - "CCVNOC": "1,82", - "CCVVHC": "1,89" - } - ] -} \ No newline at end of file diff --git a/tests/components/pvpc_hourly_pricing/fixtures/PVPC_CURV_DD_2021_06_01.json b/tests/components/pvpc_hourly_pricing/fixtures/PVPC_CURV_DD_2021_06_01.json deleted file mode 100644 index 59559d3c3f7..00000000000 --- a/tests/components/pvpc_hourly_pricing/fixtures/PVPC_CURV_DD_2021_06_01.json +++ /dev/null @@ -1,604 +0,0 @@ -{ - "PVPC": [ - { - "Dia": "01/06/2021", - "Hora": "00-01", - "PCB": "116,33", - "CYM": "116,33", - "COF2TD": "0,000088075182000000", - "PMHPCB": "104,00", - "PMHCYM": "104,00", - "SAHPCB": "3,56", - "SAHCYM": "3,56", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,17", - "FOSCYM": "0,17", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "0,00", - "PCAPCYM": "0,00", - "TEUPCB": "6,00", - "TEUCYM": "6,00", - "CCVPCB": "2,57", - "CCVCYM": "2,57", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "01-02", - "PCB": "115,95", - "CYM": "115,95", - "COF2TD": "0,000073094842000000", - "PMHPCB": "103,18", - "PMHCYM": "103,18", - "SAHPCB": "3,99", - "SAHCYM": "3,99", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,17", - "FOSCYM": "0,17", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "0,00", - "PCAPCYM": "0,00", - "TEUPCB": "6,00", - "TEUCYM": "6,00", - "CCVPCB": "2,58", - "CCVCYM": "2,58", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "02-03", - "PCB": "114,89", - "CYM": "114,89", - "COF2TD": "0,000065114032000000", - "PMHPCB": "101,87", - "PMHCYM": "101,87", - "SAHPCB": "4,25", - "SAHCYM": "4,25", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,17", - "FOSCYM": "0,17", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "0,00", - "PCAPCYM": "0,00", - "TEUPCB": "6,00", - "TEUCYM": "6,00", - "CCVPCB": "2,56", - "CCVCYM": "2,56", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "03-04", - "PCB": "114,96", - "CYM": "114,96", - "COF2TD": "0,000061272596000000", - "PMHPCB": "102,01", - "PMHCYM": "102,01", - "SAHPCB": "4,19", - "SAHCYM": "4,19", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,17", - "FOSCYM": "0,17", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "0,00", - "PCAPCYM": "0,00", - "TEUPCB": "6,00", - "TEUCYM": "6,00", - "CCVPCB": "2,57", - "CCVCYM": "2,57", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "04-05", - "PCB": "114,84", - "CYM": "114,84", - "COF2TD": "0,000059563056000000", - "PMHPCB": "101,87", - "PMHCYM": "101,87", - "SAHPCB": "4,21", - "SAHCYM": "4,21", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,17", - "FOSCYM": "0,17", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "0,00", - "PCAPCYM": "0,00", - "TEUPCB": "6,00", - "TEUCYM": "6,00", - "CCVPCB": "2,56", - "CCVCYM": "2,56", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "05-06", - "PCB": "116,03", - "CYM": "116,03", - "COF2TD": "0,000059907686000000", - "PMHPCB": "103,14", - "PMHCYM": "103,14", - "SAHPCB": "4,11", - "SAHCYM": "4,11", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,17", - "FOSCYM": "0,17", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "0,00", - "PCAPCYM": "0,00", - "TEUPCB": "6,00", - "TEUCYM": "6,00", - "CCVPCB": "2,58", - "CCVCYM": "2,58", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "06-07", - "PCB": "116,29", - "CYM": "116,29", - "COF2TD": "0,000062818713000000", - "PMHPCB": "103,64", - "PMHCYM": "103,64", - "SAHPCB": "3,88", - "SAHCYM": "3,88", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,17", - "FOSCYM": "0,17", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "0,00", - "PCAPCYM": "0,00", - "TEUPCB": "6,00", - "TEUCYM": "6,00", - "CCVPCB": "2,57", - "CCVCYM": "2,57", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "07-08", - "PCB": "115,70", - "CYM": "115,70", - "COF2TD": "0,000072575564000000", - "PMHPCB": "103,85", - "PMHCYM": "103,85", - "SAHPCB": "3,10", - "SAHCYM": "3,10", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,16", - "FOSCYM": "0,16", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "0,00", - "PCAPCYM": "0,00", - "TEUPCB": "6,00", - "TEUCYM": "6,00", - "CCVPCB": "2,55", - "CCVCYM": "2,55", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "08-09", - "PCB": "152,89", - "CYM": "152,89", - "COF2TD": "0,000086825264000000", - "PMHPCB": "105,65", - "PMHCYM": "105,65", - "SAHPCB": "2,36", - "SAHCYM": "2,36", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,17", - "FOSCYM": "0,17", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "0,34", - "PCAPCYM": "0,34", - "TEUPCB": "41,77", - "TEUCYM": "41,77", - "CCVPCB": "2,57", - "CCVCYM": "2,57", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "09-10", - "PCB": "150,83", - "CYM": "150,83", - "COF2TD": "0,000095768317000000", - "PMHPCB": "103,77", - "PMHCYM": "103,77", - "SAHPCB": "2,24", - "SAHCYM": "2,24", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,16", - "FOSCYM": "0,16", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "0,34", - "PCAPCYM": "0,34", - "TEUPCB": "41,77", - "TEUCYM": "41,77", - "CCVPCB": "2,53", - "CCVCYM": "2,53", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "10-11", - "PCB": "242,62", - "CYM": "149,28", - "COF2TD": "0,000102672431000000", - "PMHPCB": "102,38", - "PMHCYM": "102,11", - "SAHPCB": "2,38", - "SAHCYM": "2,37", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,16", - "FOSCYM": "0,16", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "2,01", - "PCAPCYM": "0,34", - "TEUPCB": "133,12", - "TEUCYM": "41,77", - "CCVPCB": "2,54", - "CCVCYM": "2,51", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "11-12", - "PCB": "240,50", - "CYM": "240,50", - "COF2TD": "0,000105691470000000", - "PMHPCB": "100,14", - "PMHCYM": "100,14", - "SAHPCB": "2,52", - "SAHCYM": "2,52", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,16", - "FOSCYM": "0,16", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "2,02", - "PCAPCYM": "2,02", - "TEUPCB": "133,12", - "TEUCYM": "133,12", - "CCVPCB": "2,51", - "CCVCYM": "2,51", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "12-13", - "PCB": "238,09", - "CYM": "238,09", - "COF2TD": "0,000110462952000000", - "PMHPCB": "97,58", - "PMHCYM": "97,58", - "SAHPCB": "2,71", - "SAHCYM": "2,71", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,16", - "FOSCYM": "0,16", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "2,02", - "PCAPCYM": "2,02", - "TEUPCB": "133,12", - "TEUCYM": "133,12", - "CCVPCB": "2,47", - "CCVCYM": "2,47", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "13-14", - "PCB": "235,30", - "CYM": "235,30", - "COF2TD": "0,000119052052000000", - "PMHPCB": "94,65", - "PMHCYM": "94,65", - "SAHPCB": "2,89", - "SAHCYM": "2,89", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,16", - "FOSCYM": "0,16", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "2,02", - "PCAPCYM": "2,02", - "TEUPCB": "133,12", - "TEUCYM": "133,12", - "CCVPCB": "2,43", - "CCVCYM": "2,43", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "14-15", - "PCB": "137,96", - "CYM": "231,28", - "COF2TD": "0,000117990009000000", - "PMHPCB": "89,95", - "PMHCYM": "90,19", - "SAHPCB": "3,37", - "SAHCYM": "3,38", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,16", - "FOSCYM": "0,16", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "0,34", - "PCAPCYM": "2,03", - "TEUPCB": "41,77", - "TEUCYM": "133,12", - "CCVPCB": "2,34", - "CCVCYM": "2,37", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "15-16", - "PCB": "132,88", - "CYM": "132,88", - "COF2TD": "0,000108598330000000", - "PMHPCB": "84,43", - "PMHCYM": "84,43", - "SAHPCB": "3,89", - "SAHCYM": "3,89", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,16", - "FOSCYM": "0,16", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "0,34", - "PCAPCYM": "0,34", - "TEUPCB": "41,77", - "TEUCYM": "41,77", - "CCVPCB": "2,26", - "CCVCYM": "2,26", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "16-17", - "PCB": "131,93", - "CYM": "131,93", - "COF2TD": "0,000104114191000000", - "PMHPCB": "83,66", - "PMHCYM": "83,66", - "SAHPCB": "3,73", - "SAHCYM": "3,73", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,16", - "FOSCYM": "0,16", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "0,34", - "PCAPCYM": "0,34", - "TEUPCB": "41,77", - "TEUCYM": "41,77", - "CCVPCB": "2,25", - "CCVCYM": "2,25", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "17-18", - "PCB": "135,99", - "CYM": "135,99", - "COF2TD": "0,000105171071000000", - "PMHPCB": "88,07", - "PMHCYM": "88,07", - "SAHPCB": "3,31", - "SAHCYM": "3,31", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,16", - "FOSCYM": "0,16", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "0,34", - "PCAPCYM": "0,34", - "TEUPCB": "41,77", - "TEUCYM": "41,77", - "CCVPCB": "2,31", - "CCVCYM": "2,31", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "18-19", - "PCB": "231,44", - "CYM": "138,13", - "COF2TD": "0,000106417649000000", - "PMHPCB": "90,57", - "PMHCYM": "90,33", - "SAHPCB": "3,16", - "SAHCYM": "3,15", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,16", - "FOSCYM": "0,16", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "2,02", - "PCAPCYM": "0,34", - "TEUPCB": "133,12", - "TEUCYM": "41,77", - "CCVPCB": "2,37", - "CCVCYM": "2,34", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "19-20", - "PCB": "240,40", - "CYM": "240,40", - "COF2TD": "0,000108017615000000", - "PMHPCB": "99,53", - "PMHCYM": "99,53", - "SAHPCB": "3,00", - "SAHCYM": "3,00", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,17", - "FOSCYM": "0,17", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "2,04", - "PCAPCYM": "2,04", - "TEUPCB": "133,12", - "TEUCYM": "133,12", - "CCVPCB": "2,52", - "CCVCYM": "2,52", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "20-21", - "PCB": "246,20", - "CYM": "246,20", - "COF2TD": "0,000114631042000000", - "PMHPCB": "104,32", - "PMHCYM": "104,32", - "SAHPCB": "3,90", - "SAHCYM": "3,90", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,17", - "FOSCYM": "0,17", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "2,05", - "PCAPCYM": "2,05", - "TEUPCB": "133,12", - "TEUCYM": "133,12", - "CCVPCB": "2,61", - "CCVCYM": "2,61", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "21-22", - "PCB": "248,08", - "CYM": "248,08", - "COF2TD": "0,000127585671000000", - "PMHPCB": "107,28", - "PMHCYM": "107,28", - "SAHPCB": "2,78", - "SAHCYM": "2,78", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,17", - "FOSCYM": "0,17", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "2,06", - "PCAPCYM": "2,06", - "TEUPCB": "133,12", - "TEUCYM": "133,12", - "CCVPCB": "2,64", - "CCVCYM": "2,64", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "22-23", - "PCB": "155,91", - "CYM": "249,41", - "COF2TD": "0,000130129026000000", - "PMHPCB": "108,02", - "PMHCYM": "108,39", - "SAHPCB": "2,93", - "SAHCYM": "2,94", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,17", - "FOSCYM": "0,17", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "0,35", - "PCAPCYM": "2,09", - "TEUPCB": "41,77", - "TEUCYM": "133,12", - "CCVPCB": "2,64", - "CCVCYM": "2,67", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - }, - { - "Dia": "01/06/2021", - "Hora": "23-24", - "PCB": "156,50", - "CYM": "156,50", - "COF2TD": "0,000110367990000000", - "PMHPCB": "108,02", - "PMHCYM": "108,02", - "SAHPCB": "3,50", - "SAHCYM": "3,50", - "FOMPCB": "0,03", - "FOMCYM": "0,03", - "FOSPCB": "0,17", - "FOSCYM": "0,17", - "INTPCB": "0,00", - "INTCYM": "0,00", - "PCAPPCB": "0,35", - "PCAPCYM": "0,35", - "TEUPCB": "41,77", - "TEUCYM": "41,77", - "CCVPCB": "2,66", - "CCVCYM": "2,66", - "EDSRPCB": "0,00", - "EDSRCYM": "0,00" - } - ] -} \ No newline at end of file diff --git a/tests/components/pvpc_hourly_pricing/fixtures/PVPC_DATA_2021_06_01.json b/tests/components/pvpc_hourly_pricing/fixtures/PVPC_DATA_2021_06_01.json new file mode 100644 index 00000000000..8e65a242447 --- /dev/null +++ b/tests/components/pvpc_hourly_pricing/fixtures/PVPC_DATA_2021_06_01.json @@ -0,0 +1,154 @@ +{ + "data": { + "type": "Precios mercado peninsular en tiempo real", + "id": "mer13", + "attributes": { + "title": "Precios mercado peninsular en tiempo real", + "last-update": "2021-05-31T20:19:18.000+02:00", + "description": null + }, + "meta": { + "cache-control": { + "cache": "MISS" + } + } + }, + "included": [ + { + "type": "PVPC (\u20ac\/MWh)", + "id": "1001", + "groupId": null, + "attributes": { + "title": "PVPC (\u20ac\/MWh)", + "description": null, + "color": "#ffcf09", + "type": null, + "magnitude": "price", + "composite": false, + "last-update": "2021-05-31T20:19:18.000+02:00", + "values": [ + { + "value": 116.33, + "percentage": 1, + "datetime": "2021-06-01T00:00:00.000+02:00" + }, + { + "value": 115.95, + "percentage": 1, + "datetime": "2021-06-01T01:00:00.000+02:00" + }, + { + "value": 114.89, + "percentage": 1, + "datetime": "2021-06-01T02:00:00.000+02:00" + }, + { + "value": 114.96, + "percentage": 1, + "datetime": "2021-06-01T03:00:00.000+02:00" + }, + { + "value": 114.84, + "percentage": 1, + "datetime": "2021-06-01T04:00:00.000+02:00" + }, + { + "value": 116.03, + "percentage": 1, + "datetime": "2021-06-01T05:00:00.000+02:00" + }, + { + "value": 116.29, + "percentage": 1, + "datetime": "2021-06-01T06:00:00.000+02:00" + }, + { + "value": 115.7, + "percentage": 1, + "datetime": "2021-06-01T07:00:00.000+02:00" + }, + { + "value": 152.89, + "percentage": 1, + "datetime": "2021-06-01T08:00:00.000+02:00" + }, + { + "value": 150.83, + "percentage": 1, + "datetime": "2021-06-01T09:00:00.000+02:00" + }, + { + "value": 149.28, + "percentage": 1, + "datetime": "2021-06-01T10:00:00.000+02:00" + }, + { + "value": 240.5, + "percentage": 1, + "datetime": "2021-06-01T11:00:00.000+02:00" + }, + { + "value": 238.09, + "percentage": 1, + "datetime": "2021-06-01T12:00:00.000+02:00" + }, + { + "value": 235.3, + "percentage": 1, + "datetime": "2021-06-01T13:00:00.000+02:00" + }, + { + "value": 231.28, + "percentage": 1, + "datetime": "2021-06-01T14:00:00.000+02:00" + }, + { + "value": 132.88, + "percentage": 1, + "datetime": "2021-06-01T15:00:00.000+02:00" + }, + { + "value": 131.93, + "percentage": 1, + "datetime": "2021-06-01T16:00:00.000+02:00" + }, + { + "value": 135.99, + "percentage": 1, + "datetime": "2021-06-01T17:00:00.000+02:00" + }, + { + "value": 138.13, + "percentage": 1, + "datetime": "2021-06-01T18:00:00.000+02:00" + }, + { + "value": 240.4, + "percentage": 1, + "datetime": "2021-06-01T19:00:00.000+02:00" + }, + { + "value": 246.2, + "percentage": 1, + "datetime": "2021-06-01T20:00:00.000+02:00" + }, + { + "value": 248.08, + "percentage": 1, + "datetime": "2021-06-01T21:00:00.000+02:00" + }, + { + "value": 249.41, + "percentage": 1, + "datetime": "2021-06-01T22:00:00.000+02:00" + }, + { + "value": 156.5, + "percentage": 1, + "datetime": "2021-06-01T23:00:00.000+02:00" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/tests/components/pvpc_hourly_pricing/test_config_flow.py b/tests/components/pvpc_hourly_pricing/test_config_flow.py index 1c9cb7e133d..ba0dc802007 100644 --- a/tests/components/pvpc_hourly_pricing/test_config_flow.py +++ b/tests/components/pvpc_hourly_pricing/test_config_flow.py @@ -90,10 +90,9 @@ async def test_config_flow( await hass.async_block_till_done() state = hass.states.get("sensor.test") check_valid_state(state, tariff=TARIFFS[1]) - price_pbc = state.state assert pvpc_aioclient_mock.call_count == 2 - assert state.attributes["period"] == "P2" - assert state.attributes["next_period"] == "P1" + assert state.attributes["period"] == "P1" + assert state.attributes["next_period"] == "P2" assert state.attributes["available_power"] == 4600 # check options flow @@ -111,10 +110,8 @@ async def test_config_flow( ) await hass.async_block_till_done() state = hass.states.get("sensor.test") - price_cym = state.state check_valid_state(state, tariff=TARIFFS[0]) assert pvpc_aioclient_mock.call_count == 3 assert state.attributes["period"] == "P2" assert state.attributes["next_period"] == "P1" assert state.attributes["available_power"] == 3000 - assert price_cym < price_pbc diff --git a/tests/components/pvpc_hourly_pricing/test_sensor.py b/tests/components/pvpc_hourly_pricing/test_sensor.py index cee2374f192..727a144e75d 100644 --- a/tests/components/pvpc_hourly_pricing/test_sensor.py +++ b/tests/components/pvpc_hourly_pricing/test_sensor.py @@ -11,100 +11,19 @@ from homeassistant.components.pvpc_hourly_pricing import ( TARIFFS, ) from homeassistant.const import CONF_NAME -from homeassistant.core import ATTR_NOW, EVENT_TIME_CHANGED from homeassistant.util import dt as dt_util from .conftest import check_valid_state -from tests.common import MockConfigEntry, date_util, mock_registry +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + date_util, + mock_registry, +) from tests.test_util.aiohttp import AiohttpClientMocker -async def _process_time_step( - hass, mock_data, key_state=None, value=None, tariff="discrimination", delta_min=60 -): - state = hass.states.get("sensor.test_dst") - check_valid_state(state, tariff=tariff, value=value, key_attr=key_state) - - mock_data["return_time"] += timedelta(minutes=delta_min) - hass.bus.async_fire(EVENT_TIME_CHANGED, {ATTR_NOW: mock_data["return_time"]}) - await hass.async_block_till_done() - return state - - -async def test_sensor_availability( - hass, caplog, legacy_patchable_time, pvpc_aioclient_mock: AiohttpClientMocker -): - """Test sensor availability and handling of cloud access.""" - hass.config.time_zone = dt_util.get_time_zone("Europe/Madrid") - config_entry = MockConfigEntry( - domain=DOMAIN, data={CONF_NAME: "test_dst", ATTR_TARIFF: "discrimination"} - ) - config_entry.add_to_hass(hass) - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - mock_data = {"return_time": datetime(2019, 10, 27, 20, 0, 0, tzinfo=date_util.UTC)} - - def mock_now(): - return mock_data["return_time"] - - with patch("homeassistant.util.dt.utcnow", new=mock_now): - assert await hass.config_entries.async_setup(config_entry.entry_id) - - # check migration - current_entries = hass.config_entries.async_entries(DOMAIN) - assert len(current_entries) == 1 - migrated_entry = current_entries[0] - assert migrated_entry.version == 1 - assert migrated_entry.data[ATTR_POWER] == migrated_entry.data[ATTR_POWER_P3] - assert migrated_entry.data[ATTR_TARIFF] == TARIFFS[0] - - await hass.async_block_till_done() - caplog.clear() - assert pvpc_aioclient_mock.call_count == 2 - - await _process_time_step(hass, mock_data, "price_21h", 0.13896) - await _process_time_step(hass, mock_data, "price_22h", 0.06893) - assert pvpc_aioclient_mock.call_count == 4 - await _process_time_step(hass, mock_data, "price_23h", 0.06935) - assert pvpc_aioclient_mock.call_count == 5 - - # sensor has no more prices, state is "unavailable" from now on - await _process_time_step(hass, mock_data, value="unavailable") - await _process_time_step(hass, mock_data, value="unavailable") - num_errors = sum( - 1 - for x in caplog.records - if x.levelno == logging.ERROR and "unknown job listener" not in x.msg - ) - num_warnings = sum(1 for x in caplog.records if x.levelno == logging.WARNING) - assert num_warnings == 1 - assert num_errors == 0 - assert pvpc_aioclient_mock.call_count == 9 - - # check that it is silent until it becomes available again - caplog.clear() - with caplog.at_level(logging.WARNING): - # silent mode - for _ in range(21): - await _process_time_step(hass, mock_data, value="unavailable") - assert pvpc_aioclient_mock.call_count == 30 - assert len(caplog.messages) == 0 - - # warning about data access recovered - await _process_time_step(hass, mock_data, value="unavailable") - assert pvpc_aioclient_mock.call_count == 31 - assert len(caplog.messages) == 1 - assert caplog.records[0].levelno == logging.WARNING - - # working ok again - await _process_time_step(hass, mock_data, "price_00h", value=0.06821) - assert pvpc_aioclient_mock.call_count == 32 - await _process_time_step(hass, mock_data, "price_01h", value=0.06627) - assert pvpc_aioclient_mock.call_count == 33 - assert len(caplog.messages) == 1 - assert caplog.records[0].levelno == logging.WARNING - - async def test_multi_sensor_migration( hass, caplog, legacy_patchable_time, pvpc_aioclient_mock: AiohttpClientMocker ): @@ -139,7 +58,7 @@ async def test_multi_sensor_migration( assert len(hass.config_entries.async_entries(DOMAIN)) == 2 assert len(entity_reg.entities) == 2 - mock_data = {"return_time": datetime(2019, 10, 27, 20, tzinfo=date_util.UTC)} + mock_data = {"return_time": datetime(2021, 6, 1, 21, tzinfo=date_util.UTC)} def mock_now(): return mock_data["return_time"] @@ -164,3 +83,15 @@ async def test_multi_sensor_migration( await hass.async_block_till_done() assert pvpc_aioclient_mock.call_count == 2 + + # check state and availability + state = hass.states.get("sensor.test_pvpc_1") + check_valid_state(state, tariff=TARIFFS[0], value=0.1565) + + mock_data["return_time"] += timedelta(minutes=60) + async_fire_time_changed(hass, mock_data["return_time"]) + await list(hass.data[DOMAIN].values())[0].async_refresh() + await hass.async_block_till_done() + state = hass.states.get("sensor.test_pvpc_1") + check_valid_state(state, tariff=TARIFFS[0], value="unavailable") + assert pvpc_aioclient_mock.call_count == 3 From 3605c4f32fbd378640d9ae7fc2aeef33119cc90d Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Thu, 23 Dec 2021 10:34:35 -0800 Subject: [PATCH 0999/2644] Add Overkiz integration (with base + sensor entity) (#62640) --- .coveragerc | 5 + CODEOWNERS | 2 + homeassistant/components/overkiz/__init__.py | 125 ++++++ .../components/overkiz/config_flow.py | 110 +++++ homeassistant/components/overkiz/const.py | 27 ++ .../components/overkiz/coordinator.py | 189 ++++++++ homeassistant/components/overkiz/entity.py | 108 +++++ homeassistant/components/overkiz/executor.py | 132 ++++++ .../components/overkiz/manifest.json | 21 + homeassistant/components/overkiz/sensor.py | 418 ++++++++++++++++++ homeassistant/components/overkiz/strings.json | 25 ++ .../components/overkiz/translations/en.json | 21 + homeassistant/generated/config_flows.py | 1 + homeassistant/generated/dhcp.py | 5 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/overkiz/test_config_flow.py | 191 ++++++++ 17 files changed, 1386 insertions(+) create mode 100644 homeassistant/components/overkiz/__init__.py create mode 100644 homeassistant/components/overkiz/config_flow.py create mode 100644 homeassistant/components/overkiz/const.py create mode 100644 homeassistant/components/overkiz/coordinator.py create mode 100644 homeassistant/components/overkiz/entity.py create mode 100644 homeassistant/components/overkiz/executor.py create mode 100644 homeassistant/components/overkiz/manifest.json create mode 100644 homeassistant/components/overkiz/sensor.py create mode 100644 homeassistant/components/overkiz/strings.json create mode 100644 homeassistant/components/overkiz/translations/en.json create mode 100644 tests/components/overkiz/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index cc96377a65d..24f01b0c463 100644 --- a/.coveragerc +++ b/.coveragerc @@ -802,6 +802,11 @@ omit = homeassistant/components/orvibo/switch.py homeassistant/components/osramlightify/light.py homeassistant/components/otp/sensor.py + homeassistant/components/overkiz/__init__.py + homeassistant/components/overkiz/coordinator.py + homeassistant/components/overkiz/entity.py + homeassistant/components/overkiz/executor.py + homeassistant/components/overkiz/sensor.py homeassistant/components/ovo_energy/__init__.py homeassistant/components/ovo_energy/const.py homeassistant/components/ovo_energy/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index bfad230f22c..217ffb0b22b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -668,6 +668,8 @@ homeassistant/components/opnsense/* @mtreinish tests/components/opnsense/* @mtreinish homeassistant/components/orangepi_gpio/* @pascallj homeassistant/components/oru/* @bvlaicu +homeassistant/components/overkiz/* @imicknl @vlebourl @tetienne +tests/components/overkiz/* @imicknl @vlebourl @tetienne homeassistant/components/ovo_energy/* @timmo001 tests/components/ovo_energy/* @timmo001 homeassistant/components/ozw/* @cgarwood @marcelveldt @MartinHjelmare diff --git a/homeassistant/components/overkiz/__init__.py b/homeassistant/components/overkiz/__init__.py new file mode 100644 index 00000000000..77c02daf2a6 --- /dev/null +++ b/homeassistant/components/overkiz/__init__.py @@ -0,0 +1,125 @@ +"""The Overkiz (by Somfy) integration.""" +from dataclasses import dataclass +import logging + +from aiohttp import ClientError, ServerDisconnectedError +from pyoverkiz.client import OverkizClient +from pyoverkiz.const import SUPPORTED_SERVERS +from pyoverkiz.exceptions import ( + BadCredentialsException, + MaintenanceException, + TooManyRequestsException, +) + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.aiohttp_client import async_create_clientsession + +from .const import ( + CONF_HUB, + DOMAIN, + PLATFORMS, + UPDATE_INTERVAL, + UPDATE_INTERVAL_ALL_ASSUMED_STATE, +) +from .coordinator import OverkizDataUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + + +@dataclass +class HomeAssistantOverkizData: + """Overkiz data stored in the Home Assistant data object.""" + + coordinator: OverkizDataUpdateCoordinator + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Overkiz from a config entry.""" + username = entry.data[CONF_USERNAME] + password = entry.data[CONF_PASSWORD] + server = SUPPORTED_SERVERS[entry.data[CONF_HUB]] + + # To allow users with multiple accounts/hubs, we create a new session so they have separate cookies + session = async_create_clientsession(hass) + client = OverkizClient( + username=username, password=password, session=session, server=server + ) + + try: + await client.login() + setup = await client.get_setup() + except BadCredentialsException: + _LOGGER.error("Invalid authentication") + return False + except TooManyRequestsException as exception: + raise ConfigEntryNotReady("Too many requests, try again later") from exception + except (TimeoutError, ClientError, ServerDisconnectedError) as exception: + raise ConfigEntryNotReady("Failed to connect") from exception + except MaintenanceException as exception: + raise ConfigEntryNotReady("Server is down for maintenance") from exception + except Exception as exception: # pylint: disable=broad-except + _LOGGER.exception(exception) + return False + + coordinator = OverkizDataUpdateCoordinator( + hass, + _LOGGER, + name="device events", + client=client, + devices=setup.devices, + places=setup.root_place, + update_interval=UPDATE_INTERVAL, + config_entry_id=entry.entry_id, + ) + + await coordinator.async_config_entry_first_refresh() + + if coordinator.is_stateless: + _LOGGER.debug( + "All devices have an assumed state. Update interval has been reduced to: %s", + UPDATE_INTERVAL_ALL_ASSUMED_STATE, + ) + coordinator.update_interval = UPDATE_INTERVAL_ALL_ASSUMED_STATE + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = HomeAssistantOverkizData( + coordinator=coordinator, + ) + + # Map Overkiz device to Home Assistant platform + for device in coordinator.data.values(): + _LOGGER.debug( + "The following device has been retrieved. Report an issue if not supported correctly (%s)", + device, + ) + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + device_registry = await dr.async_get_registry(hass) + + for gateway in setup.gateways: + _LOGGER.debug("Added gateway (%s)", gateway) + + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, gateway.id)}, + model=gateway.sub_type.beautify_name if gateway.sub_type else None, + manufacturer=server.manufacturer, + name=gateway.type.beautify_name, + sw_version=gateway.connectivity.protocol_version, + configuration_url=server.configuration_url, + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/overkiz/config_flow.py b/homeassistant/components/overkiz/config_flow.py new file mode 100644 index 00000000000..673a946d35f --- /dev/null +++ b/homeassistant/components/overkiz/config_flow.py @@ -0,0 +1,110 @@ +"""Config flow for Overkiz (by Somfy) integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from aiohttp import ClientError +from pyoverkiz.client import OverkizClient +from pyoverkiz.const import SUPPORTED_SERVERS +from pyoverkiz.exceptions import ( + BadCredentialsException, + MaintenanceException, + TooManyRequestsException, +) +from pyoverkiz.models import obfuscate_id +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components import dhcp +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import device_registry as dr + +from .const import CONF_HUB, DEFAULT_HUB, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Overkiz (by Somfy).""" + + VERSION = 1 + + async def async_validate_input(self, user_input: dict[str, Any]) -> None: + """Validate user credentials.""" + username = user_input[CONF_USERNAME] + password = user_input[CONF_PASSWORD] + server = SUPPORTED_SERVERS[user_input[CONF_HUB]] + + async with OverkizClient( + username=username, password=password, server=server + ) as client: + await client.login() + + # Set first gateway as unique id + if gateways := await client.get_gateways(): + gateway_id = gateways[0].id + await self.async_set_unique_id(gateway_id) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step via config flow.""" + errors = {} + + if user_input: + try: + await self.async_validate_input(user_input) + except TooManyRequestsException: + errors["base"] = "too_many_requests" + except BadCredentialsException: + errors["base"] = "invalid_auth" + except (TimeoutError, ClientError): + errors["base"] = "cannot_connect" + except MaintenanceException: + errors["base"] = "server_in_maintenance" + except Exception as exception: # pylint: disable=broad-except + errors["base"] = "unknown" + _LOGGER.exception(exception) + else: + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=user_input[CONF_USERNAME], data=user_input + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_HUB, default=DEFAULT_HUB): vol.In( + {key: hub.name for key, hub in SUPPORTED_SERVERS.items()} + ), + } + ), + errors=errors, + ) + + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + """Handle DHCP discovery.""" + hostname = discovery_info.hostname + gateway_id = hostname[8:22] + + _LOGGER.debug("DHCP discovery detected gateway %s", obfuscate_id(gateway_id)) + + if self._gateway_already_configured(gateway_id): + _LOGGER.debug("Gateway %s is already configured", obfuscate_id(gateway_id)) + return self.async_abort(reason="already_configured") + + return await self.async_step_user() + + def _gateway_already_configured(self, gateway_id: str) -> bool: + """See if we already have a gateway matching the id.""" + device_registry = dr.async_get(self.hass) + return bool( + device_registry.async_get_device( + identifiers={(DOMAIN, gateway_id)}, connections=set() + ) + ) diff --git a/homeassistant/components/overkiz/const.py b/homeassistant/components/overkiz/const.py new file mode 100644 index 00000000000..ff5dea13c65 --- /dev/null +++ b/homeassistant/components/overkiz/const.py @@ -0,0 +1,27 @@ +"""Constants for the Overkiz (by Somfy) integration.""" +from __future__ import annotations + +from datetime import timedelta +from typing import Final + +from pyoverkiz.enums import UIClass +from pyoverkiz.enums.ui import UIWidget + +from homeassistant.const import Platform + +DOMAIN: Final = "overkiz" + +CONF_HUB: Final = "hub" +DEFAULT_HUB: Final = "somfy_europe" + +UPDATE_INTERVAL: Final = timedelta(seconds=30) +UPDATE_INTERVAL_ALL_ASSUMED_STATE: Final = timedelta(minutes=60) + +PLATFORMS: list[Platform] = [ + Platform.SENSOR, +] + +IGNORED_OVERKIZ_DEVICES: list[UIClass | UIWidget] = [ + UIClass.PROTOCOL_GATEWAY, + UIClass.POD, +] diff --git a/homeassistant/components/overkiz/coordinator.py b/homeassistant/components/overkiz/coordinator.py new file mode 100644 index 00000000000..b258742fc6c --- /dev/null +++ b/homeassistant/components/overkiz/coordinator.py @@ -0,0 +1,189 @@ +"""Helpers to help coordinate updates.""" +from __future__ import annotations + +from collections.abc import Callable +from datetime import timedelta +import json +import logging +from typing import Any + +from aiohttp import ServerDisconnectedError +from pyoverkiz.client import OverkizClient +from pyoverkiz.enums import EventName, ExecutionState +from pyoverkiz.exceptions import ( + BadCredentialsException, + MaintenanceException, + NotAuthenticatedException, + TooManyRequestsException, +) +from pyoverkiz.models import DataType, Device, Place, State + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN, UPDATE_INTERVAL + +DATA_TYPE_TO_PYTHON: dict[DataType, Callable[[DataType], Any]] = { + DataType.INTEGER: int, + DataType.DATE: int, + DataType.STRING: str, + DataType.FLOAT: float, + DataType.BOOLEAN: bool, + DataType.JSON_ARRAY: json.loads, + DataType.JSON_OBJECT: json.loads, +} + +_LOGGER = logging.getLogger(__name__) + + +class OverkizDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching data from Overkiz platform.""" + + def __init__( + self, + hass: HomeAssistant, + logger: logging.Logger, + *, + name: str, + client: OverkizClient, + devices: list[Device], + places: Place, + update_interval: timedelta | None = None, + config_entry_id: str, + ) -> None: + """Initialize global data updater.""" + super().__init__( + hass, + logger, + name=name, + update_interval=update_interval, + ) + + self.data = {} + self.client = client + self.devices: dict[str, Device] = {d.device_url: d for d in devices} + self.is_stateless = all( + device.device_url.startswith("rts://") + or device.device_url.startswith("internal://") + for device in devices + ) + self.executions: dict[str, dict[str, str]] = {} + self.areas = self.places_to_area(places) + self._config_entry_id = config_entry_id + + async def _async_update_data(self) -> dict[str, Device]: + """Fetch Overkiz data via event listener.""" + try: + events = await self.client.fetch_events() + except BadCredentialsException as exception: + raise UpdateFailed("Invalid authentication") from exception + except TooManyRequestsException as exception: + raise UpdateFailed("Too many requests, try again later.") from exception + except MaintenanceException as exception: + raise UpdateFailed("Server is down for maintenance.") from exception + except TimeoutError as exception: + raise UpdateFailed("Failed to connect.") from exception + except (ServerDisconnectedError, NotAuthenticatedException): + self.executions = {} + + # During the relogin, similar exceptions can be thrown. + try: + await self.client.login() + self.devices = await self._get_devices() + except BadCredentialsException as exception: + raise UpdateFailed("Invalid authentication") from exception + except TooManyRequestsException as exception: + raise UpdateFailed("Too many requests, try again later.") from exception + + return self.devices + except Exception as exception: + _LOGGER.debug(exception) + raise UpdateFailed(exception) from exception + + for event in events: + _LOGGER.debug(event) + + if event.name == EventName.DEVICE_AVAILABLE: + self.devices[event.device_url].available = True + + elif event.name in [ + EventName.DEVICE_UNAVAILABLE, + EventName.DEVICE_DISABLED, + ]: + self.devices[event.device_url].available = False + + elif event.name in [ + EventName.DEVICE_CREATED, + EventName.DEVICE_UPDATED, + ]: + self.hass.async_create_task( + self.hass.config_entries.async_reload(self._config_entry_id) + ) + + elif event.name == EventName.DEVICE_REMOVED: + base_device_url, *_ = event.device_url.split("#") + registry = await device_registry.async_get_registry(self.hass) + + if registered_device := registry.async_get_device( + {(DOMAIN, base_device_url)} + ): + registry.async_remove_device(registered_device.id) + + del self.devices[event.device_url] + + elif event.name == EventName.DEVICE_STATE_CHANGED: + for state in event.device_states: + device = self.devices[event.device_url] + if state.name not in device.states: + device.states[state.name] = state + device.states[state.name].value = self._get_state(state) + + elif event.name == EventName.EXECUTION_REGISTERED: + if event.exec_id not in self.executions: + self.executions[event.exec_id] = {} + + if not self.is_stateless: + self.update_interval = timedelta(seconds=1) + + elif ( + event.name == EventName.EXECUTION_STATE_CHANGED + and event.exec_id in self.executions + and event.new_state in [ExecutionState.COMPLETED, ExecutionState.FAILED] + ): + del self.executions[event.exec_id] + + if not self.executions: + self.update_interval = UPDATE_INTERVAL + + return self.devices + + async def _get_devices(self) -> dict[str, Device]: + """Fetch devices.""" + _LOGGER.debug("Fetching all devices and state via /setup/devices") + return {d.device_url: d for d in await self.client.get_devices(refresh=True)} + + @staticmethod + def _get_state( + state: State, + ) -> dict[Any, Any] | list[Any] | float | int | bool | str | None: + """Cast string value to the right type.""" + data_type = DataType(state.type) + + if data_type == DataType.NONE: + return state.value + + cast_to_python = DATA_TYPE_TO_PYTHON[data_type] + return cast_to_python(state.value) + + def places_to_area(self, place): + """Convert places with sub_places to a flat dictionary.""" + areas = {} + if isinstance(place, Place): + areas[place.oid] = place.label + + if isinstance(place.sub_places, list): + for sub_place in place.sub_places: + areas.update(self.places_to_area(sub_place)) + + return areas diff --git a/homeassistant/components/overkiz/entity.py b/homeassistant/components/overkiz/entity.py new file mode 100644 index 00000000000..1a52a03ab36 --- /dev/null +++ b/homeassistant/components/overkiz/entity.py @@ -0,0 +1,108 @@ +"""Parent class for every Overkiz device.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from pyoverkiz.enums import OverkizAttribute, OverkizState +from pyoverkiz.models import Device + +from homeassistant.components.sensor import SensorEntityDescription +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import OverkizDataUpdateCoordinator +from .executor import OverkizExecutor + + +class OverkizEntity(CoordinatorEntity): + """Representation of an Overkiz device entity.""" + + coordinator: OverkizDataUpdateCoordinator + + def __init__( + self, device_url: str, coordinator: OverkizDataUpdateCoordinator + ) -> None: + """Initialize the device.""" + super().__init__(coordinator) + self.device_url = device_url + self.base_device_url, *_ = self.device_url.split("#") + self.executor = OverkizExecutor(device_url, coordinator) + + self._attr_assumed_state = not self.device.states + self._attr_available = self.device.available + self._attr_unique_id = self.device.device_url + self._attr_name = self.device.label + + self._attr_device_info = self.generate_device_info() + + @property + def device(self) -> Device: + """Return Overkiz device linked to this entity.""" + return self.coordinator.data[self.device_url] + + def generate_device_info(self) -> DeviceInfo: + """Return device registry information for this entity.""" + # Some devices, such as the Smart Thermostat have several devices in one physical device, + # with same device url, terminated by '#' and a number. + # In this case, we use the base device url as the device identifier. + if "#" in self.device_url and not self.device_url.endswith("#1"): + # Only return the url of the base device, to inherit device name and model from parent device. + return { + "identifiers": {(DOMAIN, self.executor.base_device_url)}, + } + + manufacturer = ( + self.executor.select_attribute(OverkizAttribute.CORE_MANUFACTURER) + or self.executor.select_state(OverkizState.CORE_MANUFACTURER_NAME) + or self.coordinator.client.server.manufacturer + ) + + model = ( + self.executor.select_state( + OverkizState.CORE_MODEL, + OverkizState.CORE_PRODUCT_MODEL_NAME, + OverkizState.IO_MODEL, + ) + or self.device.widget + ) + + return DeviceInfo( + identifiers={(DOMAIN, self.executor.base_device_url)}, + name=self.device.label, + manufacturer=manufacturer, + model=model, + sw_version=self.executor.select_attribute( + OverkizAttribute.CORE_FIRMWARE_REVISION + ), + hw_version=self.device.controllable_name, + suggested_area=self.coordinator.areas[self.device.place_oid], + via_device=self.executor.get_gateway_id(), + configuration_url=self.coordinator.client.server.configuration_url, + ) + + +@dataclass +class OverkizSensorDescription(SensorEntityDescription): + """Class to describe an Overkiz sensor.""" + + native_value: Callable[ + [str | int | float], str | int | float + ] | None = lambda val: val + + +class OverkizDescriptiveEntity(OverkizEntity): + """Representation of a Overkiz device entity based on a description.""" + + def __init__( + self, + device_url: str, + coordinator: OverkizDataUpdateCoordinator, + description: OverkizSensorDescription, + ) -> None: + """Initialize the device.""" + super().__init__(device_url, coordinator) + self.entity_description = description + self._attr_name = f"{super().name} {self.entity_description.name}" + self._attr_unique_id = f"{super().unique_id}-{self.entity_description.key}" diff --git a/homeassistant/components/overkiz/executor.py b/homeassistant/components/overkiz/executor.py new file mode 100644 index 00000000000..52e21255728 --- /dev/null +++ b/homeassistant/components/overkiz/executor.py @@ -0,0 +1,132 @@ +"""Class for helpers and communication with the OverKiz API.""" +from __future__ import annotations + +import logging +from typing import Any +from urllib.parse import urlparse + +from pyoverkiz.models import Command, Device + +from .coordinator import OverkizDataUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + + +class OverkizExecutor: + """Representation of an Overkiz device with execution handler.""" + + def __init__( + self, device_url: str, coordinator: OverkizDataUpdateCoordinator + ) -> None: + """Initialize the executor.""" + self.device_url = device_url + self.coordinator = coordinator + self.base_device_url = self.device_url.split("#")[0] + + @property + def device(self) -> Device: + """Return Overkiz device linked to this entity.""" + return self.coordinator.data[self.device_url] + + def select_command(self, *commands: str) -> str | None: + """Select first existing command in a list of commands.""" + existing_commands = self.device.definition.commands + return next((c for c in commands if c in existing_commands), None) + + def has_command(self, *commands: str) -> bool: + """Return True if a command exists in a list of commands.""" + return self.select_command(*commands) is not None + + def select_state(self, *states) -> str | None: + """Select first existing active state in a list of states.""" + for state in states: + if current_state := self.device.states[state]: + return current_state.value + + return None + + def has_state(self, *states: str) -> bool: + """Return True if a state exists in self.""" + return self.select_state(*states) is not None + + def select_attribute(self, *attributes) -> str | None: + """Select first existing active state in a list of states.""" + for attribute in attributes: + if current_attribute := self.device.attributes[attribute]: + return current_attribute.value + + return None + + async def async_execute_command(self, command_name: str, *args: Any): + """Execute device command in async context.""" + try: + exec_id = await self.coordinator.client.execute_command( + self.device.device_url, + Command(command_name, list(args)), + "Home Assistant", + ) + except Exception as exception: # pylint: disable=broad-except + _LOGGER.error(exception) + return + + # ExecutionRegisteredEvent doesn't contain the device_url, thus we need to register it here + self.coordinator.executions[exec_id] = { + "device_url": self.device.device_url, + "command_name": command_name, + } + + await self.coordinator.async_refresh() + + async def async_cancel_command(self, commands_to_cancel: list[str]) -> bool: + """Cancel running execution by command.""" + + # Cancel a running execution + # Retrieve executions initiated via Home Assistant from Data Update Coordinator queue + exec_id = next( + ( + exec_id + # Reverse dictionary to cancel the last added execution + for exec_id, execution in reversed(self.coordinator.executions.items()) + if execution.get("device_url") == self.device.device_url + and execution.get("command_name") in commands_to_cancel + ), + None, + ) + + if exec_id: + await self.async_cancel_execution(exec_id) + return True + + # Retrieve executions initiated outside Home Assistant via API + executions = await self.coordinator.client.get_current_executions() + exec_id = next( + ( + execution.id + for execution in executions + # Reverse dictionary to cancel the last added execution + for action in reversed(execution.action_group.get("actions")) + for command in action.get("commands") + if action.get("device_url") == self.device.device_url + and command.get("name") in commands_to_cancel + ), + None, + ) + + if exec_id: + await self.async_cancel_execution(exec_id) + return True + + return False + + async def async_cancel_execution(self, exec_id: str): + """Cancel running execution via execution id.""" + await self.coordinator.client.cancel_command(exec_id) + + def get_gateway_id(self): + """ + Retrieve gateway id from device url. + + device URL (:///[#]) + """ + url = urlparse(self.device_url) + return url.netloc diff --git a/homeassistant/components/overkiz/manifest.json b/homeassistant/components/overkiz/manifest.json new file mode 100644 index 00000000000..1278028d5e8 --- /dev/null +++ b/homeassistant/components/overkiz/manifest.json @@ -0,0 +1,21 @@ +{ + "domain": "overkiz", + "name": "Overkiz (by Somfy)", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/overkiz", + "requirements": [ + "pyoverkiz==1.0.0" + ], + "dhcp": [ + { + "hostname": "gateway*", + "macaddress": "F8811A*" + } + ], + "codeowners": [ + "@imicknl", + "@vlebourl", + "@tetienne" + ], + "iot_class": "cloud_polling" +} \ No newline at end of file diff --git a/homeassistant/components/overkiz/sensor.py b/homeassistant/components/overkiz/sensor.py new file mode 100644 index 00000000000..2f933055507 --- /dev/null +++ b/homeassistant/components/overkiz/sensor.py @@ -0,0 +1,418 @@ +"""Support for Overkiz sensors.""" +from __future__ import annotations + +from pyoverkiz.enums import OverkizAttribute, OverkizState, UIWidget + +from homeassistant.components.overkiz import HomeAssistantOverkizData +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONCENTRATION_PARTS_PER_MILLION, + ENERGY_WATT_HOUR, + LIGHT_LUX, + PERCENTAGE, + POWER_WATT, + SIGNAL_STRENGTH_DECIBELS, + TEMP_CELSIUS, + TIME_SECONDS, + VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR, + VOLUME_LITERS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN, IGNORED_OVERKIZ_DEVICES +from .coordinator import OverkizDataUpdateCoordinator +from .entity import OverkizDescriptiveEntity, OverkizEntity, OverkizSensorDescription + +SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ + OverkizSensorDescription( + key=OverkizState.CORE_BATTERY_LEVEL, + name="Battery Level", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + OverkizSensorDescription( + key=OverkizState.CORE_BATTERY, + name="Battery", + device_class=SensorDeviceClass.BATTERY, + native_value=lambda value: str(value).capitalize(), + entity_category=EntityCategory.DIAGNOSTIC, + ), + OverkizSensorDescription( + key=OverkizState.CORE_RSSI_LEVEL, + name="RSSI Level", + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + state_class=SensorStateClass.MEASUREMENT, + native_value=lambda value: round(float(value)), + entity_category=EntityCategory.DIAGNOSTIC, + ), + OverkizSensorDescription( + key=OverkizState.CORE_EXPECTED_NUMBER_OF_SHOWER, + name="Expected Number Of Shower", + icon="mdi:shower-head", + state_class=SensorStateClass.MEASUREMENT, + ), + OverkizSensorDescription( + key=OverkizState.CORE_NUMBER_OF_SHOWER_REMAINING, + name="Number of Shower Remaining", + icon="mdi:shower-head", + state_class=SensorStateClass.MEASUREMENT, + ), + # V40 is measured in litres (L) and shows the amount of warm (mixed) water with a temperature of 40 C, which can be drained from a switched off electric water heater. + OverkizSensorDescription( + key=OverkizState.CORE_V40_WATER_VOLUME_ESTIMATION, + name="Water Volume Estimation at 40 °C", + icon="mdi:water", + native_unit_of_measurement=VOLUME_LITERS, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + OverkizSensorDescription( + key=OverkizState.CORE_WATER_CONSUMPTION, + name="Water Consumption", + icon="mdi:water", + native_unit_of_measurement=VOLUME_LITERS, + state_class=SensorStateClass.MEASUREMENT, + ), + OverkizSensorDescription( + key=OverkizState.IO_OUTLET_ENGINE, + name="Outlet Engine", + icon="mdi:fan-chevron-down", + native_unit_of_measurement=VOLUME_LITERS, + state_class=SensorStateClass.MEASUREMENT, + ), + OverkizSensorDescription( + key=OverkizState.IO_INLET_ENGINE, + name="Inlet Engine", + icon="mdi:fan-chevron-up", + native_unit_of_measurement=VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR, + state_class=SensorStateClass.MEASUREMENT, + ), + OverkizSensorDescription( + key=OverkizState.HLRRWIFI_ROOM_TEMPERATURE, + name="Room Temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=TEMP_CELSIUS, + ), + OverkizSensorDescription( + key=OverkizState.IO_MIDDLE_WATER_TEMPERATURE, + name="Middle Water Temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=TEMP_CELSIUS, + ), + OverkizSensorDescription( + key=OverkizState.CORE_FOSSIL_ENERGY_CONSUMPTION, + name="Fossil Energy Consumption", + device_class=SensorDeviceClass.ENERGY, + ), + OverkizSensorDescription( + key=OverkizState.CORE_GAS_CONSUMPTION, + name="Gas Consumption", + ), + OverkizSensorDescription( + key=OverkizState.CORE_THERMAL_ENERGY_CONSUMPTION, + name="Thermal Energy Consumption", + ), + # LightSensor/LuminanceSensor + OverkizSensorDescription( + key=OverkizState.CORE_LUMINANCE, + name="Luminance", + device_class=SensorDeviceClass.ILLUMINANCE, + native_unit_of_measurement=LIGHT_LUX, # core:MeasuredValueType = core:LuminanceInLux + state_class=SensorStateClass.MEASUREMENT, + ), + # ElectricitySensor/CumulativeElectricPowerConsumptionSensor + OverkizSensorDescription( + key=OverkizState.CORE_ELECTRIC_ENERGY_CONSUMPTION, + name="Electric Energy Consumption", + device_class=SensorDeviceClass.ENERGY, + native_unit_of_measurement=ENERGY_WATT_HOUR, # core:MeasuredValueType = core:ElectricalEnergyInWh (not for modbus:YutakiV2DHWElectricalEnergyConsumptionComponent) + state_class=SensorStateClass.TOTAL_INCREASING, # core:MeasurementCategory attribute = electric/overall + ), + OverkizSensorDescription( + key=OverkizState.CORE_ELECTRIC_POWER_CONSUMPTION, + name="Electric Power Consumption", + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=POWER_WATT, # core:MeasuredValueType = core:ElectricalEnergyInWh (not for modbus:YutakiV2DHWElectricalEnergyConsumptionComponent) + state_class=SensorStateClass.MEASUREMENT, + ), + OverkizSensorDescription( + key=OverkizState.CORE_CONSUMPTION_TARIFF1, + name="Consumption Tariff 1", + device_class=SensorDeviceClass.ENERGY, + native_unit_of_measurement=ENERGY_WATT_HOUR, # core:MeasuredValueType = core:ElectricalEnergyInWh + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + OverkizSensorDescription( + key=OverkizState.CORE_CONSUMPTION_TARIFF2, + name="Consumption Tariff 2", + device_class=SensorDeviceClass.ENERGY, + native_unit_of_measurement=ENERGY_WATT_HOUR, # core:MeasuredValueType = core:ElectricalEnergyInWh + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + OverkizSensorDescription( + key=OverkizState.CORE_CONSUMPTION_TARIFF3, + name="Consumption Tariff 3", + device_class=SensorDeviceClass.ENERGY, + native_unit_of_measurement=ENERGY_WATT_HOUR, # core:MeasuredValueType = core:ElectricalEnergyInWh + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + OverkizSensorDescription( + key=OverkizState.CORE_CONSUMPTION_TARIFF4, + name="Consumption Tariff 4", + device_class=SensorDeviceClass.ENERGY, + native_unit_of_measurement=ENERGY_WATT_HOUR, # core:MeasuredValueType = core:ElectricalEnergyInWh + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + OverkizSensorDescription( + key=OverkizState.CORE_CONSUMPTION_TARIFF5, + name="Consumption Tariff 5", + device_class=SensorDeviceClass.ENERGY, + native_unit_of_measurement=ENERGY_WATT_HOUR, # core:MeasuredValueType = core:ElectricalEnergyInWh + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + OverkizSensorDescription( + key=OverkizState.CORE_CONSUMPTION_TARIFF6, + name="Consumption Tariff 6", + device_class=SensorDeviceClass.ENERGY, + native_unit_of_measurement=ENERGY_WATT_HOUR, # core:MeasuredValueType = core:ElectricalEnergyInWh + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + OverkizSensorDescription( + key=OverkizState.CORE_CONSUMPTION_TARIFF7, + name="Consumption Tariff 7", + device_class=SensorDeviceClass.ENERGY, + native_unit_of_measurement=ENERGY_WATT_HOUR, # core:MeasuredValueType = core:ElectricalEnergyInWh + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + OverkizSensorDescription( + key=OverkizState.CORE_CONSUMPTION_TARIFF8, + name="Consumption Tariff 8", + device_class=SensorDeviceClass.ENERGY, + native_unit_of_measurement=ENERGY_WATT_HOUR, # core:MeasuredValueType = core:ElectricalEnergyInWh + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + OverkizSensorDescription( + key=OverkizState.CORE_CONSUMPTION_TARIFF9, + name="Consumption Tariff 9", + device_class=SensorDeviceClass.ENERGY, + native_unit_of_measurement=ENERGY_WATT_HOUR, # core:MeasuredValueType = core:ElectricalEnergyInWh + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + # HumiditySensor/RelativeHumiditySensor + OverkizSensorDescription( + key=OverkizState.CORE_RELATIVE_HUMIDITY, + name="Relative Humidity", + native_value=lambda value: round(float(value), 2), + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, # core:MeasuredValueType = core:RelativeValueInPercentage + state_class=SensorStateClass.MEASUREMENT, + ), + # TemperatureSensor/TemperatureSensor + OverkizSensorDescription( + key=OverkizState.CORE_TEMPERATURE, + name="Temperature", + native_value=lambda value: round(float(value), 2), + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, # core:MeasuredValueType = core:TemperatureInCelcius + state_class=SensorStateClass.MEASUREMENT, + ), + # WeatherSensor/WeatherForecastSensor + OverkizSensorDescription( + key=OverkizState.CORE_WEATHER_STATUS, + name="Weather Status", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + OverkizSensorDescription( + key=OverkizState.CORE_MINIMUM_TEMPERATURE, + name="Minimum Temperature", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + OverkizSensorDescription( + key=OverkizState.CORE_MAXIMUM_TEMPERATURE, + name="Maximum Temperature", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + # AirSensor/COSensor + OverkizSensorDescription( + key=OverkizState.CORE_CO_CONCENTRATION, + name="CO Concentration", + device_class=SensorDeviceClass.CO, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + state_class=SensorStateClass.MEASUREMENT, + ), + # AirSensor/CO2Sensor + OverkizSensorDescription( + key=OverkizState.CORE_CO2_CONCENTRATION, + name="CO2 Concentration", + device_class=SensorDeviceClass.CO2, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + state_class=SensorStateClass.MEASUREMENT, + ), + # SunSensor/SunEnergySensor + OverkizSensorDescription( + key=OverkizState.CORE_SUN_ENERGY, + name="Sun Energy", + native_value=lambda value: round(float(value), 2), + device_class=SensorDeviceClass.ENERGY, + icon="mdi:solar-power", + state_class=SensorStateClass.MEASUREMENT, + ), + # WindSensor/WindSpeedSensor + OverkizSensorDescription( + key=OverkizState.CORE_WIND_SPEED, + name="Wind Speed", + native_value=lambda value: round(float(value), 2), + icon="mdi:weather-windy", + state_class=SensorStateClass.MEASUREMENT, + ), + # SmokeSensor/SmokeSensor + OverkizSensorDescription( + key=OverkizState.IO_SENSOR_ROOM, + name="Sensor Room", + native_value=lambda value: str(value).capitalize(), + entity_registry_enabled_default=False, + ), + OverkizSensorDescription( + key=OverkizState.IO_PRIORITY_LOCK_ORIGINATOR, + name="Priority Lock Originator", + native_value=lambda value: str(value).capitalize(), + icon="mdi:lock", + entity_registry_enabled_default=False, + ), + OverkizSensorDescription( + key=OverkizState.CORE_PRIORITY_LOCK_TIMER, + name="Priority Lock Timer", + icon="mdi:lock-clock", + native_unit_of_measurement=TIME_SECONDS, + entity_registry_enabled_default=False, + ), + OverkizSensorDescription( + key=OverkizState.CORE_DISCRETE_RSSI_LEVEL, + name="Discrete RSSI Level", + entity_registry_enabled_default=False, + native_value=lambda value: str(value).capitalize(), + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=EntityCategory.DIAGNOSTIC, + ), + # DomesticHotWaterProduction/WaterHeatingSystem + OverkizSensorDescription( + key=OverkizState.IO_HEAT_PUMP_OPERATING_TIME, + name="Heat Pump Operating Time", + ), + OverkizSensorDescription( + key=OverkizState.IO_ELECTRIC_BOOSTER_OPERATING_TIME, + name="Electric Booster Operating Time", + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +): + """Set up the Overkiz sensors from a config entry.""" + data: HomeAssistantOverkizData = hass.data[DOMAIN][entry.entry_id] + entities: list[SensorEntity] = [] + + key_supported_states = { + description.key: description for description in SENSOR_DESCRIPTIONS + } + + for device in data.coordinator.data.values(): + if ( + device.widget not in IGNORED_OVERKIZ_DEVICES + and device.ui_class not in IGNORED_OVERKIZ_DEVICES + ): + for state in device.definition.states: + if description := key_supported_states.get(state.qualified_name): + entities.append( + OverkizStateSensor( + device.device_url, + data.coordinator, + description, + ) + ) + + if device.widget == UIWidget.HOMEKIT_STACK: + entities.append( + OverkizHomeKitSetupCodeSensor( + device.device_url, + data.coordinator, + ) + ) + + async_add_entities(entities) + + +class OverkizStateSensor(OverkizDescriptiveEntity, SensorEntity): + """Representation of an Overkiz Sensor.""" + + @property + def native_value(self): + """Return the value of the sensor.""" + state = self.device.states.get(self.entity_description.key) + + if not state: + return None + + # Transform the value with a lambda function + if hasattr(self.entity_description, "native_value"): + return self.entity_description.native_value(state.value) + + return state.value + + +class OverkizHomeKitSetupCodeSensor(OverkizEntity, SensorEntity): + """Representation of an Overkiz HomeKit Setup Code.""" + + _attr_icon = "mdi:shield-home" + _attr_entity_category = EntityCategory.DIAGNOSTIC + + def __init__( + self, device_url: str, coordinator: OverkizDataUpdateCoordinator + ) -> None: + """Initialize the device.""" + super().__init__(device_url, coordinator) + self._attr_name = "HomeKit Setup Code" + + @property + def native_value(self): + """Return the value of the sensor.""" + return self.device.attributes.get(OverkizAttribute.HOMEKIT_SETUP_CODE).value + + @property + def device_info(self) -> DeviceInfo: + """Return device registry information for this entity.""" + # By default this sensor will be listed at a virtual HomekitStack device, + # but it makes more sense to show this at the gateway device in the entity registry. + return { + "identifiers": {(DOMAIN, self.executor.get_gateway_id())}, + } diff --git a/homeassistant/components/overkiz/strings.json b/homeassistant/components/overkiz/strings.json new file mode 100644 index 00000000000..57fea0d3ffb --- /dev/null +++ b/homeassistant/components/overkiz/strings.json @@ -0,0 +1,25 @@ +{ + "config": { + "step": { + "user": { + "description": "The Overkiz platform is used by various vendors like Somfy (Connexoon / TaHoma), Hitachi (Hi Kumo), Rexel (Energeasy Connect) and Atlantic (Cozytouch). Enter your application credentials and select your hub.", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "hub": "Hub" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "server_in_maintenance": "Server is down for maintenance", + "too_many_requests": "Too many requests, try again later.", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/en.json b/homeassistant/components/overkiz/translations/en.json new file mode 100644 index 00000000000..f15fe84c3ed --- /dev/null +++ b/homeassistant/components/overkiz/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password", + "username": "Username" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 473caaa44c8..928ac3dad67 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -224,6 +224,7 @@ FLOWS = [ "opentherm_gw", "openuv", "openweathermap", + "overkiz", "ovo_energy", "owntracks", "ozw", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index e68d7883181..e5bb2551427 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -201,6 +201,11 @@ DHCP = [ "domain": "nuki", "hostname": "nuki_bridge_*" }, + { + "domain": "overkiz", + "hostname": "gateway*", + "macaddress": "F8811A*" + }, { "domain": "powerwall", "hostname": "1118431-*", diff --git a/requirements_all.txt b/requirements_all.txt index f2a1aa593a1..5551a3d5675 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1724,6 +1724,9 @@ pyotgw==1.1b1 # homeassistant.components.otp pyotp==2.6.0 +# homeassistant.components.overkiz +pyoverkiz==1.0.0 + # homeassistant.components.openweathermap pyowm==3.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a222c5e968c..e9ca671e7b8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1066,6 +1066,9 @@ pyotgw==1.1b1 # homeassistant.components.otp pyotp==2.6.0 +# homeassistant.components.overkiz +pyoverkiz==1.0.0 + # homeassistant.components.openweathermap pyowm==3.2.0 diff --git a/tests/components/overkiz/test_config_flow.py b/tests/components/overkiz/test_config_flow.py new file mode 100644 index 00000000000..c9c745c9665 --- /dev/null +++ b/tests/components/overkiz/test_config_flow.py @@ -0,0 +1,191 @@ +"""Tests for Overkiz (by Somfy) config flow.""" +from __future__ import annotations + +from unittest.mock import Mock, patch + +from aiohttp import ClientError +from pyoverkiz.exceptions import ( + BadCredentialsException, + MaintenanceException, + TooManyRequestsException, +) +import pytest + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components import dhcp +from homeassistant.components.overkiz.const import DOMAIN +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, mock_device_registry + +TEST_EMAIL = "test@testdomain.com" +TEST_EMAIL2 = "test@testdomain.nl" +TEST_PASSWORD = "test-password" +TEST_PASSWORD2 = "test-password2" +TEST_HUB = "somfy_europe" +TEST_HUB2 = "hi_kumo_europe" +TEST_GATEWAY_ID = "1234-5678-9123" +TEST_GATEWAY_ID2 = "4321-5678-9123" + +MOCK_GATEWAY_RESPONSE = [Mock(id=TEST_GATEWAY_ID)] + + +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["errors"] == {} + + with patch("pyoverkiz.client.OverkizClient.login", return_value=True), patch( + "pyoverkiz.client.OverkizClient.get_gateways", return_value=None + ), patch( + "homeassistant.components.overkiz.async_setup_entry", return_value=True + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"username": TEST_EMAIL, "password": TEST_PASSWORD, "hub": TEST_HUB}, + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == TEST_EMAIL + assert result2["data"] == { + "username": TEST_EMAIL, + "password": TEST_PASSWORD, + "hub": TEST_HUB, + } + + await hass.async_block_till_done() + + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + "side_effect, error", + [ + (BadCredentialsException, "invalid_auth"), + (TooManyRequestsException, "too_many_requests"), + (TimeoutError, "cannot_connect"), + (ClientError, "cannot_connect"), + (MaintenanceException, "server_in_maintenance"), + (Exception, "unknown"), + ], +) +async def test_form_invalid_auth( + hass: HomeAssistant, side_effect: Exception, error: str +) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("pyoverkiz.client.OverkizClient.login", side_effect=side_effect): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"username": TEST_EMAIL, "password": TEST_PASSWORD, "hub": TEST_HUB}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": error} + + +async def test_abort_on_duplicate_entry(hass: HomeAssistant) -> None: + """Test config flow aborts Config Flow on duplicate entries.""" + MockConfigEntry( + domain=DOMAIN, + unique_id=TEST_GATEWAY_ID, + data={"username": TEST_EMAIL, "password": TEST_PASSWORD, "hub": TEST_HUB}, + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("pyoverkiz.client.OverkizClient.login", return_value=True), patch( + "pyoverkiz.client.OverkizClient.get_gateways", + return_value=MOCK_GATEWAY_RESPONSE, + ), patch("homeassistant.components.overkiz.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"username": TEST_EMAIL, "password": TEST_PASSWORD}, + ) + + assert result2["type"] == "abort" + assert result2["reason"] == "already_configured" + + +async def test_allow_multiple_unique_entries(hass: HomeAssistant) -> None: + """Test config flow allows Config Flow unique entries.""" + MockConfigEntry( + domain=DOMAIN, + unique_id="test2@testdomain.com", + data={"username": "test2@testdomain.com", "password": TEST_PASSWORD}, + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("pyoverkiz.client.OverkizClient.login", return_value=True), patch( + "pyoverkiz.client.OverkizClient.get_gateways", + return_value=MOCK_GATEWAY_RESPONSE, + ), patch("homeassistant.components.overkiz.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"username": TEST_EMAIL, "password": TEST_PASSWORD, "hub": TEST_HUB}, + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == TEST_EMAIL + assert result2["data"] == { + "username": TEST_EMAIL, + "password": TEST_PASSWORD, + "hub": TEST_HUB, + } + + +async def test_dhcp_flow(hass: HomeAssistant) -> None: + """Test that DHCP discovery for new bridge works.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=dhcp.DhcpServiceInfo( + hostname="gateway-1234-5678-9123", + ip="192.168.1.4", + macaddress="F8811A000000", + ), + context={"source": config_entries.SOURCE_DHCP}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == config_entries.SOURCE_USER + + +async def test_dhcp_flow_already_configured(hass: HomeAssistant) -> None: + """Test that DHCP doesn't setup already configured gateways.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=TEST_EMAIL, + data={"username": TEST_EMAIL, "password": TEST_PASSWORD, "hub": TEST_HUB}, + ) + config_entry.add_to_hass(hass) + + device_registry = mock_device_registry(hass) + device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(DOMAIN, "1234-5678-9123")}, + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=dhcp.DhcpServiceInfo( + hostname="gateway-1234-5678-9123", + ip="192.168.1.4", + macaddress="F8811A000000", + ), + context={"source": config_entries.SOURCE_DHCP}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" From df6fa43bff4ee00499bedf5a0a48cd802f55f253 Mon Sep 17 00:00:00 2001 From: Ed Coen Date: Thu, 23 Dec 2021 13:05:36 -0600 Subject: [PATCH 1000/2644] Add connectsense to homekit_controller (#62675) --- .../components/homekit_controller/const.py | 4 + .../components/homekit_controller/sensor.py | 30 ++ .../fixtures/connectsense.json | 476 ++++++++++++++++++ .../specific_devices/test_connectsense.py | 89 ++++ 4 files changed, 599 insertions(+) create mode 100644 tests/components/homekit_controller/fixtures/connectsense.json create mode 100644 tests/components/homekit_controller/specific_devices/test_connectsense.py diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index 7fab8222ae2..271834f2e92 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -47,6 +47,10 @@ HOMEKIT_ACCESSORY_DISPATCH = { } CHARACTERISTIC_PLATFORMS = { + CharacteristicsTypes.Vendor.CONNECTSENSE_ENERGY_WATT: "sensor", + CharacteristicsTypes.Vendor.CONNECTSENSE_ENERGY_AMPS: "sensor", + CharacteristicsTypes.Vendor.CONNECTSENSE_ENERGY_AMPS_20: "sensor", + CharacteristicsTypes.Vendor.CONNECTSENSE_ENERGY_KW_HOUR: "sensor", CharacteristicsTypes.Vendor.AQARA_GATEWAY_VOLUME: "number", CharacteristicsTypes.Vendor.AQARA_E1_GATEWAY_VOLUME: "number", CharacteristicsTypes.Vendor.AQARA_PAIRING_MODE: "switch", diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index a12858542b9..0ee74525dce 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -16,6 +16,8 @@ from homeassistant.components.sensor import ( from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_MILLION, + ELECTRIC_CURRENT_AMPERE, + ENERGY_KILO_WATT_HOUR, LIGHT_LUX, PERCENTAGE, POWER_WATT, @@ -37,6 +39,34 @@ class HomeKitSensorEntityDescription(SensorEntityDescription): SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = { + CharacteristicsTypes.Vendor.CONNECTSENSE_ENERGY_WATT: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.Vendor.CONNECTSENSE_ENERGY_WATT, + name="Real Time Energy", + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=POWER_WATT, + ), + CharacteristicsTypes.Vendor.CONNECTSENSE_ENERGY_AMPS: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.Vendor.CONNECTSENSE_ENERGY_AMPS, + name="Real Time Current", + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + ), + CharacteristicsTypes.Vendor.CONNECTSENSE_ENERGY_AMPS_20: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.Vendor.CONNECTSENSE_ENERGY_AMPS_20, + name="Real Time Current", + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + ), + CharacteristicsTypes.Vendor.CONNECTSENSE_ENERGY_KW_HOUR: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.Vendor.CONNECTSENSE_ENERGY_KW_HOUR, + name="Energy kWh", + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ), CharacteristicsTypes.Vendor.EVE_ENERGY_WATT: HomeKitSensorEntityDescription( key=CharacteristicsTypes.Vendor.EVE_ENERGY_WATT, name="Real Time Energy", diff --git a/tests/components/homekit_controller/fixtures/connectsense.json b/tests/components/homekit_controller/fixtures/connectsense.json new file mode 100644 index 00000000000..a2ea1c17cb0 --- /dev/null +++ b/tests/components/homekit_controller/fixtures/connectsense.json @@ -0,0 +1,476 @@ +[ + { + "aid": 1, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "iid": 2, + "type": "00000014-0000-1000-8000-0026BB765291", + "perms": [ + "pw" + ], + "ev": false, + "format": "bool" + }, + { + "iid": 3, + "value": "ConnectSense", + "type": "00000020-0000-1000-8000-0026BB765291", + "perms": [ + "pr" + ], + "ev": false, + "format": "string" + }, + { + "iid": 4, + "value": "CS-IWO", + "type": "00000021-0000-1000-8000-0026BB765291", + "perms": [ + "pr" + ], + "ev": false, + "format": "string" + }, + { + "iid": 5, + "value": "InWall Outlet-0394DE", + "type": "00000023-0000-1000-8000-0026BB765291", + "perms": [ + "pr" + ], + "ev": false, + "format": "string" + }, + { + "iid": 6, + "value": "1020301376", + "type": "00000030-0000-1000-8000-0026BB765291", + "perms": [ + "pr" + ], + "ev": false, + "format": "string" + }, + { + "iid": 7, + "value": "1.0.0", + "type": "00000052-0000-1000-8000-0026BB765291", + "perms": [ + "pr" + ], + "ev": false, + "format": "string" + } + ] + }, + { + "iid": 8, + "type": "000000A2-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "iid": 9, + "value": "1.1.0", + "type": "00000037-0000-1000-8000-0026BB765291", + "perms": [ + "pr" + ], + "ev": false, + "format": "string" + } + ] + }, + { + "iid": 10, + "type": "B3BD50B1-B30B-4974-A71F-5C68AA126698", + "hidden": true, + "characteristics": [ + { + "iid": 11, + "value": 100, + "type": "00000008-0000-1000-8000-0026BB765291", + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "format": "int", + "minValue": 0, + "maxValue": 100, + "minStep": 1, + "unit": "percentage" + }, + { + "iid": 12, + "value": 7250712, + "type": "00000005-0000-1000-8000-001D4B474349", + "perms": [ + "pr", + "ev" + ], + "ev": false, + "format": "uint32", + "description": "Epoch", + "unit": "seconds" + } + ] + }, + { + "iid": 13, + "type": "00000047-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "iid": 14, + "value": true, + "type": "00000025-0000-1000-8000-0026BB765291", + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": true, + "format": "bool" + }, + { + "iid": 15, + "value": true, + "type": "00000026-0000-1000-8000-0026BB765291", + "perms": [ + "pr", + "ev" + ], + "ev": true, + "format": "bool" + }, + { + "iid": 16, + "value": "Outlet A", + "type": "00000023-0000-1000-8000-0026BB765291", + "perms": [ + "pr" + ], + "ev": false, + "format": "string" + }, + { + "iid": 17, + "value": 126.3, + "type": "00000008-0000-1000-8000-001D4B474349", + "perms": [ + "pr", + "ev" + ], + "ev": false, + "format": "float", + "minValue": 0.0, + "maxValue": 130.0, + "minStep": 0.1, + "description": "Volts" + }, + { + "iid": 18, + "value": 0.03, + "type": "00000009-0000-1000-8000-001D4B474349", + "perms": [ + "pr", + "ev" + ], + "ev": false, + "format": "float", + "minValue": 0.0, + "maxValue": 20.0, + "minStep": 0.1, + "description": "Amps" + }, + { + "iid": 19, + "value": 0.8, + "type": "0000000A-0000-1000-8000-001D4B474349", + "perms": [ + "pr", + "ev" + ], + "ev": false, + "format": "float", + "description": "Watts" + }, + { + "iid": 20, + "value": 379.69299, + "type": "0000000C-0000-1000-8000-001D4B474349", + "perms": [ + "pr", + "ev" + ], + "ev": false, + "format": "float", + "minValue": 0.0, + "maxValue": 34028234663852885981170418348, + "minStep": 0.1, + "description": "Kilowatt-hours" + }, + { + "iid": 21, + "value": 22, + "type": "0000000B-0000-1000-8000-001D4B474349", + "perms": [ + "pr", + "ev" + ], + "ev": false, + "format": "int", + "minValue": 0, + "maxValue": 100, + "minStep": 1, + "description": "Power factor" + }, + { + "iid": 22, + "value": 390, + "type": "00000005-0000-1000-8000-001D4B474349", + "perms": [ + "pr", + "ev" + ], + "ev": false, + "format": "uint32", + "description": "State timer", + "unit": "seconds" + }, + { + "iid": 23, + "value": "Outlet A", + "type": "0000000E-0000-1000-8000-001D4B474349", + "perms": [ + "pr", + "ev" + ], + "ev": false, + "format": "string", + "description": "Assigned name" + }, + { + "iid": 24, + "value": 0, + "type": "0000000F-0000-1000-8000-001D4B474349", + "perms": [ + "pr", + "ev" + ], + "ev": false, + "format": "uint32", + "description": "Device type" + } + ] + }, + { + "iid": 25, + "type": "00000047-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "iid": 26, + "value": true, + "type": "00000025-0000-1000-8000-0026BB765291", + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": true, + "format": "bool" + }, + { + "iid": 27, + "value": true, + "type": "00000026-0000-1000-8000-0026BB765291", + "perms": [ + "pr", + "ev" + ], + "ev": true, + "format": "bool" + }, + { + "iid": 28, + "value": "Outlet B", + "type": "00000023-0000-1000-8000-0026BB765291", + "perms": [ + "pr" + ], + "ev": false, + "format": "string" + }, + { + "iid": 29, + "value": 126.3, + "type": "00000008-0000-1000-8000-001D4B474349", + "perms": [ + "pr", + "ev" + ], + "ev": false, + "format": "float", + "minValue": 0.0, + "maxValue": 130.0, + "minStep": 0.1, + "description": "Volts" + }, + { + "iid": 30, + "value": 0.05, + "type": "00000009-0000-1000-8000-001D4B474349", + "perms": [ + "pr", + "ev" + ], + "ev": false, + "format": "float", + "minValue": 0.0, + "maxValue": 20.0, + "minStep": 0.1, + "description": "Amps" + }, + { + "iid": 31, + "value": 0.8, + "type": "0000000A-0000-1000-8000-001D4B474349", + "perms": [ + "pr", + "ev" + ], + "ev": false, + "format": "float", + "description": "Watts" + }, + { + "iid": 32, + "value": 175.85001, + "type": "0000000C-0000-1000-8000-001D4B474349", + "perms": [ + "pr", + "ev" + ], + "ev": false, + "format": "float", + "minValue": 0.0, + "maxValue": 34028234663852885981170418348, + "minStep": 0.1, + "description": "Kilowatt-hours" + }, + { + "iid": 33, + "value": 13, + "type": "0000000B-0000-1000-8000-001D4B474349", + "perms": [ + "pr", + "ev" + ], + "ev": false, + "format": "int", + "minValue": 0, + "maxValue": 100, + "minStep": 1, + "description": "Power factor" + }, + { + "iid": 34, + "value": 390, + "type": "00000005-0000-1000-8000-001D4B474349", + "perms": [ + "pr", + "ev" + ], + "ev": false, + "format": "uint32", + "description": "State timer", + "unit": "seconds" + }, + { + "iid": 35, + "value": "Outlet B", + "type": "0000000E-0000-1000-8000-001D4B474349", + "perms": [ + "pr", + "ev" + ], + "ev": false, + "format": "string", + "description": "Assigned name" + }, + { + "iid": 36, + "value": 0, + "type": "0000000F-0000-1000-8000-001D4B474349", + "perms": [ + "pr", + "ev" + ], + "ev": false, + "format": "uint32", + "description": "Device type" + } + ] + }, + { + "iid": 37, + "type": "00000020-0000-1000-8000-001D4B474349", + "hidden": true, + "characteristics": [ + { + "iid": 38, + "value": 0, + "type": "00000021-0000-1000-8000-001D4B474349", + "perms": [ + "pr", + "ev" + ], + "ev": false, + "format": "uint8" + }, + { + "iid": 39, + "type": "00000022-0000-1000-8000-001D4B474349", + "perms": [ + "pw" + ], + "ev": false, + "format": "uint8" + }, + { + "iid": 40, + "type": "00000023-0000-1000-8000-001D4B474349", + "perms": [ + "pw" + ], + "ev": false, + "format": "string", + "maxLen": 256 + }, + { + "iid": 41, + "type": "00000024-0000-1000-8000-001D4B474349", + "perms": [ + "pw" + ], + "ev": false, + "format": "bool" + }, + { + "iid": 42, + "type": "00000300-0000-1000-8000-001D4B474349", + "perms": [ + "pw" + ], + "ev": false, + "format": "string", + "maxLen": 65 + } + ] + } + ] + } +] diff --git a/tests/components/homekit_controller/specific_devices/test_connectsense.py b/tests/components/homekit_controller/specific_devices/test_connectsense.py new file mode 100644 index 00000000000..29559118fe6 --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_connectsense.py @@ -0,0 +1,89 @@ +"""Make sure that ConnectSense Smart Outlet2 / In-Wall Outlet is enumerated properly.""" + +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from tests.components.homekit_controller.common import ( + Helper, + setup_accessories_from_file, + setup_test_accessories, +) + + +async def test_connectsense_setup(hass): + """Test that the accessory can be correctly setup in HA.""" + accessories = await setup_accessories_from_file(hass, "connectsense.json") + config_entry, pairing = await setup_test_accessories(hass, accessories) + + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + entities = [ + ( + "sensor.inwall_outlet_0394de_real_time_current", + "homekit-1020301376-aid:1-sid:13-cid:18", + "InWall Outlet-0394DE - Real Time Current", + ), + ( + "sensor.inwall_outlet_0394de_real_time_energy", + "homekit-1020301376-aid:1-sid:13-cid:19", + "InWall Outlet-0394DE - Real Time Energy", + ), + ( + "sensor.inwall_outlet_0394de_energy_kwh", + "homekit-1020301376-aid:1-sid:13-cid:20", + "InWall Outlet-0394DE - Energy kWh", + ), + ( + "switch.inwall_outlet_0394de", + "homekit-1020301376-13", + "InWall Outlet-0394DE", + ), + ( + "sensor.inwall_outlet_0394de_real_time_current_2", + "homekit-1020301376-aid:1-sid:25-cid:30", + "InWall Outlet-0394DE - Real Time Current", + ), + ( + "sensor.inwall_outlet_0394de_real_time_energy_2", + "homekit-1020301376-aid:1-sid:25-cid:31", + "InWall Outlet-0394DE - Real Time Energy", + ), + ( + "sensor.inwall_outlet_0394de_energy_kwh_2", + "homekit-1020301376-aid:1-sid:25-cid:32", + "InWall Outlet-0394DE - Energy kWh", + ), + ( + "switch.inwall_outlet_0394de_2", + "homekit-1020301376-25", + "InWall Outlet-0394DE", + ), + ] + + device_ids = set() + + for (entity_id, unique_id, friendly_name) in entities: + entry = entity_registry.async_get(entity_id) + assert entry.unique_id == unique_id + + helper = Helper( + hass, + entity_id, + pairing, + accessories[0], + config_entry, + ) + state = await helper.poll_and_get_state() + assert state.attributes["friendly_name"] == friendly_name + + device = device_registry.async_get(entry.device_id) + assert device.manufacturer == "ConnectSense" + assert device.name == "InWall Outlet-0394DE" + assert device.model == "CS-IWO" + assert device.sw_version == "1.0.0" + assert device.via_device_id is None + + device_ids.add(entry.device_id) + + # All entities should be part of same device + assert len(device_ids) == 1 From 00307e1ade7fab5919166ca41789ff7b77bd1e76 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 23 Dec 2021 14:07:29 -0500 Subject: [PATCH 1001/2644] Bump soco to 0.25.2 (#62691) --- homeassistant/components/sonos/manifest.json | 2 +- homeassistant/components/sonos/speaker.py | 4 ++-- homeassistant/components/sonos/switch.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/sonos/conftest.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 00006ab4e90..d4fce01fd78 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,7 +3,7 @@ "name": "Sonos", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", - "requirements": ["soco==0.25.1"], + "requirements": ["soco==0.25.2"], "dependencies": ["ssdp"], "after_dependencies": ["plex", "zeroconf"], "zeroconf": ["_sonos._tcp.local."], diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 8cd2abcf2b2..ffc0d08a1b4 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -482,7 +482,7 @@ class SonosSpeaker: for bool_var in ( "dialog_level", - "night_mode", + "night_level", "sub_enabled", "surround_enabled", ): @@ -965,7 +965,7 @@ class SonosSpeaker: self.volume = self.soco.volume self.muted = self.soco.mute self.night_mode = self.soco.night_mode - self.dialog_level = self.soco.dialog_mode + self.dialog_level = self.soco.dialog_level self.bass = self.soco.bass self.treble = self.soco.treble diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index a94df01fdc9..c82069c087a 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -37,7 +37,7 @@ ATTR_INCLUDE_LINKED_ZONES = "include_linked_zones" ATTR_CROSSFADE = "cross_fade" ATTR_NIGHT_SOUND = "night_mode" -ATTR_SPEECH_ENHANCEMENT = "dialog_mode" +ATTR_SPEECH_ENHANCEMENT = "dialog_level" ATTR_STATUS_LIGHT = "status_light" ATTR_SUB_ENABLED = "sub_enabled" ATTR_SURROUND_ENABLED = "surround_enabled" diff --git a/requirements_all.txt b/requirements_all.txt index 5551a3d5675..542d16ddce6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2204,7 +2204,7 @@ smhi-pkg==1.0.15 snapcast==2.1.3 # homeassistant.components.sonos -soco==0.25.1 +soco==0.25.2 # homeassistant.components.solaredge_local solaredge-local==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e9ca671e7b8..96667fd1309 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1310,7 +1310,7 @@ smarthab==0.21 smhi-pkg==1.0.15 # homeassistant.components.sonos -soco==0.25.1 +soco==0.25.2 # homeassistant.components.solaredge solaredge==0.0.2 diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index f9adf1be8f9..e6b48ff9a26 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -99,7 +99,7 @@ def soco_fixture(music_library, speaker_info, battery_info, alarm_clock): mock_soco.alarmClock = alarm_clock mock_soco.mute = False mock_soco.night_mode = True - mock_soco.dialog_mode = True + mock_soco.dialog_level = True mock_soco.volume = 19 mock_soco.bass = 1 mock_soco.treble = -1 From 55f4962c061d86dfc66d07f23883535d19c9d825 Mon Sep 17 00:00:00 2001 From: Ruslan Sayfutdinov Date: Thu, 23 Dec 2021 19:14:47 +0000 Subject: [PATCH 1002/2644] Fix pylint plugin which checks relative imports (#62693) --- homeassistant/__main__.py | 10 ++--- homeassistant/async_timeout_backcompat.py | 2 +- homeassistant/auth/auth_store.py | 8 +++- homeassistant/block_async_io.py | 2 +- homeassistant/bootstrap.py | 27 +++++------ homeassistant/components/zha/core/typing.py | 15 +++---- homeassistant/config.py | 44 ++++++++---------- homeassistant/config_entries.py | 45 ++++++++----------- homeassistant/const.py | 2 +- homeassistant/core.py | 26 +++++------ homeassistant/helpers/aiohttp_client.py | 3 +- homeassistant/helpers/area_registry.py | 2 +- homeassistant/helpers/check_config.py | 3 +- homeassistant/helpers/collection.py | 9 ++-- homeassistant/helpers/condition.py | 8 ++-- homeassistant/helpers/config_entry_flow.py | 3 +- .../helpers/config_entry_oauth2_flow.py | 2 +- homeassistant/helpers/config_validation.py | 6 +-- homeassistant/helpers/data_entry_flow.py | 3 +- homeassistant/helpers/device_registry.py | 4 +- homeassistant/helpers/entity.py | 11 ++--- homeassistant/helpers/entity_component.py | 10 +---- homeassistant/helpers/entity_registry.py | 6 +-- homeassistant/helpers/entityfilter.py | 3 +- homeassistant/helpers/event.py | 11 ++--- homeassistant/helpers/httpx_client.py | 3 +- homeassistant/helpers/intent.py | 3 +- homeassistant/helpers/location.py | 2 +- homeassistant/helpers/reload.py | 7 +-- homeassistant/helpers/restore_state.py | 13 +++--- homeassistant/helpers/script.py | 25 ++++------- homeassistant/helpers/service.py | 21 ++++----- homeassistant/helpers/storage.py | 2 +- homeassistant/helpers/template.py | 24 +++------- homeassistant/helpers/trace.py | 3 +- homeassistant/helpers/trigger.py | 3 +- homeassistant/helpers/update_coordinator.py | 2 +- homeassistant/loader.py | 18 ++++---- homeassistant/requirements.py | 10 ++--- homeassistant/runner.py | 10 ++--- homeassistant/setup.py | 14 +++--- homeassistant/util/executor.py | 2 +- homeassistant/util/unit_system.py | 3 +- pylint/plugins/hass_imports.py | 16 ++++--- 44 files changed, 209 insertions(+), 237 deletions(-) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index c2802e1b9c4..1a129686a8f 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -9,7 +9,7 @@ import subprocess import sys import threading -from homeassistant.const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__ +from .const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__ FAULT_LOG_FILENAME = "home-assistant.log.fault" @@ -27,7 +27,7 @@ def validate_python() -> None: def ensure_config_path(config_dir: str) -> None: """Validate the configuration directory.""" # pylint: disable=import-outside-toplevel - import homeassistant.config as config_util + from . import config as config_util lib_dir = os.path.join(config_dir, "deps") @@ -61,7 +61,7 @@ def ensure_config_path(config_dir: str) -> None: def get_arguments() -> argparse.Namespace: """Get parsed passed in arguments.""" # pylint: disable=import-outside-toplevel - import homeassistant.config as config_util + from . import config as config_util parser = argparse.ArgumentParser( description="Home Assistant: Observe, Control, Automate." @@ -282,7 +282,7 @@ def main() -> int: if args.script is not None: # pylint: disable=import-outside-toplevel - from homeassistant import scripts + from . import scripts return scripts.run(args.script) @@ -298,7 +298,7 @@ def main() -> int: write_pid(args.pid_file) # pylint: disable=import-outside-toplevel - from homeassistant import runner + from . import runner runtime_conf = runner.RuntimeConfig( config_dir=config_dir, diff --git a/homeassistant/async_timeout_backcompat.py b/homeassistant/async_timeout_backcompat.py index 189f64020bb..70b38c18708 100644 --- a/homeassistant/async_timeout_backcompat.py +++ b/homeassistant/async_timeout_backcompat.py @@ -6,7 +6,7 @@ from typing import Any import async_timeout -from homeassistant.helpers.frame import report +from .helpers.frame import report def timeout( diff --git a/homeassistant/auth/auth_store.py b/homeassistant/auth/auth_store.py index 8acb7c44398..8b8531b4aff 100644 --- a/homeassistant/auth/auth_store.py +++ b/homeassistant/auth/auth_store.py @@ -8,12 +8,16 @@ import hmac from logging import getLogger from typing import Any -from homeassistant.auth.const import ACCESS_TOKEN_EXPIRATION from homeassistant.core import HomeAssistant, callback from homeassistant.util import dt as dt_util from . import models -from .const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY, GROUP_ID_USER +from .const import ( + ACCESS_TOKEN_EXPIRATION, + GROUP_ID_ADMIN, + GROUP_ID_READ_ONLY, + GROUP_ID_USER, +) from .permissions import PermissionLookup, system_policies from .permissions.types import PolicyType diff --git a/homeassistant/block_async_io.py b/homeassistant/block_async_io.py index ec56b746706..0a1f4445c3e 100644 --- a/homeassistant/block_async_io.py +++ b/homeassistant/block_async_io.py @@ -1,7 +1,7 @@ """Block I/O being done in asyncio.""" from http.client import HTTPConnection -from homeassistant.util.async_ import protect_loop +from .util.async_ import protect_loop def enable() -> None: diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 64a6e98aa87..94dfb2bd1c8 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -15,27 +15,24 @@ from typing import TYPE_CHECKING, Any import voluptuous as vol import yarl -from homeassistant import config as conf_util, config_entries, core, loader -from homeassistant.components import http -from homeassistant.const import ( - REQUIRED_NEXT_PYTHON_HA_RELEASE, - REQUIRED_NEXT_PYTHON_VER, -) -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import area_registry, device_registry, entity_registry -from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.typing import ConfigType -from homeassistant.setup import ( +from . import config as conf_util, config_entries, core, loader +from .components import http +from .const import REQUIRED_NEXT_PYTHON_HA_RELEASE, REQUIRED_NEXT_PYTHON_VER +from .exceptions import HomeAssistantError +from .helpers import area_registry, device_registry, entity_registry +from .helpers.dispatcher import async_dispatcher_send +from .helpers.typing import ConfigType +from .setup import ( DATA_SETUP, DATA_SETUP_STARTED, DATA_SETUP_TIME, async_set_domains_to_be_loaded, async_setup_component, ) -from homeassistant.util.async_ import gather_with_concurrency -import homeassistant.util.dt as dt_util -from homeassistant.util.logging import async_activate_log_queue_handler -from homeassistant.util.package import async_get_user_site, is_virtual_env +from .util import dt as dt_util +from .util.async_ import gather_with_concurrency +from .util.logging import async_activate_log_queue_handler +from .util.package import async_get_user_site, is_virtual_env if TYPE_CHECKING: from .runner import RuntimeConfig diff --git a/homeassistant/components/zha/core/typing.py b/homeassistant/components/zha/core/typing.py index 62a797d9fd5..7e5cce8fec5 100644 --- a/homeassistant/components/zha/core/typing.py +++ b/homeassistant/components/zha/core/typing.py @@ -26,20 +26,17 @@ ZigpyGroupType = zigpy.group.Group ZigpyZdoType = zigpy.zdo.ZDO if TYPE_CHECKING: - from homeassistant.components.zha.core import channels - import homeassistant.components.zha.core.channels - import homeassistant.components.zha.core.channels.base as base_channels - import homeassistant.components.zha.core.device - import homeassistant.components.zha.core.gateway - import homeassistant.components.zha.core.group import homeassistant.components.zha.entity + from . import channels, device, gateway, group + from .channels import base as base_channels + ChannelType = base_channels.ZigbeeChannel ChannelsType = channels.Channels ChannelPoolType = channels.ChannelPool ClientChannelType = base_channels.ClientChannel ZDOChannelType = base_channels.ZDOChannel - ZhaDeviceType = homeassistant.components.zha.core.device.ZHADevice + ZhaDeviceType = device.ZHADevice ZhaEntityType = homeassistant.components.zha.entity.ZhaEntity - ZhaGatewayType = homeassistant.components.zha.core.gateway.ZHAGateway - ZhaGroupType = homeassistant.components.zha.core.group.ZHAGroup + ZhaGatewayType = gateway.ZHAGateway + ZhaGroupType = group.ZHAGroup diff --git a/homeassistant/config.py b/homeassistant/config.py index a2c613ebe69..ed9ffa7c47d 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -16,12 +16,9 @@ from awesomeversion import AwesomeVersion import voluptuous as vol from voluptuous.humanize import humanize_error -from homeassistant import auth -from homeassistant.auth import ( - mfa_modules as auth_mfa_modules, - providers as auth_providers, -) -from homeassistant.const import ( +from . import auth +from .auth import mfa_modules as auth_mfa_modules, providers as auth_providers +from .const import ( ATTR_ASSUMED_STATE, ATTR_FRIENDLY_NAME, ATTR_HIDDEN, @@ -52,25 +49,20 @@ from homeassistant.const import ( TEMP_CELSIUS, __version__, ) -from homeassistant.core import ( - DOMAIN as CONF_CORE, - ConfigSource, - HomeAssistant, - callback, +from .core import DOMAIN as CONF_CORE, ConfigSource, HomeAssistant, callback +from .exceptions import HomeAssistantError +from .helpers import ( + config_per_platform, + config_validation as cv, + extract_domain_configs, ) -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_per_platform, extract_domain_configs -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_values import EntityValues -from homeassistant.helpers.typing import ConfigType -from homeassistant.loader import Integration, IntegrationNotFound -from homeassistant.requirements import ( - RequirementsNotFound, - async_get_integration_with_requirements, -) -from homeassistant.util.package import is_docker_env -from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM -from homeassistant.util.yaml import SECRET_YAML, Secrets, load_yaml +from .helpers.entity_values import EntityValues +from .helpers.typing import ConfigType +from .loader import Integration, IntegrationNotFound +from .requirements import RequirementsNotFound, async_get_integration_with_requirements +from .util.package import is_docker_env +from .util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM +from .util.yaml import SECRET_YAML, Secrets, load_yaml _LOGGER = logging.getLogger(__name__) @@ -938,7 +930,7 @@ async def async_check_ha_config_file(hass: HomeAssistant) -> str | None: This method is a coroutine. """ # pylint: disable=import-outside-toplevel - from homeassistant.helpers import check_config + from .helpers import check_config res = await check_config.async_check_ha_config_file(hass) @@ -956,7 +948,7 @@ def async_notify_setup_error( This method must be run in the event loop. """ # pylint: disable=import-outside-toplevel - from homeassistant.components import persistent_notification + from .components import persistent_notification if (errors := hass.data.get(DATA_PERSISTENT_ERRORS)) is None: errors = hass.data[DATA_PERSISTENT_ERRORS] = {} diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index d1910364f4c..063040dc398 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -12,35 +12,26 @@ from types import MappingProxyType, MethodType from typing import TYPE_CHECKING, Any, Callable, Optional, cast import weakref -from homeassistant import data_entry_flow, loader -from homeassistant.backports.enum import StrEnum -from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback -from homeassistant.exceptions import ( - ConfigEntryAuthFailed, - ConfigEntryNotReady, - HomeAssistantError, -) -from homeassistant.helpers import device_registry, entity_registry -from homeassistant.helpers.event import Event -from homeassistant.helpers.frame import report -from homeassistant.helpers.typing import ( - UNDEFINED, - ConfigType, - DiscoveryInfoType, - UndefinedType, -) -from homeassistant.setup import async_process_deps_reqs, async_setup_component -from homeassistant.util.decorator import Registry -import homeassistant.util.uuid as uuid_util +from . import data_entry_flow, loader +from .backports.enum import StrEnum +from .const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP +from .core import CALLBACK_TYPE, CoreState, HomeAssistant, callback +from .exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady, HomeAssistantError +from .helpers import device_registry, entity_registry +from .helpers.event import Event +from .helpers.frame import report +from .helpers.typing import UNDEFINED, ConfigType, DiscoveryInfoType, UndefinedType +from .setup import async_process_deps_reqs, async_setup_component +from .util import uuid as uuid_util +from .util.decorator import Registry if TYPE_CHECKING: - from homeassistant.components.dhcp import DhcpServiceInfo - from homeassistant.components.hassio import HassioServiceInfo - from homeassistant.components.mqtt import MqttServiceInfo - from homeassistant.components.ssdp import SsdpServiceInfo - from homeassistant.components.usb import UsbServiceInfo - from homeassistant.components.zeroconf import ZeroconfServiceInfo + from .components.dhcp import DhcpServiceInfo + from .components.hassio import HassioServiceInfo + from .components.mqtt import MqttServiceInfo + from .components.ssdp import SsdpServiceInfo + from .components.usb import UsbServiceInfo + from .components.zeroconf import ZeroconfServiceInfo _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/const.py b/homeassistant/const.py index ffe472acec9..6dcda3243b1 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Final -from homeassistant.backports.enum import StrEnum +from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 2 diff --git a/homeassistant/core.py b/homeassistant/core.py index 51317d17d48..9566ad6c596 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -25,9 +25,9 @@ import attr import voluptuous as vol import yarl -from homeassistant import async_timeout_backcompat, block_async_io, loader, util -from homeassistant.backports.enum import StrEnum -from homeassistant.const import ( +from . import async_timeout_backcompat, block_async_io, loader, util +from .backports.enum import StrEnum +from .const import ( ATTR_DOMAIN, ATTR_FRIENDLY_NAME, ATTR_NOW, @@ -53,7 +53,7 @@ from homeassistant.const import ( MAX_LENGTH_STATE_STATE, __version__, ) -from homeassistant.exceptions import ( +from .exceptions import ( HomeAssistantError, InvalidEntityFormatError, InvalidStateError, @@ -61,22 +61,20 @@ from homeassistant.exceptions import ( ServiceNotFound, Unauthorized, ) -from homeassistant.util import location -from homeassistant.util.async_ import ( +from .util import dt as dt_util, location, uuid as uuid_util +from .util.async_ import ( fire_coroutine_threadsafe, run_callback_threadsafe, shutdown_run_callback_threadsafe, ) -import homeassistant.util.dt as dt_util -from homeassistant.util.timeout import TimeoutManager -from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM, UnitSystem -import homeassistant.util.uuid as uuid_util +from .util.timeout import TimeoutManager +from .util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM, UnitSystem # Typing imports that create a circular dependency if TYPE_CHECKING: - from homeassistant.auth import AuthManager - from homeassistant.components.http import HomeAssistantHTTP - from homeassistant.config_entries import ConfigEntries + from .auth import AuthManager + from .components.http import HomeAssistantHTTP + from .config_entries import ConfigEntries STAGE_1_SHUTDOWN_TIMEOUT = 100 @@ -286,7 +284,7 @@ class HomeAssistant: await self.async_start() if attach_signals: # pylint: disable=import-outside-toplevel - from homeassistant.helpers.signal import async_register_signal_handling + from .helpers.signal import async_register_signal_handling async_register_signal_handling(self) diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 908a9d68ddf..65b1b657ef4 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -18,10 +18,11 @@ import async_timeout from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE, __version__ from homeassistant.core import Event, HomeAssistant, callback -from homeassistant.helpers.frame import warn_use from homeassistant.loader import bind_hass from homeassistant.util import ssl as ssl_util +from .frame import warn_use + DATA_CONNECTOR = "aiohttp_connector" DATA_CONNECTOR_NOTVERIFY = "aiohttp_connector_notverify" DATA_CLIENTSESSION = "aiohttp_clientsession" diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index 11b7e5a78bd..8c4179dd940 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -8,10 +8,10 @@ from typing import cast import attr from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.loader import bind_hass from homeassistant.util import slugify +from . import device_registry as dr, entity_registry as er from .typing import UNDEFINED, UndefinedType # mypy: disallow-any-generics diff --git a/homeassistant/helpers/check_config.py b/homeassistant/helpers/check_config.py index 83505fc8356..6e0415b2a54 100644 --- a/homeassistant/helpers/check_config.py +++ b/homeassistant/helpers/check_config.py @@ -23,7 +23,6 @@ from homeassistant.config import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.typing import ConfigType from homeassistant.requirements import ( RequirementsNotFound, async_clear_install_history, @@ -31,6 +30,8 @@ from homeassistant.requirements import ( ) import homeassistant.util.yaml.loader as yaml_loader +from .typing import ConfigType + class CheckConfigError(NamedTuple): """Configuration check error.""" diff --git a/homeassistant/helpers/collection.py b/homeassistant/helpers/collection.py index f1ea4800c16..57e34a02d96 100644 --- a/homeassistant/helpers/collection.py +++ b/homeassistant/helpers/collection.py @@ -16,12 +16,13 @@ from homeassistant.components import websocket_api from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import entity_registry -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.storage import Store from homeassistant.util import slugify +from . import entity_registry +from .entity import Entity +from .entity_component import EntityComponent +from .storage import Store + STORAGE_VERSION = 1 SAVE_DELAY = 10 diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index f52e1bb1595..404eea5ea4a 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -52,13 +52,12 @@ from homeassistant.exceptions import ( HomeAssistantError, TemplateError, ) -from homeassistant.helpers import config_validation as cv, entity_registry as er -from homeassistant.helpers.sun import get_astral_event_date -from homeassistant.helpers.template import Template -from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.util.async_ import run_callback_threadsafe import homeassistant.util.dt as dt_util +from . import config_validation as cv, entity_registry as er +from .sun import get_astral_event_date +from .template import Template from .trace import ( TraceElement, trace_append_element, @@ -69,6 +68,7 @@ from .trace import ( trace_stack_push, trace_stack_top, ) +from .typing import ConfigType, TemplateVarsType # mypy: disallow-any-generics diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 0a565b3b9eb..a3e7ae4869d 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -8,7 +8,8 @@ from homeassistant import config_entries from homeassistant.components import dhcp, mqtt, ssdp, zeroconf from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.typing import UNDEFINED, DiscoveryInfoType, UndefinedType + +from .typing import UNDEFINED, DiscoveryInfoType, UndefinedType DiscoveryFunctionType = Callable[[HomeAssistant], Union[Awaitable[bool], bool]] diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index 3c987b1ea9e..b3a569aa071 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -25,9 +25,9 @@ from homeassistant import config_entries from homeassistant.components import http from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.network import NoURLAvailableError from .aiohttp_client import async_get_clientsession +from .network import NoURLAvailableError _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 08f232951ae..ffd1f7586c0 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -75,13 +75,11 @@ from homeassistant.const import ( ) from homeassistant.core import split_entity_id, valid_entity_id from homeassistant.exceptions import TemplateError -from homeassistant.helpers import ( - script_variables as script_variables_helper, - template as template_helper, -) from homeassistant.util import raise_if_invalid_path, slugify as util_slugify import homeassistant.util.dt as dt_util +from . import script_variables as script_variables_helper, template as template_helper + # pylint: disable=invalid-name TIME_PERIOD_ERROR = "offset {} should be format 'HH:MM', 'HH:MM:SS' or 'HH:MM:SS.F'" diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py index 061233c0e1a..09345bf51bf 100644 --- a/homeassistant/helpers/data_entry_flow.py +++ b/homeassistant/helpers/data_entry_flow.py @@ -10,7 +10,8 @@ import voluptuous as vol from homeassistant import config_entries, data_entry_flow from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator -import homeassistant.helpers.config_validation as cv + +from . import config_validation as cv class _BaseFlowManagerView(HomeAssistantView): diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 68af26160d8..bf8ef5fbd0e 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -12,12 +12,12 @@ from homeassistant.backports.enum import StrEnum from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import RequiredParameterMissing -from homeassistant.helpers import storage -from homeassistant.helpers.frame import report from homeassistant.loader import bind_hass import homeassistant.util.uuid as uuid_util +from . import storage from .debounce import Debouncer +from .frame import report from .typing import UNDEFINED, UndefinedType # mypy: disallow_any_generics diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 87891c9f2a8..474d079edbc 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -37,14 +37,15 @@ from homeassistant.const import ( ) from homeassistant.core import CALLBACK_TYPE, Context, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError, NoEntitySpecifiedError -from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity_platform import EntityPlatform -from homeassistant.helpers.event import Event, async_track_entity_registry_updated_event -from homeassistant.helpers.typing import StateType from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util, ensure_unique_string, slugify +from . import entity_registry as er +from .device_registry import DeviceEntryType +from .entity_platform import EntityPlatform +from .event import Event, async_track_entity_registry_updated_event +from .typing import StateType + _LOGGER = logging.getLogger(__name__) SLOW_UPDATE_WARNING = 10 DATA_ENTITY_SOURCE = "entity_info" diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index c190fd5fc35..66d733619d0 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -20,18 +20,12 @@ from homeassistant.const import ( ) from homeassistant.core import Event, HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import ( - config_per_platform, - config_validation as cv, - discovery, - entity, - service, -) -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.loader import async_get_integration, bind_hass from homeassistant.setup import async_prepare_setup_platform +from . import config_per_platform, config_validation as cv, discovery, entity, service from .entity_platform import EntityPlatform +from .typing import ConfigType, DiscoveryInfoType DEFAULT_SCAN_INTERVAL = timedelta(seconds=15) DATA_INSTANCES = "entity_components" diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index b79e9af209e..7c49e884646 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -38,13 +38,13 @@ from homeassistant.core import ( valid_entity_id, ) from homeassistant.exceptions import MaxLengthExceeded -from homeassistant.helpers import device_registry as dr, storage -from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED -from homeassistant.helpers.frame import report from homeassistant.loader import bind_hass from homeassistant.util import slugify, uuid as uuid_util from homeassistant.util.yaml import load_yaml +from . import device_registry as dr, storage +from .device_registry import EVENT_DEVICE_REGISTRY_UPDATED +from .frame import report from .typing import UNDEFINED, UndefinedType if TYPE_CHECKING: diff --git a/homeassistant/helpers/entityfilter.py b/homeassistant/helpers/entityfilter.py index 522f789b163..3d119404ffd 100644 --- a/homeassistant/helpers/entityfilter.py +++ b/homeassistant/helpers/entityfilter.py @@ -9,7 +9,8 @@ import voluptuous as vol from homeassistant.const import CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE from homeassistant.core import split_entity_id -from homeassistant.helpers import config_validation as cv + +from . import config_validation as cv CONF_INCLUDE_DOMAINS = "include_domains" CONF_INCLUDE_ENTITY_GLOBS = "include_entity_globs" diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 1404b3730f1..289d3321a41 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -33,15 +33,16 @@ from homeassistant.core import ( split_entity_id, ) from homeassistant.exceptions import TemplateError -from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED -from homeassistant.helpers.ratelimit import KeyedRateLimit -from homeassistant.helpers.sun import get_astral_event_next -from homeassistant.helpers.template import RenderInfo, Template, result_as_boolean -from homeassistant.helpers.typing import TemplateVarsType from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util from homeassistant.util.async_ import run_callback_threadsafe +from .entity_registry import EVENT_ENTITY_REGISTRY_UPDATED +from .ratelimit import KeyedRateLimit +from .sun import get_astral_event_next +from .template import RenderInfo, Template, result_as_boolean +from .typing import TemplateVarsType + TRACK_STATE_CHANGE_CALLBACKS = "track_state_change_callbacks" TRACK_STATE_CHANGE_LISTENER = "track_state_change_listener" diff --git a/homeassistant/helpers/httpx_client.py b/homeassistant/helpers/httpx_client.py index ec5b0a5e7ca..d1dc11aae4d 100644 --- a/homeassistant/helpers/httpx_client.py +++ b/homeassistant/helpers/httpx_client.py @@ -9,9 +9,10 @@ import httpx from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE, __version__ from homeassistant.core import Event, HomeAssistant, callback -from homeassistant.helpers.frame import warn_use from homeassistant.loader import bind_hass +from .frame import warn_use + DATA_ASYNC_CLIENT = "httpx_async_client" DATA_ASYNC_CLIENT_NOVERIFY = "httpx_async_client_noverify" SERVER_SOFTWARE = "HomeAssistant/{0} httpx/{1} Python/{2[0]}.{2[1]}".format( diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index d3494c3f41b..0c111fd9afa 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -11,9 +11,10 @@ import voluptuous as vol from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES from homeassistant.core import Context, HomeAssistant, State, T, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_validation as cv from homeassistant.loader import bind_hass +from . import config_validation as cv + _LOGGER = logging.getLogger(__name__) _SlotsType = Dict[str, Any] diff --git a/homeassistant/helpers/location.py b/homeassistant/helpers/location.py index e9d3f34b970..a3f2dfb4c6f 100644 --- a/homeassistant/helpers/location.py +++ b/homeassistant/helpers/location.py @@ -86,7 +86,7 @@ def find_coordinates( # Check if state is valid coordinate set try: # Import here, not at top-level to avoid circular import - import homeassistant.helpers.config_validation as cv # pylint: disable=import-outside-toplevel + from . import config_validation as cv # pylint: disable=import-outside-toplevel cv.gps(entity_state.state.split(",")) except vol.Invalid: diff --git a/homeassistant/helpers/reload.py b/homeassistant/helpers/reload.py index 8f9ce48a59b..baa31bb41fc 100644 --- a/homeassistant/helpers/reload.py +++ b/homeassistant/helpers/reload.py @@ -10,12 +10,13 @@ from homeassistant import config as conf_util from homeassistant.const import SERVICE_RELOAD from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_per_platform -from homeassistant.helpers.entity_platform import EntityPlatform, async_get_platforms -from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_integration from homeassistant.setup import async_setup_component +from . import config_per_platform +from .entity_platform import EntityPlatform, async_get_platforms +from .typing import ConfigType + # mypy: disallow-any-generics _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index f1e74e26908..56b5b106278 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -9,14 +9,15 @@ from typing import Any, cast from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, State, callback, valid_entity_id from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import entity_registry, start -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.json import JSONEncoder -from homeassistant.helpers.singleton import singleton -from homeassistant.helpers.storage import Store import homeassistant.util.dt as dt_util +from . import entity_registry, start +from .entity import Entity +from .event import async_track_time_interval +from .json import JSONEncoder +from .singleton import singleton +from .storage import Store + DATA_RESTORE_STATE_TASK = "restore_state_task" _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 1c199525d4d..77e4fd38508 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -56,29 +56,18 @@ from homeassistant.core import ( HomeAssistant, callback, ) -from homeassistant.helpers import condition, config_validation as cv, service, template -from homeassistant.helpers.condition import ( - ConditionCheckerType, - trace_condition_function, -) -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) -from homeassistant.helpers.event import async_call_later, async_track_template -from homeassistant.helpers.script_variables import ScriptVariables -from homeassistant.helpers.trace import script_execution_set -from homeassistant.helpers.trigger import ( - async_initialize_triggers, - async_validate_trigger_config, -) -from homeassistant.helpers.typing import ConfigType from homeassistant.util import slugify from homeassistant.util.dt import utcnow +from . import condition, config_validation as cv, service, template +from .condition import ConditionCheckerType, trace_condition_function +from .dispatcher import async_dispatcher_connect, async_dispatcher_send +from .event import async_call_later, async_track_template +from .script_variables import ScriptVariables from .trace import ( TraceElement, async_trace_path, + script_execution_set, trace_append_element, trace_id_get, trace_path, @@ -90,6 +79,8 @@ from .trace import ( trace_stack_top, trace_update_result, ) +from .trigger import async_initialize_triggers, async_validate_trigger_config +from .typing import ConfigType # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 00369d43536..13ec3cb5df5 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -31,14 +31,6 @@ from homeassistant.exceptions import ( Unauthorized, UnknownUser, ) -from homeassistant.helpers import ( - area_registry, - config_validation as cv, - device_registry, - entity_registry, - template, -) -from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.loader import ( MAX_LOAD_CONCURRENTLY, Integration, @@ -49,9 +41,18 @@ from homeassistant.util.async_ import gather_with_concurrency from homeassistant.util.yaml import load_yaml from homeassistant.util.yaml.loader import JSON_TYPE +from . import ( + area_registry, + config_validation as cv, + device_registry, + entity_registry, + template, +) +from .typing import ConfigType, TemplateVarsType + if TYPE_CHECKING: - from homeassistant.helpers.entity import Entity - from homeassistant.helpers.entity_platform import EntityPlatform + from .entity import Entity + from .entity_platform import EntityPlatform CONF_SERVICE_ENTITY_ID = "entity_id" diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index da9aa06ba4a..af0c50ec5fa 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -197,7 +197,7 @@ class Store: def async_delay_save(self, data_func: Callable[[], dict], delay: float = 0) -> None: """Save data with an optional delay.""" # pylint: disable-next=import-outside-toplevel - from homeassistant.helpers.event import async_call_later + from .event import async_call_later self._data = { "version": self.version, diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 6373f63c5a0..ce7984c03bb 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -45,13 +45,6 @@ from homeassistant.core import ( valid_entity_id, ) from homeassistant.exceptions import TemplateError -from homeassistant.helpers import ( - area_registry, - device_registry, - entity_registry, - location as loc_helper, -) -from homeassistant.helpers.typing import TemplateVarsType from homeassistant.loader import bind_hass from homeassistant.util import ( convert, @@ -62,6 +55,9 @@ from homeassistant.util import ( from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.thread import ThreadWithException +from . import area_registry, device_registry, entity_registry, location as loc_helper +from .typing import TemplateVarsType + # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -882,9 +878,7 @@ def result_as_boolean(template_result: Any | None) -> bool: try: # Import here, not at top-level to avoid circular import - from homeassistant.helpers import ( # pylint: disable=import-outside-toplevel - config_validation as cv, - ) + from . import config_validation as cv # pylint: disable=import-outside-toplevel return cv.boolean(template_result) except vol.Invalid: @@ -952,7 +946,7 @@ def integration_entities(hass: HomeAssistant, entry_name: str) -> Iterable[str]: # fallback to just returning all entities for a domain # pylint: disable=import-outside-toplevel - from homeassistant.helpers.entity import entity_sources + from .entity import entity_sources return [ entity_id @@ -1014,9 +1008,7 @@ def area_id(hass: HomeAssistant, lookup_value: str) -> str | None: ent_reg = entity_registry.async_get(hass) dev_reg = device_registry.async_get(hass) # Import here, not at top-level to avoid circular import - from homeassistant.helpers import ( # pylint: disable=import-outside-toplevel - config_validation as cv, - ) + from . import config_validation as cv # pylint: disable=import-outside-toplevel try: cv.entity_id(lookup_value) @@ -1054,9 +1046,7 @@ def area_name(hass: HomeAssistant, lookup_value: str) -> str | None: dev_reg = device_registry.async_get(hass) ent_reg = entity_registry.async_get(hass) # Import here, not at top-level to avoid circular import - from homeassistant.helpers import ( # pylint: disable=import-outside-toplevel - config_validation as cv, - ) + from . import config_validation as cv # pylint: disable=import-outside-toplevel try: cv.entity_id(lookup_value) diff --git a/homeassistant/helpers/trace.py b/homeassistant/helpers/trace.py index 4332f34107c..bc3e7ff3565 100644 --- a/homeassistant/helpers/trace.py +++ b/homeassistant/helpers/trace.py @@ -8,9 +8,10 @@ from contextvars import ContextVar from functools import wraps from typing import Any, cast -from homeassistant.helpers.typing import TemplateVarsType import homeassistant.util.dt as dt_util +from .typing import TemplateVarsType + class TraceElement: """Container for trace data.""" diff --git a/homeassistant/helpers/trigger.py b/homeassistant/helpers/trigger.py index c7ef6d31be4..175a29fcc5d 100644 --- a/homeassistant/helpers/trigger.py +++ b/homeassistant/helpers/trigger.py @@ -11,9 +11,10 @@ import voluptuous as vol from homeassistant.const import CONF_ID, CONF_PLATFORM from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.loader import IntegrationNotFound, async_get_integration +from .typing import ConfigType, TemplateVarsType + _PLATFORM_ALIASES = { "device_automation": ("device",), "homeassistant": ("event", "numeric_state", "state", "time_pattern", "time"), diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index a48fca8a01f..d9ab337e84e 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -15,9 +15,9 @@ import requests from homeassistant import config_entries from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import entity, event from homeassistant.util.dt import utcnow +from . import entity, event from .debounce import Debouncer REQUEST_REFRESH_DEFAULT_COOLDOWN = 10 diff --git a/homeassistant/loader.py b/homeassistant/loader.py index af3f90ab356..6fd66dab2fa 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -23,16 +23,16 @@ from awesomeversion import ( AwesomeVersionStrategy, ) -from homeassistant.generated.dhcp import DHCP -from homeassistant.generated.mqtt import MQTT -from homeassistant.generated.ssdp import SSDP -from homeassistant.generated.usb import USB -from homeassistant.generated.zeroconf import HOMEKIT, ZEROCONF -from homeassistant.util.async_ import gather_with_concurrency +from .generated.dhcp import DHCP +from .generated.mqtt import MQTT +from .generated.ssdp import SSDP +from .generated.usb import USB +from .generated.zeroconf import HOMEKIT, ZEROCONF +from .util.async_ import gather_with_concurrency # Typing imports that create a circular dependency if TYPE_CHECKING: - from homeassistant.core import HomeAssistant + from .core import HomeAssistant # mypy: disallow-any-generics @@ -167,7 +167,7 @@ async def async_get_custom_components( async def async_get_config_flows(hass: HomeAssistant) -> set[str]: """Return cached list of config flows.""" # pylint: disable=import-outside-toplevel - from homeassistant.generated.config_flows import FLOWS + from .generated.config_flows import FLOWS flows: set[str] = set() flows.update(FLOWS) @@ -607,7 +607,7 @@ async def _async_get_integration(hass: HomeAssistant, domain: str) -> Integratio if integration := (await async_get_custom_components(hass)).get(domain): return integration - from homeassistant import components # pylint: disable=import-outside-toplevel + from . import components # pylint: disable=import-outside-toplevel if integration := await hass.async_add_executor_job( Integration.resolve_from_root, hass, components, domain diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index e98ba2afe68..6b9c0bd0661 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -7,11 +7,11 @@ import logging import os from typing import Any, cast -from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.typing import UNDEFINED, UndefinedType -from homeassistant.loader import Integration, IntegrationNotFound, async_get_integration -import homeassistant.util.package as pkg_util +from .core import HomeAssistant, callback +from .exceptions import HomeAssistantError +from .helpers.typing import UNDEFINED, UndefinedType +from .loader import Integration, IntegrationNotFound, async_get_integration +from .util import package as pkg_util # mypy: disallow-any-generics diff --git a/homeassistant/runner.py b/homeassistant/runner.py index dcf39485531..7caf6be2c8f 100644 --- a/homeassistant/runner.py +++ b/homeassistant/runner.py @@ -8,11 +8,11 @@ import threading import traceback from typing import Any -from homeassistant import bootstrap -from homeassistant.core import callback -from homeassistant.helpers.frame import warn_use -from homeassistant.util.executor import InterruptibleThreadPoolExecutor -from homeassistant.util.thread import deadlock_safe_shutdown +from . import bootstrap +from .core import callback +from .helpers.frame import warn_use +from .util.executor import InterruptibleThreadPoolExecutor +from .util.thread import deadlock_safe_shutdown # mypy: disallow-any-generics diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 3e6db28a194..44dc6fe1014 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -8,18 +8,18 @@ import logging.handlers from timeit import default_timer as timer from types import ModuleType -from homeassistant import config as conf_util, core, loader, requirements -from homeassistant.config import async_notify_setup_error -from homeassistant.const import ( +from . import config as conf_util, core, loader, requirements +from .config import async_notify_setup_error +from .const import ( EVENT_COMPONENT_LOADED, EVENT_HOMEASSISTANT_START, PLATFORM_FORMAT, Platform, ) -from homeassistant.core import CALLBACK_TYPE -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.typing import ConfigType -from homeassistant.util import dt as dt_util, ensure_unique_string +from .core import CALLBACK_TYPE +from .exceptions import HomeAssistantError +from .helpers.typing import ConfigType +from .util import dt as dt_util, ensure_unique_string # mypy: disallow-any-generics diff --git a/homeassistant/util/executor.py b/homeassistant/util/executor.py index 9277e396bc4..8451e71e0ec 100644 --- a/homeassistant/util/executor.py +++ b/homeassistant/util/executor.py @@ -10,7 +10,7 @@ from threading import Thread import time import traceback -from homeassistant.util.thread import async_raise +from .thread import async_raise _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py index bdb637a9149..824e324095d 100644 --- a/homeassistant/util/unit_system.py +++ b/homeassistant/util/unit_system.py @@ -31,7 +31,8 @@ from homeassistant.const import ( VOLUME_LITERS, WIND_SPEED, ) -from homeassistant.util import ( + +from . import ( distance as distance_util, pressure as pressure_util, speed as speed_util, diff --git a/pylint/plugins/hass_imports.py b/pylint/plugins/hass_imports.py index 341abff202a..beb7f87616f 100644 --- a/pylint/plugins/hass_imports.py +++ b/pylint/plugins/hass_imports.py @@ -25,24 +25,28 @@ class HassImportsFormatChecker(BaseChecker): # type: ignore[misc] def __init__(self, linter: PyLinter | None = None) -> None: super().__init__(linter) - self.current_module: str | None = None + self.current_package: str | None = None def visit_module(self, node: Module) -> None: - """Called when a Import node is visited.""" - self.current_module = node.name + """Called when a Module node is visited.""" + if node.package: + self.current_package = node.name + else: + # Strip name of the current module + self.current_package = node.name[: node.name.rfind(".")] def visit_import(self, node: Import) -> None: """Called when a Import node is visited.""" for module, _alias in node.names: - if module.startswith(f"{self.current_module}."): + if module.startswith(f"{self.current_package}."): self.add_message("hass-relative-import", node=node) def visit_importfrom(self, node: ImportFrom) -> None: """Called when a ImportFrom node is visited.""" if node.level is not None: return - if node.modname == self.current_module or node.modname.startswith( - f"{self.current_module}." + if node.modname == self.current_package or node.modname.startswith( + f"{self.current_package}." ): self.add_message("hass-relative-import", node=node) From 0ec2978698666167ae92f1a88767ced4160316ce Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 23 Dec 2021 21:02:24 +0100 Subject: [PATCH 1003/2644] Add DeviceInfo to Sensibo (#62668) --- homeassistant/components/sensibo/climate.py | 11 +++++++++++ homeassistant/components/sensibo/const.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index 60ea483865f..ac907352735 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -36,6 +36,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import ( AddEntitiesCallback, ConfigType, @@ -153,6 +154,16 @@ class SensiboClimate(ClimateEntity): self._available = False self._do_update(data) self._failed_update = False + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._id)}, + name=self._name, + manufacturer="Sensibo", + configuration_url="https://home.sensibo.com/", + model=data["productModel"], + sw_version=data["firmwareVersion"], + hw_version=data["firmwareType"], + suggested_area=self._name, + ) @property def supported_features(self): diff --git a/homeassistant/components/sensibo/const.py b/homeassistant/components/sensibo/const.py index 45d53df2d80..7bb8d07b7e8 100644 --- a/homeassistant/components/sensibo/const.py +++ b/homeassistant/components/sensibo/const.py @@ -15,4 +15,4 @@ _FETCH_FIELDS = ",".join( "temperatureUnit", ] ) -_INITIAL_FETCH_FIELDS = f"id,{_FETCH_FIELDS}" +_INITIAL_FETCH_FIELDS = f"id,firmwareVersion,firmwareType,productModel,{_FETCH_FIELDS}" From 13e3ca6ab1ac5dabd1ad98bda8224f90063a55b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 23 Dec 2021 21:04:58 +0100 Subject: [PATCH 1004/2644] Add config flow to version integration (#54642) --- homeassistant/components/version/__init__.py | 47 +++- .../components/version/config_flow.py | 202 +++++++++++++++ homeassistant/components/version/const.py | 128 ++++++++++ .../components/version/coordinator.py | 54 ++++ .../components/version/manifest.json | 3 +- homeassistant/components/version/sensor.py | 223 +++++++---------- homeassistant/components/version/strings.json | 26 ++ .../components/version/translations/en.json | 26 ++ homeassistant/generated/config_flows.py | 1 + tests/components/version/common.py | 73 ++++++ tests/components/version/test_config_flow.py | 236 ++++++++++++++++++ tests/components/version/test_sensor.py | 234 ++++++++--------- 12 files changed, 1010 insertions(+), 243 deletions(-) create mode 100644 homeassistant/components/version/config_flow.py create mode 100644 homeassistant/components/version/const.py create mode 100644 homeassistant/components/version/coordinator.py create mode 100644 homeassistant/components/version/strings.json create mode 100644 homeassistant/components/version/translations/en.json create mode 100644 tests/components/version/common.py create mode 100644 tests/components/version/test_config_flow.py diff --git a/homeassistant/components/version/__init__.py b/homeassistant/components/version/__init__.py index 64b04fd7d71..75545adb8db 100644 --- a/homeassistant/components/version/__init__.py +++ b/homeassistant/components/version/__init__.py @@ -1 +1,46 @@ -"""The version integration.""" +"""The Version integration.""" +from __future__ import annotations + +from pyhaversion import HaVersion + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import ( + BOARD_MAP, + CONF_BOARD, + CONF_CHANNEL, + CONF_IMAGE, + CONF_SOURCE, + DOMAIN, + PLATFORMS, +) +from .coordinator import VersionDataUpdateCoordinator + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up the version integration from a config entry.""" + coordinator = VersionDataUpdateCoordinator( + hass=hass, + api=HaVersion( + session=async_get_clientsession(hass), + source=entry.data[CONF_SOURCE], + image=entry.data[CONF_IMAGE], + board=BOARD_MAP[entry.data[CONF_BOARD]], + channel=entry.data[CONF_CHANNEL].lower(), + ), + ) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload the config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok diff --git a/homeassistant/components/version/config_flow.py b/homeassistant/components/version/config_flow.py new file mode 100644 index 00000000000..5a501b4a97d --- /dev/null +++ b/homeassistant/components/version/config_flow.py @@ -0,0 +1,202 @@ +"""Config flow for Version integration.""" +from __future__ import annotations + +from typing import Any + +from pyhaversion.consts import HaVersionChannel, HaVersionSource +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_NAME, CONF_SOURCE +from homeassistant.data_entry_flow import FlowResult +from homeassistant.util import slugify + +from .const import ( + ATTR_VERSION_SOURCE, + CONF_BETA, + CONF_BOARD, + CONF_CHANNEL, + CONF_IMAGE, + CONF_VERSION_SOURCE, + DEFAULT_BOARD, + DEFAULT_CHANNEL, + DEFAULT_CONFIGURATION, + DEFAULT_IMAGE, + DEFAULT_NAME, + DEFAULT_NAME_CURRENT, + DEFAULT_NAME_LATEST, + DEFAULT_SOURCE, + DOMAIN, + POSTFIX_CONTAINER_NAME, + SOURCE_DOKCER, + SOURCE_HASSIO, + STEP_USER, + STEP_VERSION_SOURCE, + VALID_BOARDS, + VALID_CHANNELS, + VALID_CONTAINER_IMAGES, + VALID_IMAGES, + VERSION_SOURCE_DOCKER_HUB, + VERSION_SOURCE_LOCAL, + VERSION_SOURCE_MAP, + VERSION_SOURCE_MAP_INVERTED, + VERSION_SOURCE_VERSIONS, +) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Version.""" + + _entry_data: dict[str, Any] = DEFAULT_CONFIGURATION.copy() + + VERSION = 1 + + async def async_step_user( + self, + user_input: dict[str, Any] | None = None, + ) -> FlowResult: + """Handle the initial user step.""" + if user_input is None: + self._entry_data = DEFAULT_CONFIGURATION.copy() + return self.async_show_form( + step_id=STEP_USER, + data_schema=vol.Schema( + { + vol.Required( + CONF_VERSION_SOURCE, + default=VERSION_SOURCE_LOCAL, + ): vol.In(VERSION_SOURCE_MAP.keys()) + } + ), + ) + + user_input[CONF_SOURCE] = VERSION_SOURCE_MAP[user_input[CONF_VERSION_SOURCE]] + self._entry_data.update(user_input) + + if not self.show_advanced_options or user_input[CONF_SOURCE] in ( + HaVersionSource.LOCAL, + HaVersionSource.HAIO, + ): + return self.async_create_entry( + title=self._config_entry_name, + data=self._entry_data, + ) + + return await self.async_step_version_source() + + async def async_step_version_source( + self, + user_input: dict[str, Any] | None = None, + ) -> FlowResult: + """Handle the version_source step.""" + if user_input is None: + if self._entry_data[CONF_SOURCE] in ( + HaVersionSource.SUPERVISOR, + HaVersionSource.CONTAINER, + ): + data_schema = vol.Schema( + { + vol.Required( + CONF_CHANNEL, default=DEFAULT_CHANNEL.title() + ): vol.In(VALID_CHANNELS), + } + ) + if self._entry_data[CONF_SOURCE] == HaVersionSource.SUPERVISOR: + data_schema = data_schema.extend( + { + vol.Required(CONF_IMAGE, default=DEFAULT_IMAGE): vol.In( + VALID_IMAGES + ), + vol.Required(CONF_BOARD, default=DEFAULT_BOARD): vol.In( + VALID_BOARDS + ), + } + ) + else: + data_schema = data_schema.extend( + { + vol.Required(CONF_IMAGE, default=DEFAULT_IMAGE): vol.In( + VALID_CONTAINER_IMAGES + ) + } + ) + else: + data_schema = vol.Schema({vol.Required(CONF_BETA, default=False): bool}) + + return self.async_show_form( + step_id=STEP_VERSION_SOURCE, + data_schema=data_schema, + description_placeholders={ + ATTR_VERSION_SOURCE: self._entry_data[CONF_VERSION_SOURCE] + }, + ) + self._entry_data.update(user_input) + self._entry_data[CONF_CHANNEL] = self._entry_data[CONF_CHANNEL].lower() + + return self.async_create_entry( + title=self._config_entry_name, data=self._entry_data + ) + + async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: + """Import a config entry from configuration.yaml.""" + self._entry_data = _convert_imported_configuration(import_config) + + for entry in self._async_current_entries(): + if _fingerprint(entry.data) == _fingerprint(self._entry_data): + return self.async_abort(reason="already_configured") + + return self.async_create_entry( + title=self._config_entry_name, data=self._entry_data + ) + + @property + def _config_entry_name(self) -> str: + """Return the name of the config entry.""" + if self._entry_data[CONF_SOURCE] == HaVersionSource.LOCAL: + return DEFAULT_NAME_CURRENT + + name = self._entry_data[CONF_VERSION_SOURCE] + + if (channel := self._entry_data[CONF_CHANNEL]) != DEFAULT_CHANNEL: + return f"{name} {channel.title()}" + + return name + + +def _fingerprint(data) -> str: + """Return a fingerprint of the configuration.""" + configuration = {**DEFAULT_CONFIGURATION, **data} + return slugify("_".join(configuration.values())) + + +def _convert_imported_configuration(config: dict[str, Any]) -> Any: + """Convert a key from the imported configuration.""" + data = DEFAULT_CONFIGURATION.copy() + if config.get(CONF_BETA): + data[CONF_CHANNEL] = HaVersionChannel.BETA + + if (source := config.get(CONF_SOURCE)) and source != DEFAULT_SOURCE: + if source == SOURCE_HASSIO: + data[CONF_SOURCE] = HaVersionSource.SUPERVISOR + data[CONF_VERSION_SOURCE] = VERSION_SOURCE_VERSIONS + elif source == SOURCE_DOKCER: + data[CONF_SOURCE] = HaVersionSource.CONTAINER + data[CONF_VERSION_SOURCE] = VERSION_SOURCE_DOCKER_HUB + else: + data[CONF_SOURCE] = source + data[CONF_VERSION_SOURCE] = VERSION_SOURCE_MAP_INVERTED[source] + + if (image := config.get(CONF_IMAGE)) and image != DEFAULT_IMAGE: + if data[CONF_SOURCE] == HaVersionSource.CONTAINER: + data[CONF_IMAGE] = f"{config[CONF_IMAGE]}{POSTFIX_CONTAINER_NAME}" + else: + data[CONF_IMAGE] = config[CONF_IMAGE] + + if (name := config.get(CONF_NAME)) and name != DEFAULT_NAME: + data[CONF_NAME] = config[CONF_NAME] + else: + if data[CONF_SOURCE] == HaVersionSource.LOCAL: + data[CONF_NAME] = DEFAULT_NAME_CURRENT + else: + data[CONF_NAME] = DEFAULT_NAME_LATEST + return data diff --git a/homeassistant/components/version/const.py b/homeassistant/components/version/const.py new file mode 100644 index 00000000000..8575b17a703 --- /dev/null +++ b/homeassistant/components/version/const.py @@ -0,0 +1,128 @@ +"""Constants for the Version integration.""" +from __future__ import annotations + +from datetime import timedelta +from logging import Logger, getLogger +from typing import Any, Final + +from pyhaversion.consts import HaVersionChannel, HaVersionSource + +from homeassistant.const import CONF_NAME, Platform + +DOMAIN: Final = "version" +LOGGER: Final[Logger] = getLogger(__package__) +PLATFORMS: Final[list[Platform]] = [Platform.SENSOR] +UPDATE_COORDINATOR_UPDATE_INTERVAL: Final[timedelta] = timedelta(minutes=5) + +ENTRY_TYPE_SERVICE: Final = "service" +HOME_ASSISTANT: Final = "Home Assistant" +POSTFIX_CONTAINER_NAME: Final = "-homeassistant" + + +CONF_BETA: Final = "beta" +CONF_BOARD: Final = "board" +CONF_CHANNEL: Final = "channel" +CONF_IMAGE: Final = "image" +CONF_VERSION_SOURCE: Final = "version_source" +CONF_SOURCE: Final = "source" + +ATTR_CHANNEL: Final = CONF_CHANNEL +ATTR_VERSION_SOURCE: Final = CONF_VERSION_SOURCE +ATTR_SOURCE: Final = CONF_SOURCE + +SOURCE_DOKCER: Final = "docker" # Kept to not break existing configurations +SOURCE_HASSIO: Final = "hassio" # Kept to not break existing configurations + +VERSION_SOURCE_DOCKER_HUB: Final = "Docker Hub" +VERSION_SOURCE_HAIO: Final = "Home Assistant Website" +VERSION_SOURCE_LOCAL: Final = "Local installation" +VERSION_SOURCE_PYPI: Final = "Python Package Index (PyPI)" +VERSION_SOURCE_VERSIONS: Final = "Home Assistant Versions" + +DEFAULT_BETA: Final = False +DEFAULT_BOARD: Final = "OVA" +DEFAULT_CHANNEL: Final[HaVersionChannel] = HaVersionChannel.STABLE +DEFAULT_IMAGE: Final = "default" +DEFAULT_NAME_CURRENT: Final = "Current Version" +DEFAULT_NAME_LATEST: Final = "Latest Version" +DEFAULT_NAME: Final = "" +DEFAULT_SOURCE: Final[HaVersionSource] = HaVersionSource.LOCAL +DEFAULT_CONFIGURATION: Final[dict[str, Any]] = { + CONF_NAME: DEFAULT_NAME, + CONF_CHANNEL: DEFAULT_CHANNEL, + CONF_IMAGE: DEFAULT_IMAGE, + CONF_BOARD: DEFAULT_BOARD, + CONF_VERSION_SOURCE: VERSION_SOURCE_LOCAL, + CONF_SOURCE: DEFAULT_SOURCE, +} + +STEP_VERSION_SOURCE: Final = "version_source" +STEP_USER: Final = "user" + +HA_VERSION_SOURCES: Final[list[str]] = [source.value for source in HaVersionSource] + +BOARD_MAP: Final[dict[str, str]] = { + "OVA": "ova", + "RaspberryPi": "rpi", + "RaspberryPi Zero-W": "rpi0-w", + "RaspberryPi 2": "rpi2", + "RaspberryPi 3": "rpi3", + "RaspberryPi 3 64bit": "rpi3-64", + "RaspberryPi 4": "rpi4", + "RaspberryPi 4 64bit": "rpi4-64", + "ASUS Tinkerboard": "tinker", + "ODROID C2": "odroid-c2", + "ODROID C4": "odroid-c4", + "ODROID N2": "odroid-n2", + "ODROID XU4": "odroid-xu4", + "Generic x86-64": "generic-x86-64", + "Intel NUC": "intel-nuc", +} + +VALID_BOARDS: Final[list[str]] = list(BOARD_MAP) + +VERSION_SOURCE_MAP: Final[dict[str, HaVersionSource]] = { + VERSION_SOURCE_LOCAL: HaVersionSource.LOCAL, + VERSION_SOURCE_VERSIONS: HaVersionSource.SUPERVISOR, + VERSION_SOURCE_HAIO: HaVersionSource.HAIO, + VERSION_SOURCE_DOCKER_HUB: HaVersionSource.CONTAINER, + VERSION_SOURCE_PYPI: HaVersionSource.PYPI, +} + +VERSION_SOURCE_MAP_INVERTED: Final[dict[HaVersionSource, str]] = { + value: key for key, value in VERSION_SOURCE_MAP.items() +} + + +VALID_SOURCES: Final[list[str]] = HA_VERSION_SOURCES + [ + SOURCE_HASSIO, # Kept to not break existing configurations + SOURCE_DOKCER, # Kept to not break existing configurations +] + +VALID_IMAGES: Final = [ + "default", + "generic-x86-64", + "intel-nuc", + "odroid-c2", + "odroid-n2", + "odroid-xu", + "qemuarm-64", + "qemuarm", + "qemux86-64", + "qemux86", + "raspberrypi", + "raspberrypi2", + "raspberrypi3-64", + "raspberrypi3", + "raspberrypi4-64", + "raspberrypi4", + "tinker", +] + +VALID_CONTAINER_IMAGES: Final[list[str]] = [ + f"{image}{POSTFIX_CONTAINER_NAME}" if image != DEFAULT_IMAGE else image + for image in VALID_IMAGES +] +VALID_CHANNELS: Final[list[str]] = [ + str(channel.value).title() for channel in HaVersionChannel +] diff --git a/homeassistant/components/version/coordinator.py b/homeassistant/components/version/coordinator.py new file mode 100644 index 00000000000..d99fb531da2 --- /dev/null +++ b/homeassistant/components/version/coordinator.py @@ -0,0 +1,54 @@ +"""Data update coordinator for Version entities.""" +from __future__ import annotations + +from typing import Any + +from awesomeversion import AwesomeVersion +from pyhaversion import HaVersion +from pyhaversion.exceptions import HaVersionException + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN, LOGGER, UPDATE_COORDINATOR_UPDATE_INTERVAL + + +class VersionDataUpdateCoordinator(DataUpdateCoordinator): + """Data update coordinator for Version entities.""" + + config_entry: ConfigEntry + + def __init__( + self, + hass: HomeAssistant, + api: HaVersion, + ) -> None: + """Initialize the coordinator.""" + super().__init__( + hass=hass, + logger=LOGGER, + name=DOMAIN, + update_method=self._async_update_version_data, + update_interval=UPDATE_COORDINATOR_UPDATE_INTERVAL, + ) + self._api = api + self._version: AwesomeVersion | None = None + self._version_data: dict[str, Any] | None = None + + @property + def version(self) -> str | None: + """Return the latest version.""" + return str(self._version) if self._version else None + + @property + def version_data(self) -> dict[str, Any]: + """Return the version data.""" + return self._version_data or {} + + async def _async_update_version_data(self) -> None: + """Update version data.""" + try: + self._version, self._version_data = await self._api.get_version() + except HaVersionException as exception: + raise UpdateFailed(exception) from exception diff --git a/homeassistant/components/version/manifest.json b/homeassistant/components/version/manifest.json index f5dec053399..5a4cd70f4c7 100644 --- a/homeassistant/components/version/manifest.json +++ b/homeassistant/components/version/manifest.json @@ -10,5 +10,6 @@ "@ludeeus" ], "quality_scale": "internal", - "iot_class": "local_push" + "iot_class": "local_push", + "config_flow": true } \ No newline at end of file diff --git a/homeassistant/components/version/sensor.py b/homeassistant/components/version/sensor.py index 63e8421ed0e..1cd1aecb053 100644 --- a/homeassistant/components/version/sensor.py +++ b/homeassistant/components/version/sensor.py @@ -1,156 +1,127 @@ """Sensor that can display the current Home Assistant versions.""" -from datetime import timedelta -import logging +from __future__ import annotations + +from typing import Any, Final -from pyhaversion import ( - HaVersion, - HaVersionChannel, - HaVersionSource, - exceptions as pyhaversionexceptions, -) import voluptuous as vol +from voluptuous.schema_builder import Schema from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorEntity, SensorEntityDescription, ) -from homeassistant.const import CONF_NAME, CONF_SOURCE -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType +from homeassistant.helpers.update_coordinator import CoordinatorEntity -ALL_IMAGES = [ - "default", - "generic-x86-64", - "intel-nuc", - "odroid-c2", - "odroid-n2", - "odroid-xu", - "qemuarm-64", - "qemuarm", - "qemux86-64", - "qemux86", - "raspberrypi", - "raspberrypi2", - "raspberrypi3-64", - "raspberrypi3", - "raspberrypi4-64", - "raspberrypi4", - "tinker", -] +from .const import ( + ATTR_SOURCE, + CONF_BETA, + CONF_IMAGE, + CONF_SOURCE, + DEFAULT_BETA, + DEFAULT_IMAGE, + DEFAULT_NAME, + DEFAULT_SOURCE, + DOMAIN, + HOME_ASSISTANT, + LOGGER, + VALID_IMAGES, + VALID_SOURCES, +) +from .coordinator import VersionDataUpdateCoordinator -HA_VERSION_SOURCES = [source.value for source in HaVersionSource] - -ALL_SOURCES = HA_VERSION_SOURCES + [ - "hassio", # Kept to not break existing configurations - "docker", # Kept to not break existing configurations -] - -CONF_BETA = "beta" -CONF_IMAGE = "image" - -DEFAULT_IMAGE = "default" -DEFAULT_NAME_LATEST = "Latest Version" -DEFAULT_NAME_LOCAL = "Current Version" -DEFAULT_SOURCE = "local" - -TIME_BETWEEN_UPDATES = timedelta(minutes=5) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA: Final[Schema] = SENSOR_PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_BETA, default=False): cv.boolean, - vol.Optional(CONF_IMAGE, default=DEFAULT_IMAGE): vol.In(ALL_IMAGES), - vol.Optional(CONF_NAME, default=""): cv.string, - vol.Optional(CONF_SOURCE, default=DEFAULT_SOURCE): vol.In(ALL_SOURCES), + vol.Optional(CONF_BETA, default=DEFAULT_BETA): cv.boolean, + vol.Optional(CONF_IMAGE, default=DEFAULT_IMAGE): vol.In(VALID_IMAGES), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_SOURCE, default=DEFAULT_SOURCE): vol.In(VALID_SOURCES), } ) -_LOGGER: logging.Logger = logging.getLogger(__name__) - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Version sensor platform.""" - - beta = config.get(CONF_BETA) - image = config.get(CONF_IMAGE) - name = config.get(CONF_NAME) - source = config.get(CONF_SOURCE) - - channel = HaVersionChannel.BETA if beta else HaVersionChannel.STABLE - session = async_get_clientsession(hass) - - if source in HA_VERSION_SOURCES: - source = HaVersionSource(source) - elif source == "hassio": - source = HaVersionSource.SUPERVISOR - elif source == "docker": - source = HaVersionSource.CONTAINER - - if ( - source == HaVersionSource.CONTAINER - and image is not None - and image != DEFAULT_IMAGE - ): - image = f"{image}-homeassistant" - - if not (name := config.get(CONF_NAME)): - if source == HaVersionSource.LOCAL: - name = DEFAULT_NAME_LOCAL - else: - name = DEFAULT_NAME_LATEST - - async_add_entities( - [ - VersionSensor( - VersionData( - HaVersion( - session=session, source=source, image=image, channel=channel - ) - ), - SensorEntityDescription(key=source, name=name), - ) - ], - True, +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the legacy version sensor platform.""" + LOGGER.warning( + "Configuration of the Version platform in YAML is deprecated and will be " + "removed in Home Assistant 2022.4; Your existing configuration " + "has been imported into the UI automatically and can be safely removed " + "from your configuration.yaml file" + ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={ATTR_SOURCE: SOURCE_IMPORT}, data=config + ) ) -class VersionData: - """Get the latest data and update the states.""" +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up version sensors.""" + coordinator: VersionDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + if (entity_name := entry.data[CONF_NAME]) == DEFAULT_NAME: + entity_name = entry.title - def __init__(self, api: HaVersion) -> None: - """Initialize the data object.""" - self.api = api + version_sensor_entities: list[VersionSensorEntity] = [ + VersionSensorEntity( + coordinator=coordinator, + entity_description=SensorEntityDescription( + key=str(entry.data[CONF_SOURCE]), + name=entity_name, + ), + ) + ] - @Throttle(TIME_BETWEEN_UPDATES) - async def async_update(self): - """Get the latest version information.""" - try: - await self.api.get_version() - except pyhaversionexceptions.HaVersionFetchException as exception: - _LOGGER.warning(exception) - except pyhaversionexceptions.HaVersionParseException as exception: - _LOGGER.warning( - "Could not parse data received for %s - %s", self.api.source, exception - ) + async_add_entities(version_sensor_entities) -class VersionSensor(SensorEntity): - """Representation of a Home Assistant version sensor.""" +class VersionSensorEntity(CoordinatorEntity, SensorEntity): + """Version sensor entity class.""" _attr_icon = "mdi:package-up" + _attr_device_info = DeviceInfo( + name=f"{HOME_ASSISTANT} {DOMAIN.title()}", + identifiers={(HOME_ASSISTANT, DOMAIN)}, + manufacturer=HOME_ASSISTANT, + entry_type=DeviceEntryType.SERVICE, + ) + + coordinator: VersionDataUpdateCoordinator def __init__( self, - data: VersionData, - description: SensorEntityDescription, + coordinator: VersionDataUpdateCoordinator, + entity_description: SensorEntityDescription, ) -> None: - """Initialize the Version sensor.""" - self.data = data - self.entity_description = description + """Initialize version sensor entities.""" + super().__init__(coordinator) + self.entity_description = entity_description + self._attr_unique_id = ( + f"{coordinator.config_entry.unique_id}_{entity_description.key}" + ) - async def async_update(self): - """Get the latest version information.""" - await self.data.async_update() - self._attr_native_value = self.data.api.version - self._attr_extra_state_attributes = self.data.api.version_data + @property + def native_value(self) -> StateType: + """Return the native value of this sensor.""" + return self.coordinator.version + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return extra state attributes of this sensor.""" + return self.coordinator.version_data diff --git a/homeassistant/components/version/strings.json b/homeassistant/components/version/strings.json new file mode 100644 index 00000000000..b147422b32a --- /dev/null +++ b/homeassistant/components/version/strings.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, + "step": { + "user": { + "title": "Select installation type", + "description": "Select the source you want to track versions from", + "data": { + "version_source": "Version source" + } + }, + "version_source": { + "title": "Configure", + "description": "Configure {version_source} version tracking", + "data": { + "beta": "Include beta versions", + "board": "Which board should be tracked", + "channel": "Which channel should be tracked", + "image": "Which image should be tracked" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/version/translations/en.json b/homeassistant/components/version/translations/en.json new file mode 100644 index 00000000000..fc443a1e9c9 --- /dev/null +++ b/homeassistant/components/version/translations/en.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "step": { + "user": { + "data": { + "version_source": "Version source" + }, + "description": "Select the source you want to track versions from", + "title": "Select installation type" + }, + "version_source": { + "data": { + "beta": "Include beta versions", + "board": "Which board should be tracked", + "channel": "Which channel should be tracked", + "image": "Which image should be tracked" + }, + "description": "Configure {version_source} version tracking", + "title": "Configure" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 928ac3dad67..fa828cc6368 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -331,6 +331,7 @@ FLOWS = [ "venstar", "vera", "verisure", + "version", "vesync", "vicare", "vilfo", diff --git a/tests/components/version/common.py b/tests/components/version/common.py new file mode 100644 index 00000000000..489d1d435bf --- /dev/null +++ b/tests/components/version/common.py @@ -0,0 +1,73 @@ +"""Fixtures for version integration.""" +from __future__ import annotations + +from typing import Any, Final +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.version.const import ( + DEFAULT_CONFIGURATION, + DEFAULT_NAME_CURRENT, + DOMAIN, + UPDATE_COORDINATOR_UPDATE_INTERVAL, + VERSION_SOURCE_LOCAL, +) +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component +from homeassistant.util import dt + +from tests.common import MockConfigEntry, async_fire_time_changed + +MOCK_VERSION: Final = "1970.1.0" +MOCK_VERSION_DATA: Final = {"source": "local", "channel": "stable"} + + +MOCK_VERSION_CONFIG_ENTRY_DATA: Final[dict[str, Any]] = { + "domain": DOMAIN, + "title": VERSION_SOURCE_LOCAL, + "data": DEFAULT_CONFIGURATION, + "source": config_entries.SOURCE_USER, +} + +TEST_DEFAULT_IMPORT_CONFIG: Final = { + **DEFAULT_CONFIGURATION, + CONF_NAME: DEFAULT_NAME_CURRENT, +} + + +async def mock_get_version_update( + hass: HomeAssistant, + version: str = MOCK_VERSION, + data: dict[str, Any] = MOCK_VERSION_DATA, + side_effect: Exception = None, +) -> None: + """Mock getting version.""" + with patch( + "pyhaversion.HaVersion.get_version", + return_value=(version, data), + side_effect=side_effect, + ): + + async_fire_time_changed(hass, dt.utcnow() + UPDATE_COORDINATOR_UPDATE_INTERVAL) + await hass.async_block_till_done() + + +async def setup_version_integration(hass: HomeAssistant) -> MockConfigEntry: + """Set up the Version integration.""" + await async_setup_component(hass, "persistent_notification", {}) + mock_entry = MockConfigEntry(**MOCK_VERSION_CONFIG_ENTRY_DATA) + mock_entry.add_to_hass(hass) + + with patch( + "pyhaversion.HaVersion.get_version", + return_value=(MOCK_VERSION, MOCK_VERSION_DATA), + ): + + assert await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get("sensor.local_installation").state == MOCK_VERSION + assert mock_entry.state == config_entries.ConfigEntryState.LOADED + + return mock_entry diff --git a/tests/components/version/test_config_flow.py b/tests/components/version/test_config_flow.py new file mode 100644 index 00000000000..f45ff1764f2 --- /dev/null +++ b/tests/components/version/test_config_flow.py @@ -0,0 +1,236 @@ +"""Test the Version config flow.""" +from unittest.mock import patch + +from pyhaversion.consts import HaVersionChannel, HaVersionSource + +from homeassistant import config_entries, setup +from homeassistant.components.version.const import ( + CONF_BETA, + CONF_BOARD, + CONF_CHANNEL, + CONF_IMAGE, + CONF_VERSION_SOURCE, + DEFAULT_CONFIGURATION, + DOMAIN, + UPDATE_COORDINATOR_UPDATE_INTERVAL, + VERSION_SOURCE_DOCKER_HUB, + VERSION_SOURCE_PYPI, + VERSION_SOURCE_VERSIONS, +) +from homeassistant.const import CONF_SOURCE +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) +from homeassistant.util import dt + +from tests.common import async_fire_time_changed +from tests.components.version.common import ( + MOCK_VERSION, + MOCK_VERSION_DATA, + setup_version_integration, +) + + +async def test_reload(hass: HomeAssistant): + """Test the Version sensor with different sources.""" + config_entry = await setup_version_integration(hass) + + with patch( + "pyhaversion.HaVersion.get_version", + return_value=(MOCK_VERSION, MOCK_VERSION_DATA), + ): + assert await hass.config_entries.async_reload(config_entry.entry_id) + async_fire_time_changed(hass, dt.utcnow() + UPDATE_COORDINATOR_UPDATE_INTERVAL) + await hass.async_block_till_done() + + entry = hass.config_entries.async_get_entry(config_entry.entry_id) + assert entry.state == config_entries.ConfigEntryState.LOADED + assert hass.states.get("sensor.local_installation").state == MOCK_VERSION + + +async def test_basic_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER, "show_advanced_options": False}, + ) + assert result["type"] == RESULT_TYPE_FORM + + with patch( + "homeassistant.components.version.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_VERSION_SOURCE: VERSION_SOURCE_DOCKER_HUB}, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == VERSION_SOURCE_DOCKER_HUB + assert result2["data"] == { + **DEFAULT_CONFIGURATION, + CONF_SOURCE: HaVersionSource.CONTAINER, + CONF_VERSION_SOURCE: VERSION_SOURCE_DOCKER_HUB, + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_advanced_form_pypi(hass: HomeAssistant) -> None: + """Show advanced form when pypi is selected.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, + ) + assert result["type"] == RESULT_TYPE_FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_VERSION_SOURCE: VERSION_SOURCE_PYPI}, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "version_source" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "version_source" + + with patch( + "homeassistant.components.version.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_BETA: True} + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == VERSION_SOURCE_PYPI + assert result["data"] == { + **DEFAULT_CONFIGURATION, + CONF_BETA: True, + CONF_SOURCE: HaVersionSource.PYPI, + CONF_VERSION_SOURCE: VERSION_SOURCE_PYPI, + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_advanced_form_container(hass: HomeAssistant) -> None: + """Show advanced form when container source is selected.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, + ) + assert result["type"] == RESULT_TYPE_FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_VERSION_SOURCE: VERSION_SOURCE_DOCKER_HUB}, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "version_source" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "version_source" + + with patch( + "homeassistant.components.version.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_IMAGE: "odroid-n2-homeassistant"} + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == VERSION_SOURCE_DOCKER_HUB + assert result["data"] == { + **DEFAULT_CONFIGURATION, + CONF_IMAGE: "odroid-n2-homeassistant", + CONF_SOURCE: HaVersionSource.CONTAINER, + CONF_VERSION_SOURCE: VERSION_SOURCE_DOCKER_HUB, + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_advanced_form_supervisor(hass: HomeAssistant) -> None: + """Show advanced form when docker source is selected.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, + ) + assert result["type"] == RESULT_TYPE_FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_VERSION_SOURCE: VERSION_SOURCE_VERSIONS}, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "version_source" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "version_source" + + with patch( + "homeassistant.components.version.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_CHANNEL: "Dev", CONF_IMAGE: "odroid-n2", CONF_BOARD: "ODROID N2"}, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == f"{VERSION_SOURCE_VERSIONS} Dev" + assert result["data"] == { + **DEFAULT_CONFIGURATION, + CONF_IMAGE: "odroid-n2", + CONF_BOARD: "ODROID N2", + CONF_CHANNEL: HaVersionChannel.DEV, + CONF_SOURCE: HaVersionSource.SUPERVISOR, + CONF_VERSION_SOURCE: VERSION_SOURCE_VERSIONS, + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_existing(hass: HomeAssistant) -> None: + """Test importing existing configuration.""" + with patch( + "homeassistant.components.version.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={}, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={}, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/version/test_sensor.py b/tests/components/version/test_sensor.py index cd56223a1e6..6a37fd58b3a 100644 --- a/tests/components/version/test_sensor.py +++ b/tests/components/version/test_sensor.py @@ -1,130 +1,134 @@ """The test for the version sensor platform.""" -from datetime import timedelta +from __future__ import annotations + +from typing import Any from unittest.mock import patch -from pyhaversion import HaVersionSource, exceptions as pyhaversionexceptions +from pyhaversion import HaVersionChannel, HaVersionSource +from pyhaversion.exceptions import HaVersionException import pytest -from homeassistant.components.version.sensor import HA_VERSION_SOURCES +from homeassistant.components.version.const import ( + CONF_BETA, + CONF_CHANNEL, + CONF_IMAGE, + CONF_VERSION_SOURCE, + DEFAULT_NAME_LATEST, + DOMAIN, + VERSION_SOURCE_DOCKER_HUB, + VERSION_SOURCE_VERSIONS, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_NAME, CONF_SOURCE +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from homeassistant.util import dt -from tests.common import async_fire_time_changed +from .common import ( + MOCK_VERSION, + MOCK_VERSION_DATA, + TEST_DEFAULT_IMPORT_CONFIG, + mock_get_version_update, + setup_version_integration, +) -MOCK_VERSION = "10.0" + +async def async_setup_sensor_wrapper( + hass: HomeAssistant, config: dict[str, Any] +) -> ConfigEntry: + """Set up the Version sensor platform.""" + await async_setup_component(hass, "persistent_notification", {}) + with patch( + "pyhaversion.HaVersion.get_version", + return_value=(MOCK_VERSION, MOCK_VERSION_DATA), + ): + assert await async_setup_component( + hass, "sensor", {"sensor": {"platform": DOMAIN, **config}} + ) + await hass.async_block_till_done() + + config_entries = hass.config_entries.async_entries(DOMAIN) + print(config_entries) + config_entry = config_entries[-1] + assert config_entry.source == "import" + return config_entry + + +async def test_version_sensor(hass: HomeAssistant): + """Test the Version sensor with different sources.""" + await setup_version_integration(hass) + + state = hass.states.get("sensor.local_installation") + assert state.state == MOCK_VERSION + assert state.attributes["source"] == "local" + assert state.attributes["channel"] == "stable" + + +async def test_update(hass: HomeAssistant, caplog: pytest.LogCaptureFixture): + """Test updates.""" + await setup_version_integration(hass) + assert hass.states.get("sensor.local_installation").state == MOCK_VERSION + + await mock_get_version_update(hass, version="1970.1.1") + assert hass.states.get("sensor.local_installation").state == "1970.1.1" + + assert "Error fetching version data" not in caplog.text + await mock_get_version_update(hass, side_effect=HaVersionException) + assert hass.states.get("sensor.local_installation").state == "unavailable" + assert "Error fetching version data" in caplog.text @pytest.mark.parametrize( - "source,target_source,name", + "yaml,converted", ( ( - ("local", HaVersionSource.LOCAL, "current_version"), - ("docker", HaVersionSource.CONTAINER, "latest_version"), - ("hassio", HaVersionSource.SUPERVISOR, "latest_version"), - ) - + tuple( - (source, HaVersionSource(source), "latest_version") - for source in HA_VERSION_SOURCES - if source != HaVersionSource.LOCAL - ) + {}, + TEST_DEFAULT_IMPORT_CONFIG, + ), + ( + {CONF_NAME: "test"}, + {**TEST_DEFAULT_IMPORT_CONFIG, CONF_NAME: "test"}, + ), + ( + {CONF_SOURCE: "hassio", CONF_IMAGE: "odroid-n2"}, + { + **TEST_DEFAULT_IMPORT_CONFIG, + CONF_NAME: DEFAULT_NAME_LATEST, + CONF_SOURCE: HaVersionSource.SUPERVISOR, + CONF_VERSION_SOURCE: VERSION_SOURCE_VERSIONS, + CONF_IMAGE: "odroid-n2", + }, + ), + ( + {CONF_SOURCE: "docker"}, + { + **TEST_DEFAULT_IMPORT_CONFIG, + CONF_NAME: DEFAULT_NAME_LATEST, + CONF_SOURCE: HaVersionSource.CONTAINER, + CONF_VERSION_SOURCE: VERSION_SOURCE_DOCKER_HUB, + }, + ), + ( + {CONF_BETA: True}, + { + **TEST_DEFAULT_IMPORT_CONFIG, + CONF_CHANNEL: HaVersionChannel.BETA, + }, + ), + ( + {CONF_SOURCE: "container", CONF_IMAGE: "odroid-n2"}, + { + **TEST_DEFAULT_IMPORT_CONFIG, + CONF_NAME: DEFAULT_NAME_LATEST, + CONF_SOURCE: HaVersionSource.CONTAINER, + CONF_VERSION_SOURCE: VERSION_SOURCE_DOCKER_HUB, + CONF_IMAGE: "odroid-n2-homeassistant", + }, + ), ), ) -async def test_version_source(hass, source, target_source, name): - """Test the Version sensor with different sources.""" - config = { - "sensor": {"platform": "version", "source": source, "image": "qemux86-64"} - } - - with patch("homeassistant.components.version.sensor.HaVersion.get_version"), patch( - "homeassistant.components.version.sensor.HaVersion.version", MOCK_VERSION - ): - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() - - state = hass.states.get(f"sensor.{name}") - assert state - assert state.attributes["source"] == target_source - - assert state.state == MOCK_VERSION - - -async def test_version_fetch_exception(hass, caplog): - """Test fetch exception thrown during updates.""" - config = {"sensor": {"platform": "version"}} - with patch( - "homeassistant.components.version.sensor.HaVersion.get_version", - side_effect=pyhaversionexceptions.HaVersionFetchException( - "Fetch exception from pyhaversion" - ), - ): - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() - assert "Fetch exception from pyhaversion" in caplog.text - - -async def test_version_parse_exception(hass, caplog): - """Test parse exception thrown during updates.""" - config = {"sensor": {"platform": "version"}} - with patch( - "homeassistant.components.version.sensor.HaVersion.get_version", - side_effect=pyhaversionexceptions.HaVersionParseException, - ): - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() - assert "Could not parse data received for HaVersionSource.LOCAL" in caplog.text - - -async def test_update(hass): - """Test updates.""" - config = {"sensor": {"platform": "version"}} - - with patch("homeassistant.components.version.sensor.HaVersion.get_version"), patch( - "homeassistant.components.version.sensor.HaVersion.version", MOCK_VERSION - ): - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() - - state = hass.states.get("sensor.current_version") - assert state - assert state.state == MOCK_VERSION - - with patch("homeassistant.components.version.sensor.HaVersion.get_version"), patch( - "homeassistant.components.version.sensor.HaVersion.version", "1234" - ): - - async_fire_time_changed(hass, dt.utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() - - state = hass.states.get("sensor.current_version") - assert state - assert state.state == "1234" - - -async def test_image_name_container(hass): - """Test the Version sensor with image name for container.""" - config = { - "sensor": {"platform": "version", "source": "docker", "image": "qemux86-64"} - } - - with patch("homeassistant.components.version.sensor.HaVersion") as haversion: - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() - - constructor = haversion.call_args[1] - assert constructor["source"] == "container" - assert constructor["image"] == "qemux86-64-homeassistant" - - -async def test_image_name_supervisor(hass): - """Test the Version sensor with image name for supervisor.""" - config = { - "sensor": {"platform": "version", "source": "hassio", "image": "qemux86-64"} - } - - with patch("homeassistant.components.version.sensor.HaVersion") as haversion: - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() - - constructor = haversion.call_args[1] - assert constructor["source"] == "supervisor" - assert constructor["image"] == "qemux86-64" +async def test_config_import( + hass: HomeAssistant, yaml: dict[str, Any], converted: dict[str, Any] +) -> None: + """Test importing YAML configuration.""" + config_entry = await async_setup_sensor_wrapper(hass, yaml) + assert config_entry.data == converted From 20e6b50003fdca163d998df6875bf39529d0d1d3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Dec 2021 10:08:38 -1000 Subject: [PATCH 1005/2644] Bump yalexs to 1.1.16 (#62700) --- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index f89be2915fb..201c9ce89aa 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,7 +2,7 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": ["yalexs==1.1.15"], + "requirements": ["yalexs==1.1.16"], "codeowners": ["@bdraco"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 542d16ddce6..80d1d5d7fde 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2482,7 +2482,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.4 # homeassistant.components.august -yalexs==1.1.15 +yalexs==1.1.16 # homeassistant.components.yeelight yeelight==0.7.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 96667fd1309..d3948e7f2c7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1483,7 +1483,7 @@ xmltodict==0.12.0 yalesmartalarmclient==0.3.4 # homeassistant.components.august -yalexs==1.1.15 +yalexs==1.1.16 # homeassistant.components.yeelight yeelight==0.7.8 From caa2157b5b958618e4525feba113d27027cbb28d Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Thu, 23 Dec 2021 22:07:56 +0000 Subject: [PATCH 1006/2644] Use new enums in rdw tests (#62707) --- tests/components/rdw/test_binary_sensor.py | 4 ++-- tests/components/rdw/test_sensor.py | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/components/rdw/test_binary_sensor.py b/tests/components/rdw/test_binary_sensor.py index b3c4d5a9b3c..abf15d869ce 100644 --- a/tests/components/rdw/test_binary_sensor.py +++ b/tests/components/rdw/test_binary_sensor.py @@ -1,5 +1,5 @@ """Tests for the sensors provided by the RDW integration.""" -from homeassistant.components.binary_sensor import DEVICE_CLASS_PROBLEM +from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.rdw.const import DOMAIN from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, ATTR_ICON from homeassistant.core import HomeAssistant @@ -33,7 +33,7 @@ async def test_vehicle_binary_sensors( assert entry.unique_id == "11ZKZ3_pending_recall" assert state.state == "off" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Pending Recall" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PROBLEM + assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.PROBLEM assert ATTR_ICON not in state.attributes assert entry.device_id diff --git a/tests/components/rdw/test_sensor.py b/tests/components/rdw/test_sensor.py index 5eeea579194..32d4c368d73 100644 --- a/tests/components/rdw/test_sensor.py +++ b/tests/components/rdw/test_sensor.py @@ -1,12 +1,11 @@ """Tests for the sensors provided by the RDW integration.""" from homeassistant.components.rdw.const import DOMAIN -from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorDeviceClass from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, - DEVICE_CLASS_DATE, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -29,7 +28,7 @@ async def test_vehicle_sensors( assert entry.unique_id == "11ZKZ3_apk_expiration" assert state.state == "2022-01-04" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "APK Expiration" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_DATE + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATE assert ATTR_ICON not in state.attributes assert ATTR_STATE_CLASS not in state.attributes assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes @@ -41,7 +40,7 @@ async def test_vehicle_sensors( assert entry.unique_id == "11ZKZ3_ascription_date" assert state.state == "2021-11-04" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Ascription Date" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_DATE + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATE assert ATTR_ICON not in state.attributes assert ATTR_STATE_CLASS not in state.attributes assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes From 79ef4dea988b6eda4a914a48a96d27ee9753ee8f Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Thu, 23 Dec 2021 22:14:10 +0000 Subject: [PATCH 1007/2644] Use new enums in smartthings tests (#62708) * Use new enums in smartthings tests * Convert == to is --- tests/components/smartthings/test_binary_sensor.py | 9 +++------ tests/components/smartthings/test_sensor.py | 4 ++-- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/tests/components/smartthings/test_binary_sensor.py b/tests/components/smartthings/test_binary_sensor.py index 7f4748bc215..b636cffbc2e 100644 --- a/tests/components/smartthings/test_binary_sensor.py +++ b/tests/components/smartthings/test_binary_sensor.py @@ -13,13 +13,10 @@ from homeassistant.components.binary_sensor import ( from homeassistant.components.smartthings import binary_sensor from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import ( - ATTR_FRIENDLY_NAME, - ENTITY_CATEGORY_DIAGNOSTIC, - STATE_UNAVAILABLE, -) +from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_UNAVAILABLE from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity import EntityCategory from .conftest import setup_platform @@ -125,4 +122,4 @@ async def test_entity_category(hass, device_factory): entry = entity_registry.async_get("binary_sensor.tamper_sensor_2_tamper") assert entry - assert entry.entity_category == ENTITY_CATEGORY_DIAGNOSTIC + assert entry.entity_category is EntityCategory.DIAGNOSTIC diff --git a/tests/components/smartthings/test_sensor.py b/tests/components/smartthings/test_sensor.py index 0e22a1facba..98464af24af 100644 --- a/tests/components/smartthings/test_sensor.py +++ b/tests/components/smartthings/test_sensor.py @@ -17,13 +17,13 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, - ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, STATE_UNAVAILABLE, STATE_UNKNOWN, ) from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity import EntityCategory from .conftest import setup_platform @@ -95,7 +95,7 @@ async def test_entity_and_device_attributes(hass, device_factory): entry = entity_registry.async_get("sensor.sensor_1_battery") assert entry assert entry.unique_id == f"{device.device_id}.{Attribute.battery}" - assert entry.entity_category == ENTITY_CATEGORY_DIAGNOSTIC + assert entry.entity_category is EntityCategory.DIAGNOSTIC entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) assert entry assert entry.configuration_url == "https://account.smartthings.com" From bbebf311b15d944145e1321dfc33acc6fd669c0e Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Thu, 23 Dec 2021 23:43:30 +0100 Subject: [PATCH 1008/2644] Review AndroidTV option flow strings (#62601) Co-authored-by: Jeff Irion --- homeassistant/components/androidtv/strings.json | 10 +++++----- .../components/androidtv/translations/en.json | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/androidtv/strings.json b/homeassistant/components/androidtv/strings.json index c6840f622c7..1fd0231379f 100644 --- a/homeassistant/components/androidtv/strings.json +++ b/homeassistant/components/androidtv/strings.json @@ -32,12 +32,12 @@ "title": "Android TV Options", "data": { "apps": "Configure applications list", - "get_sources": "Whether or not to retrieve the running apps as the list of sources", - "exclude_unnamed_apps": "Exclude app with unknown name", - "screencap": "Determines if album art should be pulled from what is shown on screen", + "get_sources": "Retrieve the running apps as the list of sources", + "exclude_unnamed_apps": "Exclude apps with unknown name from the sources list", + "screencap": "Use screen capture for album art", "state_detection_rules": "Configure state detection rules", - "turn_off_command": "ADB shell command to override default turn_off command", - "turn_on_command": "ADB shell command to override default turn_on command" + "turn_off_command": "ADB shell turn off command (leave empty for default)", + "turn_on_command": "ADB shell turn on command (leave empty for default)" } }, "apps": { diff --git a/homeassistant/components/androidtv/translations/en.json b/homeassistant/components/androidtv/translations/en.json index 2b7b86e8bfe..f3232b4b229 100644 --- a/homeassistant/components/androidtv/translations/en.json +++ b/homeassistant/components/androidtv/translations/en.json @@ -43,12 +43,12 @@ "init": { "data": { "apps": "Configure applications list", - "exclude_unnamed_apps": "Exclude app with unknown name", - "get_sources": "Whether or not to retrieve the running apps as the list of sources", - "screencap": "Determines if album art should be pulled from what is shown on screen", + "exclude_unnamed_apps": "Exclude apps with unknown name from the sources list", + "get_sources": "Retrieve the running apps as the list of sources", + "screencap": "Use screen capture for album art", "state_detection_rules": "Configure state detection rules", - "turn_off_command": "ADB shell command to override default turn_off command", - "turn_on_command": "ADB shell command to override default turn_on command" + "turn_off_command": "ADB shell turn off command (leave empty for default)", + "turn_on_command": "ADB shell turn on command (leave empty for default)" }, "title": "Android TV Options" }, From 759481688bce80ebb4bfb5c3ba0185e9f5c0cfb6 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Thu, 23 Dec 2021 22:44:09 +0000 Subject: [PATCH 1009/2644] Use new enums in huisbaasje tests (#62714) --- tests/components/huisbaasje/test_sensor.py | 61 ++++++++++++++-------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/tests/components/huisbaasje/test_sensor.py b/tests/components/huisbaasje/test_sensor.py index 171c6bd25c0..4418127a0d1 100644 --- a/tests/components/huisbaasje/test_sensor.py +++ b/tests/components/huisbaasje/test_sensor.py @@ -3,7 +3,11 @@ from unittest.mock import patch from homeassistant.components import huisbaasje from homeassistant.components.huisbaasje.const import FLOW_CUBIC_METERS_PER_HOUR -from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorStateClass +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + SensorDeviceClass, + SensorStateClass, +) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ICON, @@ -11,9 +15,6 @@ from homeassistant.const import ( CONF_ID, CONF_PASSWORD, CONF_USERNAME, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_GAS, - DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR, POWER_WATT, VOLUME_CUBIC_METERS, @@ -58,7 +59,9 @@ async def test_setup_entry(hass: HomeAssistant): # Assert data is loaded current_power = hass.states.get("sensor.huisbaasje_current_power") assert current_power.state == "1012.0" - assert current_power.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER + assert ( + current_power.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER + ) assert current_power.attributes.get(ATTR_ICON) == "mdi:lightning-bolt" assert ( current_power.attributes.get(ATTR_STATE_CLASS) @@ -68,7 +71,10 @@ async def test_setup_entry(hass: HomeAssistant): current_power_in = hass.states.get("sensor.huisbaasje_current_power_in_peak") assert current_power_in.state == "1012.0" - assert current_power_in.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER + assert ( + current_power_in.attributes.get(ATTR_DEVICE_CLASS) + == SensorDeviceClass.POWER + ) assert current_power_in.attributes.get(ATTR_ICON) == "mdi:lightning-bolt" assert ( current_power_in.attributes.get(ATTR_STATE_CLASS) @@ -81,7 +87,8 @@ async def test_setup_entry(hass: HomeAssistant): ) assert current_power_in_low.state == "unknown" assert ( - current_power_in_low.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER + current_power_in_low.attributes.get(ATTR_DEVICE_CLASS) + == SensorDeviceClass.POWER ) assert current_power_in_low.attributes.get(ATTR_ICON) == "mdi:lightning-bolt" assert ( @@ -94,7 +101,10 @@ async def test_setup_entry(hass: HomeAssistant): current_power_out = hass.states.get("sensor.huisbaasje_current_power_out_peak") assert current_power_out.state == "unknown" - assert current_power_out.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER + assert ( + current_power_out.attributes.get(ATTR_DEVICE_CLASS) + == SensorDeviceClass.POWER + ) assert current_power_out.attributes.get(ATTR_ICON) == "mdi:lightning-bolt" assert ( current_power_out.attributes.get(ATTR_STATE_CLASS) @@ -108,7 +118,7 @@ async def test_setup_entry(hass: HomeAssistant): assert current_power_out_low.state == "unknown" assert ( current_power_out_low.attributes.get(ATTR_DEVICE_CLASS) - == DEVICE_CLASS_POWER + == SensorDeviceClass.POWER ) assert current_power_out_low.attributes.get(ATTR_ICON) == "mdi:lightning-bolt" assert ( @@ -125,7 +135,7 @@ async def test_setup_entry(hass: HomeAssistant): assert energy_consumption_peak_today.state == "2.67" assert ( energy_consumption_peak_today.attributes.get(ATTR_DEVICE_CLASS) - == DEVICE_CLASS_ENERGY + == SensorDeviceClass.ENERGY ) assert ( energy_consumption_peak_today.attributes.get(ATTR_ICON) @@ -146,7 +156,7 @@ async def test_setup_entry(hass: HomeAssistant): assert energy_consumption_off_peak_today.state == "0.627" assert ( energy_consumption_off_peak_today.attributes.get(ATTR_DEVICE_CLASS) - == DEVICE_CLASS_ENERGY + == SensorDeviceClass.ENERGY ) assert ( energy_consumption_off_peak_today.attributes.get(ATTR_ICON) @@ -167,7 +177,7 @@ async def test_setup_entry(hass: HomeAssistant): assert energy_production_peak_today.state == "1.512" assert ( energy_production_peak_today.attributes.get(ATTR_DEVICE_CLASS) - == DEVICE_CLASS_ENERGY + == SensorDeviceClass.ENERGY ) assert ( energy_production_peak_today.attributes.get(ATTR_ICON) @@ -188,7 +198,7 @@ async def test_setup_entry(hass: HomeAssistant): assert energy_production_off_peak_today.state == "1.093" assert ( energy_production_off_peak_today.attributes.get(ATTR_DEVICE_CLASS) - == DEVICE_CLASS_ENERGY + == SensorDeviceClass.ENERGY ) assert ( energy_production_off_peak_today.attributes.get(ATTR_ICON) @@ -205,7 +215,9 @@ async def test_setup_entry(hass: HomeAssistant): energy_today = hass.states.get("sensor.huisbaasje_energy_today") assert energy_today.state == "3.3" - assert energy_today.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY + assert ( + energy_today.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY + ) assert energy_today.attributes.get(ATTR_ICON) == "mdi:lightning-bolt" assert ( energy_today.attributes.get(ATTR_STATE_CLASS) @@ -218,7 +230,10 @@ async def test_setup_entry(hass: HomeAssistant): energy_this_week = hass.states.get("sensor.huisbaasje_energy_this_week") assert energy_this_week.state == "17.5" - assert energy_this_week.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY + assert ( + energy_this_week.attributes.get(ATTR_DEVICE_CLASS) + == SensorDeviceClass.ENERGY + ) assert energy_this_week.attributes.get(ATTR_ICON) == "mdi:lightning-bolt" assert ( energy_this_week.attributes.get(ATTR_STATE_CLASS) @@ -232,7 +247,8 @@ async def test_setup_entry(hass: HomeAssistant): energy_this_month = hass.states.get("sensor.huisbaasje_energy_this_month") assert energy_this_month.state == "103.3" assert ( - energy_this_month.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY + energy_this_month.attributes.get(ATTR_DEVICE_CLASS) + == SensorDeviceClass.ENERGY ) assert energy_this_month.attributes.get(ATTR_ICON) == "mdi:lightning-bolt" assert ( @@ -246,7 +262,10 @@ async def test_setup_entry(hass: HomeAssistant): energy_this_year = hass.states.get("sensor.huisbaasje_energy_this_year") assert energy_this_year.state == "673.0" - assert energy_this_year.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY + assert ( + energy_this_year.attributes.get(ATTR_DEVICE_CLASS) + == SensorDeviceClass.ENERGY + ) assert energy_this_year.attributes.get(ATTR_ICON) == "mdi:lightning-bolt" assert ( energy_this_year.attributes.get(ATTR_STATE_CLASS) @@ -271,7 +290,7 @@ async def test_setup_entry(hass: HomeAssistant): gas_today = hass.states.get("sensor.huisbaasje_gas_today") assert gas_today.state == "1.1" - assert gas_today.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_GAS + assert gas_today.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS assert gas_today.attributes.get(ATTR_ICON) == "mdi:counter" assert ( gas_today.attributes.get(ATTR_STATE_CLASS) @@ -281,7 +300,7 @@ async def test_setup_entry(hass: HomeAssistant): gas_this_week = hass.states.get("sensor.huisbaasje_gas_this_week") assert gas_this_week.state == "5.6" - assert gas_this_week.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_GAS + assert gas_this_week.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS assert gas_this_week.attributes.get(ATTR_ICON) == "mdi:counter" assert ( gas_this_week.attributes.get(ATTR_STATE_CLASS) @@ -294,7 +313,7 @@ async def test_setup_entry(hass: HomeAssistant): gas_this_month = hass.states.get("sensor.huisbaasje_gas_this_month") assert gas_this_month.state == "39.1" - assert gas_this_month.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_GAS + assert gas_this_month.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS assert gas_this_month.attributes.get(ATTR_ICON) == "mdi:counter" assert ( gas_this_month.attributes.get(ATTR_STATE_CLASS) @@ -307,7 +326,7 @@ async def test_setup_entry(hass: HomeAssistant): gas_this_year = hass.states.get("sensor.huisbaasje_gas_this_year") assert gas_this_year.state == "116.7" - assert gas_this_year.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_GAS + assert gas_this_year.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS assert gas_this_year.attributes.get(ATTR_ICON) == "mdi:counter" assert ( gas_this_year.attributes.get(ATTR_STATE_CLASS) From fa6d6d914b0ff027185c85d291856282dccc997d Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Thu, 23 Dec 2021 22:51:33 +0000 Subject: [PATCH 1010/2644] Use new enums in zwave tests (#62711) * Use new enums in zwave tests * Code review: == to is and BinarySensor to Sensor --- tests/components/zwave/test_sensor.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/components/zwave/test_sensor.py b/tests/components/zwave/test_sensor.py index 4f995131d15..83ebcaa3a4a 100644 --- a/tests/components/zwave/test_sensor.py +++ b/tests/components/zwave/test_sensor.py @@ -1,4 +1,5 @@ """Test Z-Wave sensor.""" +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.components.zwave import const, sensor import homeassistant.const @@ -67,7 +68,7 @@ def test_get_device_detects_battery_sensor(mock_openzwave): device = sensor.get_device(node=node, values=values, node_config={}) assert isinstance(device, sensor.ZWaveBatterySensor) - assert device.device_class == homeassistant.const.DEVICE_CLASS_BATTERY + assert device.device_class is SensorDeviceClass.BATTERY def test_multilevelsensor_value_changed_temp_fahrenheit(hass, mock_openzwave): @@ -87,7 +88,7 @@ def test_multilevelsensor_value_changed_temp_fahrenheit(hass, mock_openzwave): device.hass = hass assert device.state == 191.0 assert device.unit_of_measurement == homeassistant.const.TEMP_FAHRENHEIT - assert device.device_class == homeassistant.const.DEVICE_CLASS_TEMPERATURE + assert device.device_class is SensorDeviceClass.TEMPERATURE value.data = 197.95555 value_changed(value) assert device.state == 198.0 @@ -109,7 +110,7 @@ def test_multilevelsensor_value_changed_temp_celsius(hass, mock_openzwave): device.hass = hass assert device.state == 38.9 assert device.unit_of_measurement == homeassistant.const.TEMP_CELSIUS - assert device.device_class == homeassistant.const.DEVICE_CLASS_TEMPERATURE + assert device.device_class is SensorDeviceClass.TEMPERATURE value.data = 37.95555 value_changed(value) assert device.state == 38.0 From 41531b528e6a2f57073ecbb44a342976f80851ff Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Thu, 23 Dec 2021 17:52:42 -0500 Subject: [PATCH 1011/2644] Add identify buttons to ZHA devices (#61495) * Identify buttons * clean up and add test * use Platform * update device list * Only 1 identify button per device * cleanup press until the need arises for the branch * make imports relative --- homeassistant/components/zha/button.py | 106 + homeassistant/components/zha/core/const.py | 1 + .../components/zha/core/discovery.py | 2 + .../components/zha/core/registries.py | 10 + tests/components/zha/test_button.py | 89 + tests/components/zha/test_discover.py | 27 +- tests/components/zha/zha_devices_list.py | 2769 ++++++++++------- 7 files changed, 1858 insertions(+), 1146 deletions(-) create mode 100644 homeassistant/components/zha/button.py create mode 100644 tests/components/zha/test_button.py diff --git a/homeassistant/components/zha/button.py b/homeassistant/components/zha/button.py new file mode 100644 index 00000000000..674adbee0d9 --- /dev/null +++ b/homeassistant/components/zha/button.py @@ -0,0 +1,106 @@ +"""Support for ZHA button.""" +from __future__ import annotations + +import abc +import functools +import logging +from typing import Any + +from homeassistant.components.button import ButtonDeviceClass, ButtonEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .core import discovery +from .core.const import CHANNEL_IDENTIFY, DATA_ZHA, SIGNAL_ADD_ENTITIES +from .core.registries import ZHA_ENTITIES +from .core.typing import ChannelType, ZhaDeviceType +from .entity import ZhaEntity + +MULTI_MATCH = functools.partial(ZHA_ENTITIES.multipass_match, Platform.BUTTON) +DEFAULT_DURATION = 5 # seconds + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Zigbee Home Automation button from config entry.""" + entities_to_create = hass.data[DATA_ZHA][Platform.BUTTON] + + unsub = async_dispatcher_connect( + hass, + SIGNAL_ADD_ENTITIES, + functools.partial( + discovery.async_add_entities, + async_add_entities, + entities_to_create, + update_before_add=False, + ), + ) + config_entry.async_on_unload(unsub) + + +class ZHAButton(ZhaEntity, ButtonEntity): + """Defines a ZHA button.""" + + _command_name: str = None + + def __init__( + self, + unique_id: str, + zha_device: ZhaDeviceType, + channels: list[ChannelType], + **kwargs, + ) -> None: + """Init this button.""" + super().__init__(unique_id, zha_device, channels, **kwargs) + self._channel: ChannelType = channels[0] + + @abc.abstractmethod + def get_args(self) -> list[Any]: + """Return the arguments to use in the command.""" + + async def async_press(self) -> None: + """Send out a update command.""" + command = getattr(self._channel, self._command_name) + arguments = self.get_args() + await command(*arguments) + + +@MULTI_MATCH(channel_names=CHANNEL_IDENTIFY) +class ZHAIdentifyButton(ZHAButton): + """Defines a ZHA identify button.""" + + @classmethod + def create_entity( + cls, + unique_id: str, + zha_device: ZhaDeviceType, + channels: list[ChannelType], + **kwargs, + ) -> ZhaEntity | None: + """Entity Factory. + + Return entity if it is a supported configuration, otherwise return None + """ + platform_restrictions = ZHA_ENTITIES.single_device_matches[Platform.BUTTON] + device_restrictions = platform_restrictions[zha_device.ieee] + if CHANNEL_IDENTIFY in device_restrictions: + return None + device_restrictions.append(CHANNEL_IDENTIFY) + return cls(unique_id, zha_device, channels, **kwargs) + + _attr_device_class: ButtonDeviceClass = ButtonDeviceClass.UPDATE + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _command_name = "identify" + + def get_args(self) -> list[Any]: + """Return the arguments to use in the command.""" + + return [DEFAULT_DURATION] diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 585124c3ee9..67a79d9dea7 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -102,6 +102,7 @@ CLUSTER_TYPE_OUT = "out" PLATFORMS = ( Platform.ALARM_CONTROL_PANEL, Platform.BINARY_SENSOR, + Platform.BUTTON, Platform.CLIMATE, Platform.COVER, Platform.DEVICE_TRACKER, diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 780d7bc384b..dcc932a76a5 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -17,6 +17,7 @@ from . import const as zha_const, registries as zha_regs, typing as zha_typing from .. import ( # noqa: F401 pylint: disable=unused-import, alarm_control_panel, binary_sensor, + button, climate, cover, device_tracker, @@ -66,6 +67,7 @@ class ProbeEndpoint: self.discover_by_device_type(channel_pool) self.discover_multi_entities(channel_pool) self.discover_by_cluster_id(channel_pool) + zha_regs.ZHA_ENTITIES.clean_up() @callback def discover_by_device_type(self, channel_pool: zha_typing.ChannelPoolType) -> None: diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 146b7d43f0f..571a304b546 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -9,6 +9,7 @@ import attr from zigpy import zcl import zigpy.profiles.zha import zigpy.profiles.zll +from zigpy.types.named import EUI64 from homeassistant.const import Platform @@ -228,6 +229,9 @@ class ZHAEntityRegistry: lambda: collections.defaultdict(lambda: collections.defaultdict(list)) ) self._group_registry: dict[str, CALLABLE_T] = {} + self.single_device_matches: dict[ + Platform, dict[EUI64, list[str]] + ] = collections.defaultdict(lambda: collections.defaultdict(list)) def get_entity( self, @@ -342,5 +346,11 @@ class ZHAEntityRegistry: return decorator + def clean_up(self) -> None: + """Clean up post discovery.""" + self.single_device_matches: dict[ + Platform, dict[EUI64, list[str]] + ] = collections.defaultdict(lambda: collections.defaultdict(list)) + ZHA_ENTITIES = ZHAEntityRegistry() diff --git a/tests/components/zha/test_button.py b/tests/components/zha/test_button.py new file mode 100644 index 00000000000..762d2d46e54 --- /dev/null +++ b/tests/components/zha/test_button.py @@ -0,0 +1,89 @@ +"""Test ZHA button.""" +from unittest.mock import patch + +from freezegun import freeze_time +import pytest +from zigpy.const import SIG_EP_PROFILE +import zigpy.profiles.zha as zha +import zigpy.zcl.clusters.general as general +import zigpy.zcl.clusters.security as security +import zigpy.zcl.foundation as zcl_f + +from homeassistant.components.button import DOMAIN, ButtonDeviceClass +from homeassistant.components.button.const import SERVICE_PRESS +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ENTITY_CATEGORY_DIAGNOSTIC, + STATE_UNKNOWN, +) +from homeassistant.helpers import entity_registry as er + +from .common import find_entity_id +from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_TYPE + +from tests.common import mock_coro + + +@pytest.fixture +async def contact_sensor(hass, zigpy_device_mock, zha_device_joined_restored): + """Contact sensor fixture.""" + + zigpy_device = zigpy_device_mock( + { + 1: { + SIG_EP_INPUT: [ + general.Basic.cluster_id, + general.Identify.cluster_id, + security.IasZone.cluster_id, + ], + SIG_EP_OUTPUT: [], + SIG_EP_TYPE: zha.DeviceType.IAS_ZONE, + SIG_EP_PROFILE: zha.PROFILE_ID, + } + }, + ) + + zha_device = await zha_device_joined_restored(zigpy_device) + return zha_device, zigpy_device.endpoints[1].identify + + +@freeze_time("2021-11-04 17:37:00", tz_offset=-1) +async def test_button(hass, contact_sensor): + """Test zha button platform.""" + + entity_registry = er.async_get(hass) + zha_device, cluster = contact_sensor + assert cluster is not None + entity_id = await find_entity_id(DOMAIN, zha_device, hass) + assert entity_id is not None + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_UNKNOWN + assert state.attributes[ATTR_DEVICE_CLASS] == ButtonDeviceClass.UPDATE + + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.entity_category == ENTITY_CATEGORY_DIAGNOSTIC + + with patch( + "zigpy.zcl.Cluster.request", + return_value=mock_coro([0x00, zcl_f.Status.SUCCESS]), + ): + await hass.services.async_call( + DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + assert len(cluster.request.mock_calls) == 1 + assert cluster.request.call_args[0][0] is False + assert cluster.request.call_args[0][1] == 0 + assert cluster.request.call_args[0][3] == 5 # duration in seconds + + state = hass.states.get(entity_id) + assert state + assert state.state == "2021-11-04T16:37:00+00:00" + assert state.attributes[ATTR_DEVICE_CLASS] == ButtonDeviceClass.UPDATE diff --git a/tests/components/zha/test_discover.py b/tests/components/zha/test_discover.py index 9480f4b1e65..887c390aced 100644 --- a/tests/components/zha/test_discover.py +++ b/tests/components/zha/test_discover.py @@ -125,17 +125,25 @@ async def test_devices( ch.id for pool in zha_dev.channels.pools for ch in pool.client_channels.values() } assert event_channels == set(device[DEV_SIG_EVT_CHANNELS]) - + # we need to probe the class create entity factory so we need to reset this to get accurate results + zha_regs.ZHA_ENTITIES.clean_up() # build a dict of entity_class -> (component, unique_id, channels) tuple ha_ent_info = {} + created_entity_count = 0 for call in _dispatch.call_args_list: _, component, entity_cls, unique_id, channels = call[0] - unique_id_head = UNIQUE_ID_HD.match(unique_id).group(0) # ieee + endpoint_id - ha_ent_info[(unique_id_head, entity_cls.__name__)] = ( - component, - unique_id, - channels, - ) + # the factory can return None. We filter these out to get an accurate created entity count + response = entity_cls.create_entity(unique_id, zha_dev, channels) + if response: + created_entity_count += 1 + unique_id_head = UNIQUE_ID_HD.match(unique_id).group( + 0 + ) # ieee + endpoint_id + ha_ent_info[(unique_id_head, entity_cls.__name__)] = ( + component, + unique_id, + channels, + ) for comp_id, ent_info in device[DEV_SIG_ENT_MAP].items(): component, unique_id = comp_id @@ -156,7 +164,7 @@ async def test_devices( assert unique_id.startswith(ha_unique_id) assert {ch.name for ch in ha_channels} == set(ent_info[DEV_SIG_CHANNELS]) - assert _dispatch.call_count == len(device[DEV_SIG_ENT_MAP]) + assert created_entity_count == len(device[DEV_SIG_ENT_MAP]) entity_ids = hass_disable_services.states.async_entity_ids() await hass_disable_services.async_block_till_done() @@ -298,7 +306,6 @@ async def test_discover_endpoint(device_info, channels_mock, hass): assert device_info[DEV_SIG_EVT_CHANNELS] == sorted( ch.id for pool in channels.pools for ch in pool.client_channels.values() ) - assert new_ent.call_count == len(list(device_info[DEV_SIG_ENT_MAP].values())) # build a dict of entity_class -> (component, unique_id, channels) tuple ha_ent_info = {} @@ -326,8 +333,6 @@ async def test_discover_endpoint(device_info, channels_mock, hass): assert unique_id.startswith(ha_unique_id) assert {ch.name for ch in ha_channels} == set(ent_info[DEV_SIG_CHANNELS]) - assert new_ent.call_count == len(device_info[DEV_SIG_ENT_MAP]) - def _ch_mock(cluster): """Return mock of a channel with a cluster.""" diff --git a/tests/components/zha/zha_devices_list.py b/tests/components/zha/zha_devices_list.py index 54f9a35610b..eb6f05bc5c6 100644 --- a/tests/components/zha/zha_devices_list.py +++ b/tests/components/zha/zha_devices_list.py @@ -24,6 +24,9 @@ DEV_SIG_ZHA_QUIRK = "zha_quirk" DEVICES = [ { DEV_SIG_DEV_NO: 0, + SIG_MANUFACTURER: "ADUROLIGHT", + SIG_MODEL: "Adurolight_NCC", + SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00*d\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 2080, @@ -31,18 +34,25 @@ DEVICES = [ SIG_EP_INPUT: [0, 3, 4096, 64716], SIG_EP_OUTPUT: [3, 4, 6, 8, 4096, 64716], SIG_EP_PROFILE: 260, - } + }, }, - DEV_SIG_ENTITIES: [], - DEV_SIG_ENT_MAP: {}, DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008"], - SIG_MANUFACTURER: "ADUROLIGHT", - SIG_MODEL: "Adurolight_NCC", - SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00*d\x00\x00", - DEV_SIG_ZHA_QUIRK: "AdurolightNCC", + DEV_SIG_ENTITIES: [ + "button.adurolight_adurolight_ncc_77665544_identify", + ], + DEV_SIG_ENT_MAP: { + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.adurolight_adurolight_ncc_77665544_identify", + }, + }, }, { DEV_SIG_DEV_NO: 1, + SIG_MANUFACTURER: "Bosch", + SIG_MODEL: "ISW-ZPR1-WP13", + SIG_NODE_DESC: b"\x02@\x08\x00\x00l\x00\x00\x00\x00\x00\x00\x00", SIG_ENDPOINTS: { 5: { SIG_EP_TYPE: 1026, @@ -50,14 +60,26 @@ DEVICES = [ SIG_EP_INPUT: [0, 1, 3, 32, 1026, 1280, 2821], SIG_EP_OUTPUT: [25], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["5:0x0019"], DEV_SIG_ENTITIES: [ - "binary_sensor.bosch_isw_zpr1_wp13_77665544_ias_zone", + "button.bosch_isw_zpr1_wp13_77665544_identify", "sensor.bosch_isw_zpr1_wp13_77665544_power", "sensor.bosch_isw_zpr1_wp13_77665544_temperature", + "binary_sensor.bosch_isw_zpr1_wp13_77665544_ias_zone", ], DEV_SIG_ENT_MAP: { + ("binary_sensor", "00:11:22:33:44:55:66:77-5-1280"): { + DEV_SIG_CHANNELS: ["ias_zone"], + DEV_SIG_ENT_MAP_CLASS: "IASZone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.bosch_isw_zpr1_wp13_77665544_ias_zone", + }, + ("button", "00:11:22:33:44:55:66:77-5-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.bosch_isw_zpr1_wp13_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-5-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", @@ -68,19 +90,13 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Temperature", DEV_SIG_ENT_MAP_ID: "sensor.bosch_isw_zpr1_wp13_77665544_temperature", }, - ("binary_sensor", "00:11:22:33:44:55:66:77-5-1280"): { - DEV_SIG_CHANNELS: ["ias_zone"], - DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.bosch_isw_zpr1_wp13_77665544_ias_zone", - }, }, - DEV_SIG_EVT_CHANNELS: ["5:0x0019"], - SIG_MANUFACTURER: "Bosch", - SIG_MODEL: "ISW-ZPR1-WP13", - SIG_NODE_DESC: b"\x02@\x08\x00\x00l\x00\x00\x00\x00\x00\x00\x00", }, { DEV_SIG_DEV_NO: 2, + SIG_MANUFACTURER: "CentraLite", + SIG_MODEL: "3130", + SIG_NODE_DESC: b"\x02@\x80N\x10RR\x00\x00\x00R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 1, @@ -88,24 +104,31 @@ DEVICES = [ SIG_EP_INPUT: [0, 1, 3, 32, 2821], SIG_EP_OUTPUT: [3, 6, 8, 25], SIG_EP_PROFILE: 260, - } + }, }, - DEV_SIG_ENTITIES: ["sensor.centralite_3130_77665544_power"], + DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0019"], + DEV_SIG_ENTITIES: [ + "button.centralite_3130_77665544_identify", + "sensor.centralite_3130_77665544_power", + ], DEV_SIG_ENT_MAP: { + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.centralite_3130_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.centralite_3130_77665544_power", - } + }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0019"], - SIG_MANUFACTURER: "CentraLite", - SIG_MODEL: "3130", - SIG_NODE_DESC: b"\x02@\x80N\x10RR\x00\x00\x00R\x00\x00", - DEV_SIG_ZHA_QUIRK: "CentraLite3130", }, { DEV_SIG_DEV_NO: 3, + SIG_MANUFACTURER: "CentraLite", + SIG_MODEL: "3210-L", + SIG_NODE_DESC: b"\x01@\x8eN\x10RR\x00\x00\x00R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 81, @@ -113,12 +136,13 @@ DEVICES = [ SIG_EP_INPUT: [0, 3, 4, 5, 6, 1794, 2820, 2821, 64515], SIG_EP_OUTPUT: [25], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ + "button.centralite_3210_l_77665544_identify", "sensor.centralite_3210_l_77665544_electrical_measurement", "sensor.centralite_3210_l_77665544_electrical_measurement_apparent_power", - "sensor.centralite_3210_l_77665544_electrical_measurement_apparent_power", "sensor.centralite_3210_l_77665544_electrical_measurement_rms_current", "sensor.centralite_3210_l_77665544_electrical_measurement_rms_voltage", "sensor.centralite_3210_l_77665544_smartenergy_metering", @@ -131,15 +155,10 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Switch", DEV_SIG_ENT_MAP_ID: "switch.centralite_3210_l_77665544_on_off", }, - ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { - DEV_SIG_CHANNELS: ["smartenergy_metering"], - DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_smartenergy_metering", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { - DEV_SIG_CHANNELS: ["smartenergy_metering"], - DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_smartenergy_metering_summation_delivered", + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.centralite_3210_l_77665544_identify", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], @@ -161,14 +180,23 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_electrical_measurement_rms_voltage", }, + ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { + DEV_SIG_CHANNELS: ["smartenergy_metering"], + DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_smartenergy_metering", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { + DEV_SIG_CHANNELS: ["smartenergy_metering"], + DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_smartenergy_metering_summation_delivered", + }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0019"], - SIG_MANUFACTURER: "CentraLite", - SIG_MODEL: "3210-L", - SIG_NODE_DESC: b"\x01@\x8eN\x10RR\x00\x00\x00R\x00\x00", }, { DEV_SIG_DEV_NO: 4, + SIG_MANUFACTURER: "CentraLite", + SIG_MODEL: "3310-S", + SIG_NODE_DESC: b"\x02@\x80\xdf\xc2RR\x00\x00\x00R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 770, @@ -176,14 +204,21 @@ DEVICES = [ SIG_EP_INPUT: [0, 1, 3, 32, 1026, 2821, 64581], SIG_EP_OUTPUT: [3, 25], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "sensor.centralite_3310_s_77665544_manufacturer_specific", + "button.centralite_3310_s_77665544_identify", "sensor.centralite_3310_s_77665544_power", "sensor.centralite_3310_s_77665544_temperature", + "sensor.centralite_3310_s_77665544_manufacturer_specific", ], DEV_SIG_ENT_MAP: { + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.centralite_3310_s_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", @@ -200,14 +235,12 @@ DEVICES = [ DEV_SIG_ENT_MAP_ID: "sensor.centralite_3310_s_77665544_manufacturer_specific", }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0019"], - SIG_MANUFACTURER: "CentraLite", - SIG_MODEL: "3310-S", - SIG_NODE_DESC: b"\x02@\x80\xdf\xc2RR\x00\x00\x00R\x00\x00", - DEV_SIG_ZHA_QUIRK: "CentraLite3310S", }, { DEV_SIG_DEV_NO: 5, + SIG_MANUFACTURER: "CentraLite", + SIG_MODEL: "3315-S", + SIG_NODE_DESC: b"\x02@\x80\xdf\xc2RR\x00\x00\x00R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 1026, @@ -224,12 +257,24 @@ DEVICES = [ SIG_EP_PROFILE: 49887, }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "binary_sensor.centralite_3315_s_77665544_ias_zone", + "button.centralite_3315_s_77665544_identify", "sensor.centralite_3315_s_77665544_power", "sensor.centralite_3315_s_77665544_temperature", + "binary_sensor.centralite_3315_s_77665544_ias_zone", ], DEV_SIG_ENT_MAP: { + ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { + DEV_SIG_CHANNELS: ["ias_zone"], + DEV_SIG_ENT_MAP_CLASS: "IASZone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_3315_s_77665544_ias_zone", + }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.centralite_3315_s_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", @@ -240,20 +285,13 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Temperature", DEV_SIG_ENT_MAP_ID: "sensor.centralite_3315_s_77665544_temperature", }, - ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { - DEV_SIG_CHANNELS: ["ias_zone"], - DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_3315_s_77665544_ias_zone", - }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0019"], - SIG_MANUFACTURER: "CentraLite", - SIG_MODEL: "3315-S", - SIG_NODE_DESC: b"\x02@\x80\xdf\xc2RR\x00\x00\x00R\x00\x00", - DEV_SIG_ZHA_QUIRK: "CentraLiteIASSensor", }, { DEV_SIG_DEV_NO: 6, + SIG_MANUFACTURER: "CentraLite", + SIG_MODEL: "3320-L", + SIG_NODE_DESC: b"\x02@\x80\xdf\xc2RR\x00\x00\x00R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 1026, @@ -270,12 +308,24 @@ DEVICES = [ SIG_EP_PROFILE: 49887, }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "binary_sensor.centralite_3320_l_77665544_ias_zone", + "button.centralite_3320_l_77665544_identify", "sensor.centralite_3320_l_77665544_power", "sensor.centralite_3320_l_77665544_temperature", + "binary_sensor.centralite_3320_l_77665544_ias_zone", ], DEV_SIG_ENT_MAP: { + ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { + DEV_SIG_CHANNELS: ["ias_zone"], + DEV_SIG_ENT_MAP_CLASS: "IASZone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_3320_l_77665544_ias_zone", + }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.centralite_3320_l_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", @@ -286,20 +336,13 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Temperature", DEV_SIG_ENT_MAP_ID: "sensor.centralite_3320_l_77665544_temperature", }, - ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { - DEV_SIG_CHANNELS: ["ias_zone"], - DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_3320_l_77665544_ias_zone", - }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0019"], - SIG_MANUFACTURER: "CentraLite", - SIG_MODEL: "3320-L", - SIG_NODE_DESC: b"\x02@\x80\xdf\xc2RR\x00\x00\x00R\x00\x00", - DEV_SIG_ZHA_QUIRK: "CentraLiteIASSensor", }, { DEV_SIG_DEV_NO: 7, + SIG_MANUFACTURER: "CentraLite", + SIG_MODEL: "3326-L", + SIG_NODE_DESC: b"\x02@\x80\xdf\xc2RR\x00\x00\x00R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 1026, @@ -316,12 +359,24 @@ DEVICES = [ SIG_EP_PROFILE: 49887, }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "binary_sensor.centralite_3326_l_77665544_ias_zone", + "button.centralite_3326_l_77665544_identify", "sensor.centralite_3326_l_77665544_power", "sensor.centralite_3326_l_77665544_temperature", + "binary_sensor.centralite_3326_l_77665544_ias_zone", ], DEV_SIG_ENT_MAP: { + ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { + DEV_SIG_CHANNELS: ["ias_zone"], + DEV_SIG_ENT_MAP_CLASS: "IASZone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_3326_l_77665544_ias_zone", + }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.centralite_3326_l_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", @@ -332,20 +387,13 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Temperature", DEV_SIG_ENT_MAP_ID: "sensor.centralite_3326_l_77665544_temperature", }, - ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { - DEV_SIG_CHANNELS: ["ias_zone"], - DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_3326_l_77665544_ias_zone", - }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0019"], - SIG_MANUFACTURER: "CentraLite", - SIG_MODEL: "3326-L", - SIG_NODE_DESC: b"\x02@\x80\xdf\xc2RR\x00\x00\x00R\x00\x00", - DEV_SIG_ZHA_QUIRK: "CentraLiteMotionSensor", }, { DEV_SIG_DEV_NO: 8, + SIG_MANUFACTURER: "CentraLite", + SIG_MODEL: "Motion Sensor-A", + SIG_NODE_DESC: b"\x02@\x80N\x10RR\x00\x00\x00R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 1026, @@ -362,13 +410,25 @@ DEVICES = [ SIG_EP_PROFILE: 260, }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "binary_sensor.centralite_motion_sensor_a_77665544_ias_zone", - "binary_sensor.centralite_motion_sensor_a_77665544_occupancy", "sensor.centralite_motion_sensor_a_77665544_power", "sensor.centralite_motion_sensor_a_77665544_temperature", + "button.centralite_motion_sensor_a_77665544_identify", + "binary_sensor.centralite_motion_sensor_a_77665544_ias_zone", + "binary_sensor.centralite_motion_sensor_a_77665544_occupancy", ], DEV_SIG_ENT_MAP: { + ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { + DEV_SIG_CHANNELS: ["ias_zone"], + DEV_SIG_ENT_MAP_CLASS: "IASZone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_motion_sensor_a_77665544_ias_zone", + }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.centralite_motion_sensor_a_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", @@ -379,25 +439,18 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Temperature", DEV_SIG_ENT_MAP_ID: "sensor.centralite_motion_sensor_a_77665544_temperature", }, - ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { - DEV_SIG_CHANNELS: ["ias_zone"], - DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_motion_sensor_a_77665544_ias_zone", - }, ("binary_sensor", "00:11:22:33:44:55:66:77-2-1030"): { DEV_SIG_CHANNELS: ["occupancy"], DEV_SIG_ENT_MAP_CLASS: "Occupancy", DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_motion_sensor_a_77665544_occupancy", }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0019"], - SIG_MANUFACTURER: "CentraLite", - SIG_MODEL: "Motion Sensor-A", - SIG_NODE_DESC: b"\x02@\x80N\x10RR\x00\x00\x00R\x00\x00", - DEV_SIG_ZHA_QUIRK: "CentraLite3305S", }, { DEV_SIG_DEV_NO: 9, + SIG_MANUFACTURER: "ClimaxTechnology", + SIG_MODEL: "PSMP5_00.00.02.02TC", + SIG_NODE_DESC: b"\x01@\x8e\x00\x00P\xa0\x00\x00\x00\xa0\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 81, @@ -414,7 +467,9 @@ DEVICES = [ SIG_EP_PROFILE: 260, }, }, + DEV_SIG_EVT_CHANNELS: ["4:0x0019"], DEV_SIG_ENTITIES: [ + "button.climaxtechnology_psmp5_00_00_02_02tc_77665544_identify", "sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_smartenergy_metering", "sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_smartenergy_metering_summation_delivered", "switch.climaxtechnology_psmp5_00_00_02_02tc_77665544_on_off", @@ -425,6 +480,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Switch", DEV_SIG_ENT_MAP_ID: "switch.climaxtechnology_psmp5_00_00_02_02tc_77665544_on_off", }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.climaxtechnology_psmp5_00_00_02_02tc_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", @@ -436,13 +496,12 @@ DEVICES = [ DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_smartenergy_metering_summation_delivered", }, }, - DEV_SIG_EVT_CHANNELS: ["4:0x0019"], - SIG_MANUFACTURER: "ClimaxTechnology", - SIG_MODEL: "PSMP5_00.00.02.02TC", - SIG_NODE_DESC: b"\x01@\x8e\x00\x00P\xa0\x00\x00\x00\xa0\x00\x00", }, { DEV_SIG_DEV_NO: 10, + SIG_MANUFACTURER: "ClimaxTechnology", + SIG_MODEL: "SD8SC_00.00.03.12TC", + SIG_NODE_DESC: b"\x02@\x80\x00\x00P\xa0\x00\x00\x00\xa0\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 1026, @@ -450,25 +509,31 @@ DEVICES = [ SIG_EP_INPUT: [0, 3, 1280, 1282], SIG_EP_OUTPUT: [0], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "binary_sensor.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_zone" + "button.climaxtechnology_sd8sc_00_00_03_12tc_77665544_identify", + "binary_sensor.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_zone", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", DEV_SIG_ENT_MAP_ID: "binary_sensor.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_zone", - } + }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.climaxtechnology_sd8sc_00_00_03_12tc_77665544_identify", + }, }, - DEV_SIG_EVT_CHANNELS: [], - SIG_MANUFACTURER: "ClimaxTechnology", - SIG_MODEL: "SD8SC_00.00.03.12TC", - SIG_NODE_DESC: b"\x02@\x80\x00\x00P\xa0\x00\x00\x00\xa0\x00\x00", }, { DEV_SIG_DEV_NO: 11, + SIG_MANUFACTURER: "ClimaxTechnology", + SIG_MODEL: "WS15_00.00.03.03TC", + SIG_NODE_DESC: b"\x02@\x80\x00\x00P\xa0\x00\x00\x00\xa0\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 1026, @@ -476,25 +541,31 @@ DEVICES = [ SIG_EP_INPUT: [0, 3, 1280], SIG_EP_OUTPUT: [0], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "binary_sensor.climaxtechnology_ws15_00_00_03_03tc_77665544_ias_zone" + "button.climaxtechnology_ws15_00_00_03_03tc_77665544_identify", + "binary_sensor.climaxtechnology_ws15_00_00_03_03tc_77665544_ias_zone", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", DEV_SIG_ENT_MAP_ID: "binary_sensor.climaxtechnology_ws15_00_00_03_03tc_77665544_ias_zone", - } + }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.climaxtechnology_ws15_00_00_03_03tc_77665544_identify", + }, }, - DEV_SIG_EVT_CHANNELS: [], - SIG_MANUFACTURER: "ClimaxTechnology", - SIG_MODEL: "WS15_00.00.03.03TC", - SIG_NODE_DESC: b"\x02@\x80\x00\x00P\xa0\x00\x00\x00\xa0\x00\x00", }, { DEV_SIG_DEV_NO: 12, + SIG_MANUFACTURER: "Feibit Inc co.", + SIG_MODEL: "FB56-ZCW08KU1.1", + SIG_NODE_DESC: b"\x01@\x8e\x00\x00P\xa0\x00\x00\x00\xa0\x00\x00", SIG_ENDPOINTS: { 11: { SIG_EP_TYPE: 528, @@ -511,23 +582,29 @@ DEVICES = [ SIG_EP_PROFILE: 49246, }, }, + DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "light.feibit_inc_co_fb56_zcw08ku1_1_77665544_level_light_color_on_off" + "button.feibit_inc_co_fb56_zcw08ku1_1_77665544_identify", + "light.feibit_inc_co_fb56_zcw08ku1_1_77665544_level_light_color_on_off", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-11"): { - DEV_SIG_CHANNELS: ["level", "light_color", "on_off"], + DEV_SIG_CHANNELS: ["on_off", "level", "light_color"], DEV_SIG_ENT_MAP_CLASS: "Light", DEV_SIG_ENT_MAP_ID: "light.feibit_inc_co_fb56_zcw08ku1_1_77665544_level_light_color_on_off", - } + }, + ("button", "00:11:22:33:44:55:66:77-11-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.feibit_inc_co_fb56_zcw08ku1_1_77665544_identify", + }, }, - DEV_SIG_EVT_CHANNELS: [], - SIG_MANUFACTURER: "Feibit Inc co.", - SIG_MODEL: "FB56-ZCW08KU1.1", - SIG_NODE_DESC: b"\x01@\x8e\x00\x00P\xa0\x00\x00\x00\xa0\x00\x00", }, { DEV_SIG_DEV_NO: 13, + SIG_MANUFACTURER: "HEIMAN", + SIG_MODEL: "SmokeSensor-EM", + SIG_NODE_DESC: b"\x02@\x80\x0b\x12RR\x00\x00\x00R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 1026, @@ -535,31 +612,37 @@ DEVICES = [ SIG_EP_INPUT: [0, 1, 3, 1280, 1282], SIG_EP_OUTPUT: [25], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "binary_sensor.heiman_smokesensor_em_77665544_ias_zone", + "button.heiman_smokesensor_em_77665544_identify", "sensor.heiman_smokesensor_em_77665544_power", + "binary_sensor.heiman_smokesensor_em_77665544_ias_zone", ], DEV_SIG_ENT_MAP: { - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CHANNELS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.heiman_smokesensor_em_77665544_power", - }, ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", DEV_SIG_ENT_MAP_ID: "binary_sensor.heiman_smokesensor_em_77665544_ias_zone", }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.heiman_smokesensor_em_77665544_identify", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + DEV_SIG_CHANNELS: ["power"], + DEV_SIG_ENT_MAP_CLASS: "Battery", + DEV_SIG_ENT_MAP_ID: "sensor.heiman_smokesensor_em_77665544_power", + }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0019"], - SIG_MANUFACTURER: "HEIMAN", - SIG_MODEL: "SmokeSensor-EM", - SIG_NODE_DESC: b"\x02@\x80\x0b\x12RR\x00\x00\x00R\x00\x00", }, { DEV_SIG_DEV_NO: 14, + SIG_MANUFACTURER: "Heiman", + SIG_MODEL: "CO_V16", + SIG_NODE_DESC: b"\x02@\x84\xaa\xbb@\x00\x00\x00\x00\x00\x00\x03", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 1026, @@ -567,23 +650,31 @@ DEVICES = [ SIG_EP_INPUT: [0, 1, 3, 9, 1280], SIG_EP_OUTPUT: [25], SIG_EP_PROFILE: 260, - } + }, }, - DEV_SIG_ENTITIES: ["binary_sensor.heiman_co_v16_77665544_ias_zone"], + DEV_SIG_EVT_CHANNELS: ["1:0x0019"], + DEV_SIG_ENTITIES: [ + "button.heiman_co_v16_77665544_identify", + "binary_sensor.heiman_co_v16_77665544_ias_zone", + ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", DEV_SIG_ENT_MAP_ID: "binary_sensor.heiman_co_v16_77665544_ias_zone", - } + }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.heiman_co_v16_77665544_identify", + }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0019"], - SIG_MANUFACTURER: "Heiman", - SIG_MODEL: "CO_V16", - SIG_NODE_DESC: b"\x02@\x84\xaa\xbb@\x00\x00\x00\x00\x00\x00\x03", }, { DEV_SIG_DEV_NO: 15, + SIG_MANUFACTURER: "Heiman", + SIG_MODEL: "WarningDevice", + SIG_NODE_DESC: b"\x01@\x8e\x0b\x12RR\x00\x00\x00R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 1027, @@ -591,31 +682,37 @@ DEVICES = [ SIG_EP_INPUT: [0, 1, 3, 4, 9, 1280, 1282], SIG_EP_OUTPUT: [3, 25], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "binary_sensor.heiman_warningdevice_77665544_ias_zone", + "button.heiman_warningdevice_77665544_identify", "siren.heiman_warningdevice_77665544_ias_wd", + "binary_sensor.heiman_warningdevice_77665544_ias_zone", ], DEV_SIG_ENT_MAP: { - ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { - DEV_SIG_CHANNELS: ["ias_zone"], - DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.heiman_warningdevice_77665544_ias_zone", - }, ("siren", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHASiren", DEV_SIG_ENT_MAP_ID: "siren.heiman_warningdevice_77665544_ias_wd", }, + ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { + DEV_SIG_CHANNELS: ["ias_zone"], + DEV_SIG_ENT_MAP_CLASS: "IASZone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.heiman_warningdevice_77665544_ias_zone", + }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.heiman_warningdevice_77665544_identify", + }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0019"], - SIG_MANUFACTURER: "Heiman", - SIG_MODEL: "WarningDevice", - SIG_NODE_DESC: b"\x01@\x8e\x0b\x12RR\x00\x00\x00R\x00\x00", }, { DEV_SIG_DEV_NO: 16, + SIG_MANUFACTURER: "HiveHome.com", + SIG_MODEL: "MOT003", + SIG_NODE_DESC: b"\x02@\x809\x10PP\x00\x00\x00P\x00\x00", SIG_ENDPOINTS: { 6: { SIG_EP_TYPE: 1026, @@ -623,15 +720,27 @@ DEVICES = [ SIG_EP_INPUT: [0, 1, 3, 32, 1024, 1026, 1280], SIG_EP_OUTPUT: [25], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["6:0x0019"], DEV_SIG_ENTITIES: [ - "binary_sensor.hivehome_com_mot003_77665544_ias_zone", - "sensor.hivehome_com_mot003_77665544_illuminance", + "button.hivehome_com_mot003_77665544_identify", "sensor.hivehome_com_mot003_77665544_power", + "sensor.hivehome_com_mot003_77665544_illuminance", "sensor.hivehome_com_mot003_77665544_temperature", + "binary_sensor.hivehome_com_mot003_77665544_ias_zone", ], DEV_SIG_ENT_MAP: { + ("binary_sensor", "00:11:22:33:44:55:66:77-6-1280"): { + DEV_SIG_CHANNELS: ["ias_zone"], + DEV_SIG_ENT_MAP_CLASS: "IASZone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.hivehome_com_mot003_77665544_ias_zone", + }, + ("button", "00:11:22:33:44:55:66:77-6-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.hivehome_com_mot003_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-6-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", @@ -647,20 +756,13 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Temperature", DEV_SIG_ENT_MAP_ID: "sensor.hivehome_com_mot003_77665544_temperature", }, - ("binary_sensor", "00:11:22:33:44:55:66:77-6-1280"): { - DEV_SIG_CHANNELS: ["ias_zone"], - DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.hivehome_com_mot003_77665544_ias_zone", - }, }, - DEV_SIG_EVT_CHANNELS: ["6:0x0019"], - SIG_MANUFACTURER: "HiveHome.com", - SIG_MODEL: "MOT003", - SIG_NODE_DESC: b"\x02@\x809\x10PP\x00\x00\x00P\x00\x00", - DEV_SIG_ZHA_QUIRK: "MOT003", }, { DEV_SIG_DEV_NO: 17, + SIG_MANUFACTURER: "IKEA of Sweden", + SIG_MODEL: "TRADFRI bulb E12 WS opal 600lm", + SIG_NODE_DESC: b"\x01@\x8e|\x11RR\x00\x00,R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 268, @@ -677,23 +779,29 @@ DEVICES = [ SIG_EP_PROFILE: 41440, }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019"], DEV_SIG_ENTITIES: [ - "light.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_level_light_color_on_off" + "button.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_identify", + "light.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_level_light_color_on_off", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CHANNELS: ["level", "light_color", "on_off"], + DEV_SIG_CHANNELS: ["on_off", "light_color", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", DEV_SIG_ENT_MAP_ID: "light.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_level_light_color_on_off", - } + }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_identify", + }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019"], - SIG_MANUFACTURER: "IKEA of Sweden", - SIG_MODEL: "TRADFRI bulb E12 WS opal 600lm", - SIG_NODE_DESC: b"\x01@\x8e|\x11RR\x00\x00,R\x00\x00", }, { DEV_SIG_DEV_NO: 18, + SIG_MANUFACTURER: "IKEA of Sweden", + SIG_MODEL: "TRADFRI bulb E26 CWS opal 600lm", + SIG_NODE_DESC: b"\x01@\x8e|\x11RR\x00\x00\x00R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 512, @@ -701,25 +809,31 @@ DEVICES = [ SIG_EP_INPUT: [0, 3, 4, 5, 6, 8, 768, 2821, 4096], SIG_EP_OUTPUT: [5, 25, 32, 4096], SIG_EP_PROFILE: 49246, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019"], DEV_SIG_ENTITIES: [ - "light.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_level_light_color_on_off" + "button.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_identify", + "light.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_level_light_color_on_off", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CHANNELS: ["level", "light_color", "on_off"], + DEV_SIG_CHANNELS: ["on_off", "light_color", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", DEV_SIG_ENT_MAP_ID: "light.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_level_light_color_on_off", - } + }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_identify", + }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019"], - SIG_MANUFACTURER: "IKEA of Sweden", - SIG_MODEL: "TRADFRI bulb E26 CWS opal 600lm", - SIG_NODE_DESC: b"\x01@\x8e|\x11RR\x00\x00\x00R\x00\x00", }, { DEV_SIG_DEV_NO: 19, + SIG_MANUFACTURER: "IKEA of Sweden", + SIG_MODEL: "TRADFRI bulb E26 W opal 1000lm", + SIG_NODE_DESC: b"\x01@\x8e|\x11RR\x00\x00\x00R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 256, @@ -727,25 +841,31 @@ DEVICES = [ SIG_EP_INPUT: [0, 3, 4, 5, 6, 8, 2821, 4096], SIG_EP_OUTPUT: [5, 25, 32, 4096], SIG_EP_PROFILE: 49246, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019"], DEV_SIG_ENTITIES: [ - "light.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_77665544_level_on_off" + "button.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_77665544_identify", + "light.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_77665544_level_on_off", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CHANNELS: ["level", "on_off"], + DEV_SIG_CHANNELS: ["on_off", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", DEV_SIG_ENT_MAP_ID: "light.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_77665544_level_on_off", - } + }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_77665544_identify", + }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019"], - SIG_MANUFACTURER: "IKEA of Sweden", - SIG_MODEL: "TRADFRI bulb E26 W opal 1000lm", - SIG_NODE_DESC: b"\x01@\x8e|\x11RR\x00\x00\x00R\x00\x00", }, { DEV_SIG_DEV_NO: 20, + SIG_MANUFACTURER: "IKEA of Sweden", + SIG_MODEL: "TRADFRI bulb E26 WS opal 980lm", + SIG_NODE_DESC: b"\x01@\x8e|\x11RR\x00\x00\x00R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 544, @@ -753,25 +873,31 @@ DEVICES = [ SIG_EP_INPUT: [0, 3, 4, 5, 6, 8, 768, 2821, 4096], SIG_EP_OUTPUT: [5, 25, 32, 4096], SIG_EP_PROFILE: 49246, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019"], DEV_SIG_ENTITIES: [ - "light.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_level_light_color_on_off" + "button.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_identify", + "light.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_level_light_color_on_off", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CHANNELS: ["level", "light_color", "on_off"], + DEV_SIG_CHANNELS: ["on_off", "light_color", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", DEV_SIG_ENT_MAP_ID: "light.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_level_light_color_on_off", - } + }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_identify", + }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019"], - SIG_MANUFACTURER: "IKEA of Sweden", - SIG_MODEL: "TRADFRI bulb E26 WS opal 980lm", - SIG_NODE_DESC: b"\x01@\x8e|\x11RR\x00\x00\x00R\x00\x00", }, { DEV_SIG_DEV_NO: 21, + SIG_MANUFACTURER: "IKEA of Sweden", + SIG_MODEL: "TRADFRI bulb E26 opal 1000lm", + SIG_NODE_DESC: b"\x01@\x8e|\x11RR\x00\x00\x00R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 256, @@ -779,25 +905,31 @@ DEVICES = [ SIG_EP_INPUT: [0, 3, 4, 5, 6, 8, 2821, 4096], SIG_EP_OUTPUT: [5, 25, 32, 4096], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019"], DEV_SIG_ENTITIES: [ - "light.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_77665544_level_on_off" + "button.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_77665544_identify", + "light.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_77665544_level_on_off", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CHANNELS: ["level", "on_off"], + DEV_SIG_CHANNELS: ["on_off", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", DEV_SIG_ENT_MAP_ID: "light.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_77665544_level_on_off", - } + }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_77665544_identify", + }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019"], - SIG_MANUFACTURER: "IKEA of Sweden", - SIG_MODEL: "TRADFRI bulb E26 opal 1000lm", - SIG_NODE_DESC: b"\x01@\x8e|\x11RR\x00\x00\x00R\x00\x00", }, { DEV_SIG_DEV_NO: 22, + SIG_MANUFACTURER: "IKEA of Sweden", + SIG_MODEL: "TRADFRI control outlet", + SIG_NODE_DESC: b"\x01@\x8e|\x11RR\x00\x00,R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 266, @@ -805,26 +937,31 @@ DEVICES = [ SIG_EP_INPUT: [0, 3, 4, 5, 6, 64636], SIG_EP_OUTPUT: [5, 25, 32], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019"], DEV_SIG_ENTITIES: [ - "switch.ikea_of_sweden_tradfri_control_outlet_77665544_on_off" + "button.ikea_of_sweden_tradfri_control_outlet_77665544_identify", + "switch.ikea_of_sweden_tradfri_control_outlet_77665544_on_off", ], DEV_SIG_ENT_MAP: { ("switch", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", DEV_SIG_ENT_MAP_ID: "switch.ikea_of_sweden_tradfri_control_outlet_77665544_on_off", - } + }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_control_outlet_77665544_identify", + }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019"], - SIG_MANUFACTURER: "IKEA of Sweden", - SIG_MODEL: "TRADFRI control outlet", - SIG_NODE_DESC: b"\x01@\x8e|\x11RR\x00\x00,R\x00\x00", - DEV_SIG_ZHA_QUIRK: "TradfriPlug", }, { DEV_SIG_DEV_NO: 23, + SIG_MANUFACTURER: "IKEA of Sweden", + SIG_MODEL: "TRADFRI motion sensor", + SIG_NODE_DESC: b"\x02@\x80|\x11RR\x00\x00\x00R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 2128, @@ -832,13 +969,20 @@ DEVICES = [ SIG_EP_INPUT: [0, 1, 3, 9, 2821, 4096], SIG_EP_OUTPUT: [3, 4, 6, 25, 4096], SIG_EP_PROFILE: 49246, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0019"], DEV_SIG_ENTITIES: [ - "binary_sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_on_off", + "button.ikea_of_sweden_tradfri_motion_sensor_77665544_identify", "sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_power", + "binary_sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_on_off", ], DEV_SIG_ENT_MAP: { + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_motion_sensor_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", @@ -850,14 +994,12 @@ DEVICES = [ DEV_SIG_ENT_MAP_ID: "binary_sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_on_off", }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0019"], - SIG_MANUFACTURER: "IKEA of Sweden", - SIG_MODEL: "TRADFRI motion sensor", - SIG_NODE_DESC: b"\x02@\x80|\x11RR\x00\x00\x00R\x00\x00", - DEV_SIG_ZHA_QUIRK: "IkeaTradfriMotion", }, { DEV_SIG_DEV_NO: 24, + SIG_MANUFACTURER: "IKEA of Sweden", + SIG_MODEL: "TRADFRI on/off switch", + SIG_NODE_DESC: b"\x02@\x80|\x11RR\x00\x00,R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 2080, @@ -865,26 +1007,31 @@ DEVICES = [ SIG_EP_INPUT: [0, 1, 3, 9, 32, 4096, 64636], SIG_EP_OUTPUT: [3, 4, 6, 8, 25, 258, 4096], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0019", "1:0x0102"], DEV_SIG_ENTITIES: [ - "sensor.ikea_of_sweden_tradfri_on_off_switch_77665544_power" + "button.ikea_of_sweden_tradfri_on_off_switch_77665544_identify", + "sensor.ikea_of_sweden_tradfri_on_off_switch_77665544_power", ], DEV_SIG_ENT_MAP: { + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_on_off_switch_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_on_off_switch_77665544_power", - } + }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0019", "1:0x0102"], - SIG_MANUFACTURER: "IKEA of Sweden", - SIG_MODEL: "TRADFRI on/off switch", - SIG_NODE_DESC: b"\x02@\x80|\x11RR\x00\x00,R\x00\x00", - DEV_SIG_ZHA_QUIRK: "IkeaTradfriRemote2Btn", }, { DEV_SIG_DEV_NO: 25, + SIG_MANUFACTURER: "IKEA of Sweden", + SIG_MODEL: "TRADFRI remote control", + SIG_NODE_DESC: b"\x02@\x80|\x11RR\x00\x00\x00R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 2096, @@ -892,26 +1039,31 @@ DEVICES = [ SIG_EP_INPUT: [0, 1, 3, 9, 2821, 4096], SIG_EP_OUTPUT: [3, 4, 5, 6, 8, 25, 4096], SIG_EP_PROFILE: 49246, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0006", "1:0x0008", "1:0x0019"], DEV_SIG_ENTITIES: [ - "sensor.ikea_of_sweden_tradfri_remote_control_77665544_power" + "button.ikea_of_sweden_tradfri_remote_control_77665544_identify", + "sensor.ikea_of_sweden_tradfri_remote_control_77665544_power", ], DEV_SIG_ENT_MAP: { + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_remote_control_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_remote_control_77665544_power", - } + }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0006", "1:0x0008", "1:0x0019"], - SIG_MANUFACTURER: "IKEA of Sweden", - SIG_MODEL: "TRADFRI remote control", - SIG_NODE_DESC: b"\x02@\x80|\x11RR\x00\x00\x00R\x00\x00", - DEV_SIG_ZHA_QUIRK: "IkeaTradfriRemote", }, { DEV_SIG_DEV_NO: 26, + SIG_MANUFACTURER: "IKEA of Sweden", + SIG_MODEL: "TRADFRI signal repeater", + SIG_NODE_DESC: b"\x01@\x8e|\x11RR\x00\x00,R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 8, @@ -928,15 +1080,23 @@ DEVICES = [ SIG_EP_PROFILE: 41440, }, }, - DEV_SIG_ENTITIES: [], - DEV_SIG_ENT_MAP: {}, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], - SIG_MANUFACTURER: "IKEA of Sweden", - SIG_MODEL: "TRADFRI signal repeater", - SIG_NODE_DESC: b"\x01@\x8e|\x11RR\x00\x00,R\x00\x00", + DEV_SIG_ENTITIES: [ + "button.ikea_of_sweden_tradfri_signal_repeater_77665544_identify", + ], + DEV_SIG_ENT_MAP: { + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_signal_repeater_77665544_identify", + }, + }, }, { DEV_SIG_DEV_NO: 27, + SIG_MANUFACTURER: "IKEA of Sweden", + SIG_MODEL: "TRADFRI wireless dimmer", + SIG_NODE_DESC: b"\x02@\x80|\x11RR\x00\x00\x00R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 2064, @@ -944,25 +1104,31 @@ DEVICES = [ SIG_EP_INPUT: [0, 1, 3, 9, 2821, 4096], SIG_EP_OUTPUT: [3, 4, 6, 8, 25, 4096], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0019"], DEV_SIG_ENTITIES: [ - "sensor.ikea_of_sweden_tradfri_wireless_dimmer_77665544_power" + "button.ikea_of_sweden_tradfri_wireless_dimmer_77665544_identify", + "sensor.ikea_of_sweden_tradfri_wireless_dimmer_77665544_power", ], DEV_SIG_ENT_MAP: { + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_wireless_dimmer_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_wireless_dimmer_77665544_power", - } + }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0019"], - SIG_MANUFACTURER: "IKEA of Sweden", - SIG_MODEL: "TRADFRI wireless dimmer", - SIG_NODE_DESC: b"\x02@\x80|\x11RR\x00\x00\x00R\x00\x00", }, { DEV_SIG_DEV_NO: 28, + SIG_MANUFACTURER: "Jasco Products", + SIG_MODEL: "45852", + SIG_NODE_DESC: b"\x01@\x8e$\x11R\xff\x00\x00\x00\xff\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 257, @@ -979,17 +1145,24 @@ DEVICES = [ SIG_EP_PROFILE: 260, }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0019", "2:0x0006", "2:0x0008"], DEV_SIG_ENTITIES: [ + "button.jasco_products_45852_77665544_identify", "light.jasco_products_45852_77665544_level_on_off", "sensor.jasco_products_45852_77665544_smartenergy_metering", "sensor.jasco_products_45852_77665544_smartenergy_metering_summation_delivered", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CHANNELS: ["level", "on_off"], + DEV_SIG_CHANNELS: ["on_off", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", DEV_SIG_ENT_MAP_ID: "light.jasco_products_45852_77665544_level_on_off", }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.jasco_products_45852_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", @@ -1001,13 +1174,12 @@ DEVICES = [ DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_77665544_smartenergy_metering_summation_delivered", }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0019", "2:0x0006", "2:0x0008"], - SIG_MANUFACTURER: "Jasco Products", - SIG_MODEL: "45852", - SIG_NODE_DESC: b"\x01@\x8e$\x11R\xff\x00\x00\x00\xff\x00\x00", }, { DEV_SIG_DEV_NO: 29, + SIG_MANUFACTURER: "Jasco Products", + SIG_MODEL: "45856", + SIG_NODE_DESC: b"\x01@\x8e$\x11R\xff\x00\x00\x00\xff\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 256, @@ -1024,7 +1196,9 @@ DEVICES = [ SIG_EP_PROFILE: 260, }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0019", "2:0x0006"], DEV_SIG_ENTITIES: [ + "button.jasco_products_45856_77665544_identify", "light.jasco_products_45856_77665544_on_off", "sensor.jasco_products_45856_77665544_smartenergy_metering", "sensor.jasco_products_45856_77665544_smartenergy_metering_summation_delivered", @@ -1035,6 +1209,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Light", DEV_SIG_ENT_MAP_ID: "light.jasco_products_45856_77665544_on_off", }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.jasco_products_45856_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", @@ -1046,13 +1225,12 @@ DEVICES = [ DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_77665544_smartenergy_metering_summation_delivered", }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0019", "2:0x0006"], - SIG_MANUFACTURER: "Jasco Products", - SIG_MODEL: "45856", - SIG_NODE_DESC: b"\x01@\x8e$\x11R\xff\x00\x00\x00\xff\x00\x00", }, { DEV_SIG_DEV_NO: 30, + SIG_MANUFACTURER: "Jasco Products", + SIG_MODEL: "45857", + SIG_NODE_DESC: b"\x01@\x8e$\x11R\xff\x00\x00\x00\xff\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 257, @@ -1069,17 +1247,24 @@ DEVICES = [ SIG_EP_PROFILE: 260, }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0019", "2:0x0006", "2:0x0008"], DEV_SIG_ENTITIES: [ + "button.jasco_products_45857_77665544_identify", "light.jasco_products_45857_77665544_level_on_off", "sensor.jasco_products_45857_77665544_smartenergy_metering", "sensor.jasco_products_45857_77665544_smartenergy_metering_summation_delivered", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CHANNELS: ["level", "on_off"], + DEV_SIG_CHANNELS: ["on_off", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", DEV_SIG_ENT_MAP_ID: "light.jasco_products_45857_77665544_level_on_off", }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.jasco_products_45857_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", @@ -1091,43 +1276,35 @@ DEVICES = [ DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_77665544_smartenergy_metering_summation_delivered", }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0019", "2:0x0006", "2:0x0008"], - SIG_MANUFACTURER: "Jasco Products", - SIG_MODEL: "45857", - SIG_NODE_DESC: b"\x01@\x8e$\x11R\xff\x00\x00\x00\xff\x00\x00", }, { DEV_SIG_DEV_NO: 31, + SIG_MANUFACTURER: "Keen Home Inc", + SIG_MODEL: "SV02-610-MP-1.3", + SIG_NODE_DESC: b"\x02@\x80[\x11RR\x00\x00*R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 3, DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [ - 0, - 1, - 3, - 4, - 5, - 6, - 8, - 32, - 1026, - 1027, - 2821, - 64513, - 64514, - ], + SIG_EP_INPUT: [0, 1, 3, 4, 5, 6, 8, 32, 1026, 1027, 2821, 64513, 64514], SIG_EP_OUTPUT: [25], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "cover.keen_home_inc_sv02_610_mp_1_3_77665544_level_on_off", + "button.keen_home_inc_sv02_610_mp_1_3_77665544_identify", "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_power", "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_pressure", "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_temperature", + "cover.keen_home_inc_sv02_610_mp_1_3_77665544_level_on_off", ], DEV_SIG_ENT_MAP: { + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.keen_home_inc_sv02_610_mp_1_3_77665544_identify", + }, ("cover", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["level", "on_off"], DEV_SIG_ENT_MAP_CLASS: "KeenVent", @@ -1138,54 +1315,46 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_power", }, - ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { - DEV_SIG_CHANNELS: ["temperature"], - DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_temperature", - }, ("sensor", "00:11:22:33:44:55:66:77-1-1027"): { DEV_SIG_CHANNELS: ["pressure"], DEV_SIG_ENT_MAP_CLASS: "Pressure", DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_pressure", }, + ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { + DEV_SIG_CHANNELS: ["temperature"], + DEV_SIG_ENT_MAP_CLASS: "Temperature", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_temperature", + }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0019"], - SIG_MANUFACTURER: "Keen Home Inc", - SIG_MODEL: "SV02-610-MP-1.3", - SIG_NODE_DESC: b"\x02@\x80[\x11RR\x00\x00*R\x00\x00", }, { DEV_SIG_DEV_NO: 32, + SIG_MANUFACTURER: "Keen Home Inc", + SIG_MODEL: "SV02-612-MP-1.2", + SIG_NODE_DESC: b"\x02@\x80[\x11RR\x00\x00*R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 3, DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [ - 0, - 1, - 3, - 4, - 5, - 6, - 8, - 32, - 1026, - 1027, - 2821, - 64513, - 64514, - ], + SIG_EP_INPUT: [0, 1, 3, 4, 5, 6, 8, 32, 1026, 1027, 2821, 64513, 64514], SIG_EP_OUTPUT: [25], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "cover.keen_home_inc_sv02_612_mp_1_2_77665544_level_on_off", + "button.keen_home_inc_sv02_612_mp_1_2_77665544_identify", "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_power", "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_pressure", "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_temperature", + "cover.keen_home_inc_sv02_612_mp_1_2_77665544_level_on_off", ], DEV_SIG_ENT_MAP: { + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.keen_home_inc_sv02_612_mp_1_2_77665544_identify", + }, ("cover", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["level", "on_off"], DEV_SIG_ENT_MAP_CLASS: "KeenVent", @@ -1196,54 +1365,46 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_power", }, - ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { - DEV_SIG_CHANNELS: ["temperature"], - DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_temperature", - }, ("sensor", "00:11:22:33:44:55:66:77-1-1027"): { DEV_SIG_CHANNELS: ["pressure"], DEV_SIG_ENT_MAP_CLASS: "Pressure", DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_pressure", }, + ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { + DEV_SIG_CHANNELS: ["temperature"], + DEV_SIG_ENT_MAP_CLASS: "Temperature", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_temperature", + }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0019"], - SIG_MANUFACTURER: "Keen Home Inc", - SIG_MODEL: "SV02-612-MP-1.2", - SIG_NODE_DESC: b"\x02@\x80[\x11RR\x00\x00*R\x00\x00", }, { DEV_SIG_DEV_NO: 33, + SIG_MANUFACTURER: "Keen Home Inc", + SIG_MODEL: "SV02-612-MP-1.3", + SIG_NODE_DESC: b"\x02@\x80[\x11RR\x00\x00*R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 3, DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [ - 0, - 1, - 3, - 4, - 5, - 6, - 8, - 32, - 1026, - 1027, - 2821, - 64513, - 64514, - ], + SIG_EP_INPUT: [0, 1, 3, 4, 5, 6, 8, 32, 1026, 1027, 2821, 64513, 64514], SIG_EP_OUTPUT: [25], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "cover.keen_home_inc_sv02_612_mp_1_3_77665544_level_on_off", + "button.keen_home_inc_sv02_612_mp_1_3_77665544_identify", "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_power", "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_pressure", "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_temperature", + "cover.keen_home_inc_sv02_612_mp_1_3_77665544_level_on_off", ], DEV_SIG_ENT_MAP: { + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.keen_home_inc_sv02_612_mp_1_3_77665544_identify", + }, ("cover", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["level", "on_off"], DEV_SIG_ENT_MAP_CLASS: "KeenVent", @@ -1254,25 +1415,23 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_power", }, - ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { - DEV_SIG_CHANNELS: ["temperature"], - DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_temperature", - }, ("sensor", "00:11:22:33:44:55:66:77-1-1027"): { DEV_SIG_CHANNELS: ["pressure"], DEV_SIG_ENT_MAP_CLASS: "Pressure", DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_pressure", }, + ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { + DEV_SIG_CHANNELS: ["temperature"], + DEV_SIG_ENT_MAP_CLASS: "Temperature", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_temperature", + }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0019"], - SIG_MANUFACTURER: "Keen Home Inc", - SIG_MODEL: "SV02-612-MP-1.3", - SIG_NODE_DESC: b"\x02@\x80[\x11RR\x00\x00*R\x00\x00", - DEV_SIG_ZHA_QUIRK: "KeenHomeSmartVent", }, { DEV_SIG_DEV_NO: 34, + SIG_MANUFACTURER: "King Of Fans, Inc.", + SIG_MODEL: "HBUniversalCFRemote", + SIG_NODE_DESC: b"\x02@\x8c\x02\x10RR\x00\x00\x00R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 257, @@ -1280,32 +1439,37 @@ DEVICES = [ SIG_EP_INPUT: [0, 3, 4, 5, 6, 8, 514], SIG_EP_OUTPUT: [3, 25], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "fan.king_of_fans_inc_hbuniversalcfremote_77665544_fan", + "button.king_of_fans_inc_hbuniversalcfremote_77665544_identify", "light.king_of_fans_inc_hbuniversalcfremote_77665544_level_on_off", + "fan.king_of_fans_inc_hbuniversalcfremote_77665544_fan", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CHANNELS: ["level", "on_off"], + DEV_SIG_CHANNELS: ["on_off", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", DEV_SIG_ENT_MAP_ID: "light.king_of_fans_inc_hbuniversalcfremote_77665544_level_on_off", }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.king_of_fans_inc_hbuniversalcfremote_77665544_identify", + }, ("fan", "00:11:22:33:44:55:66:77-1-514"): { DEV_SIG_CHANNELS: ["fan"], DEV_SIG_ENT_MAP_CLASS: "ZhaFan", DEV_SIG_ENT_MAP_ID: "fan.king_of_fans_inc_hbuniversalcfremote_77665544_fan", }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0019"], - SIG_MANUFACTURER: "King Of Fans, Inc.", - SIG_MODEL: "HBUniversalCFRemote", - SIG_NODE_DESC: b"\x02@\x8c\x02\x10RR\x00\x00\x00R\x00\x00", - DEV_SIG_ZHA_QUIRK: "CeilingFan", }, { DEV_SIG_DEV_NO: 35, + SIG_MANUFACTURER: "LDS", + SIG_MODEL: "ZBT-CCTSwitch-D0001", + SIG_NODE_DESC: b"\x02@\x80h\x11RR\x00\x00,R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 2048, @@ -1313,48 +1477,31 @@ DEVICES = [ SIG_EP_INPUT: [0, 1, 3, 4096, 64769], SIG_EP_OUTPUT: [3, 4, 6, 8, 25, 768, 4096], SIG_EP_PROFILE: 260, - } + }, }, - DEV_SIG_ENTITIES: ["sensor.lds_zbt_cctswitch_d0001_77665544_power"], + DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0019", "1:0x0300"], + DEV_SIG_ENTITIES: [ + "button.lds_zbt_cctswitch_d0001_77665544_identify", + "sensor.lds_zbt_cctswitch_d0001_77665544_power", + ], DEV_SIG_ENT_MAP: { + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.lds_zbt_cctswitch_d0001_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.lds_zbt_cctswitch_d0001_77665544_power", - } + }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0019", "1:0x0300"], - SIG_MANUFACTURER: "LDS", - SIG_MODEL: "ZBT-CCTSwitch-D0001", - SIG_NODE_DESC: b"\x02@\x80h\x11RR\x00\x00,R\x00\x00", - DEV_SIG_ZHA_QUIRK: "CCTSwitch", }, { DEV_SIG_DEV_NO: 36, - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 258, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 3, 4, 5, 6, 8, 768, 2821, 64513], - SIG_EP_OUTPUT: [25], - SIG_EP_PROFILE: 260, - } - }, - DEV_SIG_ENTITIES: ["light.ledvance_a19_rgbw_77665544_level_light_color_on_off"], - DEV_SIG_ENT_MAP: { - ("light", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CHANNELS: ["level", "light_color", "on_off"], - DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.ledvance_a19_rgbw_77665544_level_light_color_on_off", - } - }, - DEV_SIG_EVT_CHANNELS: ["1:0x0019"], SIG_MANUFACTURER: "LEDVANCE", SIG_MODEL: "A19 RGBW", SIG_NODE_DESC: b"\x01@\x8e\x89\x11RR\x00\x00\x00R\x00\x00", - }, - { - DEV_SIG_DEV_NO: 37, SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 258, @@ -1362,25 +1509,63 @@ DEVICES = [ SIG_EP_INPUT: [0, 3, 4, 5, 6, 8, 768, 2821, 64513], SIG_EP_OUTPUT: [25], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "light.ledvance_flex_rgbw_77665544_level_light_color_on_off" + "button.ledvance_a19_rgbw_77665544_identify", + "light.ledvance_a19_rgbw_77665544_level_light_color_on_off", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CHANNELS: ["level", "light_color", "on_off"], + DEV_SIG_CHANNELS: ["on_off", "light_color", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.ledvance_flex_rgbw_77665544_level_light_color_on_off", - } + DEV_SIG_ENT_MAP_ID: "light.ledvance_a19_rgbw_77665544_level_light_color_on_off", + }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.ledvance_a19_rgbw_77665544_identify", + }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0019"], + }, + { + DEV_SIG_DEV_NO: 37, SIG_MANUFACTURER: "LEDVANCE", SIG_MODEL: "FLEX RGBW", SIG_NODE_DESC: b"\x01@\x8e\x89\x11RR\x00\x00\x00R\x00\x00", + SIG_ENDPOINTS: { + 1: { + SIG_EP_TYPE: 258, + DEV_SIG_EP_ID: 1, + SIG_EP_INPUT: [0, 3, 4, 5, 6, 8, 768, 2821, 64513], + SIG_EP_OUTPUT: [25], + SIG_EP_PROFILE: 260, + }, + }, + DEV_SIG_EVT_CHANNELS: ["1:0x0019"], + DEV_SIG_ENTITIES: [ + "button.ledvance_flex_rgbw_77665544_identify", + "light.ledvance_flex_rgbw_77665544_level_light_color_on_off", + ], + DEV_SIG_ENT_MAP: { + ("light", "00:11:22:33:44:55:66:77-1"): { + DEV_SIG_CHANNELS: ["on_off", "light_color", "level"], + DEV_SIG_ENT_MAP_CLASS: "Light", + DEV_SIG_ENT_MAP_ID: "light.ledvance_flex_rgbw_77665544_level_light_color_on_off", + }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.ledvance_flex_rgbw_77665544_identify", + }, + }, }, { DEV_SIG_DEV_NO: 38, + SIG_MANUFACTURER: "LEDVANCE", + SIG_MODEL: "PLUG", + SIG_NODE_DESC: b"\x01@\x8e\x89\x11RR\x00\x00\x00R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 81, @@ -1388,23 +1573,31 @@ DEVICES = [ SIG_EP_INPUT: [0, 3, 4, 5, 6, 2821, 64513, 64520], SIG_EP_OUTPUT: [3, 25], SIG_EP_PROFILE: 260, - } + }, }, - DEV_SIG_ENTITIES: ["switch.ledvance_plug_77665544_on_off"], + DEV_SIG_EVT_CHANNELS: ["1:0x0019"], + DEV_SIG_ENTITIES: [ + "button.ledvance_plug_77665544_identify", + "switch.ledvance_plug_77665544_on_off", + ], DEV_SIG_ENT_MAP: { ("switch", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", DEV_SIG_ENT_MAP_ID: "switch.ledvance_plug_77665544_on_off", - } + }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.ledvance_plug_77665544_identify", + }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0019"], - SIG_MANUFACTURER: "LEDVANCE", - SIG_MODEL: "PLUG", - SIG_NODE_DESC: b"\x01@\x8e\x89\x11RR\x00\x00\x00R\x00\x00", }, { DEV_SIG_DEV_NO: 39, + SIG_MANUFACTURER: "LEDVANCE", + SIG_MODEL: "RT RGBW", + SIG_NODE_DESC: b"\x01@\x8e\x89\x11RR\x00\x00\x00R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 258, @@ -1412,23 +1605,31 @@ DEVICES = [ SIG_EP_INPUT: [0, 3, 4, 5, 6, 8, 768, 2821, 64513], SIG_EP_OUTPUT: [25], SIG_EP_PROFILE: 260, - } - }, - DEV_SIG_ENTITIES: ["light.ledvance_rt_rgbw_77665544_level_light_color_on_off"], - DEV_SIG_ENT_MAP: { - ("light", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CHANNELS: ["level", "light_color", "on_off"], - DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.ledvance_rt_rgbw_77665544_level_light_color_on_off", - } + }, }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], - SIG_MANUFACTURER: "LEDVANCE", - SIG_MODEL: "RT RGBW", - SIG_NODE_DESC: b"\x01@\x8e\x89\x11RR\x00\x00\x00R\x00\x00", + DEV_SIG_ENTITIES: [ + "button.ledvance_rt_rgbw_77665544_identify", + "light.ledvance_rt_rgbw_77665544_level_light_color_on_off", + ], + DEV_SIG_ENT_MAP: { + ("light", "00:11:22:33:44:55:66:77-1"): { + DEV_SIG_CHANNELS: ["on_off", "light_color", "level"], + DEV_SIG_ENT_MAP_CLASS: "Light", + DEV_SIG_ENT_MAP_ID: "light.ledvance_rt_rgbw_77665544_level_light_color_on_off", + }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.ledvance_rt_rgbw_77665544_identify", + }, + }, }, { DEV_SIG_DEV_NO: 40, + SIG_MANUFACTURER: "LUMI", + SIG_MODEL: "lumi.plug.maus01", + SIG_NODE_DESC: b"\x01@\x8e_\x11\x7fd\x00\x00\x00d\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 81, @@ -1459,31 +1660,29 @@ DEVICES = [ SIG_EP_PROFILE: 260, }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "sensor.lumi_lumi_plug_maus01_77665544_analog_input", - "sensor.lumi_lumi_plug_maus01_77665544_analog_input_2", + "button.lumi_lumi_plug_maus01_77665544_identify", "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement", "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_apparent_power", "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_rms_current", "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_rms_voltage", + "sensor.lumi_lumi_plug_maus01_77665544_analog_input", + "sensor.lumi_lumi_plug_maus01_77665544_analog_input_2", + "binary_sensor.lumi_lumi_plug_maus01_77665544_binary_input", "switch.lumi_lumi_plug_maus01_77665544_on_off", ], DEV_SIG_ENT_MAP: { - ("sensor", "00:11:22:33:44:55:66:77-2-12"): { - DEV_SIG_CHANNELS: ["analog_input"], - DEV_SIG_ENT_MAP_CLASS: "AnalogInput", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_analog_input", - }, - ("sensor", "00:11:22:33:44:55:66:77-3-12"): { - DEV_SIG_CHANNELS: ["analog_input"], - DEV_SIG_ENT_MAP_CLASS: "AnalogInput", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_analog_input_2", - }, ("switch", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", DEV_SIG_ENT_MAP_ID: "switch.lumi_lumi_plug_maus01_77665544_on_off", }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_plug_maus01_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", @@ -1504,20 +1703,28 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_rms_voltage", }, + ("sensor", "00:11:22:33:44:55:66:77-2-12"): { + DEV_SIG_CHANNELS: ["analog_input"], + DEV_SIG_ENT_MAP_CLASS: "AnalogInput", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_analog_input", + }, + ("sensor", "00:11:22:33:44:55:66:77-3-12"): { + DEV_SIG_CHANNELS: ["analog_input"], + DEV_SIG_ENT_MAP_CLASS: "AnalogInput", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_analog_input_2", + }, ("binary_sensor", "00:11:22:33:44:55:66:77-100-15"): { DEV_SIG_CHANNELS: ["binary_input"], DEV_SIG_ENT_MAP_CLASS: "BinaryInput", DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_plug_maus01_77665544_binary_input", }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0019"], - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.plug.maus01", - SIG_NODE_DESC: b"\x01@\x8e_\x11\x7fd\x00\x00\x00d\x00\x00", - DEV_SIG_ZHA_QUIRK: "Plug", }, { DEV_SIG_DEV_NO: 41, + SIG_MANUFACTURER: "LUMI", + SIG_MODEL: "lumi.relay.c2acn01", + SIG_NODE_DESC: b"\x01@\x8e7\x10\x7fd\x00\x00\x00d\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 257, @@ -1534,7 +1741,9 @@ DEVICES = [ SIG_EP_PROFILE: 260, }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ + "button.lumi_lumi_relay_c2acn01_77665544_identify", "light.lumi_lumi_relay_c2acn01_77665544_on_off", "light.lumi_lumi_relay_c2acn01_77665544_on_off_2", "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement", @@ -1548,6 +1757,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Light", DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_relay_c2acn01_77665544_on_off", }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_relay_c2acn01_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", @@ -1574,14 +1788,12 @@ DEVICES = [ DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_relay_c2acn01_77665544_on_off_2", }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0019"], - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.relay.c2acn01", - SIG_NODE_DESC: b"\x01@\x8e7\x10\x7fd\x00\x00\x00d\x00\x00", - DEV_SIG_ZHA_QUIRK: "Relay", }, { DEV_SIG_DEV_NO: 42, + SIG_MANUFACTURER: "LUMI", + SIG_MODEL: "lumi.remote.b186acn01", + SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 24321, @@ -1605,22 +1817,29 @@ DEVICES = [ SIG_EP_PROFILE: 260, }, }, - DEV_SIG_ENTITIES: ["sensor.lumi_lumi_remote_b186acn01_77665544_power"], + DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019", "2:0x0005", "3:0x0005"], + DEV_SIG_ENTITIES: [ + "button.lumi_lumi_remote_b186acn01_77665544_identify", + "sensor.lumi_lumi_remote_b186acn01_77665544_power", + ], DEV_SIG_ENT_MAP: { + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b186acn01_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b186acn01_77665544_power", }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019", "2:0x0005", "3:0x0005"], - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.remote.b186acn01", - SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", - DEV_SIG_ZHA_QUIRK: "RemoteB186ACN01", }, { DEV_SIG_DEV_NO: 43, + SIG_MANUFACTURER: "LUMI", + SIG_MODEL: "lumi.remote.b286acn01", + SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 24321, @@ -1644,128 +1863,90 @@ DEVICES = [ SIG_EP_PROFILE: 260, }, }, - DEV_SIG_ENTITIES: ["sensor.lumi_lumi_remote_b286acn01_77665544_power"], + DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019", "2:0x0005", "3:0x0005"], + DEV_SIG_ENTITIES: [ + "button.lumi_lumi_remote_b286acn01_77665544_identify", + "sensor.lumi_lumi_remote_b286acn01_77665544_power", + ], DEV_SIG_ENT_MAP: { + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b286acn01_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286acn01_77665544_power", }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019", "2:0x0005", "3:0x0005"], - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.remote.b286acn01", - SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", - DEV_SIG_ZHA_QUIRK: "RemoteB286ACN01", }, { DEV_SIG_DEV_NO: 44, - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 261, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3], - SIG_EP_OUTPUT: [3, 6, 8, 768], - SIG_EP_PROFILE: 260, - }, - 2: { - SIG_EP_TYPE: -1, - DEV_SIG_EP_ID: 2, - SIG_EP_INPUT: [], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: -1, - }, - 3: { - SIG_EP_TYPE: -1, - DEV_SIG_EP_ID: 3, - SIG_EP_INPUT: [], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: -1, - }, - 4: { - SIG_EP_TYPE: -1, - DEV_SIG_EP_ID: 4, - SIG_EP_INPUT: [], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: -1, - }, - 5: { - SIG_EP_TYPE: -1, - DEV_SIG_EP_ID: 5, - SIG_EP_INPUT: [], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: -1, - }, - 6: { - SIG_EP_TYPE: -1, - DEV_SIG_EP_ID: 6, - SIG_EP_INPUT: [], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: -1, - }, - }, - DEV_SIG_ENTITIES: [], - DEV_SIG_ENT_MAP: {}, - DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0300"], SIG_MANUFACTURER: "LUMI", SIG_MODEL: "lumi.remote.b286opcn01", SIG_NODE_DESC: b"\x02@\x84_\x11\x7fd\x00\x00,d\x00\x00", + SIG_ENDPOINTS: { + 1: { + SIG_EP_TYPE: 261, + DEV_SIG_EP_ID: 1, + SIG_EP_INPUT: [0, 1, 3], + SIG_EP_OUTPUT: [3, 6, 8, 768], + SIG_EP_PROFILE: 260, + }, + 2: { + SIG_EP_TYPE: -1, + DEV_SIG_EP_ID: 2, + SIG_EP_INPUT: [], + SIG_EP_OUTPUT: [], + SIG_EP_PROFILE: -1, + }, + 3: { + SIG_EP_TYPE: -1, + DEV_SIG_EP_ID: 3, + SIG_EP_INPUT: [], + SIG_EP_OUTPUT: [], + SIG_EP_PROFILE: -1, + }, + 4: { + SIG_EP_TYPE: -1, + DEV_SIG_EP_ID: 4, + SIG_EP_INPUT: [], + SIG_EP_OUTPUT: [], + SIG_EP_PROFILE: -1, + }, + 5: { + SIG_EP_TYPE: -1, + DEV_SIG_EP_ID: 5, + SIG_EP_INPUT: [], + SIG_EP_OUTPUT: [], + SIG_EP_PROFILE: -1, + }, + 6: { + SIG_EP_TYPE: -1, + DEV_SIG_EP_ID: 6, + SIG_EP_INPUT: [], + SIG_EP_OUTPUT: [], + SIG_EP_PROFILE: -1, + }, + }, + DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0300"], + DEV_SIG_ENTITIES: [ + "button.lumi_lumi_remote_b286opcn01_77665544_identify", + ], + DEV_SIG_ENT_MAP: { + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b286opcn01_77665544_identify", + }, + }, }, { DEV_SIG_DEV_NO: 45, - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 261, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3], - SIG_EP_OUTPUT: [3, 6, 8, 768], - SIG_EP_PROFILE: 260, - }, - 2: { - SIG_EP_TYPE: 259, - DEV_SIG_EP_ID: 2, - SIG_EP_INPUT: [3], - SIG_EP_OUTPUT: [3, 6], - SIG_EP_PROFILE: 260, - }, - 3: { - SIG_EP_TYPE: -1, - DEV_SIG_EP_ID: 3, - SIG_EP_INPUT: [], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: -1, - }, - 4: { - SIG_EP_TYPE: -1, - DEV_SIG_EP_ID: 4, - SIG_EP_INPUT: [], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: -1, - }, - 5: { - SIG_EP_TYPE: -1, - DEV_SIG_EP_ID: 5, - SIG_EP_INPUT: [], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: -1, - }, - 6: { - SIG_EP_TYPE: -1, - DEV_SIG_EP_ID: 6, - SIG_EP_INPUT: [], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: -1, - }, - }, - DEV_SIG_ENTITIES: [], - DEV_SIG_ENT_MAP: {}, - DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0300", "2:0x0006"], SIG_MANUFACTURER: "LUMI", SIG_MODEL: "lumi.remote.b486opcn01", SIG_NODE_DESC: b"\x02@\x84_\x11\x7fd\x00\x00,d\x00\x00", - }, - { - DEV_SIG_DEV_NO: 46, SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 261, @@ -1773,17 +1954,86 @@ DEVICES = [ SIG_EP_INPUT: [0, 1, 3], SIG_EP_OUTPUT: [3, 6, 8, 768], SIG_EP_PROFILE: 260, - } + }, + 2: { + SIG_EP_TYPE: 259, + DEV_SIG_EP_ID: 2, + SIG_EP_INPUT: [3], + SIG_EP_OUTPUT: [3, 6], + SIG_EP_PROFILE: 260, + }, + 3: { + SIG_EP_TYPE: -1, + DEV_SIG_EP_ID: 3, + SIG_EP_INPUT: [], + SIG_EP_OUTPUT: [], + SIG_EP_PROFILE: -1, + }, + 4: { + SIG_EP_TYPE: -1, + DEV_SIG_EP_ID: 4, + SIG_EP_INPUT: [], + SIG_EP_OUTPUT: [], + SIG_EP_PROFILE: -1, + }, + 5: { + SIG_EP_TYPE: -1, + DEV_SIG_EP_ID: 5, + SIG_EP_INPUT: [], + SIG_EP_OUTPUT: [], + SIG_EP_PROFILE: -1, + }, + 6: { + SIG_EP_TYPE: -1, + DEV_SIG_EP_ID: 6, + SIG_EP_INPUT: [], + SIG_EP_OUTPUT: [], + SIG_EP_PROFILE: -1, + }, }, - DEV_SIG_ENTITIES: [], - DEV_SIG_ENT_MAP: {}, - DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0300"], + DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0300", "2:0x0006"], + DEV_SIG_ENTITIES: [ + "button.lumi_lumi_remote_b486opcn01_77665544_identify", + ], + DEV_SIG_ENT_MAP: { + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b486opcn01_77665544_identify", + }, + }, + }, + { + DEV_SIG_DEV_NO: 46, SIG_MANUFACTURER: "LUMI", SIG_MODEL: "lumi.remote.b686opcn01", SIG_NODE_DESC: b"\x02@\x84_\x11\x7fd\x00\x00,d\x00\x00", + SIG_ENDPOINTS: { + 1: { + SIG_EP_TYPE: 261, + DEV_SIG_EP_ID: 1, + SIG_EP_INPUT: [0, 1, 3], + SIG_EP_OUTPUT: [3, 6, 8, 768], + SIG_EP_PROFILE: 260, + }, + }, + DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0300"], + DEV_SIG_ENTITIES: [ + "button.lumi_lumi_remote_b686opcn01_77665544_identify", + ], + DEV_SIG_ENT_MAP: { + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b686opcn01_77665544_identify", + }, + }, }, { DEV_SIG_DEV_NO: 47, + SIG_MANUFACTURER: "LUMI", + SIG_MODEL: "lumi.remote.b686opcn01", + SIG_NODE_DESC: b"\x02@\x84_\x11\x7fd\x00\x00,d\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 261, @@ -1828,15 +2078,23 @@ DEVICES = [ SIG_EP_PROFILE: None, }, }, - DEV_SIG_ENTITIES: [], - DEV_SIG_ENT_MAP: {}, DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0300", "2:0x0006"], - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.remote.b686opcn01", - SIG_NODE_DESC: b"\x02@\x84_\x11\x7fd\x00\x00,d\x00\x00", + DEV_SIG_ENTITIES: [ + "button.lumi_lumi_remote_b686opcn01_77665544_identify", + ], + DEV_SIG_ENT_MAP: { + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b686opcn01_77665544_identify", + }, + }, }, { DEV_SIG_DEV_NO: 48, + SIG_MANUFACTURER: "LUMI", + SIG_MODEL: "lumi.router", + SIG_NODE_DESC: b"\x01@\x8e_\x11P\xa0\x00\x00\x00\xa0\x00\x00", SIG_ENDPOINTS: { 8: { SIG_EP_TYPE: 256, @@ -1844,31 +2102,31 @@ DEVICES = [ SIG_EP_INPUT: [0, 6], SIG_EP_OUTPUT: [0, 6], SIG_EP_PROFILE: 260, - } - }, - DEV_SIG_ENTITIES: [ - "binary_sensor.lumi_lumi_router_77665544_on_off", - "light.lumi_lumi_router_77665544_on_off", - ], - DEV_SIG_ENT_MAP: { - ("binary_sensor", "00:11:22:33:44:55:66:77-8-6"): { - DEV_SIG_CHANNELS: ["on_off", "on_off"], - DEV_SIG_ENT_MAP_CLASS: "Opening", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_router_77665544_on_off", - }, - ("light", "00:11:22:33:44:55:66:77-8"): { - DEV_SIG_CHANNELS: ["on_off", "on_off"], - DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_router_77665544_on_off", }, }, DEV_SIG_EVT_CHANNELS: ["8:0x0006"], - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.router", - SIG_NODE_DESC: b"\x01@\x8e_\x11P\xa0\x00\x00\x00\xa0\x00\x00", + DEV_SIG_ENTITIES: [ + "light.lumi_lumi_router_77665544_on_off", + "binary_sensor.lumi_lumi_router_77665544_on_off", + ], + DEV_SIG_ENT_MAP: { + ("light", "00:11:22:33:44:55:66:77-8"): { + DEV_SIG_CHANNELS: ["on_off"], + DEV_SIG_ENT_MAP_CLASS: "Light", + DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_router_77665544_on_off", + }, + ("binary_sensor", "00:11:22:33:44:55:66:77-8-6"): { + DEV_SIG_CHANNELS: ["on_off"], + DEV_SIG_ENT_MAP_CLASS: "Opening", + DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_router_77665544_on_off", + }, + }, }, { DEV_SIG_DEV_NO: 49, + SIG_MANUFACTURER: "LUMI", + SIG_MODEL: "lumi.router", + SIG_NODE_DESC: b"\x01@\x8e_\x11P\xa0\x00\x00\x00\xa0\x00\x00", SIG_ENDPOINTS: { 8: { SIG_EP_TYPE: 256, @@ -1876,31 +2134,31 @@ DEVICES = [ SIG_EP_INPUT: [0, 6, 11, 17], SIG_EP_OUTPUT: [0, 6], SIG_EP_PROFILE: 260, - } - }, - DEV_SIG_ENTITIES: [ - "binary_sensor.lumi_lumi_router_77665544_on_off", - "light.lumi_lumi_router_77665544_on_off", - ], - DEV_SIG_ENT_MAP: { - ("binary_sensor", "00:11:22:33:44:55:66:77-8-6"): { - DEV_SIG_CHANNELS: ["on_off", "on_off"], - DEV_SIG_ENT_MAP_CLASS: "Opening", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_router_77665544_on_off", - }, - ("light", "00:11:22:33:44:55:66:77-8"): { - DEV_SIG_CHANNELS: ["on_off", "on_off"], - DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_router_77665544_on_off", }, }, DEV_SIG_EVT_CHANNELS: ["8:0x0006"], - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.router", - SIG_NODE_DESC: b"\x01@\x8e_\x11P\xa0\x00\x00\x00\xa0\x00\x00", + DEV_SIG_ENTITIES: [ + "light.lumi_lumi_router_77665544_on_off", + "binary_sensor.lumi_lumi_router_77665544_on_off", + ], + DEV_SIG_ENT_MAP: { + ("light", "00:11:22:33:44:55:66:77-8"): { + DEV_SIG_CHANNELS: ["on_off"], + DEV_SIG_ENT_MAP_CLASS: "Light", + DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_router_77665544_on_off", + }, + ("binary_sensor", "00:11:22:33:44:55:66:77-8-6"): { + DEV_SIG_CHANNELS: ["on_off"], + DEV_SIG_ENT_MAP_CLASS: "Opening", + DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_router_77665544_on_off", + }, + }, }, { DEV_SIG_DEV_NO: 50, + SIG_MANUFACTURER: "LUMI", + SIG_MODEL: "lumi.router", + SIG_NODE_DESC: b"\x01@\x8e_\x11P\xa0\x00\x00\x00\xa0\x00\x00", SIG_ENDPOINTS: { 8: { SIG_EP_TYPE: 256, @@ -1908,31 +2166,31 @@ DEVICES = [ SIG_EP_INPUT: [0, 6, 17], SIG_EP_OUTPUT: [0, 6], SIG_EP_PROFILE: 260, - } - }, - DEV_SIG_ENTITIES: [ - "binary_sensor.lumi_lumi_router_77665544_on_off", - "light.lumi_lumi_router_77665544_on_off", - ], - DEV_SIG_ENT_MAP: { - ("binary_sensor", "00:11:22:33:44:55:66:77-8-6"): { - DEV_SIG_CHANNELS: ["on_off", "on_off"], - DEV_SIG_ENT_MAP_CLASS: "Opening", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_router_77665544_on_off", - }, - ("light", "00:11:22:33:44:55:66:77-8"): { - DEV_SIG_CHANNELS: ["on_off", "on_off"], - DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_router_77665544_on_off", }, }, DEV_SIG_EVT_CHANNELS: ["8:0x0006"], - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.router", - SIG_NODE_DESC: b"\x01@\x8e_\x11P\xa0\x00\x00\x00\xa0\x00\x00", + DEV_SIG_ENTITIES: [ + "light.lumi_lumi_router_77665544_on_off", + "binary_sensor.lumi_lumi_router_77665544_on_off", + ], + DEV_SIG_ENT_MAP: { + ("light", "00:11:22:33:44:55:66:77-8"): { + DEV_SIG_CHANNELS: ["on_off"], + DEV_SIG_ENT_MAP_CLASS: "Light", + DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_router_77665544_on_off", + }, + ("binary_sensor", "00:11:22:33:44:55:66:77-8-6"): { + DEV_SIG_CHANNELS: ["on_off"], + DEV_SIG_ENT_MAP_CLASS: "Opening", + DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_router_77665544_on_off", + }, + }, }, { DEV_SIG_DEV_NO: 51, + SIG_MANUFACTURER: "LUMI", + SIG_MODEL: "lumi.sen_ill.mgl01", + SIG_NODE_DESC: b"\x02@\x84n\x12\x7fd\x00\x00,d\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 262, @@ -1940,23 +2198,31 @@ DEVICES = [ SIG_EP_INPUT: [0, 1, 3, 1024], SIG_EP_OUTPUT: [3], SIG_EP_PROFILE: 260, - } + }, }, - DEV_SIG_ENTITIES: ["sensor.lumi_lumi_sen_ill_mgl01_77665544_illuminance"], + DEV_SIG_EVT_CHANNELS: [], + DEV_SIG_ENTITIES: [ + "button.lumi_lumi_sen_ill_mgl01_77665544_identify", + "sensor.lumi_lumi_sen_ill_mgl01_77665544_illuminance", + ], DEV_SIG_ENT_MAP: { + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sen_ill_mgl01_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1024"): { DEV_SIG_CHANNELS: ["illuminance"], DEV_SIG_ENT_MAP_CLASS: "Illuminance", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sen_ill_mgl01_77665544_illuminance", - } + }, }, - DEV_SIG_EVT_CHANNELS: [], - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.sen_ill.mgl01", - SIG_NODE_DESC: b"\x02@\x84n\x12\x7fd\x00\x00,d\x00\x00", }, { DEV_SIG_DEV_NO: 52, + SIG_MANUFACTURER: "LUMI", + SIG_MODEL: "lumi.sensor_86sw1", + SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 24321, @@ -1980,22 +2246,29 @@ DEVICES = [ SIG_EP_PROFILE: 260, }, }, - DEV_SIG_ENTITIES: ["sensor.lumi_lumi_sensor_86sw1_77665544_power"], + DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019", "2:0x0005", "3:0x0005"], + DEV_SIG_ENTITIES: [ + "button.lumi_lumi_sensor_86sw1_77665544_identify", + "sensor.lumi_lumi_sensor_86sw1_77665544_power", + ], DEV_SIG_ENT_MAP: { + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_86sw1_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_86sw1_77665544_power", }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019", "2:0x0005", "3:0x0005"], - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.sensor_86sw1", - SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", - DEV_SIG_ZHA_QUIRK: "RemoteB186ACN01", }, { DEV_SIG_DEV_NO: 53, + SIG_MANUFACTURER: "LUMI", + SIG_MODEL: "lumi.sensor_cube.aqgl01", + SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 28417, @@ -2019,22 +2292,29 @@ DEVICES = [ SIG_EP_PROFILE: 260, }, }, - DEV_SIG_ENTITIES: ["sensor.lumi_lumi_sensor_cube_aqgl01_77665544_power"], + DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019", "2:0x0005", "3:0x0005"], + DEV_SIG_ENTITIES: [ + "button.lumi_lumi_sensor_cube_aqgl01_77665544_identify", + "sensor.lumi_lumi_sensor_cube_aqgl01_77665544_power", + ], DEV_SIG_ENT_MAP: { + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_cube_aqgl01_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_cube_aqgl01_77665544_power", }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019", "2:0x0005", "3:0x0005"], - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.sensor_cube.aqgl01", - SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", - DEV_SIG_ZHA_QUIRK: "CubeAQGL01", }, { DEV_SIG_DEV_NO: 54, + SIG_MANUFACTURER: "LUMI", + SIG_MODEL: "lumi.sensor_ht", + SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 24322, @@ -2058,12 +2338,19 @@ DEVICES = [ SIG_EP_PROFILE: 260, }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019", "2:0x0005", "3:0x0005"], DEV_SIG_ENTITIES: [ - "sensor.lumi_lumi_sensor_ht_77665544_humidity", + "button.lumi_lumi_sensor_ht_77665544_identify", "sensor.lumi_lumi_sensor_ht_77665544_power", "sensor.lumi_lumi_sensor_ht_77665544_temperature", + "sensor.lumi_lumi_sensor_ht_77665544_humidity", ], DEV_SIG_ENT_MAP: { + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_ht_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", @@ -2080,14 +2367,12 @@ DEVICES = [ DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_ht_77665544_humidity", }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019", "2:0x0005", "3:0x0005"], - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.sensor_ht", - SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", - DEV_SIG_ZHA_QUIRK: "Weather", }, { DEV_SIG_DEV_NO: 55, + SIG_MANUFACTURER: "LUMI", + SIG_MODEL: "lumi.sensor_magnet", + SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 2128, @@ -2095,13 +2380,20 @@ DEVICES = [ SIG_EP_INPUT: [0, 1, 3, 25, 65535], SIG_EP_OUTPUT: [0, 3, 4, 5, 6, 8, 25], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0006", "1:0x0008", "1:0x0019"], DEV_SIG_ENTITIES: [ - "binary_sensor.lumi_lumi_sensor_magnet_77665544_on_off", + "button.lumi_lumi_sensor_magnet_77665544_identify", "sensor.lumi_lumi_sensor_magnet_77665544_power", + "binary_sensor.lumi_lumi_sensor_magnet_77665544_on_off", ], DEV_SIG_ENT_MAP: { + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_magnet_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", @@ -2113,14 +2405,12 @@ DEVICES = [ DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_magnet_77665544_on_off", }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0006", "1:0x0008", "1:0x0019"], - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.sensor_magnet", - SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", - DEV_SIG_ZHA_QUIRK: "Magnet", }, { DEV_SIG_DEV_NO: 56, + SIG_MANUFACTURER: "LUMI", + SIG_MODEL: "lumi.sensor_magnet.aq2", + SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 24321, @@ -2128,13 +2418,20 @@ DEVICES = [ SIG_EP_INPUT: [0, 1, 3, 65535], SIG_EP_OUTPUT: [0, 4, 6, 65535], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0006"], DEV_SIG_ENTITIES: [ - "binary_sensor.lumi_lumi_sensor_magnet_aq2_77665544_on_off", + "button.lumi_lumi_sensor_magnet_aq2_77665544_identify", "sensor.lumi_lumi_sensor_magnet_aq2_77665544_power", + "binary_sensor.lumi_lumi_sensor_magnet_aq2_77665544_on_off", ], DEV_SIG_ENT_MAP: { + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_magnet_aq2_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", @@ -2146,14 +2443,12 @@ DEVICES = [ DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_magnet_aq2_77665544_on_off", }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0006"], - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.sensor_magnet.aq2", - SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", - DEV_SIG_ZHA_QUIRK: "MagnetAQ2", }, { DEV_SIG_DEV_NO: 57, + SIG_MANUFACTURER: "LUMI", + SIG_MODEL: "lumi.sensor_motion.aq2", + SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 263, @@ -2161,25 +2456,17 @@ DEVICES = [ SIG_EP_INPUT: [0, 1, 3, 1024, 1030, 1280, 65535], SIG_EP_OUTPUT: [0, 25], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "binary_sensor.lumi_lumi_sensor_motion_aq2_77665544_ias_zone", - "binary_sensor.lumi_lumi_sensor_motion_aq2_77665544_occupancy", - "sensor.lumi_lumi_sensor_motion_aq2_77665544_illuminance", + "button.lumi_lumi_sensor_motion_aq2_77665544_identify", "sensor.lumi_lumi_sensor_motion_aq2_77665544_power", + "sensor.lumi_lumi_sensor_motion_aq2_77665544_illuminance", + "binary_sensor.lumi_lumi_sensor_motion_aq2_77665544_occupancy", + "binary_sensor.lumi_lumi_sensor_motion_aq2_77665544_ias_zone", ], DEV_SIG_ENT_MAP: { - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CHANNELS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_motion_aq2_77665544_power", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1024"): { - DEV_SIG_CHANNELS: ["illuminance"], - DEV_SIG_ENT_MAP_CLASS: "Illuminance", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_motion_aq2_77665544_illuminance", - }, ("binary_sensor", "00:11:22:33:44:55:66:77-1-1030"): { DEV_SIG_CHANNELS: ["occupancy"], DEV_SIG_ENT_MAP_CLASS: "Occupancy", @@ -2190,15 +2477,28 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "IASZone", DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_motion_aq2_77665544_ias_zone", }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_motion_aq2_77665544_identify", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + DEV_SIG_CHANNELS: ["power"], + DEV_SIG_ENT_MAP_CLASS: "Battery", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_motion_aq2_77665544_power", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1024"): { + DEV_SIG_CHANNELS: ["illuminance"], + DEV_SIG_ENT_MAP_CLASS: "Illuminance", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_motion_aq2_77665544_illuminance", + }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0019"], - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.sensor_motion.aq2", - SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", - DEV_SIG_ZHA_QUIRK: "MotionAQ2", }, { DEV_SIG_DEV_NO: 58, + SIG_MANUFACTURER: "LUMI", + SIG_MODEL: "lumi.sensor_smoke", + SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 1026, @@ -2206,32 +2506,37 @@ DEVICES = [ SIG_EP_INPUT: [0, 1, 3, 12, 18, 1280], SIG_EP_OUTPUT: [25], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "binary_sensor.lumi_lumi_sensor_smoke_77665544_ias_zone", + "button.lumi_lumi_sensor_smoke_77665544_identify", "sensor.lumi_lumi_sensor_smoke_77665544_power", + "binary_sensor.lumi_lumi_sensor_smoke_77665544_ias_zone", ], DEV_SIG_ENT_MAP: { - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CHANNELS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_smoke_77665544_power", - }, ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_smoke_77665544_ias_zone", }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_smoke_77665544_identify", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + DEV_SIG_CHANNELS: ["power"], + DEV_SIG_ENT_MAP_CLASS: "Battery", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_smoke_77665544_power", + }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0019"], - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.sensor_smoke", - SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", - DEV_SIG_ZHA_QUIRK: "MijiaHoneywellSmokeDetectorSensor", }, { DEV_SIG_DEV_NO: 59, + SIG_MANUFACTURER: "LUMI", + SIG_MODEL: "lumi.sensor_switch", + SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 6, @@ -2239,24 +2544,31 @@ DEVICES = [ SIG_EP_INPUT: [0, 1, 3], SIG_EP_OUTPUT: [0, 4, 5, 6, 8, 25], SIG_EP_PROFILE: 260, - } + }, }, - DEV_SIG_ENTITIES: ["sensor.lumi_lumi_sensor_switch_77665544_power"], + DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0006", "1:0x0008", "1:0x0019"], + DEV_SIG_ENTITIES: [ + "button.lumi_lumi_sensor_switch_77665544_identify", + "sensor.lumi_lumi_sensor_switch_77665544_power", + ], DEV_SIG_ENT_MAP: { + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_switch_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_77665544_power", - } + }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0006", "1:0x0008", "1:0x0019"], - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.sensor_switch", - SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", - DEV_SIG_ZHA_QUIRK: "MijaButton", }, { DEV_SIG_DEV_NO: 60, + SIG_MANUFACTURER: "LUMI", + SIG_MODEL: "lumi.sensor_switch.aq2", + SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 6, @@ -2264,24 +2576,25 @@ DEVICES = [ SIG_EP_INPUT: [0, 1, 65535], SIG_EP_OUTPUT: [0, 4, 6, 65535], SIG_EP_PROFILE: 260, - } + }, }, - DEV_SIG_ENTITIES: ["sensor.lumi_lumi_sensor_switch_aq2_77665544_power"], + DEV_SIG_EVT_CHANNELS: ["1:0x0006"], + DEV_SIG_ENTITIES: [ + "sensor.lumi_lumi_sensor_switch_aq2_77665544_power", + ], DEV_SIG_ENT_MAP: { ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq2_77665544_power", - } + }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0006"], - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.sensor_switch.aq2", - SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", - DEV_SIG_ZHA_QUIRK: "SwitchAQ2", }, { DEV_SIG_DEV_NO: 61, + SIG_MANUFACTURER: "LUMI", + SIG_MODEL: "lumi.sensor_switch.aq3", + SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 6, @@ -2289,9 +2602,12 @@ DEVICES = [ SIG_EP_INPUT: [0, 1, 18], SIG_EP_OUTPUT: [0, 6], SIG_EP_PROFILE: 260, - } + }, }, - DEV_SIG_ENTITIES: ["sensor.lumi_lumi_sensor_switch_aq3_77665544_power"], + DEV_SIG_EVT_CHANNELS: ["1:0x0006"], + DEV_SIG_ENTITIES: [ + "sensor.lumi_lumi_sensor_switch_aq3_77665544_power", + ], DEV_SIG_ENT_MAP: { ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], @@ -2299,14 +2615,12 @@ DEVICES = [ DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq3_77665544_power", }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0006"], - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.sensor_switch.aq3", - SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", - DEV_SIG_ZHA_QUIRK: "SwitchAQ3", }, { DEV_SIG_DEV_NO: 62, + SIG_MANUFACTURER: "LUMI", + SIG_MODEL: "lumi.sensor_wleak.aq1", + SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 1026, @@ -2314,32 +2628,37 @@ DEVICES = [ SIG_EP_INPUT: [0, 1, 3, 1280], SIG_EP_OUTPUT: [25], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "binary_sensor.lumi_lumi_sensor_wleak_aq1_77665544_ias_zone", + "button.lumi_lumi_sensor_wleak_aq1_77665544_identify", "sensor.lumi_lumi_sensor_wleak_aq1_77665544_power", + "binary_sensor.lumi_lumi_sensor_wleak_aq1_77665544_ias_zone", ], DEV_SIG_ENT_MAP: { - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CHANNELS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_wleak_aq1_77665544_power", - }, ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_wleak_aq1_77665544_ias_zone", }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_wleak_aq1_77665544_identify", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + DEV_SIG_CHANNELS: ["power"], + DEV_SIG_ENT_MAP_CLASS: "Battery", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_wleak_aq1_77665544_power", + }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0019"], - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.sensor_wleak.aq1", - SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", - DEV_SIG_ZHA_QUIRK: "LeakAQ1", }, { DEV_SIG_DEV_NO: 63, + SIG_MANUFACTURER: "LUMI", + SIG_MODEL: "lumi.vibration.aq1", + SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 10, @@ -2356,12 +2675,24 @@ DEVICES = [ SIG_EP_PROFILE: 260, }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019", "2:0x0005"], DEV_SIG_ENTITIES: [ + "button.lumi_lumi_vibration_aq1_77665544_identify", + "sensor.lumi_lumi_vibration_aq1_77665544_power", "binary_sensor.lumi_lumi_vibration_aq1_77665544_ias_zone", "lock.lumi_lumi_vibration_aq1_77665544_door_lock", - "sensor.lumi_lumi_vibration_aq1_77665544_power", ], DEV_SIG_ENT_MAP: { + ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { + DEV_SIG_CHANNELS: ["ias_zone"], + DEV_SIG_ENT_MAP_CLASS: "IASZone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_vibration_aq1_77665544_ias_zone", + }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_vibration_aq1_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", @@ -2372,20 +2703,13 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ZhaDoorLock", DEV_SIG_ENT_MAP_ID: "lock.lumi_lumi_vibration_aq1_77665544_door_lock", }, - ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { - DEV_SIG_CHANNELS: ["ias_zone"], - DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_vibration_aq1_77665544_ias_zone", - }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019", "2:0x0005"], - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.vibration.aq1", - SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", - DEV_SIG_ZHA_QUIRK: "VibrationAQ1", }, { DEV_SIG_DEV_NO: 64, + SIG_MANUFACTURER: "LUMI", + SIG_MODEL: "lumi.weather", + SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 24321, @@ -2393,44 +2717,49 @@ DEVICES = [ SIG_EP_INPUT: [0, 1, 3, 1026, 1027, 1029, 65535], SIG_EP_OUTPUT: [0, 4, 65535], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "sensor.lumi_lumi_weather_77665544_humidity", + "button.lumi_lumi_weather_77665544_identify", "sensor.lumi_lumi_weather_77665544_power", "sensor.lumi_lumi_weather_77665544_pressure", "sensor.lumi_lumi_weather_77665544_temperature", + "sensor.lumi_lumi_weather_77665544_humidity", ], DEV_SIG_ENT_MAP: { + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_weather_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_77665544_power", }, - ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { - DEV_SIG_CHANNELS: ["temperature"], - DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_77665544_temperature", - }, ("sensor", "00:11:22:33:44:55:66:77-1-1027"): { DEV_SIG_CHANNELS: ["pressure"], DEV_SIG_ENT_MAP_CLASS: "Pressure", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_77665544_pressure", }, + ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { + DEV_SIG_CHANNELS: ["temperature"], + DEV_SIG_ENT_MAP_CLASS: "Temperature", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_77665544_temperature", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1029"): { DEV_SIG_CHANNELS: ["humidity"], DEV_SIG_ENT_MAP_CLASS: "Humidity", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_77665544_humidity", }, }, - DEV_SIG_EVT_CHANNELS: [], - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.weather", - SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", - DEV_SIG_ZHA_QUIRK: "Weather", }, { DEV_SIG_DEV_NO: 65, + SIG_MANUFACTURER: "NYCE", + SIG_MODEL: "3010", + SIG_NODE_DESC: b"\x02@\x80\xb9\x10RR\x00\x00\x00R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 1026, @@ -2438,31 +2767,37 @@ DEVICES = [ SIG_EP_INPUT: [0, 1, 3, 32, 1280], SIG_EP_OUTPUT: [], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "binary_sensor.nyce_3010_77665544_ias_zone", + "button.nyce_3010_77665544_identify", "sensor.nyce_3010_77665544_power", + "binary_sensor.nyce_3010_77665544_ias_zone", ], DEV_SIG_ENT_MAP: { - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CHANNELS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.nyce_3010_77665544_power", - }, ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", DEV_SIG_ENT_MAP_ID: "binary_sensor.nyce_3010_77665544_ias_zone", }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.nyce_3010_77665544_identify", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + DEV_SIG_CHANNELS: ["power"], + DEV_SIG_ENT_MAP_CLASS: "Battery", + DEV_SIG_ENT_MAP_ID: "sensor.nyce_3010_77665544_power", + }, }, - DEV_SIG_EVT_CHANNELS: [], - SIG_MANUFACTURER: "NYCE", - SIG_MODEL: "3010", - SIG_NODE_DESC: b"\x02@\x80\xb9\x10RR\x00\x00\x00R\x00\x00", }, { DEV_SIG_DEV_NO: 66, + SIG_MANUFACTURER: "NYCE", + SIG_MODEL: "3014", + SIG_NODE_DESC: b"\x02@\x80\xb9\x10RR\x00\x00\x00R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 1026, @@ -2470,31 +2805,37 @@ DEVICES = [ SIG_EP_INPUT: [0, 1, 3, 32, 1280], SIG_EP_OUTPUT: [], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "binary_sensor.nyce_3014_77665544_ias_zone", + "button.nyce_3014_77665544_identify", "sensor.nyce_3014_77665544_power", + "binary_sensor.nyce_3014_77665544_ias_zone", ], DEV_SIG_ENT_MAP: { - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CHANNELS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.nyce_3014_77665544_power", - }, ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", DEV_SIG_ENT_MAP_ID: "binary_sensor.nyce_3014_77665544_ias_zone", }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.nyce_3014_77665544_identify", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + DEV_SIG_CHANNELS: ["power"], + DEV_SIG_ENT_MAP_CLASS: "Battery", + DEV_SIG_ENT_MAP_ID: "sensor.nyce_3014_77665544_power", + }, }, - DEV_SIG_EVT_CHANNELS: [], - SIG_MANUFACTURER: "NYCE", - SIG_MODEL: "3014", - SIG_NODE_DESC: b"\x02@\x80\xb9\x10RR\x00\x00\x00R\x00\x00", }, { DEV_SIG_DEV_NO: 67, + SIG_MANUFACTURER: None, + SIG_MODEL: None, + SIG_NODE_DESC: b"\x10@\x0f5\x11Y=\x00@\x00=\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 5, @@ -2511,15 +2852,15 @@ DEVICES = [ SIG_EP_PROFILE: 41440, }, }, + DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: ["1:0x0019"], DEV_SIG_ENT_MAP: {}, - DEV_SIG_EVT_CHANNELS: [], - SIG_MANUFACTURER: None, - SIG_MODEL: None, - SIG_NODE_DESC: b"\x10@\x0f5\x11Y=\x00@\x00=\x00\x00", }, { DEV_SIG_DEV_NO: 68, + SIG_MANUFACTURER: None, + SIG_MODEL: None, + SIG_NODE_DESC: b"\x00@\x8f\xcd\xabR\x80\x00\x00\x00\x80\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 48879, @@ -2527,17 +2868,17 @@ DEVICES = [ SIG_EP_INPUT: [], SIG_EP_OUTPUT: [1280], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [], DEV_SIG_ENT_MAP: {}, - DEV_SIG_EVT_CHANNELS: [], - SIG_MANUFACTURER: None, - SIG_MODEL: None, - SIG_NODE_DESC: b"\x00@\x8f\xcd\xabR\x80\x00\x00\x00\x80\x00\x00", }, { DEV_SIG_DEV_NO: 69, + SIG_MANUFACTURER: "OSRAM", + SIG_MODEL: "LIGHTIFY A19 RGBW", + SIG_NODE_DESC: b"\x01@\x8e\xaa\xbb@\x00\x00\x00\x00\x00\x00\x03", SIG_ENDPOINTS: { 3: { SIG_EP_TYPE: 258, @@ -2545,26 +2886,31 @@ DEVICES = [ SIG_EP_INPUT: [0, 3, 4, 5, 6, 8, 768, 64527], SIG_EP_OUTPUT: [25], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["3:0x0019"], DEV_SIG_ENTITIES: [ - "light.osram_lightify_a19_rgbw_77665544_level_light_color_on_off" + "button.osram_lightify_a19_rgbw_77665544_identify", + "light.osram_lightify_a19_rgbw_77665544_level_light_color_on_off", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-3"): { - DEV_SIG_CHANNELS: ["level", "light_color", "on_off"], + DEV_SIG_CHANNELS: ["on_off", "level", "light_color"], DEV_SIG_ENT_MAP_CLASS: "Light", DEV_SIG_ENT_MAP_ID: "light.osram_lightify_a19_rgbw_77665544_level_light_color_on_off", - } + }, + ("button", "00:11:22:33:44:55:66:77-3-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.osram_lightify_a19_rgbw_77665544_identify", + }, }, - DEV_SIG_EVT_CHANNELS: ["3:0x0019"], - SIG_MANUFACTURER: "OSRAM", - SIG_MODEL: "LIGHTIFY A19 RGBW", - SIG_NODE_DESC: b"\x01@\x8e\xaa\xbb@\x00\x00\x00\x00\x00\x00\x03", - DEV_SIG_ZHA_QUIRK: "LIGHTIFYA19RGBW", }, { DEV_SIG_DEV_NO: 70, + SIG_MANUFACTURER: "OSRAM", + SIG_MODEL: "LIGHTIFY Dimming Switch", + SIG_NODE_DESC: b"\x02@\x80\x0c\x11RR\x00\x00\x00R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 1, @@ -2572,24 +2918,31 @@ DEVICES = [ SIG_EP_INPUT: [0, 1, 3, 32, 2821], SIG_EP_OUTPUT: [3, 6, 8, 25], SIG_EP_PROFILE: 260, - } + }, }, - DEV_SIG_ENTITIES: ["sensor.osram_lightify_dimming_switch_77665544_power"], + DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0019"], + DEV_SIG_ENTITIES: [ + "button.osram_lightify_dimming_switch_77665544_identify", + "sensor.osram_lightify_dimming_switch_77665544_power", + ], DEV_SIG_ENT_MAP: { + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.osram_lightify_dimming_switch_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_dimming_switch_77665544_power", - } + }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0019"], - SIG_MANUFACTURER: "OSRAM", - SIG_MODEL: "LIGHTIFY Dimming Switch", - SIG_NODE_DESC: b"\x02@\x80\x0c\x11RR\x00\x00\x00R\x00\x00", - DEV_SIG_ZHA_QUIRK: "CentraLite3130", }, { DEV_SIG_DEV_NO: 71, + SIG_MANUFACTURER: "OSRAM", + SIG_MODEL: "LIGHTIFY Flex RGBW", + SIG_NODE_DESC: b"\x19@\x8e\xaa\xbb@\x00\x00\x00\x00\x00\x00\x03", SIG_ENDPOINTS: { 3: { SIG_EP_TYPE: 258, @@ -2597,26 +2950,31 @@ DEVICES = [ SIG_EP_INPUT: [0, 3, 4, 5, 6, 8, 768, 64527], SIG_EP_OUTPUT: [25], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["3:0x0019"], DEV_SIG_ENTITIES: [ - "light.osram_lightify_flex_rgbw_77665544_level_light_color_on_off" + "button.osram_lightify_flex_rgbw_77665544_identify", + "light.osram_lightify_flex_rgbw_77665544_level_light_color_on_off", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-3"): { - DEV_SIG_CHANNELS: ["level", "light_color", "on_off"], + DEV_SIG_CHANNELS: ["on_off", "level", "light_color"], DEV_SIG_ENT_MAP_CLASS: "Light", DEV_SIG_ENT_MAP_ID: "light.osram_lightify_flex_rgbw_77665544_level_light_color_on_off", - } + }, + ("button", "00:11:22:33:44:55:66:77-3-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.osram_lightify_flex_rgbw_77665544_identify", + }, }, - DEV_SIG_EVT_CHANNELS: ["3:0x0019"], - SIG_MANUFACTURER: "OSRAM", - SIG_MODEL: "LIGHTIFY Flex RGBW", - SIG_NODE_DESC: b"\x19@\x8e\xaa\xbb@\x00\x00\x00\x00\x00\x00\x03", - DEV_SIG_ZHA_QUIRK: "FlexRGBW", }, { DEV_SIG_DEV_NO: 72, + SIG_MANUFACTURER: "OSRAM", + SIG_MODEL: "LIGHTIFY RT Tunable White", + SIG_NODE_DESC: b"\x01@\x8e\xaa\xbb@\x00\x00\x00\x00\x00\x00\x03", SIG_ENDPOINTS: { 3: { SIG_EP_TYPE: 258, @@ -2624,9 +2982,11 @@ DEVICES = [ SIG_EP_INPUT: [0, 3, 4, 5, 6, 8, 768, 2820, 64527], SIG_EP_OUTPUT: [25], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["3:0x0019"], DEV_SIG_ENTITIES: [ + "button.osram_lightify_rt_tunable_white_77665544_identify", "light.osram_lightify_rt_tunable_white_77665544_level_light_color_on_off", "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement", "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_apparent_power", @@ -2635,10 +2995,15 @@ DEVICES = [ ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-3"): { - DEV_SIG_CHANNELS: ["level", "light_color", "on_off"], + DEV_SIG_CHANNELS: ["on_off", "level", "light_color"], DEV_SIG_ENT_MAP_CLASS: "Light", DEV_SIG_ENT_MAP_ID: "light.osram_lightify_rt_tunable_white_77665544_level_light_color_on_off", }, + ("button", "00:11:22:33:44:55:66:77-3-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.osram_lightify_rt_tunable_white_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-3-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", @@ -2660,14 +3025,12 @@ DEVICES = [ DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_rms_voltage", }, }, - DEV_SIG_EVT_CHANNELS: ["3:0x0019"], - SIG_MANUFACTURER: "OSRAM", - SIG_MODEL: "LIGHTIFY RT Tunable White", - SIG_NODE_DESC: b"\x01@\x8e\xaa\xbb@\x00\x00\x00\x00\x00\x00\x03", - DEV_SIG_ZHA_QUIRK: "A19TunableWhite", }, { DEV_SIG_DEV_NO: 73, + SIG_MANUFACTURER: "OSRAM", + SIG_MODEL: "Plug 01", + SIG_NODE_DESC: b"\x01@\x8e\xaa\xbb@\x00\x00\x00\x00\x00\x00\x03", SIG_ENDPOINTS: { 3: { SIG_EP_TYPE: 16, @@ -2675,9 +3038,11 @@ DEVICES = [ SIG_EP_INPUT: [0, 3, 4, 5, 6, 2820, 4096, 64527], SIG_EP_OUTPUT: [25], SIG_EP_PROFILE: 49246, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["3:0x0019"], DEV_SIG_ENTITIES: [ + "button.osram_plug_01_77665544_identify", "sensor.osram_plug_01_77665544_electrical_measurement", "sensor.osram_plug_01_77665544_electrical_measurement_apparent_power", "sensor.osram_plug_01_77665544_electrical_measurement_rms_current", @@ -2690,6 +3055,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Switch", DEV_SIG_ENT_MAP_ID: "switch.osram_plug_01_77665544_on_off", }, + ("button", "00:11:22:33:44:55:66:77-3-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.osram_plug_01_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-3-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", @@ -2711,13 +3081,12 @@ DEVICES = [ DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_77665544_electrical_measurement_rms_voltage", }, }, - DEV_SIG_EVT_CHANNELS: ["3:0x0019"], - SIG_MANUFACTURER: "OSRAM", - SIG_MODEL: "Plug 01", - SIG_NODE_DESC: b"\x01@\x8e\xaa\xbb@\x00\x00\x00\x00\x00\x00\x03", }, { DEV_SIG_DEV_NO: 74, + SIG_MANUFACTURER: "OSRAM", + SIG_MODEL: "Switch 4x-LIGHTIFY", + SIG_NODE_DESC: b"\x02@\x80\x0c\x11RR\x00\x00\x00R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 2064, @@ -2762,14 +3131,6 @@ DEVICES = [ SIG_EP_PROFILE: 260, }, }, - DEV_SIG_ENTITIES: ["sensor.osram_switch_4x_lightify_77665544_power"], - DEV_SIG_ENT_MAP: { - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CHANNELS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.osram_switch_4x_lightify_77665544_power", - } - }, DEV_SIG_EVT_CHANNELS: [ "1:0x0005", "1:0x0006", @@ -2797,13 +3158,22 @@ DEVICES = [ "6:0x0008", "6:0x0300", ], - SIG_MANUFACTURER: "OSRAM", - SIG_MODEL: "Switch 4x-LIGHTIFY", - SIG_NODE_DESC: b"\x02@\x80\x0c\x11RR\x00\x00\x00R\x00\x00", - DEV_SIG_ZHA_QUIRK: "LightifyX4", + DEV_SIG_ENTITIES: [ + "sensor.osram_switch_4x_lightify_77665544_power", + ], + DEV_SIG_ENT_MAP: { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + DEV_SIG_CHANNELS: ["power"], + DEV_SIG_ENT_MAP_CLASS: "Battery", + DEV_SIG_ENT_MAP_ID: "sensor.osram_switch_4x_lightify_77665544_power", + }, + }, }, { DEV_SIG_DEV_NO: 75, + SIG_MANUFACTURER: "Philips", + SIG_MODEL: "RWL020", + SIG_NODE_DESC: b"\x02@\x80\x0b\x10G-\x00\x00\x00-\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 2096, @@ -2820,27 +3190,35 @@ DEVICES = [ SIG_EP_PROFILE: 260, }, }, - DEV_SIG_ENTITIES: ["sensor.philips_rwl020_77665544_power"], + DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0006", "1:0x0008", "2:0x0019"], + DEV_SIG_ENTITIES: [ + "button.philips_rwl020_77665544_identify", + "sensor.philips_rwl020_77665544_power", + "binary_sensor.philips_rwl020_77665544_binary_input", + ], DEV_SIG_ENT_MAP: { - ("sensor", "00:11:22:33:44:55:66:77-2-1"): { - DEV_SIG_CHANNELS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.philips_rwl020_77665544_power", - }, ("binary_sensor", "00:11:22:33:44:55:66:77-2-15"): { DEV_SIG_CHANNELS: ["binary_input"], DEV_SIG_ENT_MAP_CLASS: "BinaryInput", DEV_SIG_ENT_MAP_ID: "binary_sensor.philips_rwl020_77665544_binary_input", }, + ("button", "00:11:22:33:44:55:66:77-2-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.philips_rwl020_77665544_identify", + }, + ("sensor", "00:11:22:33:44:55:66:77-2-1"): { + DEV_SIG_CHANNELS: ["power"], + DEV_SIG_ENT_MAP_CLASS: "Battery", + DEV_SIG_ENT_MAP_ID: "sensor.philips_rwl020_77665544_power", + }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0006", "1:0x0008", "2:0x0019"], - SIG_MANUFACTURER: "Philips", - SIG_MODEL: "RWL020", - SIG_NODE_DESC: b"\x02@\x80\x0b\x10G-\x00\x00\x00-\x00\x00", - DEV_SIG_ZHA_QUIRK: "PhilipsRWL021", }, { DEV_SIG_DEV_NO: 76, + SIG_MANUFACTURER: "Samjin", + SIG_MODEL: "button", + SIG_NODE_DESC: b"\x02@\x80A\x12RR\x00\x00,R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 1026, @@ -2848,14 +3226,26 @@ DEVICES = [ SIG_EP_INPUT: [0, 1, 3, 32, 1026, 1280, 2821], SIG_EP_OUTPUT: [3, 25], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "binary_sensor.samjin_button_77665544_ias_zone", + "button.samjin_button_77665544_identify", "sensor.samjin_button_77665544_power", "sensor.samjin_button_77665544_temperature", + "binary_sensor.samjin_button_77665544_ias_zone", ], DEV_SIG_ENT_MAP: { + ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { + DEV_SIG_CHANNELS: ["ias_zone"], + DEV_SIG_ENT_MAP_CLASS: "IASZone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.samjin_button_77665544_ias_zone", + }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.samjin_button_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", @@ -2866,20 +3256,13 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Temperature", DEV_SIG_ENT_MAP_ID: "sensor.samjin_button_77665544_temperature", }, - ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { - DEV_SIG_CHANNELS: ["ias_zone"], - DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.samjin_button_77665544_ias_zone", - }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0019"], - SIG_MANUFACTURER: "Samjin", - SIG_MODEL: "button", - SIG_NODE_DESC: b"\x02@\x80A\x12RR\x00\x00,R\x00\x00", - DEV_SIG_ZHA_QUIRK: "SamjinButton", }, { DEV_SIG_DEV_NO: 77, + SIG_MANUFACTURER: "Samjin", + SIG_MODEL: "multi", + SIG_NODE_DESC: b"\x02@\x80A\x12RR\x00\x00,R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 1026, @@ -2887,15 +3270,26 @@ DEVICES = [ SIG_EP_INPUT: [0, 1, 3, 32, 1026, 1280, 64514], SIG_EP_OUTPUT: [3, 25], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "binary_sensor.samjin_multi_77665544_ias_zone", - "binary_sensor.samjin_multi_77665544_manufacturer_specific", + "button.samjin_multi_77665544_identify", "sensor.samjin_multi_77665544_power", "sensor.samjin_multi_77665544_temperature", + "binary_sensor.samjin_multi_77665544_ias_zone", ], DEV_SIG_ENT_MAP: { + ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { + DEV_SIG_CHANNELS: ["ias_zone"], + DEV_SIG_ENT_MAP_CLASS: "IASZone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.samjin_multi_77665544_ias_zone", + }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.samjin_multi_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", @@ -2906,20 +3300,13 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Temperature", DEV_SIG_ENT_MAP_ID: "sensor.samjin_multi_77665544_temperature", }, - ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { - DEV_SIG_CHANNELS: ["ias_zone"], - DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.samjin_multi_77665544_ias_zone", - }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0019"], - SIG_MANUFACTURER: "Samjin", - SIG_MODEL: "multi", - SIG_NODE_DESC: b"\x02@\x80A\x12RR\x00\x00,R\x00\x00", - DEV_SIG_ZHA_QUIRK: "SmartthingsMultiPurposeSensor", }, { DEV_SIG_DEV_NO: 78, + SIG_MANUFACTURER: "Samjin", + SIG_MODEL: "water", + SIG_NODE_DESC: b"\x02@\x80A\x12RR\x00\x00,R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 1026, @@ -2927,14 +3314,26 @@ DEVICES = [ SIG_EP_INPUT: [0, 1, 3, 32, 1026, 1280], SIG_EP_OUTPUT: [3, 25], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "binary_sensor.samjin_water_77665544_ias_zone", + "button.samjin_water_77665544_identify", "sensor.samjin_water_77665544_power", "sensor.samjin_water_77665544_temperature", + "binary_sensor.samjin_water_77665544_ias_zone", ], DEV_SIG_ENT_MAP: { + ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { + DEV_SIG_CHANNELS: ["ias_zone"], + DEV_SIG_ENT_MAP_CLASS: "IASZone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.samjin_water_77665544_ias_zone", + }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.samjin_water_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", @@ -2945,19 +3344,13 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Temperature", DEV_SIG_ENT_MAP_ID: "sensor.samjin_water_77665544_temperature", }, - ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { - DEV_SIG_CHANNELS: ["ias_zone"], - DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.samjin_water_77665544_ias_zone", - }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0019"], - SIG_MANUFACTURER: "Samjin", - SIG_MODEL: "water", - SIG_NODE_DESC: b"\x02@\x80A\x12RR\x00\x00,R\x00\x00", }, { DEV_SIG_DEV_NO: 79, + SIG_MANUFACTURER: "Securifi Ltd.", + SIG_MODEL: None, + SIG_NODE_DESC: b"\x01@\x8e\x02\x10RR\x00\x00\x00R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 0, @@ -2965,9 +3358,11 @@ DEVICES = [ SIG_EP_INPUT: [0, 1, 3, 4, 5, 6, 2820, 2821], SIG_EP_OUTPUT: [0, 1, 3, 4, 5, 6, 25, 2820, 2821], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0006", "1:0x0019"], DEV_SIG_ENTITIES: [ + "button.securifi_ltd_unk_model_77665544_identify", "sensor.securifi_ltd_unk_model_77665544_electrical_measurement", "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_apparent_power", "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_rms_current", @@ -2975,10 +3370,10 @@ DEVICES = [ "switch.securifi_ltd_unk_model_77665544_on_off", ], DEV_SIG_ENT_MAP: { - ("switch", "00:11:22:33:44:55:66:77-1-6"): { - DEV_SIG_CHANNELS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.securifi_ltd_unk_model_77665544_on_off", + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.securifi_ltd_unk_model_77665544_identify", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], @@ -3000,14 +3395,18 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_rms_voltage", }, + ("switch", "00:11:22:33:44:55:66:77-1-6"): { + DEV_SIG_CHANNELS: ["on_off"], + DEV_SIG_ENT_MAP_CLASS: "Switch", + DEV_SIG_ENT_MAP_ID: "switch.securifi_ltd_unk_model_77665544_on_off", + }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0006", "1:0x0019"], - SIG_MANUFACTURER: "Securifi Ltd.", - SIG_MODEL: None, - SIG_NODE_DESC: b"\x01@\x8e\x02\x10RR\x00\x00\x00R\x00\x00", }, { DEV_SIG_DEV_NO: 80, + SIG_MANUFACTURER: "Sercomm Corp.", + SIG_MODEL: "SZ-DWS04N_SF", + SIG_NODE_DESC: b"\x02@\x801\x11R\xff\x00\x00\x00\xff\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 1026, @@ -3015,14 +3414,26 @@ DEVICES = [ SIG_EP_INPUT: [0, 1, 3, 32, 1026, 1280, 2821], SIG_EP_OUTPUT: [3, 25], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "binary_sensor.sercomm_corp_sz_dws04n_sf_77665544_ias_zone", + "button.sercomm_corp_sz_dws04n_sf_77665544_identify", "sensor.sercomm_corp_sz_dws04n_sf_77665544_power", "sensor.sercomm_corp_sz_dws04n_sf_77665544_temperature", + "binary_sensor.sercomm_corp_sz_dws04n_sf_77665544_ias_zone", ], DEV_SIG_ENT_MAP: { + ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { + DEV_SIG_CHANNELS: ["ias_zone"], + DEV_SIG_ENT_MAP_CLASS: "IASZone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.sercomm_corp_sz_dws04n_sf_77665544_ias_zone", + }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.sercomm_corp_sz_dws04n_sf_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", @@ -3033,19 +3444,13 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Temperature", DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_dws04n_sf_77665544_temperature", }, - ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { - DEV_SIG_CHANNELS: ["ias_zone"], - DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.sercomm_corp_sz_dws04n_sf_77665544_ias_zone", - }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0019"], - SIG_MANUFACTURER: "Sercomm Corp.", - SIG_MODEL: "SZ-DWS04N_SF", - SIG_NODE_DESC: b"\x02@\x801\x11R\xff\x00\x00\x00\xff\x00\x00", }, { DEV_SIG_DEV_NO: 81, + SIG_MANUFACTURER: "Sercomm Corp.", + SIG_MODEL: "SZ-ESW01", + SIG_NODE_DESC: b"\x01@\x8e1\x11RR\x00\x00\x00R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 256, @@ -3062,7 +3467,9 @@ DEVICES = [ SIG_EP_PROFILE: 260, }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0019", "2:0x0006"], DEV_SIG_ENTITIES: [ + "button.sercomm_corp_sz_esw01_77665544_identify", "light.sercomm_corp_sz_esw01_77665544_on_off", "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement", "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_apparent_power", @@ -3077,15 +3484,10 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Light", DEV_SIG_ENT_MAP_ID: "light.sercomm_corp_sz_esw01_77665544_on_off", }, - ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { - DEV_SIG_CHANNELS: ["smartenergy_metering"], - DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_smartenergy_metering", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { - DEV_SIG_CHANNELS: ["smartenergy_metering"], - DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_smartenergy_metering_summation_delivered", + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.sercomm_corp_sz_esw01_77665544_identify", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], @@ -3107,14 +3509,23 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_rms_voltage", }, + ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { + DEV_SIG_CHANNELS: ["smartenergy_metering"], + DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_smartenergy_metering", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { + DEV_SIG_CHANNELS: ["smartenergy_metering"], + DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_smartenergy_metering_summation_delivered", + }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0019", "2:0x0006"], - SIG_MANUFACTURER: "Sercomm Corp.", - SIG_MODEL: "SZ-ESW01", - SIG_NODE_DESC: b"\x01@\x8e1\x11RR\x00\x00\x00R\x00\x00", }, { DEV_SIG_DEV_NO: 82, + SIG_MANUFACTURER: "Sercomm Corp.", + SIG_MODEL: "SZ-PIR04", + SIG_NODE_DESC: b"\x02@\x801\x11RR\x00\x00\x00R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 1026, @@ -3122,15 +3533,27 @@ DEVICES = [ SIG_EP_INPUT: [0, 1, 3, 32, 1024, 1026, 1280, 2821], SIG_EP_OUTPUT: [25], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "binary_sensor.sercomm_corp_sz_pir04_77665544_ias_zone", - "sensor.sercomm_corp_sz_pir04_77665544_illuminance", + "button.sercomm_corp_sz_pir04_77665544_identify", "sensor.sercomm_corp_sz_pir04_77665544_power", + "sensor.sercomm_corp_sz_pir04_77665544_illuminance", "sensor.sercomm_corp_sz_pir04_77665544_temperature", + "binary_sensor.sercomm_corp_sz_pir04_77665544_ias_zone", ], DEV_SIG_ENT_MAP: { + ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { + DEV_SIG_CHANNELS: ["ias_zone"], + DEV_SIG_ENT_MAP_CLASS: "IASZone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.sercomm_corp_sz_pir04_77665544_ias_zone", + }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.sercomm_corp_sz_pir04_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", @@ -3146,19 +3569,13 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Temperature", DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_pir04_77665544_temperature", }, - ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { - DEV_SIG_CHANNELS: ["ias_zone"], - DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.sercomm_corp_sz_pir04_77665544_ias_zone", - }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0019"], - SIG_MANUFACTURER: "Sercomm Corp.", - SIG_MODEL: "SZ-PIR04", - SIG_NODE_DESC: b"\x02@\x801\x11RR\x00\x00\x00R\x00\x00", }, { DEV_SIG_DEV_NO: 83, + SIG_MANUFACTURER: "Sinope Technologies", + SIG_MODEL: "RM3250ZB", + SIG_NODE_DESC: b"\x11@\x8e\x9c\x11G+\x00\x00*+\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 2, @@ -3166,9 +3583,11 @@ DEVICES = [ SIG_EP_INPUT: [0, 3, 4, 5, 6, 2820, 2821, 65281], SIG_EP_OUTPUT: [3, 4, 25], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ + "button.sinope_technologies_rm3250zb_77665544_identify", "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement", "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_apparent_power", "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_rms_current", @@ -3176,10 +3595,10 @@ DEVICES = [ "switch.sinope_technologies_rm3250zb_77665544_on_off", ], DEV_SIG_ENT_MAP: { - ("switch", "00:11:22:33:44:55:66:77-1-6"): { - DEV_SIG_CHANNELS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.sinope_technologies_rm3250zb_77665544_on_off", + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.sinope_technologies_rm3250zb_77665544_identify", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], @@ -3201,14 +3620,18 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_rms_voltage", }, + ("switch", "00:11:22:33:44:55:66:77-1-6"): { + DEV_SIG_CHANNELS: ["on_off"], + DEV_SIG_ENT_MAP_CLASS: "Switch", + DEV_SIG_ENT_MAP_ID: "switch.sinope_technologies_rm3250zb_77665544_on_off", + }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0019"], - SIG_MANUFACTURER: "Sinope Technologies", - SIG_MODEL: "RM3250ZB", - SIG_NODE_DESC: b"\x11@\x8e\x9c\x11G+\x00\x00*+\x00\x00", }, { DEV_SIG_DEV_NO: 84, + SIG_MANUFACTURER: "Sinope Technologies", + SIG_MODEL: "TH1123ZB", + SIG_NODE_DESC: b"\x12@\x8c\x9c\x11G+\x00\x00\x00+\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 769, @@ -3225,26 +3648,28 @@ DEVICES = [ SIG_EP_PROFILE: 49757, }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "climate.sinope_technologies_th1123zb_77665544_thermostat", + "button.sinope_technologies_th1123zb_77665544_identify", "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement", "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_apparent_power", "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_rms_current", "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_rms_voltage", "sensor.sinope_technologies_th1123zb_77665544_temperature", "sensor.sinope_technologies_th1123zb_77665544_thermostat_hvac_action", + "climate.sinope_technologies_th1123zb_77665544_thermostat", ], DEV_SIG_ENT_MAP: { + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.sinope_technologies_th1123zb_77665544_identify", + }, ("climate", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["thermostat"], DEV_SIG_ENT_MAP_CLASS: "Thermostat", DEV_SIG_ENT_MAP_ID: "climate.sinope_technologies_th1123zb_77665544_thermostat", }, - ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { - DEV_SIG_CHANNELS: ["temperature"], - DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_temperature", - }, ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", @@ -3265,20 +3690,23 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_rms_voltage", }, + ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { + DEV_SIG_CHANNELS: ["temperature"], + DEV_SIG_ENT_MAP_CLASS: "Temperature", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_temperature", + }, ("sensor", "00:11:22:33:44:55:66:77-1-513-hvac_action"): { DEV_SIG_CHANNELS: ["thermostat"], DEV_SIG_ENT_MAP_CLASS: "SinopeHVACAction", DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_thermostat_hvac_action", }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0019"], - SIG_MANUFACTURER: "Sinope Technologies", - SIG_MODEL: "TH1123ZB", - SIG_NODE_DESC: b"\x12@\x8c\x9c\x11G+\x00\x00\x00+\x00\x00", - DEV_SIG_ZHA_QUIRK: "SinopeTechnologiesThermostat", }, { DEV_SIG_DEV_NO: 85, + SIG_MANUFACTURER: "Sinope Technologies", + SIG_MODEL: "TH1124ZB", + SIG_NODE_DESC: b"\x11@\x8e\x9c\x11G+\x00\x00\x00+\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 769, @@ -3295,7 +3723,9 @@ DEVICES = [ SIG_EP_PROFILE: 49757, }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ + "button.sinope_technologies_th1124zb_77665544_identify", "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement", "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_apparent_power", "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_rms_current", @@ -3305,6 +3735,11 @@ DEVICES = [ "climate.sinope_technologies_th1124zb_77665544_thermostat", ], DEV_SIG_ENT_MAP: { + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.sinope_technologies_th1124zb_77665544_identify", + }, ("climate", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["thermostat"], DEV_SIG_ENT_MAP_CLASS: "Thermostat", @@ -3341,14 +3776,12 @@ DEVICES = [ DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_rms_voltage", }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0019"], - SIG_MANUFACTURER: "Sinope Technologies", - SIG_MODEL: "TH1124ZB", - SIG_NODE_DESC: b"\x11@\x8e\x9c\x11G+\x00\x00\x00+\x00\x00", - DEV_SIG_ZHA_QUIRK: "SinopeTechnologiesThermostat", }, { DEV_SIG_DEV_NO: 86, + SIG_MANUFACTURER: "SmartThings", + SIG_MODEL: "outletv4", + SIG_NODE_DESC: b"\x01@\x8e\n\x11RR\x00\x00\x00R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 2, @@ -3356,20 +3789,28 @@ DEVICES = [ SIG_EP_INPUT: [0, 3, 4, 5, 6, 9, 15, 2820], SIG_EP_OUTPUT: [25], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ + "button.smartthings_outletv4_77665544_identify", "sensor.smartthings_outletv4_77665544_electrical_measurement", "sensor.smartthings_outletv4_77665544_electrical_measurement_apparent_power", "sensor.smartthings_outletv4_77665544_electrical_measurement_rms_current", "sensor.smartthings_outletv4_77665544_electrical_measurement_rms_voltage", + "binary_sensor.smartthings_outletv4_77665544_binary_input", "switch.smartthings_outletv4_77665544_on_off", ], DEV_SIG_ENT_MAP: { - ("switch", "00:11:22:33:44:55:66:77-1-6"): { - DEV_SIG_CHANNELS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.smartthings_outletv4_77665544_on_off", + ("binary_sensor", "00:11:22:33:44:55:66:77-1-15"): { + DEV_SIG_CHANNELS: ["binary_input"], + DEV_SIG_ENT_MAP_CLASS: "BinaryInput", + DEV_SIG_ENT_MAP_ID: "binary_sensor.smartthings_outletv4_77665544_binary_input", + }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.smartthings_outletv4_77665544_identify", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], @@ -3391,19 +3832,18 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_77665544_electrical_measurement_rms_voltage", }, - ("binary_sensor", "00:11:22:33:44:55:66:77-1-15"): { - DEV_SIG_CHANNELS: ["binary_input"], - DEV_SIG_ENT_MAP_CLASS: "BinaryInput", - DEV_SIG_ENT_MAP_ID: "binary_sensor.smartthings_outletv4_77665544_binary_input", + ("switch", "00:11:22:33:44:55:66:77-1-6"): { + DEV_SIG_CHANNELS: ["on_off"], + DEV_SIG_ENT_MAP_CLASS: "Switch", + DEV_SIG_ENT_MAP_ID: "switch.smartthings_outletv4_77665544_on_off", }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0019"], - SIG_MANUFACTURER: "SmartThings", - SIG_MODEL: "outletv4", - SIG_NODE_DESC: b"\x01@\x8e\n\x11RR\x00\x00\x00R\x00\x00", }, { DEV_SIG_DEV_NO: 87, + SIG_MANUFACTURER: "SmartThings", + SIG_MODEL: "tagv4", + SIG_NODE_DESC: b"\x02@\x80\n\x11RR\x00\x00\x00R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 32768, @@ -3411,9 +3851,14 @@ DEVICES = [ SIG_EP_INPUT: [0, 1, 3, 15, 32], SIG_EP_OUTPUT: [3, 25], SIG_EP_PROFILE: 260, - } + }, }, - DEV_SIG_ENTITIES: ["device_tracker.smartthings_tagv4_77665544_power"], + DEV_SIG_EVT_CHANNELS: ["1:0x0019"], + DEV_SIG_ENTITIES: [ + "button.smartthings_tagv4_77665544_identify", + "device_tracker.smartthings_tagv4_77665544_power", + "binary_sensor.smartthings_tagv4_77665544_binary_input", + ], DEV_SIG_ENT_MAP: { ("device_tracker", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["power"], @@ -3425,15 +3870,18 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "BinaryInput", DEV_SIG_ENT_MAP_ID: "binary_sensor.smartthings_tagv4_77665544_binary_input", }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.smartthings_tagv4_77665544_identify", + }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0019"], - SIG_MANUFACTURER: "SmartThings", - SIG_MODEL: "tagv4", - SIG_NODE_DESC: b"\x02@\x80\n\x11RR\x00\x00\x00R\x00\x00", - DEV_SIG_ZHA_QUIRK: "SmartThingsTagV4", }, { DEV_SIG_DEV_NO: 88, + SIG_MANUFACTURER: "Third Reality, Inc", + SIG_MODEL: "3RSS007Z", + SIG_NODE_DESC: b"\x02@\x803\x12\x7fd\x00\x00,d\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 2, @@ -3441,23 +3889,31 @@ DEVICES = [ SIG_EP_INPUT: [0, 3, 4, 5, 6, 25], SIG_EP_OUTPUT: [], SIG_EP_PROFILE: 260, - } + }, }, - DEV_SIG_ENTITIES: ["switch.third_reality_inc_3rss007z_77665544_on_off"], + DEV_SIG_EVT_CHANNELS: [], + DEV_SIG_ENTITIES: [ + "button.third_reality_inc_3rss007z_77665544_identify", + "switch.third_reality_inc_3rss007z_77665544_on_off", + ], DEV_SIG_ENT_MAP: { + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.third_reality_inc_3rss007z_77665544_identify", + }, ("switch", "00:11:22:33:44:55:66:77-1-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", DEV_SIG_ENT_MAP_ID: "switch.third_reality_inc_3rss007z_77665544_on_off", - } + }, }, - DEV_SIG_EVT_CHANNELS: [], - SIG_MANUFACTURER: "Third Reality, Inc", - SIG_MODEL: "3RSS007Z", - SIG_NODE_DESC: b"\x02@\x803\x12\x7fd\x00\x00,d\x00\x00", }, { DEV_SIG_DEV_NO: 89, + SIG_MANUFACTURER: "Third Reality, Inc", + SIG_MODEL: "3RSS008Z", + SIG_NODE_DESC: b"\x02@\x803\x12\x7fd\x00\x00,d\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 2, @@ -3465,13 +3921,20 @@ DEVICES = [ SIG_EP_INPUT: [0, 1, 3, 4, 5, 6, 25], SIG_EP_OUTPUT: [1], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ + "button.third_reality_inc_3rss008z_77665544_identify", "sensor.third_reality_inc_3rss008z_77665544_power", "switch.third_reality_inc_3rss008z_77665544_on_off", ], DEV_SIG_ENT_MAP: { + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.third_reality_inc_3rss008z_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", @@ -3483,14 +3946,12 @@ DEVICES = [ DEV_SIG_ENT_MAP_ID: "switch.third_reality_inc_3rss008z_77665544_on_off", }, }, - DEV_SIG_EVT_CHANNELS: [], - SIG_MANUFACTURER: "Third Reality, Inc", - SIG_MODEL: "3RSS008Z", - SIG_NODE_DESC: b"\x02@\x803\x12\x7fd\x00\x00,d\x00\x00", - DEV_SIG_ZHA_QUIRK: "Switch", }, { DEV_SIG_DEV_NO: 90, + SIG_MANUFACTURER: "Visonic", + SIG_MODEL: "MCT-340 E", + SIG_NODE_DESC: b"\x02@\x80\x11\x10RR\x00\x00\x00R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 1026, @@ -3498,14 +3959,26 @@ DEVICES = [ SIG_EP_INPUT: [0, 1, 3, 32, 1026, 1280, 2821], SIG_EP_OUTPUT: [25], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "binary_sensor.visonic_mct_340_e_77665544_ias_zone", + "button.visonic_mct_340_e_77665544_identify", "sensor.visonic_mct_340_e_77665544_power", "sensor.visonic_mct_340_e_77665544_temperature", + "binary_sensor.visonic_mct_340_e_77665544_ias_zone", ], DEV_SIG_ENT_MAP: { + ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { + DEV_SIG_CHANNELS: ["ias_zone"], + DEV_SIG_ENT_MAP_CLASS: "IASZone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.visonic_mct_340_e_77665544_ias_zone", + }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.visonic_mct_340_e_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", @@ -3516,20 +3989,13 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Temperature", DEV_SIG_ENT_MAP_ID: "sensor.visonic_mct_340_e_77665544_temperature", }, - ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { - DEV_SIG_CHANNELS: ["ias_zone"], - DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.visonic_mct_340_e_77665544_ias_zone", - }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0019"], - SIG_MANUFACTURER: "Visonic", - SIG_MODEL: "MCT-340 E", - SIG_NODE_DESC: b"\x02@\x80\x11\x10RR\x00\x00\x00R\x00\x00", - DEV_SIG_ZHA_QUIRK: "MCT340E", }, { DEV_SIG_DEV_NO: 91, + SIG_MANUFACTURER: "Zen Within", + SIG_MODEL: "Zen-01", + SIG_NODE_DESC: b"\x02@\x80X\x11R\x80\x00\x00\x00\x80\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 769, @@ -3537,37 +4003,43 @@ DEVICES = [ SIG_EP_INPUT: [0, 1, 3, 4, 5, 32, 513, 514, 516, 2821], SIG_EP_OUTPUT: [10, 25], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "climate.zen_within_zen_01_77665544_fan_thermostat", - "sensor.zen_within_zen_01_77665544_thermostat_hvac_action", + "button.zen_within_zen_01_77665544_identify", "sensor.zen_within_zen_01_77665544_power", + "sensor.zen_within_zen_01_77665544_thermostat_hvac_action", + "climate.zen_within_zen_01_77665544_fan_thermostat", ], DEV_SIG_ENT_MAP: { - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CHANNELS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_77665544_power", + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.zen_within_zen_01_77665544_identify", }, ("climate", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["thermostat", "fan"], DEV_SIG_ENT_MAP_CLASS: "ZenWithinThermostat", DEV_SIG_ENT_MAP_ID: "climate.zen_within_zen_01_77665544_fan_thermostat", }, + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + DEV_SIG_CHANNELS: ["power"], + DEV_SIG_ENT_MAP_CLASS: "Battery", + DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_77665544_power", + }, ("sensor", "00:11:22:33:44:55:66:77-1-513-hvac_action"): { DEV_SIG_CHANNELS: ["thermostat"], DEV_SIG_ENT_MAP_CLASS: "ThermostatHVACAction", DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_77665544_thermostat_hvac_action", }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0019"], - SIG_MANUFACTURER: "Zen Within", - SIG_MODEL: "Zen-01", - SIG_NODE_DESC: b"\x02@\x80X\x11R\x80\x00\x00\x00\x80\x00\x00", }, { DEV_SIG_DEV_NO: 92, + SIG_MANUFACTURER: "_TYZB01_ns1ndbww", + SIG_MODEL: "TS0004", + SIG_NODE_DESC: b"\x01@\x8e\x02\x10R\x00\x02\x00,\x00\x02\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 256, @@ -3598,6 +4070,7 @@ DEVICES = [ SIG_EP_PROFILE: 260, }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ "light.tyzb01_ns1ndbww_ts0004_77665544_on_off", "light.tyzb01_ns1ndbww_ts0004_77665544_on_off_2", @@ -3608,31 +4081,30 @@ DEVICES = [ ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.tyzb01_ns1ndbww_ts0004_77665544_on_off_4", + DEV_SIG_ENT_MAP_ID: "light.tyzb01_ns1ndbww_ts0004_77665544_on_off", }, ("light", "00:11:22:33:44:55:66:77-2"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.tyzb01_ns1ndbww_ts0004_77665544_on_off_3", + DEV_SIG_ENT_MAP_ID: "light.tyzb01_ns1ndbww_ts0004_77665544_on_off_2", }, ("light", "00:11:22:33:44:55:66:77-3"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.tyzb01_ns1ndbww_ts0004_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "light.tyzb01_ns1ndbww_ts0004_77665544_on_off_3", }, ("light", "00:11:22:33:44:55:66:77-4"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.tyzb01_ns1ndbww_ts0004_77665544_on_off_2", + DEV_SIG_ENT_MAP_ID: "light.tyzb01_ns1ndbww_ts0004_77665544_on_off_4", }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0019"], - SIG_MANUFACTURER: "_TYZB01_ns1ndbww", - SIG_MODEL: "TS0004", - SIG_NODE_DESC: b"\x01@\x8e\x02\x10R\x00\x02\x00,\x00\x02\x00", }, { DEV_SIG_DEV_NO: 93, + SIG_MANUFACTURER: "netvox", + SIG_MODEL: "Z308E3ED", + SIG_NODE_DESC: b"\x02@\x80\x9f\x10RR\x00\x00\x00R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 1026, @@ -3640,32 +4112,37 @@ DEVICES = [ SIG_EP_INPUT: [0, 1, 3, 21, 32, 1280, 2821], SIG_EP_OUTPUT: [], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "binary_sensor.netvox_z308e3ed_77665544_ias_zone", + "button.netvox_z308e3ed_77665544_identify", "sensor.netvox_z308e3ed_77665544_power", + "binary_sensor.netvox_z308e3ed_77665544_ias_zone", ], DEV_SIG_ENT_MAP: { - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CHANNELS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.netvox_z308e3ed_77665544_power", - }, ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", DEV_SIG_ENT_MAP_ID: "binary_sensor.netvox_z308e3ed_77665544_ias_zone", }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.netvox_z308e3ed_77665544_identify", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + DEV_SIG_CHANNELS: ["power"], + DEV_SIG_ENT_MAP_CLASS: "Battery", + DEV_SIG_ENT_MAP_ID: "sensor.netvox_z308e3ed_77665544_power", + }, }, - DEV_SIG_EVT_CHANNELS: [], - SIG_MANUFACTURER: "netvox", - SIG_MODEL: "Z308E3ED", - SIG_NODE_DESC: b"\x02@\x80\x9f\x10RR\x00\x00\x00R\x00\x00", - DEV_SIG_ZHA_QUIRK: "Z308E3ED", }, { DEV_SIG_DEV_NO: 94, + SIG_MANUFACTURER: "sengled", + SIG_MODEL: "E11-G13", + SIG_NODE_DESC: b"\x02@\x8c`\x11RR\x00\x00\x00R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 257, @@ -3673,19 +4150,26 @@ DEVICES = [ SIG_EP_INPUT: [0, 3, 4, 5, 6, 8, 1794, 2821], SIG_EP_OUTPUT: [25], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ + "button.sengled_e11_g13_77665544_identify", "light.sengled_e11_g13_77665544_level_on_off", "sensor.sengled_e11_g13_77665544_smartenergy_metering", "sensor.sengled_e11_g13_77665544_smartenergy_metering_summation_delivered", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CHANNELS: ["level", "on_off"], + DEV_SIG_CHANNELS: ["on_off", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", DEV_SIG_ENT_MAP_ID: "light.sengled_e11_g13_77665544_level_on_off", }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.sengled_e11_g13_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", @@ -3697,13 +4181,12 @@ DEVICES = [ DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_77665544_smartenergy_metering_summation_delivered", }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0019"], - SIG_MANUFACTURER: "sengled", - SIG_MODEL: "E11-G13", - SIG_NODE_DESC: b"\x02@\x8c`\x11RR\x00\x00\x00R\x00\x00", }, { DEV_SIG_DEV_NO: 95, + SIG_MANUFACTURER: "sengled", + SIG_MODEL: "E12-N14", + SIG_NODE_DESC: b"\x02@\x8c`\x11RR\x00\x00\x00R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 257, @@ -3711,19 +4194,26 @@ DEVICES = [ SIG_EP_INPUT: [0, 3, 4, 5, 6, 8, 1794, 2821], SIG_EP_OUTPUT: [25], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ + "button.sengled_e12_n14_77665544_identify", "light.sengled_e12_n14_77665544_level_on_off", "sensor.sengled_e12_n14_77665544_smartenergy_metering", - "sensor.sengled_e12_n14_77665544_smartenergy_metering_sumaiton_delivered", + "sensor.sengled_e12_n14_77665544_smartenergy_metering_summation_delivered", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CHANNELS: ["level", "on_off"], + DEV_SIG_CHANNELS: ["on_off", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", DEV_SIG_ENT_MAP_ID: "light.sengled_e12_n14_77665544_level_on_off", }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.sengled_e12_n14_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", @@ -3735,13 +4225,12 @@ DEVICES = [ DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_77665544_smartenergy_metering_summation_delivered", }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0019"], - SIG_MANUFACTURER: "sengled", - SIG_MODEL: "E12-N14", - SIG_NODE_DESC: b"\x02@\x8c`\x11RR\x00\x00\x00R\x00\x00", }, { DEV_SIG_DEV_NO: 96, + SIG_MANUFACTURER: "sengled", + SIG_MODEL: "Z01-A19NAE26", + SIG_NODE_DESC: b"\x02@\x8c`\x11RR\x00\x00\x00R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 257, @@ -3749,19 +4238,26 @@ DEVICES = [ SIG_EP_INPUT: [0, 3, 4, 5, 6, 8, 768, 1794, 2821], SIG_EP_OUTPUT: [25], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ + "button.sengled_z01_a19nae26_77665544_identify", "light.sengled_z01_a19nae26_77665544_level_light_color_on_off", "sensor.sengled_z01_a19nae26_77665544_smartenergy_metering", "sensor.sengled_z01_a19nae26_77665544_smartenergy_metering_summation_delivered", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CHANNELS: ["level", "light_color", "on_off"], + DEV_SIG_CHANNELS: ["on_off", "light_color", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", DEV_SIG_ENT_MAP_ID: "light.sengled_z01_a19nae26_77665544_level_light_color_on_off", }, + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.sengled_z01_a19nae26_77665544_identify", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", @@ -3773,13 +4269,12 @@ DEVICES = [ DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_77665544_smartenergy_metering_summation_delivered", }, }, - DEV_SIG_EVT_CHANNELS: ["1:0x0019"], - SIG_MANUFACTURER: "sengled", - SIG_MODEL: "Z01-A19NAE26", - SIG_NODE_DESC: b"\x02@\x8c`\x11RR\x00\x00\x00R\x00\x00", }, { DEV_SIG_DEV_NO: 97, + SIG_MANUFACTURER: "unk_manufacturer", + SIG_MODEL: "unk_model", + SIG_NODE_DESC: b"\x01@\x8e\x10\x11RR\x00\x00\x00R\x00\x00", SIG_ENDPOINTS: { 1: { SIG_EP_TYPE: 512, @@ -3787,140 +4282,154 @@ DEVICES = [ SIG_EP_INPUT: [0, 3, 4, 5, 6, 8, 10, 21, 256, 64544, 64545], SIG_EP_OUTPUT: [3, 64544], SIG_EP_PROFILE: 260, - } - }, - DEV_SIG_ENTITIES: [ - "cover.unk_manufacturer_unk_model_77665544_level_on_off_shade" - ], - DEV_SIG_ENT_MAP: { - ("cover", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CHANNELS: ["level", "on_off", "shade"], - DEV_SIG_ENT_MAP_CLASS: "Shade", - DEV_SIG_ENT_MAP_ID: "cover.unk_manufacturer_unk_model_77665544_level_on_off_shade", - } + }, }, DEV_SIG_EVT_CHANNELS: [], - SIG_MANUFACTURER: "unk_manufacturer", - SIG_MODEL: "unk_model", - SIG_NODE_DESC: b"\x01@\x8e\x10\x11RR\x00\x00\x00R\x00\x00", + DEV_SIG_ENTITIES: [ + "button.unk_manufacturer_unk_model_77665544_identify", + "cover.unk_manufacturer_unk_model_77665544_level_on_off_shade", + ], + DEV_SIG_ENT_MAP: { + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CHANNELS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.unk_manufacturer_unk_model_77665544_identify", + }, + ("cover", "00:11:22:33:44:55:66:77-1"): { + DEV_SIG_CHANNELS: ["shade", "level", "on_off"], + DEV_SIG_ENT_MAP_CLASS: "Shade", + DEV_SIG_ENT_MAP_ID: "cover.unk_manufacturer_unk_model_77665544_level_on_off_shade", + }, + }, }, { DEV_SIG_DEV_NO: 98, + SIG_MANUFACTURER: "Digi", + SIG_MODEL: "XBee3", + SIG_NODE_DESC: b"\x01@\x8e\x1e\x10R\xff\x00\x00,\xff\x00\x00", SIG_ENDPOINTS: { 208: { + SIG_EP_TYPE: 1, DEV_SIG_EP_ID: 208, - SIG_EP_PROFILE: 49413, - SIG_EP_TYPE: 0x0001, - SIG_EP_INPUT: [0x0006, 0x000C], + SIG_EP_INPUT: [6, 12], SIG_EP_OUTPUT: [], + SIG_EP_PROFILE: 49413, }, 209: { + SIG_EP_TYPE: 1, DEV_SIG_EP_ID: 209, - SIG_EP_PROFILE: 49413, - SIG_EP_TYPE: 0x0001, - SIG_EP_INPUT: [0x0006, 0x000C], + SIG_EP_INPUT: [6, 12], SIG_EP_OUTPUT: [], + SIG_EP_PROFILE: 49413, }, 210: { + SIG_EP_TYPE: 1, DEV_SIG_EP_ID: 210, - SIG_EP_PROFILE: 49413, - SIG_EP_TYPE: 0x0001, - SIG_EP_INPUT: [0x0006, 0x000C], + SIG_EP_INPUT: [6, 12], SIG_EP_OUTPUT: [], + SIG_EP_PROFILE: 49413, }, 211: { + SIG_EP_TYPE: 1, DEV_SIG_EP_ID: 211, - SIG_EP_PROFILE: 49413, - SIG_EP_TYPE: 0x0001, - SIG_EP_INPUT: [0x0006, 0x000C], + SIG_EP_INPUT: [6, 12], SIG_EP_OUTPUT: [], + SIG_EP_PROFILE: 49413, }, 212: { + SIG_EP_TYPE: 1, DEV_SIG_EP_ID: 212, - SIG_EP_PROFILE: 49413, - SIG_EP_TYPE: 0x0001, - SIG_EP_INPUT: [0x0006], + SIG_EP_INPUT: [6], SIG_EP_OUTPUT: [], + SIG_EP_PROFILE: 49413, }, 213: { + SIG_EP_TYPE: 1, DEV_SIG_EP_ID: 213, - SIG_EP_PROFILE: 49413, - SIG_EP_TYPE: 0x0001, - SIG_EP_INPUT: [0x0006], + SIG_EP_INPUT: [6], SIG_EP_OUTPUT: [], + SIG_EP_PROFILE: 49413, }, 214: { + SIG_EP_TYPE: 1, DEV_SIG_EP_ID: 214, - SIG_EP_PROFILE: 49413, - SIG_EP_TYPE: 0x0001, - SIG_EP_INPUT: [0x0006], + SIG_EP_INPUT: [6], SIG_EP_OUTPUT: [], + SIG_EP_PROFILE: 49413, }, 215: { + SIG_EP_TYPE: 1, DEV_SIG_EP_ID: 215, - SIG_EP_PROFILE: 49413, - SIG_EP_TYPE: 0x0001, - SIG_EP_INPUT: [0x0006, 0x000C], + SIG_EP_INPUT: [6, 12], SIG_EP_OUTPUT: [], + SIG_EP_PROFILE: 49413, }, 216: { + SIG_EP_TYPE: 1, DEV_SIG_EP_ID: 216, - SIG_EP_PROFILE: 49413, - SIG_EP_TYPE: 0x0001, - SIG_EP_INPUT: [0x0006], + SIG_EP_INPUT: [6], SIG_EP_OUTPUT: [], + SIG_EP_PROFILE: 49413, }, 217: { + SIG_EP_TYPE: 1, DEV_SIG_EP_ID: 217, - SIG_EP_PROFILE: 49413, - SIG_EP_TYPE: 0x0001, - SIG_EP_INPUT: [0x0006], + SIG_EP_INPUT: [6], SIG_EP_OUTPUT: [], + SIG_EP_PROFILE: 49413, }, 218: { + SIG_EP_TYPE: 1, DEV_SIG_EP_ID: 218, - SIG_EP_PROFILE: 49413, - SIG_EP_TYPE: 0x0001, - SIG_EP_INPUT: [0x0006, 0x000D], + SIG_EP_INPUT: [6, 13], SIG_EP_OUTPUT: [], + SIG_EP_PROFILE: 49413, }, 219: { + SIG_EP_TYPE: 1, DEV_SIG_EP_ID: 219, - SIG_EP_PROFILE: 49413, - SIG_EP_TYPE: 0x0001, - SIG_EP_INPUT: [0x0006, 0x000D], + SIG_EP_INPUT: [6, 13], SIG_EP_OUTPUT: [], + SIG_EP_PROFILE: 49413, }, 220: { + SIG_EP_TYPE: 1, DEV_SIG_EP_ID: 220, - SIG_EP_PROFILE: 49413, - SIG_EP_TYPE: 0x0001, - SIG_EP_INPUT: [0x0006], + SIG_EP_INPUT: [6], SIG_EP_OUTPUT: [], + SIG_EP_PROFILE: 49413, }, 221: { + SIG_EP_TYPE: 1, DEV_SIG_EP_ID: 221, - SIG_EP_PROFILE: 49413, - SIG_EP_TYPE: 0x0001, - SIG_EP_INPUT: [0x0006], + SIG_EP_INPUT: [6], SIG_EP_OUTPUT: [], + SIG_EP_PROFILE: 49413, }, 222: { + SIG_EP_TYPE: 1, DEV_SIG_EP_ID: 222, - SIG_EP_PROFILE: 49413, - SIG_EP_TYPE: 0x0001, - SIG_EP_INPUT: [0x0006], + SIG_EP_INPUT: [6], SIG_EP_OUTPUT: [], + SIG_EP_PROFILE: 49413, }, 232: { + SIG_EP_TYPE: 1, DEV_SIG_EP_ID: 232, + SIG_EP_INPUT: [17, 146], + SIG_EP_OUTPUT: [8, 17], SIG_EP_PROFILE: 49413, - SIG_EP_TYPE: 0x0001, - SIG_EP_INPUT: [0x0011, 0x0092], - SIG_EP_OUTPUT: [0x0008, 0x0011], }, }, + DEV_SIG_EVT_CHANNELS: ["232:0x0008"], DEV_SIG_ENTITIES: [ + "number.digi_xbee3_77665544_analog_output", + "number.digi_xbee3_77665544_analog_output_2", + "sensor.digi_xbee3_77665544_analog_input", + "sensor.digi_xbee3_77665544_analog_input_2", + "sensor.digi_xbee3_77665544_analog_input_3", + "sensor.digi_xbee3_77665544_analog_input_4", + "sensor.digi_xbee3_77665544_analog_input_5", "switch.digi_xbee3_77665544_on_off", "switch.digi_xbee3_77665544_on_off_2", "switch.digi_xbee3_77665544_on_off_3", @@ -3936,29 +4445,43 @@ DEVICES = [ "switch.digi_xbee3_77665544_on_off_13", "switch.digi_xbee3_77665544_on_off_14", "switch.digi_xbee3_77665544_on_off_15", - "sensor.digi_xbee3_77665544_analog_input", - "sensor.digi_xbee3_77665544_analog_input_2", - "sensor.digi_xbee3_77665544_analog_input_3", - "sensor.digi_xbee3_77665544_analog_input_4", - "number.digi_xbee3_77665544_analog_output", - "number.digi_xbee3_77665544_analog_output_2", ], DEV_SIG_ENT_MAP: { + ("sensor", "00:11:22:33:44:55:66:77-208-12"): { + DEV_SIG_CHANNELS: ["analog_input"], + DEV_SIG_ENT_MAP_CLASS: "AnalogInput", + DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_77665544_analog_input", + }, ("switch", "00:11:22:33:44:55:66:77-208-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off", }, + ("sensor", "00:11:22:33:44:55:66:77-209-12"): { + DEV_SIG_CHANNELS: ["analog_input"], + DEV_SIG_ENT_MAP_CLASS: "AnalogInput", + DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_77665544_analog_input_2", + }, ("switch", "00:11:22:33:44:55:66:77-209-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_2", }, + ("sensor", "00:11:22:33:44:55:66:77-210-12"): { + DEV_SIG_CHANNELS: ["analog_input"], + DEV_SIG_ENT_MAP_CLASS: "AnalogInput", + DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_77665544_analog_input_3", + }, ("switch", "00:11:22:33:44:55:66:77-210-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_3", }, + ("sensor", "00:11:22:33:44:55:66:77-211-12"): { + DEV_SIG_CHANNELS: ["analog_input"], + DEV_SIG_ENT_MAP_CLASS: "AnalogInput", + DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_77665544_analog_input_4", + }, ("switch", "00:11:22:33:44:55:66:77-211-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", @@ -3979,6 +4502,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Switch", DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_7", }, + ("sensor", "00:11:22:33:44:55:66:77-215-12"): { + DEV_SIG_CHANNELS: ["analog_input"], + DEV_SIG_ENT_MAP_CLASS: "AnalogInput", + DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_77665544_analog_input_5", + }, ("switch", "00:11:22:33:44:55:66:77-215-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", @@ -3994,6 +4522,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Switch", DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_10", }, + ("number", "00:11:22:33:44:55:66:77-218-13"): { + DEV_SIG_CHANNELS: ["analog_output"], + DEV_SIG_ENT_MAP_CLASS: "ZhaNumber", + DEV_SIG_ENT_MAP_ID: "number.digi_xbee3_77665544_analog_output", + }, ("switch", "00:11:22:33:44:55:66:77-218-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", @@ -4004,6 +4537,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Switch", DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_12", }, + ("number", "00:11:22:33:44:55:66:77-219-13"): { + DEV_SIG_CHANNELS: ["analog_output"], + DEV_SIG_ENT_MAP_CLASS: "ZhaNumber", + DEV_SIG_ENT_MAP_ID: "number.digi_xbee3_77665544_analog_output_2", + }, ("switch", "00:11:22:33:44:55:66:77-220-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", @@ -4019,62 +4557,27 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Switch", DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_15", }, - ("sensor", "00:11:22:33:44:55:66:77-208-12"): { - DEV_SIG_CHANNELS: ["analog_input"], - DEV_SIG_ENT_MAP_CLASS: "AnalogInput", - DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_77665544_analog_input", - }, - ("sensor", "00:11:22:33:44:55:66:77-209-12"): { - DEV_SIG_CHANNELS: ["analog_input"], - DEV_SIG_ENT_MAP_CLASS: "AnalogInput", - DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_77665544_analog_input_2", - }, - ("sensor", "00:11:22:33:44:55:66:77-210-12"): { - DEV_SIG_CHANNELS: ["analog_input"], - DEV_SIG_ENT_MAP_CLASS: "AnalogInput", - DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_77665544_analog_input_3", - }, - ("sensor", "00:11:22:33:44:55:66:77-211-12"): { - DEV_SIG_CHANNELS: ["analog_input"], - DEV_SIG_ENT_MAP_CLASS: "AnalogInput", - DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_77665544_analog_input_4", - }, - ("sensor", "00:11:22:33:44:55:66:77-215-12"): { - DEV_SIG_CHANNELS: ["analog_input"], - DEV_SIG_ENT_MAP_CLASS: "AnalogInput", - DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_77665544_analog_input_5", - }, - ("number", "00:11:22:33:44:55:66:77-218-13"): { - DEV_SIG_CHANNELS: ["analog_output"], - DEV_SIG_ENT_MAP_CLASS: "ZhaNumber", - DEV_SIG_ENT_MAP_ID: "number.digi_xbee3_77665544_analog_output", - }, - ("number", "00:11:22:33:44:55:66:77-219-13"): { - DEV_SIG_CHANNELS: ["analog_output"], - DEV_SIG_ENT_MAP_CLASS: "ZhaNumber", - DEV_SIG_ENT_MAP_ID: "number.digi_xbee3_77665544_analog_output_2", - }, }, - DEV_SIG_EVT_CHANNELS: ["232:0x0008"], - SIG_MANUFACTURER: "Digi", - SIG_MODEL: "XBee3", - SIG_NODE_DESC: b"\x01@\x8e\x1e\x10R\xff\x00\x00,\xff\x00\x00", }, { DEV_SIG_DEV_NO: 99, + SIG_MANUFACTURER: "efektalab.ru", + SIG_MODEL: "EFEKTA_PWS", + SIG_NODE_DESC: b"\x02@\x80\x00\x00P\xa0\x00\x00\x00\xa0\x00\x00", SIG_ENDPOINTS: { 1: { - SIG_EP_TYPE: 0x000C, + SIG_EP_TYPE: 12, DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0x0000, 0x0001, 0x0402, 0x0408], + SIG_EP_INPUT: [0, 1, 1026, 1032], SIG_EP_OUTPUT: [], SIG_EP_PROFILE: 260, - } + }, }, + DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ "sensor.efektalab_ru_efekta_pws_77665544_power", - "sensor.efektalab_ru_efekta_pws_77665544_temperature", "sensor.efektalab_ru_efekta_pws_77665544_soil_moisture", + "sensor.efektalab_ru_efekta_pws_77665544_temperature", ], DEV_SIG_ENT_MAP: { ("sensor", "00:11:22:33:44:55:66:77-1-1"): { @@ -4082,20 +4585,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_77665544_power", }, - ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { - DEV_SIG_CHANNELS: ["temperature"], - DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_77665544_temperature", - }, ("sensor", "00:11:22:33:44:55:66:77-1-1032"): { DEV_SIG_CHANNELS: ["soil_moisture"], DEV_SIG_ENT_MAP_CLASS: "SoilMoisture", DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_77665544_soil_moisture", }, + ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { + DEV_SIG_CHANNELS: ["temperature"], + DEV_SIG_ENT_MAP_CLASS: "Temperature", + DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_77665544_temperature", + }, }, - DEV_SIG_EVT_CHANNELS: [], - SIG_MANUFACTURER: "efektalab.ru", - SIG_MODEL: "EFEKTA_PWS", - SIG_NODE_DESC: b"\x02@\x80\x00\x00P\xa0\x00\x00\x00\xa0\x00\x00", }, ] From f7229319207b2de4d2d5c5eae9467aa37e3e8533 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Thu, 23 Dec 2021 15:28:01 -0800 Subject: [PATCH 1012/2644] Add lock entity to Overkiz integration (#62713) --- .coveragerc | 1 + homeassistant/components/overkiz/__init__.py | 14 +++++ homeassistant/components/overkiz/const.py | 6 +++ homeassistant/components/overkiz/lock.py | 52 +++++++++++++++++++ homeassistant/components/overkiz/sensor.py | 2 +- .../components/overkiz/translations/en.json | 8 ++- 6 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/overkiz/lock.py diff --git a/.coveragerc b/.coveragerc index 24f01b0c463..275a65fab95 100644 --- a/.coveragerc +++ b/.coveragerc @@ -806,6 +806,7 @@ omit = homeassistant/components/overkiz/coordinator.py homeassistant/components/overkiz/entity.py homeassistant/components/overkiz/executor.py + homeassistant/components/overkiz/lock.py homeassistant/components/overkiz/sensor.py homeassistant/components/ovo_energy/__init__.py homeassistant/components/ovo_energy/const.py diff --git a/homeassistant/components/overkiz/__init__.py b/homeassistant/components/overkiz/__init__.py index 77c02daf2a6..c60a3104839 100644 --- a/homeassistant/components/overkiz/__init__.py +++ b/homeassistant/components/overkiz/__init__.py @@ -1,4 +1,7 @@ """The Overkiz (by Somfy) integration.""" +from __future__ import annotations + +from collections import defaultdict from dataclasses import dataclass import logging @@ -10,6 +13,7 @@ from pyoverkiz.exceptions import ( MaintenanceException, TooManyRequestsException, ) +from pyoverkiz.models import Device from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -21,6 +25,7 @@ from homeassistant.helpers.aiohttp_client import async_create_clientsession from .const import ( CONF_HUB, DOMAIN, + OVERKIZ_DEVICE_TO_PLATFORM, PLATFORMS, UPDATE_INTERVAL, UPDATE_INTERVAL_ALL_ASSUMED_STATE, @@ -35,6 +40,7 @@ class HomeAssistantOverkizData: """Overkiz data stored in the Home Assistant data object.""" coordinator: OverkizDataUpdateCoordinator + platforms: dict[str, Device] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -85,8 +91,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) coordinator.update_interval = UPDATE_INTERVAL_ALL_ASSUMED_STATE + platforms: dict[str, Device] = defaultdict(list) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = HomeAssistantOverkizData( coordinator=coordinator, + platforms=platforms, ) # Map Overkiz device to Home Assistant platform @@ -96,6 +105,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device, ) + if platform := OVERKIZ_DEVICE_TO_PLATFORM.get( + device.widget + ) or OVERKIZ_DEVICE_TO_PLATFORM.get(device.ui_class): + platforms[platform].append(device) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) device_registry = await dr.async_get_registry(hass) diff --git a/homeassistant/components/overkiz/const.py b/homeassistant/components/overkiz/const.py index ff5dea13c65..95919a18265 100644 --- a/homeassistant/components/overkiz/const.py +++ b/homeassistant/components/overkiz/const.py @@ -18,6 +18,7 @@ UPDATE_INTERVAL: Final = timedelta(seconds=30) UPDATE_INTERVAL_ALL_ASSUMED_STATE: Final = timedelta(minutes=60) PLATFORMS: list[Platform] = [ + Platform.LOCK, Platform.SENSOR, ] @@ -25,3 +26,8 @@ IGNORED_OVERKIZ_DEVICES: list[UIClass | UIWidget] = [ UIClass.PROTOCOL_GATEWAY, UIClass.POD, ] + +# Used to map the Somfy widget and ui_class to the Home Assistant platform +OVERKIZ_DEVICE_TO_PLATFORM: dict[UIClass | UIWidget, Platform] = { + UIClass.DOOR_LOCK: Platform.LOCK, +} diff --git a/homeassistant/components/overkiz/lock.py b/homeassistant/components/overkiz/lock.py new file mode 100644 index 00000000000..89a091751d9 --- /dev/null +++ b/homeassistant/components/overkiz/lock.py @@ -0,0 +1,52 @@ +"""Support for Overkiz locks.""" +from __future__ import annotations + +from typing import Any + +from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState + +from homeassistant.components.lock import LockEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import HomeAssistantOverkizData +from .const import DOMAIN +from .entity import OverkizEntity + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +): + """Set up the Overkiz locks from a config entry.""" + data: HomeAssistantOverkizData = hass.data[DOMAIN][entry.entry_id] + + entities: list[OverkizLock] = [ + OverkizLock(device.device_url, data.coordinator) + for device in data.platforms[Platform.LOCK] + ] + + async_add_entities(entities) + + +class OverkizLock(OverkizEntity, LockEntity): + """Representation of an Overkiz Lock.""" + + async def async_lock(self, **_: Any) -> None: + """Lock method.""" + await self.executor.async_execute_command(OverkizCommand.LOCK) + + async def async_unlock(self, **_: Any) -> None: + """Unlock method.""" + await self.executor.async_execute_command(OverkizCommand.UNLOCK) + + @property + def is_locked(self) -> bool | None: + """Return a boolean for the state of the lock.""" + return ( + self.executor.select_state(OverkizState.CORE_LOCKED_UNLOCKED) + == OverkizCommandParam.LOCKED + ) diff --git a/homeassistant/components/overkiz/sensor.py b/homeassistant/components/overkiz/sensor.py index 2f933055507..a00587328b8 100644 --- a/homeassistant/components/overkiz/sensor.py +++ b/homeassistant/components/overkiz/sensor.py @@ -3,7 +3,6 @@ from __future__ import annotations from pyoverkiz.enums import OverkizAttribute, OverkizState, UIWidget -from homeassistant.components.overkiz import HomeAssistantOverkizData from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -26,6 +25,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import HomeAssistantOverkizData from .const import DOMAIN, IGNORED_OVERKIZ_DEVICES from .coordinator import OverkizDataUpdateCoordinator from .entity import OverkizDescriptiveEntity, OverkizEntity, OverkizSensorDescription diff --git a/homeassistant/components/overkiz/translations/en.json b/homeassistant/components/overkiz/translations/en.json index f15fe84c3ed..fe46f3fe9fb 100644 --- a/homeassistant/components/overkiz/translations/en.json +++ b/homeassistant/components/overkiz/translations/en.json @@ -1,20 +1,24 @@ { "config": { "abort": { - "already_configured": "Device is already configured" + "already_configured": "Account is already configured" }, "error": { "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication", + "server_in_maintenance": "Server is down for maintenance", + "too_many_requests": "Too many requests, try again later.", "unknown": "Unexpected error" }, "step": { "user": { "data": { "host": "Host", + "hub": "Hub", "password": "Password", "username": "Username" - } + }, + "description": "The Overkiz platform is used by various vendors like Somfy (Connexoon / TaHoma), Hitachi (Hi Kumo), Rexel (Energeasy Connect) and Atlantic (Cozytouch). Enter your application credentials and select your hub." } } } From f07030c42542c6e9b52a16c94d9e5ea88f169829 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 24 Dec 2021 00:13:45 +0000 Subject: [PATCH 1013/2644] [ci skip] Translation update --- .../components/airthings/translations/ja.json | 2 +- .../components/androidtv/translations/nl.json | 46 +++++++++++++++++++ .../azure_event_hub/translations/ja.json | 4 +- .../azure_event_hub/translations/nl.json | 1 + .../components/bosch_shc/translations/ja.json | 2 +- .../components/coinbase/translations/ca.json | 2 + .../components/coinbase/translations/de.json | 2 + .../components/coinbase/translations/en.json | 4 +- .../components/coinbase/translations/et.json | 2 + .../components/coinbase/translations/ja.json | 2 + .../components/coinbase/translations/nl.json | 2 + .../components/coinbase/translations/ru.json | 2 + .../components/coinbase/translations/tr.json | 2 + .../components/fritz/translations/ja.json | 2 +- .../components/icloud/translations/ja.json | 4 +- .../components/overkiz/translations/ca.json | 25 ++++++++++ .../components/overkiz/translations/et.json | 25 ++++++++++ .../components/ps4/translations/ja.json | 4 +- .../components/sensibo/translations/ca.json | 18 ++++++++ .../components/sensibo/translations/de.json | 18 ++++++++ .../components/sensibo/translations/hu.json | 18 ++++++++ .../components/sensibo/translations/ja.json | 18 ++++++++ .../components/sensibo/translations/nl.json | 18 ++++++++ .../components/sensibo/translations/ru.json | 18 ++++++++ .../components/sensibo/translations/tr.json | 18 ++++++++ .../sensibo/translations/zh-Hant.json | 18 ++++++++ .../components/sensor/translations/ca.json | 4 ++ .../components/sensor/translations/de.json | 4 ++ .../components/sensor/translations/en.json | 4 ++ .../components/sensor/translations/et.json | 4 ++ .../components/sensor/translations/ja.json | 4 ++ .../components/sensor/translations/ru.json | 4 ++ .../components/sensor/translations/tr.json | 4 ++ .../sensor/translations/zh-Hans.json | 4 ++ .../components/starline/translations/ja.json | 4 +- .../components/subaru/translations/ja.json | 2 +- .../components/tile/translations/nl.json | 9 +++- .../components/tuya/translations/ja.json | 4 +- .../components/version/translations/et.json | 26 +++++++++++ .../wolflink/translations/sensor.ja.json | 4 +- .../xiaomi_miio/translations/ja.json | 2 +- 41 files changed, 341 insertions(+), 19 deletions(-) create mode 100644 homeassistant/components/androidtv/translations/nl.json create mode 100644 homeassistant/components/overkiz/translations/ca.json create mode 100644 homeassistant/components/overkiz/translations/et.json create mode 100644 homeassistant/components/sensibo/translations/ca.json create mode 100644 homeassistant/components/sensibo/translations/de.json create mode 100644 homeassistant/components/sensibo/translations/hu.json create mode 100644 homeassistant/components/sensibo/translations/ja.json create mode 100644 homeassistant/components/sensibo/translations/nl.json create mode 100644 homeassistant/components/sensibo/translations/ru.json create mode 100644 homeassistant/components/sensibo/translations/tr.json create mode 100644 homeassistant/components/sensibo/translations/zh-Hant.json create mode 100644 homeassistant/components/version/translations/et.json diff --git a/homeassistant/components/airthings/translations/ja.json b/homeassistant/components/airthings/translations/ja.json index 2621fb437a7..04a39045b39 100644 --- a/homeassistant/components/airthings/translations/ja.json +++ b/homeassistant/components/airthings/translations/ja.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "description": "{url} \u306b\u30ed\u30b0\u30a4\u30f3\u3057\u3066\u3001\u8cc7\u683c\u60c5\u5831\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044", + "description": "{url} \u306b\u30ed\u30b0\u30a4\u30f3\u3057\u3066\u3001\u8a8d\u8a3c\u60c5\u5831\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044", "id": "ID", "secret": "\u30b7\u30fc\u30af\u30ec\u30c3\u30c8" } diff --git a/homeassistant/components/androidtv/translations/nl.json b/homeassistant/components/androidtv/translations/nl.json new file mode 100644 index 00000000000..2cb7d51f5b5 --- /dev/null +++ b/homeassistant/components/androidtv/translations/nl.json @@ -0,0 +1,46 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "invalid_unique_id": "Onmogelijk om een geldige unieke id voor het apparaat te bepalen" + }, + "error": { + "adbkey_not_file": "ADB key file niet gevonden", + "cannot_connect": "Kan geen verbinding maken", + "invalid_host": "Ongeldige hostnaam of IP-adres", + "key_and_server": "Geef alleen ADB-sleutel of ADB-server op" + } + }, + "options": { + "step": { + "apps": { + "data": { + "app_id": "Applicatie ID", + "app_name": "Applicatienaam" + }, + "description": "Configureer applicatie id {app_id}", + "title": "Configureer Android TV Apps" + }, + "init": { + "data": { + "apps": "Configureer applicaties lijst", + "exclude_unnamed_apps": "App met onbekende naam uitsluiten", + "get_sources": "Of de actieve apps wel of niet moeten worden opgehaald als de lijst met bronnen", + "screencap": "Bepaalt of albumhoezen moeten worden opgehaald van wat op het scherm wordt weergegeven", + "state_detection_rules": "Regels voor statusdetectie van Android TV configureren", + "turn_off_command": "ADB shell commando om standaard turn_off commando te overschrijven", + "turn_on_command": "ADB shell commando om standaard turn_on commando te overschrijven" + }, + "title": "Android TV-opties" + }, + "rules": { + "data": { + "rule_delete": "Vink aan om deze regel te verwijderen", + "rule_id": "Application ID" + }, + "description": "Detectieregel configureren voor applicatie-ID {rule_id}", + "title": "Regels voor statusdetectie van Android TV configureren" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/azure_event_hub/translations/ja.json b/homeassistant/components/azure_event_hub/translations/ja.json index ef86e5fdc41..d8c0407fbc5 100644 --- a/homeassistant/components/azure_event_hub/translations/ja.json +++ b/homeassistant/components/azure_event_hub/translations/ja.json @@ -24,8 +24,8 @@ "event_hub_sas_key": "\u30a4\u30d9\u30f3\u30c8\u30cf\u30d6SAS\u30ad\u30fc", "event_hub_sas_policy": "\u30a4\u30d9\u30f3\u30c8\u30cf\u30d6SAS\u30dd\u30ea\u30b7\u30fc" }, - "description": "\u6b21\u306eSAS(\u5171\u6709\u30a2\u30af\u30bb\u30b9\u7f72\u540d)\u8cc7\u683c\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044: {event_hub_instance_name}", - "title": "SAS\u306e\u8cc7\u683c\u60c5\u5831\u65b9\u5f0f" + "description": "\u6b21\u306eSAS(\u5171\u6709\u30a2\u30af\u30bb\u30b9\u7f72\u540d)\u8a8d\u8a3c\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044: {event_hub_instance_name}", + "title": "SAS\u306e\u8a8d\u8a3c\u60c5\u5831\u65b9\u5f0f" }, "user": { "data": { diff --git a/homeassistant/components/azure_event_hub/translations/nl.json b/homeassistant/components/azure_event_hub/translations/nl.json index 5ce2dd9f69c..92b3eee0a8d 100644 --- a/homeassistant/components/azure_event_hub/translations/nl.json +++ b/homeassistant/components/azure_event_hub/translations/nl.json @@ -29,6 +29,7 @@ }, "user": { "data": { + "event_hub_instance_name": "Event Hub Instance Naam", "use_connection_string": "Gebruik Connection String" }, "title": "Stel uw Azure Event Hub integratie in" diff --git a/homeassistant/components/bosch_shc/translations/ja.json b/homeassistant/components/bosch_shc/translations/ja.json index fb466338238..5146f81301c 100644 --- a/homeassistant/components/bosch_shc/translations/ja.json +++ b/homeassistant/components/bosch_shc/translations/ja.json @@ -14,7 +14,7 @@ "flow_title": "Bosch SHC: {name}", "step": { "confirm_discovery": { - "description": "LED\u304c\u70b9\u6ec5\u3057\u59cb\u3081\u308b\u307e\u3067\u3001Bosch Smart Home Controller\u306e\u5168\u9762\u306b\u3042\u308b\u30dc\u30bf\u30f3\u3092\u62bc\u3057\u3066\u304f\u3060\u3055\u3044\u3002\nHome Assistant\u3067\u3001{model} @ {host} \u3092\u8a2d\u5b9a\u3059\u308b\u6e96\u5099\u306f\u3067\u304d\u307e\u3057\u305f\u304b\uff1f" + "description": "LED\u304c\u70b9\u6ec5\u3057\u59cb\u3081\u308b\u307e\u3067\u3001Bosch Smart Home Controller\u306e\u524d\u9762\u306b\u3042\u308b\u30dc\u30bf\u30f3\u3092\u62bc\u3057\u3066\u304f\u3060\u3055\u3044\u3002\nHome Assistant\u3067\u3001{model} @ {host} \u3092\u8a2d\u5b9a\u3059\u308b\u6e96\u5099\u306f\u3067\u304d\u307e\u3057\u305f\u304b\uff1f" }, "credentials": { "data": { diff --git a/homeassistant/components/coinbase/translations/ca.json b/homeassistant/components/coinbase/translations/ca.json index ca0214372fb..e01597ad3d9 100644 --- a/homeassistant/components/coinbase/translations/ca.json +++ b/homeassistant/components/coinbase/translations/ca.json @@ -6,6 +6,8 @@ "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "invalid_auth_key": "Coinbase ha rebutjat les credencials API per culpa d'una clau API inv\u00e0lida.", + "invalid_auth_secret": "Coinbase ha rebutjat les credencials API per culpa d'un secret d'API inv\u00e0lid.", "unknown": "Error inesperat" }, "step": { diff --git a/homeassistant/components/coinbase/translations/de.json b/homeassistant/components/coinbase/translations/de.json index 45360acd288..76b45ae999a 100644 --- a/homeassistant/components/coinbase/translations/de.json +++ b/homeassistant/components/coinbase/translations/de.json @@ -6,6 +6,8 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", + "invalid_auth_key": "API-Anmeldeinformationen von Coinbase aufgrund eines ung\u00fcltigen API-Schl\u00fcssels abgelehnt.", + "invalid_auth_secret": "API-Anmeldeinformationen von Coinbase aufgrund eines ung\u00fcltigen API-Geheimnisses abgelehnt.", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/coinbase/translations/en.json b/homeassistant/components/coinbase/translations/en.json index fe7370cb016..023dc37298f 100644 --- a/homeassistant/components/coinbase/translations/en.json +++ b/homeassistant/components/coinbase/translations/en.json @@ -14,7 +14,9 @@ "user": { "data": { "api_key": "API Key", - "api_token": "API Secret" + "api_token": "API Secret", + "currencies": "Account Balance Currencies", + "exchange_rates": "Exchange Rates" }, "description": "Please enter the details of your API key as provided by Coinbase.", "title": "Coinbase API Key Details" diff --git a/homeassistant/components/coinbase/translations/et.json b/homeassistant/components/coinbase/translations/et.json index 84673940cab..0374765fd35 100644 --- a/homeassistant/components/coinbase/translations/et.json +++ b/homeassistant/components/coinbase/translations/et.json @@ -6,6 +6,8 @@ "error": { "cannot_connect": "\u00dchendamine nurjus", "invalid_auth": "Vigane autentimine", + "invalid_auth_key": "Coinbase l\u00fckkas API mandaadid tagasi kehtetu API-v\u00f5tme t\u00f5ttu.", + "invalid_auth_secret": "Coinbase l\u00fckkas API volitused tagasi kuna API salas\u00f5na on kehtetu.", "unknown": "Ootamtu t\u00f5rge" }, "step": { diff --git a/homeassistant/components/coinbase/translations/ja.json b/homeassistant/components/coinbase/translations/ja.json index 55333777aa2..b640a490a98 100644 --- a/homeassistant/components/coinbase/translations/ja.json +++ b/homeassistant/components/coinbase/translations/ja.json @@ -6,6 +6,8 @@ "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "invalid_auth_key": "API\u30ad\u30fc\u304c\u7121\u52b9\u306a\u305f\u3081\u3001Coinbase\u304cAPI\u8a8d\u8a3c\u3092\u62d2\u5426\u3057\u307e\u3057\u305f\u3002", + "invalid_auth_secret": "API\u30b7\u30fc\u30af\u30ec\u30c3\u30c8\u304c\u7121\u52b9\u306a\u305f\u3081\u3001Coinbase\u304cAPI\u8a8d\u8a3c\u3092\u62d2\u5426\u3057\u307e\u3057\u305f\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { diff --git a/homeassistant/components/coinbase/translations/nl.json b/homeassistant/components/coinbase/translations/nl.json index e277eaf67db..2eebb526015 100644 --- a/homeassistant/components/coinbase/translations/nl.json +++ b/homeassistant/components/coinbase/translations/nl.json @@ -6,6 +6,8 @@ "error": { "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", + "invalid_auth_key": "API-referenties geweigerd door Coinbase vanwege een ongeldige API-sleutel.", + "invalid_auth_secret": "API-gegevens geweigerd door Coinbase vanwege een ongeldig API-secret.", "unknown": "Onverwachte fout" }, "step": { diff --git a/homeassistant/components/coinbase/translations/ru.json b/homeassistant/components/coinbase/translations/ru.json index 965cc5d0b96..dcbd0995d03 100644 --- a/homeassistant/components/coinbase/translations/ru.json +++ b/homeassistant/components/coinbase/translations/ru.json @@ -6,6 +6,8 @@ "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "invalid_auth_key": "\u0423\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 API \u043e\u0442\u043a\u043b\u043e\u043d\u0435\u043d\u044b Coinbase \u0438\u0437-\u0437\u0430 \u043d\u0435\u0432\u0435\u0440\u043d\u043e\u0433\u043e \u043a\u043b\u044e\u0447\u0430 API.", + "invalid_auth_secret": "\u0423\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 API \u043e\u0442\u043a\u043b\u043e\u043d\u0435\u043d\u044b Coinbase \u0438\u0437-\u0437\u0430 \u043d\u0435\u0432\u0435\u0440\u043d\u043e\u0433\u043e \u0441\u0435\u043a\u0440\u0435\u0442\u0430 API.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/coinbase/translations/tr.json b/homeassistant/components/coinbase/translations/tr.json index 285d2f7bb96..1bb4a6eca43 100644 --- a/homeassistant/components/coinbase/translations/tr.json +++ b/homeassistant/components/coinbase/translations/tr.json @@ -6,6 +6,8 @@ "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "invalid_auth_key": "Ge\u00e7ersiz bir API Anahtar\u0131 nedeniyle Coinbase taraf\u0131ndan reddedilen API kimlik bilgileri.", + "invalid_auth_secret": "Ge\u00e7ersiz bir API Gizlilik nedeniyle Coinbase taraf\u0131ndan reddedilen API kimlik bilgileri.", "unknown": "Beklenmeyen hata" }, "step": { diff --git a/homeassistant/components/fritz/translations/ja.json b/homeassistant/components/fritz/translations/ja.json index 77700224d19..156caabbfa3 100644 --- a/homeassistant/components/fritz/translations/ja.json +++ b/homeassistant/components/fritz/translations/ja.json @@ -28,7 +28,7 @@ "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "description": "FRITZ!Box Tools\u306e\u8a8d\u8a3c\u3092\u66f4\u65b0\u3057\u307e\u3059: {host}\n\nFRITZ!Box Tools\u304c\u3001FRITZ!Box\u306b\u30ed\u30b0\u30a4\u30f3\u3067\u304d\u307e\u305b\u3093\u3002", - "title": "FRITZ!Box Tools\u306e\u30a2\u30c3\u30d7\u30c7\u30fc\u30c8 - \u8cc7\u683c\u60c5\u5831" + "title": "FRITZ!Box Tools\u306e\u30a2\u30c3\u30d7\u30c7\u30fc\u30c8 - \u8a8d\u8a3c\u60c5\u5831" }, "start_config": { "data": { diff --git a/homeassistant/components/icloud/translations/ja.json b/homeassistant/components/icloud/translations/ja.json index d60b1e1a335..4b7947b8dfa 100644 --- a/homeassistant/components/icloud/translations/ja.json +++ b/homeassistant/components/icloud/translations/ja.json @@ -31,8 +31,8 @@ "username": "E\u30e1\u30fc\u30eb", "with_family": "\u5bb6\u65cf\u3068\u5171\u6709" }, - "description": "\u8cc7\u683c\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", - "title": "iCloud \u306e\u8cc7\u683c\u60c5\u5831" + "description": "\u8a8d\u8a3c\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", + "title": "iCloud \u306e\u8a8d\u8a3c\u60c5\u5831" }, "verification_code": { "data": { diff --git a/homeassistant/components/overkiz/translations/ca.json b/homeassistant/components/overkiz/translations/ca.json new file mode 100644 index 00000000000..2478c1ccecd --- /dev/null +++ b/homeassistant/components/overkiz/translations/ca.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "El compte ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "server_in_maintenance": "El servidor est\u00e0 inoperatiu per manteniment", + "too_many_requests": "Massa sol\u00b7licituds, torna-ho a provar m\u00e9s tard.", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "hub": "Hub", + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "description": "La plataforma Overkiz \u00e9s utilitzada per diversos venedors com Somfy (Connexoon/TaHoma), Hitachi (Hi Kumo), Rexel (Energeasy Connect) i Atlantic (Cozytouch). Introdueix les credencials d'aplicaci\u00f3 i selecciona el concentrador (hub)." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/et.json b/homeassistant/components/overkiz/translations/et.json new file mode 100644 index 00000000000..72c6e463540 --- /dev/null +++ b/homeassistant/components/overkiz/translations/et.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Konto on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "server_in_maintenance": "Server on hoolduse t\u00f5ttu maas", + "too_many_requests": "Liiga palju taotlusi, proovi hiljem uuesti.", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "host": "Host", + "hub": "Hub", + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + }, + "description": "Overkizi platvormi kasutavad erinevad m\u00fc\u00fcjad, nagu Somfy (Connexoon / TaHoma), Hitachi (Hi Kumo), Rexel (Energeasy Connect) ja Atlantic (Cozytouch). Sisesta oma rakenduse mandaadid ja vali hub." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/translations/ja.json b/homeassistant/components/ps4/translations/ja.json index b8dee70a844..5315d7df416 100644 --- a/homeassistant/components/ps4/translations/ja.json +++ b/homeassistant/components/ps4/translations/ja.json @@ -2,14 +2,14 @@ "config": { "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "credential_error": "\u8cc7\u683c\u60c5\u5831\u306e\u53d6\u5f97\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002", + "credential_error": "\u8a8d\u8a3c\u60c5\u5831\u306e\u53d6\u5f97\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002", "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", "port_987_bind_error": "\u30dd\u30fc\u30c8 987\u306b\u30d0\u30a4\u30f3\u30c9\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u8a73\u7d30\u306f\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8](https://www.home-assistant.io/components/ps4/)\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "port_997_bind_error": "\u30dd\u30fc\u30c8 997\u306b\u30d0\u30a4\u30f3\u30c9\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u8a73\u7d30\u306f\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8](https://www.home-assistant.io/components/ps4/)\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "credential_timeout": "\u8cc7\u683c\u60c5\u5831\u30b5\u30fc\u30d3\u30b9\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002\u9001\u4fe1(submit)\u3092\u62bc\u3057\u3066\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", + "credential_timeout": "\u8a8d\u8a3c\u60c5\u5831\u30b5\u30fc\u30d3\u30b9\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002\u9001\u4fe1(submit)\u3092\u62bc\u3057\u3066\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "login_failed": "PlayStation 4\u3068\u306e\u30da\u30a2\u30ea\u30f3\u30b0\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002PIN\u30b3\u30fc\u30c9\u304c\u6b63\u3057\u3044\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "no_ipaddress": "\u8a2d\u5b9a\u3057\u305f\u3044PlayStation4\u306eIP\u30a2\u30c9\u30ec\u30b9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044" }, diff --git a/homeassistant/components/sensibo/translations/ca.json b/homeassistant/components/sensibo/translations/ca.json new file mode 100644 index 00000000000..adff28113ca --- /dev/null +++ b/homeassistant/components/sensibo/translations/ca.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "El compte ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "step": { + "user": { + "data": { + "api_key": "Clau API", + "name": "Nom" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/de.json b/homeassistant/components/sensibo/translations/de.json new file mode 100644 index 00000000000..5bb5dd3cf84 --- /dev/null +++ b/homeassistant/components/sensibo/translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "step": { + "user": { + "data": { + "api_key": "API-Schl\u00fcssel", + "name": "Name" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/hu.json b/homeassistant/components/sensibo/translations/hu.json new file mode 100644 index 00000000000..09aca99c669 --- /dev/null +++ b/homeassistant/components/sensibo/translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "user": { + "data": { + "api_key": "API kulcs", + "name": "N\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/ja.json b/homeassistant/components/sensibo/translations/ja.json new file mode 100644 index 00000000000..92e6e077f51 --- /dev/null +++ b/homeassistant/components/sensibo/translations/ja.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" + }, + "step": { + "user": { + "data": { + "api_key": "API\u30ad\u30fc", + "name": "\u540d\u524d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/nl.json b/homeassistant/components/sensibo/translations/nl.json new file mode 100644 index 00000000000..c1aa1c36340 --- /dev/null +++ b/homeassistant/components/sensibo/translations/nl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Account is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, + "step": { + "user": { + "data": { + "api_key": "API-sleutel", + "name": "Naam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/ru.json b/homeassistant/components/sensibo/translations/ru.json new file mode 100644 index 00000000000..0d6a22bde74 --- /dev/null +++ b/homeassistant/components/sensibo/translations/ru.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/tr.json b/homeassistant/components/sensibo/translations/tr.json new file mode 100644 index 00000000000..eb5e2eb4129 --- /dev/null +++ b/homeassistant/components/sensibo/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "name": "Ad" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/zh-Hant.json b/homeassistant/components/sensibo/translations/zh-Hant.json new file mode 100644 index 00000000000..818d64dbf1b --- /dev/null +++ b/homeassistant/components/sensibo/translations/zh-Hant.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "user": { + "data": { + "api_key": "API \u5bc6\u9470", + "name": "\u540d\u7a31" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/ca.json b/homeassistant/components/sensor/translations/ca.json index 3e914526e12..fc8db4c2791 100644 --- a/homeassistant/components/sensor/translations/ca.json +++ b/homeassistant/components/sensor/translations/ca.json @@ -1,6 +1,7 @@ { "device_automation": { "condition_type": { + "is_apparent_power": "Pot\u00e8ncia aparent actual de {entity_name}", "is_battery_level": "Nivell de bateria actual de {entity_name}", "is_carbon_dioxide": "Concentraci\u00f3 actual de di\u00f2xid de carboni de {entity_name}", "is_carbon_monoxide": "Concentraci\u00f3 actual de mon\u00f2xid de carboni de {entity_name}", @@ -20,6 +21,7 @@ "is_power": "Pot\u00e8ncia actual de {entity_name}", "is_power_factor": "Factor de pot\u00e8ncia actual de {entity_name}", "is_pressure": "Pressi\u00f3 actual de {entity_name}", + "is_reactive_power": "Pot\u00e8ncia reactiva actual de {entity_name}", "is_signal_strength": "Pot\u00e8ncia de senyal actual de {entity_name}", "is_sulphur_dioxide": "Concentraci\u00f3 actual de di\u00f2xid de sofre de {entity_name}", "is_temperature": "Temperatura actual de {entity_name}", @@ -28,6 +30,7 @@ "is_voltage": "Voltatge actual de {entity_name}" }, "trigger_type": { + "apparent_power": "Canvia la pot\u00e8ncia aparent de {entity_name}", "battery_level": "Canvia el nivell de bateria de {entity_name}", "carbon_dioxide": "Canvia la concentraci\u00f3 de di\u00f2xid de carboni de {entity_name}", "carbon_monoxide": "Canvia la concentraci\u00f3 de mon\u00f2xid de carboni de {entity_name}", @@ -47,6 +50,7 @@ "power": "Canvia la pot\u00e8ncia de {entity_name}", "power_factor": "Canvia el factor de pot\u00e8ncia de {entity_name}", "pressure": "Canvia la pressi\u00f3 de {entity_name}", + "reactive_power": "Canvia la pot\u00e8ncia reactiva de {entity_name}", "signal_strength": "Canvia la pot\u00e8ncia de senyal de {entity_name}", "sulphur_dioxide": "Canvia la concentraci\u00f3 de di\u00f2xid de sofre de {entity_name}", "temperature": "Canvia la temperatura de {entity_name}", diff --git a/homeassistant/components/sensor/translations/de.json b/homeassistant/components/sensor/translations/de.json index 237259ffc11..cb21759b2dd 100644 --- a/homeassistant/components/sensor/translations/de.json +++ b/homeassistant/components/sensor/translations/de.json @@ -1,6 +1,7 @@ { "device_automation": { "condition_type": { + "is_apparent_power": "Aktuelle Scheinleistung von {entity_name}", "is_battery_level": "{entity_name} Batteriestand", "is_carbon_dioxide": "Aktuelle {entity_name} Kohlenstoffdioxid-Konzentration", "is_carbon_monoxide": "Aktuelle {entity_name} Kohlenstoffmonoxid-Konzentration", @@ -20,6 +21,7 @@ "is_power": "Aktuelle {entity_name} Leistung", "is_power_factor": "Aktueller Leistungsfaktor f\u00fcr {entity_name}", "is_pressure": "{entity_name} Druck", + "is_reactive_power": "Aktuelle Blindleistung von {entity_name}", "is_signal_strength": "Aktuelle {entity_name} Signalst\u00e4rke", "is_sulphur_dioxide": "Aktuelle Schwefeldioxid-Konzentration von {entity_name}", "is_temperature": "Aktuelle {entity_name} Temperatur", @@ -28,6 +30,7 @@ "is_voltage": "Aktuelle Spannung von {entity_name}" }, "trigger_type": { + "apparent_power": "{entity_name} \u00c4nderungen der Scheinleistung", "battery_level": "{entity_name} Batteriestatus\u00e4nderungen", "carbon_dioxide": "{entity_name} Kohlenstoffdioxid-Konzentrations\u00e4nderung", "carbon_monoxide": "{entity_name} Kohlenstoffmonoxid-Konzentrations\u00e4nderung", @@ -47,6 +50,7 @@ "power": "{entity_name} Leistungs\u00e4nderungen", "power_factor": "{entity_name} Leistungsfaktor\u00e4nderung", "pressure": "{entity_name} Druck\u00e4nderungen", + "reactive_power": "{entity_name} Blindleistung \u00e4ndert sich", "signal_strength": "{entity_name} Signalst\u00e4rke\u00e4nderungen", "sulphur_dioxide": "\u00c4nderung der Schwefeldioxidkonzentration bei {entity_name}", "temperature": "{entity_name} Temperatur\u00e4nderungen", diff --git a/homeassistant/components/sensor/translations/en.json b/homeassistant/components/sensor/translations/en.json index 531016b2007..23b20ac9974 100644 --- a/homeassistant/components/sensor/translations/en.json +++ b/homeassistant/components/sensor/translations/en.json @@ -1,6 +1,7 @@ { "device_automation": { "condition_type": { + "is_apparent_power": "Current {entity_name} apparent power", "is_battery_level": "Current {entity_name} battery level", "is_carbon_dioxide": "Current {entity_name} carbon dioxide concentration level", "is_carbon_monoxide": "Current {entity_name} carbon monoxide concentration level", @@ -20,6 +21,7 @@ "is_power": "Current {entity_name} power", "is_power_factor": "Current {entity_name} power factor", "is_pressure": "Current {entity_name} pressure", + "is_reactive_power": "Current {entity_name} reactive power", "is_signal_strength": "Current {entity_name} signal strength", "is_sulphur_dioxide": "Current {entity_name} sulphur dioxide concentration level", "is_temperature": "Current {entity_name} temperature", @@ -28,6 +30,7 @@ "is_voltage": "Current {entity_name} voltage" }, "trigger_type": { + "apparent_power": "{entity_name} apparent power changes", "battery_level": "{entity_name} battery level changes", "carbon_dioxide": "{entity_name} carbon dioxide concentration changes", "carbon_monoxide": "{entity_name} carbon monoxide concentration changes", @@ -47,6 +50,7 @@ "power": "{entity_name} power changes", "power_factor": "{entity_name} power factor changes", "pressure": "{entity_name} pressure changes", + "reactive_power": "{entity_name} reactive power changes", "signal_strength": "{entity_name} signal strength changes", "sulphur_dioxide": "{entity_name} sulphur dioxide concentration changes", "temperature": "{entity_name} temperature changes", diff --git a/homeassistant/components/sensor/translations/et.json b/homeassistant/components/sensor/translations/et.json index 06fbe321091..664ca6cf6f5 100644 --- a/homeassistant/components/sensor/translations/et.json +++ b/homeassistant/components/sensor/translations/et.json @@ -1,6 +1,7 @@ { "device_automation": { "condition_type": { + "is_apparent_power": "Praegune {entity_name} n\u00e4iv v\u00f5imsus", "is_battery_level": "Praegune {entity_name} aku tase", "is_carbon_dioxide": "{entity_name} praegune s\u00fcsihappegaasi tase", "is_carbon_monoxide": "{entity_name} praegune vingugaasi tase", @@ -20,6 +21,7 @@ "is_power": "Praegune {entity_name} toide (v\u00f5imsus)", "is_power_factor": "Praegune {entity_name} v\u00f5imsusfaktor", "is_pressure": "Praegune {entity_name} r\u00f5hk", + "is_reactive_power": "Praegune {entity_name} reaktiivv\u00f5imsus", "is_signal_strength": "Praegune {entity_name} signaali tugevus", "is_sulphur_dioxide": "Praegune v\u00e4\u00e4veldioksiidi kontsentratsioonitase {entity_name}", "is_temperature": "Praegune {entity_name} temperatuur", @@ -28,6 +30,7 @@ "is_voltage": "Praegune {entity_name}pinge" }, "trigger_type": { + "apparent_power": "{entity_name} n\u00e4iv v\u00f5imsus muutub", "battery_level": "{entity_name} aku tase muutub", "carbon_dioxide": "{entity_name} s\u00fcsihappegaasi tase muutus", "carbon_monoxide": "{entity_name} vingugaasi tase muutus", @@ -47,6 +50,7 @@ "power": "{entity_name} energiare\u017eiimi muutub", "power_factor": "{entity_name} v\u00f5imsus muutub", "pressure": "{entity_name} r\u00f5hk muutub", + "reactive_power": "{entity_name} reaktiivv\u00f5imsus muutub", "signal_strength": "{entity_name} signaalitugevus muutub", "sulphur_dioxide": "{entity_name} v\u00e4\u00e4veldioksiidi kontsentratsiooni muutused", "temperature": "{entity_name} temperatuur muutub", diff --git a/homeassistant/components/sensor/translations/ja.json b/homeassistant/components/sensor/translations/ja.json index fccbedad2b2..dbd8bd76e5e 100644 --- a/homeassistant/components/sensor/translations/ja.json +++ b/homeassistant/components/sensor/translations/ja.json @@ -1,6 +1,7 @@ { "device_automation": { "condition_type": { + "is_apparent_power": "\u73fe\u5728\u306e {entity_name} \u898b\u304b\u3051\u306e\u96fb\u529b(apparent power)", "is_battery_level": "\u73fe\u5728\u306e {entity_name} \u96fb\u6c60\u6b8b\u91cf", "is_carbon_dioxide": "\u73fe\u5728\u306e {entity_name} \u4e8c\u9178\u5316\u70ad\u7d20\u6fc3\u5ea6\u30ec\u30d9\u30eb", "is_carbon_monoxide": "\u73fe\u5728\u306e {entity_name} \u4e00\u9178\u5316\u70ad\u7d20\u6fc3\u5ea6\u30ec\u30d9\u30eb", @@ -20,6 +21,7 @@ "is_power": "\u73fe\u5728\u306e {entity_name} \u96fb\u6e90", "is_power_factor": "\u73fe\u5728\u306e {entity_name} \u529b\u7387", "is_pressure": "\u73fe\u5728\u306e {entity_name} \u5727\u529b", + "is_reactive_power": "\u73fe\u5728\u306e {entity_name} \u30ea\u30a2\u30af\u30c6\u30a3\u30d6\u96fb\u6e90(reactive power)", "is_signal_strength": "\u73fe\u5728\u306e {entity_name} \u4fe1\u53f7\u5f37\u5ea6", "is_sulphur_dioxide": "\u73fe\u5728\u306e {entity_name} \u4e8c\u9178\u5316\u786b\u9ec4\u6fc3\u5ea6\u30ec\u30d9\u30eb", "is_temperature": "\u73fe\u5728\u306e {entity_name} \u6e29\u5ea6", @@ -28,6 +30,7 @@ "is_voltage": "\u73fe\u5728\u306e {entity_name} \u96fb\u5727" }, "trigger_type": { + "apparent_power": "{entity_name} \u898b\u304b\u3051\u306e\u96fb\u529b(apparent power)\u306e\u5909\u5316", "battery_level": "{entity_name} \u96fb\u6c60\u6b8b\u91cf\u306e\u5909\u5316", "carbon_dioxide": "{entity_name} \u4e8c\u9178\u5316\u70ad\u7d20\u6fc3\u5ea6\u306e\u5909\u5316", "carbon_monoxide": "{entity_name} \u4e00\u9178\u5316\u70ad\u7d20\u6fc3\u5ea6\u306e\u5909\u5316", @@ -47,6 +50,7 @@ "power": "{entity_name} \u96fb\u6e90(power)\u306e\u5909\u5316", "power_factor": "{entity_name} \u529b\u7387\u304c\u5909\u5316", "pressure": "{entity_name} \u5727\u529b\u306e\u5909\u5316", + "reactive_power": "{entity_name} \u30ea\u30a2\u30af\u30c6\u30a3\u30d6\u96fb\u6e90\u306e\u5909\u66f4(reactive power)", "signal_strength": "{entity_name} \u4fe1\u53f7\u5f37\u5ea6\u306e\u5909\u5316", "sulphur_dioxide": "{entity_name} \u4e8c\u9178\u5316\u786b\u9ec4\u6fc3\u5ea6\u306e\u5909\u5316", "temperature": "{entity_name} \u6e29\u5ea6\u5909\u5316", diff --git a/homeassistant/components/sensor/translations/ru.json b/homeassistant/components/sensor/translations/ru.json index b7e8a912a11..35a40104d19 100644 --- a/homeassistant/components/sensor/translations/ru.json +++ b/homeassistant/components/sensor/translations/ru.json @@ -1,6 +1,7 @@ { "device_automation": { "condition_type": { + "is_apparent_power": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "is_battery_level": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "is_carbon_dioxide": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0443\u0440\u043e\u0432\u043d\u044f \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u0438 \u0443\u0433\u043b\u0435\u043a\u0438\u0441\u043b\u043e\u0433\u043e \u0433\u0430\u0437\u0430", "is_carbon_monoxide": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0443\u0440\u043e\u0432\u043d\u044f \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u0438 \u0443\u0433\u0430\u0440\u043d\u043e\u0433\u043e \u0433\u0430\u0437\u0430", @@ -20,6 +21,7 @@ "is_power": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "is_power_factor": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043a\u043e\u044d\u0444\u0444\u0438\u0446\u0438\u0435\u043d\u0442\u0430 \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u0438", "is_pressure": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "is_reactive_power": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "is_signal_strength": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "is_sulphur_dioxide": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0443\u0440\u043e\u0432\u043d\u044f \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u0438 \u0434\u0438\u043e\u043a\u0441\u0438\u0434\u0430 \u0441\u0435\u0440\u044b", "is_temperature": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", @@ -28,6 +30,7 @@ "is_voltage": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043d\u0430\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f" }, "trigger_type": { + "apparent_power": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043f\u043e\u043b\u043d\u043e\u0439 \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u0438", "battery_level": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "carbon_dioxide": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "carbon_monoxide": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", @@ -47,6 +50,7 @@ "power": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "power_factor": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u043a\u043e\u044d\u0444\u0444\u0438\u0446\u0438\u0435\u043d\u0442 \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u0438", "pressure": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "reactive_power": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0440\u0435\u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0439 \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u0438", "signal_strength": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "sulphur_dioxide": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u0438 \u0434\u0438\u043e\u043a\u0441\u0438\u0434\u0430 \u0441\u0435\u0440\u044b", "temperature": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", diff --git a/homeassistant/components/sensor/translations/tr.json b/homeassistant/components/sensor/translations/tr.json index db5d774e453..1a6e54d8e81 100644 --- a/homeassistant/components/sensor/translations/tr.json +++ b/homeassistant/components/sensor/translations/tr.json @@ -1,6 +1,7 @@ { "device_automation": { "condition_type": { + "is_apparent_power": "Mevcut {entity_name} g\u00f6r\u00fcn\u00fcr g\u00fc\u00e7", "is_battery_level": "Mevcut {entity_name} pil seviyesi", "is_carbon_dioxide": "Mevcut {entity_name} karbondioksit konsantrasyon seviyesi", "is_carbon_monoxide": "Mevcut {entity_name} karbon monoksit konsantrasyon seviyesi", @@ -20,6 +21,7 @@ "is_power": "Mevcut {entity_name} g\u00fcc\u00fc", "is_power_factor": "Mevcut {entity_name} g\u00fc\u00e7 fakt\u00f6r\u00fc", "is_pressure": "Ge\u00e7erli {entity_name} bas\u0131nc\u0131", + "is_reactive_power": "Mevcut {entity_name} reaktif g\u00fc\u00e7", "is_signal_strength": "Mevcut {entity_name} sinyal g\u00fcc\u00fc", "is_sulphur_dioxide": "Mevcut {entity_name} k\u00fck\u00fcrt dioksit konsantrasyon seviyesi", "is_temperature": "Mevcut {entity_name} s\u0131cakl\u0131\u011f\u0131", @@ -28,6 +30,7 @@ "is_voltage": "Mevcut {entity_name} voltaj\u0131" }, "trigger_type": { + "apparent_power": "{entity_name} g\u00f6r\u00fcn\u00fcr g\u00fc\u00e7 de\u011fi\u015fiklikleri", "battery_level": "{entity_name} pil seviyesi de\u011fi\u015fiklikleri", "carbon_dioxide": "{entity_name} karbondioksit konsantrasyonu de\u011fi\u015fiklikleri", "carbon_monoxide": "{entity_name} karbon monoksit konsantrasyonu de\u011fi\u015fiklikleri", @@ -47,6 +50,7 @@ "power": "{entity_name} g\u00fc\u00e7 de\u011fi\u015fiklikleri", "power_factor": "{entity_name} g\u00fc\u00e7 fakt\u00f6r\u00fc de\u011fi\u015fiklikleri", "pressure": "{entity_name} bas\u0131n\u00e7 de\u011fi\u015fiklikleri", + "reactive_power": "{entity_name} reaktif g\u00fc\u00e7 de\u011fi\u015fiklikleri", "signal_strength": "{entity_name} sinyal g\u00fcc\u00fc de\u011fi\u015fiklikleri", "sulphur_dioxide": "{entity_name} k\u00fck\u00fcrt dioksit konsantrasyonu de\u011fi\u015fiklikleri", "temperature": "{entity_name} s\u0131cakl\u0131k de\u011fi\u015fiklikleri", diff --git a/homeassistant/components/sensor/translations/zh-Hans.json b/homeassistant/components/sensor/translations/zh-Hans.json index 2edde916c95..4fd2ec4db9d 100644 --- a/homeassistant/components/sensor/translations/zh-Hans.json +++ b/homeassistant/components/sensor/translations/zh-Hans.json @@ -1,6 +1,7 @@ { "device_automation": { "condition_type": { + "is_apparent_power": "{entity_name} \u5f53\u524d\u7684\u89c6\u5728\u529f\u7387", "is_battery_level": "{entity_name} \u5f53\u524d\u7684\u7535\u6c60\u7535\u91cf", "is_current": "{entity_name} \u5f53\u524d\u7684\u7535\u6d41", "is_energy": "{entity_name} \u5f53\u524d\u7528\u7535\u91cf", @@ -9,12 +10,14 @@ "is_power": "{entity_name} \u5f53\u524d\u7684\u529f\u7387", "is_power_factor": "{entity_name} \u5f53\u524d\u7684\u529f\u7387\u56e0\u6570", "is_pressure": "{entity_name} \u5f53\u524d\u7684\u538b\u529b", + "is_reactive_power": "{entity_name} \u5f53\u524d\u7684\u65e0\u529f\u529f\u7387", "is_signal_strength": "{entity_name} \u5f53\u524d\u7684\u4fe1\u53f7\u5f3a\u5ea6", "is_temperature": "{entity_name} \u5f53\u524d\u7684\u6e29\u5ea6", "is_value": "{entity_name} \u5f53\u524d\u7684\u503c", "is_voltage": "{entity_name} \u5f53\u524d\u7684\u7535\u538b" }, "trigger_type": { + "apparent_power": "{entity_name} \u7684\u89c6\u5728\u529f\u7387\u53d8\u5316", "battery_level": "{entity_name} \u7684\u7535\u6c60\u7535\u91cf\u53d8\u5316", "current": "{entity_name} \u7684\u7535\u6d41\u53d8\u5316", "energy": "{entity_name} \u7684\u7528\u7535\u91cf\u53d8\u5316", @@ -23,6 +26,7 @@ "power": "{entity_name} \u7684\u529f\u7387\u53d8\u5316", "power_factor": "{entity_name} \u7684\u529f\u7387\u56e0\u6570\u53d8\u5316", "pressure": "{entity_name} \u7684\u538b\u529b\u53d8\u5316", + "reactive_power": "{entity_name} \u7684\u65e0\u529f\u529f\u7387\u53d8\u5316", "signal_strength": "{entity_name} \u7684\u4fe1\u53f7\u5f3a\u5ea6\u53d8\u5316", "temperature": "{entity_name} \u7684\u6e29\u5ea6\u53d8\u5316", "value": "{entity_name} \u7684\u503c\u53d8\u5316", diff --git a/homeassistant/components/starline/translations/ja.json b/homeassistant/components/starline/translations/ja.json index 0afa9c47347..42d0b61c76d 100644 --- a/homeassistant/components/starline/translations/ja.json +++ b/homeassistant/components/starline/translations/ja.json @@ -12,7 +12,7 @@ "app_secret": "\u30b7\u30fc\u30af\u30ec\u30c3\u30c8" }, "description": "[StarLine developer account](https://my.starline.ru/developer)\u306e\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3ID\u3068\u30b7\u30fc\u30af\u30ec\u30c3\u30c8\u30b3\u30fc\u30c9", - "title": "\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u8cc7\u683c\u60c5\u5831" + "title": "\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u8a8d\u8a3c\u60c5\u5831" }, "auth_captcha": { "data": { @@ -34,7 +34,7 @@ "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "description": "StarLine\u30a2\u30ab\u30a6\u30f3\u30c8\u306e\u30e1\u30fc\u30eb\u30a2\u30c9\u30ec\u30b9\u3068\u30d1\u30b9\u30ef\u30fc\u30c9", - "title": "\u30e6\u30fc\u30b6\u30fc\u306e\u8cc7\u683c\u60c5\u5831" + "title": "\u30e6\u30fc\u30b6\u30fc\u306e\u8a8d\u8a3c\u60c5\u5831" } } } diff --git a/homeassistant/components/subaru/translations/ja.json b/homeassistant/components/subaru/translations/ja.json index c557f3419bc..b8949709464 100644 --- a/homeassistant/components/subaru/translations/ja.json +++ b/homeassistant/components/subaru/translations/ja.json @@ -24,7 +24,7 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, - "description": "MySubaru\u306e\u8cc7\u683c\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\u6ce8: \u521d\u671f\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u306b\u306f\u6700\u5927 30\u79d2\u304b\u304b\u308b\u3053\u3068\u304c\u3042\u308a\u307e\u3059", + "description": "MySubaru\u306e\u8a8d\u8a3c\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\u6ce8: \u521d\u671f\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u306b\u306f\u6700\u5927 30\u79d2\u304b\u304b\u308b\u3053\u3068\u304c\u3042\u308a\u307e\u3059", "title": "Subaru Starlink\u306e\u8a2d\u5b9a" } } diff --git a/homeassistant/components/tile/translations/nl.json b/homeassistant/components/tile/translations/nl.json index 2f81919fc4b..d2c66b4fe65 100644 --- a/homeassistant/components/tile/translations/nl.json +++ b/homeassistant/components/tile/translations/nl.json @@ -1,12 +1,19 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" }, "error": { "invalid_auth": "Ongeldige authenticatie" }, "step": { + "reauth_confirm": { + "data": { + "password": "Wachtwoord" + }, + "title": "Herauthenticeer Tile" + }, "user": { "data": { "password": "Wachtwoord", diff --git a/homeassistant/components/tuya/translations/ja.json b/homeassistant/components/tuya/translations/ja.json index 96b31ac0781..6733e70b472 100644 --- a/homeassistant/components/tuya/translations/ja.json +++ b/homeassistant/components/tuya/translations/ja.json @@ -21,7 +21,7 @@ "tuya_app_type": "\u30e2\u30d0\u30a4\u30eb\u30a2\u30d7\u30ea", "username": "\u30a2\u30ab\u30a6\u30f3\u30c8" }, - "description": "Tuya\u306e\u8cc7\u683c\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", + "description": "Tuya\u306e\u8a8d\u8a3c\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", "title": "Tuya" }, "user": { @@ -35,7 +35,7 @@ "tuya_project_type": "Tuya Cloud\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u30bf\u30a4\u30d7", "username": "\u30a2\u30ab\u30a6\u30f3\u30c8" }, - "description": "Tuya\u306e\u8cc7\u683c\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", + "description": "Tuya\u306e\u8a8d\u8a3c\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", "title": "Tuya\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3" } } diff --git a/homeassistant/components/version/translations/et.json b/homeassistant/components/version/translations/et.json new file mode 100644 index 00000000000..4dae980e180 --- /dev/null +++ b/homeassistant/components/version/translations/et.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "step": { + "user": { + "data": { + "version_source": "Versiooni allikas" + }, + "description": "Vali allikas kust soovid versioone j\u00e4lgida.", + "title": "Vali paigalduse t\u00fc\u00fcp" + }, + "version_source": { + "data": { + "beta": "Kaasa beetaversioonid", + "board": "Millist kaarti tuleks j\u00e4lgida", + "channel": "Millist kanalit tuleks j\u00e4lgida", + "image": "Millist t\u00f5mmist tuleks j\u00e4lgida" + }, + "description": "{version_source} versioonij\u00e4lgimise seadistamine", + "title": "Seadista" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/sensor.ja.json b/homeassistant/components/wolflink/translations/sensor.ja.json index 2a8b091ca36..d2ba79d4b19 100644 --- a/homeassistant/components/wolflink/translations/sensor.ja.json +++ b/homeassistant/components/wolflink/translations/sensor.ja.json @@ -57,8 +57,8 @@ "rt_frostschutz": "RT\u971c\u9632\u6b62", "ruhekontakt": "\u6b8b\u308a\u306e\u9023\u7d61\u5148(Rest contact)", "schornsteinfeger": "\u6392\u51fa\u91cf\u30c6\u30b9\u30c8", - "smart_grid": "\u30b9\u30de\u30fc\u30c8\u30b0\u30ea\u30c3\u30c9", - "smart_home": "\u30b9\u30de\u30fc\u30c8\u30db\u30fc\u30e0", + "smart_grid": "SmartGrid", + "smart_home": "SmartHome", "softstart": "\u30bd\u30d5\u30c8\u30b9\u30bf\u30fc\u30c8", "solarbetrieb": "\u30bd\u30fc\u30e9\u30fc\u30e2\u30fc\u30c9", "sparbetrieb": "\u30a8\u30b3\u30ce\u30df\u30fc\u30e2\u30fc\u30c9", diff --git a/homeassistant/components/xiaomi_miio/translations/ja.json b/homeassistant/components/xiaomi_miio/translations/ja.json index e9b6c7a6cc9..44c6f7f149e 100644 --- a/homeassistant/components/xiaomi_miio/translations/ja.json +++ b/homeassistant/components/xiaomi_miio/translations/ja.json @@ -63,7 +63,7 @@ "title": "Xiaomi Miio\u30c7\u30d0\u30a4\u30b9\u307e\u305f\u306f\u3001Xiaomi Gateway\u306b\u63a5\u7d9a" }, "reauth_confirm": { - "description": "Xiaomi Miio\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30c8\u30fc\u30af\u30f3\u3092\u66f4\u65b0\u3057\u305f\u308a\u3001\u4e0d\u8db3\u3057\u3066\u3044\u308b\u30af\u30e9\u30a6\u30c9\u306e\u8cc7\u683c\u60c5\u5831\u3092\u8ffd\u52a0\u3059\u308b\u305f\u3081\u306b\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "description": "Xiaomi Miio\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30c8\u30fc\u30af\u30f3\u3092\u66f4\u65b0\u3057\u305f\u308a\u3001\u4e0d\u8db3\u3057\u3066\u3044\u308b\u30af\u30e9\u30a6\u30c9\u306e\u8a8d\u8a3c\u60c5\u5831\u3092\u8ffd\u52a0\u3059\u308b\u305f\u3081\u306b\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" }, "select": { From fb04b19960afe8c47b5d78932e15a33a18d8662a Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Thu, 23 Dec 2021 16:21:47 -0800 Subject: [PATCH 1014/2644] Add button entity to Overkiz integration (#62719) --- .coveragerc | 1 + homeassistant/components/overkiz/button.py | 81 ++++++++++++++++++++++ homeassistant/components/overkiz/const.py | 1 + homeassistant/components/overkiz/entity.py | 3 +- 4 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/overkiz/button.py diff --git a/.coveragerc b/.coveragerc index 275a65fab95..48c8425eed8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -803,6 +803,7 @@ omit = homeassistant/components/osramlightify/light.py homeassistant/components/otp/sensor.py homeassistant/components/overkiz/__init__.py + homeassistant/components/overkiz/button.py homeassistant/components/overkiz/coordinator.py homeassistant/components/overkiz/entity.py homeassistant/components/overkiz/executor.py diff --git a/homeassistant/components/overkiz/button.py b/homeassistant/components/overkiz/button.py new file mode 100644 index 00000000000..85e7ea8c613 --- /dev/null +++ b/homeassistant/components/overkiz/button.py @@ -0,0 +1,81 @@ +"""Support for Overkiz (virtual) buttons.""" +from __future__ import annotations + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import HomeAssistantOverkizData +from .const import DOMAIN, IGNORED_OVERKIZ_DEVICES +from .entity import OverkizDescriptiveEntity + +BUTTON_DESCRIPTIONS: list[ButtonEntityDescription] = [ + # My Position (cover, light) + ButtonEntityDescription( + key="my", + name="My Position", + icon="mdi:star", + ), + # Identify + ButtonEntityDescription( + key="identify", # startIdentify and identify are reversed... Swap this when fixed in API. + name="Start Identify", + icon="mdi:human-greeting-variant", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + ButtonEntityDescription( + key="stopIdentify", + name="Stop Identify", + icon="mdi:human-greeting-variant", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + ButtonEntityDescription( + key="startIdentify", # startIdentify and identify are reversed... Swap this when fixed in API. + name="Identify", + icon="mdi:human-greeting-variant", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +): + """Set up the Overkiz button from a config entry.""" + data: HomeAssistantOverkizData = hass.data[DOMAIN][entry.entry_id] + entities: list[ButtonEntity] = [] + + supported_commands = { + description.key: description for description in BUTTON_DESCRIPTIONS + } + + for device in data.coordinator.data.values(): + if ( + device.widget not in IGNORED_OVERKIZ_DEVICES + and device.ui_class not in IGNORED_OVERKIZ_DEVICES + ): + for command in device.definition.commands: + if description := supported_commands.get(command.command_name): + entities.append( + OverkizButton( + device.device_url, + data.coordinator, + description, + ) + ) + + async_add_entities(entities) + + +class OverkizButton(OverkizDescriptiveEntity, ButtonEntity): + """Representation of an Overkiz Button.""" + + async def async_press(self) -> None: + """Handle the button press.""" + await self.executor.async_execute_command(self.entity_description.key) diff --git a/homeassistant/components/overkiz/const.py b/homeassistant/components/overkiz/const.py index 95919a18265..60591d9d761 100644 --- a/homeassistant/components/overkiz/const.py +++ b/homeassistant/components/overkiz/const.py @@ -18,6 +18,7 @@ UPDATE_INTERVAL: Final = timedelta(seconds=30) UPDATE_INTERVAL_ALL_ASSUMED_STATE: Final = timedelta(minutes=60) PLATFORMS: list[Platform] = [ + Platform.BUTTON, Platform.LOCK, Platform.SENSOR, ] diff --git a/homeassistant/components/overkiz/entity.py b/homeassistant/components/overkiz/entity.py index 1a52a03ab36..0c931bc5985 100644 --- a/homeassistant/components/overkiz/entity.py +++ b/homeassistant/components/overkiz/entity.py @@ -7,6 +7,7 @@ from dataclasses import dataclass from pyoverkiz.enums import OverkizAttribute, OverkizState from pyoverkiz.models import Device +from homeassistant.components.button import ButtonEntityDescription from homeassistant.components.sensor import SensorEntityDescription from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -99,7 +100,7 @@ class OverkizDescriptiveEntity(OverkizEntity): self, device_url: str, coordinator: OverkizDataUpdateCoordinator, - description: OverkizSensorDescription, + description: OverkizSensorDescription | ButtonEntityDescription, ) -> None: """Initialize the device.""" super().__init__(device_url, coordinator) From 27e3a5ba83d2e1917ebe2225c1ba8980c6e5754a Mon Sep 17 00:00:00 2001 From: Brian Egge Date: Thu, 23 Dec 2021 19:29:29 -0500 Subject: [PATCH 1015/2644] Generic thermostat presets (#56080) Co-authored-by: J. Nick Koston --- .../components/generic_thermostat/climate.py | 48 +++++++++----- .../generic_thermostat/test_climate.py | 63 ++++++++++++++++--- 2 files changed, 85 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index 2c27d371c5e..67d4be92c95 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -15,8 +15,12 @@ from homeassistant.components.climate.const import ( HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF, + PRESET_ACTIVITY, PRESET_AWAY, + PRESET_COMFORT, + PRESET_HOME, PRESET_NONE, + PRESET_SLEEP, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) @@ -64,10 +68,20 @@ CONF_COLD_TOLERANCE = "cold_tolerance" CONF_HOT_TOLERANCE = "hot_tolerance" CONF_KEEP_ALIVE = "keep_alive" CONF_INITIAL_HVAC_MODE = "initial_hvac_mode" -CONF_AWAY_TEMP = "away_temp" CONF_PRECISION = "precision" SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE +CONF_PRESETS = { + p: f"{p}_temp" + for p in ( + PRESET_AWAY, + PRESET_COMFORT, + PRESET_HOME, + PRESET_SLEEP, + PRESET_ACTIVITY, + ) +} + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_HEATER): cv.entity_id, @@ -84,13 +98,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_INITIAL_HVAC_MODE): vol.In( [HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF] ), - vol.Optional(CONF_AWAY_TEMP): vol.Coerce(float), vol.Optional(CONF_PRECISION): vol.In( [PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE] ), vol.Optional(CONF_UNIQUE_ID): cv.string, } -) +).extend({vol.Optional(v): vol.Coerce(float) for (k, v) in CONF_PRESETS.items()}) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -110,7 +123,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= hot_tolerance = config.get(CONF_HOT_TOLERANCE) keep_alive = config.get(CONF_KEEP_ALIVE) initial_hvac_mode = config.get(CONF_INITIAL_HVAC_MODE) - away_temp = config.get(CONF_AWAY_TEMP) + presets = { + key: config[value] for key, value in CONF_PRESETS.items() if value in config + } precision = config.get(CONF_PRECISION) unit = hass.config.units.temperature_unit unique_id = config.get(CONF_UNIQUE_ID) @@ -130,7 +145,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= hot_tolerance, keep_alive, initial_hvac_mode, - away_temp, + presets, precision, unit, unique_id, @@ -156,7 +171,7 @@ class GenericThermostat(ClimateEntity, RestoreEntity): hot_tolerance, keep_alive, initial_hvac_mode, - away_temp, + presets, precision, unit, unique_id, @@ -171,7 +186,7 @@ class GenericThermostat(ClimateEntity, RestoreEntity): self._hot_tolerance = hot_tolerance self._keep_alive = keep_alive self._hvac_mode = initial_hvac_mode - self._saved_target_temp = target_temp or away_temp + self._saved_target_temp = target_temp or next(iter(presets.values()), None) self._temp_precision = precision if self.ac_mode: self._hvac_list = [HVAC_MODE_COOL, HVAC_MODE_OFF] @@ -187,12 +202,12 @@ class GenericThermostat(ClimateEntity, RestoreEntity): self._unit = unit self._unique_id = unique_id self._support_flags = SUPPORT_FLAGS - if away_temp: + if len(presets): self._support_flags = SUPPORT_FLAGS | SUPPORT_PRESET_MODE - self._attr_preset_modes = [PRESET_NONE, PRESET_AWAY] + self._attr_preset_modes = [PRESET_NONE] + list(presets.keys()) else: self._attr_preset_modes = [PRESET_NONE] - self._away_temp = away_temp + self._presets = presets async def async_added_to_hass(self): """Run when entity about to be added.""" @@ -528,14 +543,15 @@ class GenericThermostat(ClimateEntity, RestoreEntity): if preset_mode == self._attr_preset_mode: # I don't think we need to call async_write_ha_state if we didn't change the state return - if preset_mode == PRESET_AWAY: - self._attr_preset_mode = PRESET_AWAY - self._saved_target_temp = self._target_temp - self._target_temp = self._away_temp - await self._async_control_heating(force=True) - elif preset_mode == PRESET_NONE: + if preset_mode == PRESET_NONE: self._attr_preset_mode = PRESET_NONE self._target_temp = self._saved_target_temp await self._async_control_heating(force=True) + else: + if self._attr_preset_mode == PRESET_NONE: + self._saved_target_temp = self._target_temp + self._attr_preset_mode = preset_mode + self._target_temp = self._presets[preset_mode] + await self._async_control_heating(force=True) self.async_write_ha_state() diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index a1896c94d2f..1720a54d973 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -13,8 +13,12 @@ from homeassistant.components.climate.const import ( HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF, + PRESET_ACTIVITY, PRESET_AWAY, + PRESET_COMFORT, + PRESET_HOME, PRESET_NONE, + PRESET_SLEEP, ) from homeassistant.components.generic_thermostat import ( DOMAIN as GENERIC_THERMOSTAT_DOMAIN, @@ -209,6 +213,10 @@ async def setup_comp_2(hass): "heater": ENT_SWITCH, "target_sensor": ENT_SENSOR, "away_temp": 16, + "sleep_temp": 17, + "home_temp": 19, + "comfort_temp": 20, + "activity_temp": 21, "initial_hvac_mode": HVAC_MODE_HEAT, } }, @@ -288,38 +296,73 @@ async def test_set_target_temp(hass, setup_comp_2): assert state.attributes.get("temperature") == 30.0 -async def test_set_away_mode(hass, setup_comp_2): +@pytest.mark.parametrize( + "preset,temp", + [ + (PRESET_NONE, 23), + (PRESET_AWAY, 16), + (PRESET_COMFORT, 20), + (PRESET_HOME, 19), + (PRESET_SLEEP, 17), + (PRESET_ACTIVITY, 21), + ], +) +async def test_set_away_mode(hass, setup_comp_2, preset, temp): """Test the setting away mode.""" await common.async_set_temperature(hass, 23) - await common.async_set_preset_mode(hass, PRESET_AWAY) + await common.async_set_preset_mode(hass, preset) state = hass.states.get(ENTITY) - assert state.attributes.get("temperature") == 16 + assert state.attributes.get("temperature") == temp -async def test_set_away_mode_and_restore_prev_temp(hass, setup_comp_2): +@pytest.mark.parametrize( + "preset,temp", + [ + (PRESET_NONE, 23), + (PRESET_AWAY, 16), + (PRESET_COMFORT, 20), + (PRESET_HOME, 19), + (PRESET_SLEEP, 17), + (PRESET_ACTIVITY, 21), + ], +) +async def test_set_away_mode_and_restore_prev_temp(hass, setup_comp_2, preset, temp): """Test the setting and removing away mode. Verify original temperature is restored. """ await common.async_set_temperature(hass, 23) - await common.async_set_preset_mode(hass, PRESET_AWAY) + await common.async_set_preset_mode(hass, preset) state = hass.states.get(ENTITY) - assert state.attributes.get("temperature") == 16 + assert state.attributes.get("temperature") == temp await common.async_set_preset_mode(hass, PRESET_NONE) state = hass.states.get(ENTITY) assert state.attributes.get("temperature") == 23 -async def test_set_away_mode_twice_and_restore_prev_temp(hass, setup_comp_2): +@pytest.mark.parametrize( + "preset,temp", + [ + (PRESET_NONE, 23), + (PRESET_AWAY, 16), + (PRESET_COMFORT, 20), + (PRESET_HOME, 19), + (PRESET_SLEEP, 17), + (PRESET_ACTIVITY, 21), + ], +) +async def test_set_away_mode_twice_and_restore_prev_temp( + hass, setup_comp_2, preset, temp +): """Test the setting away mode twice in a row. Verify original temperature is restored. """ await common.async_set_temperature(hass, 23) - await common.async_set_preset_mode(hass, PRESET_AWAY) - await common.async_set_preset_mode(hass, PRESET_AWAY) + await common.async_set_preset_mode(hass, preset) + await common.async_set_preset_mode(hass, preset) state = hass.states.get(ENTITY) - assert state.attributes.get("temperature") == 16 + assert state.attributes.get("temperature") == temp await common.async_set_preset_mode(hass, PRESET_NONE) state = hass.states.get(ENTITY) assert state.attributes.get("temperature") == 23 From 690b5a994bc20b561632d9aa3e332061457a3d72 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Dec 2021 22:55:31 -1000 Subject: [PATCH 1016/2644] Add missing __init__.py to overkiz tests (#62727) --- tests/components/overkiz/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 tests/components/overkiz/__init__.py diff --git a/tests/components/overkiz/__init__.py b/tests/components/overkiz/__init__.py new file mode 100644 index 00000000000..d827bcb8334 --- /dev/null +++ b/tests/components/overkiz/__init__.py @@ -0,0 +1 @@ +"""Tests for the overkiz component.""" From 1d0036b86a98c8bae48ca9c407ac38ca64a4b09c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Dec 2021 02:26:07 -1000 Subject: [PATCH 1017/2644] Bump zeroconf to 0.38.1 (#62720) --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 338456ca576..16a8a8ff26e 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.37.0"], + "requirements": ["zeroconf==0.38.1"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b035cec441a..cb76eb8ada2 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ sqlalchemy==1.4.27 voluptuous-serialize==2.5.0 voluptuous==0.12.2 yarl==1.6.3 -zeroconf==0.37.0 +zeroconf==0.38.1 # Constrain pillow to 8.2.0 because later versions are causing issues in nightly builds. # https://github.com/home-assistant/core/issues/61756 diff --git a/requirements_all.txt b/requirements_all.txt index 80d1d5d7fde..f68da65b4af 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2500,7 +2500,7 @@ youtube_dl==2021.06.06 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.37.0 +zeroconf==0.38.1 # homeassistant.components.zha zha-quirks==0.0.65 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d3948e7f2c7..3d37e20d0be 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1492,7 +1492,7 @@ yeelight==0.7.8 youless-api==0.15 # homeassistant.components.zeroconf -zeroconf==0.37.0 +zeroconf==0.38.1 # homeassistant.components.zha zha-quirks==0.0.65 From 3e567959f7dbfd084c2043ea35c2ee89d40fe507 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 24 Dec 2021 13:45:42 +0100 Subject: [PATCH 1018/2644] Add basic type hints to advantage_air (#62737) Co-authored-by: epenet --- homeassistant/components/advantage_air/__init__.py | 7 ++++--- .../components/advantage_air/binary_sensor.py | 12 ++++++++++-- homeassistant/components/advantage_air/climate.py | 13 +++++++++++-- homeassistant/components/advantage_air/cover.py | 10 ++++++++-- homeassistant/components/advantage_air/select.py | 10 ++++++++-- homeassistant/components/advantage_air/sensor.py | 13 +++++++++++-- homeassistant/components/advantage_air/switch.py | 10 ++++++++-- 7 files changed, 60 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/advantage_air/__init__.py b/homeassistant/components/advantage_air/__init__.py index 21b70ab9fc6..12c5a4593c5 100644 --- a/homeassistant/components/advantage_air/__init__.py +++ b/homeassistant/components/advantage_air/__init__.py @@ -1,11 +1,12 @@ """Advantage Air climate integration.""" - from datetime import timedelta import logging from advantage_air import ApiError, advantage_air +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, Platform +from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -24,7 +25,7 @@ PLATFORMS = [ _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Advantage Air config.""" ip_address = entry.data[CONF_IP_ADDRESS] port = entry.data[CONF_PORT] @@ -69,7 +70,7 @@ async def async_setup_entry(hass, entry): return True -async def async_unload_entry(hass, entry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload Advantage Air Config.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/advantage_air/binary_sensor.py b/homeassistant/components/advantage_air/binary_sensor.py index 3a0990c55ef..73b10b158b0 100644 --- a/homeassistant/components/advantage_air/binary_sensor.py +++ b/homeassistant/components/advantage_air/binary_sensor.py @@ -1,10 +1,14 @@ """Binary Sensor platform for Advantage Air integration.""" +from __future__ import annotations from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, ) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN from .entity import AdvantageAirEntity @@ -12,12 +16,16 @@ from .entity import AdvantageAirEntity PARALLEL_UPDATES = 0 -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up AdvantageAir motion platform.""" instance = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id] - entities = [] + entities: list[BinarySensorEntity] = [] for ac_key, ac_device in instance["coordinator"].data["aircons"].items(): entities.append(AdvantageAirZoneFilter(instance, ac_key)) for zone_key, zone in ac_device["zones"].items(): diff --git a/homeassistant/components/advantage_air/climate.py b/homeassistant/components/advantage_air/climate.py index a56273fc10a..165ecf9bb1c 100644 --- a/homeassistant/components/advantage_air/climate.py +++ b/homeassistant/components/advantage_air/climate.py @@ -1,4 +1,6 @@ """Climate platform for Advantage Air integration.""" +from __future__ import annotations + from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( FAN_AUTO, @@ -15,8 +17,11 @@ from homeassistant.components.climate.const import ( SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_platform +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( ADVANTAGE_AIR_STATE_CLOSE, @@ -59,12 +64,16 @@ ZONE_HVAC_MODES = [HVAC_MODE_OFF, HVAC_MODE_HEAT_COOL] PARALLEL_UPDATES = 0 -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up AdvantageAir climate platform.""" instance = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id] - entities = [] + entities: list[ClimateEntity] = [] for ac_key, ac_device in instance["coordinator"].data["aircons"].items(): entities.append(AdvantageAirAC(instance, ac_key)) for zone_key, zone in ac_device["zones"].items(): diff --git a/homeassistant/components/advantage_air/cover.py b/homeassistant/components/advantage_air/cover.py index d308a024f14..a54d6d8b535 100644 --- a/homeassistant/components/advantage_air/cover.py +++ b/homeassistant/components/advantage_air/cover.py @@ -1,5 +1,4 @@ """Cover platform for Advantage Air integration.""" - from homeassistant.components.cover import ( ATTR_POSITION, SUPPORT_CLOSE, @@ -8,6 +7,9 @@ from homeassistant.components.cover import ( CoverDeviceClass, CoverEntity, ) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( ADVANTAGE_AIR_STATE_CLOSE, @@ -19,7 +21,11 @@ from .entity import AdvantageAirEntity PARALLEL_UPDATES = 0 -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up AdvantageAir cover platform.""" instance = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id] diff --git a/homeassistant/components/advantage_air/select.py b/homeassistant/components/advantage_air/select.py index 79d23f8dfd1..ecc612ae1ed 100644 --- a/homeassistant/components/advantage_air/select.py +++ b/homeassistant/components/advantage_air/select.py @@ -1,6 +1,8 @@ """Select platform for Advantage Air integration.""" - from homeassistant.components.select import SelectEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN from .entity import AdvantageAirEntity @@ -8,7 +10,11 @@ from .entity import AdvantageAirEntity ADVANTAGE_AIR_INACTIVE = "Inactive" -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up AdvantageAir toggle platform.""" instance = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id] diff --git a/homeassistant/components/advantage_air/sensor.py b/homeassistant/components/advantage_air/sensor.py index bb3082a84bb..8055c37a571 100644 --- a/homeassistant/components/advantage_air/sensor.py +++ b/homeassistant/components/advantage_air/sensor.py @@ -1,4 +1,6 @@ """Sensor platform for Advantage Air integration.""" +from __future__ import annotations + import voluptuous as vol from homeassistant.components.sensor import ( @@ -6,9 +8,12 @@ from homeassistant.components.sensor import ( SensorEntity, SensorStateClass, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, TEMP_CELSIUS +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ADVANTAGE_AIR_STATE_OPEN, DOMAIN as ADVANTAGE_AIR_DOMAIN from .entity import AdvantageAirEntity @@ -20,12 +25,16 @@ ADVANTAGE_AIR_SERVICE_SET_TIME_TO = "set_time_to" PARALLEL_UPDATES = 0 -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up AdvantageAir sensor platform.""" instance = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id] - entities = [] + entities: list[SensorEntity] = [] for ac_key, ac_device in instance["coordinator"].data["aircons"].items(): entities.append(AdvantageAirTimeTo(instance, ac_key, "On")) entities.append(AdvantageAirTimeTo(instance, ac_key, "Off")) diff --git a/homeassistant/components/advantage_air/switch.py b/homeassistant/components/advantage_air/switch.py index 1a44973f8c1..90c30082329 100644 --- a/homeassistant/components/advantage_air/switch.py +++ b/homeassistant/components/advantage_air/switch.py @@ -1,6 +1,8 @@ """Switch platform for Advantage Air integration.""" - +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( ADVANTAGE_AIR_STATE_OFF, @@ -10,7 +12,11 @@ from .const import ( from .entity import AdvantageAirEntity -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up AdvantageAir toggle platform.""" instance = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id] From 6dcec898c4cdb78944df734c558522b3cd8a9b02 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 24 Dec 2021 14:06:14 +0100 Subject: [PATCH 1019/2644] Add basic type hints to abode (#62730) Co-authored-by: epenet --- homeassistant/components/abode/__init__.py | 12 +++++++----- .../components/abode/alarm_control_panel.py | 9 ++++++++- homeassistant/components/abode/binary_sensor.py | 9 ++++++++- homeassistant/components/abode/camera.py | 9 ++++++++- homeassistant/components/abode/cover.py | 9 ++++++++- homeassistant/components/abode/light.py | 9 ++++++++- homeassistant/components/abode/lock.py | 9 ++++++++- homeassistant/components/abode/sensor.py | 9 ++++++++- homeassistant/components/abode/switch.py | 13 +++++++++++-- 9 files changed, 74 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index 93b760f5f68..411a25373c2 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -7,6 +7,7 @@ import abodepy.helpers.timeline as TIMELINE from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_DATE, ATTR_DEVICE_ID, @@ -17,6 +18,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, Platform, ) +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import dispatcher_send @@ -75,7 +77,7 @@ class AbodeSystem: self.logout_listener = None -async def async_setup_entry(hass, config_entry): +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up Abode integration from a config entry.""" username = config_entry.data.get(CONF_USERNAME) password = config_entry.data.get(CONF_PASSWORD) @@ -110,7 +112,7 @@ async def async_setup_entry(hass, config_entry): return True -async def async_unload_entry(hass, config_entry): +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" hass.services.async_remove(DOMAIN, SERVICE_SETTINGS) hass.services.async_remove(DOMAIN, SERVICE_CAPTURE_IMAGE) @@ -129,7 +131,7 @@ async def async_unload_entry(hass, config_entry): return unload_ok -def setup_hass_services(hass): +def setup_hass_services(hass: HomeAssistant) -> None: """Home Assistant services.""" def change_setting(call): @@ -183,7 +185,7 @@ def setup_hass_services(hass): ) -async def setup_hass_events(hass): +async def setup_hass_events(hass: HomeAssistant) -> None: """Home Assistant start and stop callbacks.""" def logout(event): @@ -202,7 +204,7 @@ async def setup_hass_events(hass): ) -def setup_abode_events(hass): +def setup_abode_events(hass: HomeAssistant) -> None: """Event callbacks.""" def event_callback(event, event_json): diff --git a/homeassistant/components/abode/alarm_control_panel.py b/homeassistant/components/abode/alarm_control_panel.py index 947e729db3a..791c6ab393d 100644 --- a/homeassistant/components/abode/alarm_control_panel.py +++ b/homeassistant/components/abode/alarm_control_panel.py @@ -4,11 +4,14 @@ from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, SUPPORT_ALARM_ARM_HOME, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, ) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AbodeDevice from .const import DOMAIN @@ -16,7 +19,11 @@ from .const import DOMAIN ICON = "mdi:security" -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up Abode alarm control panel device.""" data = hass.data[DOMAIN] async_add_entities( diff --git a/homeassistant/components/abode/binary_sensor.py b/homeassistant/components/abode/binary_sensor.py index ea921470e8f..0f59f02842f 100644 --- a/homeassistant/components/abode/binary_sensor.py +++ b/homeassistant/components/abode/binary_sensor.py @@ -5,12 +5,19 @@ from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, ) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AbodeDevice from .const import DOMAIN -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up Abode binary sensor devices.""" data = hass.data[DOMAIN] diff --git a/homeassistant/components/abode/camera.py b/homeassistant/components/abode/camera.py index 987e32f9911..05feaf214c5 100644 --- a/homeassistant/components/abode/camera.py +++ b/homeassistant/components/abode/camera.py @@ -8,7 +8,10 @@ import abodepy.helpers.timeline as TIMELINE import requests from homeassistant.components.camera import Camera +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import Throttle from . import AbodeDevice @@ -17,7 +20,11 @@ from .const import DOMAIN, LOGGER MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up Abode camera devices.""" data = hass.data[DOMAIN] diff --git a/homeassistant/components/abode/cover.py b/homeassistant/components/abode/cover.py index d88c2fdd404..6eb65296c2d 100644 --- a/homeassistant/components/abode/cover.py +++ b/homeassistant/components/abode/cover.py @@ -2,12 +2,19 @@ import abodepy.helpers.constants as CONST from homeassistant.components.cover import CoverEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AbodeDevice from .const import DOMAIN -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up Abode cover devices.""" data = hass.data[DOMAIN] diff --git a/homeassistant/components/abode/light.py b/homeassistant/components/abode/light.py index b756c79d9de..7f90137d0f7 100644 --- a/homeassistant/components/abode/light.py +++ b/homeassistant/components/abode/light.py @@ -12,6 +12,9 @@ from homeassistant.components.light import ( SUPPORT_COLOR_TEMP, LightEntity, ) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.color import ( color_temperature_kelvin_to_mired, color_temperature_mired_to_kelvin, @@ -21,7 +24,11 @@ from . import AbodeDevice from .const import DOMAIN -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up Abode light devices.""" data = hass.data[DOMAIN] diff --git a/homeassistant/components/abode/lock.py b/homeassistant/components/abode/lock.py index 2a52663c0e7..f766ef88c2c 100644 --- a/homeassistant/components/abode/lock.py +++ b/homeassistant/components/abode/lock.py @@ -2,12 +2,19 @@ import abodepy.helpers.constants as CONST from homeassistant.components.lock import LockEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AbodeDevice from .const import DOMAIN -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up Abode lock devices.""" data = hass.data[DOMAIN] diff --git a/homeassistant/components/abode/sensor.py b/homeassistant/components/abode/sensor.py index ebc45370062..f415036aff8 100644 --- a/homeassistant/components/abode/sensor.py +++ b/homeassistant/components/abode/sensor.py @@ -8,6 +8,9 @@ from homeassistant.components.sensor import ( SensorEntity, SensorEntityDescription, ) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AbodeDevice from .const import DOMAIN @@ -31,7 +34,11 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up Abode sensor devices.""" data = hass.data[DOMAIN] diff --git a/homeassistant/components/abode/switch.py b/homeassistant/components/abode/switch.py index 75c13962c43..aacfb46d287 100644 --- a/homeassistant/components/abode/switch.py +++ b/homeassistant/components/abode/switch.py @@ -1,8 +1,13 @@ """Support for Abode Security System switches.""" +from __future__ import annotations + import abodepy.helpers.constants as CONST from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AbodeAutomation, AbodeDevice from .const import DOMAIN @@ -12,11 +17,15 @@ DEVICE_TYPES = [CONST.TYPE_SWITCH, CONST.TYPE_VALVE] ICON = "mdi:robot" -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up Abode switch devices.""" data = hass.data[DOMAIN] - entities = [] + entities: list[SwitchEntity] = [] for device_type in DEVICE_TYPES: for device in data.abode.get_devices(generic_type=device_type): From 6ffd587788f6f95c6980d974c44eb30aab3f5f59 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 24 Dec 2021 14:25:10 +0100 Subject: [PATCH 1020/2644] Add basic type hints to ads (#62738) Co-authored-by: epenet --- homeassistant/components/ads/binary_sensor.py | 12 +++++++++++- homeassistant/components/ads/cover.py | 12 +++++++++++- homeassistant/components/ads/light.py | 10 +++++++++- homeassistant/components/ads/sensor.py | 13 +++++++++++-- homeassistant/components/ads/switch.py | 12 +++++++++++- 5 files changed, 53 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/ads/binary_sensor.py b/homeassistant/components/ads/binary_sensor.py index 0cdec25313f..952562b49ec 100644 --- a/homeassistant/components/ads/binary_sensor.py +++ b/homeassistant/components/ads/binary_sensor.py @@ -1,4 +1,6 @@ """Support for ADS binary sensors.""" +from __future__ import annotations + import voluptuous as vol from homeassistant.components.binary_sensor import ( @@ -8,7 +10,10 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE, AdsEntity @@ -22,7 +27,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform(hass, config, add_entities, discovery_info=None): +def setup_platform( + hass: HomeAssistant, + config: ConfigType, + add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Set up the Binary Sensor platform for ADS.""" ads_hub = hass.data.get(DATA_ADS) diff --git a/homeassistant/components/ads/cover.py b/homeassistant/components/ads/cover.py index 976bfd58fed..636b0f77ef0 100644 --- a/homeassistant/components/ads/cover.py +++ b/homeassistant/components/ads/cover.py @@ -1,4 +1,6 @@ """Support for ADS covers.""" +from __future__ import annotations + import voluptuous as vol from homeassistant.components.cover import ( @@ -12,7 +14,10 @@ from homeassistant.components.cover import ( CoverEntity, ) from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import ( CONF_ADS_VAR, @@ -44,7 +49,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform(hass, config, add_entities, discovery_info=None): +def setup_platform( + hass: HomeAssistant, + config: ConfigType, + add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Set up the cover platform for ADS.""" ads_hub = hass.data[DATA_ADS] diff --git a/homeassistant/components/ads/light.py b/homeassistant/components/ads/light.py index fd6b5e66482..2508d486665 100644 --- a/homeassistant/components/ads/light.py +++ b/homeassistant/components/ads/light.py @@ -10,7 +10,10 @@ from homeassistant.components.light import ( LightEntity, ) from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import ( CONF_ADS_VAR, @@ -31,7 +34,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform(hass, config, add_entities, discovery_info=None): +def setup_platform( + hass: HomeAssistant, + config: ConfigType, + add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Set up the light platform for ADS.""" ads_hub = hass.data.get(DATA_ADS) diff --git a/homeassistant/components/ads/sensor.py b/homeassistant/components/ads/sensor.py index 26b04d86050..c0966535a84 100644 --- a/homeassistant/components/ads/sensor.py +++ b/homeassistant/components/ads/sensor.py @@ -1,11 +1,15 @@ """Support for ADS sensors.""" +from __future__ import annotations + import voluptuous as vol from homeassistant.components import ads from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import StateType +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType from . import CONF_ADS_FACTOR, CONF_ADS_TYPE, CONF_ADS_VAR, STATE_KEY_STATE, AdsEntity @@ -29,7 +33,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform(hass, config, add_entities, discovery_info=None): +def setup_platform( + hass: HomeAssistant, + config: ConfigType, + add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Set up an ADS sensor device.""" ads_hub = hass.data.get(ads.DATA_ADS) diff --git a/homeassistant/components/ads/switch.py b/homeassistant/components/ads/switch.py index 4888c876e1d..cea4655ca29 100644 --- a/homeassistant/components/ads/switch.py +++ b/homeassistant/components/ads/switch.py @@ -1,9 +1,14 @@ """Support for ADS switch platform.""" +from __future__ import annotations + import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE, AdsEntity @@ -17,7 +22,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform(hass, config, add_entities, discovery_info=None): +def setup_platform( + hass: HomeAssistant, + config: ConfigType, + add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Set up switch platform for ADS.""" ads_hub = hass.data.get(DATA_ADS) From 6eb31def084da542774046455fdb1ddff6470295 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 24 Dec 2021 15:09:27 +0100 Subject: [PATCH 1021/2644] CI: Use wheel to install base requirements (#62743) --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index be21087b50a..0df18ce3bf0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -170,7 +170,7 @@ jobs: run: | python -m venv venv . venv/bin/activate - pip install -U "pip<20.3" setuptools + pip install -U "pip<20.3" setuptools wheel pip install -r requirements.txt -r requirements_test.txt - name: Generate partial pre-commit restore key id: generate-pre-commit-key From 0da710c4e083379e7ab81b1f3fd1ba4219872bde Mon Sep 17 00:00:00 2001 From: Lorenzo Brescanzin <30345822+br3sc4@users.noreply.github.com> Date: Fri, 24 Dec 2021 19:39:37 +0100 Subject: [PATCH 1022/2644] Fix HomeKit sensor update check (#62705) --- homeassistant/components/homekit/type_sensors.py | 12 ++++++------ tests/components/homekit/test_type_sensors.py | 12 ++++++++++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 598d49155ec..881d91044ee 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -117,7 +117,7 @@ class TemperatureSensor(HomeAccessory): def async_update_state(self, new_state): """Update temperature after state changed.""" unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS) - if temperature := convert_to_float(new_state.state): + if (temperature := convert_to_float(new_state.state)) is not None: temperature = temperature_to_homekit(temperature, unit) self.char_temp.set_value(temperature) _LOGGER.debug( @@ -144,7 +144,7 @@ class HumiditySensor(HomeAccessory): @callback def async_update_state(self, new_state): """Update accessory after state change.""" - if humidity := convert_to_float(new_state.state): + if (humidity := convert_to_float(new_state.state)) is not None: self.char_humidity.set_value(humidity) _LOGGER.debug("%s: Percent set to %d%%", self.entity_id, humidity) @@ -171,7 +171,7 @@ class AirQualitySensor(HomeAccessory): @callback def async_update_state(self, new_state): """Update accessory after state change.""" - if density := convert_to_float(new_state.state): + if (density := convert_to_float(new_state.state)) is not None: if self.char_density.value != density: self.char_density.set_value(density) _LOGGER.debug("%s: Set density to %d", self.entity_id, density) @@ -206,7 +206,7 @@ class CarbonMonoxideSensor(HomeAccessory): @callback def async_update_state(self, new_state): """Update accessory after state change.""" - if value := convert_to_float(new_state.state): + if (value := convert_to_float(new_state.state)) is not None: self.char_level.set_value(value) if value > self.char_peak.value: self.char_peak.set_value(value) @@ -241,7 +241,7 @@ class CarbonDioxideSensor(HomeAccessory): @callback def async_update_state(self, new_state): """Update accessory after state change.""" - if value := convert_to_float(new_state.state): + if (value := convert_to_float(new_state.state)) is not None: self.char_level.set_value(value) if value > self.char_peak.value: self.char_peak.set_value(value) @@ -269,7 +269,7 @@ class LightSensor(HomeAccessory): @callback def async_update_state(self, new_state): """Update accessory after state change.""" - if luminance := convert_to_float(new_state.state): + if (luminance := convert_to_float(new_state.state)) is not None: self.char_light.set_value(luminance) _LOGGER.debug("%s: Set to %d", self.entity_id, luminance) diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index 0b51e44660c..958306e026f 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -61,6 +61,10 @@ async def test_temperature(hass, hk_driver): await hass.async_block_till_done() assert acc.char_temp.value == 20 + hass.states.async_set(entity_id, "0", {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + await hass.async_block_till_done() + assert acc.char_temp.value == 0 + hass.states.async_set( entity_id, "75.2", {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT} ) @@ -91,6 +95,10 @@ async def test_humidity(hass, hk_driver): await hass.async_block_till_done() assert acc.char_humidity.value == 20 + hass.states.async_set(entity_id, "0") + await hass.async_block_till_done() + assert acc.char_humidity.value == 0 + async def test_air_quality(hass, hk_driver): """Test if accessory is updated after state change.""" @@ -227,6 +235,10 @@ async def test_light(hass, hk_driver): await hass.async_block_till_done() assert acc.char_light.value == 300 + hass.states.async_set(entity_id, "0") + await hass.async_block_till_done() + assert acc.char_light.value == 0.0001 + async def test_binary(hass, hk_driver): """Test if accessory is updated after state change.""" From 78cc5f8d432992e9d6685f53125b882c23d6c749 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 24 Dec 2021 19:59:37 +0100 Subject: [PATCH 1023/2644] Upgrade vehicle to 0.3.1 (#62747) --- homeassistant/components/rdw/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rdw/manifest.json b/homeassistant/components/rdw/manifest.json index 757e54e97c7..c2e71185c64 100644 --- a/homeassistant/components/rdw/manifest.json +++ b/homeassistant/components/rdw/manifest.json @@ -3,7 +3,7 @@ "name": "RDW", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rdw", - "requirements": ["vehicle==0.3.0"], + "requirements": ["vehicle==0.3.1"], "codeowners": ["@frenck"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index f68da65b4af..d625f8bd760 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2392,7 +2392,7 @@ uvcclient==0.11.0 vallox-websocket-api==2.8.1 # homeassistant.components.rdw -vehicle==0.3.0 +vehicle==0.3.1 # homeassistant.components.velbus velbus-aio==2021.11.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3d37e20d0be..18e54795d16 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1423,7 +1423,7 @@ url-normalize==1.4.1 uvcclient==0.11.0 # homeassistant.components.rdw -vehicle==0.3.0 +vehicle==0.3.1 # homeassistant.components.velbus velbus-aio==2021.11.7 From 9dbba6b7f24a9ce66e291c2747d2ec3fac75f11a Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Sat, 25 Dec 2021 04:14:43 +0800 Subject: [PATCH 1024/2644] Use lock in Camera.create_stream (#62757) Rename create_stream to async_create_stream in Camera Rename get_image to async_get_image in Stream Rename get_image to async_get_image in KeyFrameConverter --- homeassistant/components/camera/__init__.py | 38 ++++++++++++--------- homeassistant/components/stream/__init__.py | 14 ++++++-- homeassistant/components/stream/core.py | 14 ++++---- tests/components/stream/test_worker.py | 2 +- 4 files changed, 40 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 45d0cf9371a..5c81ece6141 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -312,7 +312,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: camera_prefs = prefs.get(camera.entity_id) if not camera_prefs.preload_stream: continue - stream = await camera.create_stream() + stream = await camera.async_create_stream() if not stream: continue stream.keepalive = True @@ -390,6 +390,7 @@ class Camera(Entity): self.access_tokens: collections.deque = collections.deque([], 2) self._warned_old_signature = False self.async_update_token() + self._create_stream_lock: asyncio.Lock | None = None @property def entity_picture(self) -> str: @@ -454,22 +455,25 @@ class Camera(Entity): return self.stream.available return super().available - async def create_stream(self) -> Stream | None: + async def async_create_stream(self) -> Stream | None: """Create a Stream for stream_source.""" # There is at most one stream (a decode worker) per camera - if not self.stream: - async with async_timeout.timeout(CAMERA_STREAM_SOURCE_TIMEOUT): - source = await self.stream_source() - if not source: - return None - self.stream = create_stream( - self.hass, - source, - options=self.stream_options, - stream_label=self.entity_id, - ) - self.stream.set_update_callback(self.async_write_ha_state) - return self.stream + if not self._create_stream_lock: + self._create_stream_lock = asyncio.Lock() + async with self._create_stream_lock: + if not self.stream: + async with async_timeout.timeout(CAMERA_STREAM_SOURCE_TIMEOUT): + source = await self.stream_source() + if not source: + return None + self.stream = create_stream( + self.hass, + source, + options=self.stream_options, + stream_label=self.entity_id, + ) + self.stream.set_update_callback(self.async_write_ha_state) + return self.stream async def stream_source(self) -> str | None: """Return the source of the stream. @@ -918,7 +922,7 @@ async def async_handle_play_stream_service( async def _async_stream_endpoint_url( hass: HomeAssistant, camera: Camera, fmt: str ) -> str: - stream = await camera.create_stream() + stream = await camera.async_create_stream() if not stream: raise HomeAssistantError( f"{camera.entity_id} does not support play stream service" @@ -937,7 +941,7 @@ async def async_handle_record_service( camera: Camera, service_call: ServiceCall ) -> None: """Handle stream recording service calls.""" - stream = await camera.create_stream() + stream = await camera.async_create_stream() if not stream: raise HomeAssistantError(f"{camera.entity_id} does not support record service") diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index fec9731136f..1a4ce3d92e8 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -424,11 +424,19 @@ class Stream: await hls.recv() recorder.prepend(list(hls.get_segments())[-num_segments:]) - async def get_image( + async def async_get_image( self, width: int | None = None, height: int | None = None, ) -> bytes | None: - """Wrap get_image from KeyFrameConverter.""" + """ + Fetch an image from the Stream and return it as a jpeg in bytes. - return await self._keyframe_converter.get_image(width=width, height=height) + Calls async_get_image from KeyFrameConverter. async_get_image should only be + called directly from the main loop and not from an executor thread as it uses + hass.add_executor_job underneath the hood. + """ + + return await self._keyframe_converter.async_get_image( + width=width, height=height + ) diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index 08397fb6876..91414dd96d9 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -362,16 +362,16 @@ class StreamView(HomeAssistantView): class KeyFrameConverter: """ - Generate and hold the keyframe as a jpeg. + Enables generating and getting an image from the last keyframe seen in the stream. An overview of the thread and state interaction: the worker thread sets a packet - at any time, main loop can run a get_image call + get_image is called from the main asyncio loop + get_image schedules _generate_image in an executor thread _generate_image will try to create an image from the packet - Running _generate_image will clear the packet, so there will only - be one attempt per packet - If successful, _image will be updated and returned by get_image - If unsuccessful, get_image will return the previous image + _generate_image will clear the packet, so there will only be one attempt per packet + If successful, self._image will be updated and returned by get_image + If unsuccessful, get_image will return the previous image """ def __init__(self, hass: HomeAssistant) -> None: @@ -430,7 +430,7 @@ class KeyFrameConverter: bgr_array = frame.to_ndarray(format="bgr24") self._image = bytes(self._turbojpeg.encode(bgr_array)) - async def get_image( + async def async_get_image( self, width: int | None = None, height: int | None = None, diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index eb50e76a80a..6e35cc65b6f 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -892,6 +892,6 @@ async def test_get_image(hass, record_worker_sync): await record_worker_sync.join() - assert await stream.get_image() == EMPTY_8_6_JPEG + assert await stream.async_get_image() == EMPTY_8_6_JPEG stream.stop() From c37077aa9b714002d1e3e29e2d95123d59096cb1 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Fri, 24 Dec 2021 12:29:51 -0800 Subject: [PATCH 1025/2644] Add number entity to Overkiz integration (#62732) --- .coveragerc | 1 + homeassistant/components/overkiz/const.py | 1 + homeassistant/components/overkiz/entity.py | 5 +- homeassistant/components/overkiz/number.py | 103 +++++++++++++++++++++ 4 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/overkiz/number.py diff --git a/.coveragerc b/.coveragerc index 48c8425eed8..2113ea0c202 100644 --- a/.coveragerc +++ b/.coveragerc @@ -808,6 +808,7 @@ omit = homeassistant/components/overkiz/entity.py homeassistant/components/overkiz/executor.py homeassistant/components/overkiz/lock.py + homeassistant/components/overkiz/number.py homeassistant/components/overkiz/sensor.py homeassistant/components/ovo_energy/__init__.py homeassistant/components/ovo_energy/const.py diff --git a/homeassistant/components/overkiz/const.py b/homeassistant/components/overkiz/const.py index 60591d9d761..f1d1d0fdb74 100644 --- a/homeassistant/components/overkiz/const.py +++ b/homeassistant/components/overkiz/const.py @@ -20,6 +20,7 @@ UPDATE_INTERVAL_ALL_ASSUMED_STATE: Final = timedelta(minutes=60) PLATFORMS: list[Platform] = [ Platform.BUTTON, Platform.LOCK, + Platform.NUMBER, Platform.SENSOR, ] diff --git a/homeassistant/components/overkiz/entity.py b/homeassistant/components/overkiz/entity.py index 0c931bc5985..b88417b0d2b 100644 --- a/homeassistant/components/overkiz/entity.py +++ b/homeassistant/components/overkiz/entity.py @@ -7,9 +7,8 @@ from dataclasses import dataclass from pyoverkiz.enums import OverkizAttribute, OverkizState from pyoverkiz.models import Device -from homeassistant.components.button import ButtonEntityDescription from homeassistant.components.sensor import SensorEntityDescription -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN @@ -100,7 +99,7 @@ class OverkizDescriptiveEntity(OverkizEntity): self, device_url: str, coordinator: OverkizDataUpdateCoordinator, - description: OverkizSensorDescription | ButtonEntityDescription, + description: EntityDescription, ) -> None: """Initialize the device.""" super().__init__(device_url, coordinator) diff --git a/homeassistant/components/overkiz/number.py b/homeassistant/components/overkiz/number.py new file mode 100644 index 00000000000..d13370207fa --- /dev/null +++ b/homeassistant/components/overkiz/number.py @@ -0,0 +1,103 @@ +"""Support for Overkiz (virtual) numbers.""" +from __future__ import annotations + +from dataclasses import dataclass + +from pyoverkiz.enums import OverkizCommand, OverkizState + +from homeassistant.components.number import NumberEntity, NumberEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import HomeAssistantOverkizData +from .const import DOMAIN, IGNORED_OVERKIZ_DEVICES +from .entity import OverkizDescriptiveEntity + + +@dataclass +class OverkizNumberDescriptionMixin: + """Define an entity description mixin for number entities.""" + + command: str + + +@dataclass +class OverkizNumberDescription(NumberEntityDescription, OverkizNumberDescriptionMixin): + """Class to describe an Overkiz number.""" + + +NUMBER_DESCRIPTIONS: list[OverkizNumberDescription] = [ + # Cover: My Position (0 - 100) + OverkizNumberDescription( + key=OverkizState.CORE_MEMORIZED_1_POSITION, + name="My Position", + icon="mdi:content-save-cog", + command=OverkizCommand.SET_MEMORIZED_1_POSITION, + entity_category=EntityCategory.CONFIG, + ), + # WaterHeater: Expected Number Of Shower (2 - 4) + OverkizNumberDescription( + key=OverkizState.CORE_EXPECTED_NUMBER_OF_SHOWER, + name="Expected Number Of Shower", + icon="mdi:shower-head", + command=OverkizCommand.SET_EXPECTED_NUMBER_OF_SHOWER, + min_value=2, + max_value=4, + entity_category=EntityCategory.CONFIG, + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +): + """Set up the Overkiz number from a config entry.""" + data: HomeAssistantOverkizData = hass.data[DOMAIN][entry.entry_id] + entities: list[OverkizNumber] = [] + + key_supported_states = { + description.key: description for description in NUMBER_DESCRIPTIONS + } + + for device in data.coordinator.data.values(): + if ( + device.widget in IGNORED_OVERKIZ_DEVICES + or device.ui_class in IGNORED_OVERKIZ_DEVICES + ): + continue + + for state in device.definition.states: + if description := key_supported_states.get(state.qualified_name): + entities.append( + OverkizNumber( + device.device_url, + data.coordinator, + description, + ) + ) + + async_add_entities(entities) + + +class OverkizNumber(OverkizDescriptiveEntity, NumberEntity): + """Representation of an Overkiz Number.""" + + entity_description: OverkizNumberDescription + + @property + def value(self) -> float: + """Return the entity value to represent the entity state.""" + if state := self.device.states.get(self.entity_description.key): + return state.value + + return 0 + + async def async_set_value(self, value: float) -> None: + """Set new value.""" + await self.executor.async_execute_command( + self.entity_description.command, value + ) From a1be11a49276d73586d36e3362ca86fbb9d1fa65 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Fri, 24 Dec 2021 12:31:42 -0800 Subject: [PATCH 1026/2644] Store wemo device sw_version & upnp connections (#62758) --- homeassistant/components/wemo/wemo_device.py | 7 ++++++- tests/components/wemo/conftest.py | 3 +++ tests/components/wemo/test_init.py | 10 +++++++++- tests/components/wemo/test_wemo_device.py | 16 +++++++++++++++- 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wemo/wemo_device.py b/homeassistant/components/wemo/wemo_device.py index b7138cb0a94..267ea84e308 100644 --- a/homeassistant/components/wemo/wemo_device.py +++ b/homeassistant/components/wemo/wemo_device.py @@ -16,7 +16,10 @@ from homeassistant.const import ( CONF_UNIQUE_ID, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.device_registry import async_get as async_get_device_registry +from homeassistant.helpers.device_registry import ( + CONNECTION_UPNP, + async_get as async_get_device_registry, +) from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -123,10 +126,12 @@ class DeviceCoordinator(DataUpdateCoordinator): def _device_info(wemo: WeMoDevice) -> DeviceInfo: return DeviceInfo( + connections={(CONNECTION_UPNP, wemo.udn)}, identifiers={(DOMAIN, wemo.serialnumber)}, manufacturer="Belkin", model=wemo.model_name, name=wemo.name, + sw_version=wemo.firmware_version, ) diff --git a/tests/components/wemo/conftest.py b/tests/components/wemo/conftest.py index 13ec0cb2337..cf974f523a8 100644 --- a/tests/components/wemo/conftest.py +++ b/tests/components/wemo/conftest.py @@ -14,6 +14,7 @@ MOCK_HOST = "127.0.0.1" MOCK_PORT = 50000 MOCK_NAME = "WemoDeviceName" MOCK_SERIAL_NUMBER = "WemoSerialNumber" +MOCK_FIRMWARE_VERSION = "WeMo_WW_2.00.XXXXX.PVT-OWRT" @pytest.fixture(name="pywemo_model") @@ -58,6 +59,8 @@ def pywemo_device_fixture(pywemo_registry, pywemo_model): device.name = MOCK_NAME device.serialnumber = MOCK_SERIAL_NUMBER device.model_name = pywemo_model.replace("LongPress", "") + device.udn = f"uuid:{device.model_name}-1_0-{device.serialnumber}" + device.firmware_version = MOCK_FIRMWARE_VERSION device.get_state.return_value = 0 # Default to Off device.supports_long_press.return_value = cls.supports_long_press() diff --git a/tests/components/wemo/test_init.py b/tests/components/wemo/test_init.py index f34e9bd0471..6cd415792b5 100644 --- a/tests/components/wemo/test_init.py +++ b/tests/components/wemo/test_init.py @@ -10,7 +10,13 @@ from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util import dt -from .conftest import MOCK_HOST, MOCK_NAME, MOCK_PORT, MOCK_SERIAL_NUMBER +from .conftest import ( + MOCK_FIRMWARE_VERSION, + MOCK_HOST, + MOCK_NAME, + MOCK_PORT, + MOCK_SERIAL_NUMBER, +) from tests.common import async_fire_time_changed @@ -109,6 +115,8 @@ async def test_discovery(hass, pywemo_registry): device.name = f"{MOCK_NAME}_{counter}" device.serialnumber = f"{MOCK_SERIAL_NUMBER}_{counter}" device.model_name = "Motion" + device.udn = f"uuid:{device.model_name}-1_0-{device.serialnumber}" + device.firmware_version = MOCK_FIRMWARE_VERSION device.get_state.return_value = 0 # Default to Off device.supports_long_press.return_value = False return device diff --git a/tests/components/wemo/test_wemo_device.py b/tests/components/wemo/test_wemo_device.py index 9ef9e6b5685..9bd3367aeee 100644 --- a/tests/components/wemo/test_wemo_device.py +++ b/tests/components/wemo/test_wemo_device.py @@ -17,7 +17,7 @@ from homeassistant.helpers.update_coordinator import UpdateFailed from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from .conftest import MOCK_HOST +from .conftest import MOCK_FIRMWARE_VERSION, MOCK_HOST, MOCK_SERIAL_NUMBER from tests.common import async_fire_time_changed @@ -154,6 +154,20 @@ async def test_async_update_data_subscribed( pywemo_device.get_state.assert_not_called() +async def test_device_info(hass, wemo_entity): + """Verify the DeviceInfo data is set properly.""" + dr = device_registry.async_get(hass) + device_entries = list(dr.devices.values()) + + assert len(device_entries) == 1 + assert device_entries[0].connections == { + ("upnp", f"uuid:LightSwitch-1_0-{MOCK_SERIAL_NUMBER}") + } + assert device_entries[0].manufacturer == "Belkin" + assert device_entries[0].model == "LightSwitch" + assert device_entries[0].sw_version == MOCK_FIRMWARE_VERSION + + class TestInsight: """Tests specific to the WeMo Insight device.""" From 64d1a7382f903de7e77988a9133f56b670eba43e Mon Sep 17 00:00:00 2001 From: Evgeny Date: Fri, 24 Dec 2021 21:34:49 +0100 Subject: [PATCH 1027/2644] Bump roombapy to 1.6.4 (#62741) --- homeassistant/components/roomba/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roomba/manifest.json b/homeassistant/components/roomba/manifest.json index 907026fd77e..ad5857aa630 100644 --- a/homeassistant/components/roomba/manifest.json +++ b/homeassistant/components/roomba/manifest.json @@ -3,7 +3,7 @@ "name": "iRobot Roomba and Braava", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/roomba", - "requirements": ["roombapy==1.6.4"], + "requirements": ["roombapy==1.6.5"], "codeowners": ["@pschmitt", "@cyr-ius", "@shenxn"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index d625f8bd760..3bb5004e3a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2093,7 +2093,7 @@ rocketchat-API==0.6.1 rokuecp==0.8.5 # homeassistant.components.roomba -roombapy==1.6.4 +roombapy==1.6.5 # homeassistant.components.roon roonapi==0.0.38 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 18e54795d16..9c52d554e83 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1255,7 +1255,7 @@ ring_doorbell==0.7.2 rokuecp==0.8.5 # homeassistant.components.roomba -roombapy==1.6.4 +roombapy==1.6.5 # homeassistant.components.roon roonapi==0.0.38 From 0062676f61297d4679b96b66a92acc7231539fb2 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 24 Dec 2021 16:48:02 -0500 Subject: [PATCH 1028/2644] Add lqi and rssi sensors back to ZHA (#62716) * update device list * Only 1 identify button per device * Add LQI and RSSI sensors to ZHA * refactor entity creation filter * update device list and update discover test * fix reference * code reduction * walrus * parens * simplify --- homeassistant/components/zha/button.py | 7 +- .../components/zha/core/registries.py | 9 + homeassistant/components/zha/entity.py | 12 +- homeassistant/components/zha/sensor.py | 44 + tests/components/zha/test_discover.py | 10 +- tests/components/zha/zha_devices_list.py | 1214 ++++++++++++++++- 6 files changed, 1260 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/zha/button.py b/homeassistant/components/zha/button.py index 674adbee0d9..90148ba42f3 100644 --- a/homeassistant/components/zha/button.py +++ b/homeassistant/components/zha/button.py @@ -89,11 +89,10 @@ class ZHAIdentifyButton(ZHAButton): Return entity if it is a supported configuration, otherwise return None """ - platform_restrictions = ZHA_ENTITIES.single_device_matches[Platform.BUTTON] - device_restrictions = platform_restrictions[zha_device.ieee] - if CHANNEL_IDENTIFY in device_restrictions: + if ZHA_ENTITIES.prevent_entity_creation( + Platform.BUTTON, zha_device.ieee, CHANNEL_IDENTIFY + ): return None - device_restrictions.append(CHANNEL_IDENTIFY) return cls(unique_id, zha_device, channels, **kwargs) _attr_device_class: ButtonDeviceClass = ButtonDeviceClass.UPDATE diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 571a304b546..1480469ce2c 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -346,6 +346,15 @@ class ZHAEntityRegistry: return decorator + def prevent_entity_creation(self, platform: Platform, ieee: EUI64, key: str): + """Return True if the entity should not be created.""" + platform_restrictions = self.single_device_matches[platform] + device_restrictions = platform_restrictions[ieee] + if key in device_restrictions: + return True + device_restrictions.append(key) + return False + def clean_up(self) -> None: """Clean up post discovery.""" self.single_device_matches: dict[ diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 80697c704bf..ee5d2939185 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -41,7 +41,7 @@ UPDATE_GROUP_FROM_CHILD_DELAY = 0.5 class BaseZhaEntity(LogMixin, entity.Entity): """A base class for ZHA entities.""" - _unique_id_suffix: str | None = None + unique_id_suffix: str | None = None def __init__(self, unique_id: str, zha_device: ZhaDeviceType, **kwargs) -> None: """Init ZHA entity.""" @@ -49,8 +49,8 @@ class BaseZhaEntity(LogMixin, entity.Entity): self._force_update: bool = False self._should_poll: bool = False self._unique_id: str = unique_id - if self._unique_id_suffix: - self._unique_id += f"-{self._unique_id_suffix}" + if self.unique_id_suffix: + self._unique_id += f"-{self.unique_id_suffix}" self._state: Any = None self._extra_state_attributes: dict[str, Any] = {} self._zha_device: ZhaDeviceType = zha_device @@ -154,7 +154,7 @@ class ZhaEntity(BaseZhaEntity, RestoreEntity): """ super().__init_subclass__(**kwargs) if id_suffix: - cls._unique_id_suffix = id_suffix + cls.unique_id_suffix = id_suffix def __init__( self, @@ -169,8 +169,8 @@ class ZhaEntity(BaseZhaEntity, RestoreEntity): ch_names = [ch.cluster.ep_attribute for ch in channels] ch_names = ", ".join(sorted(ch_names)) self._name: str = f"{zha_device.name} {ieeetail} {ch_names}" - if self._unique_id_suffix: - self._name += f" {self._unique_id_suffix}" + if self.unique_id_suffix: + self._name += f" {self.unique_id_suffix}" self.cluster_channels: dict[str, ChannelType] = {} for channel in channels: self.cluster_channels[channel.name] = channel diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 8a4301b12fb..8b2de754ad8 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -25,6 +25,7 @@ from homeassistant.const import ( ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, + ENTITY_CATEGORY_DIAGNOSTIC, LIGHT_LUX, PERCENTAGE, POWER_VOLT_AMPERE, @@ -50,6 +51,7 @@ from homeassistant.helpers.typing import StateType from .core import discovery from .core.const import ( CHANNEL_ANALOG_INPUT, + CHANNEL_BASIC, CHANNEL_ELECTRICAL_MEASUREMENT, CHANNEL_HUMIDITY, CHANNEL_ILLUMINANCE, @@ -675,3 +677,45 @@ class SinopeHVACAction(ThermostatHVACAction): ): return CURRENT_HVAC_IDLE return CURRENT_HVAC_OFF + + +@MULTI_MATCH(channel_names=CHANNEL_BASIC) +class RSSISensor(Sensor, id_suffix="rssi"): + """RSSI sensor for a device.""" + + _state_class: SensorStateClass = SensorStateClass.MEASUREMENT + _device_class: SensorDeviceClass = SensorDeviceClass.SIGNAL_STRENGTH + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + _attr_entity_registry_enabled_default = False + + @classmethod + def create_entity( + cls, + unique_id: str, + zha_device: ZhaDeviceType, + channels: list[ChannelType], + **kwargs, + ) -> ZhaEntity | None: + """Entity Factory. + + Return entity if it is a supported configuration, otherwise return None + """ + key = f"{CHANNEL_BASIC}_{cls.unique_id_suffix}" + if ZHA_ENTITIES.prevent_entity_creation(Platform.SENSOR, zha_device.ieee, key): + return None + return cls(unique_id, zha_device, channels, **kwargs) + + @property + def native_value(self) -> StateType: + """Return the state of the entity.""" + return getattr(self._zha_device.device, self.unique_id_suffix) + + @property + def should_poll(self) -> bool: + """Poll the entity for current state.""" + return True + + +@MULTI_MATCH(channel_names=CHANNEL_BASIC) +class LQISensor(RSSISensor, id_suffix="lqi"): + """LQI sensor for a device.""" diff --git a/tests/components/zha/test_discover.py b/tests/components/zha/test_discover.py index 887c390aced..fd489d7c5d5 100644 --- a/tests/components/zha/test_discover.py +++ b/tests/components/zha/test_discover.py @@ -2,7 +2,7 @@ import re from unittest import mock -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, Mock, patch import pytest from zigpy.const import SIG_ENDPOINTS, SIG_MANUFACTURER, SIG_MODEL, SIG_NODE_DESC @@ -70,6 +70,14 @@ def channels_mock(zha_device_mock): "zigpy.zcl.clusters.general.Identify.request", new=AsyncMock(return_value=[mock.sentinel.data, zcl_f.Status.SUCCESS]), ) +# We do this here because we are testing ZHA discovery logic. Point being we want to ensure that +# all discovered entities are dispatched for creation. In order to test this we need the entities +# added to HA. So we ensure that they are all enabled even though they won't necessarily be in reality +# at runtime +@patch( + "homeassistant.components.zha.entity.ZhaEntity.entity_registry_enabled_default", + new=Mock(return_value=True), +) @pytest.mark.parametrize("device", DEVICES) async def test_devices( device, diff --git a/tests/components/zha/zha_devices_list.py b/tests/components/zha/zha_devices_list.py index eb6f05bc5c6..90ee54197ea 100644 --- a/tests/components/zha/zha_devices_list.py +++ b/tests/components/zha/zha_devices_list.py @@ -39,6 +39,8 @@ DEVICES = [ DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008"], DEV_SIG_ENTITIES: [ "button.adurolight_adurolight_ncc_77665544_identify", + "sensor.adurolight_adurolight_ncc_77665544_basic_rssi", + "sensor.adurolight_adurolight_ncc_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { @@ -46,6 +48,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", DEV_SIG_ENT_MAP_ID: "button.adurolight_adurolight_ncc_77665544_identify", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.adurolight_adurolight_ncc_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.adurolight_adurolight_ncc_77665544_basic_lqi", + }, }, }, { @@ -68,6 +80,8 @@ DEVICES = [ "sensor.bosch_isw_zpr1_wp13_77665544_power", "sensor.bosch_isw_zpr1_wp13_77665544_temperature", "binary_sensor.bosch_isw_zpr1_wp13_77665544_ias_zone", + "sensor.bosch_isw_zpr1_wp13_77665544_basic_rssi", + "sensor.bosch_isw_zpr1_wp13_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-5-1280"): { @@ -90,6 +104,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Temperature", DEV_SIG_ENT_MAP_ID: "sensor.bosch_isw_zpr1_wp13_77665544_temperature", }, + ("sensor", "00:11:22:33:44:55:66:77-5-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.bosch_isw_zpr1_wp13_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-5-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.bosch_isw_zpr1_wp13_77665544_basic_lqi", + }, }, }, { @@ -110,6 +134,8 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "button.centralite_3130_77665544_identify", "sensor.centralite_3130_77665544_power", + "sensor.centralite_3130_77665544_basic_rssi", + "sensor.centralite_3130_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { @@ -122,6 +148,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.centralite_3130_77665544_power", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3130_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3130_77665544_basic_lqi", + }, }, }, { @@ -148,6 +184,8 @@ DEVICES = [ "sensor.centralite_3210_l_77665544_smartenergy_metering", "sensor.centralite_3210_l_77665544_smartenergy_metering_summation_delivered", "switch.centralite_3210_l_77665544_on_off", + "sensor.centralite_3210_l_77665544_basic_rssi", + "sensor.centralite_3210_l_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("switch", "00:11:22:33:44:55:66:77-1"): { @@ -190,6 +228,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_smartenergy_metering_summation_delivered", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_basic_lqi", + }, }, }, { @@ -212,6 +260,8 @@ DEVICES = [ "sensor.centralite_3310_s_77665544_power", "sensor.centralite_3310_s_77665544_temperature", "sensor.centralite_3310_s_77665544_manufacturer_specific", + "sensor.centralite_3310_s_77665544_basic_rssi", + "sensor.centralite_3310_s_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { @@ -229,6 +279,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Temperature", DEV_SIG_ENT_MAP_ID: "sensor.centralite_3310_s_77665544_temperature", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3310_s_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3310_s_77665544_basic_lqi", + }, ("sensor", "00:11:22:33:44:55:66:77-1-64581"): { DEV_SIG_CHANNELS: ["manufacturer_specific"], DEV_SIG_ENT_MAP_CLASS: "Humidity", @@ -263,6 +323,8 @@ DEVICES = [ "sensor.centralite_3315_s_77665544_power", "sensor.centralite_3315_s_77665544_temperature", "binary_sensor.centralite_3315_s_77665544_ias_zone", + "sensor.centralite_3315_s_77665544_basic_rssi", + "sensor.centralite_3315_s_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { @@ -285,6 +347,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Temperature", DEV_SIG_ENT_MAP_ID: "sensor.centralite_3315_s_77665544_temperature", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3315_s_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3315_s_77665544_basic_lqi", + }, }, }, { @@ -314,6 +386,8 @@ DEVICES = [ "sensor.centralite_3320_l_77665544_power", "sensor.centralite_3320_l_77665544_temperature", "binary_sensor.centralite_3320_l_77665544_ias_zone", + "sensor.centralite_3320_l_77665544_basic_rssi", + "sensor.centralite_3320_l_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { @@ -336,6 +410,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Temperature", DEV_SIG_ENT_MAP_ID: "sensor.centralite_3320_l_77665544_temperature", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3320_l_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3320_l_77665544_basic_lqi", + }, }, }, { @@ -365,6 +449,8 @@ DEVICES = [ "sensor.centralite_3326_l_77665544_power", "sensor.centralite_3326_l_77665544_temperature", "binary_sensor.centralite_3326_l_77665544_ias_zone", + "sensor.centralite_3326_l_77665544_basic_rssi", + "sensor.centralite_3326_l_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { @@ -387,6 +473,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Temperature", DEV_SIG_ENT_MAP_ID: "sensor.centralite_3326_l_77665544_temperature", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3326_l_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3326_l_77665544_basic_lqi", + }, }, }, { @@ -412,11 +508,13 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ + "button.centralite_motion_sensor_a_77665544_identify", "sensor.centralite_motion_sensor_a_77665544_power", "sensor.centralite_motion_sensor_a_77665544_temperature", - "button.centralite_motion_sensor_a_77665544_identify", "binary_sensor.centralite_motion_sensor_a_77665544_ias_zone", "binary_sensor.centralite_motion_sensor_a_77665544_occupancy", + "sensor.centralite_motion_sensor_a_77665544_basic_rssi", + "sensor.centralite_motion_sensor_a_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { @@ -439,6 +537,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Temperature", DEV_SIG_ENT_MAP_ID: "sensor.centralite_motion_sensor_a_77665544_temperature", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_motion_sensor_a_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_motion_sensor_a_77665544_basic_lqi", + }, ("binary_sensor", "00:11:22:33:44:55:66:77-2-1030"): { DEV_SIG_CHANNELS: ["occupancy"], DEV_SIG_ENT_MAP_CLASS: "Occupancy", @@ -473,6 +581,8 @@ DEVICES = [ "sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_smartenergy_metering", "sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_smartenergy_metering_summation_delivered", "switch.climaxtechnology_psmp5_00_00_02_02tc_77665544_on_off", + "sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_basic_rssi", + "sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("switch", "00:11:22:33:44:55:66:77-1"): { @@ -495,6 +605,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_smartenergy_metering_summation_delivered", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_basic_lqi", + }, }, }, { @@ -515,6 +635,8 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "button.climaxtechnology_sd8sc_00_00_03_12tc_77665544_identify", "binary_sensor.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_zone", + "sensor.climaxtechnology_sd8sc_00_00_03_12tc_77665544_basic_rssi", + "sensor.climaxtechnology_sd8sc_00_00_03_12tc_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { @@ -527,6 +649,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", DEV_SIG_ENT_MAP_ID: "button.climaxtechnology_sd8sc_00_00_03_12tc_77665544_identify", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_sd8sc_00_00_03_12tc_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_sd8sc_00_00_03_12tc_77665544_basic_lqi", + }, }, }, { @@ -547,6 +679,8 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "button.climaxtechnology_ws15_00_00_03_03tc_77665544_identify", "binary_sensor.climaxtechnology_ws15_00_00_03_03tc_77665544_ias_zone", + "sensor.climaxtechnology_ws15_00_00_03_03tc_77665544_basic_rssi", + "sensor.climaxtechnology_ws15_00_00_03_03tc_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { @@ -559,6 +693,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", DEV_SIG_ENT_MAP_ID: "button.climaxtechnology_ws15_00_00_03_03tc_77665544_identify", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_ws15_00_00_03_03tc_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_ws15_00_00_03_03tc_77665544_basic_lqi", + }, }, }, { @@ -586,10 +730,12 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "button.feibit_inc_co_fb56_zcw08ku1_1_77665544_identify", "light.feibit_inc_co_fb56_zcw08ku1_1_77665544_level_light_color_on_off", + "sensor.feibit_inc_co_fb56_zcw08ku1_1_77665544_basic_rssi", + "sensor.feibit_inc_co_fb56_zcw08ku1_1_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-11"): { - DEV_SIG_CHANNELS: ["on_off", "level", "light_color"], + DEV_SIG_CHANNELS: ["on_off", "light_color", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", DEV_SIG_ENT_MAP_ID: "light.feibit_inc_co_fb56_zcw08ku1_1_77665544_level_light_color_on_off", }, @@ -598,6 +744,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", DEV_SIG_ENT_MAP_ID: "button.feibit_inc_co_fb56_zcw08ku1_1_77665544_identify", }, + ("sensor", "00:11:22:33:44:55:66:77-11-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.feibit_inc_co_fb56_zcw08ku1_1_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-11-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.feibit_inc_co_fb56_zcw08ku1_1_77665544_basic_lqi", + }, }, }, { @@ -619,6 +775,8 @@ DEVICES = [ "button.heiman_smokesensor_em_77665544_identify", "sensor.heiman_smokesensor_em_77665544_power", "binary_sensor.heiman_smokesensor_em_77665544_ias_zone", + "sensor.heiman_smokesensor_em_77665544_basic_rssi", + "sensor.heiman_smokesensor_em_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { @@ -636,6 +794,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.heiman_smokesensor_em_77665544_power", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.heiman_smokesensor_em_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.heiman_smokesensor_em_77665544_basic_lqi", + }, }, }, { @@ -656,6 +824,8 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "button.heiman_co_v16_77665544_identify", "binary_sensor.heiman_co_v16_77665544_ias_zone", + "sensor.heiman_co_v16_77665544_basic_rssi", + "sensor.heiman_co_v16_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { @@ -668,6 +838,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", DEV_SIG_ENT_MAP_ID: "button.heiman_co_v16_77665544_identify", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.heiman_co_v16_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.heiman_co_v16_77665544_basic_lqi", + }, }, }, { @@ -689,6 +869,8 @@ DEVICES = [ "button.heiman_warningdevice_77665544_identify", "siren.heiman_warningdevice_77665544_ias_wd", "binary_sensor.heiman_warningdevice_77665544_ias_zone", + "sensor.heiman_warningdevice_77665544_basic_rssi", + "sensor.heiman_warningdevice_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("siren", "00:11:22:33:44:55:66:77-1"): { @@ -706,6 +888,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", DEV_SIG_ENT_MAP_ID: "button.heiman_warningdevice_77665544_identify", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.heiman_warningdevice_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.heiman_warningdevice_77665544_basic_lqi", + }, }, }, { @@ -729,6 +921,8 @@ DEVICES = [ "sensor.hivehome_com_mot003_77665544_illuminance", "sensor.hivehome_com_mot003_77665544_temperature", "binary_sensor.hivehome_com_mot003_77665544_ias_zone", + "sensor.hivehome_com_mot003_77665544_basic_rssi", + "sensor.hivehome_com_mot003_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-6-1280"): { @@ -756,6 +950,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Temperature", DEV_SIG_ENT_MAP_ID: "sensor.hivehome_com_mot003_77665544_temperature", }, + ("sensor", "00:11:22:33:44:55:66:77-6-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.hivehome_com_mot003_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-6-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.hivehome_com_mot003_77665544_basic_lqi", + }, }, }, { @@ -783,10 +987,12 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "button.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_identify", "light.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_level_light_color_on_off", + "sensor.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_basic_rssi", + "sensor.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CHANNELS: ["on_off", "light_color", "level"], + DEV_SIG_CHANNELS: ["on_off", "level", "light_color"], DEV_SIG_ENT_MAP_CLASS: "Light", DEV_SIG_ENT_MAP_ID: "light.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_level_light_color_on_off", }, @@ -795,6 +1001,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_identify", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_basic_lqi", + }, }, }, { @@ -815,10 +1031,12 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "button.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_identify", "light.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_level_light_color_on_off", + "sensor.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_basic_rssi", + "sensor.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CHANNELS: ["on_off", "light_color", "level"], + DEV_SIG_CHANNELS: ["on_off", "level", "light_color"], DEV_SIG_ENT_MAP_CLASS: "Light", DEV_SIG_ENT_MAP_ID: "light.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_level_light_color_on_off", }, @@ -827,6 +1045,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_identify", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_basic_lqi", + }, }, }, { @@ -847,6 +1075,8 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "button.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_77665544_identify", "light.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_77665544_level_on_off", + "sensor.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_77665544_basic_rssi", + "sensor.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { @@ -859,6 +1089,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_77665544_identify", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_77665544_basic_lqi", + }, }, }, { @@ -879,10 +1119,12 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "button.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_identify", "light.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_level_light_color_on_off", + "sensor.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_basic_rssi", + "sensor.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CHANNELS: ["on_off", "light_color", "level"], + DEV_SIG_CHANNELS: ["on_off", "level", "light_color"], DEV_SIG_ENT_MAP_CLASS: "Light", DEV_SIG_ENT_MAP_ID: "light.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_level_light_color_on_off", }, @@ -891,6 +1133,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_identify", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_basic_lqi", + }, }, }, { @@ -911,6 +1163,8 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "button.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_77665544_identify", "light.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_77665544_level_on_off", + "sensor.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_77665544_basic_rssi", + "sensor.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { @@ -923,6 +1177,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_77665544_identify", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_77665544_basic_lqi", + }, }, }, { @@ -943,6 +1207,8 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "button.ikea_of_sweden_tradfri_control_outlet_77665544_identify", "switch.ikea_of_sweden_tradfri_control_outlet_77665544_on_off", + "sensor.ikea_of_sweden_tradfri_control_outlet_77665544_basic_rssi", + "sensor.ikea_of_sweden_tradfri_control_outlet_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("switch", "00:11:22:33:44:55:66:77-1"): { @@ -955,6 +1221,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_control_outlet_77665544_identify", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_control_outlet_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_control_outlet_77665544_basic_lqi", + }, }, }, { @@ -976,6 +1252,8 @@ DEVICES = [ "button.ikea_of_sweden_tradfri_motion_sensor_77665544_identify", "sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_power", "binary_sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_on_off", + "sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_basic_rssi", + "sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { @@ -988,6 +1266,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_power", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_basic_lqi", + }, ("binary_sensor", "00:11:22:33:44:55:66:77-1-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Motion", @@ -1013,6 +1301,8 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "button.ikea_of_sweden_tradfri_on_off_switch_77665544_identify", "sensor.ikea_of_sweden_tradfri_on_off_switch_77665544_power", + "sensor.ikea_of_sweden_tradfri_on_off_switch_77665544_basic_rssi", + "sensor.ikea_of_sweden_tradfri_on_off_switch_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { @@ -1025,6 +1315,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_on_off_switch_77665544_power", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_on_off_switch_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_on_off_switch_77665544_basic_lqi", + }, }, }, { @@ -1045,6 +1345,8 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "button.ikea_of_sweden_tradfri_remote_control_77665544_identify", "sensor.ikea_of_sweden_tradfri_remote_control_77665544_power", + "sensor.ikea_of_sweden_tradfri_remote_control_77665544_basic_rssi", + "sensor.ikea_of_sweden_tradfri_remote_control_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { @@ -1057,6 +1359,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_remote_control_77665544_power", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_remote_control_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_remote_control_77665544_basic_lqi", + }, }, }, { @@ -1083,6 +1395,8 @@ DEVICES = [ DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ "button.ikea_of_sweden_tradfri_signal_repeater_77665544_identify", + "sensor.ikea_of_sweden_tradfri_signal_repeater_77665544_basic_rssi", + "sensor.ikea_of_sweden_tradfri_signal_repeater_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { @@ -1090,6 +1404,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_signal_repeater_77665544_identify", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_signal_repeater_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_signal_repeater_77665544_basic_lqi", + }, }, }, { @@ -1110,6 +1434,8 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "button.ikea_of_sweden_tradfri_wireless_dimmer_77665544_identify", "sensor.ikea_of_sweden_tradfri_wireless_dimmer_77665544_power", + "sensor.ikea_of_sweden_tradfri_wireless_dimmer_77665544_basic_rssi", + "sensor.ikea_of_sweden_tradfri_wireless_dimmer_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { @@ -1122,6 +1448,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_wireless_dimmer_77665544_power", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_wireless_dimmer_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_wireless_dimmer_77665544_basic_lqi", + }, }, }, { @@ -1148,9 +1484,11 @@ DEVICES = [ DEV_SIG_EVT_CHANNELS: ["1:0x0019", "2:0x0006", "2:0x0008"], DEV_SIG_ENTITIES: [ "button.jasco_products_45852_77665544_identify", - "light.jasco_products_45852_77665544_level_on_off", "sensor.jasco_products_45852_77665544_smartenergy_metering", "sensor.jasco_products_45852_77665544_smartenergy_metering_summation_delivered", + "light.jasco_products_45852_77665544_level_on_off", + "sensor.jasco_products_45852_77665544_basic_rssi", + "sensor.jasco_products_45852_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { @@ -1173,6 +1511,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_77665544_smartenergy_metering_summation_delivered", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_77665544_basic_lqi", + }, }, }, { @@ -1202,6 +1550,8 @@ DEVICES = [ "light.jasco_products_45856_77665544_on_off", "sensor.jasco_products_45856_77665544_smartenergy_metering", "sensor.jasco_products_45856_77665544_smartenergy_metering_summation_delivered", + "sensor.jasco_products_45856_77665544_basic_rssi", + "sensor.jasco_products_45856_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { @@ -1224,6 +1574,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_77665544_smartenergy_metering_summation_delivered", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_77665544_basic_lqi", + }, }, }, { @@ -1253,6 +1613,8 @@ DEVICES = [ "light.jasco_products_45857_77665544_level_on_off", "sensor.jasco_products_45857_77665544_smartenergy_metering", "sensor.jasco_products_45857_77665544_smartenergy_metering_summation_delivered", + "sensor.jasco_products_45857_77665544_basic_rssi", + "sensor.jasco_products_45857_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { @@ -1275,6 +1637,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_77665544_smartenergy_metering_summation_delivered", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_77665544_basic_lqi", + }, }, }, { @@ -1298,6 +1670,8 @@ DEVICES = [ "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_pressure", "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_temperature", "cover.keen_home_inc_sv02_610_mp_1_3_77665544_level_on_off", + "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_basic_rssi", + "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { @@ -1325,6 +1699,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Temperature", DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_temperature", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_basic_lqi", + }, }, }, { @@ -1348,6 +1732,8 @@ DEVICES = [ "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_pressure", "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_temperature", "cover.keen_home_inc_sv02_612_mp_1_2_77665544_level_on_off", + "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_basic_rssi", + "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { @@ -1375,6 +1761,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Temperature", DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_temperature", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_basic_lqi", + }, }, }, { @@ -1398,6 +1794,8 @@ DEVICES = [ "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_pressure", "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_temperature", "cover.keen_home_inc_sv02_612_mp_1_3_77665544_level_on_off", + "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_basic_rssi", + "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { @@ -1425,6 +1823,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Temperature", DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_temperature", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_basic_lqi", + }, }, }, { @@ -1446,6 +1854,8 @@ DEVICES = [ "button.king_of_fans_inc_hbuniversalcfremote_77665544_identify", "light.king_of_fans_inc_hbuniversalcfremote_77665544_level_on_off", "fan.king_of_fans_inc_hbuniversalcfremote_77665544_fan", + "sensor.king_of_fans_inc_hbuniversalcfremote_77665544_basic_rssi", + "sensor.king_of_fans_inc_hbuniversalcfremote_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { @@ -1458,6 +1868,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", DEV_SIG_ENT_MAP_ID: "button.king_of_fans_inc_hbuniversalcfremote_77665544_identify", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.king_of_fans_inc_hbuniversalcfremote_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.king_of_fans_inc_hbuniversalcfremote_77665544_basic_lqi", + }, ("fan", "00:11:22:33:44:55:66:77-1-514"): { DEV_SIG_CHANNELS: ["fan"], DEV_SIG_ENT_MAP_CLASS: "ZhaFan", @@ -1483,6 +1903,8 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "button.lds_zbt_cctswitch_d0001_77665544_identify", "sensor.lds_zbt_cctswitch_d0001_77665544_power", + "sensor.lds_zbt_cctswitch_d0001_77665544_basic_rssi", + "sensor.lds_zbt_cctswitch_d0001_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { @@ -1495,6 +1917,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.lds_zbt_cctswitch_d0001_77665544_power", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lds_zbt_cctswitch_d0001_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lds_zbt_cctswitch_d0001_77665544_basic_lqi", + }, }, }, { @@ -1515,10 +1947,12 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "button.ledvance_a19_rgbw_77665544_identify", "light.ledvance_a19_rgbw_77665544_level_light_color_on_off", + "sensor.ledvance_a19_rgbw_77665544_basic_rssi", + "sensor.ledvance_a19_rgbw_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CHANNELS: ["on_off", "light_color", "level"], + DEV_SIG_CHANNELS: ["on_off", "level", "light_color"], DEV_SIG_ENT_MAP_CLASS: "Light", DEV_SIG_ENT_MAP_ID: "light.ledvance_a19_rgbw_77665544_level_light_color_on_off", }, @@ -1527,6 +1961,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", DEV_SIG_ENT_MAP_ID: "button.ledvance_a19_rgbw_77665544_identify", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.ledvance_a19_rgbw_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.ledvance_a19_rgbw_77665544_basic_lqi", + }, }, }, { @@ -1547,10 +1991,12 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "button.ledvance_flex_rgbw_77665544_identify", "light.ledvance_flex_rgbw_77665544_level_light_color_on_off", + "sensor.ledvance_flex_rgbw_77665544_basic_rssi", + "sensor.ledvance_flex_rgbw_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CHANNELS: ["on_off", "light_color", "level"], + DEV_SIG_CHANNELS: ["on_off", "level", "light_color"], DEV_SIG_ENT_MAP_CLASS: "Light", DEV_SIG_ENT_MAP_ID: "light.ledvance_flex_rgbw_77665544_level_light_color_on_off", }, @@ -1559,6 +2005,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", DEV_SIG_ENT_MAP_ID: "button.ledvance_flex_rgbw_77665544_identify", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.ledvance_flex_rgbw_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.ledvance_flex_rgbw_77665544_basic_lqi", + }, }, }, { @@ -1579,6 +2035,8 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "button.ledvance_plug_77665544_identify", "switch.ledvance_plug_77665544_on_off", + "sensor.ledvance_plug_77665544_basic_rssi", + "sensor.ledvance_plug_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("switch", "00:11:22:33:44:55:66:77-1"): { @@ -1591,6 +2049,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", DEV_SIG_ENT_MAP_ID: "button.ledvance_plug_77665544_identify", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.ledvance_plug_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.ledvance_plug_77665544_basic_lqi", + }, }, }, { @@ -1611,10 +2079,12 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "button.ledvance_rt_rgbw_77665544_identify", "light.ledvance_rt_rgbw_77665544_level_light_color_on_off", + "sensor.ledvance_rt_rgbw_77665544_basic_rssi", + "sensor.ledvance_rt_rgbw_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CHANNELS: ["on_off", "light_color", "level"], + DEV_SIG_CHANNELS: ["on_off", "level", "light_color"], DEV_SIG_ENT_MAP_CLASS: "Light", DEV_SIG_ENT_MAP_ID: "light.ledvance_rt_rgbw_77665544_level_light_color_on_off", }, @@ -1623,6 +2093,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", DEV_SIG_ENT_MAP_ID: "button.ledvance_rt_rgbw_77665544_identify", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.ledvance_rt_rgbw_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.ledvance_rt_rgbw_77665544_basic_lqi", + }, }, }, { @@ -1671,6 +2151,8 @@ DEVICES = [ "sensor.lumi_lumi_plug_maus01_77665544_analog_input_2", "binary_sensor.lumi_lumi_plug_maus01_77665544_binary_input", "switch.lumi_lumi_plug_maus01_77665544_on_off", + "sensor.lumi_lumi_plug_maus01_77665544_basic_rssi", + "sensor.lumi_lumi_plug_maus01_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("switch", "00:11:22:33:44:55:66:77-1"): { @@ -1703,6 +2185,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_rms_voltage", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_basic_lqi", + }, ("sensor", "00:11:22:33:44:55:66:77-2-12"): { DEV_SIG_CHANNELS: ["analog_input"], DEV_SIG_ENT_MAP_CLASS: "AnalogInput", @@ -1750,6 +2242,8 @@ DEVICES = [ "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_apparent_power", "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_rms_current", "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_rms_voltage", + "sensor.lumi_lumi_relay_c2acn01_77665544_basic_rssi", + "sensor.lumi_lumi_relay_c2acn01_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { @@ -1782,6 +2276,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_rms_voltage", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_77665544_basic_lqi", + }, ("light", "00:11:22:33:44:55:66:77-2"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Light", @@ -1821,6 +2325,8 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "button.lumi_lumi_remote_b186acn01_77665544_identify", "sensor.lumi_lumi_remote_b186acn01_77665544_power", + "sensor.lumi_lumi_remote_b186acn01_77665544_basic_rssi", + "sensor.lumi_lumi_remote_b186acn01_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { @@ -1833,6 +2339,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b186acn01_77665544_power", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b186acn01_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b186acn01_77665544_basic_lqi", + }, }, }, { @@ -1867,6 +2383,8 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "button.lumi_lumi_remote_b286acn01_77665544_identify", "sensor.lumi_lumi_remote_b286acn01_77665544_power", + "sensor.lumi_lumi_remote_b286acn01_77665544_basic_rssi", + "sensor.lumi_lumi_remote_b286acn01_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { @@ -1879,6 +2397,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286acn01_77665544_power", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286acn01_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286acn01_77665544_basic_lqi", + }, }, }, { @@ -1933,6 +2461,8 @@ DEVICES = [ DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0300"], DEV_SIG_ENTITIES: [ "button.lumi_lumi_remote_b286opcn01_77665544_identify", + "sensor.lumi_lumi_remote_b286opcn01_77665544_basic_rssi", + "sensor.lumi_lumi_remote_b286opcn01_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { @@ -1940,6 +2470,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b286opcn01_77665544_identify", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286opcn01_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286opcn01_77665544_basic_lqi", + }, }, }, { @@ -1994,6 +2534,8 @@ DEVICES = [ DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0300", "2:0x0006"], DEV_SIG_ENTITIES: [ "button.lumi_lumi_remote_b486opcn01_77665544_identify", + "sensor.lumi_lumi_remote_b486opcn01_77665544_basic_rssi", + "sensor.lumi_lumi_remote_b486opcn01_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { @@ -2001,6 +2543,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b486opcn01_77665544_identify", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b486opcn01_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b486opcn01_77665544_basic_lqi", + }, }, }, { @@ -2020,6 +2572,8 @@ DEVICES = [ DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0300"], DEV_SIG_ENTITIES: [ "button.lumi_lumi_remote_b686opcn01_77665544_identify", + "sensor.lumi_lumi_remote_b686opcn01_77665544_basic_rssi", + "sensor.lumi_lumi_remote_b686opcn01_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { @@ -2027,6 +2581,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b686opcn01_77665544_identify", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b686opcn01_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b686opcn01_77665544_basic_lqi", + }, }, }, { @@ -2081,6 +2645,8 @@ DEVICES = [ DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0300", "2:0x0006"], DEV_SIG_ENTITIES: [ "button.lumi_lumi_remote_b686opcn01_77665544_identify", + "sensor.lumi_lumi_remote_b686opcn01_77665544_basic_rssi", + "sensor.lumi_lumi_remote_b686opcn01_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { @@ -2088,6 +2654,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b686opcn01_77665544_identify", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b686opcn01_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b686opcn01_77665544_basic_lqi", + }, }, }, { @@ -2108,6 +2684,8 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "light.lumi_lumi_router_77665544_on_off", "binary_sensor.lumi_lumi_router_77665544_on_off", + "sensor.lumi_lumi_router_77665544_basic_rssi", + "sensor.lumi_lumi_router_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-8"): { @@ -2115,6 +2693,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Light", DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_router_77665544_on_off", }, + ("sensor", "00:11:22:33:44:55:66:77-8-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-8-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_77665544_basic_lqi", + }, ("binary_sensor", "00:11:22:33:44:55:66:77-8-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Opening", @@ -2140,6 +2728,8 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "light.lumi_lumi_router_77665544_on_off", "binary_sensor.lumi_lumi_router_77665544_on_off", + "sensor.lumi_lumi_router_77665544_basic_rssi", + "sensor.lumi_lumi_router_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-8"): { @@ -2147,6 +2737,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Light", DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_router_77665544_on_off", }, + ("sensor", "00:11:22:33:44:55:66:77-8-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-8-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_77665544_basic_lqi", + }, ("binary_sensor", "00:11:22:33:44:55:66:77-8-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Opening", @@ -2172,6 +2772,8 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "light.lumi_lumi_router_77665544_on_off", "binary_sensor.lumi_lumi_router_77665544_on_off", + "sensor.lumi_lumi_router_77665544_basic_rssi", + "sensor.lumi_lumi_router_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-8"): { @@ -2179,6 +2781,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Light", DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_router_77665544_on_off", }, + ("sensor", "00:11:22:33:44:55:66:77-8-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-8-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_77665544_basic_lqi", + }, ("binary_sensor", "00:11:22:33:44:55:66:77-8-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Opening", @@ -2204,6 +2816,8 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "button.lumi_lumi_sen_ill_mgl01_77665544_identify", "sensor.lumi_lumi_sen_ill_mgl01_77665544_illuminance", + "sensor.lumi_lumi_sen_ill_mgl01_77665544_basic_rssi", + "sensor.lumi_lumi_sen_ill_mgl01_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { @@ -2216,6 +2830,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Illuminance", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sen_ill_mgl01_77665544_illuminance", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sen_ill_mgl01_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sen_ill_mgl01_77665544_basic_lqi", + }, }, }, { @@ -2250,6 +2874,8 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "button.lumi_lumi_sensor_86sw1_77665544_identify", "sensor.lumi_lumi_sensor_86sw1_77665544_power", + "sensor.lumi_lumi_sensor_86sw1_77665544_basic_rssi", + "sensor.lumi_lumi_sensor_86sw1_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { @@ -2262,6 +2888,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_86sw1_77665544_power", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_86sw1_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_86sw1_77665544_basic_lqi", + }, }, }, { @@ -2296,6 +2932,8 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "button.lumi_lumi_sensor_cube_aqgl01_77665544_identify", "sensor.lumi_lumi_sensor_cube_aqgl01_77665544_power", + "sensor.lumi_lumi_sensor_cube_aqgl01_77665544_basic_rssi", + "sensor.lumi_lumi_sensor_cube_aqgl01_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { @@ -2308,6 +2946,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_cube_aqgl01_77665544_power", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_cube_aqgl01_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_cube_aqgl01_77665544_basic_lqi", + }, }, }, { @@ -2344,6 +2992,8 @@ DEVICES = [ "sensor.lumi_lumi_sensor_ht_77665544_power", "sensor.lumi_lumi_sensor_ht_77665544_temperature", "sensor.lumi_lumi_sensor_ht_77665544_humidity", + "sensor.lumi_lumi_sensor_ht_77665544_basic_rssi", + "sensor.lumi_lumi_sensor_ht_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { @@ -2361,6 +3011,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Temperature", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_ht_77665544_temperature", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_ht_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_ht_77665544_basic_lqi", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1029"): { DEV_SIG_CHANNELS: ["humidity"], DEV_SIG_ENT_MAP_CLASS: "Humidity", @@ -2387,6 +3047,8 @@ DEVICES = [ "button.lumi_lumi_sensor_magnet_77665544_identify", "sensor.lumi_lumi_sensor_magnet_77665544_power", "binary_sensor.lumi_lumi_sensor_magnet_77665544_on_off", + "sensor.lumi_lumi_sensor_magnet_77665544_basic_rssi", + "sensor.lumi_lumi_sensor_magnet_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { @@ -2399,6 +3061,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_77665544_power", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_77665544_basic_lqi", + }, ("binary_sensor", "00:11:22:33:44:55:66:77-1-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Opening", @@ -2425,6 +3097,8 @@ DEVICES = [ "button.lumi_lumi_sensor_magnet_aq2_77665544_identify", "sensor.lumi_lumi_sensor_magnet_aq2_77665544_power", "binary_sensor.lumi_lumi_sensor_magnet_aq2_77665544_on_off", + "sensor.lumi_lumi_sensor_magnet_aq2_77665544_basic_rssi", + "sensor.lumi_lumi_sensor_magnet_aq2_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { @@ -2437,6 +3111,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_aq2_77665544_power", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_aq2_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_aq2_77665544_basic_lqi", + }, ("binary_sensor", "00:11:22:33:44:55:66:77-1-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Opening", @@ -2465,6 +3149,8 @@ DEVICES = [ "sensor.lumi_lumi_sensor_motion_aq2_77665544_illuminance", "binary_sensor.lumi_lumi_sensor_motion_aq2_77665544_occupancy", "binary_sensor.lumi_lumi_sensor_motion_aq2_77665544_ias_zone", + "sensor.lumi_lumi_sensor_motion_aq2_77665544_basic_rssi", + "sensor.lumi_lumi_sensor_motion_aq2_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1030"): { @@ -2492,6 +3178,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Illuminance", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_motion_aq2_77665544_illuminance", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_motion_aq2_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_motion_aq2_77665544_basic_lqi", + }, }, }, { @@ -2513,6 +3209,8 @@ DEVICES = [ "button.lumi_lumi_sensor_smoke_77665544_identify", "sensor.lumi_lumi_sensor_smoke_77665544_power", "binary_sensor.lumi_lumi_sensor_smoke_77665544_ias_zone", + "sensor.lumi_lumi_sensor_smoke_77665544_basic_rssi", + "sensor.lumi_lumi_sensor_smoke_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { @@ -2530,6 +3228,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_smoke_77665544_power", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_smoke_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_smoke_77665544_basic_lqi", + }, }, }, { @@ -2550,6 +3258,8 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "button.lumi_lumi_sensor_switch_77665544_identify", "sensor.lumi_lumi_sensor_switch_77665544_power", + "sensor.lumi_lumi_sensor_switch_77665544_basic_rssi", + "sensor.lumi_lumi_sensor_switch_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { @@ -2562,6 +3272,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_77665544_power", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_77665544_basic_lqi", + }, }, }, { @@ -2581,6 +3301,8 @@ DEVICES = [ DEV_SIG_EVT_CHANNELS: ["1:0x0006"], DEV_SIG_ENTITIES: [ "sensor.lumi_lumi_sensor_switch_aq2_77665544_power", + "sensor.lumi_lumi_sensor_switch_aq2_77665544_basic_rssi", + "sensor.lumi_lumi_sensor_switch_aq2_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("sensor", "00:11:22:33:44:55:66:77-1-1"): { @@ -2588,6 +3310,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq2_77665544_power", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq2_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq2_77665544_basic_lqi", + }, }, }, { @@ -2607,6 +3339,8 @@ DEVICES = [ DEV_SIG_EVT_CHANNELS: ["1:0x0006"], DEV_SIG_ENTITIES: [ "sensor.lumi_lumi_sensor_switch_aq3_77665544_power", + "sensor.lumi_lumi_sensor_switch_aq3_77665544_basic_rssi", + "sensor.lumi_lumi_sensor_switch_aq3_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("sensor", "00:11:22:33:44:55:66:77-1-1"): { @@ -2614,6 +3348,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq3_77665544_power", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq3_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq3_77665544_basic_lqi", + }, }, }, { @@ -2635,6 +3379,8 @@ DEVICES = [ "button.lumi_lumi_sensor_wleak_aq1_77665544_identify", "sensor.lumi_lumi_sensor_wleak_aq1_77665544_power", "binary_sensor.lumi_lumi_sensor_wleak_aq1_77665544_ias_zone", + "sensor.lumi_lumi_sensor_wleak_aq1_77665544_basic_rssi", + "sensor.lumi_lumi_sensor_wleak_aq1_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { @@ -2652,6 +3398,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_wleak_aq1_77665544_power", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_wleak_aq1_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_wleak_aq1_77665544_basic_lqi", + }, }, }, { @@ -2681,6 +3437,8 @@ DEVICES = [ "sensor.lumi_lumi_vibration_aq1_77665544_power", "binary_sensor.lumi_lumi_vibration_aq1_77665544_ias_zone", "lock.lumi_lumi_vibration_aq1_77665544_door_lock", + "sensor.lumi_lumi_vibration_aq1_77665544_basic_rssi", + "sensor.lumi_lumi_vibration_aq1_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { @@ -2698,6 +3456,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_vibration_aq1_77665544_power", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_vibration_aq1_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_vibration_aq1_77665544_basic_lqi", + }, ("lock", "00:11:22:33:44:55:66:77-1-257"): { DEV_SIG_CHANNELS: ["door_lock"], DEV_SIG_ENT_MAP_CLASS: "ZhaDoorLock", @@ -2726,6 +3494,8 @@ DEVICES = [ "sensor.lumi_lumi_weather_77665544_pressure", "sensor.lumi_lumi_weather_77665544_temperature", "sensor.lumi_lumi_weather_77665544_humidity", + "sensor.lumi_lumi_weather_77665544_basic_rssi", + "sensor.lumi_lumi_weather_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { @@ -2748,6 +3518,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Temperature", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_77665544_temperature", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_77665544_basic_lqi", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1029"): { DEV_SIG_CHANNELS: ["humidity"], DEV_SIG_ENT_MAP_CLASS: "Humidity", @@ -2774,6 +3554,8 @@ DEVICES = [ "button.nyce_3010_77665544_identify", "sensor.nyce_3010_77665544_power", "binary_sensor.nyce_3010_77665544_ias_zone", + "sensor.nyce_3010_77665544_basic_rssi", + "sensor.nyce_3010_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { @@ -2791,6 +3573,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.nyce_3010_77665544_power", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.nyce_3010_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.nyce_3010_77665544_basic_lqi", + }, }, }, { @@ -2812,6 +3604,8 @@ DEVICES = [ "button.nyce_3014_77665544_identify", "sensor.nyce_3014_77665544_power", "binary_sensor.nyce_3014_77665544_ias_zone", + "sensor.nyce_3014_77665544_basic_rssi", + "sensor.nyce_3014_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { @@ -2829,6 +3623,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.nyce_3014_77665544_power", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.nyce_3014_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.nyce_3014_77665544_basic_lqi", + }, }, }, { @@ -2892,10 +3696,12 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "button.osram_lightify_a19_rgbw_77665544_identify", "light.osram_lightify_a19_rgbw_77665544_level_light_color_on_off", + "sensor.osram_lightify_a19_rgbw_77665544_basic_rssi", + "sensor.osram_lightify_a19_rgbw_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-3"): { - DEV_SIG_CHANNELS: ["on_off", "level", "light_color"], + DEV_SIG_CHANNELS: ["on_off", "light_color", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", DEV_SIG_ENT_MAP_ID: "light.osram_lightify_a19_rgbw_77665544_level_light_color_on_off", }, @@ -2904,6 +3710,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", DEV_SIG_ENT_MAP_ID: "button.osram_lightify_a19_rgbw_77665544_identify", }, + ("sensor", "00:11:22:33:44:55:66:77-3-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_a19_rgbw_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-3-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_a19_rgbw_77665544_basic_lqi", + }, }, }, { @@ -2924,6 +3740,8 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "button.osram_lightify_dimming_switch_77665544_identify", "sensor.osram_lightify_dimming_switch_77665544_power", + "sensor.osram_lightify_dimming_switch_77665544_basic_rssi", + "sensor.osram_lightify_dimming_switch_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { @@ -2936,6 +3754,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_dimming_switch_77665544_power", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_dimming_switch_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_dimming_switch_77665544_basic_lqi", + }, }, }, { @@ -2956,10 +3784,12 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "button.osram_lightify_flex_rgbw_77665544_identify", "light.osram_lightify_flex_rgbw_77665544_level_light_color_on_off", + "sensor.osram_lightify_flex_rgbw_77665544_basic_rssi", + "sensor.osram_lightify_flex_rgbw_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-3"): { - DEV_SIG_CHANNELS: ["on_off", "level", "light_color"], + DEV_SIG_CHANNELS: ["on_off", "light_color", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", DEV_SIG_ENT_MAP_ID: "light.osram_lightify_flex_rgbw_77665544_level_light_color_on_off", }, @@ -2968,6 +3798,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", DEV_SIG_ENT_MAP_ID: "button.osram_lightify_flex_rgbw_77665544_identify", }, + ("sensor", "00:11:22:33:44:55:66:77-3-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_flex_rgbw_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-3-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_flex_rgbw_77665544_basic_lqi", + }, }, }, { @@ -2992,10 +3832,12 @@ DEVICES = [ "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_apparent_power", "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_rms_current", "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_rms_voltage", + "sensor.osram_lightify_rt_tunable_white_77665544_basic_rssi", + "sensor.osram_lightify_rt_tunable_white_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-3"): { - DEV_SIG_CHANNELS: ["on_off", "level", "light_color"], + DEV_SIG_CHANNELS: ["on_off", "light_color", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", DEV_SIG_ENT_MAP_ID: "light.osram_lightify_rt_tunable_white_77665544_level_light_color_on_off", }, @@ -3024,6 +3866,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_rms_voltage", }, + ("sensor", "00:11:22:33:44:55:66:77-3-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-3-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_77665544_basic_lqi", + }, }, }, { @@ -3048,6 +3900,8 @@ DEVICES = [ "sensor.osram_plug_01_77665544_electrical_measurement_rms_current", "sensor.osram_plug_01_77665544_electrical_measurement_rms_voltage", "switch.osram_plug_01_77665544_on_off", + "sensor.osram_plug_01_77665544_basic_rssi", + "sensor.osram_plug_01_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("switch", "00:11:22:33:44:55:66:77-3"): { @@ -3080,6 +3934,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_77665544_electrical_measurement_rms_voltage", }, + ("sensor", "00:11:22:33:44:55:66:77-3-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-3-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_77665544_basic_lqi", + }, }, }, { @@ -3160,6 +4024,8 @@ DEVICES = [ ], DEV_SIG_ENTITIES: [ "sensor.osram_switch_4x_lightify_77665544_power", + "sensor.osram_switch_4x_lightify_77665544_basic_rssi", + "sensor.osram_switch_4x_lightify_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("sensor", "00:11:22:33:44:55:66:77-1-1"): { @@ -3167,6 +4033,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.osram_switch_4x_lightify_77665544_power", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.osram_switch_4x_lightify_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.osram_switch_4x_lightify_77665544_basic_lqi", + }, }, }, { @@ -3195,8 +4071,20 @@ DEVICES = [ "button.philips_rwl020_77665544_identify", "sensor.philips_rwl020_77665544_power", "binary_sensor.philips_rwl020_77665544_binary_input", + "sensor.philips_rwl020_77665544_basic_rssi", + "sensor.philips_rwl020_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.philips_rwl020_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.philips_rwl020_77665544_basic_lqi", + }, ("binary_sensor", "00:11:22:33:44:55:66:77-2-15"): { DEV_SIG_CHANNELS: ["binary_input"], DEV_SIG_ENT_MAP_CLASS: "BinaryInput", @@ -3234,6 +4122,8 @@ DEVICES = [ "sensor.samjin_button_77665544_power", "sensor.samjin_button_77665544_temperature", "binary_sensor.samjin_button_77665544_ias_zone", + "sensor.samjin_button_77665544_basic_rssi", + "sensor.samjin_button_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { @@ -3256,6 +4146,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Temperature", DEV_SIG_ENT_MAP_ID: "sensor.samjin_button_77665544_temperature", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_button_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_button_77665544_basic_lqi", + }, }, }, { @@ -3278,6 +4178,8 @@ DEVICES = [ "sensor.samjin_multi_77665544_power", "sensor.samjin_multi_77665544_temperature", "binary_sensor.samjin_multi_77665544_ias_zone", + "sensor.samjin_multi_77665544_basic_rssi", + "sensor.samjin_multi_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { @@ -3300,6 +4202,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Temperature", DEV_SIG_ENT_MAP_ID: "sensor.samjin_multi_77665544_temperature", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_multi_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_multi_77665544_basic_lqi", + }, }, }, { @@ -3322,6 +4234,8 @@ DEVICES = [ "sensor.samjin_water_77665544_power", "sensor.samjin_water_77665544_temperature", "binary_sensor.samjin_water_77665544_ias_zone", + "sensor.samjin_water_77665544_basic_rssi", + "sensor.samjin_water_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { @@ -3344,6 +4258,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Temperature", DEV_SIG_ENT_MAP_ID: "sensor.samjin_water_77665544_temperature", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_water_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_water_77665544_basic_lqi", + }, }, }, { @@ -3368,6 +4292,8 @@ DEVICES = [ "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_rms_current", "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_rms_voltage", "switch.securifi_ltd_unk_model_77665544_on_off", + "sensor.securifi_ltd_unk_model_77665544_basic_rssi", + "sensor.securifi_ltd_unk_model_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { @@ -3395,6 +4321,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_rms_voltage", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_77665544_basic_lqi", + }, ("switch", "00:11:22:33:44:55:66:77-1-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", @@ -3422,6 +4358,8 @@ DEVICES = [ "sensor.sercomm_corp_sz_dws04n_sf_77665544_power", "sensor.sercomm_corp_sz_dws04n_sf_77665544_temperature", "binary_sensor.sercomm_corp_sz_dws04n_sf_77665544_ias_zone", + "sensor.sercomm_corp_sz_dws04n_sf_77665544_basic_rssi", + "sensor.sercomm_corp_sz_dws04n_sf_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { @@ -3444,6 +4382,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Temperature", DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_dws04n_sf_77665544_temperature", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_dws04n_sf_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_dws04n_sf_77665544_basic_lqi", + }, }, }, { @@ -3470,13 +4418,15 @@ DEVICES = [ DEV_SIG_EVT_CHANNELS: ["1:0x0019", "2:0x0006"], DEV_SIG_ENTITIES: [ "button.sercomm_corp_sz_esw01_77665544_identify", - "light.sercomm_corp_sz_esw01_77665544_on_off", "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement", "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_apparent_power", "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_rms_current", "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_rms_voltage", "sensor.sercomm_corp_sz_esw01_77665544_smartenergy_metering", "sensor.sercomm_corp_sz_esw01_77665544_smartenergy_metering_summation_delivered", + "light.sercomm_corp_sz_esw01_77665544_on_off", + "sensor.sercomm_corp_sz_esw01_77665544_basic_rssi", + "sensor.sercomm_corp_sz_esw01_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { @@ -3519,6 +4469,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_smartenergy_metering_summation_delivered", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_basic_lqi", + }, }, }, { @@ -3542,6 +4502,8 @@ DEVICES = [ "sensor.sercomm_corp_sz_pir04_77665544_illuminance", "sensor.sercomm_corp_sz_pir04_77665544_temperature", "binary_sensor.sercomm_corp_sz_pir04_77665544_ias_zone", + "sensor.sercomm_corp_sz_pir04_77665544_basic_rssi", + "sensor.sercomm_corp_sz_pir04_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { @@ -3569,6 +4531,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Temperature", DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_pir04_77665544_temperature", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_pir04_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_pir04_77665544_basic_lqi", + }, }, }, { @@ -3593,6 +4565,8 @@ DEVICES = [ "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_rms_current", "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_rms_voltage", "switch.sinope_technologies_rm3250zb_77665544_on_off", + "sensor.sinope_technologies_rm3250zb_77665544_basic_rssi", + "sensor.sinope_technologies_rm3250zb_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { @@ -3620,6 +4594,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_rms_voltage", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_77665544_basic_lqi", + }, ("switch", "00:11:22:33:44:55:66:77-1-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", @@ -3658,6 +4642,8 @@ DEVICES = [ "sensor.sinope_technologies_th1123zb_77665544_temperature", "sensor.sinope_technologies_th1123zb_77665544_thermostat_hvac_action", "climate.sinope_technologies_th1123zb_77665544_thermostat", + "sensor.sinope_technologies_th1123zb_77665544_basic_rssi", + "sensor.sinope_technologies_th1123zb_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { @@ -3695,6 +4681,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Temperature", DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_temperature", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_basic_lqi", + }, ("sensor", "00:11:22:33:44:55:66:77-1-513-hvac_action"): { DEV_SIG_CHANNELS: ["thermostat"], DEV_SIG_ENT_MAP_CLASS: "SinopeHVACAction", @@ -3733,6 +4729,8 @@ DEVICES = [ "sensor.sinope_technologies_th1124zb_77665544_temperature", "sensor.sinope_technologies_th1124zb_77665544_thermostat_hvac_action", "climate.sinope_technologies_th1124zb_77665544_thermostat", + "sensor.sinope_technologies_th1124zb_77665544_basic_rssi", + "sensor.sinope_technologies_th1124zb_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { @@ -3745,16 +4743,6 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Thermostat", DEV_SIG_ENT_MAP_ID: "climate.sinope_technologies_th1124zb_77665544_thermostat", }, - ("sensor", "00:11:22:33:44:55:66:77-1-513-hvac_action"): { - DEV_SIG_CHANNELS: ["thermostat"], - DEV_SIG_ENT_MAP_CLASS: "SinopeHVACAction", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_thermostat_hvac_action", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { - DEV_SIG_CHANNELS: ["temperature"], - DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_temperature", - }, ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", @@ -3775,6 +4763,26 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_rms_voltage", }, + ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { + DEV_SIG_CHANNELS: ["temperature"], + DEV_SIG_ENT_MAP_CLASS: "Temperature", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_temperature", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_basic_lqi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-513-hvac_action"): { + DEV_SIG_CHANNELS: ["thermostat"], + DEV_SIG_ENT_MAP_CLASS: "SinopeHVACAction", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_thermostat_hvac_action", + }, }, }, { @@ -3800,6 +4808,8 @@ DEVICES = [ "sensor.smartthings_outletv4_77665544_electrical_measurement_rms_voltage", "binary_sensor.smartthings_outletv4_77665544_binary_input", "switch.smartthings_outletv4_77665544_on_off", + "sensor.smartthings_outletv4_77665544_basic_rssi", + "sensor.smartthings_outletv4_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-15"): { @@ -3832,6 +4842,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_77665544_electrical_measurement_rms_voltage", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_77665544_basic_lqi", + }, ("switch", "00:11:22:33:44:55:66:77-1-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", @@ -3858,6 +4878,8 @@ DEVICES = [ "button.smartthings_tagv4_77665544_identify", "device_tracker.smartthings_tagv4_77665544_power", "binary_sensor.smartthings_tagv4_77665544_binary_input", + "sensor.smartthings_tagv4_77665544_basic_rssi", + "sensor.smartthings_tagv4_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("device_tracker", "00:11:22:33:44:55:66:77-1"): { @@ -3875,6 +4897,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", DEV_SIG_ENT_MAP_ID: "button.smartthings_tagv4_77665544_identify", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_tagv4_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_tagv4_77665544_basic_lqi", + }, }, }, { @@ -3895,6 +4927,8 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "button.third_reality_inc_3rss007z_77665544_identify", "switch.third_reality_inc_3rss007z_77665544_on_off", + "sensor.third_reality_inc_3rss007z_77665544_basic_rssi", + "sensor.third_reality_inc_3rss007z_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { @@ -3902,6 +4936,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", DEV_SIG_ENT_MAP_ID: "button.third_reality_inc_3rss007z_77665544_identify", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.third_reality_inc_3rss007z_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.third_reality_inc_3rss007z_77665544_basic_lqi", + }, ("switch", "00:11:22:33:44:55:66:77-1-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", @@ -3928,6 +4972,8 @@ DEVICES = [ "button.third_reality_inc_3rss008z_77665544_identify", "sensor.third_reality_inc_3rss008z_77665544_power", "switch.third_reality_inc_3rss008z_77665544_on_off", + "sensor.third_reality_inc_3rss008z_77665544_basic_rssi", + "sensor.third_reality_inc_3rss008z_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { @@ -3940,6 +4986,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.third_reality_inc_3rss008z_77665544_power", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.third_reality_inc_3rss008z_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.third_reality_inc_3rss008z_77665544_basic_lqi", + }, ("switch", "00:11:22:33:44:55:66:77-1-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", @@ -3967,6 +5023,8 @@ DEVICES = [ "sensor.visonic_mct_340_e_77665544_power", "sensor.visonic_mct_340_e_77665544_temperature", "binary_sensor.visonic_mct_340_e_77665544_ias_zone", + "sensor.visonic_mct_340_e_77665544_basic_rssi", + "sensor.visonic_mct_340_e_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { @@ -3989,6 +5047,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Temperature", DEV_SIG_ENT_MAP_ID: "sensor.visonic_mct_340_e_77665544_temperature", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.visonic_mct_340_e_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.visonic_mct_340_e_77665544_basic_lqi", + }, }, }, { @@ -4011,6 +5079,8 @@ DEVICES = [ "sensor.zen_within_zen_01_77665544_power", "sensor.zen_within_zen_01_77665544_thermostat_hvac_action", "climate.zen_within_zen_01_77665544_fan_thermostat", + "sensor.zen_within_zen_01_77665544_basic_rssi", + "sensor.zen_within_zen_01_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { @@ -4028,6 +5098,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_77665544_power", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_77665544_basic_lqi", + }, ("sensor", "00:11:22:33:44:55:66:77-1-513-hvac_action"): { DEV_SIG_CHANNELS: ["thermostat"], DEV_SIG_ENT_MAP_CLASS: "ThermostatHVACAction", @@ -4076,6 +5156,8 @@ DEVICES = [ "light.tyzb01_ns1ndbww_ts0004_77665544_on_off_2", "light.tyzb01_ns1ndbww_ts0004_77665544_on_off_3", "light.tyzb01_ns1ndbww_ts0004_77665544_on_off_4", + "sensor.tyzb01_ns1ndbww_ts0004_77665544_basic_rssi", + "sensor.tyzb01_ns1ndbww_ts0004_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { @@ -4083,6 +5165,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Light", DEV_SIG_ENT_MAP_ID: "light.tyzb01_ns1ndbww_ts0004_77665544_on_off", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.tyzb01_ns1ndbww_ts0004_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.tyzb01_ns1ndbww_ts0004_77665544_basic_lqi", + }, ("light", "00:11:22:33:44:55:66:77-2"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Light", @@ -4119,6 +5211,8 @@ DEVICES = [ "button.netvox_z308e3ed_77665544_identify", "sensor.netvox_z308e3ed_77665544_power", "binary_sensor.netvox_z308e3ed_77665544_ias_zone", + "sensor.netvox_z308e3ed_77665544_basic_rssi", + "sensor.netvox_z308e3ed_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { @@ -4136,6 +5230,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Battery", DEV_SIG_ENT_MAP_ID: "sensor.netvox_z308e3ed_77665544_power", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.netvox_z308e3ed_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.netvox_z308e3ed_77665544_basic_lqi", + }, }, }, { @@ -4158,6 +5262,8 @@ DEVICES = [ "light.sengled_e11_g13_77665544_level_on_off", "sensor.sengled_e11_g13_77665544_smartenergy_metering", "sensor.sengled_e11_g13_77665544_smartenergy_metering_summation_delivered", + "sensor.sengled_e11_g13_77665544_basic_rssi", + "sensor.sengled_e11_g13_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { @@ -4180,6 +5286,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_77665544_smartenergy_metering_summation_delivered", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_77665544_basic_lqi", + }, }, }, { @@ -4202,6 +5318,8 @@ DEVICES = [ "light.sengled_e12_n14_77665544_level_on_off", "sensor.sengled_e12_n14_77665544_smartenergy_metering", "sensor.sengled_e12_n14_77665544_smartenergy_metering_summation_delivered", + "sensor.sengled_e12_n14_77665544_basic_rssi", + "sensor.sengled_e12_n14_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { @@ -4224,6 +5342,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_77665544_smartenergy_metering_summation_delivered", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_77665544_basic_lqi", + }, }, }, { @@ -4246,10 +5374,12 @@ DEVICES = [ "light.sengled_z01_a19nae26_77665544_level_light_color_on_off", "sensor.sengled_z01_a19nae26_77665544_smartenergy_metering", "sensor.sengled_z01_a19nae26_77665544_smartenergy_metering_summation_delivered", + "sensor.sengled_z01_a19nae26_77665544_basic_rssi", + "sensor.sengled_z01_a19nae26_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CHANNELS: ["on_off", "light_color", "level"], + DEV_SIG_CHANNELS: ["on_off", "level", "light_color"], DEV_SIG_ENT_MAP_CLASS: "Light", DEV_SIG_ENT_MAP_ID: "light.sengled_z01_a19nae26_77665544_level_light_color_on_off", }, @@ -4268,6 +5398,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_77665544_smartenergy_metering_summation_delivered", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_77665544_basic_lqi", + }, }, }, { @@ -4288,6 +5428,8 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "button.unk_manufacturer_unk_model_77665544_identify", "cover.unk_manufacturer_unk_model_77665544_level_on_off_shade", + "sensor.unk_manufacturer_unk_model_77665544_basic_rssi", + "sensor.unk_manufacturer_unk_model_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { @@ -4296,10 +5438,20 @@ DEVICES = [ DEV_SIG_ENT_MAP_ID: "button.unk_manufacturer_unk_model_77665544_identify", }, ("cover", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CHANNELS: ["shade", "level", "on_off"], + DEV_SIG_CHANNELS: ["level", "on_off", "shade"], DEV_SIG_ENT_MAP_CLASS: "Shade", DEV_SIG_ENT_MAP_ID: "cover.unk_manufacturer_unk_model_77665544_level_on_off_shade", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.unk_manufacturer_unk_model_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.unk_manufacturer_unk_model_77665544_basic_lqi", + }, }, }, { @@ -4578,6 +5730,8 @@ DEVICES = [ "sensor.efektalab_ru_efekta_pws_77665544_power", "sensor.efektalab_ru_efekta_pws_77665544_soil_moisture", "sensor.efektalab_ru_efekta_pws_77665544_temperature", + "sensor.efektalab_ru_efekta_pws_77665544_basic_rssi", + "sensor.efektalab_ru_efekta_pws_77665544_basic_lqi", ], DEV_SIG_ENT_MAP: { ("sensor", "00:11:22:33:44:55:66:77-1-1"): { @@ -4595,6 +5749,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Temperature", DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_77665544_temperature", }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_77665544_basic_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CHANNELS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_77665544_basic_lqi", + }, }, }, ] From 8c8c7e91a23b1e94fb63ed08b8aad5b056e28f83 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 25 Dec 2021 00:13:29 +0000 Subject: [PATCH 1029/2644] [ci skip] Translation update --- .../adguard/translations/zh-Hans.json | 2 +- .../components/androidtv/translations/ca.json | 10 ++--- .../components/androidtv/translations/de.json | 10 ++--- .../components/androidtv/translations/et.json | 10 ++--- .../components/androidtv/translations/nl.json | 23 +++++++++++- .../components/androidtv/translations/ru.json | 10 ++--- .../androidtv/translations/zh-Hans.json | 37 +++++++++++++++++++ .../apple_tv/translations/zh-Hans.json | 22 ++++++----- .../components/aurora/translations/ja.json | 2 +- .../cloudflare/translations/zh-Hans.json | 2 +- .../coinbase/translations/zh-Hans.json | 6 +-- .../dlna_dmr/translations/zh-Hans.json | 26 ++++++------- .../emulated_roku/translations/ja.json | 2 +- .../translations/zh-Hans.json | 7 ++++ .../ezviz/translations/zh-Hans.json | 2 +- .../growatt_server/translations/ja.json | 2 +- .../homeassistant/translations/ja.json | 4 +- .../homekit_controller/translations/ja.json | 2 +- .../jellyfin/translations/zh-Hans.json | 2 +- .../kostal_plenticore/translations/ja.json | 2 +- .../components/mqtt/translations/zh-Hans.json | 24 ++++++------ .../mysensors/translations/zh-Hans.json | 6 +-- .../nmap_tracker/translations/ja.json | 2 +- .../components/overkiz/translations/de.json | 25 +++++++++++++ .../components/overkiz/translations/ja.json | 25 +++++++++++++ .../components/overkiz/translations/nl.json | 13 +++++++ .../components/overkiz/translations/ru.json | 25 +++++++++++++ .../components/picnic/translations/ja.json | 2 +- .../srp_energy/translations/ja.json | 2 +- .../tailscale/translations/zh-Hans.json | 2 +- .../translations/zh-Hans.json | 2 +- .../tradfri/translations/zh-Hans.json | 2 +- .../components/tuya/translations/zh-Hans.json | 2 +- .../components/upnp/translations/zh-Hans.json | 2 +- .../components/version/translations/ca.json | 26 +++++++++++++ .../components/version/translations/de.json | 26 +++++++++++++ .../components/version/translations/ja.json | 26 +++++++++++++ .../components/version/translations/nl.json | 9 +++++ .../components/version/translations/ru.json | 26 +++++++++++++ .../xiaomi_miio/translations/zh-Hans.json | 6 ++- 40 files changed, 352 insertions(+), 84 deletions(-) create mode 100644 homeassistant/components/androidtv/translations/zh-Hans.json create mode 100644 homeassistant/components/evil_genius_labs/translations/zh-Hans.json create mode 100644 homeassistant/components/overkiz/translations/de.json create mode 100644 homeassistant/components/overkiz/translations/ja.json create mode 100644 homeassistant/components/overkiz/translations/nl.json create mode 100644 homeassistant/components/overkiz/translations/ru.json create mode 100644 homeassistant/components/version/translations/ca.json create mode 100644 homeassistant/components/version/translations/de.json create mode 100644 homeassistant/components/version/translations/ja.json create mode 100644 homeassistant/components/version/translations/nl.json create mode 100644 homeassistant/components/version/translations/ru.json diff --git a/homeassistant/components/adguard/translations/zh-Hans.json b/homeassistant/components/adguard/translations/zh-Hans.json index e48fd2fd9cc..0d60c5e8c07 100644 --- a/homeassistant/components/adguard/translations/zh-Hans.json +++ b/homeassistant/components/adguard/translations/zh-Hans.json @@ -13,7 +13,7 @@ "host": "\u4e3b\u673a\u5730\u5740", "password": "\u5bc6\u7801", "port": "\u7aef\u53e3", - "ssl": "\u4f7f\u7528 SSL \u8fde\u63a5", + "ssl": "\u4f7f\u7528 SSL \u8bc1\u4e66", "username": "\u7528\u6237\u540d", "verify_ssl": "\u9a8c\u8bc1 SSL \u8bc1\u4e66\u51ed\u8bc1" }, diff --git a/homeassistant/components/androidtv/translations/ca.json b/homeassistant/components/androidtv/translations/ca.json index 0e3e6263fbf..3fccdc45771 100644 --- a/homeassistant/components/androidtv/translations/ca.json +++ b/homeassistant/components/androidtv/translations/ca.json @@ -43,12 +43,12 @@ "init": { "data": { "apps": "Configura la llista d'aplicacions", - "exclude_unnamed_apps": "Exclou aplicacions amb nom desconegut", - "get_sources": "Si s'han d'obtenir, o no, les aplicacions en execuci\u00f3 com a llista de fonts", - "screencap": "Determina si la imatge d'\u00e0lbum s'ha d'extreure del que es mostra en pantalla", + "exclude_unnamed_apps": "Exclou, de la llista de fonts, les aplicacions amb nom desconegut", + "get_sources": "Obt\u00e9 les aplicacions en execuci\u00f3 com a llista de fonts", + "screencap": "Utilitza la captura de pantalla per les imatges d'\u00e0lbum", "state_detection_rules": "Configura les regles de detecci\u00f3 d'estat", - "turn_off_command": "Comanda de shell ADB per substituir la comanda predeterminada turn_off", - "turn_on_command": "Comanda de shell ADB per substituir la comanda predeterminada turn_on" + "turn_off_command": "Comanda d'apagada de shell ADB (deixa-ho buit per utilitzar la predeterminada)", + "turn_on_command": "Comanda d'engegada de shell ADB (deixa-ho buit per utilitzar la predeterminada)" }, "title": "Opcions d'Android TV" }, diff --git a/homeassistant/components/androidtv/translations/de.json b/homeassistant/components/androidtv/translations/de.json index bf07b6fee80..e69e9ac2183 100644 --- a/homeassistant/components/androidtv/translations/de.json +++ b/homeassistant/components/androidtv/translations/de.json @@ -43,12 +43,12 @@ "init": { "data": { "apps": "Anwendungsliste konfigurieren", - "exclude_unnamed_apps": "App mit unbekanntem Namen ausschlie\u00dfen", - "get_sources": "Ob die laufenden Apps als Liste der Quellen abgerufen werden sollen oder nicht", - "screencap": "Legt fest, ob Albumcover von der Bildschirmanzeige \u00fcbernommen werden sollen", + "exclude_unnamed_apps": "Apps mit unbekanntem Namen aus der Quellenliste ausschlie\u00dfen", + "get_sources": "Abrufen der laufenden Anwendungen als Liste der Quellen", + "screencap": "Bildschirmaufnahme als Albumcover verwenden", "state_detection_rules": "Regeln zur Statuserkennung konfigurieren", - "turn_off_command": "ADB-Shell-Befehl zum \u00dcberschreiben des Standardbefehls turn_off", - "turn_on_command": "ADB-Shell-Befehl zum \u00dcberschreiben des Standardbefehls turn_on" + "turn_off_command": "ADB-Shell-Abschaltbefehl (f\u00fcr Standard leer lassen)", + "turn_on_command": "ADB-Shell-Einschaltbefehl (f\u00fcr Standard leer lassen)" }, "title": "Android TV-Optionen" }, diff --git a/homeassistant/components/androidtv/translations/et.json b/homeassistant/components/androidtv/translations/et.json index 9800fdb3946..292b01fa849 100644 --- a/homeassistant/components/androidtv/translations/et.json +++ b/homeassistant/components/androidtv/translations/et.json @@ -43,12 +43,12 @@ "init": { "data": { "apps": "Rakenduste loendi seadistamine", - "exclude_unnamed_apps": "V\u00e4lista tundmatu nimega rakendus", - "get_sources": "Kas tuua t\u00f6\u00f6tavad rakendused allikate loendina v\u00f5i mitte", - "screencap": "M\u00e4\u00e4rab kas albumi kujundus tuleks ekraanil kuvatavast pildist v\u00f5tta", + "exclude_unnamed_apps": "V\u00e4lista allikate loendist tundmatu nimega rakendused", + "get_sources": "Jooksvate rakenduste toomine allikate loendina", + "screencap": "Kasutage albumipildi jaoks ekraanit\u00f5mmist", "state_detection_rules": "M\u00e4\u00e4ra oleku tuvastamise reeglid", - "turn_off_command": "ADB k\u00e4sk vaikimisi k\u00e4su turn_off t\u00fchistamiseks", - "turn_on_command": "ADB k\u00e4sk vaikimisi k\u00e4su turn_on t\u00fchistamiseks" + "turn_off_command": "ADB shell turn off k\u00e4sk (vaikimisi j\u00e4ta t\u00fchjaks)", + "turn_on_command": "ADB shell turn on k\u00e4sk (vaikimisi j\u00e4ta t\u00fchjaks)" }, "title": "Android TV suvandid" }, diff --git a/homeassistant/components/androidtv/translations/nl.json b/homeassistant/components/androidtv/translations/nl.json index 2cb7d51f5b5..38a4bd8005d 100644 --- a/homeassistant/components/androidtv/translations/nl.json +++ b/homeassistant/components/androidtv/translations/nl.json @@ -8,13 +8,31 @@ "adbkey_not_file": "ADB key file niet gevonden", "cannot_connect": "Kan geen verbinding maken", "invalid_host": "Ongeldige hostnaam of IP-adres", - "key_and_server": "Geef alleen ADB-sleutel of ADB-server op" + "key_and_server": "Geef alleen ADB-sleutel of ADB-server op", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "adb_server_ip": "IP-adres van de ADB-server (leeg laten om niet te gebruiken)", + "adb_server_port": "Poort van de ADB-server", + "adbkey": "Pad naar uw ADB-key file (leeg laten om automatisch te genereren)", + "device_class": "Het type apparaat", + "host": "Host" + }, + "description": "Stel de vereiste parameters in om verbinding te maken met uw Android TV-apparaat", + "title": "Android TV" + } } }, "options": { + "error": { + "invalid_det_rules": "Ongeldige statusdetectieregels" + }, "step": { "apps": { "data": { + "app_delete": "Vink aan om deze applicatie te verwijderen", "app_id": "Applicatie ID", "app_name": "Applicatienaam" }, @@ -36,7 +54,8 @@ "rules": { "data": { "rule_delete": "Vink aan om deze regel te verwijderen", - "rule_id": "Application ID" + "rule_id": "Application ID", + "rule_values": "Lijst met statusdetectieregels (zie documentatie)" }, "description": "Detectieregel configureren voor applicatie-ID {rule_id}", "title": "Regels voor statusdetectie van Android TV configureren" diff --git a/homeassistant/components/androidtv/translations/ru.json b/homeassistant/components/androidtv/translations/ru.json index 31acebe5f30..0803a16c986 100644 --- a/homeassistant/components/androidtv/translations/ru.json +++ b/homeassistant/components/androidtv/translations/ru.json @@ -43,12 +43,12 @@ "init": { "data": { "apps": "\u041d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439", - "exclude_unnamed_apps": "\u0418\u0441\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0441 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u043c \u0438\u043c\u0435\u043d\u0435\u043c", - "get_sources": "\u0421\u043b\u0435\u0434\u0443\u0435\u0442 \u043b\u0438 \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c \u0437\u0430\u043f\u0443\u0449\u0435\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0432 \u0432\u0438\u0434\u0435 \u0441\u043f\u0438\u0441\u043a\u0430 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u0432", - "screencap": "\u041d\u0443\u0436\u043d\u043e \u043b\u0438 \u0438\u0437\u0432\u043b\u0435\u043a\u0430\u0442\u044c \u043e\u0431\u043b\u043e\u0436\u043a\u0443 \u0430\u043b\u044c\u0431\u043e\u043c\u0430 \u0438\u0437 \u0442\u043e\u0433\u043e, \u0447\u0442\u043e \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u0442\u0441\u044f \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435", + "exclude_unnamed_apps": "\u0418\u0441\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0438\u0437 \u0441\u043f\u0438\u0441\u043a\u0430 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u0432 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0431\u0435\u0437 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0439", + "get_sources": "\u041f\u043e\u043b\u0443\u0447\u0430\u0442\u044c \u0437\u0430\u043f\u0443\u0449\u0435\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u043a\u0430\u043a \u0441\u043f\u0438\u0441\u043e\u043a \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u0432", + "screencap": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0441\u043d\u0438\u043c\u043e\u043a \u044d\u043a\u0440\u0430\u043d\u0430 \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u043e\u0431\u043b\u043e\u0436\u043a\u0438 \u0430\u043b\u044c\u0431\u043e\u043c\u0430", "state_detection_rules": "\u041d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u0440\u0430\u0432\u0438\u043b\u0430 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0439", - "turn_off_command": "\u041a\u043e\u043c\u0430\u043d\u0434\u0430 \u043e\u0431\u043e\u043b\u043e\u0447\u043a\u0438 ADB \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u043a\u043e\u043c\u0430\u043d\u0434\u044b turn_off \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e", - "turn_on_command": "\u041a\u043e\u043c\u0430\u043d\u0434\u0430 \u043e\u0431\u043e\u043b\u043e\u0447\u043a\u0438 ADB \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u043a\u043e\u043c\u0430\u043d\u0434\u044b turn_on \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e" + "turn_off_command": "\u041a\u043e\u043c\u0430\u043d\u0434\u0430 \u043e\u0431\u043e\u043b\u043e\u0447\u043a\u0438 ADB \u0434\u043b\u044f \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f (\u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u043e\u043b\u0435 \u043f\u0443\u0441\u0442\u044b\u043c)", + "turn_on_command": "\u041a\u043e\u043c\u0430\u043d\u0434\u0430 \u043e\u0431\u043e\u043b\u043e\u0447\u043a\u0438 ADB \u0434\u043b\u044f \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f (\u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u043e\u043b\u0435 \u043f\u0443\u0441\u0442\u044b\u043c)" }, "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Android TV" }, diff --git a/homeassistant/components/androidtv/translations/zh-Hans.json b/homeassistant/components/androidtv/translations/zh-Hans.json new file mode 100644 index 00000000000..454725d52da --- /dev/null +++ b/homeassistant/components/androidtv/translations/zh-Hans.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u7ecf\u914d\u7f6e\u8fc7\u4e86", + "invalid_unique_id": "\u65e0\u6cd5\u786e\u5b9a\u8bbe\u5907\u7684\u6709\u6548 unique ID" + }, + "error": { + "adbkey_not_file": "\u672a\u627e\u5230 ADB \u5bc6\u94a5\u6587\u4ef6", + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "key_and_server": "\u53ea\u63d0\u4f9b\u4e86 ADB \u79d8\u94a5 \u6216 ADB \u670d\u52a1\u5668", + "unknown": "\u975e\u9884\u671f\u7684\u9519\u8bef" + }, + "step": { + "user": { + "data": { + "adb_server_ip": "ADB \u670d\u52a1\u5668 IP \u5730\u5740\uff08\u7559\u7a7a\u4e0d\u4f7f\u7528\uff09", + "adb_server_port": "ADB \u670d\u52a1\u5668\u7aef\u53e3", + "adbkey": "ADB \u5bc6\u94a5\u6587\u4ef6\u8def\u5f84\uff08\u7559\u7a7a\u4ee5\u81ea\u52a8\u751f\u6210\uff09", + "device_class": "\u8bbe\u5907\u7c7b\u578b" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "exclude_unnamed_apps": "\u4ece\u4fe1\u53f7\u6e90\u5217\u8868\u4e2d\u6392\u9664\u672a\u77e5\u540d\u79f0\u7684\u5e94\u7528", + "get_sources": "\u83b7\u53d6\u8fd0\u884c\u4e2d\u7684\u5e94\u7528\u4f5c\u4e3a\u4fe1\u53f7\u6e90\u5217\u8868", + "screencap": "\u4f7f\u7528\u5c4f\u5e55\u622a\u56fe\u4f5c\u4e3a\u4e13\u8f91\u5c01\u9762", + "turn_off_command": "ADB shell \u5173\u673a\u547d\u4ee4\uff08\u7559\u7a7a\u4ee5\u4f7f\u7528\u9ed8\u8ba4\u503c\uff09", + "turn_on_command": "ADB shell \u5f00\u673a\u547d\u4ee4\uff08\u7559\u7a7a\u4ee5\u4f7f\u7528\u9ed8\u8ba4\u503c\uff09" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/zh-Hans.json b/homeassistant/components/apple_tv/translations/zh-Hans.json index 2232ea02b41..65ab4d27238 100644 --- a/homeassistant/components/apple_tv/translations/zh-Hans.json +++ b/homeassistant/components/apple_tv/translations/zh-Hans.json @@ -1,15 +1,16 @@ { "config": { "abort": { - "already_configured": "\u8bbe\u5907\u5df2\u88ab\u914d\u7f6e", + "already_configured": "\u8bbe\u5907\u5df2\u7ecf\u914d\u7f6e\u8fc7\u4e86", "already_configured_device": "\u8bbe\u5907\u5df2\u88ab\u914d\u7f6e", "already_in_progress": "\u914d\u7f6e\u6d41\u7a0b\u5df2\u5728\u8fdb\u884c\u4e2d", "backoff": "\u8bbe\u5907\u76ee\u524d\u6682\u4e0d\u63a5\u53d7\u914d\u5bf9\u8bf7\u6c42\uff08\u53ef\u80fd\u591a\u6b21\u8f93\u5165\u65e0\u6548 PIN \u7801\uff09\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5\u3002", - "device_not_found": "\u65e0\u6cd5\u4fa6\u6d4b\u5230\u8bbe\u5907\uff0c\u8bf7\u5c1d\u8bd5\u91cd\u65b0\u6dfb\u52a0", + "device_not_found": "\u672a\u627e\u5230\u8bbe\u5907\uff0c\u8bf7\u5c1d\u8bd5\u91cd\u65b0\u6dfb\u52a0\u3002", + "inconsistent_device": "\u641c\u7d22\u671f\u95f4\u672a\u53d1\u73b0\u914d\u7f6e\u8bbe\u5907\u6240\u5fc5\u9700\u7684\u534f\u8bae\u3002\u8fd9\u901a\u5e38\u662f\u56e0\u4e3a mDNS \u534f\u8bae\uff08zeroconf\uff09\u5b58\u5728\u95ee\u9898\u3002\u8bf7\u7a0d\u540e\u518d\u91cd\u65b0\u5c1d\u8bd5\u6dfb\u52a0\u8bbe\u5907\u3002", "invalid_config": "\u6b64\u8bbe\u5907\u7684\u914d\u7f6e\u4fe1\u606f\u4e0d\u5b8c\u6574\u3002\u8bf7\u5c1d\u8bd5\u91cd\u65b0\u6dfb\u52a0\u3002", "no_devices_found": "\u672a\u5728\u6b64\u7f51\u7edc\u53d1\u73b0\u76f8\u5173\u8bbe\u5907", - "reauth_successful": "\u91cd\u9a8c\u8bc1\u6210\u529f", - "setup_failed": "\u65e0\u6cd5\u8bbe\u7f6e\u8bbe\u5907\u3002", + "reauth_successful": "\u91cd\u65b0\u8ba4\u8bc1\u6210\u529f", + "setup_failed": "\u8bbe\u7f6e\u8bbe\u5907\u5931\u8d25\u3002", "unknown": "\u672a\u77e5\u9519\u8bef" }, "error": { @@ -22,11 +23,11 @@ "flow_title": "{name} ({type})", "step": { "confirm": { - "description": "\u60a8\u5373\u5c06\u6dfb\u52a0 Apple TV (\u540d\u79f0\u4e3a\u201c{name}\u201d)\u5230 Home Assistant\u3002 \n\n **\u8981\u5b8c\u6210\u6b64\u8fc7\u7a0b\uff0c\u53ef\u80fd\u9700\u8981\u8f93\u5165\u591a\u4e2a PIN \u7801\u3002** \n\n\u8bf7\u6ce8\u610f\uff0c\u6b64\u96c6\u6210*\u4e0d\u80fd*\u5173\u95ed Apple TV \u7684\u7535\u6e90\uff0c\u53ea\u4f1a\u5173\u95ed Home Assistant \u4e2d\u7684\u5a92\u4f53\u64ad\u653e\u5668\uff01", + "description": "\u60a8\u5373\u5c06\u6dfb\u52a0\u201c{type}\u201d(\u540d\u79f0\u4e3a\u201c{name}\u201d) \u5230 Home Assistant\u3002 \n\n **\u8981\u5b8c\u6210\u6b64\u8fc7\u7a0b\uff0c\u53ef\u80fd\u9700\u8981\u8f93\u5165\u591a\u4e2a PIN \u7801\u3002** \n\n\u8bf7\u6ce8\u610f\uff0c\u6b64\u96c6\u6210*\u4e0d\u80fd*\u5173\u95ed Apple TV \u7684\u7535\u6e90\uff0c\u53ea\u4f1a\u5173\u95ed Home Assistant \u4e2d\u7684\u5a92\u4f53\u64ad\u653e\u5668\uff01", "title": "\u786e\u8ba4\u6dfb\u52a0 Apple TV" }, "pair_no_pin": { - "description": "`{protocol}` \u670d\u52a1\u9700\u8981\u914d\u5bf9\u3002\u8bf7\u5728\u60a8\u7684 Apple TV \u4e0a\u8f93\u5165 PIN {pin}", + "description": "\u201c{protocol}\u201d\u670d\u52a1\u9700\u8981\u914d\u5bf9\u3002\u8bf7\u5728\u60a8\u7684 Apple TV \u4e0a\u8f93\u5165 PIN {pin} \u4ee5\u7ee7\u7eed\u3002", "title": "\u914d\u5bf9\u4e2d" }, "pair_with_pin": { @@ -36,14 +37,15 @@ "title": "\u914d\u5bf9\u4e2d" }, "password": { - "description": "`{protocol}` \u9700\u8981\u8f93\u5165\u5bc6\u7801\u3002\u76ee\u524d\u8be5\u8bbe\u5907\u6682\u4e0d\u652f\u6301\u6b64\u529f\u80fd\uff0c\u8bf7\u7981\u7528\u540e\u518d\u7ee7\u7eed\u64cd\u4f5c\u3002", - "title": "\u8f93\u5165\u5bc6\u7801" + "description": "\u201c{protocol}\u201d\u9700\u8981\u8f93\u5165\u5bc6\u7801\uff0c\u76ee\u524d\u6682\u4e0d\u652f\u6301\u3002\u8bf7\u7981\u7528\u5bc6\u7801\u540e\u518d\u7ee7\u7eed\u3002", + "title": "\u9700\u8981\u5bc6\u7801" }, "protocol_disabled": { + "description": "\u201c{protocol}\u201d\u534f\u8bae\u9700\u8981\u914d\u5bf9\uff0c\u4f46\u662f\u5df2\u5728\u8bbe\u5907\u4e0a\u7981\u6b62\u914d\u5bf9\u3002\u8bf7\u68c0\u67e5\u8bbe\u5907\u4e0a\u7684\u8bbf\u95ee\u6743\u9650\u8bbe\u7f6e\uff0c\u4f8b\u5982\u5c06\u201c\u5141\u8bb8\u8bbf\u95ee\u626c\u58f0\u5668\u548c\u7535\u89c6\u201d\u8bbe\u4e3a\u201c\u540c\u4e00\u7f51\u7edc\u4e2d\u7684\u4efb\u4f55\u4eba\u201d\u3002\n\n\u5728\u4e0d\u914d\u5bf9\u201c{protocol}\u201d\u534f\u8bae\u7684\u60c5\u51b5\u4e0b\u4e5f\u53ef\u4ee5\u7ee7\u7eed\uff0c\u4f46\u662f\u4e0d\u80fd\u4f7f\u7528\u6240\u6709\u529f\u80fd\u3002", "title": "\u65e0\u6cd5\u914d\u5bf9" }, "reconfigure": { - "description": "\u8be5 Apple TV \u9047\u5230\u4e00\u4e9b\u8fde\u63a5\u95ee\u9898\uff0c\u987b\u91cd\u65b0\u914d\u7f6e\u3002", + "description": "\u91cd\u65b0\u914d\u7f6e\u8bbe\u5907\u4ee5\u6062\u590d\u5176\u529f\u80fd\u3002", "title": "\u8bbe\u5907\u91cd\u65b0\u914d\u7f6e" }, "service_problem": { @@ -53,7 +55,7 @@ "data": { "device_input": "\u8bbe\u5907\u5730\u5740" }, - "description": "\u8981\u5f00\u59cb\uff0c\u8bf7\u8f93\u5165\u8981\u6dfb\u52a0\u7684 Apple TV \u7684\u8bbe\u5907\u540d\u79f0\u6216 IP \u5730\u5740\u3002\u5728\u7f51\u7edc\u4e0a\u81ea\u52a8\u53d1\u73b0\u7684\u8bbe\u5907\u4f1a\u663e\u793a\u5728\u4e0b\u65b9\u3002 \n\n\u5982\u679c\u6ca1\u6709\u53d1\u73b0\u8bbe\u5907\u6216\u9047\u5230\u4efb\u4f55\u95ee\u9898\uff0c\u8bf7\u5c1d\u8bd5\u6307\u5b9a\u8bbe\u5907 IP \u5730\u5740\u3002 \n\n {devices}", + "description": "\u8981\u5f00\u59cb\uff0c\u8bf7\u8f93\u5165\u8981\u6dfb\u52a0\u7684 Apple TV \u7684\u8bbe\u5907\u540d\u79f0\u6216 IP \u5730\u5740\u3002\n\n\u5982\u679c\u6ca1\u6709\u53d1\u73b0\u8bbe\u5907\u6216\u9047\u5230\u4efb\u4f55\u95ee\u9898\uff0c\u8bf7\u5c1d\u8bd5\u6307\u5b9a\u8bbe\u5907 IP \u5730\u5740\u3002", "title": "\u8bbe\u7f6e\u65b0\u7684 Apple TV" } } diff --git a/homeassistant/components/aurora/translations/ja.json b/homeassistant/components/aurora/translations/ja.json index 86e38f981f9..a4d9b830968 100644 --- a/homeassistant/components/aurora/translations/ja.json +++ b/homeassistant/components/aurora/translations/ja.json @@ -22,5 +22,5 @@ } } }, - "title": "NOAA\u30aa\u30fc\u30ed\u30e9\u30bb\u30f3\u30b5\u30fc" + "title": "NOAA Aurora Sensor" } \ No newline at end of file diff --git a/homeassistant/components/cloudflare/translations/zh-Hans.json b/homeassistant/components/cloudflare/translations/zh-Hans.json index 54d1a3b55f2..8e4bd974cbb 100644 --- a/homeassistant/components/cloudflare/translations/zh-Hans.json +++ b/homeassistant/components/cloudflare/translations/zh-Hans.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "\u91cd\u9a8c\u8bc1\u6210\u529f" + "reauth_successful": "\u91cd\u65b0\u8ba4\u8bc1\u6210\u529f" }, "error": { "cannot_connect": "\u8fde\u63a5\u5931\u8d25", diff --git a/homeassistant/components/coinbase/translations/zh-Hans.json b/homeassistant/components/coinbase/translations/zh-Hans.json index 02d3f5e6773..1a5eaa19dec 100644 --- a/homeassistant/components/coinbase/translations/zh-Hans.json +++ b/homeassistant/components/coinbase/translations/zh-Hans.json @@ -5,13 +5,13 @@ }, "error": { "cannot_connect": "\u8fde\u63a5\u5931\u8d25", - "invalid_auth": "\u9a8c\u8bc1\u65e0\u6548", - "unknown": "\u672a\u77e5\u9519\u8bef" + "invalid_auth": "\u8eab\u4efd\u8ba4\u8bc1\u65e0\u6548", + "unknown": "\u975e\u9884\u671f\u7684\u9519\u8bef" }, "step": { "user": { "data": { - "api_key": "API Key", + "api_key": "API \u5bc6\u94a5", "api_token": "API Token", "currencies": "\u8d26\u6237\u4f59\u989d", "exchange_rates": "\u6c47\u7387" diff --git a/homeassistant/components/dlna_dmr/translations/zh-Hans.json b/homeassistant/components/dlna_dmr/translations/zh-Hans.json index 8bcf49d86c2..2046f1c2a47 100644 --- a/homeassistant/components/dlna_dmr/translations/zh-Hans.json +++ b/homeassistant/components/dlna_dmr/translations/zh-Hans.json @@ -1,39 +1,39 @@ { "config": { "abort": { - "already_configured": "\u8bbe\u5907\u5df2\u88ab\u914d\u7f6e", + "already_configured": "\u8bbe\u5907\u5df2\u7ecf\u914d\u7f6e\u8fc7\u4e86", "alternative_integration": "\u8be5\u8bbe\u5907\u5728\u53e6\u4e00\u96c6\u6210\u80fd\u63d0\u4f9b\u66f4\u597d\u7684\u652f\u6301", "cannot_connect": "\u8fde\u63a5\u5931\u8d25", - "could_not_connect": "\u65e0\u6cd5\u8fde\u63a5\u5230 DLNA \u8bbe\u5907", + "could_not_connect": "\u8fde\u63a5 DLNA \u8bbe\u5907\u5931\u8d25", "discovery_error": "\u672a\u53d1\u73b0\u53ef\u7528\u7684 DLNA \u8bbe\u5907", "incomplete_config": "\u914d\u7f6e\u7f3a\u5c11\u5fc5\u8981\u7684\u53d8\u91cf\u4fe1\u606f", - "non_unique_id": "\u53d1\u73b0\u591a\u53f0\u8bbe\u5907\u5177\u6709\u76f8\u540c\u7684\u552f\u4e00 ID", - "not_dmr": "\u8be5\u8bbe\u5907\u4e0d\u662f\u4e00\u4e2a\u53d7\u652f\u6301\u7684\u6570\u5b57\u5a92\u4f53\u6e32\u67d3\u5668" + "non_unique_id": "\u53d1\u73b0\u591a\u53f0\u8bbe\u5907\u5177\u6709\u76f8\u540c\u7684 unique ID", + "not_dmr": "\u8be5\u8bbe\u5907\u4e0d\u662f\u53d7\u652f\u6301\u7684\u6570\u5b57\u5a92\u4f53\u6e32\u67d3\u5668\uff08DMR\uff09" }, "error": { "cannot_connect": "\u8fde\u63a5\u5931\u8d25", "could_not_connect": "\u65e0\u6cd5\u8fde\u63a5\u5230 DLNA \u8bbe\u5907", - "not_dmr": "\u8be5\u8bbe\u5907\u4e0d\u662f\u4e00\u4e2a\u53d7\u652f\u6301\u7684\u6570\u5b57\u5a92\u4f53\u6e32\u67d3\u5668" + "not_dmr": "\u8be5\u8bbe\u5907\u4e0d\u662f\u53d7\u652f\u6301\u7684\u6570\u5b57\u5a92\u4f53\u6e32\u67d3\u5668\uff08DMR\uff09" }, "flow_title": "{name}", "step": { "confirm": { - "description": "\u4f60\u60f3\u8981\u5f00\u59cb\u8bbe\u7f6e\u5417\uff1f" + "description": "\u60a8\u8981\u5f00\u59cb\u8bbe\u7f6e\u5417\uff1f" }, "import_turn_on": { - "description": "\u8bf7\u6253\u5f00\u8bbe\u5907\u5e76\u5355\u51fb\u201c\u63d0\u4ea4\u201d\u6309\u94ae\u4ee5\u7ee7\u7eed\u8fc1\u79fb" + "description": "\u8bf7\u6253\u5f00\u8bbe\u5907\uff0c\u7136\u540e\u70b9\u51fb\u201c\u63d0\u4ea4\u201d\u4ee5\u7ee7\u7eed\u8fc1\u79fb" }, "manual": { "data": { - "url": "URL" + "url": "\u7f51\u5740" }, - "description": "\u8bbe\u5907\u63cf\u8ff0\u6587\u4ef6(.xml)\u7f51\u5740", + "description": "\u8bbe\u5907\u63cf\u8ff0 XML \u6587\u4ef6\u7f51\u5740", "title": "\u624b\u52a8\u914d\u7f6e DLNA DMR \u8bbe\u5907\u8fde\u63a5" }, "user": { "data": { - "host": "\u4e3b\u673a\u5730\u5740", - "url": "URL" + "host": "\u4e3b\u673a", + "url": "\u7f51\u5740" }, "title": "\u53d1\u73b0 DLNA DMR \u8bbe\u5907" } @@ -46,8 +46,8 @@ "step": { "init": { "data": { - "callback_url_override": "\u4e8b\u4ef6\u4fa6\u542c\u5668\u56de\u8c03 URL", - "listen_port": "\u4e8b\u4ef6\u4fa6\u542c\u5668\u7aef\u53e3\uff08\u5982\u4e0d\u6307\u5b9a\u5219\u968f\u673a\u7aef\u53e3\u53f7\uff09", + "callback_url_override": "\u4e8b\u4ef6\u76d1\u542c\u5668\u7684\u56de\u8c03 URL", + "listen_port": "\u4e8b\u4ef6\u76d1\u542c\u5668\u7aef\u53e3\uff08\u5982\u4e0d\u6307\u5b9a\u5219\u968f\u673a\u7aef\u53e3\u53f7\uff09", "poll_availability": "\u8f6e\u8be2\u8bbe\u5907\u53ef\u7528\u6027" }, "title": "DLNA \u6570\u5b57\u5a92\u4f53\u6e32\u67d3\u5668\u914d\u7f6e" diff --git a/homeassistant/components/emulated_roku/translations/ja.json b/homeassistant/components/emulated_roku/translations/ja.json index 2ea21df53a1..302eeb8e6c7 100644 --- a/homeassistant/components/emulated_roku/translations/ja.json +++ b/homeassistant/components/emulated_roku/translations/ja.json @@ -17,5 +17,5 @@ } } }, - "title": "Roku\u3092\u30a8\u30df\u30e5\u30ec\u30fc\u30c8" + "title": "Emulated Roku" } \ No newline at end of file diff --git a/homeassistant/components/evil_genius_labs/translations/zh-Hans.json b/homeassistant/components/evil_genius_labs/translations/zh-Hans.json new file mode 100644 index 00000000000..a9828445302 --- /dev/null +++ b/homeassistant/components/evil_genius_labs/translations/zh-Hans.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "timeout": "\u5efa\u7acb\u8fde\u63a5\u8d85\u65f6" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/zh-Hans.json b/homeassistant/components/ezviz/translations/zh-Hans.json index ee3cf07bdb3..e88a68c17c2 100644 --- a/homeassistant/components/ezviz/translations/zh-Hans.json +++ b/homeassistant/components/ezviz/translations/zh-Hans.json @@ -23,7 +23,7 @@ "user": { "data": { "password": "\u5bc6\u7801", - "url": "\u9009\u62e9\u670d\u52a1\u5668\u5730\u5740\uff1a", + "url": "\u7f51\u5740", "username": "\u7528\u6237\u540d" }, "title": "\u8fde\u63a5\u81f3\u8424\u77f3\u4e91" diff --git a/homeassistant/components/growatt_server/translations/ja.json b/homeassistant/components/growatt_server/translations/ja.json index 1693f027c78..a32991ab9e0 100644 --- a/homeassistant/components/growatt_server/translations/ja.json +++ b/homeassistant/components/growatt_server/translations/ja.json @@ -24,5 +24,5 @@ } } }, - "title": "Growatt\u30b5\u30fc\u30d0\u30fc" + "title": "Growatt Server" } \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/ja.json b/homeassistant/components/homeassistant/translations/ja.json index f0c5aaa197b..14b1deb55c8 100644 --- a/homeassistant/components/homeassistant/translations/ja.json +++ b/homeassistant/components/homeassistant/translations/ja.json @@ -6,8 +6,8 @@ "docker": "Docker", "hassio": "Supervisor", "installation_type": "\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u7a2e\u5225", - "os_name": "\u30aa\u30da\u30ec\u30fc\u30c6\u30a3\u30f3\u30b0\u30b7\u30b9\u30c6\u30e0\u30d5\u30a1\u30df\u30ea\u30fc", - "os_version": "\u30aa\u30da\u30ec\u30fc\u30c6\u30a3\u30f3\u30b0\u30b7\u30b9\u30c6\u30e0\u306e\u30d0\u30fc\u30b8\u30e7\u30f3", + "os_name": "\u30aa\u30da\u30ec\u30fc\u30c6\u30a3\u30f3\u30b0\u30b7\u30b9\u30c6\u30e0 \uff8c\uff67\uff90\uff98\uff70", + "os_version": "\u30aa\u30da\u30ec\u30fc\u30c6\u30a3\u30f3\u30b0\u30b7\u30b9\u30c6\u30e0\u306e\uff8a\uff9e\uff70\uff7c\uff9e\uff6e\uff9d", "python_version": "Python\u30d0\u30fc\u30b8\u30e7\u30f3", "timezone": "\u30bf\u30a4\u30e0\u30be\u30fc\u30f3", "user": "\u30e6\u30fc\u30b6\u30fc", diff --git a/homeassistant/components/homekit_controller/translations/ja.json b/homeassistant/components/homekit_controller/translations/ja.json index cddf95badf5..8e1e35c60e2 100644 --- a/homeassistant/components/homekit_controller/translations/ja.json +++ b/homeassistant/components/homekit_controller/translations/ja.json @@ -69,5 +69,5 @@ "single_press": "\"{subtype}\" \u304c\u3001\u62bc\u3055\u308c\u307e\u3057\u305f" } }, - "title": "HomeKit\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u30fc" + "title": "HomeKit Controller" } \ No newline at end of file diff --git a/homeassistant/components/jellyfin/translations/zh-Hans.json b/homeassistant/components/jellyfin/translations/zh-Hans.json index df97498e25e..2df66a1d385 100644 --- a/homeassistant/components/jellyfin/translations/zh-Hans.json +++ b/homeassistant/components/jellyfin/translations/zh-Hans.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u5f53\u524d\u90e8\u4ef6\u5df2\u5b58\u5728\u914d\u7f6e\uff0c\u8bf7\u5220\u9664\u73b0\u6709\u914d\u7f6e\u540e\u91cd\u8bd5\u3002" + "single_instance_allowed": "\u8be5\u96c6\u6210\u5df2\u7ecf\u914d\u7f6e\u8fc7\u4e86\uff0c\u4e14\u53ea\u80fd\u914d\u7f6e\u4e00\u6b21\u3002\u82e5\u8981\u91cd\u65b0\u914d\u7f6e\uff0c\u8bf7\u5148\u5220\u9664\u65e7\u96c6\u6210\u3002" }, "error": { "cannot_connect": "\u8fde\u63a5\u5931\u8d25", diff --git a/homeassistant/components/kostal_plenticore/translations/ja.json b/homeassistant/components/kostal_plenticore/translations/ja.json index d4f08a1dd5c..8a16c8c918e 100644 --- a/homeassistant/components/kostal_plenticore/translations/ja.json +++ b/homeassistant/components/kostal_plenticore/translations/ja.json @@ -17,5 +17,5 @@ } } }, - "title": "Kostal Plenticore\u30bd\u30fc\u30e9\u30fc\u30a4\u30f3\u30d0\u30fc\u30bf\u30fc" + "title": "Kostal Plenticore Solar Inverter" } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/zh-Hans.json b/homeassistant/components/mqtt/translations/zh-Hans.json index fafb91a66f3..f897bee3c9b 100644 --- a/homeassistant/components/mqtt/translations/zh-Hans.json +++ b/homeassistant/components/mqtt/translations/zh-Hans.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u670d\u52a1\u5df2\u88ab\u914d\u7f6e", - "single_instance_allowed": "\u914d\u7f6e\u5df2\u5b58\u5728\uff0c\u8bf7\u5220\u9664\u73b0\u6709\u914d\u7f6e\u540e\u518d\u91cd\u8bd5\u3002" + "single_instance_allowed": "\u8be5\u96c6\u6210\u5df2\u7ecf\u914d\u7f6e\u8fc7\u4e86\uff0c\u4e14\u53ea\u80fd\u914d\u7f6e\u4e00\u6b21\u3002\u82e5\u8981\u91cd\u65b0\u914d\u7f6e\uff0c\u8bf7\u5148\u5220\u9664\u65e7\u96c6\u6210\u3002" }, "error": { "cannot_connect": "\u65e0\u6cd5\u8fde\u63a5\u5230\u670d\u52a1\u5668\u3002" @@ -11,7 +11,7 @@ "broker": { "data": { "broker": "\u670d\u52a1\u5668", - "discovery": "\u542f\u7528\u53d1\u73b0\u529f\u80fd", + "discovery": "\u542f\u7528\u53d1\u73b0", "password": "\u5bc6\u7801", "port": "\u7aef\u53e3", "username": "\u7528\u6237\u540d" @@ -20,7 +20,7 @@ }, "hassio_confirm": { "data": { - "discovery": "\u542f\u7528\u53d1\u73b0\u529f\u80fd" + "discovery": "\u542f\u7528\u53d1\u73b0" }, "description": "\u662f\u5426\u8981\u914d\u7f6e Home Assistant \u8fde\u63a5\u5230 Supervisor \u52a0\u8f7d\u9879 {addon} \u63d0\u4f9b\u7684 MQTT \u670d\u52a1\u5668\uff1f", "title": "\u6765\u81ea Supervisor \u52a0\u8f7d\u9879\u7684 MQTT \u670d\u52a1\u5668" @@ -51,8 +51,8 @@ }, "options": { "error": { - "bad_birth": "\u65e0\u6548\u7684\u51fa\u751f\u6d88\u606f(bitrh)\u7c7b\u578b", - "bad_will": "\u65e0\u6548\u7684\u9057\u5631\u6d88\u606f(will)\u7c7b\u578b", + "bad_birth": "\u51fa\u751f\u6d88\u606f\u4e3b\u9898\u65e0\u6548", + "bad_will": "\u9057\u5631\u6d88\u606f\u4e3b\u9898\u65e0\u6548", "cannot_connect": "\u8fde\u63a5\u5931\u8d25" }, "step": { @@ -63,19 +63,19 @@ "port": "\u7aef\u53e3", "username": "\u7528\u6237\u540d" }, - "description": "\u8bf7\u8f93\u5165\u60a8\u7684 MQTT \u670d\u52a1\u5668\u8fde\u63a5\u4fe1\u606f", + "description": "\u8bf7\u8f93\u5165 MQTT \u670d\u52a1\u5668\u7684\u8fde\u63a5\u4fe1\u606f\u3002", "title": "\u670d\u52a1\u5668\u9009\u9879" }, "options": { "data": { - "birth_enable": "\u542f\u7528\u51fa\u751f\u6d88\u606f(birth)", + "birth_enable": "\u542f\u7528\u51fa\u751f\u6d88\u606f", "birth_qos": "\u51fa\u751f\u6d88\u606f QoS", - "birth_topic": "\u51fa\u751f\u6d88\u606f(birth)\u7c7b\u578b", - "discovery": "\u542f\u7528\u53d1\u73b0\u529f\u80fd", - "will_enable": "\u542f\u7528\u9057\u5631\u6d88\u606f(will)", - "will_topic": "\u9057\u5631\u6d88\u606f\u7c7b\u578b(will)" + "birth_topic": "\u51fa\u751f\u6d88\u606f\u4e3b\u9898", + "discovery": "\u542f\u7528\u53d1\u73b0", + "will_enable": "\u542f\u7528\u9057\u5631\u6d88\u606f", + "will_topic": "\u9057\u5631\u6d88\u606f\u4e3b\u9898" }, - "description": "\u201c\u53d1\u73b0\u201d\u529f\u80fd - \u82e5\u201c\u53d1\u73b0\u201d\u542f\u7528\u529f\u80fd(\u63a8\u8350)\uff0cHome Assistant \u5c06\u4f1a\u901a\u8fc7\u4e0e\u5176\u8fde\u63a5\u7684 MQTT \u670d\u52a1\u5668\u4e2d\u81ea\u52a8\u641c\u5bfb\u76f8\u5173\u8bbe\u5907\u548c\u5b9e\u4f53\u3002\u5982\u679c\u201c\u53d1\u73b0\u201d\u529f\u80fd\u5173\u95ed\uff0c\u5219\u6240\u6709\u4e0e\u5176\u76f8\u5173\u7684\u914d\u7f6e\u9700\u624b\u52a8\u914d\u7f6e\u3002\n\n\u51fa\u751f\u6d88\u606f(bitrh message) - Home Assistant \u5c06\u4f1a\u5728\u6bcf\u6b21(\u91cd)\u8fde\u63a5\u65f6\u90fd\u4f1a\u53d1\u9001\u51fa\u751f\u6d88\u606f\u5230 MQTT \u670d\u52a1\u5668\n\u9057\u5631\u6d88\u606f(will message) - Home Assistant \u5c06\u4f1a\u5728\u6bcf\u6b21\u4e0e MQTT \u670d\u52a1\u5668\u5931\u53bb\u8fde\u63a5\u65f6\uff0c\u5bf9\u5176\u53d1\u9001\u9057\u5631\u4fe1\u606f\uff0c\u65e0\u8bba\u662f\u6b63\u5e38\u79bb\u7ebf(\u4f8b\u5982 Home Assistant \u6b63\u5e38\u5173\u95ed)\u6216\u975e\u6b63\u5e38\u79bb\u7ebf(\u4f8b\u5982 Home Assistant \u5d29\u6e83\u6216\u7f51\u7edc\u8fde\u63a5\u65ad\u5f00)\u3002", + "description": "\u53d1\u73b0\uff1a\u5982\u679c\u542f\u7528\u201c\u53d1\u73b0\u201d\u529f\u80fd\uff08\u63a8\u8350\uff09\uff0c\u5bf9\u4e8e\u90a3\u4e9b\u5c06\u81ea\u8eab\u7684\u914d\u7f6e\u4fe1\u606f\u53d1\u5e03\u5230 MQTT \u670d\u52a1\u5668\u4e0a\u7684\u8bbe\u5907\uff0cHome Assistant \u4f1a\u81ea\u52a8\u641c\u7d22\u5230\u5b83\u4eec\u3002\u5982\u679c\u5173\u95ed\u201c\u53d1\u73b0\u201d\u529f\u80fd\uff0c\u5219\u9700\u8981\u624b\u52a8\u914d\u7f6e\u8fd9\u4e9b\u8bbe\u5907\u3002\n\n\u51fa\u751f\u6d88\u606f\uff1a\u6bcf\u5f53 Home Assistant \u8fde\u63a5\u6216\u91cd\u65b0\u8fde\u63a5\u5230 MQTT \u670d\u52a1\u5668\u65f6\uff0c\u8be5\u6d88\u606f\u90fd\u4f1a\u88ab\u53d1\u9001\u5230 MQTT \u670d\u52a1\u5668\u3002\n\n\u9057\u5631\u6d88\u606f\uff1a\u6bcf\u5f53 Home Assistant \u4e0e MQTT \u670d\u52a1\u5668\u65ad\u5f00\u8fde\u63a5\u65f6\u3010\u65e0\u8bba\u662f\u6b63\u5e38\u79bb\u7ebf\uff08\u4f8b\u5982 Home Assistant \u6b63\u5e38\u5173\u95ed\uff09\u6216\u975e\u6b63\u5e38\u79bb\u7ebf\uff08\u4f8b\u5982 Home Assistant \u5d29\u6e83\u6216\u7f51\u7edc\u8fde\u63a5\u4e2d\u65ad\uff09\u3011\uff0c\u8be5\u6d88\u606f\u90fd\u4f1a\u88ab\u53d1\u9001\u5230 MQTT \u670d\u52a1\u5668\u3002", "title": "MQTT \u9009\u9879" } } diff --git a/homeassistant/components/mysensors/translations/zh-Hans.json b/homeassistant/components/mysensors/translations/zh-Hans.json index fa797918a5a..a1df0a563a5 100644 --- a/homeassistant/components/mysensors/translations/zh-Hans.json +++ b/homeassistant/components/mysensors/translations/zh-Hans.json @@ -1,14 +1,14 @@ { "config": { "error": { - "mqtt_required": "\u8be5 MQTT \u96c6\u6210\u672a\u914d\u7f6e" + "mqtt_required": "\u672a\u914d\u7f6e MQTT \u96c6\u6210" }, "step": { "gw_mqtt": { "data": { "persistence_file": "\u6301\u4e45\u6027\u6587\u4ef6\uff08\u7559\u7a7a\u5c06\u81ea\u52a8\u751f\u6210\uff09", - "topic_in_prefix": "\u8f93\u5165\u7c7b\u578b\u524d\u7f00 (topic_in_prefix)", - "topic_out_prefix": "\u8f93\u51fa\u7c7b\u578b\u524d\u7f00 (topic_out_prefix)", + "topic_in_prefix": "\u8f93\u5165\u4e3b\u9898\u524d\u7f00 (topic_in_prefix)", + "topic_out_prefix": "\u8f93\u51fa\u4e3b\u9898\u524d\u7f00 (topic_out_prefix)", "version": "MySensor \u7248\u672c" }, "description": "MQTT \u7f51\u5173\u8bbe\u7f6e" diff --git a/homeassistant/components/nmap_tracker/translations/ja.json b/homeassistant/components/nmap_tracker/translations/ja.json index fe75eca98c5..2e60a814468 100644 --- a/homeassistant/components/nmap_tracker/translations/ja.json +++ b/homeassistant/components/nmap_tracker/translations/ja.json @@ -37,5 +37,5 @@ } } }, - "title": "Nmap\u30c8\u30e9\u30c3\u30ab\u30fc" + "title": "Nmap Tracker" } \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/de.json b/homeassistant/components/overkiz/translations/de.json new file mode 100644 index 00000000000..27719b662fd --- /dev/null +++ b/homeassistant/components/overkiz/translations/de.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "server_in_maintenance": "Server ist wegen Wartungsarbeiten au\u00dfer Betrieb", + "too_many_requests": "Zu viele Anfragen, versuche es sp\u00e4ter erneut.", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host", + "hub": "Hub", + "password": "Passwort", + "username": "Benutzername" + }, + "description": "Die Overkiz-Plattform wird von verschiedenen Anbietern wie Somfy (Connexoon / TaHoma), Hitachi (Hi Kumo), Rexel (Energeasy Connect) und Atlantic (Cozytouch) verwendet. Gib deine Anmeldeinformationen ein und w\u00e4hle deinen Hub aus." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/ja.json b/homeassistant/components/overkiz/translations/ja.json new file mode 100644 index 00000000000..6ff74c5a61e --- /dev/null +++ b/homeassistant/components/overkiz/translations/ja.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "server_in_maintenance": "\u30e1\u30f3\u30c6\u30ca\u30f3\u30b9\u306e\u305f\u3081\u30b5\u30fc\u30d0\u30fc\u304c\u30c0\u30a6\u30f3\u3057\u3066\u3044\u307e\u3059", + "too_many_requests": "\u30ea\u30af\u30a8\u30b9\u30c8\u304c\u591a\u3059\u304e\u307e\u3059\u3002\u3057\u3070\u3089\u304f\u3057\u3066\u304b\u3089\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8", + "hub": "\u30cf\u30d6", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + }, + "description": "Overkiz\u30d7\u30e9\u30c3\u30c8\u30d5\u30a9\u30fc\u30e0\u306f\u3001Somfy(Connexoon / TaHoma)\u3001\u65e5\u7acb(Hi Kumo)\u3001Rexel(Energeasy Connect)\u3001Atlantic(Cozytouch)\u306a\u3069\u69d8\u3005\u306a\u30d9\u30f3\u30c0\u30fc\u3067\u4f7f\u7528\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u306e\u8a8d\u8a3c\u60c5\u5831\u3092\u5165\u529b\u3057\u3001\u30cf\u30d6\u3092\u9078\u629e\u3057\u307e\u3059\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/nl.json b/homeassistant/components/overkiz/translations/nl.json new file mode 100644 index 00000000000..aea848819ce --- /dev/null +++ b/homeassistant/components/overkiz/translations/nl.json @@ -0,0 +1,13 @@ +{ + "config": { + "error": { + "server_in_maintenance": "Server is offline wegens onderhoud", + "too_many_requests": "Te veel verzoeken, probeer het later opnieuw." + }, + "step": { + "user": { + "description": "Het Overkiz platform wordt gebruikt door fabrikanten zoals Somfy (Connexoon / TaHoma), Hitachi (Hi Kumo), Rexel (Energeasy Connect) en Atlantic (Cozytouch). Voer je inloggegevens in en selecteer je hub." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/ru.json b/homeassistant/components/overkiz/translations/ru.json new file mode 100644 index 00000000000..3373a23e407 --- /dev/null +++ b/homeassistant/components/overkiz/translations/ru.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "server_in_maintenance": "\u0421\u0435\u0440\u0432\u0435\u0440 \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0432 \u0441\u0432\u044f\u0437\u0438 \u0441 \u0442\u0435\u0445\u043d\u0438\u0447\u0435\u0441\u043a\u0438\u043c \u043e\u0431\u0441\u043b\u0443\u0436\u0438\u0432\u0430\u043d\u0438\u0435\u043c.", + "too_many_requests": "\u0421\u043b\u0438\u0448\u043a\u043e\u043c \u043c\u043d\u043e\u0433\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "hub": "\u0425\u0430\u0431", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "description": "\u041f\u043b\u0430\u0442\u0444\u043e\u0440\u043c\u0430 Overkiz \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0440\u0430\u0437\u043b\u0438\u0447\u043d\u044b\u043c\u0438 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044f\u043c\u0438, \u0442\u0430\u043a\u0438\u043c\u0438 \u043a\u0430\u043a Somfy (Connexoon / TaHoma), Hitachi (Hi Kumo), Rexel (Energeasy Connect) \u0438 Atlantic (Cozytouch). \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0412\u0430\u0448\u0435\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0445\u0430\u0431." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/ja.json b/homeassistant/components/picnic/translations/ja.json index 9233c2f6aea..5379949aa96 100644 --- a/homeassistant/components/picnic/translations/ja.json +++ b/homeassistant/components/picnic/translations/ja.json @@ -18,5 +18,5 @@ } } }, - "title": "\u30d4\u30af\u30cb\u30c3\u30af" + "title": "Picnic" } \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/ja.json b/homeassistant/components/srp_energy/translations/ja.json index 69fc945db58..432c553910b 100644 --- a/homeassistant/components/srp_energy/translations/ja.json +++ b/homeassistant/components/srp_energy/translations/ja.json @@ -20,5 +20,5 @@ } } }, - "title": "SRP\u30a8\u30cd\u30eb\u30ae\u30fc" + "title": "SRP Energy" } \ No newline at end of file diff --git a/homeassistant/components/tailscale/translations/zh-Hans.json b/homeassistant/components/tailscale/translations/zh-Hans.json index c60891584b7..aad29828e47 100644 --- a/homeassistant/components/tailscale/translations/zh-Hans.json +++ b/homeassistant/components/tailscale/translations/zh-Hans.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u8fde\u63a5\u5931\u8d25", - "invalid_auth": "\u9a8c\u8bc1\u65e0\u6548" + "invalid_auth": "\u8eab\u4efd\u8ba4\u8bc1\u65e0\u6548" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/tesla_wall_connector/translations/zh-Hans.json b/homeassistant/components/tesla_wall_connector/translations/zh-Hans.json index c50e493685b..21ee02edbf0 100644 --- a/homeassistant/components/tesla_wall_connector/translations/zh-Hans.json +++ b/homeassistant/components/tesla_wall_connector/translations/zh-Hans.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "\u4e3b\u673a\u5730\u5740" + "host": "\u4e3b\u673a" }, "title": "\u914d\u7f6e Tesla \u58c1\u6302\u5f0f\u5145\u7535\u8fde\u63a5\u5668" } diff --git a/homeassistant/components/tradfri/translations/zh-Hans.json b/homeassistant/components/tradfri/translations/zh-Hans.json index 50142115451..80136e6cf85 100644 --- a/homeassistant/components/tradfri/translations/zh-Hans.json +++ b/homeassistant/components/tradfri/translations/zh-Hans.json @@ -5,7 +5,7 @@ "already_in_progress": "\u6865\u914d\u7f6e\u5df2\u5728\u8fdb\u884c\u4e2d\u3002" }, "error": { - "cannot_authenticate": "\u8fde\u63a5\u5931\u8d25\uff0c\u8bf7\u68c0\u67e5\u7f51\u5173\u662f\u5426\u4e0e\u5176\u5b83\u670d\u52a1\u5668\u5df2\u914d\u5bf9", + "cannot_authenticate": "\u8ba4\u8bc1\u5931\u8d25\uff0c\u8bf7\u68c0\u67e5\u7f51\u5173\u662f\u5426\u5df2\u901a\u8fc7\u5176\u4ed6\u670d\u52a1\u914d\u5bf9\uff0c\u4f8b\u5982 HomeKit\u3002", "cannot_connect": "\u65e0\u6cd5\u8fde\u63a5\u5230\u7f51\u5173\u3002", "invalid_key": "\u65e0\u6cd5\u7528\u63d0\u4f9b\u7684\u5bc6\u94a5\u6ce8\u518c\u3002\u5982\u679c\u9519\u8bef\u6301\u7eed\u53d1\u751f\uff0c\u8bf7\u5c1d\u8bd5\u91cd\u65b0\u542f\u52a8\u7f51\u5173\u3002", "timeout": "\u4ee3\u7801\u9a8c\u8bc1\u8d85\u65f6" diff --git a/homeassistant/components/tuya/translations/zh-Hans.json b/homeassistant/components/tuya/translations/zh-Hans.json index 5c4e0420f93..8be688d821e 100644 --- a/homeassistant/components/tuya/translations/zh-Hans.json +++ b/homeassistant/components/tuya/translations/zh-Hans.json @@ -3,7 +3,7 @@ "abort": { "cannot_connect": "\u8fde\u63a5\u5931\u8d25", "invalid_auth": "\u8eab\u4efd\u8ba4\u8bc1\u65e0\u6548", - "single_instance_allowed": "\u5df2\u7ecf\u914d\u7f6e\u8fc7\u4e86\uff0c\u4e14\u53ea\u80fd\u914d\u7f6e\u4e00\u6b21\u3002" + "single_instance_allowed": "\u8be5\u96c6\u6210\u5df2\u7ecf\u914d\u7f6e\u8fc7\u4e86\uff0c\u4e14\u53ea\u80fd\u914d\u7f6e\u4e00\u6b21\u3002\u82e5\u8981\u91cd\u65b0\u914d\u7f6e\uff0c\u8bf7\u5148\u5220\u9664\u65e7\u96c6\u6210\u3002" }, "error": { "invalid_auth": "\u8eab\u4efd\u8ba4\u8bc1\u65e0\u6548", diff --git a/homeassistant/components/upnp/translations/zh-Hans.json b/homeassistant/components/upnp/translations/zh-Hans.json index 172fdc51e0b..09cf281418b 100644 --- a/homeassistant/components/upnp/translations/zh-Hans.json +++ b/homeassistant/components/upnp/translations/zh-Hans.json @@ -21,7 +21,7 @@ "step": { "init": { "data": { - "scan_interval": "\u66f4\u65b0\u95f4\u9694\uff08\u5355\u4f4d\uff1a\u79d2\uff0c\u6700\u77ed 30\u79d2\uff09" + "scan_interval": "\u66f4\u65b0\u95f4\u9694\uff08\u79d2\uff0c\u6700\u77ed 30 \u79d2\uff09" } } } diff --git a/homeassistant/components/version/translations/ca.json b/homeassistant/components/version/translations/ca.json new file mode 100644 index 00000000000..c429e56900b --- /dev/null +++ b/homeassistant/components/version/translations/ca.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "step": { + "user": { + "data": { + "version_source": "Origen de la versi\u00f3" + }, + "description": "Selecciona la font de la qual vols fer el seguiment de versions", + "title": "Selecciona el tipus d'instal\u00b7laci\u00f3" + }, + "version_source": { + "data": { + "beta": "Inclou versions beta", + "board": "Quina placa se li ha de fer seguiment", + "channel": "Quin canal se li ha de fer seguiment", + "image": "Quina imatge se li ha de fer seguiment" + }, + "description": "Configura el seguiment de versions de {version_source}", + "title": "Configuraci\u00f3" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/version/translations/de.json b/homeassistant/components/version/translations/de.json new file mode 100644 index 00000000000..e0c4d531566 --- /dev/null +++ b/homeassistant/components/version/translations/de.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "step": { + "user": { + "data": { + "version_source": "Versionsquelle" + }, + "description": "W\u00e4hle die Quelle aus, aus der du Versionen verfolgen m\u00f6chtest", + "title": "Installationstyp ausw\u00e4hlen" + }, + "version_source": { + "data": { + "beta": "Betaversionen einschlie\u00dfen", + "board": "Welches Board soll getrackt werden", + "channel": "Welcher Kanal soll getrackt werden", + "image": "Welches Bild soll verfolgt werden" + }, + "description": "Konfiguriere {version_source} Versionsverfolgung", + "title": "Konfigurieren" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/version/translations/ja.json b/homeassistant/components/version/translations/ja.json new file mode 100644 index 00000000000..a2eba2ffbc0 --- /dev/null +++ b/homeassistant/components/version/translations/ja.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "step": { + "user": { + "data": { + "version_source": "\u30d0\u30fc\u30b8\u30e7\u30f3\u30bd\u30fc\u30b9" + }, + "description": "\u30d0\u30fc\u30b8\u30e7\u30f3\u3092\u30c8\u30e9\u30c3\u30ad\u30f3\u30b0\u3057\u305f\u3044\u30bd\u30fc\u30b9\u3092\u9078\u629e", + "title": "\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u30bf\u30a4\u30d7\u306e\u9078\u629e" + }, + "version_source": { + "data": { + "beta": "\u30d9\u30fc\u30bf\u7248\u3092\u542b\u3081\u308b", + "board": "\u3069\u306e\u30dc\u30fc\u30c9\u3092\u8ffd\u8de1\u3059\u308b\u304b", + "channel": "\u3069\u306e\u30c1\u30e3\u30f3\u30cd\u30eb\u3092\u8ffd\u8de1\u3059\u308b\u304b", + "image": "\u3069\u306e\u753b\u50cf\u3092\u8ffd\u8de1\u3059\u308b\u304b" + }, + "description": "{version_source} \u30d0\u30fc\u30b8\u30e7\u30f3\u30c8\u30e9\u30c3\u30ad\u30f3\u30b0\u306e\u8a2d\u5b9a", + "title": "\u8a2d\u5b9a" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/version/translations/nl.json b/homeassistant/components/version/translations/nl.json new file mode 100644 index 00000000000..7517e57927e --- /dev/null +++ b/homeassistant/components/version/translations/nl.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "version_source": { + "title": "Configureer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/version/translations/ru.json b/homeassistant/components/version/translations/ru.json new file mode 100644 index 00000000000..f8db1079700 --- /dev/null +++ b/homeassistant/components/version/translations/ru.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "step": { + "user": { + "data": { + "version_source": "\u0418\u0441\u0442\u043e\u0447\u043d\u0438\u043a \u0432\u0435\u0440\u0441\u0438\u0438" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a, \u0438\u0437 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c \u0432\u0435\u0440\u0441\u0438\u0438.", + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438" + }, + "version_source": { + "data": { + "beta": "\u0412\u043a\u043b\u044e\u0447\u0430\u044f \u0431\u0435\u0442\u0430-\u0432\u0435\u0440\u0441\u0438\u0438", + "board": "\u041a\u0430\u043a\u0443\u044e \u043f\u043b\u0430\u0442\u0443 \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c", + "channel": "\u041a\u0430\u043a\u043e\u0439 \u043a\u0430\u043d\u0430\u043b \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c", + "image": "\u041a\u0430\u043a\u043e\u0439 \u043e\u0431\u0440\u0430\u0437 \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u044f \u0432\u0435\u0440\u0441\u0438\u0439 {version_source}", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/zh-Hans.json b/homeassistant/components/xiaomi_miio/translations/zh-Hans.json index c3eb4affc4c..02519414492 100644 --- a/homeassistant/components/xiaomi_miio/translations/zh-Hans.json +++ b/homeassistant/components/xiaomi_miio/translations/zh-Hans.json @@ -4,7 +4,8 @@ "already_configured": "\u8bbe\u5907\u5df2\u7ecf\u914d\u7f6e\u8fc7\u4e86", "already_in_progress": "\u6b64\u5c0f\u7c73\u8bbe\u5907\u7684\u914d\u7f6e\u6d41\u7a0b\u5df2\u5728\u8fdb\u884c\u4e2d\u3002", "incomplete_info": "\u8bbe\u5907\u4fe1\u606f\u4e0d\u5b8c\u6574\uff0c\u672a\u63d0\u4f9b IP \u6216 token\u3002", - "not_xiaomi_miio": "Xiaomi Miio \u6682\u672a\u9002\u914d\u8be5\u8bbe\u5907\u3002" + "not_xiaomi_miio": "Xiaomi Miio \u6682\u672a\u9002\u914d\u8be5\u8bbe\u5907\u3002", + "reauth_successful": "\u91cd\u65b0\u8ba4\u8bc1\u6210\u529f" }, "error": { "cannot_connect": "\u8fde\u63a5\u5931\u8d25", @@ -60,7 +61,8 @@ "description": "\u60a8\u9700\u8981\u83b7\u53d6\u4e00\u4e2a 32 \u4f4d\u7684 API Token\uff0c\u8bf7\u53c2\u8003 https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token \u4e2d\u63d0\u5230\u7684\u65b9\u6cd5\u83b7\u53d6\u8be5\u4fe1\u606f\u3002\u8bf7\u6ce8\u610f\uff0c\u8be5 API Token \u4e0d\u540c\u4e8e \"Xiaomi Aqara\" \u96c6\u6210\u6240\u4f7f\u7528\u7684\u5bc6\u94a5\u3002" }, "reauth_confirm": { - "description": "\u5c0f\u7c73 Miio \u96c6\u6210\u9700\u8981\u91cd\u65b0\u9a8c\u8bc1\u60a8\u7684\u5e10\u6237\uff0c\u4ee5\u4fbf\u66f4\u65b0 token \u6216\u6dfb\u52a0\u4e22\u5931\u7684\u4e91\u7aef\u51ed\u636e\u3002" + "description": "\u5c0f\u7c73 Miio \u96c6\u6210\u9700\u8981\u91cd\u65b0\u9a8c\u8bc1\u60a8\u7684\u5e10\u6237\uff0c\u4ee5\u4fbf\u66f4\u65b0 token \u6216\u6dfb\u52a0\u4e22\u5931\u7684\u4e91\u7aef\u51ed\u636e\u3002", + "title": "\u4f7f\u96c6\u6210\u91cd\u65b0\u8fdb\u884c\u8eab\u4efd\u8ba4\u8bc1" }, "select": { "data": { From 639181108fa2814b17504d404405059b98a31ab7 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sat, 25 Dec 2021 04:33:20 -0500 Subject: [PATCH 1030/2644] Add zwave_js support for Fortrezz SSA3 (#62765) --- .../components/zwave_js/discovery.py | 4 +- tests/components/zwave_js/conftest.py | 14 + .../fixtures/fortrezz_ssa3_siren_state.json | 355 ++++++++++++++++++ tests/components/zwave_js/test_discovery.py | 5 + 4 files changed, 376 insertions(+), 2 deletions(-) create mode 100644 tests/components/zwave_js/fixtures/fortrezz_ssa3_siren_state.json diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 6f2d83f99c3..3692e50d595 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -442,13 +442,13 @@ DISCOVERY_SCHEMAS = [ dependent_value=ZwaveValueID(2, CommandClass.CONFIGURATION, endpoint=0), ), ), - # FortrezZ SSA1/SSA2 + # FortrezZ SSA1/SSA2/SSA3 ZWaveDiscoverySchema( platform="select", hint="multilevel_switch", manufacturer_id={0x0084}, product_id={0x0107, 0x0108, 0x010B, 0x0205}, - product_type={0x0311, 0x0313, 0x0341, 0x0343}, + product_type={0x0311, 0x0313, 0x0331, 0x0341, 0x0343}, primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, data_template=BaseDiscoverySchemaDataTemplate( { diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index e2db9fe7a6b..12eaccec612 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -473,6 +473,12 @@ def fortrezz_ssa1_siren_state_fixture(): return json.loads(load_fixture("zwave_js/fortrezz_ssa1_siren_state.json")) +@pytest.fixture(name="fortrezz_ssa3_siren_state", scope="session") +def fortrezz_ssa3_siren_state_fixture(): + """Load the fortrezz ssa3 siren node state fixture data.""" + return json.loads(load_fixture("zwave_js/fortrezz_ssa3_siren_state.json")) + + @pytest.fixture(name="client") def mock_client_fixture(controller_state, version_state, log_config_state): """Mock a client.""" @@ -901,6 +907,14 @@ def fortrezz_ssa1_siren_fixture(client, fortrezz_ssa1_siren_state): return node +@pytest.fixture(name="fortrezz_ssa3_siren") +def fortrezz_ssa3_siren_fixture(client, fortrezz_ssa3_siren_state): + """Mock a fortrezz ssa3 siren node.""" + node = Node(client, copy.deepcopy(fortrezz_ssa3_siren_state)) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="firmware_file") def firmware_file_fixture(): """Return mock firmware file stream.""" diff --git a/tests/components/zwave_js/fixtures/fortrezz_ssa3_siren_state.json b/tests/components/zwave_js/fixtures/fortrezz_ssa3_siren_state.json new file mode 100644 index 00000000000..fb31f838667 --- /dev/null +++ b/tests/components/zwave_js/fixtures/fortrezz_ssa3_siren_state.json @@ -0,0 +1,355 @@ +{ + "nodeId": 61, + "index": 0, + "status": 4, + "ready": true, + "isListening": true, + "isRouting": true, + "isSecure": false, + "manufacturerId": 132, + "productId": 267, + "productType": 817, + "firmwareVersion": "1.11", + "deviceConfig": { + "filename": "/data/db/devices/0x0084/ssa3.json", + "isEmbedded": true, + "manufacturer": "FortrezZ LLC", + "manufacturerId": 132, + "label": "SSA3", + "description": "Siren and Strobe Alarm", + "devices": [ + { + "productType": 833, + "productId": 517 + }, + { + "productType": 817, + "productId": 267 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "paramInformation": { + "_map": {} + } + }, + "label": "SSA3", + "interviewAttempts": 1, + "endpoints": [ + { + "nodeId": 61, + "index": 0, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 0, + "label": "Unused" + }, + "mandatorySupportedCCs": [ + 32, + 38 + ], + "mandatoryControlledCCs": [] + } + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": [ + "transitionDuration" + ], + "min": 0, + "max": 99 + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 1, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Transition duration" + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value", + "min": 0, + "max": 99 + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Up", + "propertyName": "Up", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (Up)", + "ccSpecific": { + "switchType": 2 + } + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Down", + "propertyName": "Down", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (Down)", + "ccSpecific": { + "switchType": 2 + } + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535 + }, + "value": 132 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535 + }, + "value": 817 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535 + }, + "value": 267 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Library type", + "states": { + "0": "Unknown", + "1": "Static Controller", + "2": "Controller", + "3": "Enhanced Slave", + "4": "Slave", + "5": "Installer", + "6": "Routing Slave", + "7": "Bridge Controller", + "8": "Device under Test", + "9": "N/A", + "10": "AV Remote", + "11": "AV Device" + } + }, + "value": 6 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 1, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "2.97" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 1, + "metadata": { + "type": "string[]", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": [ + "1.11" + ] + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 1, + "propertyName": "Delay before accept of Basic Set Off", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Delay before accept of Basic Set Off", + "default": 0, + "min": 0, + "max": 255, + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + } + ], + "isFrequentListening": false, + "maxDataRate": 40000, + "supportedDataRates": [ + 40000 + ], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 0, + "label": "Unused" + }, + "mandatorySupportedCCs": [ + 32, + 38 + ], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 38, + "name": "Multilevel Switch", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 1, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 2, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + } + ], + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0084:0x0331:0x010b:1.11", + "statistics": { + "commandsTX": 12, + "commandsRX": 11, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 1 + }, + "highestSecurityClass": -1 +} diff --git a/tests/components/zwave_js/test_discovery.py b/tests/components/zwave_js/test_discovery.py index ad176d0168e..37cdbf3386d 100644 --- a/tests/components/zwave_js/test_discovery.py +++ b/tests/components/zwave_js/test_discovery.py @@ -71,6 +71,11 @@ async def test_lock_popp_electric_strike_lock_control( ) +async def test_fortrez_ssa3_siren(hass, client, fortrezz_ssa3_siren, integration): + """Test Fortrezz SSA3 siren gets discovered correctly.""" + assert hass.states.get("select.siren_and_strobe_alarm") is not None + + async def test_firmware_version_range_exception(hass): """Test FirmwareVersionRange exception.""" with pytest.raises(ValueError): From 7d7f5272fec56ebf95b8124349f3e8950cb48741 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 25 Dec 2021 10:52:31 +0100 Subject: [PATCH 1031/2644] Add device info to Luftdaten (#62692) --- homeassistant/components/luftdaten/sensor.py | 8 ++++++ tests/components/luftdaten/test_sensor.py | 27 +++++++++++++++----- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/luftdaten/sensor.py b/homeassistant/components/luftdaten/sensor.py index 422e5ed7117..69ad30eb926 100644 --- a/homeassistant/components/luftdaten/sensor.py +++ b/homeassistant/components/luftdaten/sensor.py @@ -20,6 +20,7 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -114,6 +115,13 @@ class LuftdatenSensor(CoordinatorEntity, SensorEntity): self._attr_extra_state_attributes = { ATTR_SENSOR_ID: sensor_id, } + self._attr_device_info = DeviceInfo( + configuration_url=f"https://devices.sensor.community/sensors/{sensor_id}/settings", + identifiers={(DOMAIN, str(sensor_id))}, + name=f"Sensor {sensor_id}", + manufacturer="Luftdaten.info", + ) + if show_on_map: self._attr_extra_state_attributes[ATTR_LONGITUDE] = coordinator.data[ "longitude" diff --git a/tests/components/luftdaten/test_sensor.py b/tests/components/luftdaten/test_sensor.py index ae016615047..46570d6cb69 100644 --- a/tests/components/luftdaten/test_sensor.py +++ b/tests/components/luftdaten/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the sensors provided by the Luftdaten integration.""" +from homeassistant.components.luftdaten.const import DOMAIN from homeassistant.components.sensor import ( ATTR_STATE_CLASS, SensorDeviceClass, @@ -15,7 +16,7 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.common import MockConfigEntry @@ -26,10 +27,11 @@ async def test_luftdaten_sensors( ) -> None: """Test the Luftdaten sensors.""" entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) entry = entity_registry.async_get("sensor.temperature") assert entry - assert not entry.device_id + assert entry.device_id assert entry.unique_id == "12345_temperature" state = hass.states.get("sensor.temperature") @@ -43,7 +45,7 @@ async def test_luftdaten_sensors( entry = entity_registry.async_get("sensor.humidity") assert entry - assert not entry.device_id + assert entry.device_id assert entry.unique_id == "12345_humidity" state = hass.states.get("sensor.humidity") @@ -57,7 +59,7 @@ async def test_luftdaten_sensors( entry = entity_registry.async_get("sensor.pressure") assert entry - assert not entry.device_id + assert entry.device_id assert entry.unique_id == "12345_pressure" state = hass.states.get("sensor.pressure") @@ -71,7 +73,7 @@ async def test_luftdaten_sensors( entry = entity_registry.async_get("sensor.pressure_at_sealevel") assert entry - assert not entry.device_id + assert entry.device_id assert entry.unique_id == "12345_pressure_at_sealevel" state = hass.states.get("sensor.pressure_at_sealevel") @@ -85,7 +87,7 @@ async def test_luftdaten_sensors( entry = entity_registry.async_get("sensor.pm10") assert entry - assert not entry.device_id + assert entry.device_id assert entry.unique_id == "12345_P1" state = hass.states.get("sensor.pm10") @@ -102,7 +104,7 @@ async def test_luftdaten_sensors( entry = entity_registry.async_get("sensor.pm2_5") assert entry - assert not entry.device_id + assert entry.device_id assert entry.unique_id == "12345_P2" state = hass.states.get("sensor.pm2_5") @@ -116,3 +118,14 @@ async def test_luftdaten_sensors( == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) assert ATTR_ICON not in state.attributes + + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.identifiers == {(DOMAIN, "12345")} + assert device_entry.manufacturer == "Luftdaten.info" + assert device_entry.name == "Sensor 12345" + assert ( + device_entry.configuration_url + == "https://devices.sensor.community/sensors/12345/settings" + ) From cc92aa557ad0e56a8c01c10203c7fcd4bcf4b010 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 26 Dec 2021 00:15:41 +0000 Subject: [PATCH 1032/2644] [ci skip] Translation update --- .../accuweather/translations/zh-Hant.json | 8 +++--- .../aemet/translations/zh-Hant.json | 6 ++--- .../airly/translations/zh-Hant.json | 6 ++--- .../airnow/translations/zh-Hant.json | 4 +-- .../airthings/translations/zh-Hant.json | 2 +- .../airvisual/translations/zh-Hant.json | 8 +++--- .../ambee/translations/zh-Hant.json | 6 ++--- .../amberelectric/translations/zh-Hant.json | 2 +- .../ambient_station/translations/zh-Hant.json | 6 ++--- .../components/androidtv/translations/tr.json | 10 +++---- .../androidtv/translations/zh-Hant.json | 16 ++++++------ .../asuswrt/translations/zh-Hant.json | 8 +++--- .../azure_devops/translations/zh-Hant.json | 6 ++--- .../azure_event_hub/translations/zh-Hant.json | 2 +- .../components/braviatv/translations/da.json | 12 +++++++++ .../climacell/translations/zh-Hant.json | 4 +-- .../coinbase/translations/zh-Hant.json | 10 ++++--- .../daikin/translations/zh-Hant.json | 6 ++--- .../components/deconz/translations/da.json | 8 ++++++ .../ecobee/translations/zh-Hant.json | 10 +++---- .../efergy/translations/zh-Hant.json | 2 +- .../components/esphome/translations/it.json | 6 ++--- .../esphome/translations/zh-Hant.json | 10 +++---- .../flick_electric/translations/zh-Hant.json | 2 +- .../components/flume/translations/da.json | 11 ++++++++ .../flume/translations/zh-Hant.json | 4 +-- .../forecast_solar/translations/zh-Hant.json | 2 +- .../freedompro/translations/zh-Hant.json | 6 ++--- .../translations/zh-Hant.json | 2 +- .../habitica/translations/zh-Hant.json | 2 +- .../components/hassio/translations/it.json | 4 +-- .../homeassistant/translations/it.json | 2 +- .../homekit_controller/translations/it.json | 4 +-- .../components/lcn/translations/zh-Hant.json | 2 +- .../metoffice/translations/zh-Hant.json | 2 +- .../modern_forms/translations/tr.json | 2 +- .../motion_blinds/translations/zh-Hant.json | 6 ++--- .../components/mysensors/translations/tr.json | 2 +- .../nightscout/translations/zh-Hant.json | 4 +-- .../components/nws/translations/zh-Hant.json | 4 +-- .../octoprint/translations/zh-Hant.json | 2 +- .../opengarage/translations/zh-Hant.json | 2 +- .../openuv/translations/zh-Hant.json | 4 +-- .../openweathermap/translations/zh-Hant.json | 6 ++--- .../components/overkiz/translations/id.json | 22 ++++++++++++++++ .../components/overkiz/translations/tr.json | 25 ++++++++++++++++++ .../overkiz/translations/zh-Hant.json | 25 ++++++++++++++++++ .../components/ozw/translations/zh-Hant.json | 2 +- .../panasonic_viera/translations/da.json | 23 ++++++++++++++++ .../pi_hole/translations/zh-Hant.json | 4 +-- .../rachio/translations/zh-Hant.json | 4 +-- .../components/roomba/translations/da.json | 12 +++++++++ .../season/translations/sensor.da.json | 6 +++++ .../components/sensibo/translations/id.json | 18 +++++++++++++ .../sensibo/translations/zh-Hant.json | 2 +- .../sensor/translations/zh-Hant.json | 4 +++ .../components/sia/translations/zh-Hant.json | 6 ++--- .../solaredge/translations/zh-Hant.json | 4 +-- .../sonarr/translations/zh-Hant.json | 2 +- .../starline/translations/zh-Hant.json | 6 ++--- .../synology_dsm/translations/da.json | 5 ++++ .../system_bridge/translations/tr.json | 2 +- .../system_bridge/translations/zh-Hant.json | 6 ++--- .../tailscale/translations/zh-Hant.json | 8 +++--- .../translations/zh-Hant.json | 2 +- .../components/tuya/translations/zh-Hant.json | 4 +-- .../uptimerobot/translations/zh-Hant.json | 12 ++++----- .../components/version/translations/id.json | 26 +++++++++++++++++++ .../components/version/translations/tr.json | 26 +++++++++++++++++++ .../version/translations/zh-Hant.json | 26 +++++++++++++++++++ .../vicare/translations/zh-Hant.json | 4 +-- .../xiaomi_aqara/translations/zh-Hant.json | 6 ++--- .../xiaomi_miio/translations/zh-Hant.json | 2 +- .../zwave/translations/zh-Hant.json | 2 +- .../components/zwave_js/translations/it.json | 10 +++---- .../zwave_js/translations/zh-Hant.json | 24 ++++++++--------- 76 files changed, 407 insertions(+), 156 deletions(-) create mode 100644 homeassistant/components/braviatv/translations/da.json create mode 100644 homeassistant/components/flume/translations/da.json create mode 100644 homeassistant/components/overkiz/translations/id.json create mode 100644 homeassistant/components/overkiz/translations/tr.json create mode 100644 homeassistant/components/overkiz/translations/zh-Hant.json create mode 100644 homeassistant/components/panasonic_viera/translations/da.json create mode 100644 homeassistant/components/roomba/translations/da.json create mode 100644 homeassistant/components/sensibo/translations/id.json create mode 100644 homeassistant/components/version/translations/id.json create mode 100644 homeassistant/components/version/translations/tr.json create mode 100644 homeassistant/components/version/translations/zh-Hant.json diff --git a/homeassistant/components/accuweather/translations/zh-Hant.json b/homeassistant/components/accuweather/translations/zh-Hant.json index 4ebb296d5d3..11df415d4c9 100644 --- a/homeassistant/components/accuweather/translations/zh-Hant.json +++ b/homeassistant/components/accuweather/translations/zh-Hant.json @@ -5,13 +5,13 @@ }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", - "invalid_api_key": "API \u5bc6\u9470\u7121\u6548", - "requests_exceeded": "\u5df2\u8d85\u904e Accuweather API \u5141\u8a31\u7684\u8acb\u6c42\u6b21\u6578\u3002\u5fc5\u9808\u7b49\u5019\u6216\u8b8a\u66f4 API \u5bc6\u9470\u3002" + "invalid_api_key": "API \u91d1\u9470\u7121\u6548", + "requests_exceeded": "\u5df2\u8d85\u904e Accuweather API \u5141\u8a31\u7684\u8acb\u6c42\u6b21\u6578\u3002\u5fc5\u9808\u7b49\u5019\u6216\u8b8a\u66f4 API \u91d1\u9470\u3002" }, "step": { "user": { "data": { - "api_key": "API \u5bc6\u9470", + "api_key": "API \u91d1\u9470", "latitude": "\u7def\u5ea6", "longitude": "\u7d93\u5ea6", "name": "\u540d\u7a31" @@ -27,7 +27,7 @@ "data": { "forecast": "\u5929\u6c23\u9810\u5831" }, - "description": "\u7531\u65bc AccuWeather API \u5bc6\u9470\u514d\u8cbb\u7248\u672c\u9650\u5236\uff0c\u7576\u958b\u555f\u5929\u6c23\u9810\u5831\u6642\u3001\u6578\u64da\u6703\u6bcf 80 \u5206\u9418\u66f4\u65b0\u4e00\u6b21\uff0c\u800c\u975e 40 \u5206\u9418\u3002", + "description": "\u7531\u65bc AccuWeather API \u91d1\u9470\u514d\u8cbb\u7248\u672c\u9650\u5236\uff0c\u7576\u958b\u555f\u5929\u6c23\u9810\u5831\u6642\u3001\u6578\u64da\u6703\u6bcf 80 \u5206\u9418\u66f4\u65b0\u4e00\u6b21\uff0c\u800c\u975e 40 \u5206\u9418\u3002", "title": "AccuWeather \u9078\u9805" } } diff --git a/homeassistant/components/aemet/translations/zh-Hant.json b/homeassistant/components/aemet/translations/zh-Hant.json index e2b1eef10b9..e064a6c0192 100644 --- a/homeassistant/components/aemet/translations/zh-Hant.json +++ b/homeassistant/components/aemet/translations/zh-Hant.json @@ -4,17 +4,17 @@ "already_configured": "\u5ea7\u6a19\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { - "invalid_api_key": "API \u5bc6\u9470\u7121\u6548" + "invalid_api_key": "API \u91d1\u9470\u7121\u6548" }, "step": { "user": { "data": { - "api_key": "API \u5bc6\u9470", + "api_key": "API \u91d1\u9470", "latitude": "\u7def\u5ea6", "longitude": "\u7d93\u5ea6", "name": "\u6574\u5408\u540d\u7a31" }, - "description": "\u6b32\u8a2d\u5b9a AEMET OpenData \u6574\u5408\u3002\u8acb\u81f3 https://opendata.aemet.es/centrodedescargas/altaUsuario \u7522\u751f API \u5bc6\u9470", + "description": "\u6b32\u8a2d\u5b9a AEMET OpenData \u6574\u5408\u3002\u8acb\u81f3 https://opendata.aemet.es/centrodedescargas/altaUsuario \u7522\u751f API \u91d1\u9470", "title": "AEMET OpenData" } } diff --git a/homeassistant/components/airly/translations/zh-Hant.json b/homeassistant/components/airly/translations/zh-Hant.json index 19ef2ae7532..e289bc7cd50 100644 --- a/homeassistant/components/airly/translations/zh-Hant.json +++ b/homeassistant/components/airly/translations/zh-Hant.json @@ -4,18 +4,18 @@ "already_configured": "\u5ea7\u6a19\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { - "invalid_api_key": "API \u5bc6\u9470\u7121\u6548", + "invalid_api_key": "API \u91d1\u9470\u7121\u6548", "wrong_location": "\u8a72\u5340\u57df\u6c92\u6709 Arily \u76e3\u6e2c\u7ad9\u3002" }, "step": { "user": { "data": { - "api_key": "API \u5bc6\u9470", + "api_key": "API \u91d1\u9470", "latitude": "\u7def\u5ea6", "longitude": "\u7d93\u5ea6", "name": "\u540d\u7a31" }, - "description": "\u6b32\u8a2d\u5b9a Airly \u7a7a\u6c23\u54c1\u8cea\u6574\u5408\u3002\u8acb\u81f3 https://developer.airly.eu/register \u7522\u751f API \u5bc6\u9470", + "description": "\u6b32\u8a2d\u5b9a Airly \u7a7a\u6c23\u54c1\u8cea\u6574\u5408\u3002\u8acb\u81f3 https://developer.airly.eu/register \u7522\u751f API \u91d1\u9470", "title": "Airly" } } diff --git a/homeassistant/components/airnow/translations/zh-Hant.json b/homeassistant/components/airnow/translations/zh-Hant.json index 0cdb4a11bed..08d7aab5878 100644 --- a/homeassistant/components/airnow/translations/zh-Hant.json +++ b/homeassistant/components/airnow/translations/zh-Hant.json @@ -12,12 +12,12 @@ "step": { "user": { "data": { - "api_key": "API \u5bc6\u9470", + "api_key": "API \u91d1\u9470", "latitude": "\u7def\u5ea6", "longitude": "\u7d93\u5ea6", "radius": "\u89c0\u6e2c\u7ad9\u534a\u5f91\uff08\u82f1\u91cc\uff1b\u9078\u9805\uff09" }, - "description": "\u6b32\u8a2d\u5b9a AirNow \u7a7a\u6c23\u54c1\u8cea\u6574\u5408\u3002\u8acb\u81f3 https://docs.airnowapi.org/account/request/ \u7522\u751f API \u5bc6\u9470", + "description": "\u6b32\u8a2d\u5b9a AirNow \u7a7a\u6c23\u54c1\u8cea\u6574\u5408\u3002\u8acb\u81f3 https://docs.airnowapi.org/account/request/ \u7522\u751f API \u91d1\u9470", "title": "AirNow" } } diff --git a/homeassistant/components/airthings/translations/zh-Hant.json b/homeassistant/components/airthings/translations/zh-Hant.json index 0cafeb9886d..6317b5903a9 100644 --- a/homeassistant/components/airthings/translations/zh-Hant.json +++ b/homeassistant/components/airthings/translations/zh-Hant.json @@ -13,7 +13,7 @@ "data": { "description": "\u767b\u5165 {url} \u4ee5\u53d6\u5f97\u6191\u8b49", "id": "ID", - "secret": "\u5bc6\u78bc" + "secret": "\u79c1\u9470" } } } diff --git a/homeassistant/components/airvisual/translations/zh-Hant.json b/homeassistant/components/airvisual/translations/zh-Hant.json index 172f57de938..fed34b3346b 100644 --- a/homeassistant/components/airvisual/translations/zh-Hant.json +++ b/homeassistant/components/airvisual/translations/zh-Hant.json @@ -7,13 +7,13 @@ "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "general_error": "\u672a\u9810\u671f\u932f\u8aa4", - "invalid_api_key": "API \u5bc6\u9470\u7121\u6548", + "invalid_api_key": "API \u91d1\u9470\u7121\u6548", "location_not_found": "\u627e\u4e0d\u5230\u5730\u9ede" }, "step": { "geography_by_coords": { "data": { - "api_key": "API \u5bc6\u9470", + "api_key": "API \u91d1\u9470", "latitude": "\u7def\u5ea6", "longitude": "\u7d93\u5ea6" }, @@ -22,7 +22,7 @@ }, "geography_by_name": { "data": { - "api_key": "API \u5bc6\u9470", + "api_key": "API \u91d1\u9470", "city": "\u57ce\u5e02", "country": "\u570b\u5bb6", "state": "\u5dde" @@ -40,7 +40,7 @@ }, "reauth_confirm": { "data": { - "api_key": "API \u5bc6\u9470" + "api_key": "API \u91d1\u9470" }, "title": "\u91cd\u65b0\u8a8d\u8b49 AirVisual" }, diff --git a/homeassistant/components/ambee/translations/zh-Hant.json b/homeassistant/components/ambee/translations/zh-Hant.json index d2b53af8e5e..2e1de25fde2 100644 --- a/homeassistant/components/ambee/translations/zh-Hant.json +++ b/homeassistant/components/ambee/translations/zh-Hant.json @@ -5,18 +5,18 @@ }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", - "invalid_api_key": "API \u5bc6\u9470\u7121\u6548" + "invalid_api_key": "API \u91d1\u9470\u7121\u6548" }, "step": { "reauth_confirm": { "data": { - "api_key": "API \u5bc6\u9470", + "api_key": "API \u91d1\u9470", "description": "\u91cd\u65b0\u8a8d\u8b49 Ambee \u5e33\u865f\u3002" } }, "user": { "data": { - "api_key": "API \u5bc6\u9470", + "api_key": "API \u91d1\u9470", "latitude": "\u7def\u5ea6", "longitude": "\u7d93\u5ea6", "name": "\u540d\u7a31" diff --git a/homeassistant/components/amberelectric/translations/zh-Hant.json b/homeassistant/components/amberelectric/translations/zh-Hant.json index 0af0e5e60bb..c2cbef2778b 100644 --- a/homeassistant/components/amberelectric/translations/zh-Hant.json +++ b/homeassistant/components/amberelectric/translations/zh-Hant.json @@ -14,7 +14,7 @@ "api_token": "API \u6b0a\u6756", "site_id": "\u4f4d\u5740 ID" }, - "description": "\u9023\u7dda\u81f3 {api_url} \u4ee5\u7522\u751f API \u5bc6\u9470", + "description": "\u9023\u7dda\u81f3 {api_url} \u4ee5\u7522\u751f API \u91d1\u9470", "title": "Amber Electric" } } diff --git a/homeassistant/components/ambient_station/translations/zh-Hant.json b/homeassistant/components/ambient_station/translations/zh-Hant.json index dab15def7b4..8b1f528f01c 100644 --- a/homeassistant/components/ambient_station/translations/zh-Hant.json +++ b/homeassistant/components/ambient_station/translations/zh-Hant.json @@ -4,14 +4,14 @@ "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { - "invalid_key": "API \u5bc6\u9470\u7121\u6548", + "invalid_key": "API \u91d1\u9470\u7121\u6548", "no_devices": "\u5e33\u865f\u4e2d\u627e\u4e0d\u5230\u4efb\u4f55\u88dd\u7f6e" }, "step": { "user": { "data": { - "api_key": "API \u5bc6\u9470", - "app_key": "\u61c9\u7528\u5bc6\u9470" + "api_key": "API \u91d1\u9470", + "app_key": "\u61c9\u7528\u91d1\u9470" }, "title": "\u586b\u5beb\u8cc7\u8a0a" } diff --git a/homeassistant/components/androidtv/translations/tr.json b/homeassistant/components/androidtv/translations/tr.json index f139082ce36..bb77c35ed1b 100644 --- a/homeassistant/components/androidtv/translations/tr.json +++ b/homeassistant/components/androidtv/translations/tr.json @@ -43,12 +43,12 @@ "init": { "data": { "apps": "Uygulamalar listesini yap\u0131land\u0131r", - "exclude_unnamed_apps": "Bilinmeyen ada sahip uygulamay\u0131 hari\u00e7 tut", - "get_sources": "\u00c7al\u0131\u015fan uygulamalar\u0131n kaynak listesi olarak al\u0131n\u0131p al\u0131nmayaca\u011f\u0131", - "screencap": "Alb\u00fcm resminin ekranda g\u00f6sterilenden \u00e7ekilmesi gerekip gerekmedi\u011fini belirler", + "exclude_unnamed_apps": "Kaynak listesinden bilinmeyen ada sahip uygulamalar\u0131 hari\u00e7 tutun", + "get_sources": "\u00c7al\u0131\u015fan uygulamalar\u0131 kaynak listesi olarak al\u0131n", + "screencap": "Alb\u00fcm resmi i\u00e7in ekran g\u00f6r\u00fcnt\u00fcs\u00fcn\u00fc kullan\u0131n", "state_detection_rules": "Durum alg\u0131lama kurallar\u0131n\u0131 yap\u0131land\u0131r\u0131n", - "turn_off_command": "Varsay\u0131lan turn_off komutunu ge\u00e7ersiz k\u0131lmak i\u00e7in ADB kabuk komutu", - "turn_on_command": "Varsay\u0131lan turn_on komutunu ge\u00e7ersiz k\u0131lmak i\u00e7in ADB kabuk komutu" + "turn_off_command": "ADB kabu\u011fu kapatma komutu (varsay\u0131lan olarak bo\u015f b\u0131rak\u0131n)", + "turn_on_command": "ADB kabu\u011fu a\u00e7ma komutu (varsay\u0131lan olarak bo\u015f b\u0131rak\u0131n)" }, "title": "Android TV Se\u00e7enekleri" }, diff --git a/homeassistant/components/androidtv/translations/zh-Hant.json b/homeassistant/components/androidtv/translations/zh-Hant.json index 7e113048a0c..6f6e6fd8180 100644 --- a/homeassistant/components/androidtv/translations/zh-Hant.json +++ b/homeassistant/components/androidtv/translations/zh-Hant.json @@ -5,10 +5,10 @@ "invalid_unique_id": "\u7121\u6cd5\u78ba\u8a8d\u88dd\u7f6e\u6709\u6548\u552f\u4e00 ID" }, "error": { - "adbkey_not_file": "\u627e\u4e0d\u5230 ADB \u5bc6\u9470\u6a94\u6848", + "adbkey_not_file": "\u627e\u4e0d\u5230 ADB \u91d1\u9470\u6a94\u6848", "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_host": "\u7121\u6548\u4e3b\u6a5f\u540d\u7a31\u6216 IP \u4f4d\u5740", - "key_and_server": "\u50c5\u63d0\u4f9b ADB \u5bc6\u9470\u6216 ADB \u4f3a\u670d\u5668", + "key_and_server": "\u50c5\u63d0\u4f9b ADB \u91d1\u9470\u6216 ADB \u4f3a\u670d\u5668", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { @@ -16,7 +16,7 @@ "data": { "adb_server_ip": "ADB \u4f3a\u670d\u5668 IP \u4f4d\u5740\uff08\u4fdd\u7559\u7a7a\u767d\u70ba\u4e0d\u4f7f\u7528\uff09", "adb_server_port": "ADB \u4f3a\u670d\u5668\u901a\u8a0a\u57e0", - "adbkey": "ADB \u5bc6\u9470\u6a94\u6848\u8def\u5f91\uff08\u4fdd\u7559\u7a7a\u767d\u5c07\u6703\u81ea\u52d5\u7522\u751f\uff09", + "adbkey": "ADB \u91d1\u9470\u6a94\u6848\u8def\u5f91\uff08\u4fdd\u7559\u7a7a\u767d\u5c07\u6703\u81ea\u52d5\u7522\u751f\uff09", "device_class": "\u88dd\u7f6e\u985e\u578b", "host": "\u4e3b\u6a5f\u7aef", "port": "\u901a\u8a0a\u57e0" @@ -43,12 +43,12 @@ "init": { "data": { "apps": "\u8a2d\u5b9a\u61c9\u7528\u7a0b\u5f0f\u5217\u8868", - "exclude_unnamed_apps": "\u6392\u9664\u672a\u77e5\u540d\u7a31\u61c9\u7528\u7a0b\u5f0f", - "get_sources": "\u662f\u5426\u7531\u4f86\u6e90\u5217\u8868\u53d6\u5f97\u57f7\u884c\u7a0b\u5f0f\u5217\u8868", - "screencap": "\u6c7a\u5b9a\u662f\u5426\u7531\u756b\u9762\u4e0a\u986f\u793a\u5167\u5bb9\u64f7\u53d6\u5c08\u8f2f\u5c01\u9762", + "exclude_unnamed_apps": "\u7531\u4f86\u6e90\u5217\u8868\u6392\u9664\u672a\u77e5\u540d\u7a31\u61c9\u7528\u7a0b\u5f0f", + "get_sources": "\u7531\u4f86\u6e90\u5217\u8868\u53d6\u5f97\u57f7\u884c\u7a0b\u5f0f\u5217\u8868", + "screencap": "\u4f7f\u7528\u756b\u9762\u64f7\u53d6\u4f5c\u70ba\u5c01\u9762", "state_detection_rules": "\u8a2d\u5b9a\u72c0\u614b\u5075\u6e2c\u898f\u5247", - "turn_off_command": "ADB shell \u6307\u4ee4\u4ee5\u8986\u5beb\u9810\u8a2d turn_off \u6307\u4ee4", - "turn_on_command": "ADB shell \u6307\u4ee4\u4ee5\u8986\u5beb\u9810\u8a2d turn_on \u6307\u4ee4" + "turn_off_command": "ADB shell turn off \u6307\u4ee4\uff08\u9810\u8a2d\u7a7a\u767d\uff09", + "turn_on_command": "ADB shell turn on \u6307\u4ee4\uff08\u9810\u8a2d\u7a7a\u767d\uff09" }, "title": "Android TV \u9078\u9805" }, diff --git a/homeassistant/components/asuswrt/translations/zh-Hant.json b/homeassistant/components/asuswrt/translations/zh-Hant.json index 8caddacd23e..7aabf592ee3 100644 --- a/homeassistant/components/asuswrt/translations/zh-Hant.json +++ b/homeassistant/components/asuswrt/translations/zh-Hant.json @@ -6,9 +6,9 @@ "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_host": "\u7121\u6548\u4e3b\u6a5f\u540d\u7a31\u6216 IP \u4f4d\u5740", - "pwd_and_ssh": "\u50c5\u63d0\u4f9b\u5bc6\u78bc\u6216 SSH \u5bc6\u9470\u6a94\u6848", - "pwd_or_ssh": "\u8acb\u8f38\u5165\u5bc6\u78bc\u6216 SSH \u5bc6\u9470\u6a94\u6848", - "ssh_not_file": "\u627e\u4e0d\u5230 SSH \u5bc6\u9470\u6a94\u6848", + "pwd_and_ssh": "\u50c5\u63d0\u4f9b\u5bc6\u78bc\u6216 SSH \u91d1\u9470\u6a94\u6848", + "pwd_or_ssh": "\u8acb\u8f38\u5165\u5bc6\u78bc\u6216 SSH \u91d1\u9470\u6a94\u6848", + "ssh_not_file": "\u627e\u4e0d\u5230 SSH \u91d1\u9470\u6a94\u6848", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { @@ -20,7 +20,7 @@ "password": "\u5bc6\u78bc", "port": "\u901a\u8a0a\u57e0", "protocol": "\u4f7f\u7528\u901a\u8a0a\u606f\u5354\u5b9a", - "ssh_key": "SSH \u5bc6\u9470\u6a94\u6848\u8def\u5f91\uff08\u975e\u5bc6\u78bc\uff09", + "ssh_key": "SSH \u91d1\u9470\u6a94\u6848\u8def\u5f91\uff08\u975e\u5bc6\u78bc\uff09", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, "description": "\u8a2d\u5b9a\u6240\u9700\u53c3\u6578\u4ee5\u9023\u7dda\u81f3\u8def\u7531\u5668", diff --git a/homeassistant/components/azure_devops/translations/zh-Hant.json b/homeassistant/components/azure_devops/translations/zh-Hant.json index 13f6fcbe276..58ac777f34a 100644 --- a/homeassistant/components/azure_devops/translations/zh-Hant.json +++ b/homeassistant/components/azure_devops/translations/zh-Hant.json @@ -13,7 +13,7 @@ "step": { "reauth": { "data": { - "personal_access_token": "\u500b\u4eba\u5b58\u53d6\u5bc6\u9470\uff08PAT\uff09" + "personal_access_token": "\u500b\u4eba\u5b58\u53d6\u91d1\u9470\uff08PAT\uff09" }, "description": "{project_url}\u8a8d\u8b49\u5931\u6557\u3002\u8acb\u8f38\u5165\u76ee\u524d\u8b49\u66f8\u3002", "title": "\u91cd\u65b0\u8a8d\u8b49" @@ -21,10 +21,10 @@ "user": { "data": { "organization": "\u7d44\u7e54", - "personal_access_token": "\u500b\u4eba\u5b58\u53d6\u5bc6\u9470\uff08PAT\uff09", + "personal_access_token": "\u500b\u4eba\u5b58\u53d6\u91d1\u9470\uff08PAT\uff09", "project": "\u5c08\u6848" }, - "description": "\u8a2d\u5b9a Azure DevOps \u4ee5\u5b58\u53d6\u5c08\u6848\u3002\u79c1\u4eba\u5c08\u6848\u5247\u9700\u8981\u8f38\u5165\u300c\u500b\u4eba\u5b58\u53d6\u5bc6\u9470\uff09\u3002", + "description": "\u8a2d\u5b9a Azure DevOps \u4ee5\u5b58\u53d6\u5c08\u6848\u3002\u79c1\u4eba\u5c08\u6848\u5247\u9700\u8981\u8f38\u5165\u300c\u500b\u4eba\u5b58\u53d6\u91d1\u9470\uff09\u3002", "title": "\u65b0\u589e Azure DevOps \u5c08\u6848" } } diff --git a/homeassistant/components/azure_event_hub/translations/zh-Hant.json b/homeassistant/components/azure_event_hub/translations/zh-Hant.json index fbc84038703..64f713f5bd8 100644 --- a/homeassistant/components/azure_event_hub/translations/zh-Hant.json +++ b/homeassistant/components/azure_event_hub/translations/zh-Hant.json @@ -21,7 +21,7 @@ "sas": { "data": { "event_hub_namespace": "\u4e8b\u4ef6\u4e2d\u6a1e Namespace", - "event_hub_sas_key": "\u4e8b\u4ef6\u4e2d\u6a1e SAS \u5bc6\u9470", + "event_hub_sas_key": "\u4e8b\u4ef6\u4e2d\u6a1e SAS \u91d1\u9470", "event_hub_sas_policy": "\u4e8b\u4ef6\u4e2d\u6a1e SAS \u96b1\u79c1\u653f\u7b56" }, "description": "\u8acb\u8f38\u5165 SAS \uff08\u5171\u7528\u5b58\u53d6\u7c3d\u7ae0\uff09\u6191\u8b49\uff1a{event_hub_instance_name}", diff --git a/homeassistant/components/braviatv/translations/da.json b/homeassistant/components/braviatv/translations/da.json new file mode 100644 index 00000000000..006d6b708e5 --- /dev/null +++ b/homeassistant/components/braviatv/translations/da.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "authorize": { + "title": "Godkend Sony Bravia TV" + }, + "user": { + "title": "Sony Bravia TV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/zh-Hant.json b/homeassistant/components/climacell/translations/zh-Hant.json index 64b8e90b6ea..5ef7396b0e5 100644 --- a/homeassistant/components/climacell/translations/zh-Hant.json +++ b/homeassistant/components/climacell/translations/zh-Hant.json @@ -2,14 +2,14 @@ "config": { "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", - "invalid_api_key": "API \u5bc6\u9470\u7121\u6548", + "invalid_api_key": "API \u91d1\u9470\u7121\u6548", "rate_limited": "\u9054\u5230\u9650\u5236\u983b\u7387\u3001\u8acb\u7a0d\u5019\u518d\u8a66\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { "user": { "data": { - "api_key": "API \u5bc6\u9470", + "api_key": "API \u91d1\u9470", "api_version": "API \u7248\u672c", "latitude": "\u7def\u5ea6", "longitude": "\u7d93\u5ea6", diff --git a/homeassistant/components/coinbase/translations/zh-Hant.json b/homeassistant/components/coinbase/translations/zh-Hant.json index e6c92f1cb75..03b9333fef1 100644 --- a/homeassistant/components/coinbase/translations/zh-Hant.json +++ b/homeassistant/components/coinbase/translations/zh-Hant.json @@ -6,18 +6,20 @@ "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "invalid_auth_key": "API \u91d1\u9470\u7121\u6548\u3001Coinbase \u62d2\u7d55\u6191\u8b49\u3002", + "invalid_auth_secret": "API \u79c1\u9470\u7121\u6548\u3001Coinbase \u62d2\u7d55\u6191\u8b49\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { "user": { "data": { - "api_key": "API \u5bc6\u9470", - "api_token": "API \u5bc6\u9470", + "api_key": "API \u91d1\u9470", + "api_token": "API \u79c1\u9470", "currencies": "\u5e33\u6236\u9918\u984d\u8ca8\u5e63", "exchange_rates": "\u532f\u7387" }, - "description": "\u8acb\u8f38\u5165\u7531 Coinbase \u63d0\u4f9b\u7684 API \u5bc6\u9470\u8cc7\u8a0a\u3002", - "title": "Coinbase API \u5bc6\u9470\u8cc7\u6599" + "description": "\u8acb\u8f38\u5165\u7531 Coinbase \u63d0\u4f9b\u7684 API \u91d1\u9470\u8cc7\u8a0a\u3002", + "title": "Coinbase API \u91d1\u9470\u8cc7\u6599" } } }, diff --git a/homeassistant/components/daikin/translations/zh-Hant.json b/homeassistant/components/daikin/translations/zh-Hant.json index 8072d135c20..9128c035921 100644 --- a/homeassistant/components/daikin/translations/zh-Hant.json +++ b/homeassistant/components/daikin/translations/zh-Hant.json @@ -5,7 +5,7 @@ "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "error": { - "api_password": "\u9a57\u8b49\u78bc\u7121\u6548\u3001\u8acb\u4f7f\u7528 API \u5bc6\u9470\u6216\u5bc6\u78bc\u3002", + "api_password": "\u9a57\u8b49\u78bc\u7121\u6548\u3001\u8acb\u4f7f\u7528 API \u91d1\u9470\u6216\u5bc6\u78bc\u3002", "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" @@ -13,11 +13,11 @@ "step": { "user": { "data": { - "api_key": "API \u5bc6\u9470", + "api_key": "API \u91d1\u9470", "host": "\u4e3b\u6a5f\u7aef", "password": "\u5bc6\u78bc" }, - "description": "\u8f38\u5165\u60a8\u7684\u5927\u91d1\u7a7a\u8abfIP \u4f4d\u5740\u3002\n\n\u8acb\u6ce8\u610f\uff1aBRP072Cxx \u8207 SKYFi \u88dd\u7f6e\u4e4b API \u5bc6\u9470\u8207\u5bc6\u78bc\u70ba\u5206\u958b\u4f7f\u7528\u3002", + "description": "\u8f38\u5165\u60a8\u7684\u5927\u91d1\u7a7a\u8abfIP \u4f4d\u5740\u3002\n\n\u8acb\u6ce8\u610f\uff1aBRP072Cxx \u8207 SKYFi \u88dd\u7f6e\u4e4b API \u91d1\u9470\u8207\u5bc6\u78bc\u70ba\u5206\u958b\u4f7f\u7528\u3002", "title": "\u8a2d\u5b9a\u5927\u91d1\u7a7a\u8abf" } } diff --git a/homeassistant/components/deconz/translations/da.json b/homeassistant/components/deconz/translations/da.json index 00e054aecc9..6f63540c924 100644 --- a/homeassistant/components/deconz/translations/da.json +++ b/homeassistant/components/deconz/translations/da.json @@ -19,12 +19,19 @@ "link": { "description": "L\u00e5s din deCONZ-gateway op for at registrere dig med Home Assistant. \n\n 1. G\u00e5 til deCONZ settings -> Gateway -> Advanced\n 2. Tryk p\u00e5 knappen \"Authenticate app\"", "title": "Forbind med deCONZ" + }, + "manual_input": { + "data": { + "host": "V\u00e6rt", + "port": "Port" + } } } }, "device_automation": { "trigger_subtype": { "both_buttons": "Begge knapper", + "bottom_buttons": "Nederste knapper", "button_1": "F\u00f8rste knap", "button_2": "Anden knap", "button_3": "Tredje knap", @@ -41,6 +48,7 @@ "side_4": "Side 4", "side_5": "Side 5", "side_6": "Side 6", + "top_buttons": "\u00d8verste knapper", "turn_off": "Sluk", "turn_on": "T\u00e6nd" }, diff --git a/homeassistant/components/ecobee/translations/zh-Hant.json b/homeassistant/components/ecobee/translations/zh-Hant.json index e9789c855d0..b4604218206 100644 --- a/homeassistant/components/ecobee/translations/zh-Hant.json +++ b/homeassistant/components/ecobee/translations/zh-Hant.json @@ -4,8 +4,8 @@ "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { - "pin_request_failed": "ecobee \u6240\u9700\u4ee3\u78bc\u932f\u8aa4\uff0c\u8acb\u78ba\u8a8d\u5bc6\u9470\u6b63\u78ba\u6027\u3002", - "token_request_failed": "ecobee \u6240\u9700\u5bc6\u9470\u932f\u8aa4\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002" + "pin_request_failed": "ecobee \u6240\u9700\u4ee3\u78bc\u932f\u8aa4\uff0c\u8acb\u78ba\u8a8d\u91d1\u9470\u6b63\u78ba\u6027\u3002", + "token_request_failed": "ecobee \u6240\u9700\u91d1\u9470\u932f\u8aa4\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002" }, "step": { "authorize": { @@ -14,10 +14,10 @@ }, "user": { "data": { - "api_key": "API \u5bc6\u9470" + "api_key": "API \u91d1\u9470" }, - "description": "\u8acb\u8f38\u5165\u7531 ecobee.com \u6240\u7372\u5f97\u7684 API \u5bc6\u9470\u3002", - "title": "ecobee API \u5bc6\u9470" + "description": "\u8acb\u8f38\u5165\u7531 ecobee.com \u6240\u7372\u5f97\u7684 API \u91d1\u9470\u3002", + "title": "ecobee API \u91d1\u9470" } } } diff --git a/homeassistant/components/efergy/translations/zh-Hant.json b/homeassistant/components/efergy/translations/zh-Hant.json index 7b51c998fb2..c8f2d660c2f 100644 --- a/homeassistant/components/efergy/translations/zh-Hant.json +++ b/homeassistant/components/efergy/translations/zh-Hant.json @@ -12,7 +12,7 @@ "step": { "user": { "data": { - "api_key": "API \u5bc6\u9470" + "api_key": "API \u91d1\u9470" }, "title": "Efergy" } diff --git a/homeassistant/components/esphome/translations/it.json b/homeassistant/components/esphome/translations/it.json index a65b1e8a033..62c75238378 100644 --- a/homeassistant/components/esphome/translations/it.json +++ b/homeassistant/components/esphome/translations/it.json @@ -8,7 +8,7 @@ "error": { "connection_error": "Impossibile connettersi a ESP. Assicurati che il tuo file YAML contenga una riga \"api:\".", "invalid_auth": "Autenticazione non valida", - "invalid_psk": "La chiave di crittografia del trasporto non \u00e8 valida. Assicurati che corrisponda a ci\u00f2 che hai nella tua configurazione", + "invalid_psk": "La chiave di cifratura del trasporto non \u00e8 valida. Assicurati che corrisponda a ci\u00f2 che hai nella tua configurazione", "resolve_error": "Impossibile risolvere l'indirizzo dell'ESP. Se questo errore persiste, imposta un indirizzo IP statico: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, "flow_title": "{name}", @@ -25,9 +25,9 @@ }, "encryption_key": { "data": { - "noise_psk": "Chiave di crittografia" + "noise_psk": "Chiave di cifratura" }, - "description": "Inserisci la chiave di crittografia che hai impostato nella configurazione per {name}." + "description": "Inserisci la chiave di cifratura che hai impostato nella configurazione per {name}." }, "reauth_confirm": { "data": { diff --git a/homeassistant/components/esphome/translations/zh-Hant.json b/homeassistant/components/esphome/translations/zh-Hant.json index 0b415a35c38..976d0317faa 100644 --- a/homeassistant/components/esphome/translations/zh-Hant.json +++ b/homeassistant/components/esphome/translations/zh-Hant.json @@ -8,7 +8,7 @@ "error": { "connection_error": "\u7121\u6cd5\u9023\u7dda\u81f3 ESP\uff0c\u8acb\u78ba\u5b9a\u60a8\u7684 YAML \u6a94\u6848\u5305\u542b\u300capi:\u300d\u8a2d\u5b9a\u5217\u3002", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", - "invalid_psk": "\u50b3\u8f38\u5bc6\u9470\u7121\u6548\u3002\u8acb\u78ba\u5b9a\u8207\u8a2d\u5b9a\u5167\u6240\u8a2d\u5b9a\u4e4b\u5bc6\u9470\u76f8\u7b26\u5408", + "invalid_psk": "\u50b3\u8f38\u91d1\u9470\u7121\u6548\u3002\u8acb\u78ba\u5b9a\u8207\u8a2d\u5b9a\u5167\u6240\u8a2d\u5b9a\u4e4b\u91d1\u9470\u76f8\u7b26\u5408", "resolve_error": "\u7121\u6cd5\u89e3\u6790 ESP \u4f4d\u5740\uff0c\u5047\u5982\u6b64\u932f\u8aa4\u6301\u7e8c\u767c\u751f\uff0c\u8acb\u53c3\u8003\u8aaa\u660e\u8a2d\u5b9a\u70ba\u975c\u614b\u56fa\u5b9a IP \uff1a https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, "flow_title": "{name}", @@ -25,15 +25,15 @@ }, "encryption_key": { "data": { - "noise_psk": "\u5bc6\u9470" + "noise_psk": "\u91d1\u9470" }, - "description": "\u8acb\u8f38\u5165 {name} \u8a2d\u5b9a\u4e2d\u6240\u8a2d\u5b9a\u4e4b\u5bc6\u9470\u3002" + "description": "\u8acb\u8f38\u5165 {name} \u8a2d\u5b9a\u4e2d\u6240\u8a2d\u5b9a\u4e4b\u91d1\u9470\u3002" }, "reauth_confirm": { "data": { - "noise_psk": "\u5bc6\u9470" + "noise_psk": "\u91d1\u9470" }, - "description": "ESPHome \u88dd\u7f6e {name} \u5df2\u958b\u555f\u50b3\u8f38\u52a0\u5bc6\u6216\u8b8a\u66f4\u5bc6\u9470\u3002\u8acb\u8f38\u5165\u66f4\u65b0\u5bc6\u9470\u3002" + "description": "ESPHome \u88dd\u7f6e {name} \u5df2\u958b\u555f\u50b3\u8f38\u52a0\u5bc6\u6216\u8b8a\u66f4\u91d1\u9470\u3002\u8acb\u8f38\u5165\u66f4\u65b0\u91d1\u9470\u3002" }, "user": { "data": { diff --git a/homeassistant/components/flick_electric/translations/zh-Hant.json b/homeassistant/components/flick_electric/translations/zh-Hant.json index 3df68984ec5..10e76956fd6 100644 --- a/homeassistant/components/flick_electric/translations/zh-Hant.json +++ b/homeassistant/components/flick_electric/translations/zh-Hant.json @@ -12,7 +12,7 @@ "user": { "data": { "client_id": "\u5ba2\u6236\u7aef ID\uff08\u9078\u9805\uff09", - "client_secret": "\u5ba2\u6236\u7aef\u5bc6\u9470\uff08\u9078\u9805\uff09", + "client_secret": "\u5ba2\u6236\u7aef\u79c1\u9470\uff08\u9078\u9805\uff09", "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, diff --git a/homeassistant/components/flume/translations/da.json b/homeassistant/components/flume/translations/da.json new file mode 100644 index 00000000000..7155813152d --- /dev/null +++ b/homeassistant/components/flume/translations/da.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "client_id": "Klient-ID" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flume/translations/zh-Hant.json b/homeassistant/components/flume/translations/zh-Hant.json index 9aae3792609..6e5a3e540bb 100644 --- a/homeassistant/components/flume/translations/zh-Hant.json +++ b/homeassistant/components/flume/translations/zh-Hant.json @@ -20,11 +20,11 @@ "user": { "data": { "client_id": "\u5ba2\u6236\u7aef ID", - "client_secret": "\u5ba2\u6236\u7aef\u5bc6\u9470", + "client_secret": "\u5ba2\u6236\u7aef\u79c1\u9470", "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "description": "\u6b32\u5b58\u53d6 Flume \u500b\u4eba API\u3001\u5c07\u9700\u8981\u65bc https://portal.flumetech.com/settings#token \u7372\u5f97\u5ba2\u6236\u7aef ID\uff08Client ID\u300f\u53ca\u5ba2\u6236\u7aef\u5bc6\u9470\uff08Client Secret\uff09", + "description": "\u6b32\u5b58\u53d6 Flume \u500b\u4eba API\u3001\u5c07\u9700\u8981\u65bc https://portal.flumetech.com/settings#token \u7372\u5f97\u5ba2\u6236\u7aef ID\uff08Client ID\u300f\u53ca\u5ba2\u6236\u7aef\u79c1\u9470\uff08Client Secret\uff09", "title": "\u9023\u7dda\u81f3 Flume \u5e33\u865f" } } diff --git a/homeassistant/components/forecast_solar/translations/zh-Hant.json b/homeassistant/components/forecast_solar/translations/zh-Hant.json index 43c7da0f593..fca97b9da01 100644 --- a/homeassistant/components/forecast_solar/translations/zh-Hant.json +++ b/homeassistant/components/forecast_solar/translations/zh-Hant.json @@ -18,7 +18,7 @@ "step": { "init": { "data": { - "api_key": "Forecast.Solar API \u5bc6\u9470\uff08\u9078\u9805\uff09", + "api_key": "Forecast.Solar API \u91d1\u9470\uff08\u9078\u9805\uff09", "azimuth": "\u65b9\u4f4d\u89d2\uff08360 \u5ea6\u55ae\u4f4d\u30020 = \u5317\u300190 = \u6771\u3001180 = \u5357\u3001270 = \u897f\uff09", "damping": "\u963b\u5c3c\u56e0\u7d20\uff1a\u8abf\u6574\u6e05\u6668\u8207\u508d\u665a\u7d50\u679c", "declination": "\u504f\u89d2\uff080 = \u6c34\u5e73\u300190 = \u5782\u76f4\uff09", diff --git a/homeassistant/components/freedompro/translations/zh-Hant.json b/homeassistant/components/freedompro/translations/zh-Hant.json index 2baa8719e2e..b4c901d58e7 100644 --- a/homeassistant/components/freedompro/translations/zh-Hant.json +++ b/homeassistant/components/freedompro/translations/zh-Hant.json @@ -10,10 +10,10 @@ "step": { "user": { "data": { - "api_key": "API \u5bc6\u9470" + "api_key": "API \u91d1\u9470" }, - "description": "\u8acb\u8f38\u5165\u7531 https://home.freedompro.eu \u6240\u7372\u5f97\u7684 API \u5bc6\u9470", - "title": "Freedompro API \u5bc6\u9470" + "description": "\u8acb\u8f38\u5165\u7531 https://home.freedompro.eu \u6240\u7372\u5f97\u7684 API \u91d1\u9470", + "title": "Freedompro API \u91d1\u9470" } } } diff --git a/homeassistant/components/google_travel_time/translations/zh-Hant.json b/homeassistant/components/google_travel_time/translations/zh-Hant.json index cce3cb59131..929810a8564 100644 --- a/homeassistant/components/google_travel_time/translations/zh-Hant.json +++ b/homeassistant/components/google_travel_time/translations/zh-Hant.json @@ -9,7 +9,7 @@ "step": { "user": { "data": { - "api_key": "API \u5bc6\u9470", + "api_key": "API \u91d1\u9470", "destination": "\u76ee\u7684\u5730", "name": "\u540d\u7a31", "origin": "\u51fa\u767c\u5730" diff --git a/homeassistant/components/habitica/translations/zh-Hant.json b/homeassistant/components/habitica/translations/zh-Hant.json index 001682b5c88..65918685161 100644 --- a/homeassistant/components/habitica/translations/zh-Hant.json +++ b/homeassistant/components/habitica/translations/zh-Hant.json @@ -7,7 +7,7 @@ "step": { "user": { "data": { - "api_key": "API \u5bc6\u9470", + "api_key": "API \u91d1\u9470", "api_user": "Habitica \u4e4b API \u4f7f\u7528\u8005 ID", "name": "\u8986\u5beb Habitica \u4f7f\u7528\u8005\u540d\u7a31\u3001\u7528\u4ee5\u670d\u52d9\u547c\u53eb", "url": "\u7db2\u5740" diff --git a/homeassistant/components/hassio/translations/it.json b/homeassistant/components/hassio/translations/it.json index 2fb36ce622d..b601a11241d 100644 --- a/homeassistant/components/hassio/translations/it.json +++ b/homeassistant/components/hassio/translations/it.json @@ -8,8 +8,8 @@ "healthy": "Integrit\u00e0", "host_os": "Sistema operativo dell'host", "installed_addons": "Componenti aggiuntivi installati", - "supervisor_api": "API Supervisor", - "supervisor_version": "Versione Supervisor", + "supervisor_api": "API supervisore", + "supervisor_version": "Versione supervisore", "supported": "Supportato", "update_channel": "Canale di aggiornamento", "version_api": "Versione API" diff --git a/homeassistant/components/homeassistant/translations/it.json b/homeassistant/components/homeassistant/translations/it.json index 0b797a3e84e..b85f3072620 100644 --- a/homeassistant/components/homeassistant/translations/it.json +++ b/homeassistant/components/homeassistant/translations/it.json @@ -4,7 +4,7 @@ "arch": "Architettura della CPU", "dev": "Sviluppo", "docker": "Docker", - "hassio": "Supervisor", + "hassio": "Supervisore", "installation_type": "Tipo di installazione", "os_name": "Famiglia del sistema operativo", "os_version": "Versione del sistema operativo", diff --git a/homeassistant/components/homekit_controller/translations/it.json b/homeassistant/components/homekit_controller/translations/it.json index 7a50e07126e..d95eff05cea 100644 --- a/homeassistant/components/homekit_controller/translations/it.json +++ b/homeassistant/components/homekit_controller/translations/it.json @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "Consenti l'associazione con codici di installazione non sicuri.", "pairing_code": "Codice di abbinamento" }, - "description": "Il controller HomeKit comunica con {name} sulla rete locale utilizzando una connessione crittografata sicura senza un controller HomeKit separato o iCloud. Inserisci il tuo codice di associazione HomeKit (nel formato XXX-XX-XXX) per utilizzare questo accessorio. Questo codice si trova solitamente sul dispositivo stesso o nella confezione.", + "description": "Il controller HomeKit comunica con {name} sulla rete locale utilizzando una connessione cifrata sicura senza un controller HomeKit separato o iCloud. Inserisci il tuo codice di associazione HomeKit (nel formato XXX-XX-XXX) per utilizzare questo accessorio. Questo codice si trova solitamente sul dispositivo stesso o nella confezione.", "title": "Associazione con un dispositivo tramite il Protocollo degli Accessori HomeKit" }, "protocol_error": { @@ -44,7 +44,7 @@ "data": { "device": "Dispositivo" }, - "description": "Il controller HomeKit comunica sulla rete locale utilizzando una connessione crittografata sicura senza un controller HomeKit separato o iCloud. Seleziona il dispositivo che desideri associare:", + "description": "Il controller HomeKit comunica sulla rete locale utilizzando una connessione cifrata sicura senza un controller HomeKit separato o iCloud. Seleziona il dispositivo che desideri associare:", "title": "Selezione del dispositivo" } } diff --git a/homeassistant/components/lcn/translations/zh-Hant.json b/homeassistant/components/lcn/translations/zh-Hant.json index d72235caabe..fe80da6694f 100644 --- a/homeassistant/components/lcn/translations/zh-Hant.json +++ b/homeassistant/components/lcn/translations/zh-Hant.json @@ -2,7 +2,7 @@ "device_automation": { "trigger_type": { "fingerprint": "\u5df2\u6536\u5230\u6307\u7d0b\u78bc", - "send_keys": "\u5df2\u6536\u5230\u50b3\u9001\u5bc6\u9470", + "send_keys": "\u5df2\u6536\u5230\u50b3\u9001\u91d1\u9470", "transmitter": "\u5df2\u6536\u5230\u767c\u5c04\u5668\u78bc", "transponder": "\u5df2\u6536\u5230\u8a62\u7b54\u5668\u78bc" } diff --git a/homeassistant/components/metoffice/translations/zh-Hant.json b/homeassistant/components/metoffice/translations/zh-Hant.json index 381e2b36d80..4d2c3bc4195 100644 --- a/homeassistant/components/metoffice/translations/zh-Hant.json +++ b/homeassistant/components/metoffice/translations/zh-Hant.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "api_key": "API \u5bc6\u9470", + "api_key": "API \u91d1\u9470", "latitude": "\u7def\u5ea6", "longitude": "\u7d93\u5ea6" }, diff --git a/homeassistant/components/modern_forms/translations/tr.json b/homeassistant/components/modern_forms/translations/tr.json index 36caba41ee6..e27881802fb 100644 --- a/homeassistant/components/modern_forms/translations/tr.json +++ b/homeassistant/components/modern_forms/translations/tr.json @@ -19,7 +19,7 @@ "description": "Modern Forms fan\u0131n\u0131z\u0131 Home Assistant ile entegre olacak \u015fekilde ayarlay\u0131n." }, "zeroconf_confirm": { - "description": "\"{name}\" adl\u0131 Modern Formlar fan\u0131n\u0131 Ev Asistan\u0131'na eklemek istiyor musunuz?", + "description": "\"{name}\" adl\u0131 Modern Formlar fan\u0131n\u0131 Home Asistan\u0131'na eklemek istiyor musunuz?", "title": "Ke\u015ffedilen Modern Formlar fan cihaz\u0131" } } diff --git a/homeassistant/components/motion_blinds/translations/zh-Hant.json b/homeassistant/components/motion_blinds/translations/zh-Hant.json index f014a43f4ba..7aeb111ed3f 100644 --- a/homeassistant/components/motion_blinds/translations/zh-Hant.json +++ b/homeassistant/components/motion_blinds/translations/zh-Hant.json @@ -13,10 +13,10 @@ "step": { "connect": { "data": { - "api_key": "API \u5bc6\u9470", + "api_key": "API \u91d1\u9470", "interface": "\u4f7f\u7528\u7684\u7db2\u8def\u4ecb\u9762" }, - "description": "\u5c07\u9700\u8981\u8f38\u5165 16 \u4f4d\u5b57\u5143 API \u5bc6\u9470\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key \u4ee5\u7372\u5f97\u7372\u53d6\u5bc6\u9470\u7684\u6559\u5b78\u3002", + "description": "\u5c07\u9700\u8981\u8f38\u5165 16 \u4f4d\u5b57\u5143 API \u91d1\u9470\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key \u4ee5\u7372\u5f97\u7372\u53d6\u91d1\u9470\u7684\u6559\u5b78\u3002", "title": "Motion Blinds" }, "select": { @@ -28,7 +28,7 @@ }, "user": { "data": { - "api_key": "API \u5bc6\u9470", + "api_key": "API \u91d1\u9470", "host": "IP \u4f4d\u5740" }, "description": "\u9023\u7dda\u81f3 Motion \u9598\u9053\u5668\uff0c\u5047\u5982\u672a\u63d0\u4f9b IP \u4f4d\u5740\uff0c\u5c07\u4f7f\u7528\u81ea\u52d5\u63a2\u7d22", diff --git a/homeassistant/components/mysensors/translations/tr.json b/homeassistant/components/mysensors/translations/tr.json index 4ea99e6cdc7..59f7a319e07 100644 --- a/homeassistant/components/mysensors/translations/tr.json +++ b/homeassistant/components/mysensors/translations/tr.json @@ -76,5 +76,5 @@ } } }, - "title": "Sens\u00f6rlerim" + "title": "MySensors" } \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/zh-Hant.json b/homeassistant/components/nightscout/translations/zh-Hant.json index 83b7066b23c..2ad1c4fde39 100644 --- a/homeassistant/components/nightscout/translations/zh-Hant.json +++ b/homeassistant/components/nightscout/translations/zh-Hant.json @@ -12,10 +12,10 @@ "step": { "user": { "data": { - "api_key": "API \u5bc6\u9470", + "api_key": "API \u91d1\u9470", "url": "\u7db2\u5740" }, - "description": "- URL\uff1aNightscout \u88dd\u7f6e\u4f4d\u5740\u3002\u4f8b\u5982\uff1ahttps://myhomeassistant.duckdns.org:5423\n- API \u5bc6\u9470\uff08\u9078\u9805\uff09\uff1a\u50c5\u65bc\u88dd\u7f6e\u70ba\u4fdd\u8b77\u72c0\u614b\uff08(auth_default_roles != readable\uff09\u4e0b\u4f7f\u7528\u3002", + "description": "- URL\uff1aNightscout \u88dd\u7f6e\u4f4d\u5740\u3002\u4f8b\u5982\uff1ahttps://myhomeassistant.duckdns.org:5423\n- API \u91d1\u9470\uff08\u9078\u9805\uff09\uff1a\u50c5\u65bc\u88dd\u7f6e\u70ba\u4fdd\u8b77\u72c0\u614b\uff08(auth_default_roles != readable\uff09\u4e0b\u4f7f\u7528\u3002", "title": "\u8f38\u5165 Nightscout \u4f3a\u670d\u5668\u8cc7\u8a0a\u3002" } } diff --git a/homeassistant/components/nws/translations/zh-Hant.json b/homeassistant/components/nws/translations/zh-Hant.json index c3abf6ceba3..6f5bdde48d0 100644 --- a/homeassistant/components/nws/translations/zh-Hant.json +++ b/homeassistant/components/nws/translations/zh-Hant.json @@ -10,12 +10,12 @@ "step": { "user": { "data": { - "api_key": "API \u5bc6\u9470", + "api_key": "API \u91d1\u9470", "latitude": "\u7def\u5ea6", "longitude": "\u7d93\u5ea6", "station": "METAR \u6a5f\u5834\u4ee3\u78bc" }, - "description": "\u5047\u5982\u672a\u6307\u5b9a METAR \u6a5f\u5834\u4ee3\u78bc\uff0c\u5c07\u6703\u4f7f\u7528\u7d93\u7def\u5ea6\u8cc7\u8a0a\u5c0b\u627e\u6700\u8fd1\u7684\u6a5f\u5834\u3002\u76ee\u524d\uff0cAPI \u5bc6\u9470\u53ef\u8f38\u5165\u4efb\u4f55\u8cc7\u8a0a\uff0c\u5efa\u8b70\u70ba\u6709\u6548\u5730\u96fb\u5b50\u90f5\u4ef6\u4f4d\u5740\u3002", + "description": "\u5047\u5982\u672a\u6307\u5b9a METAR \u6a5f\u5834\u4ee3\u78bc\uff0c\u5c07\u6703\u4f7f\u7528\u7d93\u7def\u5ea6\u8cc7\u8a0a\u5c0b\u627e\u6700\u8fd1\u7684\u6a5f\u5834\u3002\u76ee\u524d\uff0cAPI \u91d1\u9470\u53ef\u8f38\u5165\u4efb\u4f55\u8cc7\u8a0a\uff0c\u5efa\u8b70\u70ba\u6709\u6548\u5730\u96fb\u5b50\u90f5\u4ef6\u4f4d\u5740\u3002", "title": "\u9023\u7dda\u81f3\u7f8e\u570b\u570b\u5bb6\u6c23\u8c61\u5c40\u670d\u52d9" } } diff --git a/homeassistant/components/octoprint/translations/zh-Hant.json b/homeassistant/components/octoprint/translations/zh-Hant.json index 870ff087588..e0ec444b8b6 100644 --- a/homeassistant/components/octoprint/translations/zh-Hant.json +++ b/homeassistant/components/octoprint/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "auth_failed": "\u63a5\u6536\u61c9\u7528\u7a0b\u5f0f API \u5bc6\u9470\u5931\u6557", + "auth_failed": "\u63a5\u6536\u61c9\u7528\u7a0b\u5f0f API \u91d1\u9470\u5931\u6557", "cannot_connect": "\u9023\u7dda\u5931\u6557", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, diff --git a/homeassistant/components/opengarage/translations/zh-Hant.json b/homeassistant/components/opengarage/translations/zh-Hant.json index fffbd19b551..b31c13582ea 100644 --- a/homeassistant/components/opengarage/translations/zh-Hant.json +++ b/homeassistant/components/opengarage/translations/zh-Hant.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "device_key": "\u88dd\u7f6e\u5bc6\u9470", + "device_key": "\u88dd\u7f6e\u91d1\u9470", "host": "\u4e3b\u6a5f\u7aef", "port": "\u901a\u8a0a\u57e0", "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" diff --git a/homeassistant/components/openuv/translations/zh-Hant.json b/homeassistant/components/openuv/translations/zh-Hant.json index c8aeb8f4a55..16d083da340 100644 --- a/homeassistant/components/openuv/translations/zh-Hant.json +++ b/homeassistant/components/openuv/translations/zh-Hant.json @@ -4,12 +4,12 @@ "already_configured": "\u5ea7\u6a19\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { - "invalid_api_key": "API \u5bc6\u9470\u7121\u6548" + "invalid_api_key": "API \u91d1\u9470\u7121\u6548" }, "step": { "user": { "data": { - "api_key": "API \u5bc6\u9470", + "api_key": "API \u91d1\u9470", "elevation": "\u6d77\u62d4", "latitude": "\u7def\u5ea6", "longitude": "\u7d93\u5ea6" diff --git a/homeassistant/components/openweathermap/translations/zh-Hant.json b/homeassistant/components/openweathermap/translations/zh-Hant.json index c14163b1d98..653fb373af3 100644 --- a/homeassistant/components/openweathermap/translations/zh-Hant.json +++ b/homeassistant/components/openweathermap/translations/zh-Hant.json @@ -5,19 +5,19 @@ }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", - "invalid_api_key": "API \u5bc6\u9470\u7121\u6548" + "invalid_api_key": "API \u91d1\u9470\u7121\u6548" }, "step": { "user": { "data": { - "api_key": "API \u5bc6\u9470", + "api_key": "API \u91d1\u9470", "language": "\u8a9e\u8a00", "latitude": "\u7def\u5ea6", "longitude": "\u7d93\u5ea6", "mode": "\u6a21\u5f0f", "name": "\u6574\u5408\u540d\u7a31" }, - "description": "\u6b32\u8a2d\u5b9a OpenWeatherMap \u6574\u5408\u3002\u8acb\u81f3 https://openweathermap.org/appid \u7522\u751f API \u5bc6\u9470", + "description": "\u6b32\u8a2d\u5b9a OpenWeatherMap \u6574\u5408\u3002\u8acb\u81f3 https://openweathermap.org/appid \u7522\u751f API \u91d1\u9470", "title": "OpenWeatherMap" } } diff --git a/homeassistant/components/overkiz/translations/id.json b/homeassistant/components/overkiz/translations/id.json new file mode 100644 index 00000000000..1c99dc8c08b --- /dev/null +++ b/homeassistant/components/overkiz/translations/id.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "host": "Host", + "hub": "Hub", + "password": "Kata Sandi", + "username": "Nama Pengguna" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/tr.json b/homeassistant/components/overkiz/translations/tr.json new file mode 100644 index 00000000000..16c0e882cd4 --- /dev/null +++ b/homeassistant/components/overkiz/translations/tr.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "server_in_maintenance": "Sunucu bak\u0131m nedeniyle kapal\u0131", + "too_many_requests": "\u00c7ok fazla istek var, daha sonra tekrar deneyin.", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "Sunucu", + "hub": "Hub", + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "Overkiz platformu, Somfy (Connexoon / TaHoma), Hitachi (Hi Kumo), Rexel (Energeasy Connect) ve Atlantic (Cozytouch) gibi \u00e7e\u015fitli sat\u0131c\u0131lar taraf\u0131ndan kullan\u0131lmaktad\u0131r. Uygulama kimlik bilgilerinizi girin ve hub'\u0131n\u0131z\u0131 se\u00e7in." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/zh-Hant.json b/homeassistant/components/overkiz/translations/zh-Hant.json new file mode 100644 index 00000000000..f20636f9d18 --- /dev/null +++ b/homeassistant/components/overkiz/translations/zh-Hant.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "server_in_maintenance": "\u4f3a\u670d\u5668\u7dad\u8b77\u4e2d", + "too_many_requests": "\u8acb\u6c42\u6b21\u6578\u904e\u591a\uff0c\u8acb\u7a0d\u5f8c\u91cd\u8a66\u3002", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "hub": "Hub", + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "Overkiz \u5e73\u53f0\u7531\u4f8b\u5982 Somfy (Connexoon / TaHoma)\u3001Hitachi (Hi Kumo)\u3001Rexel (Energeasy Connect) \u53ca Atlantic (Cozytouch) \u591a\u500b\u4f9b\u61c9\u5546\u6240\u5ee3\u6cdb\u4f7f\u7528\uff0c\u8f38\u5165\u61c9\u7528\u7a0b\u5f0f\u6191\u8b49\u4e26\u9078\u64c7 Hub\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/zh-Hant.json b/homeassistant/components/ozw/translations/zh-Hant.json index 9651b75386d..5ad1ca7ff6b 100644 --- a/homeassistant/components/ozw/translations/zh-Hant.json +++ b/homeassistant/components/ozw/translations/zh-Hant.json @@ -31,7 +31,7 @@ }, "start_addon": { "data": { - "network_key": "\u7db2\u8def\u5bc6\u9470", + "network_key": "\u7db2\u8def\u91d1\u9470", "usb_path": "USB \u88dd\u7f6e\u8def\u5f91" }, "title": "\u8acb\u8f38\u5165 OpenZWave \u9644\u52a0\u5143\u4ef6\u8a2d\u5b9a\u3002" diff --git a/homeassistant/components/panasonic_viera/translations/da.json b/homeassistant/components/panasonic_viera/translations/da.json new file mode 100644 index 00000000000..a8cc77d759f --- /dev/null +++ b/homeassistant/components/panasonic_viera/translations/da.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "invalid_pin_code": "Den angivne [%n\u00f8gle:f\u00e6lles::config_flow::d ata::p in%] var ugyldig" + }, + "step": { + "pairing": { + "data": { + "pin": "PIN kode" + }, + "description": "Indtast den PIN-kode, der vises p\u00e5 dit tv", + "title": "Parring" + }, + "user": { + "data": { + "host": "IP adresse" + }, + "description": "Indtast dit Panasonic Viera TV's ", + "title": "Konfigurer dit TV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/zh-Hant.json b/homeassistant/components/pi_hole/translations/zh-Hant.json index 1527b48f580..e8948ae5735 100644 --- a/homeassistant/components/pi_hole/translations/zh-Hant.json +++ b/homeassistant/components/pi_hole/translations/zh-Hant.json @@ -9,12 +9,12 @@ "step": { "api_key": { "data": { - "api_key": "API \u5bc6\u9470" + "api_key": "API \u91d1\u9470" } }, "user": { "data": { - "api_key": "API \u5bc6\u9470", + "api_key": "API \u91d1\u9470", "host": "\u4e3b\u6a5f\u7aef", "location": "\u5ea7\u6a19", "name": "\u540d\u7a31", diff --git a/homeassistant/components/rachio/translations/zh-Hant.json b/homeassistant/components/rachio/translations/zh-Hant.json index a65e4e279f9..0ab726db45b 100644 --- a/homeassistant/components/rachio/translations/zh-Hant.json +++ b/homeassistant/components/rachio/translations/zh-Hant.json @@ -11,9 +11,9 @@ "step": { "user": { "data": { - "api_key": "API \u5bc6\u9470" + "api_key": "API \u91d1\u9470" }, - "description": "\u5c07\u6703\u9700\u8981\u7531 https://app.rach.io/ \u53d6\u5f97 App \u5bc6\u9470\u3002\u9078\u64c7\u8a2d\u5b9a\u4e26\u9078\u64c7\u7372\u5f97\u5bc6\u9470\uff08GET API KEY\uff09\u3002", + "description": "\u5c07\u6703\u9700\u8981\u7531 https://app.rach.io/ \u53d6\u5f97 App \u91d1\u9470\u3002\u9078\u64c7\u8a2d\u5b9a\u4e26\u9078\u64c7\u7372\u5f97\u91d1\u9470\uff08GET API KEY\uff09\u3002", "title": "\u9023\u7dda\u81f3 Rachio \u88dd\u7f6e" } } diff --git a/homeassistant/components/roomba/translations/da.json b/homeassistant/components/roomba/translations/da.json new file mode 100644 index 00000000000..2a8057ffc95 --- /dev/null +++ b/homeassistant/components/roomba/translations/da.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "delay": "Forsinkelse" + }, + "title": "Tilslut automatisk til enheden" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/sensor.da.json b/homeassistant/components/season/translations/sensor.da.json index 7a577b90ce1..c94a23cc191 100644 --- a/homeassistant/components/season/translations/sensor.da.json +++ b/homeassistant/components/season/translations/sensor.da.json @@ -1,5 +1,11 @@ { "state": { + "season__season": { + "autumn": "Efter\u00e5r", + "spring": "For\u00e5r", + "summer": "Sommer", + "winter": "Vinter" + }, "season__season__": { "autumn": "Efter\u00e5r", "spring": "For\u00e5r", diff --git a/homeassistant/components/sensibo/translations/id.json b/homeassistant/components/sensibo/translations/id.json new file mode 100644 index 00000000000..d4ea1c29254 --- /dev/null +++ b/homeassistant/components/sensibo/translations/id.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "user": { + "data": { + "api_key": "Kunci API", + "name": "Nama" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/zh-Hant.json b/homeassistant/components/sensibo/translations/zh-Hant.json index 818d64dbf1b..4abe3544048 100644 --- a/homeassistant/components/sensibo/translations/zh-Hant.json +++ b/homeassistant/components/sensibo/translations/zh-Hant.json @@ -9,7 +9,7 @@ "step": { "user": { "data": { - "api_key": "API \u5bc6\u9470", + "api_key": "API \u91d1\u9470", "name": "\u540d\u7a31" } } diff --git a/homeassistant/components/sensor/translations/zh-Hant.json b/homeassistant/components/sensor/translations/zh-Hant.json index f549d18dbc7..1dcb63d4052 100644 --- a/homeassistant/components/sensor/translations/zh-Hant.json +++ b/homeassistant/components/sensor/translations/zh-Hant.json @@ -1,6 +1,7 @@ { "device_automation": { "condition_type": { + "is_apparent_power": "\u76ee\u524d{entity_name}\u8996\u5728\u529f\u7387", "is_battery_level": "\u76ee\u524d{entity_name}\u96fb\u91cf", "is_carbon_dioxide": "\u76ee\u524d {entity_name} \u4e8c\u6c27\u5316\u78b3\u6fc3\u5ea6\u72c0\u614b", "is_carbon_monoxide": "\u76ee\u524d {entity_name} \u4e00\u6c27\u5316\u78b3\u6fc3\u5ea6\u72c0\u614b", @@ -20,6 +21,7 @@ "is_power": "\u76ee\u524d{entity_name}\u96fb\u529b", "is_power_factor": "\u76ee\u524d{entity_name}\u529f\u7387\u56e0\u6578", "is_pressure": "\u76ee\u524d{entity_name}\u58d3\u529b", + "is_reactive_power": "\u76ee\u524d{entity_name}\u7121\u6548\u529f\u7387", "is_signal_strength": "\u76ee\u524d{entity_name}\u8a0a\u865f\u5f37\u5ea6", "is_sulphur_dioxide": "\u76ee\u524d {entity_name} \u4e8c\u6c27\u5316\u786b\u6fc3\u5ea6\u72c0\u614b", "is_temperature": "\u76ee\u524d{entity_name}\u6eab\u5ea6", @@ -28,6 +30,7 @@ "is_voltage": "\u76ee\u524d{entity_name}\u96fb\u58d3" }, "trigger_type": { + "apparent_power": "{entity_name}\u8996\u5728\u529f\u7387\u8b8a\u66f4", "battery_level": "{entity_name}\u96fb\u91cf\u8b8a\u66f4", "carbon_dioxide": "{entity_name} \u4e8c\u6c27\u5316\u78b3\u6fc3\u5ea6\u8b8a\u5316", "carbon_monoxide": "{entity_name} \u4e00\u6c27\u5316\u78b3\u6fc3\u5ea6\u8b8a\u5316", @@ -47,6 +50,7 @@ "power": "{entity_name}\u96fb\u529b\u8b8a\u66f4", "power_factor": "\u76ee\u524d{entity_name}\u529f\u7387\u56e0\u6578\u8b8a\u66f4", "pressure": "{entity_name}\u58d3\u529b\u8b8a\u66f4", + "reactive_power": "{entity_name}\u7121\u6548\u529f\u7387\u8b8a\u66f4", "signal_strength": "{entity_name}\u8a0a\u865f\u5f37\u5ea6\u8b8a\u66f4", "sulphur_dioxide": "{entity_name} \u4e8c\u6c27\u5316\u786b\u6fc3\u5ea6\u8b8a\u5316", "temperature": "{entity_name}\u6eab\u5ea6\u8b8a\u66f4", diff --git a/homeassistant/components/sia/translations/zh-Hant.json b/homeassistant/components/sia/translations/zh-Hant.json index 6cd3c879656..81a0ad0fab4 100644 --- a/homeassistant/components/sia/translations/zh-Hant.json +++ b/homeassistant/components/sia/translations/zh-Hant.json @@ -3,7 +3,7 @@ "error": { "invalid_account_format": "\u5e33\u865f\u70ba\u5341\u516d\u9032\u4f4d\u6578\u503c\u3001\u8acb\u4f7f\u7528 0-9 \u53ca A-F\u3002", "invalid_account_length": "\u5e33\u865f\u9577\u5ea6\u4e0d\u6b63\u78ba\u3001\u5fc5\u9808\u4ecb\u65bc 3 \u81f3 16 \u500b\u5b57\u5143\u4e4b\u9593\u3002", - "invalid_key_format": "\u5bc6\u9470\u70ba\u5341\u516d\u9032\u4f4d\u6578\u503c\u3001\u8acb\u4f7f\u7528 0-9 \u53ca A-F\u3002", + "invalid_key_format": "\u91d1\u9470\u70ba\u5341\u516d\u9032\u4f4d\u6578\u503c\u3001\u8acb\u4f7f\u7528 0-9 \u53ca A-F\u3002", "invalid_key_length": "\u5e33\u865f\u9577\u5ea6\u4e0d\u6b63\u78ba\u3001\u5fc5\u9808\u70ba 16\u300124 \u6216 32 \u500b\u5341\u516d\u9032\u4f4d\u5b57\u5143\u3002", "invalid_ping": "Ping \u9593\u8ddd\u5fc5\u9808\u70ba 1 \u81f3 1440 \u5206\u9418\u4e4b\u9593\u3002", "invalid_zones": "\u81f3\u5c11\u5fc5\u9808\u6709\u4e00\u500b\u5206\u5340\u3002", @@ -14,7 +14,7 @@ "data": { "account": "\u5e33\u865f ID", "additional_account": "\u9644\u52a0\u5e33\u865f", - "encryption_key": "\u5bc6\u9470", + "encryption_key": "\u91d1\u9470", "ping_interval": "Pin \u9593\u8ddd\uff08\u5206\u9418\uff09", "zones": "\u5e33\u865f\u5206\u5340\u6578\u76ee" }, @@ -24,7 +24,7 @@ "data": { "account": "\u5e33\u865f ID", "additional_account": "\u9644\u52a0\u5e33\u865f", - "encryption_key": "\u5bc6\u9470", + "encryption_key": "\u91d1\u9470", "ping_interval": "Pin \u9593\u8ddd\uff08\u5206\u9418\uff09", "port": "\u901a\u8a0a\u57e0", "protocol": "\u901a\u8a0a\u5354\u5b9a", diff --git a/homeassistant/components/solaredge/translations/zh-Hant.json b/homeassistant/components/solaredge/translations/zh-Hant.json index ff1a01b3567..7fa8f45914f 100644 --- a/homeassistant/components/solaredge/translations/zh-Hant.json +++ b/homeassistant/components/solaredge/translations/zh-Hant.json @@ -6,13 +6,13 @@ "error": { "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "could_not_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 solaredge API", - "invalid_api_key": "API \u5bc6\u9470\u7121\u6548", + "invalid_api_key": "API \u91d1\u9470\u7121\u6548", "site_not_active": "\u7db2\u7ad9\u672a\u555f\u7528" }, "step": { "user": { "data": { - "api_key": "API \u5bc6\u9470", + "api_key": "API \u91d1\u9470", "name": "\u5b89\u88dd\u540d\u7a31", "site_id": "SolarEdge site-id" }, diff --git a/homeassistant/components/sonarr/translations/zh-Hant.json b/homeassistant/components/sonarr/translations/zh-Hant.json index c6f97c1892e..0a107efae6e 100644 --- a/homeassistant/components/sonarr/translations/zh-Hant.json +++ b/homeassistant/components/sonarr/translations/zh-Hant.json @@ -17,7 +17,7 @@ }, "user": { "data": { - "api_key": "API \u5bc6\u9470", + "api_key": "API \u91d1\u9470", "base_path": "API \u8def\u5f91", "host": "\u4e3b\u6a5f\u7aef", "port": "\u901a\u8a0a\u57e0", diff --git a/homeassistant/components/starline/translations/zh-Hant.json b/homeassistant/components/starline/translations/zh-Hant.json index 722c5daaad4..48fd70a1d9a 100644 --- a/homeassistant/components/starline/translations/zh-Hant.json +++ b/homeassistant/components/starline/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "error": { - "error_auth_app": "\u61c9\u7528\u7a0b\u5f0f ID \u932f\u8aa4\u6216\u4e0d\u6b63\u78ba", + "error_auth_app": "\u61c9\u7528\u7a0b\u5f0f ID \u6216\u79c1\u9470\u932f\u8aa4", "error_auth_mfa": "\u5bc6\u78bc\u932f\u8aa4", "error_auth_user": "\u4f7f\u7528\u8005\u540d\u7a31\u6216\u5bc6\u78bc\u932f\u8aa4" }, @@ -9,9 +9,9 @@ "auth_app": { "data": { "app_id": "App ID", - "app_secret": "\u5bc6\u78bc" + "app_secret": "\u79c1\u9470" }, - "description": "\u7531 [StarLine \u958b\u767c\u8005\u5e33\u865f] (https://my.starline.ru/developer) \u6240\u53d6\u5f97\u4e4b\u61c9\u7528\u7a0b\u5f0f ID \u8207\u5bc6\u78bc", + "description": "\u7531 [StarLine \u958b\u767c\u8005\u5e33\u865f] (https://my.starline.ru/developer) \u6240\u53d6\u5f97\u4e4b\u61c9\u7528\u7a0b\u5f0f ID \u8207\u79c1\u9470", "title": "\u61c9\u7528\u6191\u8b49" }, "auth_captcha": { diff --git a/homeassistant/components/synology_dsm/translations/da.json b/homeassistant/components/synology_dsm/translations/da.json index f95e08df3c1..48e75cc5f95 100644 --- a/homeassistant/components/synology_dsm/translations/da.json +++ b/homeassistant/components/synology_dsm/translations/da.json @@ -1,6 +1,11 @@ { "config": { "step": { + "2sa": { + "data": { + "otp_code": "Kode" + } + }, "link": { "data": { "password": "Adgangskode", diff --git a/homeassistant/components/system_bridge/translations/tr.json b/homeassistant/components/system_bridge/translations/tr.json index 0a67529a511..17e43680cca 100644 --- a/homeassistant/components/system_bridge/translations/tr.json +++ b/homeassistant/components/system_bridge/translations/tr.json @@ -28,5 +28,5 @@ } } }, - "title": "Sistem K\u00f6pr\u00fcs\u00fc" + "title": "System Bridge" } \ No newline at end of file diff --git a/homeassistant/components/system_bridge/translations/zh-Hant.json b/homeassistant/components/system_bridge/translations/zh-Hant.json index 2ca44e48710..5f45cc5dfcd 100644 --- a/homeassistant/components/system_bridge/translations/zh-Hant.json +++ b/homeassistant/components/system_bridge/translations/zh-Hant.json @@ -14,13 +14,13 @@ "step": { "authenticate": { "data": { - "api_key": "API \u5bc6\u9470" + "api_key": "API \u91d1\u9470" }, - "description": "\u8acb\u8f38\u5165 {name} \u8a2d\u5b9a\u4e2d\u6240\u8a2d\u5b9a\u4e4b API \u5bc6\u9470\u3002" + "description": "\u8acb\u8f38\u5165 {name} \u8a2d\u5b9a\u4e2d\u6240\u8a2d\u5b9a\u4e4b API \u91d1\u9470\u3002" }, "user": { "data": { - "api_key": "API \u5bc6\u9470", + "api_key": "API \u91d1\u9470", "host": "\u4e3b\u6a5f\u7aef", "port": "\u901a\u8a0a\u57e0" }, diff --git a/homeassistant/components/tailscale/translations/zh-Hant.json b/homeassistant/components/tailscale/translations/zh-Hant.json index b47dd0cc57b..5ed5f1deb8f 100644 --- a/homeassistant/components/tailscale/translations/zh-Hant.json +++ b/homeassistant/components/tailscale/translations/zh-Hant.json @@ -10,16 +10,16 @@ "step": { "reauth_confirm": { "data": { - "api_key": "API \u5bc6\u9470" + "api_key": "API \u91d1\u9470" }, - "description": "Tailscale API \u6b0a\u6756\u6709\u6548\u671f\u70ba 90 \u5929\u3002\u53ef\u4ee5\u65bc https://login.tailscale.com/admin/settings/authkeys \u53d6\u5f97\u66f4\u65b0 Tailscale API \u5bc6\u9470\u3002" + "description": "Tailscale API \u6b0a\u6756\u6709\u6548\u671f\u70ba 90 \u5929\u3002\u53ef\u4ee5\u65bc https://login.tailscale.com/admin/settings/authkeys \u53d6\u5f97\u66f4\u65b0 Tailscale API \u91d1\u9470\u3002" }, "user": { "data": { - "api_key": "API \u5bc6\u9470", + "api_key": "API \u91d1\u9470", "tailnet": "Tailnet" }, - "description": "\u6b32\u4f7f\u7528 Tailscale \u8a8d\u8b49\u3001\u5c07\u9700\u8981\u65bc https://login.tailscale.com/admin/settings/authkeys \u65b0\u589e\u4e00\u7d44 API \u5bc6\u9470 \n\nTailnet \u70ba Tailscale \u7db2\u8def\u7684\u540d\u7a31\uff0c\u53ef\u4ee5\u65bc Tailscale \u7ba1\u7406\u9762\u677f\uff08Tailscale Logo \u65c1\uff09\u7684\u5de6\u4e0a\u65b9\u627e\u5230\u6b64\u8cc7\u8a0a\u3002" + "description": "\u6b32\u4f7f\u7528 Tailscale \u8a8d\u8b49\u3001\u5c07\u9700\u8981\u65bc https://login.tailscale.com/admin/settings/authkeys \u65b0\u589e\u4e00\u7d44 API \u91d1\u9470 \n\nTailnet \u70ba Tailscale \u7db2\u8def\u7684\u540d\u7a31\uff0c\u53ef\u4ee5\u65bc Tailscale \u7ba1\u7406\u9762\u677f\uff08Tailscale Logo \u65c1\uff09\u7684\u5de6\u4e0a\u65b9\u627e\u5230\u6b64\u8cc7\u8a0a\u3002" } } } diff --git a/homeassistant/components/trafikverket_weatherstation/translations/zh-Hant.json b/homeassistant/components/trafikverket_weatherstation/translations/zh-Hant.json index 4323ed42b7a..47394b08af9 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/zh-Hant.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/zh-Hant.json @@ -12,7 +12,7 @@ "step": { "user": { "data": { - "api_key": "API \u5bc6\u9470", + "api_key": "API \u91d1\u9470", "conditions": "\u5df2\u76e3\u63a7\u72c0\u614b", "name": "\u4f7f\u7528\u8005\u540d\u7a31", "station": "\u76e3\u63a7\u7ad9" diff --git a/homeassistant/components/tuya/translations/zh-Hant.json b/homeassistant/components/tuya/translations/zh-Hant.json index 042c3a929e1..f99a4781fdc 100644 --- a/homeassistant/components/tuya/translations/zh-Hant.json +++ b/homeassistant/components/tuya/translations/zh-Hant.json @@ -14,7 +14,7 @@ "login": { "data": { "access_id": "Access ID", - "access_secret": "Access Secret", + "access_secret": "\u5b58\u53d6\u79c1\u9470", "country_code": "\u570b\u78bc", "endpoint": "\u53ef\u7528\u5340\u57df", "password": "\u5bc6\u78bc", @@ -27,7 +27,7 @@ "user": { "data": { "access_id": "Tuya IoT Access ID", - "access_secret": "Tuya IoT Access Secret", + "access_secret": "Tuya IoT \u5b58\u53d6\u79c1\u9470", "country_code": "\u570b\u5bb6", "password": "\u5bc6\u78bc", "platform": "\u5e33\u6236\u8a3b\u518a\u6240\u5728\u4f4d\u7f6e", diff --git a/homeassistant/components/uptimerobot/translations/zh-Hant.json b/homeassistant/components/uptimerobot/translations/zh-Hant.json index 89977ea815f..8b01cab6d7c 100644 --- a/homeassistant/components/uptimerobot/translations/zh-Hant.json +++ b/homeassistant/components/uptimerobot/translations/zh-Hant.json @@ -8,23 +8,23 @@ }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", - "invalid_api_key": "API \u5bc6\u9470\u7121\u6548", - "reauth_failed_matching_account": "\u6240\u63d0\u4f9b\u7684\u5bc6\u9470\u8207\u73fe\u6709\u8a2d\u5b9a\u5e33\u865f ID \u4e0d\u7b26\u3002", + "invalid_api_key": "API \u91d1\u9470\u7121\u6548", + "reauth_failed_matching_account": "\u6240\u63d0\u4f9b\u7684\u91d1\u9470\u8207\u73fe\u6709\u8a2d\u5b9a\u5e33\u865f ID \u4e0d\u7b26\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { "reauth_confirm": { "data": { - "api_key": "API \u5bc6\u9470" + "api_key": "API \u91d1\u9470" }, - "description": "\u9700\u8981\u63d0\u4f9b\u7531 UptimeRobot \u53d6\u5f97\u4e00\u7d44\u65b0\u7684\u552f\u8b80 API \u5bc6\u9470", + "description": "\u9700\u8981\u63d0\u4f9b\u7531 UptimeRobot \u53d6\u5f97\u4e00\u7d44\u65b0\u7684\u552f\u8b80 API \u91d1\u9470", "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" }, "user": { "data": { - "api_key": "API \u5bc6\u9470" + "api_key": "API \u91d1\u9470" }, - "description": "\u9700\u8981\u63d0\u4f9b\u7531 UptimeRobot \u53d6\u5f97\u552f\u8b80 API \u5bc6\u9470" + "description": "\u9700\u8981\u63d0\u4f9b\u7531 UptimeRobot \u53d6\u5f97\u552f\u8b80 API \u91d1\u9470" } } } diff --git a/homeassistant/components/version/translations/id.json b/homeassistant/components/version/translations/id.json new file mode 100644 index 00000000000..a751d6eb2a5 --- /dev/null +++ b/homeassistant/components/version/translations/id.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "step": { + "user": { + "data": { + "version_source": "Sumber versi" + }, + "description": "Pilih sumber yang ingin Anda lacak versinya", + "title": "Pilih jenis instalasi" + }, + "version_source": { + "data": { + "beta": "Sertakan versi beta", + "board": "Board mana yang harus dilacak", + "channel": "Kanal mana yang harus dilacak", + "image": "Citra mana yang harus dilacak" + }, + "description": "Konfigurasikan pelacakan versi {version_source}", + "title": "Konfigurasikan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/version/translations/tr.json b/homeassistant/components/version/translations/tr.json new file mode 100644 index 00000000000..a36bfcb6644 --- /dev/null +++ b/homeassistant/components/version/translations/tr.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "step": { + "user": { + "data": { + "version_source": "S\u00fcr\u00fcm kayna\u011f\u0131" + }, + "description": "S\u00fcr\u00fcmlerini izlemek istedi\u011finiz kayna\u011f\u0131 se\u00e7in", + "title": "Kurulum t\u00fcr\u00fcn\u00fc se\u00e7in" + }, + "version_source": { + "data": { + "beta": "Beta s\u00fcr\u00fcmlerini dahil et", + "board": "Hangi pano izlenmeli", + "channel": "Hangi kanal izlenmeli", + "image": "Hangi g\u00f6r\u00fcnt\u00fc izlenmeli" + }, + "description": "{version_source} s\u00fcr\u00fcm izlemeyi yap\u0131land\u0131r\u0131n", + "title": "Yap\u0131land\u0131r" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/version/translations/zh-Hant.json b/homeassistant/components/version/translations/zh-Hant.json new file mode 100644 index 00000000000..ee24e6a4298 --- /dev/null +++ b/homeassistant/components/version/translations/zh-Hant.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "step": { + "user": { + "data": { + "version_source": "\u7248\u672c\u4f86\u6e90" + }, + "description": "\u8ffd\u8e64\u7248\u672c\u4f86\u6e90", + "title": "\u9078\u64c7\u5b89\u88dd\u985e\u578b" + }, + "version_source": { + "data": { + "beta": "\u5305\u542b\u6e2c\u8a66\u7248\u672c", + "board": "\u6240\u8981\u8ffd\u8e64\u7684\u786c\u9ad4\u985e\u578b", + "channel": "\u6240\u8981\u8ffd\u8e64\u7684\u7248\u6b21", + "image": "\u6240\u8981\u8ffd\u8e64\u7684\u6620\u50cf\u6a94\u7248\u672c" + }, + "description": "\u8a2d\u5b9a {version_source} \u7248\u672c\u8ffd\u8e64", + "title": "\u8a2d\u5b9a" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vicare/translations/zh-Hant.json b/homeassistant/components/vicare/translations/zh-Hant.json index 10119fcce93..648acb7e35f 100644 --- a/homeassistant/components/vicare/translations/zh-Hant.json +++ b/homeassistant/components/vicare/translations/zh-Hant.json @@ -11,14 +11,14 @@ "step": { "user": { "data": { - "client_id": "API \u5bc6\u9470", + "client_id": "API \u91d1\u9470", "heating_type": "\u6696\u6c23\u985e\u578b", "name": "\u540d\u7a31", "password": "\u5bc6\u78bc", "scan_interval": "\u6383\u63cf\u9593\u8ddd\uff08\u79d2\uff09", "username": "\u96fb\u5b50\u90f5\u4ef6" }, - "description": "\u6b32\u8a2d\u5b9a ViCare \u6574\u5408\u3002\u8acb\u81f3 https://developer.viessmann.com \u7522\u751f API \u5bc6\u9470", + "description": "\u6b32\u8a2d\u5b9a ViCare \u6574\u5408\u3002\u8acb\u81f3 https://developer.viessmann.com \u7522\u751f API \u91d1\u9470", "title": "{name}" } } diff --git a/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json b/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json index 4fe5decd858..dcc7b76a0c4 100644 --- a/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json +++ b/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json @@ -9,7 +9,7 @@ "discovery_error": "\u63a2\u7d22\u5c0f\u7c73 Aqara \u7db2\u95dc\u5931\u6557\uff0c\u8acb\u5617\u8a66\u4f7f\u7528\u57f7\u884c Home Assistant \u88dd\u7f6e\u7684 IP \u4f5c\u70ba\u4ecb\u9762", "invalid_host": "\u7121\u6548\u4e3b\u6a5f\u540d\u7a31\u6216 IP \u4f4d\u5740\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", "invalid_interface": "\u7db2\u8def\u4ecb\u9762\u7121\u6548", - "invalid_key": "\u7db2\u95dc\u5bc6\u9470\u7121\u6548", + "invalid_key": "\u7db2\u95dc\u91d1\u9470\u7121\u6548", "invalid_mac": "\u7121\u6548\u7684 Mac \u4f4d\u5740" }, "flow_title": "{name}", @@ -23,10 +23,10 @@ }, "settings": { "data": { - "key": "\u7db2\u95dc\u5bc6\u9470", + "key": "\u7db2\u95dc\u91d1\u9470", "name": "\u7db2\u95dc\u540d\u7a31" }, - "description": "\u5bc6\u9470\uff08\u5bc6\u78bc\uff09\u53d6\u5f97\u8acb\u53c3\u8003\u4e0b\u65b9\u6559\u5b78\uff1ahttps://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz\u3002\u5047\u5982\u672a\u63d0\u4f9b\u5bc6\u9470\u3001\u5247\u50c5\u6703\u6536\u5230\u611f\u6e2c\u5668\u88dd\u7f6e\u7684\u8cc7\u8a0a", + "description": "\u91d1\u9470\uff08\u5bc6\u78bc\uff09\u53d6\u5f97\u8acb\u53c3\u8003\u4e0b\u65b9\u6559\u5b78\uff1ahttps://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz\u3002\u5047\u5982\u672a\u63d0\u4f9b\u91d1\u9470\u3001\u5247\u50c5\u6703\u6536\u5230\u611f\u6e2c\u5668\u88dd\u7f6e\u7684\u8cc7\u8a0a", "title": "\u5c0f\u7c73 Aqara \u7db2\u95dc\u9078\u9805\u8a2d\u5b9a" }, "user": { diff --git a/homeassistant/components/xiaomi_miio/translations/zh-Hant.json b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json index db9db466f4e..2812f91be7e 100644 --- a/homeassistant/components/xiaomi_miio/translations/zh-Hant.json +++ b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json @@ -59,7 +59,7 @@ "host": "IP \u4f4d\u5740", "token": "API \u6b0a\u6756" }, - "description": "\u5c07\u9700\u8981\u8f38\u5165 32 \u4f4d\u5b57\u5143 API \u6b0a\u6756\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token \u4ee5\u7372\u5f97\u7372\u53d6\u5bc6\u9470\u7684\u6559\u5b78\u3002\u8acb\u6ce8\u610f\uff1a\u6b64 API \u6b0a\u6756\u8207\u5c0f\u7c73 Aqara \u6574\u5408\u6240\u4f7f\u7528\u4e4b\u5bc6\u9470\u4e0d\u540c\u3002", + "description": "\u5c07\u9700\u8981\u8f38\u5165 32 \u4f4d\u5b57\u5143 API \u6b0a\u6756\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token \u4ee5\u7372\u5f97\u7372\u53d6\u91d1\u9470\u7684\u6559\u5b78\u3002\u8acb\u6ce8\u610f\uff1a\u6b64 API \u6b0a\u6756\u8207\u5c0f\u7c73 Aqara \u6574\u5408\u6240\u4f7f\u7528\u4e4b\u91d1\u9470\u4e0d\u540c\u3002", "title": "\u9023\u7dda\u81f3\u5c0f\u7c73 MIIO \u88dd\u7f6e\u6216\u5c0f\u7c73\u7db2\u95dc" }, "reauth_confirm": { diff --git a/homeassistant/components/zwave/translations/zh-Hant.json b/homeassistant/components/zwave/translations/zh-Hant.json index 4be9b77a8c6..f7979daff9e 100644 --- a/homeassistant/components/zwave/translations/zh-Hant.json +++ b/homeassistant/components/zwave/translations/zh-Hant.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "network_key": "\u7db2\u8def\u5bc6\u9470\uff08\u4fdd\u7559\u7a7a\u767d\u5c07\u6703\u81ea\u52d5\u7522\u751f\uff09", + "network_key": "\u7db2\u8def\u91d1\u9470\uff08\u4fdd\u7559\u7a7a\u767d\u5c07\u6703\u81ea\u52d5\u7522\u751f\uff09", "usb_path": "USB \u88dd\u7f6e\u8def\u5f91" }, "description": "\u6b64\u6574\u5408\u5df2\u7d93\u4e0d\u518d\u9032\u884c\u7dad\u8b77\uff0c\u8acb\u4f7f\u7528 Z-Wave JS \u53d6\u4ee3\u70ba\u65b0\u5b89\u88dd\u65b9\u5f0f\u3002\n\n\u8acb\u53c3\u95b1 https://www.home-assistant.io/docs/z-wave/installation/ \u4ee5\n\u7372\u5f97\u8a2d\u5b9a\u8b8a\u6578\u8cc7\u8a0a" diff --git a/homeassistant/components/zwave_js/translations/it.json b/homeassistant/components/zwave_js/translations/it.json index 70c7062bc57..bab70cd4b94 100644 --- a/homeassistant/components/zwave_js/translations/it.json +++ b/homeassistant/components/zwave_js/translations/it.json @@ -9,7 +9,7 @@ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", "cannot_connect": "Impossibile connettersi", - "discovery_requires_supervisor": "Il rilevamento richiede il Supervisor.", + "discovery_requires_supervisor": "Il rilevamento richiede il supervisore.", "not_zwave_device": "Il dispositivo rilevato non \u00e8 un dispositivo Z-Wave." }, "error": { @@ -49,9 +49,9 @@ }, "on_supervisor": { "data": { - "use_addon": "Usa il componente aggiuntivo Z-Wave JS Supervisor" + "use_addon": "Usa il componente aggiuntivo Z-Wave JS del supervisore" }, - "description": "Desideri utilizzare il componente aggiuntivo Z-Wave JS Supervisor?", + "description": "Desideri utilizzare il componente aggiuntivo Z-Wave JS del supervisore?", "title": "Seleziona il metodo di connessione" }, "start_addon": { @@ -133,9 +133,9 @@ }, "on_supervisor": { "data": { - "use_addon": "Usa il componente aggiuntivo Z-Wave JS di Supervisor" + "use_addon": "Usa il componente aggiuntivo Z-Wave JS del supervisore" }, - "description": "Desideri utilizzare il componente aggiuntivo Z-Wave JS di Supervisor?", + "description": "Desideri utilizzare il componente aggiuntivo Z-Wave JS del supervisore?", "title": "Seleziona il metodo di connessione" }, "start_addon": { diff --git a/homeassistant/components/zwave_js/translations/zh-Hant.json b/homeassistant/components/zwave_js/translations/zh-Hant.json index 5088766395f..9dd44c4457f 100644 --- a/homeassistant/components/zwave_js/translations/zh-Hant.json +++ b/homeassistant/components/zwave_js/translations/zh-Hant.json @@ -26,14 +26,14 @@ "step": { "configure_addon": { "data": { - "network_key": "\u7db2\u8def\u5bc6\u9470", - "s0_legacy_key": "S0 \u5bc6\u9470\uff08\u820a\u7248\uff09", - "s2_access_control_key": "S2 \u5b58\u53d6\u63a7\u5236\u5bc6\u9470", - "s2_authenticated_key": "S2 \u9a57\u8b49\u5bc6\u9470", - "s2_unauthenticated_key": "S2 \u672a\u9a57\u8b49\u5bc6\u9470", + "network_key": "\u7db2\u8def\u91d1\u9470", + "s0_legacy_key": "S0 \u91d1\u9470\uff08\u820a\u7248\uff09", + "s2_access_control_key": "S2 \u5b58\u53d6\u63a7\u5236\u91d1\u9470", + "s2_authenticated_key": "S2 \u9a57\u8b49\u91d1\u9470", + "s2_unauthenticated_key": "S2 \u672a\u9a57\u8b49\u91d1\u9470", "usb_path": "USB \u88dd\u7f6e\u8def\u5f91" }, - "description": "\u5047\u5982\u6b04\u4f4d\u4fdd\u6301\u7a7a\u767d\u3001\u9644\u52a0\u5143\u4ef6\u5c07\u6703\u7522\u751f\u4e00\u7d44\u5b89\u5168\u5bc6\u9470\u3002", + "description": "\u5047\u5982\u6b04\u4f4d\u4fdd\u6301\u7a7a\u767d\u3001\u9644\u52a0\u5143\u4ef6\u5c07\u6703\u7522\u751f\u4e00\u7d44\u5b89\u5168\u91d1\u9470\u3002", "title": "\u8f38\u5165 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u8a2d\u5b9a" }, "hassio_confirm": { @@ -113,14 +113,14 @@ "data": { "emulate_hardware": "\u6a21\u64ec\u786c\u9ad4", "log_level": "\u65e5\u8a8c\u8a18\u9304\u7b49\u7d1a", - "network_key": "\u7db2\u8def\u5bc6\u9470", - "s0_legacy_key": "S0 \u5bc6\u9470\uff08\u820a\u7248\uff09", - "s2_access_control_key": "S2 \u5b58\u53d6\u63a7\u5236\u5bc6\u9470", - "s2_authenticated_key": "S2 \u9a57\u8b49\u5bc6\u9470", - "s2_unauthenticated_key": "S2 \u672a\u9a57\u8b49\u5bc6\u9470", + "network_key": "\u7db2\u8def\u91d1\u9470", + "s0_legacy_key": "S0 \u91d1\u9470\uff08\u820a\u7248\uff09", + "s2_access_control_key": "S2 \u5b58\u53d6\u63a7\u5236\u91d1\u9470", + "s2_authenticated_key": "S2 \u9a57\u8b49\u91d1\u9470", + "s2_unauthenticated_key": "S2 \u672a\u9a57\u8b49\u91d1\u9470", "usb_path": "USB \u88dd\u7f6e\u8def\u5f91" }, - "description": "\u5047\u5982\u6b04\u4f4d\u4fdd\u6301\u7a7a\u767d\u3001\u9644\u52a0\u5143\u4ef6\u5c07\u6703\u7522\u751f\u4e00\u7d44\u5b89\u5168\u5bc6\u9470\u3002", + "description": "\u5047\u5982\u6b04\u4f4d\u4fdd\u6301\u7a7a\u767d\u3001\u9644\u52a0\u5143\u4ef6\u5c07\u6703\u7522\u751f\u4e00\u7d44\u5b89\u5168\u91d1\u9470\u3002", "title": "\u8f38\u5165 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u8a2d\u5b9a" }, "install_addon": { From c54439ef42f0015ee6c4aaa8286e25a388be7d09 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 25 Dec 2021 21:47:45 -0800 Subject: [PATCH 1033/2644] Bump google-nest-sdm to 1.0.0 (#62783) * Bump google-nest-sdm to 1.0.0 See release log in https://github.com/allenporter/python-google-nest-sdm/compare/0.4.9...1.0.0 * Remove typing ignore now that typing is fixed --- homeassistant/components/nest/manifest.json | 2 +- homeassistant/components/nest/media_source.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index f943293775a..0e6d219eca4 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["ffmpeg", "http", "media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.9"], + "requirements": ["python-nest==4.1.0", "google-nest-sdm==1.0.0"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/homeassistant/components/nest/media_source.py b/homeassistant/components/nest/media_source.py index 66f6cec43d1..889c1a4a2db 100644 --- a/homeassistant/components/nest/media_source.py +++ b/homeassistant/components/nest/media_source.py @@ -135,7 +135,7 @@ class NestEventMediaStore(EventMediaStore): ) return self._data - async def async_save(self, data: dict) -> None: # type: ignore[override] + async def async_save(self, data: dict) -> None: """Save data.""" self._data = data diff --git a/requirements_all.txt b/requirements_all.txt index 3bb5004e3a3..65228686105 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -746,7 +746,7 @@ google-cloud-pubsub==2.1.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==0.4.9 +google-nest-sdm==1.0.0 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9c52d554e83..20ef660a159 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -468,7 +468,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.1.0 # homeassistant.components.nest -google-nest-sdm==0.4.9 +google-nest-sdm==1.0.0 # homeassistant.components.google_travel_time googlemaps==2.5.1 From e982e7403a17a0b7c2d88648c7a010c2c901eb10 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Sun, 26 Dec 2021 01:12:57 -0500 Subject: [PATCH 1034/2644] Add unifiprotect integration (#62697) Co-authored-by: J. Nick Koston --- .strict-typing | 1 + CODEOWNERS | 2 + .../components/unifiprotect/__init__.py | 106 ++++ .../components/unifiprotect/camera.py | 169 ++++++ .../components/unifiprotect/config_flow.py | 218 ++++++++ .../components/unifiprotect/const.py | 54 ++ homeassistant/components/unifiprotect/data.py | 148 ++++++ .../components/unifiprotect/entity.py | 113 ++++ .../components/unifiprotect/manifest.json | 16 + .../components/unifiprotect/strings.json | 34 ++ .../unifiprotect/translations/en.json | 34 ++ homeassistant/generated/config_flows.py | 1 + mypy.ini | 11 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/unifiprotect/__init__.py | 1 + tests/components/unifiprotect/conftest.py | 168 ++++++ .../sample_data/sample_camera.json | 482 ++++++++++++++++++ tests/components/unifiprotect/test_camera.py | 358 +++++++++++++ .../unifiprotect/test_config_flow.py | 227 +++++++++ tests/components/unifiprotect/test_init.py | 109 ++++ 21 files changed, 2258 insertions(+) create mode 100644 homeassistant/components/unifiprotect/__init__.py create mode 100644 homeassistant/components/unifiprotect/camera.py create mode 100644 homeassistant/components/unifiprotect/config_flow.py create mode 100644 homeassistant/components/unifiprotect/const.py create mode 100644 homeassistant/components/unifiprotect/data.py create mode 100644 homeassistant/components/unifiprotect/entity.py create mode 100644 homeassistant/components/unifiprotect/manifest.json create mode 100644 homeassistant/components/unifiprotect/strings.json create mode 100644 homeassistant/components/unifiprotect/translations/en.json create mode 100644 tests/components/unifiprotect/__init__.py create mode 100644 tests/components/unifiprotect/conftest.py create mode 100644 tests/components/unifiprotect/sample_data/sample_camera.json create mode 100644 tests/components/unifiprotect/test_camera.py create mode 100644 tests/components/unifiprotect/test_config_flow.py create mode 100644 tests/components/unifiprotect/test_init.py diff --git a/.strict-typing b/.strict-typing index 66fc72efc77..cc64f5ca5b0 100644 --- a/.strict-typing +++ b/.strict-typing @@ -143,6 +143,7 @@ homeassistant.components.tractive.* homeassistant.components.tradfri.* homeassistant.components.tts.* homeassistant.components.twentemilieu.* +homeassistant.components.unifiprotect.* homeassistant.components.upcloud.* homeassistant.components.uptime.* homeassistant.components.uptimerobot.* diff --git a/CODEOWNERS b/CODEOWNERS index 217ffb0b22b..bbcfadaf3bf 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -966,6 +966,8 @@ homeassistant/components/ubus/* @noltari homeassistant/components/unifi/* @Kane610 tests/components/unifi/* @Kane610 homeassistant/components/unifiled/* @florisvdk +homeassistant/components/unifiprotect/* @briis @AngellusMortis @bdraco +tests/components/unifiprotect/* @briis @AngellusMortis @bdraco homeassistant/components/upb/* @gwww tests/components/upb/* @gwww homeassistant/components/upc_connect/* @pvizeli @fabaff diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py new file mode 100644 index 00000000000..84cef044c6b --- /dev/null +++ b/homeassistant/components/unifiprotect/__init__.py @@ -0,0 +1,106 @@ +"""UniFi Protect Platform.""" +from __future__ import annotations + +import asyncio +from datetime import timedelta +import logging + +from aiohttp import CookieJar +from aiohttp.client_exceptions import ServerDisconnectedError +from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + CONF_VERIFY_SSL, + EVENT_HOMEASSISTANT_STOP, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_create_clientsession + +from .const import ( + CONF_ALL_UPDATES, + CONF_OVERRIDE_CHOST, + DEFAULT_SCAN_INTERVAL, + DEVICES_FOR_SUBSCRIBE, + DOMAIN, + MIN_REQUIRED_PROTECT_V, + PLATFORMS, +) +from .data import ProtectData + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(seconds=DEFAULT_SCAN_INTERVAL) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up the UniFi Protect config entries.""" + + session = async_create_clientsession(hass, cookie_jar=CookieJar(unsafe=True)) + protect = ProtectApiClient( + host=entry.data[CONF_HOST], + port=entry.data[CONF_PORT], + username=entry.data[CONF_USERNAME], + password=entry.data[CONF_PASSWORD], + verify_ssl=entry.data[CONF_VERIFY_SSL], + session=session, + subscribed_models=DEVICES_FOR_SUBSCRIBE, + override_connection_host=entry.options.get(CONF_OVERRIDE_CHOST, False), + ignore_stats=not entry.options.get(CONF_ALL_UPDATES, False), + ) + _LOGGER.debug("Connect to UniFi Protect") + data_service = ProtectData(hass, protect, SCAN_INTERVAL, entry) + + try: + nvr_info = await protect.get_nvr() + except NotAuthorized as err: + raise ConfigEntryAuthFailed(err) from err + except (asyncio.TimeoutError, NvrError, ServerDisconnectedError) as notreadyerror: + raise ConfigEntryNotReady from notreadyerror + + if nvr_info.version < MIN_REQUIRED_PROTECT_V: + _LOGGER.error( + ( + "You are running v%s of UniFi Protect. Minimum required version is v%s. " + "Please upgrade UniFi Protect and then retry" + ), + nvr_info.version, + MIN_REQUIRED_PROTECT_V, + ) + return False + + if entry.unique_id is None: + hass.config_entries.async_update_entry(entry, unique_id=nvr_info.mac) + + await data_service.async_setup() + if not data_service.last_update_success: + raise ConfigEntryNotReady + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = data_service + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + entry.async_on_unload(entry.add_update_listener(_async_options_updated)) + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, data_service.async_stop) + ) + + return True + + +async def _async_options_updated(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Update options.""" + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload UniFi Protect config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + data: ProtectData = hass.data[DOMAIN][entry.entry_id] + await data.async_stop() + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok diff --git a/homeassistant/components/unifiprotect/camera.py b/homeassistant/components/unifiprotect/camera.py new file mode 100644 index 00000000000..2aeaefd322a --- /dev/null +++ b/homeassistant/components/unifiprotect/camera.py @@ -0,0 +1,169 @@ +"""Support for Ubiquiti's UniFi Protect NVR.""" +from __future__ import annotations + +from collections.abc import Callable, Generator, Sequence +import logging +from typing import Any + +from pyunifiprotect.api import ProtectApiClient +from pyunifiprotect.data import Camera as UFPCamera +from pyunifiprotect.data.devices import CameraChannel + +from homeassistant.components.camera import SUPPORT_STREAM, Camera +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import Entity + +from .const import ( + ATTR_BITRATE, + ATTR_CHANNEL_ID, + ATTR_FPS, + ATTR_HEIGHT, + ATTR_WIDTH, + DOMAIN, +) +from .data import ProtectData +from .entity import ProtectDeviceEntity + +_LOGGER = logging.getLogger(__name__) + + +def get_camera_channels( + protect: ProtectApiClient, +) -> Generator[tuple[UFPCamera, CameraChannel, bool], None, None]: + """Get all the camera channels.""" + for camera in protect.bootstrap.cameras.values(): + if len(camera.channels) == 0: + _LOGGER.warning( + "Camera does not have any channels: %s (id: %s)", camera.name, camera.id + ) + continue + + is_default = True + for channel in camera.channels: + if channel.is_rtsp_enabled: + yield camera, channel, is_default + is_default = False + + # no RTSP enabled use first channel with no stream + if is_default: + yield camera, camera.channels[0], True + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[Sequence[Entity]], None], +) -> None: + """Discover cameras on a UniFi Protect NVR.""" + data: ProtectData = hass.data[DOMAIN][entry.entry_id] + disable_stream = data.disable_stream + + entities = [] + for camera, channel, is_default in get_camera_channels(data.api): + entities.append( + ProtectCamera( + data, + camera, + channel, + is_default, + True, + disable_stream, + ) + ) + + if channel.is_rtsp_enabled: + entities.append( + ProtectCamera( + data, + camera, + channel, + is_default, + False, + disable_stream, + ) + ) + async_add_entities(entities) + + +class ProtectCamera(ProtectDeviceEntity, Camera): + """A Ubiquiti UniFi Protect Camera.""" + + def __init__( + self, + data: ProtectData, + camera: UFPCamera, + channel: CameraChannel, + is_default: bool, + secure: bool, + disable_stream: bool, + ) -> None: + """Initialize an UniFi camera.""" + self.device: UFPCamera = camera + self.channel = channel + self._secure = secure + self._disable_stream = disable_stream + self._last_image: bytes | None = None + super().__init__(data) + + if self._secure: + self._attr_unique_id = f"{self.device.id}_{self.channel.id}" + self._attr_name = f"{self.device.name} {self.channel.name}" + else: + self._attr_unique_id = f"{self.device.id}_{self.channel.id}_insecure" + self._attr_name = f"{self.device.name} {self.channel.name} Insecure" + # only the default (first) channel is enabled by default + self._attr_entity_registry_enabled_default = is_default and secure + + @callback + def _async_set_stream_source(self) -> None: + disable_stream = self._disable_stream + if not self.channel.is_rtsp_enabled: + disable_stream = False + + rtsp_url = self.channel.rtsp_url + if self._secure: + rtsp_url = self.channel.rtsps_url + + # _async_set_stream_source called by __init__ + self._stream_source = ( # pylint: disable=attribute-defined-outside-init + None if disable_stream else rtsp_url + ) + self._attr_supported_features: int = ( + SUPPORT_STREAM if self._stream_source else 0 + ) + + @callback + def _async_update_device_from_protect(self) -> None: + super()._async_update_device_from_protect() + self.channel = self.device.channels[self.channel.id] + self._attr_motion_detection_enabled = ( + self.device.is_connected and self.device.feature_flags.has_motion_zones + ) + self._attr_is_recording = self.device.is_connected and self.device.is_recording + + self._async_set_stream_source() + + @callback + def _async_update_extra_attrs_from_protect(self) -> dict[str, Any]: + """Add additional Attributes to Camera.""" + return { + **super()._async_update_extra_attrs_from_protect(), + ATTR_WIDTH: self.channel.width, + ATTR_HEIGHT: self.channel.height, + ATTR_FPS: self.channel.fps, + ATTR_BITRATE: self.channel.bitrate, + ATTR_CHANNEL_ID: self.channel.id, + } + + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: + """Return the Camera Image.""" + last_image = await self.device.get_snapshot(width, height) + self._last_image = last_image + return self._last_image + + async def stream_source(self) -> str | None: + """Return the Stream Source.""" + return self._stream_source diff --git a/homeassistant/components/unifiprotect/config_flow.py b/homeassistant/components/unifiprotect/config_flow.py new file mode 100644 index 00000000000..805318a9af5 --- /dev/null +++ b/homeassistant/components/unifiprotect/config_flow.py @@ -0,0 +1,218 @@ +"""Config Flow to configure UniFi Protect Integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from aiohttp import CookieJar +from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient +from pyunifiprotect.data.nvr import NVR +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import ( + CONF_HOST, + CONF_ID, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_create_clientsession + +from .const import ( + CONF_ALL_UPDATES, + CONF_DISABLE_RTSP, + CONF_OVERRIDE_CHOST, + DEFAULT_PORT, + DEFAULT_VERIFY_SSL, + DOMAIN, + MIN_REQUIRED_PROTECT_V, +) + +_LOGGER = logging.getLogger(__name__) + + +class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a UniFi Protect config flow.""" + + VERSION = 2 + + def __init__(self) -> None: + """Init the config flow.""" + super().__init__() + + self.entry: config_entries.ConfigEntry | None = None + + @staticmethod + @callback + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> config_entries.OptionsFlow: + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) + + @callback + async def _async_create_entry(self, title: str, data: dict[str, Any]) -> FlowResult: + return self.async_create_entry( + title=title, + data={**data, CONF_ID: title}, + options={ + CONF_DISABLE_RTSP: False, + CONF_ALL_UPDATES: False, + CONF_OVERRIDE_CHOST: False, + }, + ) + + @callback + async def _async_get_nvr_data( + self, + user_input: dict[str, Any], + ) -> tuple[NVR | None, dict[str, str]]: + session = async_create_clientsession( + self.hass, cookie_jar=CookieJar(unsafe=True) + ) + + host = user_input[CONF_HOST] + port = user_input.get(CONF_PORT, DEFAULT_PORT) + verify_ssl = user_input.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL) + + protect = ProtectApiClient( + session=session, + host=host, + port=port, + username=user_input[CONF_USERNAME], + password=user_input[CONF_PASSWORD], + verify_ssl=verify_ssl, + ) + + errors = {} + nvr_data = None + try: + nvr_data = await protect.get_nvr() + except NotAuthorized as ex: + _LOGGER.debug(ex) + errors[CONF_PASSWORD] = "invalid_auth" + except NvrError as ex: + _LOGGER.debug(ex) + errors["base"] = "nvr_error" + else: + if nvr_data.version < MIN_REQUIRED_PROTECT_V: + _LOGGER.debug("UniFi Protect Version not supported") + errors["base"] = "protect_version" + + return nvr_data, errors + + async def async_step_reauth(self, user_input: dict[str, Any]) -> FlowResult: + """Perform reauth upon an API authentication error.""" + + self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm reauth.""" + errors: dict[str, str] = {} + assert self.entry is not None + + # prepopulate fields + form_data = {**self.entry.data} + if user_input is not None: + form_data.update(user_input) + + # validate login data + _, errors = await self._async_get_nvr_data(form_data) + if not errors: + self.hass.config_entries.async_update_entry(self.entry, data=form_data) + await self.hass.config_entries.async_reload(self.entry.entry_id) + return self.async_abort(reason="reauth_successful") + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema( + { + vol.Required( + CONF_USERNAME, default=form_data.get(CONF_USERNAME) + ): str, + vol.Required(CONF_PASSWORD): str, + } + ), + errors=errors, + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initiated by the user.""" + + errors: dict[str, str] = {} + if user_input is not None: + nvr_data, errors = await self._async_get_nvr_data(user_input) + + if nvr_data and not errors: + await self.async_set_unique_id(nvr_data.mac) + self._abort_if_unique_id_configured() + + return await self._async_create_entry(nvr_data.name, user_input) + + user_input = user_input or {} + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_HOST, default=user_input.get(CONF_HOST)): str, + vol.Required( + CONF_PORT, default=user_input.get(CONF_PORT, DEFAULT_PORT) + ): int, + vol.Required( + CONF_VERIFY_SSL, + default=user_input.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL), + ): bool, + vol.Required( + CONF_USERNAME, default=user_input.get(CONF_USERNAME) + ): str, + vol.Required(CONF_PASSWORD): str, + } + ), + errors=errors, + ) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle options.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage the options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional( + CONF_DISABLE_RTSP, + default=self.config_entry.options.get(CONF_DISABLE_RTSP, False), + ): bool, + vol.Optional( + CONF_ALL_UPDATES, + default=self.config_entry.options.get(CONF_ALL_UPDATES, False), + ): bool, + vol.Optional( + CONF_OVERRIDE_CHOST, + default=self.config_entry.options.get( + CONF_OVERRIDE_CHOST, False + ), + ): bool, + } + ), + ) diff --git a/homeassistant/components/unifiprotect/const.py b/homeassistant/components/unifiprotect/const.py new file mode 100644 index 00000000000..049cb5cc0ac --- /dev/null +++ b/homeassistant/components/unifiprotect/const.py @@ -0,0 +1,54 @@ +"""Constant definitions for UniFi Protect Integration.""" + +# from typing_extensions import Required +from datetime import timedelta + +from pyunifiprotect.data.types import ModelType, Version + +DOMAIN = "unifiprotect" + +ATTR_EVENT_SCORE = "event_score" +ATTR_EVENT_OBJECT = "event_object" +ATTR_EVENT_THUMB = "event_thumbnail" +ATTR_WIDTH = "width" +ATTR_HEIGHT = "height" +ATTR_FPS = "fps" +ATTR_BITRATE = "bitrate" +ATTR_CHANNEL_ID = "channel_id" + +CONF_DOORBELL_TEXT = "doorbell_text" +CONF_DISABLE_RTSP = "disable_rtsp" +CONF_MESSAGE = "message" +CONF_DURATION = "duration" +CONF_ALL_UPDATES = "all_updates" +CONF_OVERRIDE_CHOST = "override_connection_host" + +CONFIG_OPTIONS = [ + CONF_ALL_UPDATES, + CONF_DISABLE_RTSP, + CONF_OVERRIDE_CHOST, +] + +DEFAULT_PORT = 443 +DEFAULT_ATTRIBUTION = "Powered by UniFi Protect Server" +DEFAULT_BRAND = "Ubiquiti" +DEFAULT_SCAN_INTERVAL = 2 +DEFAULT_VERIFY_SSL = False + +RING_INTERVAL = timedelta(seconds=3) + +DEVICE_TYPE_CAMERA = "camera" +DEVICES_THAT_ADOPT = { + ModelType.CAMERA, + ModelType.LIGHT, + ModelType.VIEWPORT, + ModelType.SENSOR, +} +DEVICES_WITH_ENTITIES = DEVICES_THAT_ADOPT | {ModelType.NVR} +DEVICES_FOR_SUBSCRIBE = DEVICES_WITH_ENTITIES | {ModelType.EVENT} + +MIN_REQUIRED_PROTECT_V = Version("1.20.0") + +PLATFORMS = [ + "camera", +] diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py new file mode 100644 index 00000000000..9004357b683 --- /dev/null +++ b/homeassistant/components/unifiprotect/data.py @@ -0,0 +1,148 @@ +"""Base class for protect data.""" +from __future__ import annotations + +import collections +from datetime import timedelta +import logging +from typing import Any + +from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient +from pyunifiprotect.data import Bootstrap, WSSubscriptionMessage +from pyunifiprotect.data.base import ProtectDeviceModel + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.helpers.event import async_track_time_interval + +from .const import CONF_DISABLE_RTSP, DEVICES_THAT_ADOPT, DEVICES_WITH_ENTITIES + +_LOGGER = logging.getLogger(__name__) + + +class ProtectData: + """Coordinate updates.""" + + def __init__( + self, + hass: HomeAssistant, + protect: ProtectApiClient, + update_interval: timedelta, + entry: ConfigEntry, + ) -> None: + """Initialize an subscriber.""" + super().__init__() + + self._hass = hass + self._entry = entry + self._hass = hass + self._update_interval = update_interval + self._subscriptions: dict[str, list[CALLBACK_TYPE]] = {} + self._unsub_interval: CALLBACK_TYPE | None = None + self._unsub_websocket: CALLBACK_TYPE | None = None + + self.last_update_success = False + self.access_tokens: dict[str, collections.deque] = {} + self.api = protect + + @property + def disable_stream(self) -> bool: + """Check if RTSP is disabled.""" + return self._entry.options.get(CONF_DISABLE_RTSP, False) + + async def async_setup(self) -> None: + """Subscribe and do the refresh.""" + self._unsub_websocket = self.api.subscribe_websocket( + self._async_process_ws_message + ) + await self.async_refresh() + + async def async_stop(self, *args: Any) -> None: + """Stop processing data.""" + if self._unsub_websocket: + self._unsub_websocket() + self._unsub_websocket = None + if self._unsub_interval: + self._unsub_interval() + self._unsub_interval = None + await self.api.async_disconnect_ws() + + async def async_refresh(self, *_: Any, force: bool = False) -> None: + """Update the data.""" + + # if last update was failure, force until success + if not self.last_update_success: + force = True + + try: + updates = await self.api.update(force=force) + except NvrError: + if self.last_update_success: + _LOGGER.exception("Error while updating") + self.last_update_success = False + # manually trigger update to mark entities unavailable + self._async_process_updates(self.api.bootstrap) + except NotAuthorized: + await self.async_stop() + _LOGGER.exception("Reauthentication required") + self._entry.async_start_reauth(self._hass) + self.last_update_success = False + else: + self.last_update_success = True + self._async_process_updates(updates) + + @callback + def _async_process_ws_message(self, message: WSSubscriptionMessage) -> None: + if message.new_obj.model in DEVICES_WITH_ENTITIES: + self.async_signal_device_id_update(message.new_obj.id) + + @callback + def _async_process_updates(self, updates: Bootstrap | None) -> None: + """Process update from the protect data.""" + + # Websocket connected, use data from it + if updates is None: + return + + self.async_signal_device_id_update(self.api.bootstrap.nvr.id) + for device_type in DEVICES_THAT_ADOPT: + attr = f"{device_type.value}s" + devices: dict[str, ProtectDeviceModel] = getattr(self.api.bootstrap, attr) + for device_id in devices.keys(): + self.async_signal_device_id_update(device_id) + + @callback + def async_subscribe_device_id( + self, device_id: str, update_callback: CALLBACK_TYPE + ) -> CALLBACK_TYPE: + """Add an callback subscriber.""" + if not self._subscriptions: + self._unsub_interval = async_track_time_interval( + self._hass, self.async_refresh, self._update_interval + ) + self._subscriptions.setdefault(device_id, []).append(update_callback) + + def _unsubscribe() -> None: + self.async_unsubscribe_device_id(device_id, update_callback) + + return _unsubscribe + + @callback + def async_unsubscribe_device_id( + self, device_id: str, update_callback: CALLBACK_TYPE + ) -> None: + """Remove a callback subscriber.""" + self._subscriptions[device_id].remove(update_callback) + if not self._subscriptions[device_id]: + del self._subscriptions[device_id] + if not self._subscriptions and self._unsub_interval: + self._unsub_interval() + self._unsub_interval = None + + @callback + def async_signal_device_id_update(self, device_id: str) -> None: + """Call the callbacks for a device_id.""" + if not self._subscriptions.get(device_id): + return + + for update_callback in self._subscriptions[device_id]: + update_callback() diff --git a/homeassistant/components/unifiprotect/entity.py b/homeassistant/components/unifiprotect/entity.py new file mode 100644 index 00000000000..61e9f6225b6 --- /dev/null +++ b/homeassistant/components/unifiprotect/entity.py @@ -0,0 +1,113 @@ +"""Shared Entity definition for UniFi Protect Integration.""" +from __future__ import annotations + +from typing import Any + +from pyunifiprotect.data import ProtectAdoptableDeviceModel + +from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.core import callback +import homeassistant.helpers.device_registry as dr +from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription + +from .const import DEFAULT_ATTRIBUTION, DEFAULT_BRAND, DOMAIN +from .data import ProtectData + + +class ProtectDeviceEntity(Entity): + """Base class for UniFi protect entities.""" + + _attr_should_poll = False + + def __init__( + self, + data: ProtectData, + device: ProtectAdoptableDeviceModel | None = None, + description: EntityDescription | None = None, + ) -> None: + """Initialize the entity.""" + super().__init__() + self.data: ProtectData = data + + if device and not hasattr(self, "device"): + self.device: ProtectAdoptableDeviceModel = device + + if description and not hasattr(self, "entity_description"): + self.entity_description = description + elif hasattr(self, "entity_description"): + description = self.entity_description + + if description is None: + self._attr_unique_id = f"{self.device.id}" + self._attr_name = f"{self.device.name}" + else: + self._attr_unique_id = f"{self.device.id}_{description.key}" + name = description.name or "" + self._attr_name = f"{self.device.name} {name.title()}" + + self._async_set_device_info() + self._async_update_device_from_protect() + + async def async_update(self) -> None: + """Update the entity. + + Only used by the generic entity update service. + """ + await self.data.async_refresh() + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return UniFi Protect device attributes.""" + attrs = super().extra_state_attributes or {} + return { + **attrs, + ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION, + **self._extra_state_attributes, + } + + @callback + def _async_set_device_info(self) -> None: + self._attr_device_info = DeviceInfo( + name=self.device.name, + manufacturer=DEFAULT_BRAND, + model=self.device.type, + via_device=(DOMAIN, self.data.api.bootstrap.nvr.mac), + sw_version=self.device.firmware_version, + connections={(dr.CONNECTION_NETWORK_MAC, self.device.mac)}, + configuration_url=self.device.protect_url, + ) + + @callback + def _async_update_extra_attrs_from_protect( # pylint: disable=no-self-use + self, + ) -> dict[str, Any]: + """Calculate extra state attributes. Primarily for subclass to override.""" + return {} + + @callback + def _async_update_device_from_protect(self) -> None: + """Update Entity object from Protect device.""" + if self.data.last_update_success: + assert self.device.model + devices = getattr(self.data.api.bootstrap, f"{self.device.model.value}s") + self.device = devices[self.device.id] + + self._attr_available = ( + self.data.last_update_success and self.device.is_connected + ) + self._extra_state_attributes = self._async_update_extra_attrs_from_protect() + + @callback + def _async_updated_event(self) -> None: + """Call back for incoming data.""" + self._async_update_device_from_protect() + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + self.async_on_remove( + self.data.async_subscribe_device_id( + self.device.id, self._async_updated_event + ) + ) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json new file mode 100644 index 00000000000..41d698b61e2 --- /dev/null +++ b/homeassistant/components/unifiprotect/manifest.json @@ -0,0 +1,16 @@ +{ + "domain": "unifiprotect", + "name": "UniFi Protect", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/unifiprotect", + "requirements": [ + "pyunifiprotect==1.4.4" + ], + "codeowners": [ + "@briis", + "@AngellusMortis", + "@bdraco" + ], + "quality_scale": "platinum", + "iot_class": "local_push" +} diff --git a/homeassistant/components/unifiprotect/strings.json b/homeassistant/components/unifiprotect/strings.json new file mode 100644 index 00000000000..4dd000aade8 --- /dev/null +++ b/homeassistant/components/unifiprotect/strings.json @@ -0,0 +1,34 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "options": { + "step": { + "init": { + "title": "UniFi Protect Options", + "description": "Realtime metrics option should only be enabled if you have enabled the diagnostics sensors and want them updated in realtime. If if not enabled, they will only update once every 15 minutes.", + "data": { + "disable_rtsp": "Disable the RTSP stream", + "all_updates": "Realtime metrics (WARNING: Greatly increases CPU usage)", + "override_connection_host": "Override Connection Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifiprotect/translations/en.json b/homeassistant/components/unifiprotect/translations/en.json new file mode 100644 index 00000000000..d62fcd651ec --- /dev/null +++ b/homeassistant/components/unifiprotect/translations/en.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password", + "username": "Username" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "all_updates": "Realtime metrics (WARNING: Greatly increases CPU usage)", + "disable_rtsp": "Disable the RTSP stream", + "override_connection_host": "Override Connection Host" + }, + "description": "Realtime metrics option should only be enabled if you have enabled the diagnostics sensors and want them updated in realtime. If if not enabled, they will only update once every 15 minutes.", + "title": "UniFi Protect Options" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index fa828cc6368..b2888d7d8b4 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -323,6 +323,7 @@ FLOWS = [ "twilio", "twinkly", "unifi", + "unifiprotect", "upb", "upcloud", "upnp", diff --git a/mypy.ini b/mypy.ini index 6ccb0b8106c..4cfa08ca38a 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1584,6 +1584,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.unifiprotect.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.upcloud.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 65228686105..f06376a0b5f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1999,6 +1999,9 @@ pytrafikverket==0.1.6.2 # homeassistant.components.usb pyudev==0.22.0 +# homeassistant.components.unifiprotect +pyunifiprotect==1.4.4 + # homeassistant.components.uptimerobot pyuptimerobot==21.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 20ef660a159..1b25c05b4be 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1206,6 +1206,9 @@ pytrafikverket==0.1.6.2 # homeassistant.components.usb pyudev==0.22.0 +# homeassistant.components.unifiprotect +pyunifiprotect==1.4.4 + # homeassistant.components.uptimerobot pyuptimerobot==21.11.0 diff --git a/tests/components/unifiprotect/__init__.py b/tests/components/unifiprotect/__init__.py new file mode 100644 index 00000000000..64e0e31005c --- /dev/null +++ b/tests/components/unifiprotect/__init__.py @@ -0,0 +1 @@ +"""Tests for the UniFi Protect integration.""" diff --git a/tests/components/unifiprotect/conftest.py b/tests/components/unifiprotect/conftest.py new file mode 100644 index 00000000000..ba0dcabd6bc --- /dev/null +++ b/tests/components/unifiprotect/conftest.py @@ -0,0 +1,168 @@ +"""Fixtures and test data for UniFi Protect methods.""" +from __future__ import annotations + +from dataclasses import dataclass +from datetime import timedelta +from ipaddress import IPv4Address +import json +from pathlib import Path +from typing import Any, Callable +from unittest.mock import AsyncMock, Mock, patch + +import pytest +from pyunifiprotect.data import Camera, Version +from pyunifiprotect.data.websocket import WSSubscriptionMessage + +from homeassistant.components.unifiprotect.const import DOMAIN, MIN_REQUIRED_PROTECT_V +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +import homeassistant.util.dt as dt_util + +from tests.common import MockConfigEntry, async_fire_time_changed + +MAC_ADDR = "aa:bb:cc:dd:ee:ff" + + +@dataclass +class MockPortData: + """Mock Port information.""" + + rtsp: int = 7441 + rtsps: int = 7447 + + +@dataclass +class MockNvrData: + """Mock for NVR.""" + + version: Version + mac: str + name: str + id: str + ports: MockPortData = MockPortData() + + +@dataclass +class MockBootstrap: + """Mock for Bootstrap.""" + + nvr: MockNvrData + cameras: dict[str, Any] + lights: dict[str, Any] + sensors: dict[str, Any] + viewers: dict[str, Any] + + +@dataclass +class MockEntityFixture: + """Mock for NVR.""" + + entry: MockConfigEntry + api: Mock + + +MOCK_NVR_DATA = MockNvrData( + version=MIN_REQUIRED_PROTECT_V, mac=MAC_ADDR, name="UnifiProtect", id="test_id" +) +MOCK_OLD_NVR_DATA = MockNvrData( + version=Version("1.19.0"), mac=MAC_ADDR, name="UnifiProtect", id="test_id" +) + +MOCK_BOOTSTRAP = MockBootstrap( + nvr=MOCK_NVR_DATA, cameras={}, lights={}, sensors={}, viewers={} +) + + +@pytest.fixture +def mock_client(): + """Mock ProtectApiClient for testing.""" + client = Mock() + client.bootstrap = MOCK_BOOTSTRAP + + client.base_url = "https://127.0.0.1" + client.connection_host = IPv4Address("127.0.0.1") + client.get_nvr = AsyncMock(return_value=MOCK_NVR_DATA) + client.update = AsyncMock(return_value=MOCK_BOOTSTRAP) + client.async_disconnect_ws = AsyncMock() + + def subscribe(ws_callback: Callable[[WSSubscriptionMessage], None]) -> Any: + client.ws_subscription = ws_callback + + return Mock() + + client.subscribe_websocket = subscribe + return client + + +@pytest.fixture +def mock_entry(hass: HomeAssistant, mock_client): + """Mock ProtectApiClient for testing.""" + + with patch("homeassistant.components.unifiprotect.ProtectApiClient") as mock_api: + mock_config = MockConfigEntry( + domain=DOMAIN, + data={ + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + "id": "UnifiProtect", + "port": 443, + "verify_ssl": False, + }, + version=2, + ) + mock_config.add_to_hass(hass) + + mock_api.return_value = mock_client + + yield MockEntityFixture(mock_config, mock_client) + + +@pytest.fixture +def mock_camera(): + """Mock UniFi Protect Camera device.""" + + path = Path(__file__).parent / "sample_data" / "sample_camera.json" + with open(path, encoding="utf-8") as f: + data = json.load(f) + + yield Camera.from_unifi_dict(**data) + + +@pytest.fixture +async def simple_camera( + hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: Camera +): + """Fixture for a single camera, no extra setup.""" + + camera = mock_camera.copy(deep=True) + camera._api = mock_entry.api + camera.channels[0]._api = mock_entry.api + camera.channels[1]._api = mock_entry.api + camera.channels[2]._api = mock_entry.api + camera.name = "Test Camera" + camera.channels[0].is_rtsp_enabled = True + camera.channels[0].name = "High" + camera.channels[1].is_rtsp_enabled = False + camera.channels[2].is_rtsp_enabled = False + + mock_entry.api.bootstrap.cameras = { + camera.id: camera, + } + + await hass.config_entries.async_setup(mock_entry.entry.entry_id) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + + assert len(hass.states.async_all()) == 1 + assert len(entity_registry.entities) == 2 + + yield (camera, "camera.test_camera_high") + + +async def time_changed(hass, seconds): + """Trigger time changed.""" + next_update = dt_util.utcnow() + timedelta(seconds) + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() diff --git a/tests/components/unifiprotect/sample_data/sample_camera.json b/tests/components/unifiprotect/sample_data/sample_camera.json new file mode 100644 index 00000000000..7cc660f428b --- /dev/null +++ b/tests/components/unifiprotect/sample_data/sample_camera.json @@ -0,0 +1,482 @@ +{ + "isDeleting": false, + "mac": "72C7836A47DC", + "host": "192.168.6.90", + "connectionHost": "192.168.178.217", + "type": "UVC G4 Instant", + "name": "Fufail Qqjx", + "upSince": 1640020678036, + "uptime": 3203, + "lastSeen": 1640023881036, + "connectedSince": 1640020710448, + "state": "CONNECTED", + "hardwareRevision": "11", + "firmwareVersion": "4.47.13", + "latestFirmwareVersion": "4.47.13", + "firmwareBuild": "0a55423.211124.718", + "isUpdating": false, + "isAdopting": false, + "isAdopted": true, + "isAdoptedByOther": false, + "isProvisioned": true, + "isRebooting": false, + "isSshEnabled": true, + "canAdopt": false, + "isAttemptingToConnect": false, + "lastMotion": 1640021213927, + "micVolume": 100, + "isMicEnabled": true, + "isRecording": false, + "isWirelessUplinkEnabled": true, + "isMotionDetected": false, + "isSmartDetected": false, + "phyRate": 72, + "hdrMode": true, + "videoMode": "default", + "isProbingForWifi": false, + "apMac": null, + "apRssi": null, + "elementInfo": null, + "chimeDuration": 0, + "isDark": false, + "lastPrivacyZonePositionId": null, + "lastRing": null, + "isLiveHeatmapEnabled": false, + "anonymousDeviceId": "7722b5e7-ecfa-468c-a385-3eafea917b0c", + "eventStats": { + "motion": { + "today": 10, + "average": 39, + "lastDays": [ + 48, + 45, + 33, + 41, + 44, + 60, + 6 + ], + "recentHours": [ + 0, + 4, + 1, + 2, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 0, + 0 + ] + }, + "smart": { + "today": 0, + "average": 0, + "lastDays": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + } + }, + "videoReconfigurationInProgress": false, + "voltage": null, + "wiredConnectionState": { + "phyRate": null + }, + "channels": [ + { + "id": 0, + "videoId": "video1", + "name": "Jzi Bftu", + "enabled": true, + "isRtspEnabled": true, + "rtspAlias": "ANOAPfoKMW7VixG1", + "width": 2688, + "height": 1512, + "fps": 30, + "bitrate": 10000000, + "minBitrate": 32000, + "maxBitrate": 10000000, + "minClientAdaptiveBitRate": 0, + "minMotionAdaptiveBitRate": 2000000, + "fpsValues": [ + 1, + 2, + 3, + 4, + 5, + 6, + 8, + 9, + 10, + 12, + 15, + 16, + 18, + 20, + 24, + 25, + 30 + ], + "idrInterval": 5 + }, + { + "id": 1, + "videoId": "video2", + "name": "Rgcpxsf Xfwt", + "enabled": true, + "isRtspEnabled": true, + "rtspAlias": "XHXAdHVKGVEzMNTP", + "width": 1280, + "height": 720, + "fps": 30, + "bitrate": 1500000, + "minBitrate": 32000, + "maxBitrate": 2000000, + "minClientAdaptiveBitRate": 150000, + "minMotionAdaptiveBitRate": 750000, + "fpsValues": [ + 1, + 2, + 3, + 4, + 5, + 6, + 8, + 9, + 10, + 12, + 15, + 16, + 18, + 20, + 24, + 25, + 30 + ], + "idrInterval": 5 + }, + { + "id": 2, + "videoId": "video3", + "name": "Umefvk Fug", + "enabled": true, + "isRtspEnabled": false, + "rtspAlias": null, + "width": 640, + "height": 360, + "fps": 30, + "bitrate": 200000, + "minBitrate": 32000, + "maxBitrate": 1000000, + "minClientAdaptiveBitRate": 0, + "minMotionAdaptiveBitRate": 200000, + "fpsValues": [ + 1, + 2, + 3, + 4, + 5, + 6, + 8, + 9, + 10, + 12, + 15, + 16, + 18, + 20, + 24, + 25, + 30 + ], + "idrInterval": 5 + } + ], + "ispSettings": { + "aeMode": "auto", + "irLedMode": "auto", + "irLedLevel": 255, + "wdr": 1, + "icrSensitivity": 0, + "brightness": 50, + "contrast": 50, + "hue": 50, + "saturation": 50, + "sharpness": 50, + "denoise": 50, + "isFlippedVertical": false, + "isFlippedHorizontal": false, + "isAutoRotateEnabled": true, + "isLdcEnabled": true, + "is3dnrEnabled": true, + "isExternalIrEnabled": false, + "isAggressiveAntiFlickerEnabled": false, + "isPauseMotionEnabled": false, + "dZoomCenterX": 50, + "dZoomCenterY": 50, + "dZoomScale": 0, + "dZoomStreamId": 4, + "focusMode": "ztrig", + "focusPosition": 0, + "touchFocusX": 1001, + "touchFocusY": 1001, + "zoomPosition": 0, + "mountPosition": "wall" + }, + "talkbackSettings": { + "typeFmt": "aac", + "typeIn": "serverudp", + "bindAddr": "0.0.0.0", + "bindPort": 7004, + "filterAddr": "", + "filterPort": 0, + "channels": 1, + "samplingRate": 22050, + "bitsPerSample": 16, + "quality": 100 + }, + "osdSettings": { + "isNameEnabled": true, + "isDateEnabled": true, + "isLogoEnabled": false, + "isDebugEnabled": false + }, + "ledSettings": { + "isEnabled": false, + "blinkRate": 0 + }, + "speakerSettings": { + "isEnabled": true, + "areSystemSoundsEnabled": false, + "volume": 100 + }, + "recordingSettings": { + "prePaddingSecs": 10, + "postPaddingSecs": 10, + "minMotionEventTrigger": 1000, + "endMotionEventDelay": 3000, + "suppressIlluminationSurge": false, + "mode": "detections", + "geofencing": "off", + "motionAlgorithm": "enhanced", + "enablePirTimelapse": false, + "useNewMotionAlgorithm": true + }, + "smartDetectSettings": { + "objectTypes": [] + }, + "recordingSchedules": [], + "motionZones": [ + { + "id": 1, + "name": "Default", + "color": "#AB46BC", + "points": [ + [ + 0, + 0 + ], + [ + 1, + 0 + ], + [ + 1, + 1 + ], + [ + 0, + 1 + ] + ], + "sensitivity": 50 + } + ], + "privacyZones": [], + "smartDetectZones": [ + { + "id": 1, + "name": "Default", + "color": "#AB46BC", + "points": [ + [ + 0, + 0 + ], + [ + 1, + 0 + ], + [ + 1, + 1 + ], + [ + 0, + 1 + ] + ], + "sensitivity": 50, + "objectTypes": [] + } + ], + "smartDetectLines": [], + "stats": { + "rxBytes": 33684237, + "txBytes": 1208318620, + "wifi": { + "channel": 6, + "frequency": 2437, + "linkSpeedMbps": null, + "signalQuality": 100, + "signalStrength": -35 + }, + "battery": { + "percentage": null, + "isCharging": false, + "sleepState": "disconnected" + }, + "video": { + "recordingStart": 1639219284079, + "recordingEnd": 1640021215245, + "recordingStartLQ": 1639219283987, + "recordingEndLQ": 1640021217213, + "timelapseStart": 1639219284030, + "timelapseEnd": 1640023738713, + "timelapseStartLQ": 1639219284030, + "timelapseEndLQ": 1640021765237 + }, + "storage": { + "used": 20401094656, + "rate": 693.424269097809 + }, + "wifiQuality": 100, + "wifiStrength": -35 + }, + "featureFlags": { + "canAdjustIrLedLevel": false, + "canMagicZoom": false, + "canOpticalZoom": false, + "canTouchFocus": false, + "hasAccelerometer": true, + "hasAec": true, + "hasBattery": false, + "hasBluetooth": true, + "hasChime": false, + "hasExternalIr": false, + "hasIcrSensitivity": true, + "hasLdc": false, + "hasLedIr": true, + "hasLedStatus": true, + "hasLineIn": false, + "hasMic": true, + "hasPrivacyMask": true, + "hasRtc": false, + "hasSdCard": false, + "hasSpeaker": true, + "hasWifi": true, + "hasHdr": true, + "hasAutoICROnly": true, + "videoModes": [ + "default" + ], + "videoModeMaxFps": [], + "hasMotionZones": true, + "hasLcdScreen": false, + "mountPositions": [], + "smartDetectTypes": [], + "motionAlgorithms": [ + "enhanced" + ], + "hasSquareEventThumbnail": true, + "hasPackageCamera": false, + "privacyMaskCapability": { + "maxMasks": 4, + "rectangleOnly": true + }, + "focus": { + "steps": { + "max": null, + "min": null, + "step": null + }, + "degrees": { + "max": null, + "min": null, + "step": null + } + }, + "pan": { + "steps": { + "max": null, + "min": null, + "step": null + }, + "degrees": { + "max": null, + "min": null, + "step": null + } + }, + "tilt": { + "steps": { + "max": null, + "min": null, + "step": null + }, + "degrees": { + "max": null, + "min": null, + "step": null + } + }, + "zoom": { + "steps": { + "max": null, + "min": null, + "step": null + }, + "degrees": { + "max": null, + "min": null, + "step": null + } + }, + "hasSmartDetect": false + }, + "pirSettings": { + "pirSensitivity": 100, + "pirMotionClipLength": 15, + "timelapseFrameInterval": 15, + "timelapseTransferInterval": 600 + }, + "lcdMessage": {}, + "wifiConnectionState": { + "channel": 6, + "frequency": 2437, + "phyRate": 72, + "signalQuality": 100, + "signalStrength": -35, + "ssid": "Mortis Camera" + }, + "lenses": [], + "id": "0de062b4f6922d489d3b312d", + "isConnected": true, + "platform": "sav530q", + "hasSpeaker": true, + "hasWifi": true, + "audioBitrate": 64000, + "canManage": false, + "isManaged": true, + "marketName": "G4 Instant", + "modelKey": "camera" +} diff --git a/tests/components/unifiprotect/test_camera.py b/tests/components/unifiprotect/test_camera.py new file mode 100644 index 00000000000..d3a122c401c --- /dev/null +++ b/tests/components/unifiprotect/test_camera.py @@ -0,0 +1,358 @@ +"""Test the UniFi Protect camera platform.""" +from __future__ import annotations + +from copy import copy +from typing import cast +from unittest.mock import AsyncMock, Mock + +from pyunifiprotect.data import Camera as ProtectCamera +from pyunifiprotect.exceptions import NvrError + +from homeassistant.components.camera import Camera, async_get_image +from homeassistant.components.unifiprotect.const import ( + ATTR_BITRATE, + ATTR_CHANNEL_ID, + ATTR_FPS, + ATTR_HEIGHT, + ATTR_WIDTH, + DEFAULT_ATTRIBUTION, + DEFAULT_SCAN_INTERVAL, + DOMAIN, +) +from homeassistant.components.unifiprotect.data import ProtectData +from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component + +from .conftest import MockEntityFixture, time_changed + + +async def validate_camera_entity( + hass: HomeAssistant, + camera: ProtectCamera, + channel_id: int, + secure: bool, + rtsp_enabled: bool, + enabled: bool, +): + """Validate a camera entity.""" + + channel = camera.channels[channel_id] + + entity_name = f"{camera.name} {channel.name}" + unique_id = f"{camera.id}_{channel.id}" + if not secure: + entity_name += " Insecure" + unique_id += "_insecure" + entity_id = f"camera.{entity_name.replace(' ', '_').lower()}" + + entity_registry = er.async_get(hass) + entity = entity_registry.async_get(entity_id) + assert entity + assert entity.disabled is (not enabled) + assert entity.unique_id == unique_id + + if not enabled: + return + + camera_platform = hass.data.get("camera") + assert camera_platform + ha_camera = cast(Camera, camera_platform.get_entity(entity_id)) + assert ha_camera + if rtsp_enabled: + if secure: + assert await ha_camera.stream_source() == channel.rtsps_url + else: + assert await ha_camera.stream_source() == channel.rtsp_url + else: + assert await ha_camera.stream_source() is None + + entity_state = hass.states.get(entity_id) + assert entity_state + assert entity_state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION + assert entity_state.attributes[ATTR_WIDTH] == channel.width + assert entity_state.attributes[ATTR_HEIGHT] == channel.height + assert entity_state.attributes[ATTR_FPS] == channel.fps + assert entity_state.attributes[ATTR_BITRATE] == channel.bitrate + assert entity_state.attributes[ATTR_CHANNEL_ID] == channel.id + + +async def test_basic_setup( + hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: ProtectCamera +): + """Test working setup of unifiprotect entry.""" + + camera_high_only = mock_camera.copy(deep=True) + camera_high_only._api = mock_entry.api + camera_high_only.channels[0]._api = mock_entry.api + camera_high_only.channels[1]._api = mock_entry.api + camera_high_only.channels[2]._api = mock_entry.api + camera_high_only.name = "Test Camera 1" + camera_high_only.id = "test_high" + camera_high_only.channels[0].is_rtsp_enabled = True + camera_high_only.channels[0].name = "High" + camera_high_only.channels[0].rtsp_alias = "test_high_alias" + camera_high_only.channels[1].is_rtsp_enabled = False + camera_high_only.channels[2].is_rtsp_enabled = False + + camera_all_channels = mock_camera.copy(deep=True) + camera_all_channels._api = mock_entry.api + camera_all_channels.channels[0]._api = mock_entry.api + camera_all_channels.channels[1]._api = mock_entry.api + camera_all_channels.channels[2]._api = mock_entry.api + camera_all_channels.name = "Test Camera 2" + camera_all_channels.id = "test_all" + camera_all_channels.channels[0].is_rtsp_enabled = True + camera_all_channels.channels[0].name = "High" + camera_all_channels.channels[0].rtsp_alias = "test_high_alias" + camera_all_channels.channels[1].is_rtsp_enabled = True + camera_all_channels.channels[1].name = "Medium" + camera_all_channels.channels[1].rtsp_alias = "test_medium_alias" + camera_all_channels.channels[2].is_rtsp_enabled = True + camera_all_channels.channels[2].name = "Low" + camera_all_channels.channels[2].rtsp_alias = "test_low_alias" + + camera_no_channels = mock_camera.copy(deep=True) + camera_no_channels._api = mock_entry.api + camera_no_channels.channels[0]._api = mock_entry.api + camera_no_channels.channels[1]._api = mock_entry.api + camera_no_channels.channels[2]._api = mock_entry.api + camera_no_channels.name = "Test Camera 3" + camera_no_channels.id = "test_none" + camera_no_channels.channels[0].is_rtsp_enabled = False + camera_no_channels.channels[0].name = "High" + camera_no_channels.channels[1].is_rtsp_enabled = False + camera_no_channels.channels[2].is_rtsp_enabled = False + + mock_entry.api.bootstrap.cameras = { + camera_high_only.id: camera_high_only, + camera_all_channels.id: camera_all_channels, + camera_no_channels.id: camera_no_channels, + } + + await hass.config_entries.async_setup(mock_entry.entry.entry_id) + await hass.async_block_till_done() + + await validate_camera_entity( + hass, camera_high_only, 0, secure=True, rtsp_enabled=True, enabled=True + ) + await validate_camera_entity( + hass, camera_high_only, 0, secure=False, rtsp_enabled=True, enabled=False + ) + + await validate_camera_entity( + hass, camera_all_channels, 0, secure=True, rtsp_enabled=True, enabled=True + ) + await validate_camera_entity( + hass, camera_all_channels, 0, secure=False, rtsp_enabled=True, enabled=False + ) + await validate_camera_entity( + hass, camera_all_channels, 1, secure=True, rtsp_enabled=True, enabled=False + ) + await validate_camera_entity( + hass, camera_all_channels, 1, secure=False, rtsp_enabled=True, enabled=False + ) + await validate_camera_entity( + hass, camera_all_channels, 2, secure=True, rtsp_enabled=True, enabled=False + ) + await validate_camera_entity( + hass, camera_all_channels, 2, secure=False, rtsp_enabled=True, enabled=False + ) + + await validate_camera_entity( + hass, camera_no_channels, 0, secure=True, rtsp_enabled=False, enabled=True + ) + + +async def test_missing_channels( + hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: ProtectCamera +): + """Test setting up camera with no camera channels.""" + + camera = mock_camera.copy(deep=True) + camera.channels = [] + + mock_entry.api.bootstrap.cameras = {camera.id: camera} + + await hass.config_entries.async_setup(mock_entry.entry.entry_id) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + + assert len(hass.states.async_all()) == 0 + assert len(entity_registry.entities) == 0 + + +async def test_camera_image( + hass: HomeAssistant, + mock_entry: MockEntityFixture, + simple_camera: tuple[Camera, str], +): + """Test retrieving camera image.""" + + mock_entry.api.get_camera_snapshot = AsyncMock() + + await async_get_image(hass, simple_camera[1]) + mock_entry.api.get_camera_snapshot.assert_called_once() + + +async def test_camera_generic_update( + hass: HomeAssistant, + mock_entry: MockEntityFixture, + simple_camera: tuple[ProtectCamera, str], +): + """Tests generic entity update service.""" + + assert await async_setup_component(hass, "homeassistant", {}) + + data: ProtectData = hass.data[DOMAIN][mock_entry.entry.entry_id] + assert data + assert data.last_update_success + + state = hass.states.get(simple_camera[1]) + assert state and state.state == "idle" + + mock_entry.api.update = AsyncMock(return_value=None) + await hass.services.async_call( + "homeassistant", + "update_entity", + {ATTR_ENTITY_ID: simple_camera[1]}, + blocking=True, + ) + + state = hass.states.get(simple_camera[1]) + assert state and state.state == "idle" + + +async def test_camera_interval_update( + hass: HomeAssistant, + mock_entry: MockEntityFixture, + simple_camera: tuple[ProtectCamera, str], +): + """Interval updates updates camera entity.""" + + data: ProtectData = hass.data[DOMAIN][mock_entry.entry.entry_id] + assert data + assert data.last_update_success + + state = hass.states.get(simple_camera[1]) + assert state and state.state == "idle" + + new_bootstrap = copy(mock_entry.api.bootstrap) + new_camera = simple_camera[0].copy() + new_camera.is_recording = True + + new_bootstrap.cameras = {new_camera.id: new_camera} + mock_entry.api.update = AsyncMock(return_value=new_bootstrap) + mock_entry.api.bootstrap = new_bootstrap + await time_changed(hass, DEFAULT_SCAN_INTERVAL) + + state = hass.states.get(simple_camera[1]) + assert state and state.state == "recording" + + +async def test_camera_bad_interval_update( + hass: HomeAssistant, + mock_entry: MockEntityFixture, + simple_camera: tuple[Camera, str], +): + """Interval updates marks camera unavailable.""" + + data: ProtectData = hass.data[DOMAIN][mock_entry.entry.entry_id] + assert data + assert data.last_update_success + + state = hass.states.get(simple_camera[1]) + assert state and state.state == "idle" + + # update fails + mock_entry.api.update = AsyncMock(side_effect=NvrError) + await time_changed(hass, DEFAULT_SCAN_INTERVAL) + + assert not data.last_update_success + state = hass.states.get(simple_camera[1]) + assert state and state.state == "unavailable" + + # next update succeeds + mock_entry.api.update = AsyncMock(return_value=mock_entry.api.bootstrap) + await time_changed(hass, DEFAULT_SCAN_INTERVAL) + + assert data.last_update_success + state = hass.states.get(simple_camera[1]) + assert state and state.state == "idle" + + +async def test_camera_ws_update( + hass: HomeAssistant, + mock_entry: MockEntityFixture, + simple_camera: tuple[ProtectCamera, str], +): + """WS update updates camera entity.""" + + data: ProtectData = hass.data[DOMAIN][mock_entry.entry.entry_id] + assert data + assert data.last_update_success + + state = hass.states.get(simple_camera[1]) + assert state and state.state == "idle" + + new_bootstrap = copy(mock_entry.api.bootstrap) + new_camera = simple_camera[0].copy() + new_camera.is_recording = True + + mock_msg = Mock() + mock_msg.new_obj = new_camera + + new_bootstrap.cameras = {new_camera.id: new_camera} + mock_entry.api.bootstrap = new_bootstrap + mock_entry.api.ws_subscription(mock_msg) + await hass.async_block_till_done() + + state = hass.states.get(simple_camera[1]) + assert state and state.state == "recording" + + +async def test_camera_ws_update_offline( + hass: HomeAssistant, + mock_entry: MockEntityFixture, + simple_camera: tuple[ProtectCamera, str], +): + """WS updates marks camera unavailable.""" + + data: ProtectData = hass.data[DOMAIN][mock_entry.entry.entry_id] + assert data + assert data.last_update_success + + state = hass.states.get(simple_camera[1]) + assert state and state.state == "idle" + + # camera goes offline + new_bootstrap = copy(mock_entry.api.bootstrap) + new_camera = simple_camera[0].copy() + new_camera.is_connected = False + + mock_msg = Mock() + mock_msg.new_obj = new_camera + + new_bootstrap.cameras = {new_camera.id: new_camera} + mock_entry.api.bootstrap = new_bootstrap + mock_entry.api.ws_subscription(mock_msg) + await hass.async_block_till_done() + + state = hass.states.get(simple_camera[1]) + assert state and state.state == "unavailable" + + # camera comes back online + new_camera.is_connected = True + + mock_msg = Mock() + mock_msg.new_obj = new_camera + + new_bootstrap.cameras = {new_camera.id: new_camera} + mock_entry.api.bootstrap = new_bootstrap + mock_entry.api.ws_subscription(mock_msg) + await hass.async_block_till_done() + + state = hass.states.get(simple_camera[1]) + assert state and state.state == "idle" diff --git a/tests/components/unifiprotect/test_config_flow.py b/tests/components/unifiprotect/test_config_flow.py new file mode 100644 index 00000000000..80c6019c28a --- /dev/null +++ b/tests/components/unifiprotect/test_config_flow.py @@ -0,0 +1,227 @@ +"""Test the UniFi Protect config flow.""" +from __future__ import annotations + +from unittest.mock import patch + +from pyunifiprotect import NotAuthorized, NvrError + +from homeassistant import config_entries +from homeassistant.components.unifiprotect.const import ( + CONF_ALL_UPDATES, + CONF_DISABLE_RTSP, + CONF_OVERRIDE_CHOST, + DOMAIN, +) +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) +from homeassistant.helpers import device_registry as dr + +from .conftest import MAC_ADDR, MOCK_NVR_DATA, MOCK_OLD_NVR_DATA + +from tests.common import MockConfigEntry + + +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert not result["errors"] + + with patch( + "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_nvr", + return_value=MOCK_NVR_DATA, + ), patch( + "homeassistant.components.unifiprotect.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "UnifiProtect" + assert result2["data"] == { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + "id": "UnifiProtect", + "port": 443, + "verify_ssl": False, + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_version_too_old(hass: HomeAssistant) -> None: + """Test we handle the version being too old.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_nvr", + return_value=MOCK_OLD_NVR_DATA, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "protect_version"} + + +async def test_form_invalid_auth(hass: HomeAssistant) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_nvr", + side_effect=NotAuthorized, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"password": "invalid_auth"} + + +async def test_form_cannot_connect(hass: HomeAssistant) -> None: + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_nvr", + side_effect=NvrError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "nvr_error"} + + +async def test_form_reauth_auth(hass: HomeAssistant) -> None: + """Test we handle reauth auth.""" + mock_config = MockConfigEntry( + domain=DOMAIN, + data={ + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + "id": "UnifiProtect", + "port": 443, + "verify_ssl": False, + }, + unique_id=dr.format_mac(MAC_ADDR), + ) + mock_config.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": mock_config.entry_id, + }, + ) + assert result["type"] == RESULT_TYPE_FORM + assert not result["errors"] + + with patch( + "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_nvr", + side_effect=NotAuthorized, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"password": "invalid_auth"} + assert result2["step_id"] == "reauth_confirm" + + with patch( + "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_nvr", + return_value=MOCK_NVR_DATA, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + + assert result3["type"] == RESULT_TYPE_ABORT + assert result3["reason"] == "reauth_successful" + + +async def test_form_options(hass: HomeAssistant) -> None: + """Test we handle options flows.""" + mock_config = MockConfigEntry( + domain=DOMAIN, + data={ + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + "id": "UnifiProtect", + "port": 443, + "verify_ssl": False, + }, + version=2, + unique_id=dr.format_mac(MAC_ADDR), + ) + mock_config.add_to_hass(hass) + + # Integration not setup, since we are only flipping bits in options entry + + result = await hass.config_entries.options.async_init(mock_config.entry_id) + assert result["type"] == RESULT_TYPE_FORM + assert not result["errors"] + assert result["step_id"] == "init" + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + {CONF_DISABLE_RTSP: True, CONF_ALL_UPDATES: True, CONF_OVERRIDE_CHOST: True}, + ) + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["data"] == { + "all_updates": True, + "disable_rtsp": True, + "override_connection_host": True, + } diff --git a/tests/components/unifiprotect/test_init.py b/tests/components/unifiprotect/test_init.py new file mode 100644 index 00000000000..bc42573e26b --- /dev/null +++ b/tests/components/unifiprotect/test_init.py @@ -0,0 +1,109 @@ +"""Test the UniFi Protect setup flow.""" +from __future__ import annotations + +from unittest.mock import AsyncMock + +from pyunifiprotect import NotAuthorized, NvrError + +from homeassistant.components.unifiprotect.const import CONF_DISABLE_RTSP, DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from .conftest import MOCK_OLD_NVR_DATA, MockEntityFixture + + +async def test_setup(hass: HomeAssistant, mock_entry: MockEntityFixture): + """Test working setup of unifiprotect entry.""" + + await hass.config_entries.async_setup(mock_entry.entry.entry_id) + await hass.async_block_till_done() + + assert mock_entry.entry.state == ConfigEntryState.LOADED + assert mock_entry.api.update.called + assert mock_entry.entry.unique_id == mock_entry.api.bootstrap.nvr.mac + assert mock_entry.entry.entry_id in hass.data[DOMAIN] + + +async def test_reload(hass: HomeAssistant, mock_entry: MockEntityFixture): + """Test updating entry reload entry.""" + + await hass.config_entries.async_setup(mock_entry.entry.entry_id) + await hass.async_block_till_done() + assert mock_entry.entry.state == ConfigEntryState.LOADED + + options = dict(mock_entry.entry.options) + options[CONF_DISABLE_RTSP] = True + hass.config_entries.async_update_entry(mock_entry.entry, options=options) + await hass.async_block_till_done() + + assert mock_entry.entry.state == ConfigEntryState.LOADED + assert mock_entry.entry.entry_id in hass.data[DOMAIN] + assert mock_entry.api.async_disconnect_ws.called + + +async def test_unload(hass: HomeAssistant, mock_entry: MockEntityFixture): + """Test unloading of unifiprotect entry.""" + + await hass.config_entries.async_setup(mock_entry.entry.entry_id) + await hass.async_block_till_done() + assert mock_entry.entry.state == ConfigEntryState.LOADED + + await hass.config_entries.async_unload(mock_entry.entry.entry_id) + assert mock_entry.entry.state == ConfigEntryState.NOT_LOADED + assert mock_entry.api.async_disconnect_ws.called + + +async def test_setup_too_old(hass: HomeAssistant, mock_entry: MockEntityFixture): + """Test setup of unifiprotect entry with too old of version of UniFi Protect.""" + + mock_entry.api.get_nvr.return_value = MOCK_OLD_NVR_DATA + + await hass.config_entries.async_setup(mock_entry.entry.entry_id) + await hass.async_block_till_done() + assert mock_entry.entry.state == ConfigEntryState.SETUP_ERROR + assert not mock_entry.api.update.called + + +async def test_setup_failed_update(hass: HomeAssistant, mock_entry: MockEntityFixture): + """Test setup of unifiprotect entry with failed update.""" + + mock_entry.api.update = AsyncMock(side_effect=NvrError) + + await hass.config_entries.async_setup(mock_entry.entry.entry_id) + await hass.async_block_till_done() + assert mock_entry.entry.state == ConfigEntryState.SETUP_RETRY + assert mock_entry.api.update.called + + +async def test_setup_failed_update_reauth( + hass: HomeAssistant, mock_entry: MockEntityFixture +): + """Test setup of unifiprotect entry with update that gives unauthroized error.""" + + mock_entry.api.update = AsyncMock(side_effect=NotAuthorized) + + await hass.config_entries.async_setup(mock_entry.entry.entry_id) + await hass.async_block_till_done() + assert mock_entry.entry.state == ConfigEntryState.SETUP_RETRY + assert mock_entry.api.update.called + + +async def test_setup_failed_error(hass: HomeAssistant, mock_entry: MockEntityFixture): + """Test setup of unifiprotect entry with generic error.""" + + mock_entry.api.get_nvr = AsyncMock(side_effect=NvrError) + + await hass.config_entries.async_setup(mock_entry.entry.entry_id) + await hass.async_block_till_done() + assert mock_entry.entry.state == ConfigEntryState.SETUP_RETRY + assert not mock_entry.api.update.called + + +async def test_setup_failed_auth(hass: HomeAssistant, mock_entry: MockEntityFixture): + """Test setup of unifiprotect entry with unauthorized error.""" + + mock_entry.api.get_nvr = AsyncMock(side_effect=NotAuthorized) + + await hass.config_entries.async_setup(mock_entry.entry.entry_id) + assert mock_entry.entry.state == ConfigEntryState.SETUP_ERROR + assert not mock_entry.api.update.called From 3e3fb52dfaf8807fd94d5182930daaabcebded59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 26 Dec 2021 09:17:59 +0200 Subject: [PATCH 1035/2644] Huawei LTE simplifications (#62770) * Use enum types rather than strs in sensor type hints * Name sensor meta fields same as in SensorEntityDescription * Make integration shared state a NamedTuple * Use dataclasses instead of attr --- .../components/huawei_lte/__init__.py | 53 +++++++------- .../components/huawei_lte/binary_sensor.py | 30 ++++---- .../components/huawei_lte/device_tracker.py | 14 ++-- homeassistant/components/huawei_lte/notify.py | 8 +-- homeassistant/components/huawei_lte/sensor.py | 71 ++++++++++--------- homeassistant/components/huawei_lte/switch.py | 20 +++--- 6 files changed, 101 insertions(+), 95 deletions(-) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 4f4451330f6..ecaa1bcf237 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -4,12 +4,12 @@ from __future__ import annotations from collections import defaultdict from collections.abc import Callable from contextlib import suppress +from dataclasses import dataclass, field from datetime import timedelta import logging import time -from typing import Any, cast +from typing import Any, NamedTuple, cast -import attr from huawei_lte_api.AuthorizedConnection import AuthorizedConnection from huawei_lte_api.Client import Client from huawei_lte_api.Connection import Connection @@ -126,26 +126,28 @@ PLATFORMS = [ ] -@attr.s +@dataclass class Router: """Class for router state.""" - hass: HomeAssistant = attr.ib() - config_entry: ConfigEntry = attr.ib() - connection: Connection = attr.ib() - url: str = attr.ib() + hass: HomeAssistant + config_entry: ConfigEntry + connection: Connection + url: str - data: dict[str, Any] = attr.ib(init=False, factory=dict) - subscriptions: dict[str, set[str]] = attr.ib( + data: dict[str, Any] = field(default_factory=dict, init=False) + subscriptions: dict[str, set[str]] = field( + default_factory=lambda: defaultdict( + set, ((x, {"initial_scan"}) for x in ALL_KEYS) + ), init=False, - factory=lambda: defaultdict(set, ((x, {"initial_scan"}) for x in ALL_KEYS)), ) - inflight_gets: set[str] = attr.ib(init=False, factory=set) - client: Client - suspended = attr.ib(init=False, default=False) - notify_last_attempt: float = attr.ib(init=False, default=-1) + inflight_gets: set[str] = field(default_factory=set, init=False) + client: Client = field(init=False) + suspended: bool = field(default=False, init=False) + notify_last_attempt: float = field(default=-1, init=False) - def __attrs_post_init__(self) -> None: + def __post_init__(self) -> None: """Set up internal state on init.""" self.client = Client(self.connection) @@ -293,14 +295,13 @@ class Router: self.logout() -@attr.s -class HuaweiLteData: +class HuaweiLteData(NamedTuple): """Shared state.""" - hass_config: ConfigType = attr.ib() + hass_config: ConfigType # Our YAML config, keyed by router URL - config: dict[str, dict[str, Any]] = attr.ib() - routers: dict[str, Router] = attr.ib(init=False, factory=dict) + config: dict[str, dict[str, Any]] + routers: dict[str, Router] async def async_setup_entry( # noqa: C901 @@ -509,7 +510,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # Arrange our YAML config to dict with normalized URLs as keys domain_config: dict[str, dict[str, Any]] = {} if DOMAIN not in hass.data: - hass.data[DOMAIN] = HuaweiLteData(hass_config=config, config=domain_config) + hass.data[DOMAIN] = HuaweiLteData( + hass_config=config, config=domain_config, routers={} + ) for router_config in config.get(DOMAIN, []): domain_config[url_normalize(router_config.pop(CONF_URL))] = router_config @@ -607,14 +610,14 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return True -@attr.s +@dataclass class HuaweiLteBaseEntity(Entity): """Huawei LTE entity base class.""" - router: Router = attr.ib() + router: Router - _available: bool = attr.ib(init=False, default=True) - _unsub_handlers: list[Callable] = attr.ib(init=False, factory=list) + _available: bool = field(default=True, init=False) + _unsub_handlers: list[Callable] = field(default_factory=list, init=False) @property def _entity_name(self) -> str: diff --git a/homeassistant/components/huawei_lte/binary_sensor.py b/homeassistant/components/huawei_lte/binary_sensor.py index 85cfd00d7aa..bc4e7ce33a2 100644 --- a/homeassistant/components/huawei_lte/binary_sensor.py +++ b/homeassistant/components/huawei_lte/binary_sensor.py @@ -1,10 +1,10 @@ """Support for Huawei LTE binary sensors.""" from __future__ import annotations +from dataclasses import dataclass, field import logging from typing import Any -import attr from huawei_lte_api.enums.cradle import ConnectionStatusEnum from homeassistant.components.binary_sensor import ( @@ -48,13 +48,13 @@ async def async_setup_entry( async_add_entities(entities, True) -@attr.s +@dataclass class HuaweiLteBaseBinarySensor(HuaweiLteBaseEntity, BinarySensorEntity): """Huawei LTE binary sensor device base class.""" - key: str - item: str - _raw_state: str | None = attr.ib(init=False, default=None) + key: str = field(init=False) + item: str = field(init=False) + _raw_state: str | None = field(default=None, init=False) @property def entity_registry_enabled_default(self) -> bool: @@ -101,11 +101,11 @@ CONNECTION_STATE_ATTRIBUTES = { } -@attr.s +@dataclass class HuaweiLteMobileConnectionBinarySensor(HuaweiLteBaseBinarySensor): """Huawei LTE mobile connection binary sensor.""" - def __attrs_post_init__(self) -> None: + def __post_init__(self) -> None: """Initialize identifiers.""" self.key = KEY_MONITORING_STATUS self.item = "ConnectionStatus" @@ -172,11 +172,11 @@ class HuaweiLteBaseWifiStatusBinarySensor(HuaweiLteBaseBinarySensor): return "mdi:wifi" if self.is_on else "mdi:wifi-off" -@attr.s +@dataclass class HuaweiLteWifiStatusBinarySensor(HuaweiLteBaseWifiStatusBinarySensor): """Huawei LTE WiFi status binary sensor.""" - def __attrs_post_init__(self) -> None: + def __post_init__(self) -> None: """Initialize identifiers.""" self.key = KEY_MONITORING_STATUS self.item = "WifiStatus" @@ -186,11 +186,11 @@ class HuaweiLteWifiStatusBinarySensor(HuaweiLteBaseWifiStatusBinarySensor): return "WiFi status" -@attr.s +@dataclass class HuaweiLteWifi24ghzStatusBinarySensor(HuaweiLteBaseWifiStatusBinarySensor): """Huawei LTE 2.4GHz WiFi status binary sensor.""" - def __attrs_post_init__(self) -> None: + def __post_init__(self) -> None: """Initialize identifiers.""" self.key = KEY_WLAN_WIFI_FEATURE_SWITCH self.item = "wifi24g_switch_enable" @@ -200,11 +200,11 @@ class HuaweiLteWifi24ghzStatusBinarySensor(HuaweiLteBaseWifiStatusBinarySensor): return "2.4GHz WiFi status" -@attr.s +@dataclass class HuaweiLteWifi5ghzStatusBinarySensor(HuaweiLteBaseWifiStatusBinarySensor): """Huawei LTE 5GHz WiFi status binary sensor.""" - def __attrs_post_init__(self) -> None: + def __post_init__(self) -> None: """Initialize identifiers.""" self.key = KEY_WLAN_WIFI_FEATURE_SWITCH self.item = "wifi5g_enabled" @@ -214,11 +214,11 @@ class HuaweiLteWifi5ghzStatusBinarySensor(HuaweiLteBaseWifiStatusBinarySensor): return "5GHz WiFi status" -@attr.s +@dataclass class HuaweiLteSmsStorageFullBinarySensor(HuaweiLteBaseBinarySensor): """Huawei LTE SMS storage full binary sensor.""" - def __attrs_post_init__(self) -> None: + def __post_init__(self) -> None: """Initialize identifiers.""" self.key = KEY_MONITORING_CHECK_NOTIFICATIONS self.item = "SmsStorageFull" diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index 7c3f3d16c92..239693fc05e 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -1,11 +1,11 @@ """Support for device tracking of Huawei LTE routers.""" from __future__ import annotations +from dataclasses import dataclass, field import logging import re from typing import Any, Dict, List, cast -import attr from stringcase import snakecase from homeassistant.components.device_tracker.config_entry import ScannerEntity @@ -173,16 +173,16 @@ def _better_snakecase(text: str) -> str: return cast(str, snakecase(text)) -@attr.s +@dataclass class HuaweiLteScannerEntity(HuaweiLteBaseEntity, ScannerEntity): """Huawei LTE router scanner entity.""" - _mac_address: str = attr.ib() + _mac_address: str - _ip_address: str | None = attr.ib(init=False, default=None) - _is_connected: bool = attr.ib(init=False, default=False) - _hostname: str | None = attr.ib(init=False, default=None) - _extra_state_attributes: dict[str, Any] = attr.ib(init=False, factory=dict) + _ip_address: str | None = field(default=None, init=False) + _is_connected: bool = field(default=False, init=False) + _hostname: str | None = field(default=None, init=False) + _extra_state_attributes: dict[str, Any] = field(default_factory=dict, init=False) @property def _entity_name(self) -> str: diff --git a/homeassistant/components/huawei_lte/notify.py b/homeassistant/components/huawei_lte/notify.py index fab19427637..6e01158a800 100644 --- a/homeassistant/components/huawei_lte/notify.py +++ b/homeassistant/components/huawei_lte/notify.py @@ -1,11 +1,11 @@ """Support for Huawei LTE router notifications.""" from __future__ import annotations +from dataclasses import dataclass import logging import time from typing import Any -import attr from huawei_lte_api.exceptions import ResponseErrorException from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService @@ -33,12 +33,12 @@ async def async_get_service( return HuaweiLteSmsNotificationService(router, default_targets) -@attr.s +@dataclass class HuaweiLteSmsNotificationService(BaseNotificationService): """Huawei LTE router SMS notification service.""" - router: Router = attr.ib() - default_targets: list[str] = attr.ib() + router: Router + default_targets: list[str] def send_message(self, message: str = "", **kwargs: Any) -> None: """Send message to target numbers.""" diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index c894fc39659..ad75e6f84ac 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -3,12 +3,11 @@ from __future__ import annotations from bisect import bisect from collections.abc import Callable +from dataclasses import dataclass, field import logging import re from typing import NamedTuple -import attr - from homeassistant.components.sensor import ( DOMAIN as SENSOR_DOMAIN, SensorDeviceClass, @@ -51,12 +50,12 @@ class SensorMeta(NamedTuple): """Metadata for defining sensors.""" name: str | None = None - device_class: str | None = None + device_class: SensorDeviceClass | None = None icon: str | Callable[[StateType], str] | None = None - unit: str | None = None - state_class: str | None = None - enabled_default: bool = False - entity_category: str | None = None + native_unit_of_measurement: str | None = None + state_class: SensorStateClass | None = None + entity_registry_enabled_default: bool = False + entity_category: EntityCategory | None = None include: re.Pattern[str] | None = None exclude: re.Pattern[str] | None = None formatter: Callable[[str], tuple[StateType, str | None]] | None = None @@ -70,7 +69,7 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { name="WAN IP address", icon="mdi:ip", entity_category=EntityCategory.DIAGNOSTIC, - enabled_default=True, + entity_registry_enabled_default=True, ), (KEY_DEVICE_INFORMATION, "WanIPv6Address"): SensorMeta( name="WAN IPv6 address", @@ -80,7 +79,7 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { (KEY_DEVICE_INFORMATION, "uptime"): SensorMeta( name="Uptime", icon="mdi:timer-outline", - unit=TIME_SECONDS, + native_unit_of_measurement=TIME_SECONDS, entity_category=EntityCategory.DIAGNOSTIC, ), (KEY_DEVICE_SIGNAL, "band"): SensorMeta( @@ -181,7 +180,7 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { )[bisect((-11, -8, -5), x if x is not None else -1000)], state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, - enabled_default=True, + entity_registry_enabled_default=True, ), (KEY_DEVICE_SIGNAL, "rsrp"): SensorMeta( name="RSRP", @@ -195,7 +194,7 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { )[bisect((-110, -95, -80), x if x is not None else -1000)], state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, - enabled_default=True, + entity_registry_enabled_default=True, ), (KEY_DEVICE_SIGNAL, "rssi"): SensorMeta( name="RSSI", @@ -209,7 +208,7 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { )[bisect((-80, -70, -60), x if x is not None else -1000)], state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, - enabled_default=True, + entity_registry_enabled_default=True, ), (KEY_DEVICE_SIGNAL, "sinr"): SensorMeta( name="SINR", @@ -223,7 +222,7 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { )[bisect((0, 5, 10), x if x is not None else -1000)], state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, - enabled_default=True, + entity_registry_enabled_default=True, ), (KEY_DEVICE_SIGNAL, "rscp"): SensorMeta( name="RSCP", @@ -298,13 +297,13 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { ), (KEY_MONITORING_MONTH_STATISTICS, "CurrentMonthDownload"): SensorMeta( name="Current month download", - unit=DATA_BYTES, + native_unit_of_measurement=DATA_BYTES, icon="mdi:download", state_class=SensorStateClass.TOTAL_INCREASING, ), (KEY_MONITORING_MONTH_STATISTICS, "CurrentMonthUpload"): SensorMeta( name="Current month upload", - unit=DATA_BYTES, + native_unit_of_measurement=DATA_BYTES, icon="mdi:upload", state_class=SensorStateClass.TOTAL_INCREASING, ), @@ -317,7 +316,7 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { (KEY_MONITORING_STATUS, "BatteryPercent"): SensorMeta( name="Battery", device_class=SensorDeviceClass.BATTERY, - unit=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), @@ -351,47 +350,49 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { exclude=re.compile(r"^showtraffic$", re.IGNORECASE) ), (KEY_MONITORING_TRAFFIC_STATISTICS, "CurrentConnectTime"): SensorMeta( - name="Current connection duration", unit=TIME_SECONDS, icon="mdi:timer-outline" + name="Current connection duration", + native_unit_of_measurement=TIME_SECONDS, + icon="mdi:timer-outline", ), (KEY_MONITORING_TRAFFIC_STATISTICS, "CurrentDownload"): SensorMeta( name="Current connection download", - unit=DATA_BYTES, + native_unit_of_measurement=DATA_BYTES, icon="mdi:download", state_class=SensorStateClass.TOTAL_INCREASING, ), (KEY_MONITORING_TRAFFIC_STATISTICS, "CurrentDownloadRate"): SensorMeta( name="Current download rate", - unit=DATA_RATE_BYTES_PER_SECOND, + native_unit_of_measurement=DATA_RATE_BYTES_PER_SECOND, icon="mdi:download", state_class=SensorStateClass.MEASUREMENT, ), (KEY_MONITORING_TRAFFIC_STATISTICS, "CurrentUpload"): SensorMeta( name="Current connection upload", - unit=DATA_BYTES, + native_unit_of_measurement=DATA_BYTES, icon="mdi:upload", state_class=SensorStateClass.TOTAL_INCREASING, ), (KEY_MONITORING_TRAFFIC_STATISTICS, "CurrentUploadRate"): SensorMeta( name="Current upload rate", - unit=DATA_RATE_BYTES_PER_SECOND, + native_unit_of_measurement=DATA_RATE_BYTES_PER_SECOND, icon="mdi:upload", state_class=SensorStateClass.MEASUREMENT, ), (KEY_MONITORING_TRAFFIC_STATISTICS, "TotalConnectTime"): SensorMeta( name="Total connected duration", - unit=TIME_SECONDS, + native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", state_class=SensorStateClass.TOTAL_INCREASING, ), (KEY_MONITORING_TRAFFIC_STATISTICS, "TotalDownload"): SensorMeta( name="Total download", - unit=DATA_BYTES, + native_unit_of_measurement=DATA_BYTES, icon="mdi:download", state_class=SensorStateClass.TOTAL_INCREASING, ), (KEY_MONITORING_TRAFFIC_STATISTICS, "TotalUpload"): SensorMeta( name="Total upload", - unit=DATA_BYTES, + native_unit_of_measurement=DATA_BYTES, icon="mdi:upload", state_class=SensorStateClass.TOTAL_INCREASING, ), @@ -521,16 +522,16 @@ def format_default(value: StateType) -> tuple[StateType, str | None]: return value, unit -@attr.s +@dataclass class HuaweiLteSensor(HuaweiLteBaseEntity, SensorEntity): """Huawei LTE sensor entity.""" - key: str = attr.ib() - item: str = attr.ib() - meta: SensorMeta = attr.ib() + key: str + item: str + meta: SensorMeta - _state: StateType = attr.ib(init=False, default=STATE_UNKNOWN) - _unit: str | None = attr.ib(init=False) + _state: StateType = field(default=STATE_UNKNOWN, init=False) + _unit: str | None = field(default=None, init=False) async def async_added_to_hass(self) -> None: """Subscribe to needed data on add.""" @@ -556,14 +557,14 @@ class HuaweiLteSensor(HuaweiLteBaseEntity, SensorEntity): return self._state @property - def device_class(self) -> str | None: + def device_class(self) -> SensorDeviceClass | None: """Return sensor device class.""" return self.meta.device_class @property def native_unit_of_measurement(self) -> str | None: """Return sensor's unit of measurement.""" - return self.meta.unit or self._unit + return self.meta.native_unit_of_measurement or self._unit @property def icon(self) -> str | None: @@ -574,14 +575,14 @@ class HuaweiLteSensor(HuaweiLteBaseEntity, SensorEntity): return icon @property - def state_class(self) -> str | None: + def state_class(self) -> SensorStateClass | None: """Return sensor state class.""" return self.meta.state_class @property def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" - return self.meta.enabled_default + return self.meta.entity_registry_enabled_default async def async_update(self) -> None: """Update state.""" @@ -599,6 +600,6 @@ class HuaweiLteSensor(HuaweiLteBaseEntity, SensorEntity): self._available = value is not None @property - def entity_category(self) -> str | None: + def entity_category(self) -> EntityCategory | None: """Return category of entity, if any.""" return self.meta.entity_category diff --git a/homeassistant/components/huawei_lte/switch.py b/homeassistant/components/huawei_lte/switch.py index af2a382c4db..112e6c820e6 100644 --- a/homeassistant/components/huawei_lte/switch.py +++ b/homeassistant/components/huawei_lte/switch.py @@ -1,11 +1,10 @@ """Support for Huawei LTE switches.""" from __future__ import annotations +from dataclasses import dataclass, field import logging from typing import Any -import attr - from homeassistant.components.switch import ( DOMAIN as SWITCH_DOMAIN, SwitchDeviceClass, @@ -37,14 +36,17 @@ async def async_setup_entry( async_add_entities(switches, True) -@attr.s +@dataclass class HuaweiLteBaseSwitch(HuaweiLteBaseEntity, SwitchEntity): """Huawei LTE switch device base class.""" - key: str - item: str - _attr_device_class = SwitchDeviceClass.SWITCH - _raw_state: str | None = attr.ib(init=False, default=None) + key: str = field(init=False) + item: str = field(init=False) + + _attr_device_class: SwitchDeviceClass = field( + default=SwitchDeviceClass.SWITCH, init=False + ) + _raw_state: str | None = field(default=None, init=False) def _turn(self, state: bool) -> None: raise NotImplementedError @@ -79,11 +81,11 @@ class HuaweiLteBaseSwitch(HuaweiLteBaseEntity, SwitchEntity): self._raw_state = str(value) -@attr.s +@dataclass class HuaweiLteMobileDataSwitch(HuaweiLteBaseSwitch): """Huawei LTE mobile data switch device.""" - def __attrs_post_init__(self) -> None: + def __post_init__(self) -> None: """Initialize identifiers.""" self.key = KEY_DIALUP_MOBILE_DATASWITCH self.item = "dataswitch" From 08a3140e6cbffc5677971f98191e24eea7085236 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Sun, 26 Dec 2021 15:53:14 +0800 Subject: [PATCH 1036/2644] Allow generic camera conf without still_image_url (#62611) * Allow generic config with no CONF_STILL_IMAGE_URL * Use Stream.async_get_image when no CONF_STILL_IMAGE_URL * Remove GenericCamera.camera_image --- homeassistant/components/generic/camera.py | 24 +++++------ homeassistant/components/stream/__init__.py | 2 + tests/components/generic/test_camera.py | 47 ++++++++++++++++++++- 3 files changed, 59 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index b6e08ea8582..23fdd4191a3 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -1,7 +1,6 @@ """Support for IP Cameras.""" from __future__ import annotations -import asyncio import logging import httpx @@ -45,8 +44,8 @@ GET_IMAGE_TIMEOUT = 10 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - vol.Required(CONF_STILL_IMAGE_URL): cv.template, - vol.Optional(CONF_STREAM_SOURCE): cv.template, + vol.Required(vol.Any(CONF_STILL_IMAGE_URL, CONF_STREAM_SOURCE)): cv.template, + vol.Optional(vol.Any(CONF_STILL_IMAGE_URL, CONF_STREAM_SOURCE)): cv.template, vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION): vol.In( [HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION] ), @@ -81,9 +80,10 @@ class GenericCamera(Camera): self.hass = hass self._authentication = device_info.get(CONF_AUTHENTICATION) self._name = device_info.get(CONF_NAME) - self._still_image_url = device_info[CONF_STILL_IMAGE_URL] + self._still_image_url = device_info.get(CONF_STILL_IMAGE_URL) + if self._still_image_url: + self._still_image_url.hass = hass self._stream_source = device_info.get(CONF_STREAM_SOURCE) - self._still_image_url.hass = hass if self._stream_source is not None: self._stream_source.hass = hass self._limit_refetch = device_info[CONF_LIMIT_REFETCH_TO_URL_CHANGE] @@ -120,18 +120,16 @@ class GenericCamera(Camera): """Return the interval between frames of the mjpeg stream.""" return self._frame_interval - def camera_image( - self, width: int | None = None, height: int | None = None - ) -> bytes | None: - """Return bytes of camera image.""" - return asyncio.run_coroutine_threadsafe( - self.async_camera_image(), self.hass.loop - ).result() - async def async_camera_image( self, width: int | None = None, height: int | None = None ) -> bytes | None: """Return a still image response from the camera.""" + if not self._still_image_url: + if not self.stream: + await self.async_create_stream() + if self.stream: + return await self.stream.async_get_image(width, height) + return None try: url = self._still_image_url.async_render(parse_result=False) except TemplateError as err: diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 1a4ce3d92e8..7019dbe60b2 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -437,6 +437,8 @@ class Stream: hass.add_executor_job underneath the hood. """ + self.add_provider(HLS_PROVIDER) + self.start() return await self._keyframe_converter.async_get_image( width=width, height=height ) diff --git a/tests/components/generic/test_camera.py b/tests/components/generic/test_camera.py index c52b4bf6e8f..e9b8d886bc3 100644 --- a/tests/components/generic/test_camera.py +++ b/tests/components/generic/test_camera.py @@ -14,7 +14,7 @@ from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.const import SERVICE_RELOAD from homeassistant.setup import async_setup_component -from tests.common import get_fixture_path +from tests.common import AsyncMock, Mock, get_fixture_path @respx.mock @@ -459,3 +459,48 @@ async def test_timeout_cancelled(hass, hass_client): assert respx.calls.call_count == total_calls assert resp.status == HTTPStatus.OK assert await resp.text() == "hello world" + + +async def test_no_still_image_url(hass, hass_client): + """Test that the component can grab images from stream with no still_image_url.""" + assert await async_setup_component( + hass, + "camera", + { + "camera": { + "name": "config_test", + "platform": "generic", + "stream_source": "rtsp://example.com:554/rtsp/", + }, + }, + ) + await hass.async_block_till_done() + + client = await hass_client() + + with patch( + "homeassistant.components.generic.camera.GenericCamera.stream_source", + return_value=None, + ) as mock_stream_source: + + # First test when there is no stream_source should fail + resp = await client.get("/api/camera_proxy/camera.config_test") + await hass.async_block_till_done() + mock_stream_source.assert_called_once() + assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR + + with patch("homeassistant.components.camera.create_stream") as mock_create_stream: + + # Now test when creating the stream succeeds + mock_stream = Mock() + mock_stream.async_get_image = AsyncMock() + mock_stream.async_get_image.return_value = b"stream_keyframe_image" + mock_create_stream.return_value = mock_stream + + # should start the stream and get the image + resp = await client.get("/api/camera_proxy/camera.config_test") + await hass.async_block_till_done() + mock_create_stream.assert_called_once() + mock_stream.async_get_image.assert_called_once() + assert resp.status == HTTPStatus.OK + assert await resp.read() == b"stream_keyframe_image" From 746aa948bb989a9a012177e28b3a566675764a5f Mon Sep 17 00:00:00 2001 From: Thomas Schamm Date: Sun, 26 Dec 2021 18:53:40 +0100 Subject: [PATCH 1037/2644] Bumped boschshcpy 0.2.27 to 0.2.28 (#62778) --- homeassistant/components/bosch_shc/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bosch_shc/manifest.json b/homeassistant/components/bosch_shc/manifest.json index 43d92906592..2ed89c0bf5b 100644 --- a/homeassistant/components/bosch_shc/manifest.json +++ b/homeassistant/components/bosch_shc/manifest.json @@ -3,7 +3,7 @@ "name": "Bosch SHC", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/bosch_shc", - "requirements": ["boschshcpy==0.2.27"], + "requirements": ["boschshcpy==0.2.28"], "zeroconf": [{ "type": "_http._tcp.local.", "name": "bosch shc*" }], "iot_class": "local_push", "codeowners": ["@tschamm"], diff --git a/requirements_all.txt b/requirements_all.txt index f06376a0b5f..df0ae387418 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -426,7 +426,7 @@ blockchain==1.4.4 bond-api==0.1.15 # homeassistant.components.bosch_shc -boschshcpy==0.2.27 +boschshcpy==0.2.28 # homeassistant.components.amazon_polly # homeassistant.components.route53 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1b25c05b4be..b394918f4fc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -273,7 +273,7 @@ blinkpy==0.18.0 bond-api==0.1.15 # homeassistant.components.bosch_shc -boschshcpy==0.2.27 +boschshcpy==0.2.28 # homeassistant.components.braviatv bravia-tv==1.0.11 From c07077833f0dc1c26e8b567d26986af504defd95 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sun, 26 Dec 2021 21:26:24 +0100 Subject: [PATCH 1038/2644] Add missing entity category for gen2 devices (#62812) --- homeassistant/components/shelly/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index bccb538bf9d..d715d7871e8 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -286,6 +286,7 @@ RPC_SENSORS: Final = { device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, default_enabled=False, + entity_category=EntityCategory.DIAGNOSTIC, ), "rssi": RpcAttributeDescription( key="wifi", From 6f1675944ea8aa3cfba7c23a7b659bd9494b7c50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 26 Dec 2021 22:36:00 +0200 Subject: [PATCH 1039/2644] Add huawei_lte hardware version (#62773) --- homeassistant/components/huawei_lte/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index ecaa1bcf237..b9738b9136a 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -25,6 +25,7 @@ import voluptuous as vol from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( + ATTR_HW_VERSION, ATTR_MODEL, ATTR_SW_VERSION, CONF_MAC, @@ -433,8 +434,10 @@ async def async_setup_entry( # noqa: C901 name=router.device_name, manufacturer="Huawei", ) + hw_version = None sw_version = None if router_info: + hw_version = router_info.get("HardwareVersion") sw_version = router_info.get("SoftwareVersion") if router_info.get("DeviceName"): device_info[ATTR_MODEL] = router_info["DeviceName"] @@ -442,6 +445,8 @@ async def async_setup_entry( # noqa: C901 sw_version = router.data[KEY_DEVICE_BASIC_INFORMATION].get( "SoftwareVersion" ) + if hw_version: + device_info[ATTR_HW_VERSION] = hw_version if sw_version: device_info[ATTR_SW_VERSION] = sw_version device_registry = dr.async_get(hass) From e00406c7b82ee33a7ae914b0abbba8231e695ee0 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 27 Dec 2021 00:40:55 +0000 Subject: [PATCH 1040/2644] [ci skip] Translation update --- .../components/coinbase/translations/hu.json | 2 ++ .../components/overkiz/translations/hu.json | 25 ++++++++++++++ .../components/sensor/translations/hu.json | 4 +++ .../unifiprotect/translations/ca.json | 34 +++++++++++++++++++ .../unifiprotect/translations/de.json | 34 +++++++++++++++++++ .../unifiprotect/translations/hu.json | 34 +++++++++++++++++++ .../unifiprotect/translations/ru.json | 34 +++++++++++++++++++ .../unifiprotect/translations/zh-Hant.json | 34 +++++++++++++++++++ .../components/version/translations/hu.json | 26 ++++++++++++++ 9 files changed, 227 insertions(+) create mode 100644 homeassistant/components/overkiz/translations/hu.json create mode 100644 homeassistant/components/unifiprotect/translations/ca.json create mode 100644 homeassistant/components/unifiprotect/translations/de.json create mode 100644 homeassistant/components/unifiprotect/translations/hu.json create mode 100644 homeassistant/components/unifiprotect/translations/ru.json create mode 100644 homeassistant/components/unifiprotect/translations/zh-Hant.json create mode 100644 homeassistant/components/version/translations/hu.json diff --git a/homeassistant/components/coinbase/translations/hu.json b/homeassistant/components/coinbase/translations/hu.json index 5fb22f9be3b..d62c6bd4aca 100644 --- a/homeassistant/components/coinbase/translations/hu.json +++ b/homeassistant/components/coinbase/translations/hu.json @@ -6,6 +6,8 @@ "error": { "cannot_connect": "Nem siker\u00fclt csatlakozni", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "invalid_auth_key": "A Coinbase elutas\u00edtotta az API hiteles\u00edt\u0151 adatokat \u00e9rv\u00e9nytelen API kulcs miatt.", + "invalid_auth_secret": "A Coinbase elutas\u00edtotta az API hiteles\u00edt\u0151 adatokat \u00e9rv\u00e9nytelen API jelsz\u00f3 miatt.", "unknown": "Ismeretlen hiba" }, "step": { diff --git a/homeassistant/components/overkiz/translations/hu.json b/homeassistant/components/overkiz/translations/hu.json new file mode 100644 index 00000000000..2129b81aa10 --- /dev/null +++ b/homeassistant/components/overkiz/translations/hu.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "server_in_maintenance": "A szerver karbantart\u00e1s miatt nem el\u00e9rhet\u0151", + "too_many_requests": "T\u00fal sok a k\u00e9r\u00e9s, pr\u00f3b\u00e1lja meg k\u00e9s\u0151bb \u00fajra.", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "host": "C\u00edm", + "hub": "Hub", + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "description": "Az Overkiz platformot k\u00fcl\u00f6nb\u00f6z\u0151 gy\u00e1rt\u00f3k haszn\u00e1lj\u00e1k, mint p\u00e9ld\u00e1ul a Somfy (Connexoon / TaHoma), a Hitachi (Hi Kumo), a Rexel (Energeasy Connect) \u00e9s az Atlantic (Cozytouch). Adja meg az alkalmaz\u00e1s hiteles\u00edt\u0151 adatait, \u00e9s v\u00e1lassza ki a hubot." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/hu.json b/homeassistant/components/sensor/translations/hu.json index c8650cd1579..0f74b6cdd0f 100644 --- a/homeassistant/components/sensor/translations/hu.json +++ b/homeassistant/components/sensor/translations/hu.json @@ -1,6 +1,7 @@ { "device_automation": { "condition_type": { + "is_apparent_power": "Aktu\u00e1lis {entity_name} l\u00e1tsz\u00f3lagos teljes\u00edtm\u00e9ny", "is_battery_level": "{entity_name} aktu\u00e1lis akku szintje", "is_carbon_dioxide": "Jelenlegi {entity_name} sz\u00e9n-dioxid koncentr\u00e1ci\u00f3 szint", "is_carbon_monoxide": "Jelenlegi {entity_name} sz\u00e9n-monoxid koncentr\u00e1ci\u00f3 szint", @@ -20,6 +21,7 @@ "is_power": "{entity_name} aktu\u00e1lis teljes\u00edtm\u00e9nye", "is_power_factor": "A jelenlegi {entity_name} teljes\u00edtm\u00e9nyt\u00e9nyez\u0151", "is_pressure": "{entity_name} aktu\u00e1lis nyom\u00e1sa", + "is_reactive_power": "Aktu\u00e1lis {entity_name} reakt\u00edv teljes\u00edtm\u00e9ny", "is_signal_strength": "{entity_name} aktu\u00e1lis jeler\u0151ss\u00e9ge", "is_sulphur_dioxide": "A {entity_name} k\u00e9n-dioxid koncentr\u00e1ci\u00f3 jelenlegi szintje", "is_temperature": "{entity_name} aktu\u00e1lis h\u0151m\u00e9rs\u00e9klete", @@ -28,6 +30,7 @@ "is_voltage": "A jelenlegi {entity_name} fesz\u00fclts\u00e9g" }, "trigger_type": { + "apparent_power": "{entity_name} l\u00e1tsz\u00f3lagos teljes\u00edtm\u00e9ny v\u00e1ltoz\u00e1sok", "battery_level": "{entity_name} akku szintje v\u00e1ltozik", "carbon_dioxide": "{entity_name} sz\u00e9n-dioxid koncentr\u00e1ci\u00f3ja megv\u00e1ltozik", "carbon_monoxide": "{entity_name} sz\u00e9n-monoxid koncentr\u00e1ci\u00f3ja megv\u00e1ltozik", @@ -47,6 +50,7 @@ "power": "{entity_name} teljes\u00edtm\u00e9nye v\u00e1ltozik", "power_factor": "{entity_name} teljes\u00edtm\u00e9nyt\u00e9nyez\u0151 megv\u00e1ltozik", "pressure": "{entity_name} nyom\u00e1sa v\u00e1ltozik", + "reactive_power": "{entity_name} reakt\u00edv teljes\u00edtm\u00e9ny v\u00e1ltoz\u00e1sok", "signal_strength": "{entity_name} jeler\u0151ss\u00e9ge v\u00e1ltozik", "sulphur_dioxide": "{entity_name} k\u00e9n-dioxid koncentr\u00e1ci\u00f3v\u00e1ltoz\u00e1s", "temperature": "{entity_name} h\u0151m\u00e9rs\u00e9klete v\u00e1ltozik", diff --git a/homeassistant/components/unifiprotect/translations/ca.json b/homeassistant/components/unifiprotect/translations/ca.json new file mode 100644 index 00000000000..3e39ebbeac5 --- /dev/null +++ b/homeassistant/components/unifiprotect/translations/ca.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "password": "Contrasenya", + "username": "Nom d'usuari" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "all_updates": "M\u00e8triques en temps real (ALERTA: augmenta considerablement l'\u00fas de CPU)", + "disable_rtsp": "Desactiva el flux RTSP", + "override_connection_host": "Substitueix l'amfitri\u00f3 de connexi\u00f3" + }, + "description": "Les m\u00e8triques en temps real nom\u00e9s s'haurien d'activar si has habilitat sensors de diagn\u00f2stic i vols que s'actualitzin en temps real. Si no actives aquesta opci\u00f3, els sensors s'actualitzaran cada 15 minuts.", + "title": "Opcions d'UniFi Protect" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifiprotect/translations/de.json b/homeassistant/components/unifiprotect/translations/de.json new file mode 100644 index 00000000000..09d9a9e7c8d --- /dev/null +++ b/homeassistant/components/unifiprotect/translations/de.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Passwort", + "username": "Benutzername" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "all_updates": "Echtzeitmetriken (WARNUNG: Erh\u00f6ht die CPU-Auslastung erheblich)", + "disable_rtsp": "RTSP-Stream deaktivieren", + "override_connection_host": "Verbindungshost \u00fcberschreiben" + }, + "description": "Die Option Echtzeitmetriken sollte nur aktiviert werden, wenn du die Diagnosesensoren aktiviert hast und m\u00f6chtest, dass sie in Echtzeit aktualisiert werden. Wenn sie nicht aktiviert sind, werden sie nur alle 15 Minuten aktualisiert.", + "title": "UniFi Protect-Optionen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifiprotect/translations/hu.json b/homeassistant/components/unifiprotect/translations/hu.json new file mode 100644 index 00000000000..f205a442b4f --- /dev/null +++ b/homeassistant/components/unifiprotect/translations/hu.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "host": "C\u00edm", + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "all_updates": "Val\u00f3s idej\u0171 m\u00e9r\u0151sz\u00e1mok (FIGYELEM: nagym\u00e9rt\u00e9kben n\u00f6veli a CPU terhel\u00e9st)", + "disable_rtsp": "Az RTSP adatfolyam letilt\u00e1sa", + "override_connection_host": "Kapcsolat c\u00edm\u00e9nek fel\u00fclb\u00edr\u00e1l\u00e1sa" + }, + "description": "A Val\u00f3s idej\u0171 m\u00e9r\u0151sz\u00e1mokat csak akkor javasolt haszn\u00e1lni, ha enged\u00e9lyezte a diagnosztikai \u00e9rz\u00e9kel\u0151ket, \u00e9s szeretn\u00e9, hogy azok val\u00f3s id\u0151ben friss\u00fcljenek. Ha nincs enged\u00e9lyezve, akkor csak 15 percenk\u00e9nt friss\u00fclnek.", + "title": "UniFi Protect be\u00e1ll\u00edt\u00e1sok" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifiprotect/translations/ru.json b/homeassistant/components/unifiprotect/translations/ru.json new file mode 100644 index 00000000000..2c5b1b67755 --- /dev/null +++ b/homeassistant/components/unifiprotect/translations/ru.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "all_updates": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u0435\u043b\u0438 \u0432 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u043c \u0432\u0440\u0435\u043c\u0435\u043d\u0438 (\u0412\u041d\u0418\u041c\u0410\u041d\u0418\u0415: \u0437\u043d\u0430\u0447\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0443\u0432\u0435\u043b\u0438\u0447\u0438\u0432\u0430\u0435\u0442 \u043d\u0430\u0433\u0440\u0443\u0437\u043a\u0443 \u043d\u0430 \u0426\u041f)", + "disable_rtsp": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u043e\u0442\u043e\u043a RTSP", + "override_connection_host": "\u041f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0443\u0437\u0435\u043b \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f" + }, + "description": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 '\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u0435\u043b\u0438 \u0432 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u043c \u0432\u0440\u0435\u043c\u0435\u043d\u0438' \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0432\u043a\u043b\u044e\u0447\u0430\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u0432 \u0442\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435, \u0435\u0441\u043b\u0438 \u0412\u044b \u0432\u043a\u043b\u044e\u0447\u0438\u043b\u0438 \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0434\u0438\u0430\u0433\u043d\u043e\u0441\u0442\u0438\u043a\u0438 \u0438 \u0445\u043e\u0442\u0438\u0442\u0435, \u0447\u0442\u043e\u0431\u044b \u043e\u043d\u0438 \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u043b\u0438\u0441\u044c \u0432 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u043c \u0432\u0440\u0435\u043c\u0435\u043d\u0438. \u0415\u0441\u043b\u0438 \u044d\u0442\u043e\u0442 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \u0434\u0435\u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u043d, \u043e\u043d\u0438 \u0431\u0443\u0434\u0443\u0442 \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0442\u044c\u0441\u044f \u0440\u0430\u0437 \u0432 15 \u043c\u0438\u043d\u0443\u0442.", + "title": "UniFi Protect" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifiprotect/translations/zh-Hant.json b/homeassistant/components/unifiprotect/translations/zh-Hant.json new file mode 100644 index 00000000000..0c8109602a0 --- /dev/null +++ b/homeassistant/components/unifiprotect/translations/zh-Hant.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "all_updates": "\u5373\u6642\u6307\u6a19\uff08\u8b66\u544a\uff1a\u5927\u91cf\u63d0\u5347 CPU \u4f7f\u7528\u7387\uff09", + "disable_rtsp": "\u95dc\u9589 RTSP \u4e32\u6d41", + "override_connection_host": "\u7f6e\u63db\u9023\u7dda\u4e3b\u6a5f\u7aef" + }, + "description": "\u50c5\u6709\u7576\u958b\u555f\u8a3a\u65b7\u50b3\u611f\u5668\u3001\u4e26\u9700\u8981\u5373\u6642\u66f4\u65b0\u6642\uff0c\u624d\u5efa\u8b70\u958b\u555f\u5373\u6642\u6307\u6a19\u9078\u9805\u3002\u672a\u555f\u7528\u72c0\u6cc1\u4e0b\u70ba\u6bcf 15 \u5206\u9418\u66f4\u65b0\u4e00\u6b21\u3002", + "title": "UniFi \u4fdd\u8b77\u9078\u9805" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/version/translations/hu.json b/homeassistant/components/version/translations/hu.json new file mode 100644 index 00000000000..b7d31883c92 --- /dev/null +++ b/homeassistant/components/version/translations/hu.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "step": { + "user": { + "data": { + "version_source": "Verzi\u00f3 forr\u00e1sa" + }, + "description": "V\u00e1lassza ki azt a forr\u00e1st, amelyb\u0151l a verzi\u00f3kat nyomon k\u00edv\u00e1nja k\u00f6vetni", + "title": "V\u00e1lassza ki a telep\u00edt\u00e9s m\u00f3dj\u00e1t" + }, + "version_source": { + "data": { + "beta": "Tartalmazza a b\u00e9ta verzi\u00f3kat", + "board": "Melyik t\u00e1bl\u00e1t kell nyomon k\u00f6vetni", + "channel": "A figyelend\u0151 csatorna", + "image": "A figyelend\u0151 image" + }, + "description": "{version_source} verzi\u00f3k\u00f6vet\u00e9s be\u00e1ll\u00edt\u00e1sa", + "title": "Be\u00e1ll\u00edt\u00e1s" + } + } + } +} \ No newline at end of file From a3e526d6cf199d74793565f890408c73322054ca Mon Sep 17 00:00:00 2001 From: flfue Date: Mon, 27 Dec 2021 08:31:51 +0100 Subject: [PATCH 1041/2644] Add return for certified devices to not apply availability check (#62728) --- homeassistant/components/hue/v2/entity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/hue/v2/entity.py b/homeassistant/components/hue/v2/entity.py index 69b299d574f..8253d4ffbef 100644 --- a/homeassistant/components/hue/v2/entity.py +++ b/homeassistant/components/hue/v2/entity.py @@ -145,6 +145,7 @@ class HueBaseEntity(Entity): if self.device.product_data.certified: # certified products report their state correctly self._ignore_availability = False + return # some (3th party) Hue lights report their connection status incorrectly # causing the zigbee availability to report as disconnected while in fact # it can be controlled. Although this is in fact something the device manufacturer From 417172eef237bc06c336af2f9c50d5ed2c4e8ab2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 26 Dec 2021 22:53:33 -1000 Subject: [PATCH 1042/2644] Cleanup HomeKit names to avoid unknown error when adding (#62831) --- .../components/homekit/accessories.py | 8 +++- homeassistant/components/homekit/type_fans.py | 6 ++- .../components/homekit/type_media_players.py | 7 ++-- .../components/homekit/type_remotes.py | 21 +++++----- .../components/homekit/type_switches.py | 7 ++-- homeassistant/components/homekit/util.py | 9 ++++- tests/components/homekit/test_type_remote.py | 38 +++++++++++++++++++ tests/components/homekit/test_type_sensors.py | 34 +++++++++++++++++ 8 files changed, 107 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 23f546910f1..75a3a0fe797 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -57,7 +57,6 @@ from .const import ( MANUFACTURER, MAX_MANUFACTURER_LENGTH, MAX_MODEL_LENGTH, - MAX_NAME_LENGTH, MAX_SERIAL_LENGTH, MAX_VERSION_LENGTH, SERV_BATTERY_SERVICE, @@ -73,6 +72,7 @@ from .util import ( accessory_friendly_name, async_dismiss_setup_message, async_show_setup_message, + cleanup_name_for_homekit, convert_to_float, format_sw_version, validate_media_player_features, @@ -239,7 +239,11 @@ class HomeAccessory(Accessory): ): """Initialize a Accessory object.""" super().__init__( - driver=driver, display_name=name[:MAX_NAME_LENGTH], aid=aid, *args, **kwargs + driver=driver, + display_name=cleanup_name_for_homekit(name), + aid=aid, + *args, + **kwargs, ) self.config = config or {} if device_id: diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py index d25f197e0ca..54d6424e8d5 100644 --- a/homeassistant/components/homekit/type_fans.py +++ b/homeassistant/components/homekit/type_fans.py @@ -39,11 +39,11 @@ from .const import ( CHAR_ROTATION_DIRECTION, CHAR_ROTATION_SPEED, CHAR_SWING_MODE, - MAX_NAME_LENGTH, PROP_MIN_STEP, SERV_FANV2, SERV_SWITCH, ) +from .util import cleanup_name_for_homekit _LOGGER = logging.getLogger(__name__) @@ -102,7 +102,9 @@ class Fan(HomeAccessory): serv_fan.add_linked_service(preset_serv) preset_serv.configure_char( CHAR_NAME, - value=f"{self.display_name} {preset_mode}"[:MAX_NAME_LENGTH], + value=cleanup_name_for_homekit( + f"{self.display_name} {preset_mode}" + ), ) self.preset_mode_chars[preset_mode] = preset_serv.configure_char( diff --git a/homeassistant/components/homekit/type_media_players.py b/homeassistant/components/homekit/type_media_players.py index 61c043ebcaa..8faa1385e18 100644 --- a/homeassistant/components/homekit/type_media_players.py +++ b/homeassistant/components/homekit/type_media_players.py @@ -55,12 +55,11 @@ from .const import ( FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, KEY_PLAY_PAUSE, - MAX_NAME_LENGTH, SERV_SWITCH, SERV_TELEVISION_SPEAKER, ) from .type_remotes import REMOTE_KEYS, RemoteInputSelectAccessory -from .util import get_media_player_features +from .util import cleanup_name_for_homekit, get_media_player_features _LOGGER = logging.getLogger(__name__) @@ -135,7 +134,9 @@ class MediaPlayer(HomeAccessory): def generate_service_name(self, mode): """Generate name for individual service.""" - return f"{self.display_name} {MODE_FRIENDLY_NAME[mode]}"[:MAX_NAME_LENGTH] + return cleanup_name_for_homekit( + f"{self.display_name} {MODE_FRIENDLY_NAME[mode]}" + ) def set_on_off(self, value): """Move switch state to value if call came from HomeKit.""" diff --git a/homeassistant/components/homekit/type_remotes.py b/homeassistant/components/homekit/type_remotes.py index 69267c733d2..be1ab2a4f94 100644 --- a/homeassistant/components/homekit/type_remotes.py +++ b/homeassistant/components/homekit/type_remotes.py @@ -47,10 +47,10 @@ from .const import ( KEY_PREVIOUS_TRACK, KEY_REWIND, KEY_SELECT, - MAX_NAME_LENGTH, SERV_INPUT_SOURCE, SERV_TELEVISION, ) +from .util import cleanup_name_for_homekit MAXIMUM_SOURCES = ( 90 # Maximum services per accessory is 100. The base acccessory uses 9 @@ -103,7 +103,9 @@ class RemoteInputSelectAccessory(HomeAccessory): self.entity_id, MAXIMUM_SOURCES, ) - self.sources = sources[:MAXIMUM_SOURCES] + self.sources = [ + cleanup_name_for_homekit(source) for source in sources[:MAXIMUM_SOURCES] + ] if self.sources: self.support_select_source = True @@ -132,10 +134,8 @@ class RemoteInputSelectAccessory(HomeAccessory): SERV_INPUT_SOURCE, [CHAR_IDENTIFIER, CHAR_NAME] ) serv_tv.add_linked_service(serv_input) - serv_input.configure_char( - CHAR_CONFIGURED_NAME, value=source[:MAX_NAME_LENGTH] - ) - serv_input.configure_char(CHAR_NAME, value=source[:MAX_NAME_LENGTH]) + serv_input.configure_char(CHAR_CONFIGURED_NAME, value=source) + serv_input.configure_char(CHAR_NAME, value=source) serv_input.configure_char(CHAR_IDENTIFIER, value=index) serv_input.configure_char(CHAR_IS_CONFIGURED, value=True) input_type = 3 if "hdmi" in source.lower() else 0 @@ -161,7 +161,8 @@ class RemoteInputSelectAccessory(HomeAccessory): # Set active input if not self.support_select_source or not self.sources: return - source_name = new_state.attributes.get(self.source_key) + source = new_state.attributes.get(self.source_key) + source_name = cleanup_name_for_homekit(source) _LOGGER.debug("%s: Set current input to %s", self.entity_id, source_name) if source_name in self.sources: index = self.sources.index(source_name) @@ -169,8 +170,8 @@ class RemoteInputSelectAccessory(HomeAccessory): return possible_sources = new_state.attributes.get(self.source_list_key, []) - if source_name in possible_sources: - index = possible_sources.index(source_name) + if source in possible_sources: + index = possible_sources.index(source) if index >= MAXIMUM_SOURCES: _LOGGER.debug( "%s: Source %s and above are not supported", @@ -189,7 +190,7 @@ class RemoteInputSelectAccessory(HomeAccessory): _LOGGER.debug( "%s: Source %s does not exist the source list: %s", self.entity_id, - source_name, + source, possible_sources, ) self.char_input_source.set_value(0) diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index cd0d4243726..7bc529d7e40 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -42,7 +42,6 @@ from .const import ( CHAR_ON, CHAR_OUTLET_IN_USE, CHAR_VALVE_TYPE, - MAX_NAME_LENGTH, SERV_OUTLET, SERV_SWITCH, SERV_VALVE, @@ -51,6 +50,7 @@ from .const import ( TYPE_SPRINKLER, TYPE_VALVE, ) +from .util import cleanup_name_for_homekit _LOGGER = logging.getLogger(__name__) @@ -263,8 +263,7 @@ class SelectSwitch(HomeAccessory): SERV_OUTLET, [CHAR_NAME, CHAR_IN_USE] ) serv_option.configure_char( - CHAR_NAME, - value=f"{option}"[:MAX_NAME_LENGTH], + CHAR_NAME, value=cleanup_name_for_homekit(option) ) serv_option.configure_char(CHAR_IN_USE, value=False) self.select_chars[option] = serv_option.configure_char( @@ -286,6 +285,6 @@ class SelectSwitch(HomeAccessory): @callback def async_update_state(self, new_state): """Update switch state after state changed.""" - current_option = new_state.state + current_option = cleanup_name_for_homekit(new_state.state) for option, char in self.select_chars.items(): char.set_value(option == current_option) diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 894bfcf8985..2906bf97e7e 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -1,4 +1,6 @@ """Collection of useful functions for the HomeKit component.""" +from __future__ import annotations + import io import ipaddress import logging @@ -76,6 +78,7 @@ from .const import ( FEATURE_TOGGLE_MUTE, HOMEKIT_PAIRING_QR, HOMEKIT_PAIRING_QR_SECRET, + MAX_NAME_LENGTH, TYPE_FAUCET, TYPE_OUTLET, TYPE_SHOWER, @@ -352,14 +355,16 @@ def convert_to_float(state): return None -def cleanup_name_for_homekit(name): +def cleanup_name_for_homekit(name: str | None) -> str | None: """Ensure the name of the device will not crash homekit.""" # # This is not a security measure. # # UNICODE_EMOJI is also not allowed but that # likely isn't a problem - return name.translate(HOMEKIT_CHAR_TRANSLATIONS) + if name is None: + return None + return name.translate(HOMEKIT_CHAR_TRANSLATIONS)[:MAX_NAME_LENGTH] def temperature_to_homekit(temperature, unit): diff --git a/tests/components/homekit/test_type_remote.py b/tests/components/homekit/test_type_remote.py index 5c5b5ee6cd9..e77dd5de54c 100644 --- a/tests/components/homekit/test_type_remote.py +++ b/tests/components/homekit/test_type_remote.py @@ -167,3 +167,41 @@ async def test_activity_remote(hass, hk_driver, events, caplog): ) await hass.async_block_till_done() assert call_reset_accessory[0].data[ATTR_ENTITY_ID] == entity_id + + +async def test_activity_remote_bad_names(hass, hk_driver, events, caplog): + """Test if remote accessory with invalid names works as expected.""" + entity_id = "remote.harmony" + hass.states.async_set( + entity_id, + None, + { + ATTR_SUPPORTED_FEATURES: SUPPORT_ACTIVITY, + ATTR_CURRENT_ACTIVITY: "Apple TV", + ATTR_ACTIVITY_LIST: ["TV", "Apple TV", "[[[--Special--]]]", "Super"], + }, + ) + await hass.async_block_till_done() + acc = ActivityRemote(hass, hk_driver, "ActivityRemote", entity_id, 2, None) + await acc.run() + await hass.async_block_till_done() + + assert acc.aid == 2 + assert acc.category == 31 # Television + + assert acc.char_active.value == 0 + assert acc.char_remote_key.value == 0 + assert acc.char_input_source.value == 1 + + hass.states.async_set( + entity_id, + STATE_ON, + { + ATTR_SUPPORTED_FEATURES: SUPPORT_ACTIVITY, + ATTR_CURRENT_ACTIVITY: "[[[--Special--]]]", + ATTR_ACTIVITY_LIST: ["TV", "Apple TV", "[[[--Special--]]]", "Super"], + }, + ) + await hass.async_block_till_done() + assert acc.char_active.value == 1 + assert acc.char_input_source.value == 2 diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index 958306e026f..d864a90fe61 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -366,3 +366,37 @@ async def test_sensor_restore(hass, hk_driver, events): acc = get_accessory(hass, hk_driver, hass.states.get("sensor.humidity"), 2, {}) assert acc.category == 10 + + +async def test_bad_name(hass, hk_driver): + """Test an entity with a bad name.""" + entity_id = "sensor.humidity" + + hass.states.async_set(entity_id, "20") + await hass.async_block_till_done() + acc = HumiditySensor(hass, hk_driver, "[[Humid]]", entity_id, 2, None) + await acc.run() + await hass.async_block_till_done() + + assert acc.aid == 2 + assert acc.category == 10 # Sensor + + assert acc.char_humidity.value == 20 + assert acc.display_name == "--Humid--" + + +async def test_empty_name(hass, hk_driver): + """Test an entity with a empty name.""" + entity_id = "sensor.humidity" + + hass.states.async_set(entity_id, "20") + await hass.async_block_till_done() + acc = HumiditySensor(hass, hk_driver, None, entity_id, 2, None) + await acc.run() + await hass.async_block_till_done() + + assert acc.aid == 2 + assert acc.category == 10 # Sensor + + assert acc.char_humidity.value == 20 + assert acc.display_name is None From a721927b9a3904afe8f3993c1d8ea95a972d0397 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 27 Dec 2021 10:08:29 +0100 Subject: [PATCH 1043/2644] Use ConfigEntryDisabler enum (#62816) --- homeassistant/components/config/config_entries.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index a30d65f5982..f42a635f192 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -311,6 +311,8 @@ async def config_entry_update(hass, connection, msg): async def config_entry_disable(hass, connection, msg): """Disable config entry.""" disabled_by = msg["disabled_by"] + if disabled_by is not None: + disabled_by = config_entries.ConfigEntryDisabler(disabled_by) result = False try: From 7a46e04fd195f5d19f11c82735efdf7d226e3957 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 26 Dec 2021 23:12:02 -1000 Subject: [PATCH 1044/2644] Remove unused homekit.start service (#62827) --- homeassistant/components/homekit/__init__.py | 21 ------------------- homeassistant/components/homekit/const.py | 1 - .../components/homekit/services.yaml | 4 ---- 3 files changed, 26 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 503a76418a9..02232aee1e8 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -91,7 +91,6 @@ from .const import ( MANUFACTURER, PERSIST_LOCK, SERVICE_HOMEKIT_RESET_ACCESSORY, - SERVICE_HOMEKIT_START, SERVICE_HOMEKIT_UNPAIR, SHUTDOWN_TIMEOUT, ) @@ -418,26 +417,6 @@ def _async_register_events_and_services(hass: HomeAssistant): schema=UNPAIR_SERVICE_SCHEMA, ) - async def async_handle_homekit_service_start(service): - """Handle start HomeKit service call.""" - tasks = [] - for homekit in _async_all_homekit_instances(hass): - if homekit.status == STATUS_RUNNING: - _LOGGER.debug("HomeKit is already running") - continue - if homekit.status != STATUS_READY: - _LOGGER.warning( - "HomeKit is not ready. Either it is already starting up or has " - "been stopped" - ) - continue - tasks.append(homekit.async_start()) - await asyncio.gather(*tasks) - - hass.services.async_register( - DOMAIN, SERVICE_HOMEKIT_START, async_handle_homekit_service_start - ) - async def _handle_homekit_reload(service): """Handle start HomeKit service call.""" config = await async_integration_yaml_config(hass, DOMAIN) diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 1dd40d0fa31..63357fe2cf9 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -92,7 +92,6 @@ DEFAULT_HOMEKIT_MODE = HOMEKIT_MODE_BRIDGE HOMEKIT_MODES = [HOMEKIT_MODE_BRIDGE, HOMEKIT_MODE_ACCESSORY] # #### HomeKit Component Services #### -SERVICE_HOMEKIT_START = "start" SERVICE_HOMEKIT_RESET_ACCESSORY = "reset_accessory" SERVICE_HOMEKIT_UNPAIR = "unpair" diff --git a/homeassistant/components/homekit/services.yaml b/homeassistant/components/homekit/services.yaml index 68e7804697b..a982e9ccf8d 100644 --- a/homeassistant/components/homekit/services.yaml +++ b/homeassistant/components/homekit/services.yaml @@ -1,9 +1,5 @@ # Describes the format for available HomeKit services -start: - name: Start - description: Starts the HomeKit driver - reload: name: Reload description: Reload homekit and re-process YAML configuration From a17fffbfc2eaf3a10d9796e113f907c449d12b26 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Mon, 27 Dec 2021 09:42:00 +0000 Subject: [PATCH 1045/2644] Update to pycarwings 2.13 (#62821) --- homeassistant/components/nissan_leaf/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nissan_leaf/manifest.json b/homeassistant/components/nissan_leaf/manifest.json index 89e55cb69d9..42169105930 100644 --- a/homeassistant/components/nissan_leaf/manifest.json +++ b/homeassistant/components/nissan_leaf/manifest.json @@ -2,7 +2,7 @@ "domain": "nissan_leaf", "name": "Nissan Leaf", "documentation": "https://www.home-assistant.io/integrations/nissan_leaf", - "requirements": ["pycarwings2==2.12"], + "requirements": ["pycarwings2==2.13"], "codeowners": ["@filcole"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index df0ae387418..a301ee040a9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1401,7 +1401,7 @@ pyblackbird==0.5 pybotvac==0.0.22 # homeassistant.components.nissan_leaf -pycarwings2==2.12 +pycarwings2==2.13 # homeassistant.components.cloudflare pycfdns==1.2.2 From 38723b277e05e498e4defb816cb47e4e5d63bfd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Bjarni?= Date: Mon, 27 Dec 2021 09:49:38 +0000 Subject: [PATCH 1046/2644] Added XML RSS as Content-Type (#62822) --- homeassistant/components/rest/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index 422ce84cc46..f18fcc8287f 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -144,6 +144,7 @@ class RestSensor(RestEntity, SensorEntity): content_type.startswith("text/xml") or content_type.startswith("application/xml") or content_type.startswith("application/xhtml+xml") + or content_type.startswith("application/rss+xml") ): try: value = json.dumps(xmltodict.parse(value)) From 22e475790f8582f85851ab6a30d2aadfa8de1441 Mon Sep 17 00:00:00 2001 From: kpine Date: Mon, 27 Dec 2021 03:31:31 -0800 Subject: [PATCH 1047/2644] Avoid removing zwave_js devices for non-ready nodes (#59964) * Only replace a node if the mfgr id / prod id / prod type differ * Prefer original device name for unready node * move register_node_in_dev_reg into async_setup_entry * simplify get_device_id_ext * Don't need hex ids * Revert "move register_node_in_dev_reg into async_setup_entry" This reverts commit f900e5fb0c67cc81657a1452b51c313bccb6f9e1. * Revert Callable change * Revert device backup name * Add test fixtures * Update existing not ready test with new fixture data * Check device properties after node added event * Add entity check * Check for extended device id * better device info checks * Use receive_event to properly setup components * Cleanup tests * improve test_replace_different_node * improve test_replace_same_node * add test test_node_model_change * Clean up long comments and strings * Format * Reload integration to detect node device config changes * update assertions * Disable entities on "value removed" event * Disable node status sensor on node replacement * Add test for disabling entities on remove value event * Add test for disabling node status sensor on node replacement * disable entity -> remove entity Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/__init__.py | 42 +- homeassistant/components/zwave_js/entity.py | 26 +- homeassistant/components/zwave_js/helpers.py | 13 + homeassistant/components/zwave_js/sensor.py | 7 + tests/components/zwave_js/conftest.py | 28 + .../fixtures/zp3111-5_not_ready_state.json | 68 ++ .../zwave_js/fixtures/zp3111-5_state.json | 706 ++++++++++++++++++ tests/components/zwave_js/test_init.py | 692 ++++++++++++++--- 8 files changed, 1480 insertions(+), 102 deletions(-) create mode 100644 tests/components/zwave_js/fixtures/zp3111-5_not_ready_state.json create mode 100644 tests/components/zwave_js/fixtures/zp3111-5_state.json diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 7d2af4af126..10088f62414 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -88,7 +88,12 @@ from .discovery import ( async_discover_node_values, async_discover_single_value, ) -from .helpers import async_enable_statistics, get_device_id, get_unique_id +from .helpers import ( + async_enable_statistics, + get_device_id, + get_device_id_ext, + get_unique_id, +) from .migrate import async_migrate_discovered_value from .services import ZWaveServices @@ -116,17 +121,27 @@ def register_node_in_dev_reg( ) -> device_registry.DeviceEntry: """Register node in dev reg.""" device_id = get_device_id(client, node) - # If a device already exists but it doesn't match the new node, it means the node - # was replaced with a different device and the device needs to be removeed so the - # new device can be created. Otherwise if the device exists and the node is the same, - # the node was replaced with the same device model and we can reuse the device. - if (device := dev_reg.async_get_device({device_id})) and ( - device.model != node.device_config.label - or device.manufacturer != node.device_config.manufacturer + device_id_ext = get_device_id_ext(client, node) + device = dev_reg.async_get_device({device_id}) + + # Replace the device if it can be determined that this node is not the + # same product as it was previously. + if ( + device_id_ext + and device + and len(device.identifiers) == 2 + and device_id_ext not in device.identifiers ): remove_device_func(device) + device = None + + if device_id_ext: + ids = {device_id, device_id_ext} + else: + ids = {device_id} + params = { - ATTR_IDENTIFIERS: {device_id}, + ATTR_IDENTIFIERS: ids, ATTR_SW_VERSION: node.firmware_version, ATTR_NAME: node.name or node.device_config.description @@ -338,7 +353,14 @@ async def async_setup_entry( # noqa: C901 device = dev_reg.async_get_device({dev_id}) # We assert because we know the device exists assert device - if not replaced: + if replaced: + discovered_value_ids.pop(device.id, None) + + async_dispatcher_send( + hass, + f"{DOMAIN}_{client.driver.controller.home_id}.{node.node_id}.node_status_remove_entity", + ) + else: remove_device(device) @callback diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index cf15f32932b..87e9f3adbbd 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -20,6 +20,7 @@ from .migrate import async_add_migration_entity_value LOGGER = logging.getLogger(__name__) EVENT_VALUE_UPDATED = "value updated" +EVENT_VALUE_REMOVED = "value removed" EVENT_DEAD = "dead" EVENT_ALIVE = "alive" @@ -99,6 +100,10 @@ class ZWaveBaseEntity(Entity): self.async_on_remove( self.info.node.on(EVENT_VALUE_UPDATED, self._value_changed) ) + self.async_on_remove( + self.info.node.on(EVENT_VALUE_REMOVED, self._value_removed) + ) + for status_event in (EVENT_ALIVE, EVENT_DEAD): self.async_on_remove( self.info.node.on(status_event, self._node_status_alive_or_dead) @@ -171,7 +176,7 @@ class ZWaveBaseEntity(Entity): @callback def _value_changed(self, event_data: dict) -> None: - """Call when (one of) our watched values changes. + """Call when a value associated with our node changes. Should not be overridden by subclasses. """ @@ -193,6 +198,25 @@ class ZWaveBaseEntity(Entity): self.on_value_update() self.async_write_ha_state() + @callback + def _value_removed(self, event_data: dict) -> None: + """Call when a value associated with our node is removed. + + Should not be overridden by subclasses. + """ + value_id = event_data["value"].value_id + + if value_id != self.info.primary_value.value_id: + return + + LOGGER.debug( + "[%s] Primary value %s is being removed", + self.entity_id, + value_id, + ) + + self.hass.async_create_task(self.async_remove()) + @callback def get_zwave_value( self, diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index aa6db532616..363762ac72b 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -66,6 +66,19 @@ def get_device_id(client: ZwaveClient, node: ZwaveNode) -> tuple[str, str]: return (DOMAIN, f"{client.driver.controller.home_id}-{node.node_id}") +@callback +def get_device_id_ext(client: ZwaveClient, node: ZwaveNode) -> tuple[str, str] | None: + """Get extended device registry identifier for Z-Wave node.""" + if None in (node.manufacturer_id, node.product_type, node.product_id): + return None + + domain, dev_id = get_device_id(client, node) + return ( + domain, + f"{dev_id}-{node.manufacturer_id}:{node.product_type}:{node.product_id}", + ) + + @callback def get_home_and_node_id_from_device_id(device_id: tuple[str, ...]) -> list[str]: """ diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 1fb7b933972..81ab803d982 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -502,4 +502,11 @@ class ZWaveNodeStatusSensor(SensorEntity): self.async_poll_value, ) ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{DOMAIN}_{self.unique_id}_remove_entity", + self.async_remove, + ) + ) self.async_write_ha_state() diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 12eaccec612..4f21f616ae1 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -479,6 +479,18 @@ def fortrezz_ssa3_siren_state_fixture(): return json.loads(load_fixture("zwave_js/fortrezz_ssa3_siren_state.json")) +@pytest.fixture(name="zp3111_not_ready_state", scope="session") +def zp3111_not_ready_state_fixture(): + """Load the zp3111 4-in-1 sensor not-ready node state fixture data.""" + return json.loads(load_fixture("zwave_js/zp3111-5_not_ready_state.json")) + + +@pytest.fixture(name="zp3111_state", scope="session") +def zp3111_state_fixture(): + """Load the zp3111 4-in-1 sensor node state fixture data.""" + return json.loads(load_fixture("zwave_js/zp3111-5_state.json")) + + @pytest.fixture(name="client") def mock_client_fixture(controller_state, version_state, log_config_state): """Mock a client.""" @@ -919,3 +931,19 @@ def fortrezz_ssa3_siren_fixture(client, fortrezz_ssa3_siren_state): def firmware_file_fixture(): """Return mock firmware file stream.""" return io.BytesIO(bytes(10)) + + +@pytest.fixture(name="zp3111_not_ready") +def zp3111_not_ready_fixture(client, zp3111_not_ready_state): + """Mock a zp3111 4-in-1 sensor node in a not-ready state.""" + node = Node(client, copy.deepcopy(zp3111_not_ready_state)) + client.driver.controller.nodes[node.node_id] = node + return node + + +@pytest.fixture(name="zp3111") +def zp3111_fixture(client, zp3111_state): + """Mock a zp3111 4-in-1 sensor node.""" + node = Node(client, copy.deepcopy(zp3111_state)) + client.driver.controller.nodes[node.node_id] = node + return node diff --git a/tests/components/zwave_js/fixtures/zp3111-5_not_ready_state.json b/tests/components/zwave_js/fixtures/zp3111-5_not_ready_state.json new file mode 100644 index 00000000000..f892eb5570e --- /dev/null +++ b/tests/components/zwave_js/fixtures/zp3111-5_not_ready_state.json @@ -0,0 +1,68 @@ +{ + "nodeId": 22, + "index": 0, + "status": 1, + "ready": false, + "isListening": false, + "isRouting": true, + "isSecure": "unknown", + "interviewAttempts": 1, + "endpoints": [ + { + "nodeId": 22, + "index": 0, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 7, + "label": "Notification Sensor" + }, + "specific": { + "key": 1, + "label": "Notification Sensor" + }, + "mandatorySupportedCCs": [], + "mandatoryControlledCCs": [] + } + } + ], + "values": [], + "isFrequentListening": false, + "maxDataRate": 100000, + "supportedDataRates": [ + 40000, + 100000 + ], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 7, + "label": "Notification Sensor" + }, + "specific": { + "key": 1, + "label": "Notification Sensor" + }, + "mandatorySupportedCCs": [], + "mandatoryControlledCCs": [] + }, + "commandClasses": [], + "interviewStage": "ProtocolInfo", + "statistics": { + "commandsTX": 0, + "commandsRX": 0, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 0 + } +} diff --git a/tests/components/zwave_js/fixtures/zp3111-5_state.json b/tests/components/zwave_js/fixtures/zp3111-5_state.json new file mode 100644 index 00000000000..8de7dd2b713 --- /dev/null +++ b/tests/components/zwave_js/fixtures/zp3111-5_state.json @@ -0,0 +1,706 @@ +{ + "nodeId": 22, + "index": 0, + "installerIcon": 3079, + "userIcon": 3079, + "status": 2, + "ready": true, + "isListening": false, + "isRouting": true, + "isSecure": false, + "manufacturerId": 265, + "productId": 8449, + "productType": 8225, + "firmwareVersion": "5.1", + "zwavePlusVersion": 1, + "deviceConfig": { + "filename": "/cache/db/devices/0x0109/zp3111-5.json", + "isEmbedded": true, + "manufacturer": "Vision Security", + "manufacturerId": 265, + "label": "ZP3111-5", + "description": "4-in-1 Sensor", + "devices": [ + { + "productType": 8225, + "productId": 8449 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "paramInformation": { + "_map": {} + }, + "metadata": { + "inclusion": "To add the ZP3111 to the Z-Wave network (inclusion), place the Z-Wave primary controller into inclusion mode. Press the Program Switch of ZP3111 for sending the NIF. After sending NIF, Z-Wave will send the auto inclusion, otherwise, ZP3111 will go to sleep after 20 seconds.", + "exclusion": "To remove the ZP3111 from the Z-Wave network (exclusion), place the Z-Wave primary controller into “exclusion” mode, and following its instruction to delete the ZP3111 to the controller. Press the Program Switch of ZP3111 once to be excluded.", + "reset": "Remove cover to trigged tamper switch, LED flash once & send out Alarm Report. Press Program Switch 10 times within 10 seconds, ZP3111 will send the “Device Reset Locally Notification” command and reset to the factory default. (Remark: This is to be used only in the case of primary controller being inoperable or otherwise unavailable.)", + "manual": "https://products.z-wavealliance.org/ProductManual/File?folder=&filename=MarketCertificationFiles/2479/ZP3111-5_R2_20170316.pdf" + } + }, + "label": "ZP3111-5", + "interviewAttempts": 1, + "endpoints": [ + { + "nodeId": 22, + "index": 0, + "installerIcon": 3079, + "userIcon": 3079, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 7, + "label": "Notification Sensor" + }, + "specific": { + "key": 1, + "label": "Notification Sensor" + }, + "mandatorySupportedCCs": [], + "mandatoryControlledCCs": [] + } + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Library type", + "states": { + "0": "Unknown", + "1": "Static Controller", + "2": "Controller", + "3": "Enhanced Slave", + "4": "Slave", + "5": "Installer", + "6": "Routing Slave", + "7": "Bridge Controller", + "8": "Device under Test", + "9": "N/A", + "10": "AV Remote", + "11": "AV Device" + } + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "4.5" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 2, + "metadata": { + "type": "string[]", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": [ + "5.1", + "10.1" + ] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535 + }, + "value": 265 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535 + }, + "value": 8225 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535 + }, + "value": 8449 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "level", + "propertyName": "level", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Battery level", + "min": 0, + "max": 100, + "unit": "%" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "isLow", + "propertyName": "isLow", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level" + }, + "value": true + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Home Security", + "propertyKey": "Cover status", + "propertyName": "Home Security", + "propertyKeyName": "Cover status", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Cover status", + "ccSpecific": { + "notificationType": 7 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "3": "Tampering, product cover removed" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Home Security", + "propertyKey": "Motion sensor status", + "propertyName": "Home Security", + "propertyKeyName": "Motion sensor status", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Motion sensor status", + "ccSpecific": { + "notificationType": 7 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "8": "Motion detection" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmType", + "propertyName": "alarmType", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Type", + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmLevel", + "propertyName": "alarmLevel", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Level", + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Air temperature", + "propertyName": "Air temperature", + "ccVersion": 7, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Air temperature", + "ccSpecific": { + "sensorType": 1, + "scale": 0 + }, + "unit": "°C" + }, + "value": 21.98 + }, + { + "endpoint": 0, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Illuminance", + "propertyName": "Illuminance", + "ccVersion": 7, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Illuminance", + "ccSpecific": { + "sensorType": 3, + "scale": 0 + }, + "unit": "%" + }, + "value": 7.31 + }, + { + "endpoint": 0, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Humidity", + "propertyName": "Humidity", + "ccVersion": 7, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Humidity", + "ccSpecific": { + "sensorType": 5, + "scale": 0 + }, + "unit": "%" + }, + "value": 51.98 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 1, + "propertyName": "Temperature Scale", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Temperature Scale", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Celsius", + "1": "Fahrenheit" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 2, + "propertyName": "Temperature offset", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Temperature offset", + "default": 1, + "min": 0, + "max": 50, + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 3, + "propertyName": "Humidity", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Configure Relative Humidity", + "label": "Humidity", + "default": 10, + "min": 1, + "max": 50, + "unit": "percent", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 4, + "propertyName": "Light Sensor", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Light Sensor", + "default": 10, + "min": 1, + "max": 50, + "unit": "percent", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 5, + "propertyName": "Trigger Interval", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Set the trigger interval for motion sensor re-activation.", + "label": "Trigger Interval", + "default": 180, + "min": 1, + "max": 255, + "unit": "seconds", + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 6, + "propertyName": "Motion Sensor Sensitivity", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Adjust sensitivity of the motion sensor.", + "label": "Motion Sensor Sensitivity", + "default": 4, + "min": 1, + "max": 7, + "states": { + "1": "highest", + "2": "higher", + "3": "high", + "4": "normal", + "5": "low", + "6": "lower", + "7": "lowest" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 4 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 7, + "propertyName": "LED indicator mode", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "LED indicator mode", + "default": 3, + "min": 1, + "max": 3, + "states": { + "1": "Off", + "2": "Pulsing Temperature, Flashing Motion", + "3": "Flashing Temperature and Motion" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 132, + "commandClassName": "Wake Up", + "property": "wakeUpInterval", + "propertyName": "wakeUpInterval", + "ccVersion": 2, + "metadata": { + "type": "number", + "default": 3600, + "readable": false, + "writeable": true, + "label": "Wake Up interval", + "min": 600, + "max": 604800, + "steps": 600 + }, + "value": 3600 + }, + { + "endpoint": 0, + "commandClass": 132, + "commandClassName": "Wake Up", + "property": "controllerNodeId", + "propertyName": "controllerNodeId", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Node ID of the controller" + }, + "value": 1 + } + ], + "isFrequentListening": false, + "maxDataRate": 100000, + "supportedDataRates": [ + 40000, + 100000 + ], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "zwavePlusNodeType": 0, + "zwavePlusRoleType": 6, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 7, + "label": "Notification Sensor" + }, + "specific": { + "key": 1, + "label": "Notification Sensor" + }, + "mandatorySupportedCCs": [], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": false + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 4, + "isSecure": false + }, + { + "id": 49, + "name": "Multilevel Sensor", + "version": 7, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 132, + "name": "Wake Up", + "version": 2, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 2, + "isSecure": false + } + ], + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0109:0x2021:0x2101:5.1", + "statistics": { + "commandsTX": 39, + "commandsRX": 38, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 0 + }, + "highestSecurityClass": -1 +} diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index d9a1695f60f..7e39b784533 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -12,7 +12,11 @@ from homeassistant.components.zwave_js.const import DOMAIN from homeassistant.components.zwave_js.helpers import get_device_id from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import ( + area_registry as ar, + device_registry as dr, + entity_registry as er, +) from .common import AIR_TEMPERATURE_SENSOR, EATON_RF9640_ENTITY @@ -159,7 +163,7 @@ async def test_new_entity_on_value_added(hass, multisensor_6, client, integratio async def test_on_node_added_ready(hass, multisensor_6_state, client, integration): - """Test we handle a ready node added event.""" + """Test we handle a node added event with a ready node.""" dev_reg = dr.async_get(hass) node = Node(client, deepcopy(multisensor_6_state)) event = {"node": node} @@ -182,38 +186,34 @@ async def test_on_node_added_ready(hass, multisensor_6_state, client, integratio assert dev_reg.async_get_device(identifiers={(DOMAIN, air_temperature_device_id)}) -async def test_on_node_added_not_ready(hass, multisensor_6_state, client, integration): - """Test we handle a non ready node added event.""" +async def test_on_node_added_not_ready( + hass, zp3111_not_ready_state, client, integration +): + """Test we handle a node added event with a non-ready node.""" dev_reg = dr.async_get(hass) - node_data = deepcopy(multisensor_6_state) # Copy to allow modification in tests. - node = Node(client, node_data) - node.data["ready"] = False - event = {"node": node} - air_temperature_device_id = f"{client.driver.controller.home_id}-{node.node_id}" + device_id = f"{client.driver.controller.home_id}-{zp3111_not_ready_state['nodeId']}" - state = hass.states.get(AIR_TEMPERATURE_SENSOR) + assert len(hass.states.async_all()) == 0 + assert not dev_reg.devices - assert not state # entity and device not yet added - assert not dev_reg.async_get_device( - identifiers={(DOMAIN, air_temperature_device_id)} + event = Event( + type="node added", + data={ + "source": "controller", + "event": "node added", + "node": deepcopy(zp3111_not_ready_state), + }, ) - - client.driver.controller.emit("node added", event) + client.driver.receive_event(event) await hass.async_block_till_done() - state = hass.states.get(AIR_TEMPERATURE_SENSOR) + # the only entity is the node status sensor + assert len(hass.states.async_all()) == 1 - assert not state # entity not yet added but device added in registry - assert dev_reg.async_get_device(identifiers={(DOMAIN, air_temperature_device_id)}) - - node.data["ready"] = True - node.emit("ready", event) - await hass.async_block_till_done() - - state = hass.states.get(AIR_TEMPERATURE_SENSOR) - - assert state # entity added - assert state.state != STATE_UNAVAILABLE + device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) + assert device + # no extended device identifier yet + assert len(device.identifiers) == 1 async def test_existing_node_ready(hass, client, multisensor_6, integration): @@ -221,12 +221,157 @@ async def test_existing_node_ready(hass, client, multisensor_6, integration): dev_reg = dr.async_get(hass) node = multisensor_6 air_temperature_device_id = f"{client.driver.controller.home_id}-{node.node_id}" + air_temperature_device_id_ext = ( + f"{air_temperature_device_id}-{node.manufacturer_id}:" + f"{node.product_type}:{node.product_id}" + ) state = hass.states.get(AIR_TEMPERATURE_SENSOR) assert state # entity and device added assert state.state != STATE_UNAVAILABLE - assert dev_reg.async_get_device(identifiers={(DOMAIN, air_temperature_device_id)}) + + device = dev_reg.async_get_device(identifiers={(DOMAIN, air_temperature_device_id)}) + assert device + assert device == dev_reg.async_get_device( + identifiers={(DOMAIN, air_temperature_device_id_ext)} + ) + + +async def test_existing_node_not_ready(hass, zp3111_not_ready, client, integration): + """Test we handle a non-ready node that exists during integration setup.""" + dev_reg = dr.async_get(hass) + node = zp3111_not_ready + device_id = f"{client.driver.controller.home_id}-{node.node_id}" + + device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) + assert device.name == f"Node {node.node_id}" + assert not device.manufacturer + assert not device.model + assert not device.sw_version + + # the only entity is the node status sensor + assert len(hass.states.async_all()) == 1 + + device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) + assert device + # no extended device identifier yet + assert len(device.identifiers) == 1 + + +async def test_existing_node_not_replaced_when_not_ready( + hass, zp3111, zp3111_not_ready_state, zp3111_state, client, integration +): + """Test when a node added event with a non-ready node is received. + + The existing node should not be replaced, and no customization should be lost. + """ + dev_reg = dr.async_get(hass) + er_reg = er.async_get(hass) + kitchen_area = ar.async_get(hass).async_create("Kitchen") + + device_id = f"{client.driver.controller.home_id}-{zp3111.node_id}" + device_id_ext = ( + f"{device_id}-{zp3111.manufacturer_id}:" + f"{zp3111.product_type}:{zp3111.product_id}" + ) + + device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) + assert device + assert device.name == "4-in-1 Sensor" + assert not device.name_by_user + assert device.manufacturer == "Vision Security" + assert device.model == "ZP3111-5" + assert device.sw_version == "5.1" + assert not device.area_id + assert device == dev_reg.async_get_device(identifiers={(DOMAIN, device_id_ext)}) + + motion_entity = "binary_sensor.4_in_1_sensor_home_security_motion_detection" + state = hass.states.get(motion_entity) + assert state + assert state.name == "4-in-1 Sensor: Home Security - Motion detection" + + dev_reg.async_update_device( + device.id, name_by_user="Custom Device Name", area_id=kitchen_area.id + ) + + custom_device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) + assert custom_device + assert custom_device.name == "4-in-1 Sensor" + assert custom_device.name_by_user == "Custom Device Name" + assert custom_device.manufacturer == "Vision Security" + assert custom_device.model == "ZP3111-5" + assert device.sw_version == "5.1" + assert custom_device.area_id == kitchen_area.id + assert custom_device == dev_reg.async_get_device( + identifiers={(DOMAIN, device_id_ext)} + ) + + custom_entity = "binary_sensor.custom_motion_sensor" + er_reg.async_update_entity( + motion_entity, new_entity_id=custom_entity, name="Custom Entity Name" + ) + await hass.async_block_till_done() + state = hass.states.get(custom_entity) + assert state + assert state.name == "Custom Entity Name" + assert not hass.states.get(motion_entity) + + event = Event( + type="node added", + data={ + "source": "controller", + "event": "node added", + "node": deepcopy(zp3111_not_ready_state), + }, + ) + client.driver.receive_event(event) + await hass.async_block_till_done() + + device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) + assert device + assert device == dev_reg.async_get_device(identifiers={(DOMAIN, device_id_ext)}) + assert device.id == custom_device.id + assert device.identifiers == custom_device.identifiers + assert device.name == f"Node {zp3111.node_id}" + assert device.name_by_user == "Custom Device Name" + assert not device.manufacturer + assert not device.model + assert not device.sw_version + assert device.area_id == kitchen_area.id + + state = hass.states.get(custom_entity) + assert state + assert state.name == "Custom Entity Name" + + event = Event( + type="ready", + data={ + "source": "node", + "event": "ready", + "nodeId": zp3111_state["nodeId"], + "nodeState": deepcopy(zp3111_state), + }, + ) + client.driver.receive_event(event) + await hass.async_block_till_done() + + device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) + assert device + assert device == dev_reg.async_get_device(identifiers={(DOMAIN, device_id_ext)}) + assert device.id == custom_device.id + assert device.identifiers == custom_device.identifiers + assert device.name == "4-in-1 Sensor" + assert device.name_by_user == "Custom Device Name" + assert device.manufacturer == "Vision Security" + assert device.model == "ZP3111-5" + assert device.area_id == kitchen_area.id + assert device.sw_version == "5.1" + + state = hass.states.get(custom_entity) + assert state + assert state.state != STATE_UNAVAILABLE + assert state.name == "Custom Entity Name" async def test_null_name(hass, client, null_name_check, integration): @@ -235,38 +380,6 @@ async def test_null_name(hass, client, null_name_check, integration): assert hass.states.get(f"switch.node_{node.node_id}") -async def test_existing_node_not_ready(hass, client, multisensor_6): - """Test we handle a non ready node that exists during integration setup.""" - dev_reg = dr.async_get(hass) - node = multisensor_6 - node.data = deepcopy(node.data) # Copy to allow modification in tests. - node.data["ready"] = False - event = {"node": node} - air_temperature_device_id = f"{client.driver.controller.home_id}-{node.node_id}" - entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"}) - entry.add_to_hass(hass) - - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - state = hass.states.get(AIR_TEMPERATURE_SENSOR) - - assert not state # entity not yet added - assert dev_reg.async_get_device( # device should be added - identifiers={(DOMAIN, air_temperature_device_id)} - ) - - node.data["ready"] = True - node.emit("ready", event) - await hass.async_block_till_done() - - state = hass.states.get(AIR_TEMPERATURE_SENSOR) - - assert state # entity and device added - assert state.state != STATE_UNAVAILABLE - assert dev_reg.async_get_device(identifiers={(DOMAIN, air_temperature_device_id)}) - - async def test_start_addon( hass, addon_installed, install_addon, addon_options, set_addon_options, start_addon ): @@ -740,63 +853,460 @@ async def test_node_removed(hass, multisensor_6_state, client, integration): assert not dev_reg.async_get(old_device.id) -async def test_replace_same_node(hass, multisensor_6_state, client, integration): +async def test_replace_same_node( + hass, multisensor_6, multisensor_6_state, client, integration +): """Test when a node is replaced with itself that the device remains.""" dev_reg = dr.async_get(hass) - node = Node(client, deepcopy(multisensor_6_state)) - device_id = f"{client.driver.controller.home_id}-{node.node_id}" - event = {"node": node} + node_id = multisensor_6.node_id + multisensor_6_state = deepcopy(multisensor_6_state) - client.driver.controller.emit("node added", event) + device_id = f"{client.driver.controller.home_id}-{node_id}" + multisensor_6_device_id = ( + f"{device_id}-{multisensor_6.manufacturer_id}:" + f"{multisensor_6.product_type}:{multisensor_6.product_id}" + ) + + device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) + assert device + assert device == dev_reg.async_get_device( + identifiers={(DOMAIN, multisensor_6_device_id)} + ) + assert device.manufacturer == "AEON Labs" + assert device.model == "ZW100" + dev_id = device.id + + assert hass.states.get(AIR_TEMPERATURE_SENSOR) + + # A replace node event has the extra field "replaced" set to True + # to distinguish it from an exclusion + event = Event( + type="node removed", + data={ + "source": "controller", + "event": "node removed", + "replaced": True, + "node": multisensor_6_state, + }, + ) + client.driver.receive_event(event) await hass.async_block_till_done() - old_device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) - assert old_device.id - event = {"node": node, "replaced": True} + # Device should still be there after the node was removed + device = dev_reg.async_get(dev_id) + assert device - client.driver.controller.emit("node removed", event) + # When the node is replaced, a non-ready node added event is emitted + event = Event( + type="node added", + data={ + "source": "controller", + "event": "node added", + "node": { + "nodeId": node_id, + "index": 0, + "status": 4, + "ready": False, + "isSecure": "unknown", + "interviewAttempts": 1, + "endpoints": [{"nodeId": node_id, "index": 0, "deviceClass": None}], + "values": [], + "deviceClass": None, + "commandClasses": [], + "interviewStage": "None", + "statistics": { + "commandsTX": 0, + "commandsRX": 0, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 0, + }, + }, + }, + ) + + # Device is still not removed + client.driver.receive_event(event) await hass.async_block_till_done() - # Assert device has remained - assert dev_reg.async_get(old_device.id) - event = {"node": node} + device = dev_reg.async_get(dev_id) + assert device - client.driver.controller.emit("node added", event) + event = Event( + type="ready", + data={ + "source": "node", + "event": "ready", + "nodeId": node_id, + "nodeState": multisensor_6_state, + }, + ) + client.driver.receive_event(event) await hass.async_block_till_done() - # Assert device has remained - assert dev_reg.async_get(old_device.id) + + # Device is the same + device = dev_reg.async_get(dev_id) + assert device + assert device == dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) + assert device == dev_reg.async_get_device( + identifiers={(DOMAIN, multisensor_6_device_id)} + ) + assert device.manufacturer == "AEON Labs" + assert device.model == "ZW100" + + assert hass.states.get(AIR_TEMPERATURE_SENSOR) async def test_replace_different_node( - hass, multisensor_6_state, hank_binary_switch_state, client, integration + hass, + multisensor_6, + multisensor_6_state, + hank_binary_switch_state, + client, + integration, ): """Test when a node is replaced with a different node.""" - hank_binary_switch_state = deepcopy(hank_binary_switch_state) - multisensor_6_state = deepcopy(multisensor_6_state) - hank_binary_switch_state["nodeId"] = multisensor_6_state["nodeId"] dev_reg = dr.async_get(hass) - old_node = Node(client, multisensor_6_state) - device_id = f"{client.driver.controller.home_id}-{old_node.node_id}" - new_node = Node(client, hank_binary_switch_state) - event = {"node": old_node} + node_id = multisensor_6.node_id + hank_binary_switch_state = deepcopy(hank_binary_switch_state) + hank_binary_switch_state["nodeId"] = node_id + + device_id = f"{client.driver.controller.home_id}-{node_id}" + multisensor_6_device_id = ( + f"{device_id}-{multisensor_6.manufacturer_id}:" + f"{multisensor_6.product_type}:{multisensor_6.product_id}" + ) + hank_device_id = ( + f"{device_id}-{hank_binary_switch_state['manufacturerId']}:" + f"{hank_binary_switch_state['productType']}:" + f"{hank_binary_switch_state['productId']}" + ) - client.driver.controller.emit("node added", event) - await hass.async_block_till_done() device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) assert device + assert device == dev_reg.async_get_device( + identifiers={(DOMAIN, multisensor_6_device_id)} + ) + assert device.manufacturer == "AEON Labs" + assert device.model == "ZW100" + dev_id = device.id - event = {"node": old_node, "replaced": True} + assert hass.states.get(AIR_TEMPERATURE_SENSOR) - client.driver.controller.emit("node removed", event) + # A replace node event has the extra field "replaced" set to True + # to distinguish it from an exclusion + event = Event( + type="node removed", + data={ + "source": "controller", + "event": "node removed", + "replaced": True, + "node": multisensor_6_state, + }, + ) + client.driver.receive_event(event) await hass.async_block_till_done() + # Device should still be there after the node was removed + device = dev_reg.async_get(dev_id) assert device - event = {"node": new_node} + # When the node is replaced, a non-ready node added event is emitted + event = Event( + type="node added", + data={ + "source": "controller", + "event": "node added", + "node": { + "nodeId": multisensor_6.node_id, + "index": 0, + "status": 4, + "ready": False, + "isSecure": "unknown", + "interviewAttempts": 1, + "endpoints": [ + {"nodeId": multisensor_6.node_id, "index": 0, "deviceClass": None} + ], + "values": [], + "deviceClass": None, + "commandClasses": [], + "interviewStage": "None", + "statistics": { + "commandsTX": 0, + "commandsRX": 0, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 0, + }, + }, + }, + ) - client.driver.controller.emit("node added", event) + # Device is still not removed + client.driver.receive_event(event) await hass.async_block_till_done() - device = dev_reg.async_get(device.id) - # assert device is new + + device = dev_reg.async_get(dev_id) assert device + + event = Event( + type="ready", + data={ + "source": "node", + "event": "ready", + "nodeId": node_id, + "nodeState": hank_binary_switch_state, + }, + ) + client.driver.receive_event(event) + await hass.async_block_till_done() + + # Old device and entities were removed, but the ID is re-used + device = dev_reg.async_get(dev_id) + assert device + assert device == dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) + assert device == dev_reg.async_get_device(identifiers={(DOMAIN, hank_device_id)}) + assert not dev_reg.async_get_device(identifiers={(DOMAIN, multisensor_6_device_id)}) assert device.manufacturer == "HANK Electronics Ltd." + assert device.model == "HKZW-SO01" + + assert not hass.states.get(AIR_TEMPERATURE_SENSOR) + assert hass.states.get("switch.smart_plug_with_two_usb_ports") + + +async def test_node_model_change(hass, zp3111, client, integration): + """Test when a node's model is changed due to an updated device config file. + + The device and entities should not be removed. + """ + dev_reg = dr.async_get(hass) + er_reg = er.async_get(hass) + + device_id = f"{client.driver.controller.home_id}-{zp3111.node_id}" + device_id_ext = ( + f"{device_id}-{zp3111.manufacturer_id}:" + f"{zp3111.product_type}:{zp3111.product_id}" + ) + + # Verify device and entities have default names/ids + device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) + assert device + assert device == dev_reg.async_get_device(identifiers={(DOMAIN, device_id_ext)}) + assert device.manufacturer == "Vision Security" + assert device.model == "ZP3111-5" + assert device.name == "4-in-1 Sensor" + assert not device.name_by_user + + dev_id = device.id + + motion_entity = "binary_sensor.4_in_1_sensor_home_security_motion_detection" + state = hass.states.get(motion_entity) + assert state + assert state.name == "4-in-1 Sensor: Home Security - Motion detection" + + # Customize device and entity names/ids + dev_reg.async_update_device(device.id, name_by_user="Custom Device Name") + device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) + assert device + assert device.id == dev_id + assert device == dev_reg.async_get_device(identifiers={(DOMAIN, device_id_ext)}) + assert device.manufacturer == "Vision Security" + assert device.model == "ZP3111-5" + assert device.name == "4-in-1 Sensor" + assert device.name_by_user == "Custom Device Name" + + custom_entity = "binary_sensor.custom_motion_sensor" + er_reg.async_update_entity( + motion_entity, new_entity_id=custom_entity, name="Custom Entity Name" + ) + await hass.async_block_till_done() + assert not hass.states.get(motion_entity) + state = hass.states.get(custom_entity) + assert state + assert state.name == "Custom Entity Name" + + # Unload the integration + assert await hass.config_entries.async_unload(integration.entry_id) + await hass.async_block_till_done() + assert integration.state is ConfigEntryState.NOT_LOADED + assert not hass.data.get(DOMAIN) + + # Simulate changes to the node labels + zp3111.device_config.data["description"] = "New Device Name" + zp3111.device_config.data["label"] = "New Device Model" + zp3111.device_config.data["manufacturer"] = "New Device Manufacturer" + + # Reload integration, it will re-add the nodes + integration.add_to_hass(hass) + await hass.config_entries.async_setup(integration.entry_id) + await hass.async_block_till_done() + + # Device name changes, but the customization is the same + device = dev_reg.async_get(dev_id) + assert device + assert device.id == dev_id + assert device.manufacturer == "New Device Manufacturer" + assert device.model == "New Device Model" + assert device.name == "New Device Name" + assert device.name_by_user == "Custom Device Name" + + assert not hass.states.get(motion_entity) + state = hass.states.get(custom_entity) + assert state + assert state.name == "Custom Entity Name" + + +async def test_disabled_node_status_entity_on_node_replaced( + hass, zp3111_state, zp3111, client, integration +): + """Test that when a node replacement event is received the node status sensor is removed.""" + node_status_entity = "sensor.4_in_1_sensor_node_status" + state = hass.states.get(node_status_entity) + assert state + assert state.state != STATE_UNAVAILABLE + + event = Event( + type="node removed", + data={ + "source": "controller", + "event": "node removed", + "replaced": True, + "node": zp3111_state, + }, + ) + client.driver.receive_event(event) + await hass.async_block_till_done() + + state = hass.states.get(node_status_entity) + assert state + assert state.state == STATE_UNAVAILABLE + + +async def test_disabled_entity_on_value_removed(hass, zp3111, client, integration): + """Test that when entity primary values are removed the entity is removed.""" + er_reg = er.async_get(hass) + + # re-enable this default-disabled entity + sensor_cover_entity = "sensor.4_in_1_sensor_home_security_cover_status" + er_reg.async_update_entity(entity_id=sensor_cover_entity, disabled_by=None) + await hass.async_block_till_done() + + # must reload the integration when enabling an entity + await hass.config_entries.async_unload(integration.entry_id) + await hass.async_block_till_done() + assert integration.state is ConfigEntryState.NOT_LOADED + integration.add_to_hass(hass) + await hass.config_entries.async_setup(integration.entry_id) + await hass.async_block_till_done() + assert integration.state is ConfigEntryState.LOADED + + state = hass.states.get(sensor_cover_entity) + assert state + assert state.state != STATE_UNAVAILABLE + + # check for expected entities + binary_cover_entity = ( + "binary_sensor.4_in_1_sensor_home_security_tampering_product_cover_removed" + ) + state = hass.states.get(binary_cover_entity) + assert state + assert state.state != STATE_UNAVAILABLE + + battery_level_entity = "sensor.4_in_1_sensor_battery_level" + state = hass.states.get(battery_level_entity) + assert state + assert state.state != STATE_UNAVAILABLE + + unavailable_entities = { + state.entity_id + for state in hass.states.async_all() + if state.state == STATE_UNAVAILABLE + } + + # This value ID removal does not remove any entity + event = Event( + type="value removed", + data={ + "source": "node", + "event": "value removed", + "nodeId": zp3111.node_id, + "args": { + "commandClassName": "Wake Up", + "commandClass": 132, + "endpoint": 0, + "property": "wakeUpInterval", + "prevValue": 3600, + "propertyName": "wakeUpInterval", + }, + }, + ) + client.driver.receive_event(event) + await hass.async_block_till_done() + + assert all(state != STATE_UNAVAILABLE for state in hass.states.async_all()) + + # This value ID removal only affects the battery level entity + event = Event( + type="value removed", + data={ + "source": "node", + "event": "value removed", + "nodeId": zp3111.node_id, + "args": { + "commandClassName": "Battery", + "commandClass": 128, + "endpoint": 0, + "property": "level", + "prevValue": 100, + "propertyName": "level", + }, + }, + ) + client.driver.receive_event(event) + await hass.async_block_till_done() + + state = hass.states.get(battery_level_entity) + assert state + assert state.state == STATE_UNAVAILABLE + + # This value ID removal affects its multiple notification sensors + event = Event( + type="value removed", + data={ + "source": "node", + "event": "value removed", + "nodeId": zp3111.node_id, + "args": { + "commandClassName": "Notification", + "commandClass": 113, + "endpoint": 0, + "property": "Home Security", + "propertyKey": "Cover status", + "prevValue": 0, + "propertyName": "Home Security", + "propertyKeyName": "Cover status", + }, + }, + ) + client.driver.receive_event(event) + await hass.async_block_till_done() + + state = hass.states.get(binary_cover_entity) + assert state + assert state.state == STATE_UNAVAILABLE + + state = hass.states.get(sensor_cover_entity) + assert state + assert state.state == STATE_UNAVAILABLE + + # existing entities and the entities with removed values should be unavailable + new_unavailable_entities = { + state.entity_id + for state in hass.states.async_all() + if state.state == STATE_UNAVAILABLE + } + assert ( + unavailable_entities + | {battery_level_entity, binary_cover_entity, sensor_cover_entity} + == new_unavailable_entities + ) From 35bb19b4ebba10202f1cc33e743e25dcdc0a827e Mon Sep 17 00:00:00 2001 From: gjong Date: Mon, 27 Dec 2021 12:32:25 +0100 Subject: [PATCH 1048/2644] Upgrade youless library to version 0.16 (#62837) --- homeassistant/components/youless/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/youless/manifest.json b/homeassistant/components/youless/manifest.json index 49a5b6187e6..f5713c51680 100644 --- a/homeassistant/components/youless/manifest.json +++ b/homeassistant/components/youless/manifest.json @@ -3,7 +3,7 @@ "name": "YouLess", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/youless", - "requirements": ["youless-api==0.15"], + "requirements": ["youless-api==0.16"], "codeowners": ["@gjong"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index a301ee040a9..da911d50a23 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2494,7 +2494,7 @@ yeelight==0.7.8 yeelightsunflower==0.0.10 # homeassistant.components.youless -youless-api==0.15 +youless-api==0.16 # homeassistant.components.media_extractor youtube_dl==2021.06.06 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b394918f4fc..03aa30aa325 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1492,7 +1492,7 @@ yalexs==1.1.16 yeelight==0.7.8 # homeassistant.components.youless -youless-api==0.15 +youless-api==0.16 # homeassistant.components.zeroconf zeroconf==0.38.1 From cf6fb7bf3991bbcdbbcd85ca3821506cac449459 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Mon, 27 Dec 2021 03:35:59 -0800 Subject: [PATCH 1049/2644] Add light entity to Overkiz integration (#62835) --- .coveragerc | 1 + homeassistant/components/overkiz/const.py | 2 + homeassistant/components/overkiz/light.py | 109 ++++++++++++++++++++++ 3 files changed, 112 insertions(+) create mode 100644 homeassistant/components/overkiz/light.py diff --git a/.coveragerc b/.coveragerc index 2113ea0c202..f409745475f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -807,6 +807,7 @@ omit = homeassistant/components/overkiz/coordinator.py homeassistant/components/overkiz/entity.py homeassistant/components/overkiz/executor.py + homeassistant/components/overkiz/light.py homeassistant/components/overkiz/lock.py homeassistant/components/overkiz/number.py homeassistant/components/overkiz/sensor.py diff --git a/homeassistant/components/overkiz/const.py b/homeassistant/components/overkiz/const.py index f1d1d0fdb74..543cd1e187a 100644 --- a/homeassistant/components/overkiz/const.py +++ b/homeassistant/components/overkiz/const.py @@ -19,6 +19,7 @@ UPDATE_INTERVAL_ALL_ASSUMED_STATE: Final = timedelta(minutes=60) PLATFORMS: list[Platform] = [ Platform.BUTTON, + Platform.LIGHT, Platform.LOCK, Platform.NUMBER, Platform.SENSOR, @@ -32,4 +33,5 @@ IGNORED_OVERKIZ_DEVICES: list[UIClass | UIWidget] = [ # Used to map the Somfy widget and ui_class to the Home Assistant platform OVERKIZ_DEVICE_TO_PLATFORM: dict[UIClass | UIWidget, Platform] = { UIClass.DOOR_LOCK: Platform.LOCK, + UIClass.LIGHT: Platform.LIGHT, } diff --git a/homeassistant/components/overkiz/light.py b/homeassistant/components/overkiz/light.py new file mode 100644 index 00000000000..63510f67567 --- /dev/null +++ b/homeassistant/components/overkiz/light.py @@ -0,0 +1,109 @@ +"""Support for Overkiz lights.""" +from __future__ import annotations + +from typing import Any + +from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_RGB_COLOR, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_ONOFF, + COLOR_MODE_RGB, + LightEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import HomeAssistantOverkizData +from .const import DOMAIN +from .coordinator import OverkizDataUpdateCoordinator +from .entity import OverkizEntity + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +): + """Set up the Overkiz lights from a config entry.""" + data: HomeAssistantOverkizData = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + OverkizLight(device.device_url, data.coordinator) + for device in data.platforms[Platform.LIGHT] + ) + + +class OverkizLight(OverkizEntity, LightEntity): + """Representation of an Overkiz Light.""" + + def __init__( + self, device_url: str, coordinator: OverkizDataUpdateCoordinator + ) -> None: + """Initialize a device.""" + super().__init__(device_url, coordinator) + + self._attr_supported_color_modes = set() + + if self.executor.has_command(OverkizCommand.SET_RGB): + self._attr_supported_color_modes.add(COLOR_MODE_RGB) + if self.executor.has_command(OverkizCommand.SET_INTENSITY): + self._attr_supported_color_modes.add(COLOR_MODE_BRIGHTNESS) + if not self.supported_color_modes: + self._attr_supported_color_modes = {COLOR_MODE_ONOFF} + + @property + def is_on(self) -> bool: + """Return true if light is on.""" + return ( + self.executor.select_state(OverkizState.CORE_ON_OFF) + == OverkizCommandParam.ON + ) + + @property + def rgb_color(self) -> tuple[int, int, int] | None: + """Return the rgb color value [int, int, int] (0-255).""" + red = self.executor.select_state(OverkizState.CORE_RED_COLOR_INTENSITY) + green = self.executor.select_state(OverkizState.CORE_GREEN_COLOR_INTENSITY) + blue = self.executor.select_state(OverkizState.CORE_BLUE_COLOR_INTENSITY) + + if red is None or green is None or blue is None: + return None + + return (int(red), int(green), int(blue)) + + @property + def brightness(self) -> int | None: + """Return the brightness of this light (0-255).""" + if brightness := self.executor.select_state(OverkizState.CORE_LIGHT_INTENSITY): + return round(int(brightness) * 255 / 100) + + return None + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the light on.""" + rgb_color = kwargs.get(ATTR_RGB_COLOR) + brightness = kwargs.get(ATTR_BRIGHTNESS) + + if rgb_color is not None: + await self.executor.async_execute_command( + OverkizCommand.SET_RGB, + *[round(float(c)) for c in kwargs[ATTR_RGB_COLOR]], + ) + return + + if brightness is not None: + await self.executor.async_execute_command( + OverkizCommand.SET_INTENSITY, round(float(brightness) / 255 * 100) + ) + return + + await self.executor.async_execute_command(OverkizCommand.ON) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the light off.""" + await self.executor.async_execute_command(OverkizCommand.OFF) From dc3f21dd1e353569d1a1ebaef481886b07430822 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Mon, 27 Dec 2021 06:39:24 -0500 Subject: [PATCH 1050/2644] Followup PR for UniFi Protect integration (#62806) * Followup improvements from initial PR * Update tests/components/unifiprotect/conftest.py Co-authored-by: J. Nick Koston * Update translations * Fixes log message * Fixes log message * Unknown to cannot connect * Update tests/components/unifiprotect/test_config_flow.py Co-authored-by: Martin Hjelmare * Fixes camera coverage Co-authored-by: J. Nick Koston Co-authored-by: Martin Hjelmare --- .../components/unifiprotect/__init__.py | 10 +- .../components/unifiprotect/camera.py | 16 +- .../components/unifiprotect/config_flow.py | 14 +- .../components/unifiprotect/const.py | 20 +- .../components/unifiprotect/entity.py | 22 +- .../components/unifiprotect/strings.json | 6 +- .../unifiprotect/translations/en.json | 4 +- tests/components/unifiprotect/conftest.py | 16 +- tests/components/unifiprotect/test_camera.py | 252 ++++++++++++------ .../unifiprotect/test_config_flow.py | 11 +- tests/components/unifiprotect/test_init.py | 4 +- 11 files changed, 227 insertions(+), 148 deletions(-) diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index 84cef044c6b..49e88f866b6 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -29,6 +29,7 @@ from .const import ( DEVICES_FOR_SUBSCRIBE, DOMAIN, MIN_REQUIRED_PROTECT_V, + OUTDATED_LOG_MESSAGE, PLATFORMS, ) from .data import ProtectData @@ -60,15 +61,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: nvr_info = await protect.get_nvr() except NotAuthorized as err: raise ConfigEntryAuthFailed(err) from err - except (asyncio.TimeoutError, NvrError, ServerDisconnectedError) as notreadyerror: - raise ConfigEntryNotReady from notreadyerror + except (asyncio.TimeoutError, NvrError, ServerDisconnectedError) as err: + raise ConfigEntryNotReady from err if nvr_info.version < MIN_REQUIRED_PROTECT_V: _LOGGER.error( - ( - "You are running v%s of UniFi Protect. Minimum required version is v%s. " - "Please upgrade UniFi Protect and then retry" - ), + OUTDATED_LOG_MESSAGE, nvr_info.version, MIN_REQUIRED_PROTECT_V, ) diff --git a/homeassistant/components/unifiprotect/camera.py b/homeassistant/components/unifiprotect/camera.py index 2aeaefd322a..95bc630e58d 100644 --- a/homeassistant/components/unifiprotect/camera.py +++ b/homeassistant/components/unifiprotect/camera.py @@ -1,9 +1,8 @@ """Support for Ubiquiti's UniFi Protect NVR.""" from __future__ import annotations -from collections.abc import Callable, Generator, Sequence +from collections.abc import Generator import logging -from typing import Any from pyunifiprotect.api import ProtectApiClient from pyunifiprotect.data import Camera as UFPCamera @@ -12,7 +11,7 @@ from pyunifiprotect.data.devices import CameraChannel from homeassistant.components.camera import SUPPORT_STREAM, Camera from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( ATTR_BITRATE, @@ -33,7 +32,7 @@ def get_camera_channels( ) -> Generator[tuple[UFPCamera, CameraChannel, bool], None, None]: """Get all the camera channels.""" for camera in protect.bootstrap.cameras.values(): - if len(camera.channels) == 0: + if not camera.channels: _LOGGER.warning( "Camera does not have any channels: %s (id: %s)", camera.name, camera.id ) @@ -53,7 +52,7 @@ def get_camera_channels( async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[Sequence[Entity]], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Discover cameras on a UniFi Protect NVR.""" data: ProtectData = hass.data[DOMAIN][entry.entry_id] @@ -143,12 +142,7 @@ class ProtectCamera(ProtectDeviceEntity, Camera): self._attr_is_recording = self.device.is_connected and self.device.is_recording self._async_set_stream_source() - - @callback - def _async_update_extra_attrs_from_protect(self) -> dict[str, Any]: - """Add additional Attributes to Camera.""" - return { - **super()._async_update_extra_attrs_from_protect(), + self._attr_extra_state_attributes = { ATTR_WIDTH: self.channel.width, ATTR_HEIGHT: self.channel.height, ATTR_FPS: self.channel.fps, diff --git a/homeassistant/components/unifiprotect/config_flow.py b/homeassistant/components/unifiprotect/config_flow.py index 805318a9af5..5298e162f1f 100644 --- a/homeassistant/components/unifiprotect/config_flow.py +++ b/homeassistant/components/unifiprotect/config_flow.py @@ -30,6 +30,7 @@ from .const import ( DEFAULT_VERIFY_SSL, DOMAIN, MIN_REQUIRED_PROTECT_V, + OUTDATED_LOG_MESSAGE, ) _LOGGER = logging.getLogger(__name__) @@ -55,7 +56,7 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return OptionsFlowHandler(config_entry) @callback - async def _async_create_entry(self, title: str, data: dict[str, Any]) -> FlowResult: + def _async_create_entry(self, title: str, data: dict[str, Any]) -> FlowResult: return self.async_create_entry( title=title, data={**data, CONF_ID: title}, @@ -66,7 +67,6 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): }, ) - @callback async def _async_get_nvr_data( self, user_input: dict[str, Any], @@ -97,10 +97,14 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors[CONF_PASSWORD] = "invalid_auth" except NvrError as ex: _LOGGER.debug(ex) - errors["base"] = "nvr_error" + errors["base"] = "cannot_connect" else: if nvr_data.version < MIN_REQUIRED_PROTECT_V: - _LOGGER.debug("UniFi Protect Version not supported") + _LOGGER.debug( + OUTDATED_LOG_MESSAGE, + nvr_data.version, + MIN_REQUIRED_PROTECT_V, + ) errors["base"] = "protect_version" return nvr_data, errors @@ -156,7 +160,7 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(nvr_data.mac) self._abort_if_unique_id_configured() - return await self._async_create_entry(nvr_data.name, user_input) + return self._async_create_entry(nvr_data.name, user_input) user_input = user_input or {} return self.async_show_form( diff --git a/homeassistant/components/unifiprotect/const.py b/homeassistant/components/unifiprotect/const.py index 049cb5cc0ac..37ba103f4ba 100644 --- a/homeassistant/components/unifiprotect/const.py +++ b/homeassistant/components/unifiprotect/const.py @@ -1,25 +1,18 @@ """Constant definitions for UniFi Protect Integration.""" -# from typing_extensions import Required -from datetime import timedelta - from pyunifiprotect.data.types import ModelType, Version +from homeassistant.const import Platform + DOMAIN = "unifiprotect" -ATTR_EVENT_SCORE = "event_score" -ATTR_EVENT_OBJECT = "event_object" -ATTR_EVENT_THUMB = "event_thumbnail" ATTR_WIDTH = "width" ATTR_HEIGHT = "height" ATTR_FPS = "fps" ATTR_BITRATE = "bitrate" ATTR_CHANNEL_ID = "channel_id" -CONF_DOORBELL_TEXT = "doorbell_text" CONF_DISABLE_RTSP = "disable_rtsp" -CONF_MESSAGE = "message" -CONF_DURATION = "duration" CONF_ALL_UPDATES = "all_updates" CONF_OVERRIDE_CHOST = "override_connection_host" @@ -32,11 +25,9 @@ CONFIG_OPTIONS = [ DEFAULT_PORT = 443 DEFAULT_ATTRIBUTION = "Powered by UniFi Protect Server" DEFAULT_BRAND = "Ubiquiti" -DEFAULT_SCAN_INTERVAL = 2 +DEFAULT_SCAN_INTERVAL = 5 DEFAULT_VERIFY_SSL = False -RING_INTERVAL = timedelta(seconds=3) - DEVICE_TYPE_CAMERA = "camera" DEVICES_THAT_ADOPT = { ModelType.CAMERA, @@ -48,7 +39,6 @@ DEVICES_WITH_ENTITIES = DEVICES_THAT_ADOPT | {ModelType.NVR} DEVICES_FOR_SUBSCRIBE = DEVICES_WITH_ENTITIES | {ModelType.EVENT} MIN_REQUIRED_PROTECT_V = Version("1.20.0") +OUTDATED_LOG_MESSAGE = "You are running v%s of UniFi Protect. Minimum required version is v%s. Please upgrade UniFi Protect and then retry" -PLATFORMS = [ - "camera", -] +PLATFORMS = [Platform.CAMERA] diff --git a/homeassistant/components/unifiprotect/entity.py b/homeassistant/components/unifiprotect/entity.py index 61e9f6225b6..c7ae7344345 100644 --- a/homeassistant/components/unifiprotect/entity.py +++ b/homeassistant/components/unifiprotect/entity.py @@ -1,11 +1,8 @@ """Shared Entity definition for UniFi Protect Integration.""" from __future__ import annotations -from typing import Any - from pyunifiprotect.data import ProtectAdoptableDeviceModel -from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import callback import homeassistant.helpers.device_registry as dr from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription @@ -45,6 +42,7 @@ class ProtectDeviceEntity(Entity): name = description.name or "" self._attr_name = f"{self.device.name} {name.title()}" + self._attr_attribution = DEFAULT_ATTRIBUTION self._async_set_device_info() self._async_update_device_from_protect() @@ -55,16 +53,6 @@ class ProtectDeviceEntity(Entity): """ await self.data.async_refresh() - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return UniFi Protect device attributes.""" - attrs = super().extra_state_attributes or {} - return { - **attrs, - ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION, - **self._extra_state_attributes, - } - @callback def _async_set_device_info(self) -> None: self._attr_device_info = DeviceInfo( @@ -77,13 +65,6 @@ class ProtectDeviceEntity(Entity): configuration_url=self.device.protect_url, ) - @callback - def _async_update_extra_attrs_from_protect( # pylint: disable=no-self-use - self, - ) -> dict[str, Any]: - """Calculate extra state attributes. Primarily for subclass to override.""" - return {} - @callback def _async_update_device_from_protect(self) -> None: """Update Entity object from Protect device.""" @@ -95,7 +76,6 @@ class ProtectDeviceEntity(Entity): self._attr_available = ( self.data.last_update_success and self.device.is_connected ) - self._extra_state_attributes = self._async_update_extra_attrs_from_protect() @callback def _async_updated_event(self) -> None: diff --git a/homeassistant/components/unifiprotect/strings.json b/homeassistant/components/unifiprotect/strings.json index 4dd000aade8..e397e0cc922 100644 --- a/homeassistant/components/unifiprotect/strings.json +++ b/homeassistant/components/unifiprotect/strings.json @@ -12,7 +12,7 @@ "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "protect_version": "Minimum required version is v1.20.0. Please upgrade UniFi Protect and then retry." }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" @@ -22,7 +22,7 @@ "step": { "init": { "title": "UniFi Protect Options", - "description": "Realtime metrics option should only be enabled if you have enabled the diagnostics sensors and want them updated in realtime. If if not enabled, they will only update once every 15 minutes.", + "description": "Realtime metrics option should only be enabled if you have enabled the diagnostics sensors and want them updated in realtime. If not enabled, they will only update once every 15 minutes.", "data": { "disable_rtsp": "Disable the RTSP stream", "all_updates": "Realtime metrics (WARNING: Greatly increases CPU usage)", @@ -31,4 +31,4 @@ } } } -} \ No newline at end of file +} diff --git a/homeassistant/components/unifiprotect/translations/en.json b/homeassistant/components/unifiprotect/translations/en.json index d62fcd651ec..a28b9af1e32 100644 --- a/homeassistant/components/unifiprotect/translations/en.json +++ b/homeassistant/components/unifiprotect/translations/en.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" + "protect_version": "Minimum required version is v1.20.0. Please upgrade UniFi Protect and then retry." }, "step": { "user": { @@ -26,7 +26,7 @@ "disable_rtsp": "Disable the RTSP stream", "override_connection_host": "Override Connection Host" }, - "description": "Realtime metrics option should only be enabled if you have enabled the diagnostics sensors and want them updated in realtime. If if not enabled, they will only update once every 15 minutes.", + "description": "Realtime metrics option should only be enabled if you have enabled the diagnostics sensors and want them updated in realtime. If not enabled, they will only update once every 15 minutes.", "title": "UniFi Protect Options" } } diff --git a/tests/components/unifiprotect/conftest.py b/tests/components/unifiprotect/conftest.py index ba0dcabd6bc..ac4e1491b38 100644 --- a/tests/components/unifiprotect/conftest.py +++ b/tests/components/unifiprotect/conftest.py @@ -161,8 +161,22 @@ async def simple_camera( yield (camera, "camera.test_camera_high") -async def time_changed(hass, seconds): +async def time_changed(hass: HomeAssistant, seconds: int) -> None: """Trigger time changed.""" next_update = dt_util.utcnow() + timedelta(seconds) async_fire_time_changed(hass, next_update) await hass.async_block_till_done() + + +async def enable_entity( + hass: HomeAssistant, entry_id: str, entity_id: str +) -> er.RegistryEntry: + """Enable a disabled entity.""" + entity_registry = er.async_get(hass) + + updated_entity = entity_registry.async_update_entity(entity_id, disabled_by=None) + assert not updated_entity.disabled + await hass.config_entries.async_reload(entry_id) + await hass.async_block_till_done() + + return updated_entity diff --git a/tests/components/unifiprotect/test_camera.py b/tests/components/unifiprotect/test_camera.py index d3a122c401c..1ab9e56ff11 100644 --- a/tests/components/unifiprotect/test_camera.py +++ b/tests/components/unifiprotect/test_camera.py @@ -2,13 +2,18 @@ from __future__ import annotations from copy import copy -from typing import cast from unittest.mock import AsyncMock, Mock from pyunifiprotect.data import Camera as ProtectCamera +from pyunifiprotect.data.devices import CameraChannel from pyunifiprotect.exceptions import NvrError -from homeassistant.components.camera import Camera, async_get_image +from homeassistant.components.camera import ( + SUPPORT_STREAM, + Camera, + async_get_image, + async_get_stream_source, +) from homeassistant.components.unifiprotect.const import ( ATTR_BITRATE, ATTR_CHANNEL_ID, @@ -17,60 +22,96 @@ from homeassistant.components.unifiprotect.const import ( ATTR_WIDTH, DEFAULT_ATTRIBUTION, DEFAULT_SCAN_INTERVAL, - DOMAIN, ) -from homeassistant.components.unifiprotect.data import ProtectData -from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ENTITY_ID +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component -from .conftest import MockEntityFixture, time_changed +from .conftest import MockEntityFixture, enable_entity, time_changed -async def validate_camera_entity( +def validate_default_camera_entity( hass: HomeAssistant, camera: ProtectCamera, channel_id: int, - secure: bool, - rtsp_enabled: bool, - enabled: bool, -): +) -> str: """Validate a camera entity.""" channel = camera.channels[channel_id] entity_name = f"{camera.name} {channel.name}" unique_id = f"{camera.id}_{channel.id}" - if not secure: - entity_name += " Insecure" - unique_id += "_insecure" entity_id = f"camera.{entity_name.replace(' ', '_').lower()}" entity_registry = er.async_get(hass) entity = entity_registry.async_get(entity_id) assert entity - assert entity.disabled is (not enabled) + assert entity.disabled is False assert entity.unique_id == unique_id - if not enabled: - return + return entity_id - camera_platform = hass.data.get("camera") - assert camera_platform - ha_camera = cast(Camera, camera_platform.get_entity(entity_id)) - assert ha_camera - if rtsp_enabled: - if secure: - assert await ha_camera.stream_source() == channel.rtsps_url - else: - assert await ha_camera.stream_source() == channel.rtsp_url - else: - assert await ha_camera.stream_source() is None +def validate_rtsps_camera_entity( + hass: HomeAssistant, + camera: ProtectCamera, + channel_id: int, +) -> str: + """Validate a disabled RTSPS camera entity.""" + + channel = camera.channels[channel_id] + + entity_name = f"{camera.name} {channel.name}" + unique_id = f"{camera.id}_{channel.id}" + entity_id = f"camera.{entity_name.replace(' ', '_').lower()}" + + entity_registry = er.async_get(hass) + entity = entity_registry.async_get(entity_id) + assert entity + assert entity.disabled is True + assert entity.unique_id == unique_id + + return entity_id + + +def validate_rtsp_camera_entity( + hass: HomeAssistant, + camera: ProtectCamera, + channel_id: int, +) -> str: + """Validate a disabled RTSP camera entity.""" + + channel = camera.channels[channel_id] + + entity_name = f"{camera.name} {channel.name} Insecure" + unique_id = f"{camera.id}_{channel.id}_insecure" + entity_id = f"camera.{entity_name.replace(' ', '_').lower()}" + + entity_registry = er.async_get(hass) + entity = entity_registry.async_get(entity_id) + assert entity + assert entity.disabled is True + assert entity.unique_id == unique_id + + return entity_id + + +def validate_common_camera_state( + hass: HomeAssistant, + channel: CameraChannel, + entity_id: str, + features: int = SUPPORT_STREAM, +): + """Validate state that is common to all camera entity, regradless of type.""" entity_state = hass.states.get(entity_id) assert entity_state assert entity_state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION + assert entity_state.attributes[ATTR_SUPPORTED_FEATURES] == features assert entity_state.attributes[ATTR_WIDTH] == channel.width assert entity_state.attributes[ATTR_HEIGHT] == channel.height assert entity_state.attributes[ATTR_FPS] == channel.fps @@ -78,6 +119,48 @@ async def validate_camera_entity( assert entity_state.attributes[ATTR_CHANNEL_ID] == channel.id +async def validate_rtsps_camera_state( + hass: HomeAssistant, + camera: ProtectCamera, + channel_id: int, + entity_id: str, + features: int = SUPPORT_STREAM, +): + """Validate a camera's state.""" + channel = camera.channels[channel_id] + + assert await async_get_stream_source(hass, entity_id) == channel.rtsps_url + validate_common_camera_state(hass, channel, entity_id, features) + + +async def validate_rtsp_camera_state( + hass: HomeAssistant, + camera: ProtectCamera, + channel_id: int, + entity_id: str, + features: int = SUPPORT_STREAM, +): + """Validate a camera's state.""" + channel = camera.channels[channel_id] + + assert await async_get_stream_source(hass, entity_id) == channel.rtsp_url + validate_common_camera_state(hass, channel, entity_id, features) + + +async def validate_no_stream_camera_state( + hass: HomeAssistant, + camera: ProtectCamera, + channel_id: int, + entity_id: str, + features: int = SUPPORT_STREAM, +): + """Validate a camera's state.""" + channel = camera.channels[channel_id] + + assert await async_get_stream_source(hass, entity_id) is None + validate_common_camera_state(hass, channel, entity_id, features) + + async def test_basic_setup( hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: ProtectCamera ): @@ -96,12 +179,25 @@ async def test_basic_setup( camera_high_only.channels[1].is_rtsp_enabled = False camera_high_only.channels[2].is_rtsp_enabled = False + camera_medium_only = mock_camera.copy(deep=True) + camera_medium_only._api = mock_entry.api + camera_medium_only.channels[0]._api = mock_entry.api + camera_medium_only.channels[1]._api = mock_entry.api + camera_medium_only.channels[2]._api = mock_entry.api + camera_medium_only.name = "Test Camera 2" + camera_medium_only.id = "test_medium" + camera_medium_only.channels[0].is_rtsp_enabled = False + camera_medium_only.channels[1].is_rtsp_enabled = True + camera_medium_only.channels[1].name = "Medium" + camera_medium_only.channels[1].rtsp_alias = "test_medium_alias" + camera_medium_only.channels[2].is_rtsp_enabled = False + camera_all_channels = mock_camera.copy(deep=True) camera_all_channels._api = mock_entry.api camera_all_channels.channels[0]._api = mock_entry.api camera_all_channels.channels[1]._api = mock_entry.api camera_all_channels.channels[2]._api = mock_entry.api - camera_all_channels.name = "Test Camera 2" + camera_all_channels.name = "Test Camera 3" camera_all_channels.id = "test_all" camera_all_channels.channels[0].is_rtsp_enabled = True camera_all_channels.channels[0].name = "High" @@ -118,7 +214,7 @@ async def test_basic_setup( camera_no_channels.channels[0]._api = mock_entry.api camera_no_channels.channels[1]._api = mock_entry.api camera_no_channels.channels[2]._api = mock_entry.api - camera_no_channels.name = "Test Camera 3" + camera_no_channels.name = "Test Camera 4" camera_no_channels.id = "test_none" camera_no_channels.channels[0].is_rtsp_enabled = False camera_no_channels.channels[0].name = "High" @@ -127,6 +223,7 @@ async def test_basic_setup( mock_entry.api.bootstrap.cameras = { camera_high_only.id: camera_high_only, + camera_medium_only.id: camera_medium_only, camera_all_channels.id: camera_all_channels, camera_no_channels.id: camera_no_channels, } @@ -134,34 +231,55 @@ async def test_basic_setup( await hass.config_entries.async_setup(mock_entry.entry.entry_id) await hass.async_block_till_done() - await validate_camera_entity( - hass, camera_high_only, 0, secure=True, rtsp_enabled=True, enabled=True - ) - await validate_camera_entity( - hass, camera_high_only, 0, secure=False, rtsp_enabled=True, enabled=False - ) + entity_registry = er.async_get(hass) - await validate_camera_entity( - hass, camera_all_channels, 0, secure=True, rtsp_enabled=True, enabled=True - ) - await validate_camera_entity( - hass, camera_all_channels, 0, secure=False, rtsp_enabled=True, enabled=False - ) - await validate_camera_entity( - hass, camera_all_channels, 1, secure=True, rtsp_enabled=True, enabled=False - ) - await validate_camera_entity( - hass, camera_all_channels, 1, secure=False, rtsp_enabled=True, enabled=False - ) - await validate_camera_entity( - hass, camera_all_channels, 2, secure=True, rtsp_enabled=True, enabled=False - ) - await validate_camera_entity( - hass, camera_all_channels, 2, secure=False, rtsp_enabled=True, enabled=False - ) + assert len(hass.states.async_all()) == 4 + assert len(entity_registry.entities) == 11 - await validate_camera_entity( - hass, camera_no_channels, 0, secure=True, rtsp_enabled=False, enabled=True + # test camera 1 + entity_id = validate_default_camera_entity(hass, camera_high_only, 0) + await validate_rtsps_camera_state(hass, camera_high_only, 0, entity_id) + + entity_id = validate_rtsp_camera_entity(hass, camera_high_only, 0) + await enable_entity(hass, mock_entry.entry.entry_id, entity_id) + await validate_rtsp_camera_state(hass, camera_high_only, 0, entity_id) + + # test camera 2 + entity_id = validate_default_camera_entity(hass, camera_medium_only, 1) + await validate_rtsps_camera_state(hass, camera_medium_only, 1, entity_id) + + entity_id = validate_rtsp_camera_entity(hass, camera_medium_only, 1) + await enable_entity(hass, mock_entry.entry.entry_id, entity_id) + await validate_rtsp_camera_state(hass, camera_medium_only, 1, entity_id) + + # test camera 3 + entity_id = validate_default_camera_entity(hass, camera_all_channels, 0) + await validate_rtsps_camera_state(hass, camera_all_channels, 0, entity_id) + + entity_id = validate_rtsp_camera_entity(hass, camera_all_channels, 0) + await enable_entity(hass, mock_entry.entry.entry_id, entity_id) + await validate_rtsp_camera_state(hass, camera_all_channels, 0, entity_id) + + entity_id = validate_rtsps_camera_entity(hass, camera_all_channels, 1) + await enable_entity(hass, mock_entry.entry.entry_id, entity_id) + await validate_rtsps_camera_state(hass, camera_all_channels, 1, entity_id) + + entity_id = validate_rtsp_camera_entity(hass, camera_all_channels, 1) + await enable_entity(hass, mock_entry.entry.entry_id, entity_id) + await validate_rtsp_camera_state(hass, camera_all_channels, 1, entity_id) + + entity_id = validate_rtsps_camera_entity(hass, camera_all_channels, 2) + await enable_entity(hass, mock_entry.entry.entry_id, entity_id) + await validate_rtsps_camera_state(hass, camera_all_channels, 2, entity_id) + + entity_id = validate_rtsp_camera_entity(hass, camera_all_channels, 2) + await enable_entity(hass, mock_entry.entry.entry_id, entity_id) + await validate_rtsp_camera_state(hass, camera_all_channels, 2, entity_id) + + # test camera 4 + entity_id = validate_default_camera_entity(hass, camera_no_channels, 0) + await validate_no_stream_camera_state( + hass, camera_no_channels, 0, entity_id, features=0 ) @@ -206,10 +324,6 @@ async def test_camera_generic_update( assert await async_setup_component(hass, "homeassistant", {}) - data: ProtectData = hass.data[DOMAIN][mock_entry.entry.entry_id] - assert data - assert data.last_update_success - state = hass.states.get(simple_camera[1]) assert state and state.state == "idle" @@ -232,10 +346,6 @@ async def test_camera_interval_update( ): """Interval updates updates camera entity.""" - data: ProtectData = hass.data[DOMAIN][mock_entry.entry.entry_id] - assert data - assert data.last_update_success - state = hass.states.get(simple_camera[1]) assert state and state.state == "idle" @@ -259,10 +369,6 @@ async def test_camera_bad_interval_update( ): """Interval updates marks camera unavailable.""" - data: ProtectData = hass.data[DOMAIN][mock_entry.entry.entry_id] - assert data - assert data.last_update_success - state = hass.states.get(simple_camera[1]) assert state and state.state == "idle" @@ -270,7 +376,6 @@ async def test_camera_bad_interval_update( mock_entry.api.update = AsyncMock(side_effect=NvrError) await time_changed(hass, DEFAULT_SCAN_INTERVAL) - assert not data.last_update_success state = hass.states.get(simple_camera[1]) assert state and state.state == "unavailable" @@ -278,7 +383,6 @@ async def test_camera_bad_interval_update( mock_entry.api.update = AsyncMock(return_value=mock_entry.api.bootstrap) await time_changed(hass, DEFAULT_SCAN_INTERVAL) - assert data.last_update_success state = hass.states.get(simple_camera[1]) assert state and state.state == "idle" @@ -290,10 +394,6 @@ async def test_camera_ws_update( ): """WS update updates camera entity.""" - data: ProtectData = hass.data[DOMAIN][mock_entry.entry.entry_id] - assert data - assert data.last_update_success - state = hass.states.get(simple_camera[1]) assert state and state.state == "idle" @@ -320,10 +420,6 @@ async def test_camera_ws_update_offline( ): """WS updates marks camera unavailable.""" - data: ProtectData = hass.data[DOMAIN][mock_entry.entry.entry_id] - assert data - assert data.last_update_success - state = hass.states.get(simple_camera[1]) assert state and state.state == "idle" diff --git a/tests/components/unifiprotect/test_config_flow.py b/tests/components/unifiprotect/test_config_flow.py index 80c6019c28a..9065f7f964b 100644 --- a/tests/components/unifiprotect/test_config_flow.py +++ b/tests/components/unifiprotect/test_config_flow.py @@ -129,7 +129,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: ) assert result2["type"] == RESULT_TYPE_FORM - assert result2["errors"] == {"base": "nvr_error"} + assert result2["errors"] == {"base": "cannot_connect"} async def test_form_reauth_auth(hass: HomeAssistant) -> None: @@ -190,7 +190,7 @@ async def test_form_reauth_auth(hass: HomeAssistant) -> None: assert result3["reason"] == "reauth_successful" -async def test_form_options(hass: HomeAssistant) -> None: +async def test_form_options(hass: HomeAssistant, mock_client) -> None: """Test we handle options flows.""" mock_config = MockConfigEntry( domain=DOMAIN, @@ -207,7 +207,12 @@ async def test_form_options(hass: HomeAssistant) -> None: ) mock_config.add_to_hass(hass) - # Integration not setup, since we are only flipping bits in options entry + with patch("homeassistant.components.unifiprotect.ProtectApiClient") as mock_api: + mock_api.return_value = mock_client + + await hass.config_entries.async_setup(mock_config.entry_id) + await hass.async_block_till_done() + assert mock_config.state == config_entries.ConfigEntryState.LOADED result = await hass.config_entries.options.async_init(mock_config.entry_id) assert result["type"] == RESULT_TYPE_FORM diff --git a/tests/components/unifiprotect/test_init.py b/tests/components/unifiprotect/test_init.py index bc42573e26b..4b651c9cda0 100644 --- a/tests/components/unifiprotect/test_init.py +++ b/tests/components/unifiprotect/test_init.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock from pyunifiprotect import NotAuthorized, NvrError -from homeassistant.components.unifiprotect.const import CONF_DISABLE_RTSP, DOMAIN +from homeassistant.components.unifiprotect.const import CONF_DISABLE_RTSP from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant @@ -21,7 +21,6 @@ async def test_setup(hass: HomeAssistant, mock_entry: MockEntityFixture): assert mock_entry.entry.state == ConfigEntryState.LOADED assert mock_entry.api.update.called assert mock_entry.entry.unique_id == mock_entry.api.bootstrap.nvr.mac - assert mock_entry.entry.entry_id in hass.data[DOMAIN] async def test_reload(hass: HomeAssistant, mock_entry: MockEntityFixture): @@ -37,7 +36,6 @@ async def test_reload(hass: HomeAssistant, mock_entry: MockEntityFixture): await hass.async_block_till_done() assert mock_entry.entry.state == ConfigEntryState.LOADED - assert mock_entry.entry.entry_id in hass.data[DOMAIN] assert mock_entry.api.async_disconnect_ws.called From b0704c190f6d1096efb0889c7c307820c78071d5 Mon Sep 17 00:00:00 2001 From: corneyl Date: Mon, 27 Dec 2021 17:44:45 +0100 Subject: [PATCH 1051/2644] Fix picnic sensor time unit (#62437) --- homeassistant/components/picnic/const.py | 30 ++++++++++++----- homeassistant/components/picnic/sensor.py | 9 ++--- tests/components/picnic/test_sensor.py | 40 ++++++++++++++++++----- 3 files changed, 58 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/picnic/const.py b/homeassistant/components/picnic/const.py index 59b969236c4..da86b00bb22 100644 --- a/homeassistant/components/picnic/const.py +++ b/homeassistant/components/picnic/const.py @@ -3,11 +3,13 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +from datetime import datetime from typing import Any, Literal from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription from homeassistant.const import CURRENCY_EURO from homeassistant.helpers.typing import StateType +from homeassistant.util import dt as dt_util DOMAIN = "picnic" @@ -42,7 +44,7 @@ class PicnicRequiredKeysMixin: """Mixin for required keys.""" data_type: Literal["cart_data", "slot_data", "last_order_data"] - value_fn: Callable[[Any], StateType] + value_fn: Callable[[Any], StateType | datetime] @dataclass @@ -73,7 +75,7 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( icon="mdi:calendar-start", entity_registry_enabled_default=True, data_type="slot_data", - value_fn=lambda slot: slot.get("window_start"), + value_fn=lambda slot: dt_util.parse_datetime(str(slot.get("window_start"))), ), PicnicSensorEntityDescription( key=SENSOR_SELECTED_SLOT_END, @@ -81,7 +83,7 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( icon="mdi:calendar-end", entity_registry_enabled_default=True, data_type="slot_data", - value_fn=lambda slot: slot.get("window_end"), + value_fn=lambda slot: dt_util.parse_datetime(str(slot.get("window_end"))), ), PicnicSensorEntityDescription( key=SENSOR_SELECTED_SLOT_MAX_ORDER_TIME, @@ -89,7 +91,7 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( icon="mdi:clock-alert-outline", entity_registry_enabled_default=True, data_type="slot_data", - value_fn=lambda slot: slot.get("cut_off_time"), + value_fn=lambda slot: dt_util.parse_datetime(str(slot.get("cut_off_time"))), ), PicnicSensorEntityDescription( key=SENSOR_SELECTED_SLOT_MIN_ORDER_VALUE, @@ -108,14 +110,18 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.TIMESTAMP, icon="mdi:calendar-start", data_type="last_order_data", - value_fn=lambda last_order: last_order.get("slot", {}).get("window_start"), + value_fn=lambda last_order: dt_util.parse_datetime( + str(last_order.get("slot", {}).get("window_start")) + ), ), PicnicSensorEntityDescription( key=SENSOR_LAST_ORDER_SLOT_END, device_class=SensorDeviceClass.TIMESTAMP, icon="mdi:calendar-end", data_type="last_order_data", - value_fn=lambda last_order: last_order.get("slot", {}).get("window_end"), + value_fn=lambda last_order: dt_util.parse_datetime( + str(last_order.get("slot", {}).get("window_end")) + ), ), PicnicSensorEntityDescription( key=SENSOR_LAST_ORDER_STATUS, @@ -129,7 +135,9 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( icon="mdi:clock-start", entity_registry_enabled_default=True, data_type="last_order_data", - value_fn=lambda last_order: last_order.get("eta", {}).get("start"), + value_fn=lambda last_order: dt_util.parse_datetime( + str(last_order.get("eta", {}).get("start")) + ), ), PicnicSensorEntityDescription( key=SENSOR_LAST_ORDER_ETA_END, @@ -137,7 +145,9 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( icon="mdi:clock-end", entity_registry_enabled_default=True, data_type="last_order_data", - value_fn=lambda last_order: last_order.get("eta", {}).get("end"), + value_fn=lambda last_order: dt_util.parse_datetime( + str(last_order.get("eta", {}).get("end")) + ), ), PicnicSensorEntityDescription( key=SENSOR_LAST_ORDER_DELIVERY_TIME, @@ -145,7 +155,9 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( icon="mdi:timeline-clock", entity_registry_enabled_default=True, data_type="last_order_data", - value_fn=lambda last_order: last_order.get("delivery_time", {}).get("start"), + value_fn=lambda last_order: dt_util.parse_datetime( + str(last_order.get("delivery_time", {}).get("start")) + ), ), PicnicSensorEntityDescription( key=SENSOR_LAST_ORDER_TOTAL_PRICE, diff --git a/homeassistant/components/picnic/sensor.py b/homeassistant/components/picnic/sensor.py index 95612e7b272..8e64283c5f0 100644 --- a/homeassistant/components/picnic/sensor.py +++ b/homeassistant/components/picnic/sensor.py @@ -1,6 +1,7 @@ """Definition of Picnic sensors.""" from __future__ import annotations +from datetime import datetime from typing import Any, cast from homeassistant.components.sensor import SensorEntity @@ -62,8 +63,8 @@ class PicnicSensor(SensorEntity, CoordinatorEntity): self._attr_unique_id = f"{config_entry.unique_id}.{description.key}" @property - def native_value(self) -> StateType: - """Return the state of the entity.""" + def native_value(self) -> StateType | datetime: + """Return the value reported by the sensor.""" data_set = ( self.coordinator.data.get(self.entity_description.data_type, {}) if self.coordinator.data is not None @@ -73,8 +74,8 @@ class PicnicSensor(SensorEntity, CoordinatorEntity): @property def available(self) -> bool: - """Return True if entity is available.""" - return self.coordinator.last_update_success and self.state is not None + """Return True if last update was successful.""" + return self.coordinator.last_update_success @property def device_info(self) -> DeviceInfo: diff --git a/tests/components/picnic/test_sensor.py b/tests/components/picnic/test_sensor.py index 4773206f5cf..f1882bfa098 100644 --- a/tests/components/picnic/test_sensor.py +++ b/tests/components/picnic/test_sensor.py @@ -1,6 +1,7 @@ """The tests for the Picnic sensor platform.""" import copy from datetime import timedelta +from typing import Dict import unittest from unittest.mock import patch @@ -11,7 +12,12 @@ from homeassistant import config_entries from homeassistant.components.picnic import const from homeassistant.components.picnic.const import CONF_COUNTRY_CODE, SENSOR_TYPES from homeassistant.components.sensor import SensorDeviceClass -from homeassistant.const import CONF_ACCESS_TOKEN, CURRENCY_EURO, STATE_UNAVAILABLE +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CURRENCY_EURO, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.util import dt @@ -99,6 +105,7 @@ class TestPicnicSensor(unittest.IsolatedAsyncioTestCase): # Patch the api client self.picnic_patcher = patch("homeassistant.components.picnic.PicnicAPI") self.picnic_mock = self.picnic_patcher.start() + self.picnic_mock().session.auth_token = "3q29fpwhulzes" # Add a config entry and setup the integration config_data = { @@ -277,13 +284,11 @@ class TestPicnicSensor(unittest.IsolatedAsyncioTestCase): await self._setup_platform() # Assert sensors are unknown - self._assert_sensor("sensor.picnic_selected_slot_start", STATE_UNAVAILABLE) - self._assert_sensor("sensor.picnic_selected_slot_end", STATE_UNAVAILABLE) + self._assert_sensor("sensor.picnic_selected_slot_start", STATE_UNKNOWN) + self._assert_sensor("sensor.picnic_selected_slot_end", STATE_UNKNOWN) + self._assert_sensor("sensor.picnic_selected_slot_max_order_time", STATE_UNKNOWN) self._assert_sensor( - "sensor.picnic_selected_slot_max_order_time", STATE_UNAVAILABLE - ) - self._assert_sensor( - "sensor.picnic_selected_slot_min_order_value", STATE_UNAVAILABLE + "sensor.picnic_selected_slot_min_order_value", STATE_UNKNOWN ) async def test_sensors_last_order_in_future(self): @@ -300,7 +305,7 @@ class TestPicnicSensor(unittest.IsolatedAsyncioTestCase): await self._setup_platform() # Assert delivery time is not available, but eta is - self._assert_sensor("sensor.picnic_last_order_delivery_time", STATE_UNAVAILABLE) + self._assert_sensor("sensor.picnic_last_order_delivery_time", STATE_UNKNOWN) self._assert_sensor( "sensor.picnic_last_order_eta_start", "2021-02-26T19:54:00+00:00" ) @@ -308,6 +313,25 @@ class TestPicnicSensor(unittest.IsolatedAsyncioTestCase): "sensor.picnic_last_order_eta_end", "2021-02-26T20:14:00+00:00" ) + async def test_sensors_eta_date_malformed(self): + """Test sensor states when last order eta dates are malformed.""" + # Set-up platform with default mock responses + await self._setup_platform(use_default_responses=True) + + # Set non-datetime strings as eta + eta_dates: Dict[str, str] = { + "start": "wrong-time", + "end": "other-malformed-datetime", + } + delivery_response = copy.deepcopy(DEFAULT_DELIVERY_RESPONSE) + delivery_response["eta2"] = eta_dates + self.picnic_mock().get_deliveries.return_value = [delivery_response] + await self._coordinator.async_refresh() + + # Assert eta times are not available due to malformed date strings + self._assert_sensor("sensor.picnic_last_order_eta_start", STATE_UNKNOWN) + self._assert_sensor("sensor.picnic_last_order_eta_end", STATE_UNKNOWN) + async def test_sensors_use_detailed_eta_if_available(self): """Test sensor states when last order is not yet delivered.""" # Set-up platform with default mock responses From 2c904c09747e9e25439a479f3395970cd9d9896b Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 27 Dec 2021 17:55:17 +0100 Subject: [PATCH 1052/2644] Bump mypy to 0.930 (#62642) Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- homeassistant/auth/mfa_modules/__init__.py | 4 ++-- homeassistant/auth/providers/__init__.py | 5 ++-- homeassistant/backports/enum.py | 2 +- .../components/device_tracker/legacy.py | 8 +++---- homeassistant/components/esphome/camera.py | 6 +++-- homeassistant/components/http/forwarded.py | 2 +- homeassistant/components/notify/legacy.py | 4 ++-- homeassistant/config.py | 14 +++++------ homeassistant/config_entries.py | 24 +++++++++++-------- homeassistant/core.py | 4 ++-- homeassistant/helpers/check_config.py | 4 +--- homeassistant/helpers/condition.py | 4 ++-- homeassistant/helpers/config_validation.py | 14 +++++++++-- homeassistant/helpers/reload.py | 4 ++-- homeassistant/helpers/script.py | 4 ++-- homeassistant/helpers/service.py | 5 ++-- homeassistant/loader.py | 2 +- homeassistant/scripts/__init__.py | 2 +- homeassistant/setup.py | 7 +++--- homeassistant/util/color.py | 4 ++-- homeassistant/util/executor.py | 6 ++--- requirements_test.txt | 2 +- 22 files changed, 73 insertions(+), 58 deletions(-) diff --git a/homeassistant/auth/mfa_modules/__init__.py b/homeassistant/auth/mfa_modules/__init__.py index 4adaf4776a0..60f790fa490 100644 --- a/homeassistant/auth/mfa_modules/__init__.py +++ b/homeassistant/auth/mfa_modules/__init__.py @@ -133,7 +133,7 @@ async def auth_mfa_module_from_config( module = await _load_mfa_module(hass, module_name) try: - config = module.CONFIG_SCHEMA(config) # type: ignore + config = module.CONFIG_SCHEMA(config) except vol.Invalid as err: _LOGGER.error( "Invalid configuration for multi-factor module %s: %s", @@ -168,7 +168,7 @@ async def _load_mfa_module(hass: HomeAssistant, module_name: str) -> types.Modul # https://github.com/python/mypy/issues/1424 await requirements.async_process_requirements( - hass, module_path, module.REQUIREMENTS # type: ignore + hass, module_path, module.REQUIREMENTS ) processed.add(module_name) diff --git a/homeassistant/auth/providers/__init__.py b/homeassistant/auth/providers/__init__.py index dc5f8f2580c..73968191182 100644 --- a/homeassistant/auth/providers/__init__.py +++ b/homeassistant/auth/providers/__init__.py @@ -142,7 +142,7 @@ async def auth_provider_from_config( module = await load_auth_provider_module(hass, provider_name) try: - config = module.CONFIG_SCHEMA(config) # type: ignore + config = module.CONFIG_SCHEMA(config) except vol.Invalid as err: _LOGGER.error( "Invalid configuration for auth provider %s: %s", @@ -174,8 +174,7 @@ async def load_auth_provider_module( elif provider in processed: return module - # https://github.com/python/mypy/issues/1424 - reqs = module.REQUIREMENTS # type: ignore + reqs = module.REQUIREMENTS await requirements.async_process_requirements( hass, f"auth provider {provider}", reqs ) diff --git a/homeassistant/backports/enum.py b/homeassistant/backports/enum.py index 3fa9a582f79..21302fe9f7b 100644 --- a/homeassistant/backports/enum.py +++ b/homeassistant/backports/enum.py @@ -14,7 +14,7 @@ class StrEnum(str, Enum): """Create a new StrEnum instance.""" if not isinstance(value, str): raise TypeError(f"{value!r} is not a string") - return super().__new__(cls, value, *args, **kwargs) # type: ignore[call-overload,no-any-return] + return super().__new__(cls, value, *args, **kwargs) def __str__(self) -> str: """Return self.value.""" diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index d81743c530a..3614beccdfa 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -238,22 +238,22 @@ class DeviceTrackerPlatform: scanner = None setup = None if hasattr(self.platform, "async_get_scanner"): - scanner = await self.platform.async_get_scanner( # type: ignore[attr-defined] + scanner = await self.platform.async_get_scanner( hass, {DOMAIN: self.config} ) elif hasattr(self.platform, "get_scanner"): scanner = await hass.async_add_executor_job( - self.platform.get_scanner, # type: ignore[attr-defined] + self.platform.get_scanner, hass, {DOMAIN: self.config}, ) elif hasattr(self.platform, "async_setup_scanner"): - setup = await self.platform.async_setup_scanner( # type: ignore[attr-defined] + setup = await self.platform.async_setup_scanner( hass, self.config, tracker.async_see, discovery_info ) elif hasattr(self.platform, "setup_scanner"): setup = await hass.async_add_executor_job( - self.platform.setup_scanner, # type: ignore[attr-defined] + self.platform.setup_scanner, hass, self.config, tracker.see, diff --git a/homeassistant/components/esphome/camera.py b/homeassistant/components/esphome/camera.py index 47010324290..390208f689d 100644 --- a/homeassistant/components/esphome/camera.py +++ b/homeassistant/components/esphome/camera.py @@ -60,7 +60,8 @@ class EsphomeCamera(Camera, EsphomeEntity[CameraInfo, CameraState]): async with self._image_cond: await self._image_cond.wait() if not self.available: - return None + # Availability can change while waiting for 'self._image.cond' + return None # type: ignore[unreachable] return self._state.data[:] async def _async_camera_stream_image(self) -> bytes | None: @@ -71,7 +72,8 @@ class EsphomeCamera(Camera, EsphomeEntity[CameraInfo, CameraState]): async with self._image_cond: await self._image_cond.wait() if not self.available: - return None + # Availability can change while waiting for 'self._image.cond' + return None # type: ignore[unreachable] return self._state.data[:] async def handle_async_mjpeg_stream( diff --git a/homeassistant/components/http/forwarded.py b/homeassistant/components/http/forwarded.py index 4cc330a85ed..ff50e9bd965 100644 --- a/homeassistant/components/http/forwarded.py +++ b/homeassistant/components/http/forwarded.py @@ -88,7 +88,7 @@ def async_setup_forwarded( remote = False # Skip requests from Remote UI - if remote and remote.is_cloud_request.get(): # type: ignore + if remote and remote.is_cloud_request.get(): return await handler(request) # Handle X-Forwarded-For diff --git a/homeassistant/components/notify/legacy.py b/homeassistant/components/notify/legacy.py index 8eb007b6398..c5be8a1800e 100644 --- a/homeassistant/components/notify/legacy.py +++ b/homeassistant/components/notify/legacy.py @@ -58,12 +58,12 @@ async def async_setup_legacy(hass: HomeAssistant, config: ConfigType) -> None: notify_service = None try: if hasattr(platform, "async_get_service"): - notify_service = await platform.async_get_service( # type: ignore + notify_service = await platform.async_get_service( hass, p_config, discovery_info ) elif hasattr(platform, "get_service"): notify_service = await hass.async_add_executor_job( - platform.get_service, hass, p_config, discovery_info # type: ignore + platform.get_service, hass, p_config, discovery_info ) else: raise HomeAssistantError("Invalid notify platform.") diff --git a/homeassistant/config.py b/homeassistant/config.py index ed9ffa7c47d..3d1ff4f3c1d 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -642,10 +642,10 @@ def _log_pkg_error(package: str, component: str, config: dict, message: str) -> def _identify_config_schema(module: ModuleType) -> str | None: """Extract the schema and identify list or dict based.""" - if not isinstance(module.CONFIG_SCHEMA, vol.Schema): # type: ignore + if not isinstance(module.CONFIG_SCHEMA, vol.Schema): return None - schema = module.CONFIG_SCHEMA.schema # type: ignore + schema = module.CONFIG_SCHEMA.schema if isinstance(schema, vol.All): for subschema in schema.validators: @@ -656,7 +656,7 @@ def _identify_config_schema(module: ModuleType) -> str | None: return None try: - key = next(k for k in schema if k == module.DOMAIN) # type: ignore + key = next(k for k in schema if k == module.DOMAIN) except (TypeError, AttributeError, StopIteration): return None except Exception: # pylint: disable=broad-except @@ -666,8 +666,8 @@ def _identify_config_schema(module: ModuleType) -> str | None: if hasattr(key, "default") and not isinstance( key.default, vol.schema_builder.Undefined ): - default_value = module.CONFIG_SCHEMA({module.DOMAIN: key.default()})[ # type: ignore - module.DOMAIN # type: ignore + default_value = module.CONFIG_SCHEMA({module.DOMAIN: key.default()})[ + module.DOMAIN ] if isinstance(default_value, dict): @@ -747,7 +747,7 @@ async def merge_packages_config( # If integration has a custom config validator, it needs to provide a hint. if config_platform is not None: - merge_list = config_platform.PACKAGE_MERGE_HINT == "list" # type: ignore[attr-defined] + merge_list = config_platform.PACKAGE_MERGE_HINT == "list" if not merge_list: merge_list = hasattr(component, "PLATFORM_SCHEMA") @@ -889,7 +889,7 @@ async def async_process_component_config( # noqa: C901 # Validate platform specific schema if hasattr(platform, "PLATFORM_SCHEMA"): try: - p_validated = platform.PLATFORM_SCHEMA(p_config) # type: ignore + p_validated = platform.PLATFORM_SCHEMA(p_config) except vol.Invalid as ex: async_log_exception( ex, diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 063040dc398..da524bf068d 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -9,7 +9,7 @@ from enum import Enum import functools import logging from types import MappingProxyType, MethodType -from typing import TYPE_CHECKING, Any, Callable, Optional, cast +from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar, cast import weakref from . import data_entry_flow, loader @@ -70,6 +70,8 @@ PATH_CONFIG = ".config_entries.json" SAVE_DELAY = 1 +_T = TypeVar("_T", bound="ConfigEntryState") + class ConfigEntryState(Enum): """Config entry state.""" @@ -89,12 +91,12 @@ class ConfigEntryState(Enum): _recoverable: bool - def __new__(cls: type[object], value: str, recoverable: bool) -> ConfigEntryState: + def __new__(cls: type[_T], value: str, recoverable: bool) -> _T: """Create new ConfigEntryState.""" obj = object.__new__(cls) obj._value_ = value obj._recoverable = recoverable - return cast("ConfigEntryState", obj) + return obj @property def recoverable(self) -> bool: @@ -321,7 +323,7 @@ class ConfigEntry: error_reason = None try: - result = await component.async_setup_entry(hass, self) # type: ignore + result = await component.async_setup_entry(hass, self) if not isinstance(result, bool): _LOGGER.error( @@ -460,7 +462,7 @@ class ConfigEntry: return False try: - result = await component.async_unload_entry(hass, self) # type: ignore + result = await component.async_unload_entry(hass, self) assert isinstance(result, bool) @@ -471,7 +473,8 @@ class ConfigEntry: self._async_process_on_unload() - return result + # https://github.com/python/mypy/issues/11839 + return result # type: ignore[no-any-return] except Exception: # pylint: disable=broad-except _LOGGER.exception( "Error unloading entry %s for %s", self.title, integration.domain @@ -499,7 +502,7 @@ class ConfigEntry: if not hasattr(component, "async_remove_entry"): return try: - await component.async_remove_entry(hass, self) # type: ignore + await component.async_remove_entry(hass, self) except Exception: # pylint: disable=broad-except _LOGGER.exception( "Error calling entry remove callback %s for %s", @@ -536,7 +539,7 @@ class ConfigEntry: return False try: - result = await component.async_migrate_entry(hass, self) # type: ignore + result = await component.async_migrate_entry(hass, self) if not isinstance(result, bool): _LOGGER.error( "%s.async_migrate_entry did not return boolean", self.domain @@ -545,7 +548,8 @@ class ConfigEntry: if result: # pylint: disable=protected-access hass.config_entries._async_schedule_save() - return result + # https://github.com/python/mypy/issues/11839 + return result # type: ignore[no-any-return] except Exception: # pylint: disable=broad-except _LOGGER.exception( "Error migrating entry %s for %s", self.title, self.domain @@ -1169,7 +1173,7 @@ class ConfigFlow(data_entry_flow.FlowHandler): def __init_subclass__(cls, domain: str | None = None, **kwargs: Any) -> None: """Initialize a subclass, register if possible.""" - super().__init_subclass__(**kwargs) # type: ignore + super().__init_subclass__(**kwargs) if domain is not None: HANDLERS.register(domain)(cls) diff --git a/homeassistant/core.py b/homeassistant/core.py index 9566ad6c596..819db0b6825 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -362,7 +362,7 @@ class HomeAssistant: raise ValueError("Don't call async_add_job with None") if asyncio.iscoroutine(target): - return self.async_create_task(cast(Coroutine, target)) + return self.async_create_task(target) return self.async_add_hass_job(HassJob(target), *args) @@ -462,7 +462,7 @@ class HomeAssistant: args: parameters for method to call. """ if asyncio.iscoroutine(target): - return self.async_create_task(cast(Coroutine, target)) + return self.async_create_task(target) return self.async_run_hass_job(HassJob(target), *args) diff --git a/homeassistant/helpers/check_config.py b/homeassistant/helpers/check_config.py index 6e0415b2a54..3da444a8549 100644 --- a/homeassistant/helpers/check_config.py +++ b/homeassistant/helpers/check_config.py @@ -159,9 +159,7 @@ async def async_check_ha_config_file( # noqa: C901 ): try: result[domain] = ( - await config_validator.async_validate_config( # type: ignore - hass, config - ) + await config_validator.async_validate_config(hass, config) )[domain] continue except (vol.Invalid, HomeAssistantError) as ex: diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 404eea5ea4a..45fb4bc6aa1 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -880,7 +880,7 @@ async def async_device_from_config( return trace_condition_function( cast( ConditionCheckerType, - platform.async_condition_from_config(hass, config), # type: ignore + platform.async_condition_from_config(hass, config), ) ) @@ -946,7 +946,7 @@ async def async_validate_condition_config( ) if hasattr(platform, "async_validate_condition_config"): return await platform.async_validate_condition_config(hass, config) # type: ignore - return cast(ConfigType, platform.CONDITION_SCHEMA(config)) # type: ignore + return cast(ConfigType, platform.CONDITION_SCHEMA(config)) if condition in ("numeric_state", "state"): validator = getattr( diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index ffd1f7586c0..06560de815d 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -16,7 +16,7 @@ from numbers import Number import os import re from socket import _GLOBAL_DEFAULT_TIMEOUT # type: ignore # private, not in typeshed -from typing import Any, Dict, TypeVar, cast +from typing import Any, Dict, TypeVar, cast, overload from urllib.parse import urlparse from uuid import UUID @@ -245,7 +245,17 @@ def isdir(value: Any) -> str: return dir_in -def ensure_list(value: T | list[T] | None) -> list[T]: +@overload +def ensure_list(value: None) -> list[Any]: + ... + + +@overload +def ensure_list(value: T | list[T]) -> list[T]: + ... + + +def ensure_list(value: T | list[T] | None) -> list[T] | list[Any]: """Wrap value in list if it is not one.""" if value is None: return [] diff --git a/homeassistant/helpers/reload.py b/homeassistant/helpers/reload.py index baa31bb41fc..a0388f53a93 100644 --- a/homeassistant/helpers/reload.py +++ b/homeassistant/helpers/reload.py @@ -79,8 +79,8 @@ async def _resetup_platform( if hasattr(component, "async_reset_platform"): # If the integration has its own way to reset # use this method. - await component.async_reset_platform(hass, integration_name) # type: ignore - await component.async_setup(hass, root_config) # type: ignore + await component.async_reset_platform(hass, integration_name) + await component.async_setup(hass, root_config) return # If it's an entity platform, we use the entity_platform diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 77e4fd38508..925a5b9746f 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -248,9 +248,9 @@ async def async_validate_action_config( hass, config[CONF_DOMAIN], device_automation.DeviceAutomationType.ACTION ) if hasattr(platform, "async_validate_action_config"): - config = await platform.async_validate_action_config(hass, config) # type: ignore + config = await platform.async_validate_action_config(hass, config) else: - config = platform.ACTION_SCHEMA(config) # type: ignore + config = platform.ACTION_SCHEMA(config) elif action_type == cv.SCRIPT_ACTION_CHECK_CONDITION: config = await condition.async_validate_condition_config(hass, config) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 13ec3cb5df5..bbf605e6bff 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -8,6 +8,7 @@ from functools import partial, wraps import logging from typing import TYPE_CHECKING, Any, TypedDict +from typing_extensions import TypeGuard import voluptuous as vol from homeassistant.auth.permissions.const import CAT_ENTITIES, POLICY_CONTROL @@ -319,7 +320,7 @@ async def async_extract_entity_ids( return referenced.referenced | referenced.indirectly_referenced -def _has_match(ids: str | list | None) -> bool: +def _has_match(ids: str | list[str] | None) -> TypeGuard[str | list[str]]: """Check if ids can match anything.""" return ids not in (None, ENTITY_MATCH_NONE) @@ -706,7 +707,7 @@ async def _handle_entity_call( func, entity.entity_id, ) - await result # type: ignore + await result @bind_hass diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 6fd66dab2fa..3fc9edfaf74 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -318,7 +318,7 @@ class Integration: cls, hass: HomeAssistant, root_module: ModuleType, domain: str ) -> Integration | None: """Resolve an integration from a root module.""" - for base in root_module.__path__: # type: ignore + for base in root_module.__path__: manifest_path = pathlib.Path(base) / domain / "manifest.json" if not manifest_path.is_file(): diff --git a/homeassistant/scripts/__init__.py b/homeassistant/scripts/__init__.py index 69ca1d6083b..5b781d4eb37 100644 --- a/homeassistant/scripts/__init__.py +++ b/homeassistant/scripts/__init__.py @@ -64,7 +64,7 @@ def run(args: list[str]) -> int: asyncio.set_event_loop_policy(runner.HassEventLoopPolicy(False)) - return script.run(args[1:]) # type: ignore + return script.run(args[1:]) def extract_config_dir(args: Sequence[str] | None = None) -> str: diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 44dc6fe1014..b509419bcb5 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -7,6 +7,7 @@ import contextlib import logging.handlers from timeit import default_timer as timer from types import ModuleType +from typing import Any from . import config as conf_util, core, loader, requirements from .config import async_notify_setup_error @@ -210,15 +211,15 @@ async def _async_setup_component( ) task = None - result = True + result: Any | bool = True try: if hasattr(component, "async_setup"): - task = component.async_setup(hass, processed_config) # type: ignore + task = component.async_setup(hass, processed_config) elif hasattr(component, "setup"): # This should not be replaced with hass.async_add_executor_job because # we don't want to track this task in case it blocks startup. task = hass.loop.run_in_executor( - None, component.setup, hass, processed_config # type: ignore + None, component.setup, hass, processed_config ) elif not hasattr(component, "async_setup_entry"): log_error("No setup or config entry setup function defined.") diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index ccb4980492f..9c01c3040c2 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -3,7 +3,7 @@ from __future__ import annotations import colorsys import math -from typing import NamedTuple +from typing import NamedTuple, cast import attr @@ -299,7 +299,7 @@ def color_xy_brightness_to_RGB( r, g, b = map( lambda x: (12.92 * x) if (x <= 0.0031308) - else ((1.0 + 0.055) * pow(x, (1.0 / 2.4)) - 0.055), + else ((1.0 + 0.055) * cast(float, pow(x, (1.0 / 2.4))) - 0.055), [r, g, b], ) diff --git a/homeassistant/util/executor.py b/homeassistant/util/executor.py index 8451e71e0ec..5a8f15f434f 100644 --- a/homeassistant/util/executor.py +++ b/homeassistant/util/executor.py @@ -64,7 +64,7 @@ class InterruptibleThreadPoolExecutor(ThreadPoolExecutor): def shutdown(self, *args, **kwargs) -> None: # type: ignore """Shutdown backport from cpython 3.9 with interrupt support added.""" - with self._shutdown_lock: # type: ignore[attr-defined] + with self._shutdown_lock: self._shutdown = True # Drain all work items from the queue, and then cancel their # associated futures. @@ -77,7 +77,7 @@ class InterruptibleThreadPoolExecutor(ThreadPoolExecutor): work_item.future.cancel() # Send a wake-up to prevent threads calling # _work_queue.get(block=True) from permanently blocking. - self._work_queue.put(None) + self._work_queue.put(None) # type: ignore[arg-type] # The above code is backported from python 3.9 # @@ -89,7 +89,7 @@ class InterruptibleThreadPoolExecutor(ThreadPoolExecutor): def join_threads_or_timeout(self) -> None: """Join threads or timeout.""" - remaining_threads = set(self._threads) # type: ignore[attr-defined] + remaining_threads = set(self._threads) start_time = time.monotonic() timeout_remaining: float = EXECUTOR_SHUTDOWN_TIMEOUT attempt = 0 diff --git a/requirements_test.txt b/requirements_test.txt index 6f580cb5159..7beef3dd792 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -12,7 +12,7 @@ coverage==6.2.0 freezegun==1.1.0 jsonpickle==1.4.1 mock-open==1.4.0 -mypy==0.910 +mypy==0.930 pre-commit==2.16.0 pylint==2.12.1 pipdeptree==2.2.0 From 377b0efc60c4f8dd522ea3b7a10501b275eb22ee Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 27 Dec 2021 17:56:13 +0100 Subject: [PATCH 1053/2644] Add basic type hints to ffmpeg (#62744) Co-authored-by: epenet --- homeassistant/components/ffmpeg/__init__.py | 3 ++- homeassistant/components/ffmpeg/camera.py | 10 +++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ffmpeg/__init__.py b/homeassistant/components/ffmpeg/__init__.py index e23be94f0ce..7da1bc572f5 100644 --- a/homeassistant/components/ffmpeg/__init__.py +++ b/homeassistant/components/ffmpeg/__init__.py @@ -20,6 +20,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass DOMAIN = "ffmpeg" @@ -54,7 +55,7 @@ CONFIG_SCHEMA = vol.Schema( SERVICE_FFMPEG_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids}) -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the FFmpeg component.""" conf = config.get(DOMAIN, {}) diff --git a/homeassistant/components/ffmpeg/camera.py b/homeassistant/components/ffmpeg/camera.py index 323eae7c129..6e41d88fc85 100644 --- a/homeassistant/components/ffmpeg/camera.py +++ b/homeassistant/components/ffmpeg/camera.py @@ -7,8 +7,11 @@ import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import CONF_EXTRA_ARGUMENTS, CONF_INPUT, DATA_FFMPEG, async_get_image @@ -24,7 +27,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Set up a FFmpeg camera.""" async_add_entities([FFmpegCamera(hass, config)]) From 8fd60dbd51a5a192e4ed1e2f250d7c618a82b1aa Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Mon, 27 Dec 2021 17:23:08 +0000 Subject: [PATCH 1054/2644] Refactor entity_category str types (#62790) --- homeassistant/components/mqtt/mixins.py | 3 ++- homeassistant/components/neato/sensor.py | 2 +- homeassistant/components/shelly/entity.py | 14 +++++++------- homeassistant/components/tasmota/sensor.py | 2 +- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 713f0e5c030..9b6d4323433 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -33,6 +33,7 @@ from homeassistant.helpers.entity import ( ENTITY_CATEGORIES_SCHEMA, DeviceInfo, Entity, + EntityCategory, async_generate_entity_id, ) from homeassistant.helpers.typing import ConfigType @@ -695,7 +696,7 @@ class MqttEntity( return self._config[CONF_ENABLED_BY_DEFAULT] @property - def entity_category(self) -> str | None: + def entity_category(self) -> EntityCategory | str | None: """Return the entity category if any.""" return self._config.get(CONF_ENTITY_CATEGORY) diff --git a/homeassistant/components/neato/sensor.py b/homeassistant/components/neato/sensor.py index 7cc2d0f171a..2b20158ff8b 100644 --- a/homeassistant/components/neato/sensor.py +++ b/homeassistant/components/neato/sensor.py @@ -84,7 +84,7 @@ class NeatoSensor(SensorEntity): return SensorDeviceClass.BATTERY @property - def entity_category(self) -> str: + def entity_category(self) -> EntityCategory: """Device entity category.""" return EntityCategory.DIAGNOSTIC diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 65ce8eeba56..d4f26c7767d 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -19,7 +19,7 @@ from homeassistant.helpers import ( entity_registry, update_coordinator, ) -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import StateType @@ -237,7 +237,7 @@ class BlockAttributeDescription: # Callable (settings, block), return true if entity should be removed removal_condition: Callable[[dict, Block], bool] | None = None extra_state_attributes: Callable[[Block], dict | None] | None = None - entity_category: str | None = None + entity_category: EntityCategory | None = None @dataclass @@ -256,7 +256,7 @@ class RpcAttributeDescription: available: Callable[[dict], bool] | None = None removal_condition: Callable[[dict, str], bool] | None = None extra_state_attributes: Callable[[dict, dict], dict | None] | None = None - entity_category: str | None = None + entity_category: EntityCategory | None = None @dataclass @@ -271,7 +271,7 @@ class RestAttributeDescription: state_class: str | None = None default_enabled: bool = True extra_state_attributes: Callable[[dict], dict | None] | None = None - entity_category: str | None = None + entity_category: EntityCategory | None = None class ShellyBlockEntity(entity.Entity): @@ -471,7 +471,7 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity): return self.description.extra_state_attributes(self.block) @property - def entity_category(self) -> str | None: + def entity_category(self) -> EntityCategory | None: """Return category of entity.""" return self.description.entity_category @@ -548,7 +548,7 @@ class ShellyRestAttributeEntity(update_coordinator.CoordinatorEntity): return self.description.extra_state_attributes(self.wrapper.device.status) @property - def entity_category(self) -> str | None: + def entity_category(self) -> EntityCategory | None: """Return category of entity.""" return self.description.entity_category @@ -614,7 +614,7 @@ class ShellyRpcAttributeEntity(ShellyRpcEntity, entity.Entity): ) @property - def entity_category(self) -> str | None: + def entity_category(self) -> EntityCategory | None: """Return category of entity.""" return self.description.entity_category diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index ece45b9dfd7..feb15cd5639 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -274,7 +274,7 @@ class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, SensorEntity): return class_or_icon.get(STATE_CLASS) @property - def entity_category(self) -> str | None: + def entity_category(self) -> EntityCategory | None: """Return the category of the entity, if any.""" if self._tasmota_entity.quantity in status_sensor.SENSORS: return EntityCategory.DIAGNOSTIC From 089dcb2b222f6a653b1bcb31d62504dcc56d916b Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Mon, 27 Dec 2021 09:26:55 -0800 Subject: [PATCH 1055/2644] Address feedback to Overkiz integration (#62841) --- homeassistant/components/overkiz/button.py | 22 +++++----- homeassistant/components/overkiz/entity.py | 13 ------ homeassistant/components/overkiz/lock.py | 10 ++--- homeassistant/components/overkiz/sensor.py | 48 ++++++++++++++-------- 4 files changed, 48 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/overkiz/button.py b/homeassistant/components/overkiz/button.py index 85e7ea8c613..7a2c0b0448c 100644 --- a/homeassistant/components/overkiz/button.py +++ b/homeassistant/components/overkiz/button.py @@ -57,18 +57,20 @@ async def async_setup_entry( for device in data.coordinator.data.values(): if ( - device.widget not in IGNORED_OVERKIZ_DEVICES - and device.ui_class not in IGNORED_OVERKIZ_DEVICES + device.widget in IGNORED_OVERKIZ_DEVICES + or device.ui_class in IGNORED_OVERKIZ_DEVICES ): - for command in device.definition.commands: - if description := supported_commands.get(command.command_name): - entities.append( - OverkizButton( - device.device_url, - data.coordinator, - description, - ) + continue + + for command in device.definition.commands: + if description := supported_commands.get(command.command_name): + entities.append( + OverkizButton( + device.device_url, + data.coordinator, + description, ) + ) async_add_entities(entities) diff --git a/homeassistant/components/overkiz/entity.py b/homeassistant/components/overkiz/entity.py index b88417b0d2b..13f9fc781a7 100644 --- a/homeassistant/components/overkiz/entity.py +++ b/homeassistant/components/overkiz/entity.py @@ -1,13 +1,9 @@ """Parent class for every Overkiz device.""" from __future__ import annotations -from collections.abc import Callable -from dataclasses import dataclass - from pyoverkiz.enums import OverkizAttribute, OverkizState from pyoverkiz.models import Device -from homeassistant.components.sensor import SensorEntityDescription from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -83,15 +79,6 @@ class OverkizEntity(CoordinatorEntity): ) -@dataclass -class OverkizSensorDescription(SensorEntityDescription): - """Class to describe an Overkiz sensor.""" - - native_value: Callable[ - [str | int | float], str | int | float - ] | None = lambda val: val - - class OverkizDescriptiveEntity(OverkizEntity): """Representation of a Overkiz device entity based on a description.""" diff --git a/homeassistant/components/overkiz/lock.py b/homeassistant/components/overkiz/lock.py index 89a091751d9..c900b2faab8 100644 --- a/homeassistant/components/overkiz/lock.py +++ b/homeassistant/components/overkiz/lock.py @@ -24,22 +24,20 @@ async def async_setup_entry( """Set up the Overkiz locks from a config entry.""" data: HomeAssistantOverkizData = hass.data[DOMAIN][entry.entry_id] - entities: list[OverkizLock] = [ + async_add_entities( OverkizLock(device.device_url, data.coordinator) for device in data.platforms[Platform.LOCK] - ] - - async_add_entities(entities) + ) class OverkizLock(OverkizEntity, LockEntity): """Representation of an Overkiz Lock.""" - async def async_lock(self, **_: Any) -> None: + async def async_lock(self, **kwargs: Any) -> None: """Lock method.""" await self.executor.async_execute_command(OverkizCommand.LOCK) - async def async_unlock(self, **_: Any) -> None: + async def async_unlock(self, **kwargs: Any) -> None: """Unlock method.""" await self.executor.async_execute_command(OverkizCommand.UNLOCK) diff --git a/homeassistant/components/overkiz/sensor.py b/homeassistant/components/overkiz/sensor.py index a00587328b8..eb35e8b8208 100644 --- a/homeassistant/components/overkiz/sensor.py +++ b/homeassistant/components/overkiz/sensor.py @@ -1,11 +1,15 @@ """Support for Overkiz sensors.""" from __future__ import annotations +from collections.abc import Callable +from dataclasses import dataclass + from pyoverkiz.enums import OverkizAttribute, OverkizState, UIWidget from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, + SensorEntityDescription, SensorStateClass, ) from homeassistant.config_entries import ConfigEntry @@ -28,7 +32,15 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HomeAssistantOverkizData from .const import DOMAIN, IGNORED_OVERKIZ_DEVICES from .coordinator import OverkizDataUpdateCoordinator -from .entity import OverkizDescriptiveEntity, OverkizEntity, OverkizSensorDescription +from .entity import OverkizDescriptiveEntity, OverkizEntity + + +@dataclass +class OverkizSensorDescription(SensorEntityDescription): + """Class to describe an Overkiz sensor.""" + + native_value: Callable[[str | int | float], str | int | float] | None = None + SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ OverkizSensorDescription( @@ -347,20 +359,6 @@ async def async_setup_entry( } for device in data.coordinator.data.values(): - if ( - device.widget not in IGNORED_OVERKIZ_DEVICES - and device.ui_class not in IGNORED_OVERKIZ_DEVICES - ): - for state in device.definition.states: - if description := key_supported_states.get(state.qualified_name): - entities.append( - OverkizStateSensor( - device.device_url, - data.coordinator, - description, - ) - ) - if device.widget == UIWidget.HOMEKIT_STACK: entities.append( OverkizHomeKitSetupCodeSensor( @@ -369,12 +367,30 @@ async def async_setup_entry( ) ) + if ( + device.widget in IGNORED_OVERKIZ_DEVICES + or device.ui_class in IGNORED_OVERKIZ_DEVICES + ): + continue + + for state in device.definition.states: + if description := key_supported_states.get(state.qualified_name): + entities.append( + OverkizStateSensor( + device.device_url, + data.coordinator, + description, + ) + ) + async_add_entities(entities) class OverkizStateSensor(OverkizDescriptiveEntity, SensorEntity): """Representation of an Overkiz Sensor.""" + entity_description: OverkizSensorDescription + @property def native_value(self): """Return the value of the sensor.""" @@ -384,7 +400,7 @@ class OverkizStateSensor(OverkizDescriptiveEntity, SensorEntity): return None # Transform the value with a lambda function - if hasattr(self.entity_description, "native_value"): + if self.entity_description.native_value: return self.entity_description.native_value(state.value) return state.value From 0d957ad93b6aeb437caaebc9f7b4cb64a4eda7c7 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 27 Dec 2021 19:34:00 +0100 Subject: [PATCH 1056/2644] Code improvements Sensibo (#62810) --- homeassistant/components/sensibo/__init__.py | 8 +- homeassistant/components/sensibo/climate.py | 246 +++++++----------- .../components/sensibo/config_flow.py | 17 +- homeassistant/components/sensibo/const.py | 6 +- tests/components/sensibo/test_config_flow.py | 9 +- 5 files changed, 105 insertions(+), 181 deletions(-) diff --git a/homeassistant/components/sensibo/__init__.py b/homeassistant/components/sensibo/__init__.py index c384c826859..7401a8c2150 100644 --- a/homeassistant/components/sensibo/__init__.py +++ b/homeassistant/components/sensibo/__init__.py @@ -24,11 +24,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: client = pysensibo.SensiboClient( entry.data[CONF_API_KEY], session=async_get_clientsession(hass), timeout=TIMEOUT ) - devicelist = [] + devices = [] try: async with async_timeout.timeout(TIMEOUT): for dev in await client.async_get_devices(_INITIAL_FETCH_FIELDS): - devicelist.append(dev) + devices.append(dev) except ( aiohttp.client_exceptions.ClientConnectorError, asyncio.TimeoutError, @@ -38,11 +38,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: f"Failed to get devices from Sensibo servers: {err}" ) from err - if not devicelist: + if not devices: return False hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { - "devices": devicelist, + "devices": devices, "client": client, } diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index ac907352735..fd577099627 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -1,11 +1,13 @@ """Support for Sensibo wifi-enabled home thermostats.""" +from __future__ import annotations import asyncio import logging +from typing import Any import aiohttp import async_timeout -import pysensibo +from pysensibo import SensiboClient, SensiboError import voluptuous as vol from homeassistant.components.climate import ( @@ -73,6 +75,7 @@ SENSIBO_TO_HA = { "fan": HVAC_MODE_FAN_ONLY, "auto": HVAC_MODE_HEAT_COOL, "dry": HVAC_MODE_DRY, + "": HVAC_MODE_OFF, } HA_TO_SENSIBO = {value: key for key, value in SENSIBO_TO_HA.items()} @@ -83,7 +86,7 @@ async def async_setup_platform( config: ConfigType, async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType = None, -): +) -> None: """Set up Sensibo devices.""" _LOGGER.warning( "Loading Sensibo via platform setup is deprecated; Please remove it from your configuration" @@ -104,23 +107,23 @@ async def async_setup_entry( data = hass.data[DOMAIN][entry.entry_id] client = data["client"] - devicelist = data["devices"] + devices = data["devices"] - devices = [ + entities = [ SensiboClimate(client, dev, hass.config.units.temperature_unit) - for dev in devicelist + for dev in devices ] - async_add_entities(devices) + async_add_entities(entities) async def async_assume_state(service): """Set state according to external service call..""" if entity_ids := service.data.get(ATTR_ENTITY_ID): target_climate = [ - device for device in devices if device.entity_id in entity_ids + entity for entity in entities if entity.entity_id in entity_ids ] else: - target_climate = devices + target_climate = entities update_tasks = [] for climate in target_climate: @@ -141,44 +144,63 @@ async def async_setup_entry( class SensiboClimate(ClimateEntity): """Representation of a Sensibo device.""" - def __init__(self, client, data, units): - """Build SensiboClimate. - - client: aiohttp session. - data: initially-fetched data. - """ + def __init__(self, client: SensiboClient, data: dict[str, Any], units: str) -> None: + """Initiate SensiboClimate.""" self._client = client self._id = data["id"] self._external_state = None self._units = units - self._available = False - self._do_update(data) self._failed_update = False + self._attr_available = False + self._attr_unique_id = self._id + self._attr_temperature_unit = ( + TEMP_CELSIUS if data["temperatureUnit"] == "C" else TEMP_FAHRENHEIT + ) + self._do_update(data) + self._attr_target_temperature_step = ( + 1 if self.temperature_unit == units else None + ) self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self._id)}, - name=self._name, + name=self._attr_name, manufacturer="Sensibo", configuration_url="https://home.sensibo.com/", model=data["productModel"], sw_version=data["firmwareVersion"], hw_version=data["firmwareType"], - suggested_area=self._name, + suggested_area=self._attr_name, ) - @property - def supported_features(self): - """Return the list of supported features.""" - return self._supported_features - - def _do_update(self, data): - self._name = data["room"]["name"] - self._measurements = data["measurements"] + def _do_update(self, data) -> None: + self._attr_name = data["room"]["name"] self._ac_states = data["acState"] - self._available = data["connectionStatus"]["isAlive"] + self._attr_extra_state_attributes = { + "battery": data["measurements"].get("batteryVoltage") + } + self._attr_current_temperature = convert_temperature( + data["measurements"].get("temperature"), + TEMP_CELSIUS, + self._attr_temperature_unit, + ) + self._attr_current_humidity = data["measurements"].get("humidity") + + self._attr_target_temperature = self._ac_states.get("targetTemperature") + if self._ac_states["on"]: + self._attr_hvac_mode = SENSIBO_TO_HA.get(self._ac_states["mode"], "") + else: + self._attr_hvac_mode = HVAC_MODE_OFF + self._attr_fan_mode = self._ac_states.get("fanLevel") + self._attr_swing_mode = self._ac_states.get("swing") + + self._attr_available = data["connectionStatus"].get("isAlive") capabilities = data["remoteCapabilities"] - self._operations = [SENSIBO_TO_HA[mode] for mode in capabilities["modes"]] - self._operations.append(HVAC_MODE_OFF) - self._current_capabilities = capabilities["modes"][self._ac_states["mode"]] + self._attr_hvac_modes = [SENSIBO_TO_HA[mode] for mode in capabilities["modes"]] + self._attr_hvac_modes.append(HVAC_MODE_OFF) + + current_capabilities = capabilities["modes"][self._ac_states.get("mode")] + self._attr_fan_modes = current_capabilities.get("fanLevels") + self._attr_swing_modes = current_capabilities.get("swing") + temperature_unit_key = data.get("temperatureUnit") or self._ac_states.get( "temperatureUnit" ) @@ -187,129 +209,29 @@ class SensiboClimate(ClimateEntity): TEMP_CELSIUS if temperature_unit_key == "C" else TEMP_FAHRENHEIT ) self._temperatures_list = ( - self._current_capabilities["temperatures"] + current_capabilities["temperatures"] .get(temperature_unit_key, {}) .get("values", []) ) else: self._temperature_unit = self._units self._temperatures_list = [] - self._supported_features = 0 - for key in self._ac_states: - if key in FIELD_TO_FLAG: - self._supported_features |= FIELD_TO_FLAG[key] - - @property - def state(self): - """Return the current state.""" - return self._external_state or super().state - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return {"battery": self.current_battery} - - @property - def temperature_unit(self): - """Return the unit of measurement which this thermostat uses.""" - return self._temperature_unit - - @property - def available(self): - """Return True if entity is available.""" - return self._available - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self._ac_states.get("targetTemperature") - - @property - def target_temperature_step(self): - """Return the supported step of target temperature.""" - if self.temperature_unit == self.hass.config.units.temperature_unit: - # We are working in same units as the a/c unit. Use whole degrees - # like the API supports. - return 1 - # Unit conversion is going on. No point to stick to specific steps. - return None - - @property - def hvac_mode(self): - """Return current operation ie. heat, cool, idle.""" - if not self._ac_states["on"]: - return HVAC_MODE_OFF - return SENSIBO_TO_HA.get(self._ac_states["mode"]) - - @property - def current_humidity(self): - """Return the current humidity.""" - return self._measurements["humidity"] - - @property - def current_battery(self): - """Return the current battery voltage.""" - return self._measurements.get("batteryVoltage") - - @property - def current_temperature(self): - """Return the current temperature.""" - # This field is not affected by temperatureUnit. - # It is always in C - return convert_temperature( - self._measurements["temperature"], TEMP_CELSIUS, self.temperature_unit - ) - - @property - def hvac_modes(self): - """List of available operation modes.""" - return self._operations - - @property - def fan_mode(self): - """Return the fan setting.""" - return self._ac_states.get("fanLevel") - - @property - def fan_modes(self): - """List of available fan modes.""" - return self._current_capabilities.get("fanLevels") - - @property - def swing_mode(self): - """Return the fan setting.""" - return self._ac_states.get("swing") - - @property - def swing_modes(self): - """List of available swing modes.""" - return self._current_capabilities.get("swing") - - @property - def name(self): - """Return the name of the entity.""" - return self._name - - @property - def min_temp(self): - """Return the minimum temperature.""" - return ( + self._attr_min_temp = ( self._temperatures_list[0] if self._temperatures_list else super().min_temp ) - - @property - def max_temp(self): - """Return the maximum temperature.""" - return ( + self._attr_max_temp = ( self._temperatures_list[-1] if self._temperatures_list else super().max_temp ) + self._attr_temperature_unit = self._temperature_unit - @property - def unique_id(self): - """Return unique ID based on Sensibo ID.""" - return self._id + self._attr_supported_features = 0 + for key in self._ac_states: + if key in FIELD_TO_FLAG: + self._attr_supported_features |= FIELD_TO_FLAG[key] - async def async_set_temperature(self, **kwargs): + self._attr_state = self._external_state or super().state + + async def async_set_temperature(self, **kwargs) -> None: """Set new target temperature.""" if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return @@ -331,11 +253,11 @@ class SensiboClimate(ClimateEntity): await self._async_set_ac_state_property("targetTemperature", temperature) - async def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode) -> None: """Set new target fan mode.""" await self._async_set_ac_state_property("fanLevel", fan_mode) - async def async_set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode) -> None: """Set new target operation mode.""" if hvac_mode == HVAC_MODE_OFF: await self._async_set_ac_state_property("on", False) @@ -347,19 +269,19 @@ class SensiboClimate(ClimateEntity): await self._async_set_ac_state_property("mode", HA_TO_SENSIBO[hvac_mode]) - async def async_set_swing_mode(self, swing_mode): + async def async_set_swing_mode(self, swing_mode) -> None: """Set new target swing operation.""" await self._async_set_ac_state_property("swing", swing_mode) - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn Sensibo unit on.""" await self._async_set_ac_state_property("on", True) - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn Sensibo unit on.""" await self._async_set_ac_state_property("on", False) - async def async_assume_state(self, state): + async def async_assume_state(self, state) -> None: """Set external state.""" change_needed = (state != HVAC_MODE_OFF and not self._ac_states["on"]) or ( state == HVAC_MODE_OFF and self._ac_states["on"] @@ -373,7 +295,7 @@ class SensiboClimate(ClimateEntity): else: self._external_state = state - async def async_update(self): + async def async_update(self) -> None: """Retrieve latest state.""" try: async with async_timeout.timeout(TIMEOUT): @@ -381,25 +303,33 @@ class SensiboClimate(ClimateEntity): except ( aiohttp.client_exceptions.ClientError, asyncio.TimeoutError, - pysensibo.SensiboError, - ): + SensiboError, + ) as err: if self._failed_update: _LOGGER.warning( - "Failed to update data for device '%s' from Sensibo servers", - self.name, + "Failed to update data for device '%s' from Sensibo servers with error %s", + self._attr_name, + err, ) - self._available = False + self._attr_available = False self.async_write_ha_state() return - _LOGGER.debug("First failed update data for device '%s'", self.name) + _LOGGER.debug("First failed update data for device '%s'", self._attr_name) self._failed_update = True return + if self.temperature_unit == self.hass.config.units.temperature_unit: + self._attr_target_temperature_step = 1 + else: + self._attr_target_temperature_step = None + self._failed_update = False self._do_update(data) - async def _async_set_ac_state_property(self, name, value, assumed_state=False): + async def _async_set_ac_state_property( + self, name, value, assumed_state=False + ) -> None: """Set AC state.""" try: async with async_timeout.timeout(TIMEOUT): @@ -409,10 +339,10 @@ class SensiboClimate(ClimateEntity): except ( aiohttp.client_exceptions.ClientError, asyncio.TimeoutError, - pysensibo.SensiboError, + SensiboError, ) as err: - self._available = False + self._attr_available = False self.async_write_ha_state() raise Exception( - f"Failed to set AC state for device {self.name} to Sensibo servers" + f"Failed to set AC state for device {self._attr_name} to Sensibo servers" ) from err diff --git a/homeassistant/components/sensibo/config_flow.py b/homeassistant/components/sensibo/config_flow.py index 0d9e7880f38..77f1049d8d2 100644 --- a/homeassistant/components/sensibo/config_flow.py +++ b/homeassistant/components/sensibo/config_flow.py @@ -10,8 +10,9 @@ from pysensibo import SensiboClient, SensiboError import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_API_KEY, CONF_NAME +from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -22,7 +23,6 @@ _LOGGER = logging.getLogger(__name__) DATA_SCHEMA = vol.Schema( { vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_NAME, default=DEFAULT_NAME): cv.string, } ) @@ -53,25 +53,22 @@ class SensiboConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_import(self, config: dict): + async def async_step_import(self, config: dict) -> FlowResult: """Import a configuration from config.yaml.""" self.context.update( {"title_placeholders": {"Sensibo": f"YAML import {DOMAIN}"}} ) - if CONF_NAME not in config: - config[CONF_NAME] = DEFAULT_NAME return await self.async_step_user(user_input=config) - async def async_step_user(self, user_input=None): + async def async_step_user(self, user_input=None) -> FlowResult: """Handle the initial step.""" errors: dict[str, str] = {} - if user_input is not None: + if user_input: api_key = user_input[CONF_API_KEY] - name = user_input[CONF_NAME] await self.async_set_unique_id(api_key) self._abort_if_unique_id_configured() @@ -79,8 +76,8 @@ class SensiboConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): validate = await async_validate_api(self.hass, api_key) if validate: return self.async_create_entry( - title=name, - data={CONF_NAME: name, CONF_API_KEY: api_key}, + title=DEFAULT_NAME, + data={CONF_API_KEY: api_key}, ) errors["base"] = "cannot_connect" diff --git a/homeassistant/components/sensibo/const.py b/homeassistant/components/sensibo/const.py index 7bb8d07b7e8..fb387e64a1a 100644 --- a/homeassistant/components/sensibo/const.py +++ b/homeassistant/components/sensibo/const.py @@ -1,9 +1,11 @@ """Constants for Sensibo.""" +from homeassistant.const import Platform + DOMAIN = "sensibo" -PLATFORMS = ["climate"] +PLATFORMS = [Platform.CLIMATE] ALL = ["all"] -DEFAULT_NAME = "Sensibo@Home" +DEFAULT_NAME = "Sensibo" TIMEOUT = 8 _FETCH_FIELDS = ",".join( [ diff --git a/tests/components/sensibo/test_config_flow.py b/tests/components/sensibo/test_config_flow.py index b277ed80e96..cf3716f09e4 100644 --- a/tests/components/sensibo/test_config_flow.py +++ b/tests/components/sensibo/test_config_flow.py @@ -9,7 +9,7 @@ from pysensibo import SensiboError import pytest from homeassistant import config_entries -from homeassistant.const import CONF_API_KEY, CONF_NAME +from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, @@ -47,7 +47,6 @@ async def test_form(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_NAME: "Sensibo@Home", CONF_API_KEY: "1234567890", }, ) @@ -55,7 +54,6 @@ async def test_form(hass: HomeAssistant) -> None: assert result2["type"] == RESULT_TYPE_CREATE_ENTRY assert result2["data"] == { - "name": "Sensibo@Home", "api_key": "1234567890", } @@ -82,9 +80,8 @@ async def test_import_flow_success(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["type"] == RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == "Sensibo@Home" + assert result2["title"] == "Sensibo" assert result2["data"] == { - "name": "Sensibo@Home", "api_key": "1234567890", } assert len(mock_setup_entry.mock_calls) == 1 @@ -96,7 +93,6 @@ async def test_import_flow_already_exist(hass: HomeAssistant) -> None: MockConfigEntry( domain=DOMAIN, data={ - CONF_NAME: "Sensibo@Home", CONF_API_KEY: "1234567890", }, unique_id="1234567890", @@ -147,7 +143,6 @@ async def test_flow_fails(hass: HomeAssistant, error_message) -> None: result4 = await hass.config_entries.flow.async_configure( result4["flow_id"], user_input={ - CONF_NAME: "Sensibo@Home", CONF_API_KEY: "1234567890", }, ) From 1f425b19427a782da9ac52f18c903e6717fb44ca Mon Sep 17 00:00:00 2001 From: Alberto Geniola Date: Mon, 27 Dec 2021 19:50:43 +0100 Subject: [PATCH 1057/2644] Improve Elmax code quality (#61273) Co-authored-by: Marvin Wichmann Co-authored-by: Franck Nijhof Co-authored-by: Martin Hjelmare --- homeassistant/components/elmax/common.py | 121 ++---- homeassistant/components/elmax/config_flow.py | 135 ++++--- homeassistant/components/elmax/strings.json | 7 +- homeassistant/components/elmax/switch.py | 83 ++-- .../components/elmax/translations/en.json | 17 +- tests/components/elmax/test_config_flow.py | 382 ++++++++++++------ 6 files changed, 426 insertions(+), 319 deletions(-) diff --git a/homeassistant/components/elmax/common.py b/homeassistant/components/elmax/common.py index 43d1cbff150..0008ebc4218 100644 --- a/homeassistant/components/elmax/common.py +++ b/homeassistant/components/elmax/common.py @@ -1,11 +1,9 @@ """Elmax integration common classes and utilities.""" from __future__ import annotations -from collections.abc import Mapping from datetime import timedelta import logging from logging import Logger -from typing import Any import async_timeout from elmax_api.exceptions import ( @@ -15,20 +13,24 @@ from elmax_api.exceptions import ( ElmaxNetworkError, ) from elmax_api.http import Elmax +from elmax_api.model.actuator import Actuator from elmax_api.model.endpoint import DeviceEndpoint from elmax_api.model.panel import PanelEntry, PanelStatus from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError -from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) from .const import DEFAULT_TIMEOUT, DOMAIN _LOGGER = logging.getLogger(__name__) -class ElmaxCoordinator(DataUpdateCoordinator): +class ElmaxCoordinator(DataUpdateCoordinator[PanelStatus]): """Coordinator helper to handle Elmax API polling.""" def __init__( @@ -57,16 +59,11 @@ class ElmaxCoordinator(DataUpdateCoordinator): """Return the panel entry.""" return self._panel_entry - @property - def panel_status(self) -> PanelStatus | None: - """Return the last fetched panel status.""" - return self.data - - def get_endpoint_state(self, endpoint_id: str) -> DeviceEndpoint | None: - """Return the last fetched status for the given endpoint-id.""" + def get_actuator_state(self, actuator_id: str) -> Actuator: + """Return state of a specific actuator.""" if self._state_by_endpoint is not None: - return self._state_by_endpoint.get(endpoint_id) - return None + return self._state_by_endpoint.get(actuator_id) + raise HomeAssistantError("Unknown actuator") @property def http_client(self): @@ -78,18 +75,17 @@ class ElmaxCoordinator(DataUpdateCoordinator): async with async_timeout.timeout(DEFAULT_TIMEOUT): # Retrieve the panel online status first panels = await self._client.list_control_panels() - panels = list(filter(lambda x: x.hash == self._panel_id, panels)) + panel = next( + (panel for panel in panels if panel.hash == self._panel_id), None + ) # If the panel is no more available within the given. Raise config error as the user must # reconfigure it in order to make it work again - if len(panels) < 1: - _LOGGER.error( - "Panel ID %s is no more linked to this user account", - self._panel_id, + if not panel: + raise ConfigEntryAuthFailed( + f"Panel ID {self._panel_id} is no more linked to this user account" ) - raise ConfigEntryAuthFailed() - panel = panels[0] self._panel_entry = panel # If the panel is online, proceed with fetching its state @@ -109,27 +105,22 @@ class ElmaxCoordinator(DataUpdateCoordinator): return None except ElmaxBadPinError as err: - _LOGGER.error("Control panel pin was refused") - raise ConfigEntryAuthFailed from err + raise ConfigEntryAuthFailed("Control panel pin was refused") from err except ElmaxBadLoginError as err: - _LOGGER.error("Refused username/password") - raise ConfigEntryAuthFailed from err + raise ConfigEntryAuthFailed("Refused username/password") from err except ElmaxApiError as err: - raise HomeAssistantError( - f"Error communicating with ELMAX API: {err}" - ) from err + raise UpdateFailed(f"Error communicating with ELMAX API: {err}") from err except ElmaxNetworkError as err: - raise HomeAssistantError( - "Network error occurred while contacting ELMAX cloud" + raise UpdateFailed( + "A network error occurred while communicating with Elmax cloud." ) from err - except Exception as err: - _LOGGER.exception("Unexpected exception") - raise HomeAssistantError("An unexpected error occurred") from err -class ElmaxEntity(Entity): +class ElmaxEntity(CoordinatorEntity): """Wrapper for Elmax entities.""" + coordinator: ElmaxCoordinator + def __init__( self, panel: PanelEntry, @@ -138,21 +129,11 @@ class ElmaxEntity(Entity): coordinator: ElmaxCoordinator, ) -> None: """Construct the object.""" + super().__init__(coordinator=coordinator) self._panel = panel self._device = elmax_device self._panel_version = panel_version - self._coordinator = coordinator - self._transitory_state = None - - @property - def transitory_state(self) -> Any | None: - """Return the transitory state for this entity.""" - return self._transitory_state - - @transitory_state.setter - def transitory_state(self, value: Any) -> None: - """Set the transitory state value.""" - self._transitory_state = value + self._client = coordinator.http_client @property def panel_id(self) -> str: @@ -169,21 +150,13 @@ class ElmaxEntity(Entity): """Return the entity name.""" return self._device.name - @property - def extra_state_attributes(self) -> Mapping[str, Any] | None: - """Return extra attributes.""" - return { - "index": self._device.index, - "visible": self._device.visible, - } - @property def device_info(self): """Return device specific attributes.""" return { "identifiers": {(DOMAIN, self._panel.hash)}, "name": self._panel.get_name_by_user( - self._coordinator.http_client.get_authenticated_username() + self.coordinator.http_client.get_authenticated_username() ), "manufacturer": "Elmax", "model": self._panel_version, @@ -192,39 +165,5 @@ class ElmaxEntity(Entity): @property def available(self) -> bool: - """Return True if entity is available.""" - return self._panel.online - - def _http_data_changed(self) -> None: - # Whenever new HTTP data is received from the coordinator we extract the stat of this - # device and store it locally for later use - device_state = self._coordinator.get_endpoint_state(self._device.endpoint_id) - if self._device is None or device_state.__dict__ != self._device.__dict__: - # If HTTP data has changed, we need to schedule a forced refresh - self._device = device_state - self.async_schedule_update_ha_state(force_refresh=True) - - # Reset the transitory state as we did receive a fresh state - self._transitory_state = None - - @property - def should_poll(self) -> bool: - """Return True if entity has to be polled for state. - - False if entity pushes its state to HA. - """ - return False - - async def async_added_to_hass(self) -> None: - """Run when entity about to be added to hass. - - To be extended by integrations. - """ - self._coordinator.async_add_listener(self._http_data_changed) - - async def async_will_remove_from_hass(self) -> None: - """Run when entity will be removed from hass. - - To be extended by integrations. - """ - self._coordinator.async_remove_listener(self._http_data_changed) + """Return if entity is available.""" + return super().available and self._panel.online diff --git a/homeassistant/components/elmax/config_flow.py b/homeassistant/components/elmax/config_flow.py index 5cd2169c695..6872a555b8a 100644 --- a/homeassistant/components/elmax/config_flow.py +++ b/homeassistant/components/elmax/config_flow.py @@ -50,16 +50,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for elmax-cloud.""" VERSION = 1 - - def __init__(self): - """Initialize.""" - self._client: Elmax = None - self._username: str = None - self._password: str = None - self._panels_schema = None - self._panel_names = None - self._reauth_username = None - self._reauth_panelid = None + _client: Elmax + _username: str + _password: str + _panels_schema: vol.Schema + _panel_names: dict + _reauth_username: str | None + _reauth_panelid: str | None async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -69,69 +66,73 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is None: return self.async_show_form(step_id="user", data_schema=LOGIN_FORM_SCHEMA) - errors: dict[str, str] = {} username = user_input[CONF_ELMAX_USERNAME] password = user_input[CONF_ELMAX_PASSWORD] # Otherwise, it means we are handling now the "submission" of the user form. # In this case, let's try to log in to the Elmax cloud and retrieve the available panels. try: - client = Elmax(username=username, password=password) - await client.login() - - # If the login succeeded, retrieve the list of available panels and filter the online ones - online_panels = [x for x in await client.list_control_panels() if x.online] - - # If no online panel was found, we display an error in the next UI. - panels = list(online_panels) - if len(panels) < 1: - raise NoOnlinePanelsError() - - # Show the panel selection. - # We want the user to choose the panel using the associated name, we set up a mapping - # dictionary to handle that case. - panel_names: dict[str, str] = {} - username = client.get_authenticated_username() - for panel in panels: - _store_panel_by_name( - panel=panel, username=username, panel_names=panel_names - ) - - self._client = client - self._panel_names = panel_names - schema = vol.Schema( - { - vol.Required(CONF_ELMAX_PANEL_NAME): vol.In( - self._panel_names.keys() - ), - vol.Required(CONF_ELMAX_PANEL_PIN, default="000000"): str, - } - ) - self._panels_schema = schema - self._username = username - self._password = password - return self.async_show_form( - step_id="panels", data_schema=schema, errors=errors - ) + client = await self._async_login(username=username, password=password) except ElmaxBadLoginError: - _LOGGER.error("Wrong credentials or failed login") - errors["base"] = "bad_auth" - except NoOnlinePanelsError: - _LOGGER.warning("No online device panel was found") - errors["base"] = "no_panel_online" + return self.async_show_form( + step_id="user", + data_schema=LOGIN_FORM_SCHEMA, + errors={"base": "invalid_auth"}, + ) except ElmaxNetworkError: _LOGGER.exception("A network error occurred") - errors["base"] = "network_error" + return self.async_show_form( + step_id="user", + data_schema=LOGIN_FORM_SCHEMA, + errors={"base": "network_error"}, + ) - # If an error occurred, show back the login form. - return self.async_show_form( - step_id="user", data_schema=LOGIN_FORM_SCHEMA, errors=errors + # If the login succeeded, retrieve the list of available panels and filter the online ones + online_panels = [x for x in await client.list_control_panels() if x.online] + + # If no online panel was found, we display an error in the next UI. + if not online_panels: + return self.async_show_form( + step_id="user", + data_schema=LOGIN_FORM_SCHEMA, + errors={"base": "no_panel_online"}, + ) + + # Show the panel selection. + # We want the user to choose the panel using the associated name, we set up a mapping + # dictionary to handle that case. + panel_names: dict[str, str] = {} + username = client.get_authenticated_username() + for panel in online_panels: + _store_panel_by_name( + panel=panel, username=username, panel_names=panel_names + ) + + self._client = client + self._panel_names = panel_names + schema = vol.Schema( + { + vol.Required(CONF_ELMAX_PANEL_NAME): vol.In(self._panel_names.keys()), + vol.Required(CONF_ELMAX_PANEL_PIN, default="000000"): str, + } ) + self._panels_schema = schema + self._username = username + self._password = password + # If everything went OK, proceed to panel selection. + return await self.async_step_panels(user_input=None) - async def async_step_panels(self, user_input: dict[str, Any]) -> FlowResult: + async def async_step_panels( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle Panel selection step.""" - errors = {} + errors: dict[str, Any] = {} + if user_input is None: + return self.async_show_form( + step_id="panels", data_schema=self._panels_schema, errors=errors + ) + panel_name = user_input[CONF_ELMAX_PANEL_NAME] panel_pin = user_input[CONF_ELMAX_PANEL_PIN] @@ -160,7 +161,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "invalid_pin" except Exception: # pylint: disable=broad-except _LOGGER.exception("Error occurred") - errors["base"] = "unknown_error" + errors["base"] = "unknown" return self.async_show_form( step_id="panels", data_schema=self._panels_schema, errors=errors @@ -184,8 +185,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # and verify its pin is correct. try: # Test login. - client = Elmax(username=self._reauth_username, password=password) - await client.login() + client = await self._async_login( + username=self._reauth_username, password=password + ) # Make sure the panel we are authenticating to is still available. panels = [ @@ -220,7 +222,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.error( "Wrong credentials or failed login while re-authenticating" ) - errors["base"] = "bad_auth" + errors["base"] = "invalid_auth" except NoOnlinePanelsError: _LOGGER.warning( "Panel ID %s is no longer associated to this user", @@ -245,6 +247,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="reauth_confirm", data_schema=schema, errors=errors ) + @staticmethod + async def _async_login(username: str, password: str) -> Elmax: + """Log in to the Elmax cloud and return the http client.""" + client = Elmax(username=username, password=password) + await client.login() + return client + class NoOnlinePanelsError(HomeAssistantError): """Error occurring when no online panel was found.""" diff --git a/homeassistant/components/elmax/strings.json b/homeassistant/components/elmax/strings.json index 505622aa6ae..3bfce6bb0b0 100644 --- a/homeassistant/components/elmax/strings.json +++ b/homeassistant/components/elmax/strings.json @@ -1,9 +1,7 @@ { - "title": "Elmax Cloud Setup", "config": { "step": { "user": { - "title": "Account Login", "description": "Please login to the Elmax cloud using your credentials", "data": { "password": "[%key:common::config_flow::data::password%]", @@ -11,7 +9,6 @@ } }, "panels": { - "title": "Panel selection", "description": "Select which panel you would like to control with this integration. Please note that the panel must be ON in order to be configured.", "data": { "panel_name": "Panel Name", @@ -22,10 +19,10 @@ }, "error": { "no_panel_online": "No online Elmax control panel was found.", - "bad_auth": "Invalid authentication", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "network_error": "A network error occurred", "invalid_pin": "The provided pin is invalid", - "unknown_error": "An unexpected error occurred" + "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" diff --git a/homeassistant/components/elmax/switch.py b/homeassistant/components/elmax/switch.py index 11c0c406576..66f864585e8 100644 --- a/homeassistant/components/elmax/switch.py +++ b/homeassistant/components/elmax/switch.py @@ -1,4 +1,6 @@ """Elmax switch platform.""" +import asyncio +import logging from typing import Any from elmax_api.model.command import SwitchCommand @@ -13,39 +15,7 @@ from . import ElmaxCoordinator from .common import ElmaxEntity from .const import DOMAIN - -class ElmaxSwitch(ElmaxEntity, SwitchEntity): - """Implement the Elmax switch entity.""" - - @property - def is_on(self) -> bool: - """Return True if entity is on.""" - if self.transitory_state is not None: - return self.transitory_state - return self._device.opened - - async def async_turn_on(self, **kwargs: Any) -> None: - """Turn the entity on.""" - client = self._coordinator.http_client - await client.execute_command( - endpoint_id=self._device.endpoint_id, command=SwitchCommand.TURN_ON - ) - self.transitory_state = True - await self.async_update_ha_state() - - async def async_turn_off(self, **kwargs: Any) -> None: - """Turn the entity off.""" - client = self._coordinator.http_client - await client.execute_command( - endpoint_id=self._device.endpoint_id, command=SwitchCommand.TURN_OFF - ) - self.transitory_state = False - await self.async_update_ha_state() - - @property - def assumed_state(self) -> bool: - """Return True if unable to access real state of the entity.""" - return False +_LOGGER = logging.getLogger(__name__) async def async_setup_entry( @@ -58,7 +28,7 @@ async def async_setup_entry( known_devices = set() def _discover_new_devices(): - panel_status = coordinator.panel_status # type: PanelStatus + panel_status: PanelStatus = coordinator.data # In case the panel is offline, its status will be None. In that case, simply do nothing if panel_status is None: return @@ -82,3 +52,48 @@ async def async_setup_entry( # Immediately run a discovery, so we don't need to wait for the next update _discover_new_devices() + + +class ElmaxSwitch(ElmaxEntity, SwitchEntity): + """Implement the Elmax switch entity.""" + + @property + def is_on(self) -> bool: + """Return True if entity is on.""" + return self.coordinator.get_actuator_state(self._device.endpoint_id).opened + + async def _wait_for_state_change(self) -> bool: + """Refresh data and wait until the state state changes.""" + old_state = self.coordinator.get_actuator_state(self._device.endpoint_id).opened + + # Wait a bit at first to let Elmax cloud assimilate the new state. + await asyncio.sleep(2.0) + await self.coordinator.async_refresh() + new_state = self.coordinator.get_actuator_state(self._device.endpoint_id).opened + + # First check attempt. + if new_state == old_state: + # Otherwise sleep a bit more and then trigger a final update. + await asyncio.sleep(5.0) + await self.coordinator.async_refresh() + new_state = self.coordinator.get_actuator_state( + self._device.endpoint_id + ).opened + + return new_state != old_state + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + await self.coordinator.http_client.execute_command( + endpoint_id=self._device.endpoint_id, command=SwitchCommand.TURN_ON + ) + if await self._wait_for_state_change(): + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + await self.coordinator.http_client.execute_command( + endpoint_id=self._device.endpoint_id, command=SwitchCommand.TURN_OFF + ) + if await self._wait_for_state_change(): + self.async_write_ha_state() diff --git a/homeassistant/components/elmax/translations/en.json b/homeassistant/components/elmax/translations/en.json index b3de51d64fc..6a73dfa2c07 100644 --- a/homeassistant/components/elmax/translations/en.json +++ b/homeassistant/components/elmax/translations/en.json @@ -1,14 +1,15 @@ { "config": { "abort": { - "already_configured": "Device is already configured" + "single_instance_allowed": "There already is an integration for that Elmaxc panel." }, "error": { - "bad_auth": "Invalid authentication", + "invalid_auth": "Invalid authentication", "invalid_pin": "The provided pin is invalid", "network_error": "A network error occurred", "no_panel_online": "No online Elmax control panel was found.", - "unknown_error": "An unexpected error occurred" + "reauth_panel_disappeared": "The panel is no longer associated to your account.", + "unknown": "Unexpected error" }, "step": { "panels": { @@ -20,6 +21,16 @@ "description": "Select which panel you would like to control with this integration. Please note that the panel must be ON in order to be configured.", "title": "Panel selection" }, + "reauth_confirm": { + "data": { + "username": "Username", + "password": "Password", + "panel_id": "Panel ID", + "panel_pin": "PIN Code" + }, + "description": "Please authenticate again to the Elmax cloud.", + "title": "Re-Authenticate" + }, "user": { "data": { "password": "Password", diff --git a/tests/components/elmax/test_config_flow.py b/tests/components/elmax/test_config_flow.py index 4584ab679f4..5b8d42799e9 100644 --- a/tests/components/elmax/test_config_flow.py +++ b/tests/components/elmax/test_config_flow.py @@ -1,4 +1,4 @@ -"""Tests for the Abode config flow.""" +"""Tests for the Elmax config flow.""" from unittest.mock import patch from elmax_api.exceptions import ElmaxBadLoginError, ElmaxBadPinError, ElmaxNetworkError @@ -13,8 +13,8 @@ from homeassistant.components.elmax.const import ( DOMAIN, ) from homeassistant.config_entries import SOURCE_REAUTH -from homeassistant.data_entry_flow import FlowResult +from tests.common import MockConfigEntry from tests.components.elmax import ( MOCK_PANEL_ID, MOCK_PANEL_NAME, @@ -26,78 +26,6 @@ from tests.components.elmax import ( CONF_POLLING = "polling" -def _has_error(errors): - return errors is not None and len(errors.keys()) > 0 - - -async def _bootstrap( - hass, - source=config_entries.SOURCE_USER, - username=MOCK_USERNAME, - password=MOCK_PASSWORD, - panel_name=MOCK_PANEL_NAME, - panel_pin=MOCK_PANEL_PIN, -) -> FlowResult: - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": source} - ) - if result["type"] != data_entry_flow.RESULT_TYPE_FORM or _has_error( - result["errors"] - ): - return result - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_ELMAX_USERNAME: username, - CONF_ELMAX_PASSWORD: password, - }, - ) - if result2["type"] != data_entry_flow.RESULT_TYPE_FORM or _has_error( - result2["errors"] - ): - return result2 - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], - { - CONF_ELMAX_PANEL_NAME: panel_name, - CONF_ELMAX_PANEL_PIN: panel_pin, - }, - ) - return result3 - - -async def _reauth(hass): - - # Trigger reauth - result2 = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data={ - CONF_ELMAX_PANEL_ID: MOCK_PANEL_ID, - CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, - CONF_ELMAX_USERNAME: MOCK_USERNAME, - CONF_ELMAX_PASSWORD: MOCK_PASSWORD, - }, - ) - if result2["type"] != data_entry_flow.RESULT_TYPE_FORM or _has_error( - result2["errors"] - ): - return result2 - - # Perform reauth confirm step - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], - { - CONF_ELMAX_PANEL_ID: MOCK_PANEL_ID, - CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, - CONF_ELMAX_USERNAME: MOCK_USERNAME, - CONF_ELMAX_PASSWORD: MOCK_PASSWORD, - }, - ) - return result3 - - async def test_show_form(hass): """Test that the form is served with no input.""" result = await hass.config_entries.flow.async_init( @@ -107,16 +35,67 @@ async def test_show_form(hass): assert result["step_id"] == "user" +async def test_standard_setup(hass): + """Test the standard setup case.""" + # Setup once. + show_form_result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + with patch( + "homeassistant.components.elmax.async_setup_entry", + return_value=True, + ): + login_result = await hass.config_entries.flow.async_configure( + show_form_result["flow_id"], + { + CONF_ELMAX_USERNAME: MOCK_USERNAME, + CONF_ELMAX_PASSWORD: MOCK_PASSWORD, + }, + ) + result = await hass.config_entries.flow.async_configure( + login_result["flow_id"], + { + CONF_ELMAX_PANEL_NAME: MOCK_PANEL_NAME, + CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, + }, + ) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + async def test_one_config_allowed(hass): """Test that only one Elmax configuration is allowed for each panel.""" - # Setup once. - attempt1 = await _bootstrap(hass) - assert attempt1["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + MockConfigEntry( + domain=DOMAIN, + data={ + CONF_ELMAX_PANEL_ID: MOCK_PANEL_ID, + CONF_ELMAX_USERNAME: MOCK_USERNAME, + CONF_ELMAX_PASSWORD: MOCK_PASSWORD, + CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, + }, + unique_id=MOCK_PANEL_ID, + ).add_to_hass(hass) # Attempt to add another instance of the integration for the very same panel, it must fail. - attempt2 = await _bootstrap(hass) - assert attempt2["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert attempt2["reason"] == "already_configured" + show_form_result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + login_result = await hass.config_entries.flow.async_configure( + show_form_result["flow_id"], + { + CONF_ELMAX_USERNAME: MOCK_USERNAME, + CONF_ELMAX_PASSWORD: MOCK_PASSWORD, + }, + ) + result = await hass.config_entries.flow.async_configure( + login_result["flow_id"], + { + CONF_ELMAX_PANEL_NAME: MOCK_PANEL_NAME, + CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" async def test_invalid_credentials(hass): @@ -125,12 +104,19 @@ async def test_invalid_credentials(hass): "elmax_api.http.Elmax.login", side_effect=ElmaxBadLoginError(), ): - result = await _bootstrap( - hass, username="wrong_user_name@email.com", password="incorrect_password" + show_form_result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "bad_auth"} + login_result = await hass.config_entries.flow.async_configure( + show_form_result["flow_id"], + { + CONF_ELMAX_USERNAME: "wrong_user_name@email.com", + CONF_ELMAX_PASSWORD: "incorrect_password", + }, + ) + assert login_result["step_id"] == "user" + assert login_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert login_result["errors"] == {"base": "invalid_auth"} async def test_connection_error(hass): @@ -139,12 +125,19 @@ async def test_connection_error(hass): "elmax_api.http.Elmax.login", side_effect=ElmaxNetworkError(), ): - result = await _bootstrap( - hass, username="wrong_user_name@email.com", password="incorrect_password" + show_form_result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "network_error"} + login_result = await hass.config_entries.flow.async_configure( + show_form_result["flow_id"], + { + CONF_ELMAX_USERNAME: MOCK_USERNAME, + CONF_ELMAX_PASSWORD: MOCK_PASSWORD, + }, + ) + assert login_result["step_id"] == "user" + assert login_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert login_result["errors"] == {"base": "network_error"} async def test_unhandled_error(hass): @@ -153,10 +146,26 @@ async def test_unhandled_error(hass): "elmax_api.http.Elmax.get_panel_status", side_effect=Exception(), ): - result = await _bootstrap(hass) + show_form_result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + login_result = await hass.config_entries.flow.async_configure( + show_form_result["flow_id"], + { + CONF_ELMAX_USERNAME: MOCK_USERNAME, + CONF_ELMAX_PASSWORD: MOCK_PASSWORD, + }, + ) + result = await hass.config_entries.flow.async_configure( + login_result["flow_id"], + { + CONF_ELMAX_PANEL_NAME: MOCK_PANEL_NAME, + CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, + }, + ) assert result["step_id"] == "panels" assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "unknown_error"} + assert result["errors"] == {"base": "unknown"} async def test_invalid_pin(hass): @@ -166,7 +175,23 @@ async def test_invalid_pin(hass): "elmax_api.http.Elmax.get_panel_status", side_effect=ElmaxBadPinError(), ): - result = await _bootstrap(hass) + show_form_result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + login_result = await hass.config_entries.flow.async_configure( + show_form_result["flow_id"], + { + CONF_ELMAX_USERNAME: MOCK_USERNAME, + CONF_ELMAX_PASSWORD: MOCK_PASSWORD, + }, + ) + result = await hass.config_entries.flow.async_configure( + login_result["flow_id"], + { + CONF_ELMAX_PANEL_NAME: MOCK_PANEL_NAME, + CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, + }, + ) assert result["step_id"] == "panels" assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {"base": "invalid_pin"} @@ -179,22 +204,19 @@ async def test_no_online_panel(hass): "elmax_api.http.Elmax.list_control_panels", return_value=[], ): - result = await _bootstrap(hass) - assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "no_panel_online"} - - -async def test_step_user(hass): - """Test that the user step works.""" - result = await _bootstrap(hass) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["data"] == { - CONF_ELMAX_PANEL_ID: MOCK_PANEL_ID, - CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, - CONF_ELMAX_USERNAME: MOCK_USERNAME, - CONF_ELMAX_PASSWORD: MOCK_PASSWORD, - } + show_form_result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + login_result = await hass.config_entries.flow.async_configure( + show_form_result["flow_id"], + { + CONF_ELMAX_USERNAME: MOCK_USERNAME, + CONF_ELMAX_PASSWORD: MOCK_PASSWORD, + }, + ) + assert login_result["step_id"] == "user" + assert login_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert login_result["errors"] == {"base": "no_panel_online"} async def test_show_reauth(hass): @@ -215,24 +237,84 @@ async def test_show_reauth(hass): async def test_reauth_flow(hass): """Test that the reauth flow works.""" - # Simulate a first setup - await _bootstrap(hass) + MockConfigEntry( + domain=DOMAIN, + data={ + CONF_ELMAX_PANEL_ID: MOCK_PANEL_ID, + CONF_ELMAX_USERNAME: MOCK_USERNAME, + CONF_ELMAX_PASSWORD: MOCK_PASSWORD, + CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, + }, + unique_id=MOCK_PANEL_ID, + ).add_to_hass(hass) + # Trigger reauth - result = await _reauth(hass) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "reauth_successful" + with patch( + "homeassistant.components.elmax.async_setup_entry", + return_value=True, + ): + reauth_result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH}, + data={ + CONF_ELMAX_PANEL_ID: MOCK_PANEL_ID, + CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, + CONF_ELMAX_USERNAME: MOCK_USERNAME, + CONF_ELMAX_PASSWORD: MOCK_PASSWORD, + }, + ) + result = await hass.config_entries.flow.async_configure( + reauth_result["flow_id"], + { + CONF_ELMAX_PANEL_ID: MOCK_PANEL_ID, + CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, + CONF_ELMAX_USERNAME: MOCK_USERNAME, + CONF_ELMAX_PASSWORD: MOCK_PASSWORD, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + await hass.async_block_till_done() + assert result["reason"] == "reauth_successful" async def test_reauth_panel_disappeared(hass): """Test that the case where panel is no longer associated with the user.""" # Simulate a first setup - await _bootstrap(hass) + MockConfigEntry( + domain=DOMAIN, + data={ + CONF_ELMAX_PANEL_ID: MOCK_PANEL_ID, + CONF_ELMAX_USERNAME: MOCK_USERNAME, + CONF_ELMAX_PASSWORD: MOCK_PASSWORD, + CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, + }, + unique_id=MOCK_PANEL_ID, + ).add_to_hass(hass) + # Trigger reauth with patch( "elmax_api.http.Elmax.list_control_panels", return_value=[], ): - result = await _reauth(hass) + reauth_result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH}, + data={ + CONF_ELMAX_PANEL_ID: MOCK_PANEL_ID, + CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, + CONF_ELMAX_USERNAME: MOCK_USERNAME, + CONF_ELMAX_PASSWORD: MOCK_PASSWORD, + }, + ) + result = await hass.config_entries.flow.async_configure( + reauth_result["flow_id"], + { + CONF_ELMAX_PANEL_ID: MOCK_PANEL_ID, + CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, + CONF_ELMAX_USERNAME: MOCK_USERNAME, + CONF_ELMAX_PASSWORD: MOCK_PASSWORD, + }, + ) assert result["step_id"] == "reauth_confirm" assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {"base": "reauth_panel_disappeared"} @@ -240,14 +322,41 @@ async def test_reauth_panel_disappeared(hass): async def test_reauth_invalid_pin(hass): """Test that the case where panel is no longer associated with the user.""" - # Simulate a first setup - await _bootstrap(hass) + MockConfigEntry( + domain=DOMAIN, + data={ + CONF_ELMAX_PANEL_ID: MOCK_PANEL_ID, + CONF_ELMAX_USERNAME: MOCK_USERNAME, + CONF_ELMAX_PASSWORD: MOCK_PASSWORD, + CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, + }, + unique_id=MOCK_PANEL_ID, + ).add_to_hass(hass) + # Trigger reauth with patch( "elmax_api.http.Elmax.get_panel_status", side_effect=ElmaxBadPinError(), ): - result = await _reauth(hass) + reauth_result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH}, + data={ + CONF_ELMAX_PANEL_ID: MOCK_PANEL_ID, + CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, + CONF_ELMAX_USERNAME: MOCK_USERNAME, + CONF_ELMAX_PASSWORD: MOCK_PASSWORD, + }, + ) + result = await hass.config_entries.flow.async_configure( + reauth_result["flow_id"], + { + CONF_ELMAX_PANEL_ID: MOCK_PANEL_ID, + CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, + CONF_ELMAX_USERNAME: MOCK_USERNAME, + CONF_ELMAX_PASSWORD: MOCK_PASSWORD, + }, + ) assert result["step_id"] == "reauth_confirm" assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {"base": "invalid_pin"} @@ -255,14 +364,41 @@ async def test_reauth_invalid_pin(hass): async def test_reauth_bad_login(hass): """Test bad login attempt at reauth time.""" - # Simulate a first setup - await _bootstrap(hass) + MockConfigEntry( + domain=DOMAIN, + data={ + CONF_ELMAX_PANEL_ID: MOCK_PANEL_ID, + CONF_ELMAX_USERNAME: MOCK_USERNAME, + CONF_ELMAX_PASSWORD: MOCK_PASSWORD, + CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, + }, + unique_id=MOCK_PANEL_ID, + ).add_to_hass(hass) + # Trigger reauth with patch( "elmax_api.http.Elmax.login", side_effect=ElmaxBadLoginError(), ): - result = await _reauth(hass) + reauth_result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH}, + data={ + CONF_ELMAX_PANEL_ID: MOCK_PANEL_ID, + CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, + CONF_ELMAX_USERNAME: MOCK_USERNAME, + CONF_ELMAX_PASSWORD: MOCK_PASSWORD, + }, + ) + result = await hass.config_entries.flow.async_configure( + reauth_result["flow_id"], + { + CONF_ELMAX_PANEL_ID: MOCK_PANEL_ID, + CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, + CONF_ELMAX_USERNAME: MOCK_USERNAME, + CONF_ELMAX_PASSWORD: MOCK_PASSWORD, + }, + ) assert result["step_id"] == "reauth_confirm" assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "bad_auth"} + assert result["errors"] == {"base": "invalid_auth"} From 7fc56056395d8ff41f23010bfc3f962f94174276 Mon Sep 17 00:00:00 2001 From: corneyl Date: Mon, 27 Dec 2021 20:31:35 +0100 Subject: [PATCH 1058/2644] Fix keyerror when no previous Picnic orders exist (#62870) --- homeassistant/components/picnic/coordinator.py | 13 +++++++------ tests/components/picnic/test_sensor.py | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/picnic/coordinator.py b/homeassistant/components/picnic/coordinator.py index bcd4e79a098..71a6559975c 100644 --- a/homeassistant/components/picnic/coordinator.py +++ b/homeassistant/components/picnic/coordinator.py @@ -60,11 +60,11 @@ class PicnicUpdateCoordinator(DataUpdateCoordinator): """Fetch the data from the Picnic API and return a flat dict with only needed sensor data.""" # Fetch from the API and pre-process the data cart = self.picnic_api_client.get_cart() - last_order = self._get_last_order() - if not cart or not last_order: + if not cart: raise UpdateFailed("API response doesn't contain expected data.") + last_order = self._get_last_order() slot_data = self._get_slot_data(cart) return { @@ -102,11 +102,12 @@ class PicnicUpdateCoordinator(DataUpdateCoordinator): """Get data of the last order from the list of deliveries.""" # Get the deliveries deliveries = self.picnic_api_client.get_deliveries(summary=True) - if not deliveries: - return {} - # Determine the last order - last_order = copy.deepcopy(deliveries[0]) + # Determine the last order and return an empty dict if there is none + try: + last_order = copy.deepcopy(deliveries[0]) + except KeyError: + return {} # Get the position details if the order is not delivered yet delivery_position = {} diff --git a/tests/components/picnic/test_sensor.py b/tests/components/picnic/test_sensor.py index f1882bfa098..7ade1e38d09 100644 --- a/tests/components/picnic/test_sensor.py +++ b/tests/components/picnic/test_sensor.py @@ -387,6 +387,21 @@ class TestPicnicSensor(unittest.IsolatedAsyncioTestCase): self._assert_sensor("sensor.picnic_last_order_eta_end", STATE_UNAVAILABLE) self._assert_sensor("sensor.picnic_last_order_delivery_time", STATE_UNAVAILABLE) + async def test_sensors_malformed_delivery_data(self): + """Test sensor states when the delivery api returns not a list.""" + # Setup platform with default responses + await self._setup_platform(use_default_responses=True) + + # Change mock responses to empty data and refresh the coordinator + self.picnic_mock().get_deliveries.return_value = {"error": "message"} + await self._coordinator.async_refresh() + + # Assert all last-order sensors have STATE_UNAVAILABLE because the delivery info fetch failed + assert self._coordinator.last_update_success is True + self._assert_sensor("sensor.picnic_last_order_eta_start", STATE_UNKNOWN) + self._assert_sensor("sensor.picnic_last_order_eta_end", STATE_UNKNOWN) + self._assert_sensor("sensor.picnic_last_order_delivery_time", STATE_UNKNOWN) + async def test_sensors_malformed_response(self): """Test coordinator update fails when API yields ValueError.""" # Setup platform with default responses From 17fbfe2eeda3301ce34bb8aba20e57f1f73bd938 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 27 Dec 2021 11:39:57 -0800 Subject: [PATCH 1059/2644] Set a suggested_area on nest devices based on the Google Home room name (#62871) --- homeassistant/components/nest/device_info.py | 19 +++++++---- tests/components/nest/test_device_info.py | 36 ++++++++++++++++++++ 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/nest/device_info.py b/homeassistant/components/nest/device_info.py index 714e9cb00da..b9aa52aa2c6 100644 --- a/homeassistant/components/nest/device_info.py +++ b/homeassistant/components/nest/device_info.py @@ -35,6 +35,7 @@ class NestDeviceInfo: manufacturer=self.device_brand, model=self.device_model, name=self.device_name, + suggested_area=self.suggested_area, ) @property @@ -44,12 +45,9 @@ class NestDeviceInfo: trait: InfoTrait = self._device.traits[InfoTrait.NAME] if trait.custom_name: return str(trait.custom_name) - # Build a name from the room/structure. Note: This room/structure name - # is not associated with a home assistant Area. - if parent_relations := self._device.parent_relations: - items = sorted(parent_relations.items()) - names = [name for id, name in items] - return " ".join(names) + # Build a name from the room/structure if not set explicitly + if area := self.suggested_area: + return area return self.device_model @property @@ -59,3 +57,12 @@ class NestDeviceInfo: # devices, instead relying on traits, but we can infer a generic model # name based on the type return DEVICE_TYPE_MAP.get(self._device.type) + + @property + def suggested_area(self) -> str | None: + """Return device suggested area based on the Google Home room.""" + if parent_relations := self._device.parent_relations: + items = sorted(parent_relations.items()) + names = [name for id, name in items] + return " ".join(names) + return None diff --git a/tests/components/nest/test_device_info.py b/tests/components/nest/test_device_info.py index a333a31c2d2..a31a155b4ba 100644 --- a/tests/components/nest/test_device_info.py +++ b/tests/components/nest/test_device_info.py @@ -8,6 +8,7 @@ from homeassistant.const import ( ATTR_MANUFACTURER, ATTR_MODEL, ATTR_NAME, + ATTR_SUGGESTED_AREA, ) @@ -35,6 +36,7 @@ def test_device_custom_name(): ATTR_NAME: "My Doorbell", ATTR_MANUFACTURER: "Google Nest", ATTR_MODEL: "Doorbell", + ATTR_SUGGESTED_AREA: None, } @@ -60,6 +62,7 @@ def test_device_name_room(): ATTR_NAME: "Some Room", ATTR_MANUFACTURER: "Google Nest", ATTR_MODEL: "Doorbell", + ATTR_SUGGESTED_AREA: "Some Room", } @@ -79,6 +82,7 @@ def test_device_no_name(): ATTR_NAME: "Doorbell", ATTR_MANUFACTURER: "Google Nest", ATTR_MODEL: "Doorbell", + ATTR_SUGGESTED_AREA: None, } @@ -106,4 +110,36 @@ def test_device_invalid_type(): ATTR_NAME: "My Doorbell", ATTR_MANUFACTURER: "Google Nest", ATTR_MODEL: None, + ATTR_SUGGESTED_AREA: None, + } + + +def test_suggested_area(): + """Test the suggested area with different device name and room name.""" + device = Device.MakeDevice( + { + "name": "some-device-id", + "type": "sdm.devices.types.DOORBELL", + "traits": { + "sdm.devices.traits.Info": { + "customName": "My Doorbell", + }, + }, + "parentRelations": [ + {"parent": "some-structure-id", "displayName": "Some Room"} + ], + }, + auth=None, + ) + + device_info = NestDeviceInfo(device) + assert device_info.device_name == "My Doorbell" + assert device_info.device_model == "Doorbell" + assert device_info.device_brand == "Google Nest" + assert device_info.device_info == { + ATTR_IDENTIFIERS: {("nest", "some-device-id")}, + ATTR_NAME: "My Doorbell", + ATTR_MANUFACTURER: "Google Nest", + ATTR_MODEL: "Doorbell", + ATTR_SUGGESTED_AREA: "Some Room", } From 45ab9a3e33367d62715f656ae7a5c96bb19cf78e Mon Sep 17 00:00:00 2001 From: Amos Yuen Date: Mon, 27 Dec 2021 11:52:26 -0800 Subject: [PATCH 1060/2644] Init template trigger binary sensor to None instead of False (#62769) --- homeassistant/components/template/binary_sensor.py | 2 +- tests/components/template/test_binary_sensor.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index c3557ba21f4..688c8e42973 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -324,7 +324,7 @@ class TriggerBinarySensorEntity(TriggerEntity, BinarySensorEntity): self._delay_cancel = None self._auto_off_cancel = None - self._state = False + self._state = None @property def is_on(self) -> bool: diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index 4ed4d59c124..344ae27fcaa 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -909,11 +909,11 @@ async def test_trigger_entity(hass, start_ha): await hass.async_block_till_done() state = hass.states.get("binary_sensor.hello_name") assert state is not None - assert state.state == OFF + assert state.state == STATE_UNKNOWN state = hass.states.get("binary_sensor.bare_minimum") assert state is not None - assert state.state == OFF + assert state.state == STATE_UNKNOWN context = Context() hass.bus.async_fire("test_event", {"beer": 2}, context=context) @@ -976,7 +976,7 @@ async def test_trigger_entity(hass, start_ha): async def test_template_with_trigger_templated_delay_on(hass, start_ha): """Test binary sensor template with template delay on.""" state = hass.states.get("binary_sensor.test") - assert state.state == OFF + assert state.state == STATE_UNKNOWN context = Context() hass.bus.async_fire("test_event", {"beer": 2}, context=context) From de2adce1ca299034f7246b2d8c73ea1021d502c5 Mon Sep 17 00:00:00 2001 From: htmltiger <1429451+htmltiger@users.noreply.github.com> Date: Mon, 27 Dec 2021 19:55:43 +0000 Subject: [PATCH 1061/2644] Fix TypeError of vacuum battery level None (#62722) Co-authored-by: Franck Nijhof --- homeassistant/components/google_assistant/trait.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 26cce0bd5a5..28c939ec261 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -650,6 +650,8 @@ class EnergyStorageTrait(_Trait): def query_attributes(self): """Return EnergyStorage query attributes.""" battery_level = self.state.attributes.get(ATTR_BATTERY_LEVEL) + if battery_level is None: + return {} if battery_level == 100: descriptive_capacity_remaining = "FULL" elif 75 <= battery_level < 100: From 5824477298dbcf344f2603e22cdad994a9242a3c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 27 Dec 2021 20:58:33 +0100 Subject: [PATCH 1062/2644] Update tuya-iot-py-sdk to 0.6.6 (#62858) --- homeassistant/components/tuya/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tuya/manifest.json b/homeassistant/components/tuya/manifest.json index c48771b85be..1b8772a36df 100644 --- a/homeassistant/components/tuya/manifest.json +++ b/homeassistant/components/tuya/manifest.json @@ -2,7 +2,7 @@ "domain": "tuya", "name": "Tuya", "documentation": "https://www.home-assistant.io/integrations/tuya", - "requirements": ["tuya-iot-py-sdk==0.6.3"], + "requirements": ["tuya-iot-py-sdk==0.6.6"], "dependencies": ["ffmpeg"], "codeowners": ["@Tuya", "@zlinoliver", "@METISU", "@frenck"], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index da911d50a23..c7f3650e4c6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2358,7 +2358,7 @@ tp-connected==0.0.4 transmissionrpc==0.11 # homeassistant.components.tuya -tuya-iot-py-sdk==0.6.3 +tuya-iot-py-sdk==0.6.6 # homeassistant.components.twentemilieu twentemilieu==0.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 03aa30aa325..7abff39ae50 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1398,7 +1398,7 @@ total_connect_client==2021.12 transmissionrpc==0.11 # homeassistant.components.tuya -tuya-iot-py-sdk==0.6.3 +tuya-iot-py-sdk==0.6.6 # homeassistant.components.twentemilieu twentemilieu==0.5.0 From 3c2d5d5f8ca345784dbb79b957702f7de1a31253 Mon Sep 17 00:00:00 2001 From: Florent Thoumie Date: Mon, 27 Dec 2021 12:20:55 -0800 Subject: [PATCH 1063/2644] Update to iaqualink 0.4.1 (#53745) Co-authored-by: J. Nick Koston --- .../components/iaqualink/__init__.py | 60 +-- homeassistant/components/iaqualink/climate.py | 9 +- .../components/iaqualink/config_flow.py | 22 +- homeassistant/components/iaqualink/light.py | 20 +- .../components/iaqualink/manifest.json | 2 +- .../components/iaqualink/strings.json | 3 +- homeassistant/components/iaqualink/switch.py | 5 +- homeassistant/components/iaqualink/utils.py | 16 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/iaqualink/conftest.py | 82 +++++ .../components/iaqualink/test_config_flow.py | 58 ++- tests/components/iaqualink/test_init.py | 341 ++++++++++++++++++ tests/components/iaqualink/test_utils.py | 23 ++ 14 files changed, 580 insertions(+), 65 deletions(-) create mode 100644 homeassistant/components/iaqualink/utils.py create mode 100644 tests/components/iaqualink/conftest.py create mode 100644 tests/components/iaqualink/test_init.py create mode 100644 tests/components/iaqualink/test_utils.py diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index 61f2c906c17..73aa6f18867 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -6,16 +6,16 @@ from functools import wraps import logging import aiohttp.client_exceptions -from iaqualink import ( +from iaqualink.client import AqualinkClient +from iaqualink.device import ( AqualinkBinarySensor, - AqualinkClient, AqualinkDevice, AqualinkLight, - AqualinkLoginException, AqualinkSensor, AqualinkThermostat, AqualinkToggle, ) +from iaqualink.exception import AqualinkServiceException import voluptuous as vol from homeassistant import config_entries @@ -73,12 +73,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Aqualink component.""" conf = config.get(DOMAIN) - hass.data[DOMAIN] = {} - if conf is not None: hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=conf + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=conf, ) ) @@ -90,6 +90,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: username = entry.data[CONF_USERNAME] password = entry.data[CONF_PASSWORD] + hass.data.setdefault(DOMAIN, {}) + # These will contain the initialized devices binary_sensors = hass.data[DOMAIN][BINARY_SENSOR_DOMAIN] = [] climates = hass.data[DOMAIN][CLIMATE_DOMAIN] = [] @@ -101,24 +103,36 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: aqualink = AqualinkClient(username, password, session) try: await aqualink.login() - except AqualinkLoginException as login_exception: + except AqualinkServiceException as login_exception: _LOGGER.error("Failed to login: %s", login_exception) return False except ( asyncio.TimeoutError, aiohttp.client_exceptions.ClientConnectorError, ) as aio_exception: - _LOGGER.warning("Exception raised while attempting to login: %s", aio_exception) - raise ConfigEntryNotReady from aio_exception + raise ConfigEntryNotReady( + f"Error while attempting login: {aio_exception}" + ) from aio_exception + + try: + systems = await aqualink.get_systems() + except AqualinkServiceException as svc_exception: + raise ConfigEntryNotReady( + f"Error while attempting to retrieve systems list: {svc_exception}" + ) from svc_exception - systems = await aqualink.get_systems() systems = list(systems.values()) if not systems: _LOGGER.error("No systems detected or supported") return False # Only supporting the first system for now. - devices = await systems[0].get_devices() + try: + devices = await systems[0].get_devices() + except AqualinkServiceException as svc_exception: + raise ConfigEntryNotReady( + f"Error while attempting to retrieve devices list: {svc_exception}" + ) from svc_exception for dev in devices.values(): if isinstance(dev, AqualinkThermostat): @@ -151,15 +165,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def _async_systems_update(now): """Refresh internal state for all systems.""" - prev = systems[0].last_run_success + prev = systems[0].online - await systems[0].update() - success = systems[0].last_run_success - - if not success and prev: - _LOGGER.warning("Failed to refresh iAqualink state") - elif success and not prev: - _LOGGER.warning("Reconnected to iAqualink") + try: + await systems[0].update() + except AqualinkServiceException as svc_exception: + if prev is not None: + _LOGGER.warning("Failed to refresh iAqualink state: %s", svc_exception) + else: + cur = systems[0].online + if cur is True and prev is not True: + _LOGGER.warning("Reconnected to iAqualink") async_dispatcher_send(hass, DOMAIN) @@ -174,7 +190,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: platform for platform in PLATFORMS if platform in hass.data[DOMAIN] ] - hass.data[DOMAIN].clear() + del hass.data[DOMAIN] return await hass.config_entries.async_unload_platforms(entry, platforms_to_unload) @@ -228,12 +244,12 @@ class AqualinkEntity(Entity): @property def assumed_state(self) -> bool: """Return whether the state is based on actual reading from the device.""" - return not self.dev.system.last_run_success + return self.dev.system.online in [False, None] @property def available(self) -> bool: """Return whether the device is available or not.""" - return self.dev.system.online + return self.dev.system.online is True @property def device_info(self) -> DeviceInfo: diff --git a/homeassistant/components/iaqualink/climate.py b/homeassistant/components/iaqualink/climate.py index 13245429c0a..2986bea2dc7 100644 --- a/homeassistant/components/iaqualink/climate.py +++ b/homeassistant/components/iaqualink/climate.py @@ -3,13 +3,13 @@ from __future__ import annotations import logging -from iaqualink import AqualinkHeater, AqualinkPump, AqualinkSensor, AqualinkState from iaqualink.const import ( AQUALINK_TEMP_CELSIUS_HIGH, AQUALINK_TEMP_CELSIUS_LOW, AQUALINK_TEMP_FAHRENHEIT_HIGH, AQUALINK_TEMP_FAHRENHEIT_LOW, ) +from iaqualink.device import AqualinkHeater, AqualinkPump, AqualinkSensor, AqualinkState from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -24,6 +24,7 @@ from homeassistant.core import HomeAssistant from . import AqualinkEntity, refresh_system from .const import CLIMATE_SUPPORTED_MODES, DOMAIN as AQUALINK_DOMAIN +from .utils import await_or_reraise _LOGGER = logging.getLogger(__name__) @@ -76,9 +77,9 @@ class HassAqualinkThermostat(AqualinkEntity, ClimateEntity): async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Turn the underlying heater switch on or off.""" if hvac_mode == HVAC_MODE_HEAT: - await self.heater.turn_on() + await await_or_reraise(self.heater.turn_on()) elif hvac_mode == HVAC_MODE_OFF: - await self.heater.turn_off() + await await_or_reraise(self.heater.turn_off()) else: _LOGGER.warning("Unknown operation mode: %s", hvac_mode) @@ -111,7 +112,7 @@ class HassAqualinkThermostat(AqualinkEntity, ClimateEntity): @refresh_system async def async_set_temperature(self, **kwargs) -> None: """Set new target temperature.""" - await self.dev.set_temperature(int(kwargs[ATTR_TEMPERATURE])) + await await_or_reraise(self.dev.set_temperature(int(kwargs[ATTR_TEMPERATURE]))) @property def sensor(self) -> AqualinkSensor: diff --git a/homeassistant/components/iaqualink/config_flow.py b/homeassistant/components/iaqualink/config_flow.py index 5380a97901e..a91964ba3bc 100644 --- a/homeassistant/components/iaqualink/config_flow.py +++ b/homeassistant/components/iaqualink/config_flow.py @@ -3,7 +3,11 @@ from __future__ import annotations from typing import Any -from iaqualink import AqualinkClient, AqualinkLoginException +from iaqualink.client import AqualinkClient +from iaqualink.exception import ( + AqualinkServiceException, + AqualinkServiceUnauthorizedException, +) import voluptuous as vol from homeassistant import config_entries @@ -32,16 +36,22 @@ class AqualinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): password = user_input[CONF_PASSWORD] try: - aqualink = AqualinkClient(username, password) - await aqualink.login() - return self.async_create_entry(title=username, data=user_input) - except AqualinkLoginException: + async with AqualinkClient(username, password): + pass + except AqualinkServiceUnauthorizedException: + errors["base"] = "invalid_auth" + except AqualinkServiceException: errors["base"] = "cannot_connect" + else: + return self.async_create_entry(title=username, data=user_input) return self.async_show_form( step_id="user", data_schema=vol.Schema( - {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } ), errors=errors, ) diff --git a/homeassistant/components/iaqualink/light.py b/homeassistant/components/iaqualink/light.py index 17b686947ce..9cc5f5c2194 100644 --- a/homeassistant/components/iaqualink/light.py +++ b/homeassistant/components/iaqualink/light.py @@ -1,5 +1,5 @@ """Support for Aqualink pool lights.""" -from iaqualink import AqualinkLightEffect +import logging from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -14,6 +14,9 @@ from homeassistant.core import HomeAssistant from . import AqualinkEntity, refresh_system from .const import DOMAIN as AQUALINK_DOMAIN +from .utils import await_or_reraise + +_LOGGER = logging.getLogger(__name__) PARALLEL_UPDATES = 0 @@ -49,20 +52,19 @@ class HassAqualinkLight(AqualinkEntity, LightEntity): them. """ # For now I'm assuming lights support either effects or brightness. - if effect := kwargs.get(ATTR_EFFECT): - effect = AqualinkLightEffect[effect].value - await self.dev.set_effect(effect) + if effect_name := kwargs.get(ATTR_EFFECT): + await await_or_reraise(self.dev.set_effect_by_name(effect_name)) elif brightness := kwargs.get(ATTR_BRIGHTNESS): # Aqualink supports percentages in 25% increments. pct = int(round(brightness * 4.0 / 255)) * 25 - await self.dev.set_brightness(pct) + await await_or_reraise(self.dev.set_brightness(pct)) else: - await self.dev.turn_on() + await await_or_reraise(self.dev.turn_on()) @refresh_system async def async_turn_off(self, **kwargs) -> None: """Turn off the light.""" - await self.dev.turn_off() + await await_or_reraise(self.dev.turn_off()) @property def brightness(self) -> int: @@ -75,12 +77,12 @@ class HassAqualinkLight(AqualinkEntity, LightEntity): @property def effect(self) -> str: """Return the current light effect if supported.""" - return AqualinkLightEffect(self.dev.effect).name + return self.dev.effect @property def effect_list(self) -> list: """Return supported light effects.""" - return list(AqualinkLightEffect.__members__) + return list(self.dev.supported_light_effects) @property def supported_features(self) -> int: diff --git a/homeassistant/components/iaqualink/manifest.json b/homeassistant/components/iaqualink/manifest.json index 26c6e0b4bfd..8061163943d 100644 --- a/homeassistant/components/iaqualink/manifest.json +++ b/homeassistant/components/iaqualink/manifest.json @@ -4,6 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iaqualink/", "codeowners": ["@flz"], - "requirements": ["iaqualink==0.3.90"], + "requirements": ["iaqualink==0.4.1"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/iaqualink/strings.json b/homeassistant/components/iaqualink/strings.json index 5e7fcf6aa7a..85b49996f51 100644 --- a/homeassistant/components/iaqualink/strings.json +++ b/homeassistant/components/iaqualink/strings.json @@ -11,7 +11,8 @@ } }, "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" }, "abort": { "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" diff --git a/homeassistant/components/iaqualink/switch.py b/homeassistant/components/iaqualink/switch.py index a9fde150af3..fffd03add1a 100644 --- a/homeassistant/components/iaqualink/switch.py +++ b/homeassistant/components/iaqualink/switch.py @@ -5,6 +5,7 @@ from homeassistant.core import HomeAssistant from . import AqualinkEntity, refresh_system from .const import DOMAIN as AQUALINK_DOMAIN +from .utils import await_or_reraise PARALLEL_UPDATES = 0 @@ -47,9 +48,9 @@ class HassAqualinkSwitch(AqualinkEntity, SwitchEntity): @refresh_system async def async_turn_on(self, **kwargs) -> None: """Turn on the switch.""" - await self.dev.turn_on() + await await_or_reraise(self.dev.turn_on()) @refresh_system async def async_turn_off(self, **kwargs) -> None: """Turn off the switch.""" - await self.dev.turn_off() + await await_or_reraise(self.dev.turn_off()) diff --git a/homeassistant/components/iaqualink/utils.py b/homeassistant/components/iaqualink/utils.py new file mode 100644 index 00000000000..b047af5869c --- /dev/null +++ b/homeassistant/components/iaqualink/utils.py @@ -0,0 +1,16 @@ +"""Utility functions for Aqualink devices.""" +from __future__ import annotations + +from collections.abc import Awaitable + +from iaqualink.exception import AqualinkServiceException + +from homeassistant.exceptions import HomeAssistantError + + +async def await_or_reraise(awaitable: Awaitable) -> None: + """Execute API call while catching service exceptions.""" + try: + await awaitable + except AqualinkServiceException as svc_exception: + raise HomeAssistantError(f"Aqualink error: {svc_exception}") from svc_exception diff --git a/requirements_all.txt b/requirements_all.txt index c7f3650e4c6..49acf2268e8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -869,7 +869,7 @@ hyperion-py==0.7.4 iammeter==0.1.7 # homeassistant.components.iaqualink -iaqualink==0.3.90 +iaqualink==0.4.1 # homeassistant.components.watson_tts ibm-watson==5.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7abff39ae50..8fc576d3430 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -550,7 +550,7 @@ huisbaasje-client==0.1.0 hyperion-py==0.7.4 # homeassistant.components.iaqualink -iaqualink==0.3.90 +iaqualink==0.4.1 # homeassistant.components.ping icmplib==3.0 diff --git a/tests/components/iaqualink/conftest.py b/tests/components/iaqualink/conftest.py new file mode 100644 index 00000000000..01fc78691b3 --- /dev/null +++ b/tests/components/iaqualink/conftest.py @@ -0,0 +1,82 @@ +"""Configuration for iAqualink tests.""" +import random +from unittest.mock import AsyncMock + +from iaqualink.client import AqualinkClient +from iaqualink.device import AqualinkDevice +from iaqualink.system import AqualinkSystem +import pytest + +from homeassistant.components.iaqualink import DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from tests.common import MockConfigEntry + +MOCK_USERNAME = "test@example.com" +MOCK_PASSWORD = "password" +MOCK_DATA = {CONF_USERNAME: MOCK_USERNAME, CONF_PASSWORD: MOCK_PASSWORD} + + +def async_returns(x): + """Return value-returning async mock.""" + return AsyncMock(return_value=x) + + +def async_raises(x): + """Return exception-raising async mock.""" + return AsyncMock(side_effect=x) + + +@pytest.fixture(name="client") +def client_fixture(): + """Create client fixture.""" + return AqualinkClient(username=MOCK_USERNAME, password=MOCK_PASSWORD) + + +def get_aqualink_system(aqualink, cls=None, data=None): + """Create aqualink system.""" + if cls is None: + cls = AqualinkSystem + + if data is None: + data = {} + + num = random.randint(0, 99999) + data["serial_number"] = f"SN{num:05}" + + return cls(aqualink=aqualink, data=data) + + +def get_aqualink_device(system, cls=None, data=None): + """Create aqualink device.""" + if cls is None: + cls = AqualinkDevice + + if data is None: + data = {} + + num = random.randint(0, 999) + data["name"] = f"name_{num:03}" + + return cls(system=system, data=data) + + +@pytest.fixture(name="config_data") +def config_data_fixture(): + """Create hass config fixture.""" + return MOCK_DATA + + +@pytest.fixture(name="config") +def config_fixture(): + """Create hass config fixture.""" + return {DOMAIN: MOCK_DATA} + + +@pytest.fixture(name="config_entry") +def config_entry_fixture(): + """Create a mock HEOS config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data=MOCK_DATA, + ) diff --git a/tests/components/iaqualink/test_config_flow.py b/tests/components/iaqualink/test_config_flow.py index 38dd2ec1a3a..2d00284775d 100644 --- a/tests/components/iaqualink/test_config_flow.py +++ b/tests/components/iaqualink/test_config_flow.py @@ -1,20 +1,19 @@ """Tests for iAqualink config flow.""" from unittest.mock import patch -import iaqualink +from iaqualink.exception import ( + AqualinkServiceException, + AqualinkServiceUnauthorizedException, +) import pytest from homeassistant.components.iaqualink import config_flow -from tests.common import MockConfigEntry, mock_coro - -DATA = {"username": "test@example.com", "password": "pass"} - @pytest.mark.parametrize("step", ["import", "user"]) -async def test_already_configured(hass, step): +async def test_already_configured(hass, config_entry, config_data, step): """Test config flow when iaqualink component is already setup.""" - MockConfigEntry(domain="iaqualink", data=DATA).add_to_hass(hass) + config_entry.add_to_hass(hass) flow = config_flow.AqualinkFlowHandler() flow.hass = hass @@ -22,14 +21,14 @@ async def test_already_configured(hass, step): fname = f"async_step_{step}" func = getattr(flow, fname) - result = await func(DATA) + result = await func(config_data) assert result["type"] == "abort" @pytest.mark.parametrize("step", ["import", "user"]) async def test_without_config(hass, step): - """Test with no configuration.""" + """Test config flow with no configuration.""" flow = config_flow.AqualinkFlowHandler() flow.hass = hass flow.context = {} @@ -44,7 +43,7 @@ async def test_without_config(hass, step): @pytest.mark.parametrize("step", ["import", "user"]) -async def test_with_invalid_credentials(hass, step): +async def test_with_invalid_credentials(hass, config_data, step): """Test config flow with invalid username and/or password.""" flow = config_flow.AqualinkFlowHandler() flow.hass = hass @@ -52,9 +51,29 @@ async def test_with_invalid_credentials(hass, step): fname = f"async_step_{step}" func = getattr(flow, fname) with patch( - "iaqualink.AqualinkClient.login", side_effect=iaqualink.AqualinkLoginException + "homeassistant.components.iaqualink.config_flow.AqualinkClient.login", + side_effect=AqualinkServiceUnauthorizedException, ): - result = await func(DATA) + result = await func(config_data) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {"base": "invalid_auth"} + + +@pytest.mark.parametrize("step", ["import", "user"]) +async def test_service_exception(hass, config_data, step): + """Test config flow encountering service exception.""" + flow = config_flow.AqualinkFlowHandler() + flow.hass = hass + + fname = f"async_step_{step}" + func = getattr(flow, fname) + with patch( + "homeassistant.components.iaqualink.config_flow.AqualinkClient.login", + side_effect=AqualinkServiceException, + ): + result = await func(config_data) assert result["type"] == "form" assert result["step_id"] == "user" @@ -62,17 +81,20 @@ async def test_with_invalid_credentials(hass, step): @pytest.mark.parametrize("step", ["import", "user"]) -async def test_with_existing_config(hass, step): - """Test with existing configuration.""" +async def test_with_existing_config(hass, config_data, step): + """Test config flow with existing configuration.""" flow = config_flow.AqualinkFlowHandler() flow.hass = hass flow.context = {} fname = f"async_step_{step}" func = getattr(flow, fname) - with patch("iaqualink.AqualinkClient.login", return_value=mock_coro(None)): - result = await func(DATA) + with patch( + "homeassistant.components.iaqualink.config_flow.AqualinkClient.login", + return_value=None, + ): + result = await func(config_data) assert result["type"] == "create_entry" - assert result["title"] == DATA["username"] - assert result["data"] == DATA + assert result["title"] == config_data["username"] + assert result["data"] == config_data diff --git a/tests/components/iaqualink/test_init.py b/tests/components/iaqualink/test_init.py new file mode 100644 index 00000000000..ea7dad86908 --- /dev/null +++ b/tests/components/iaqualink/test_init.py @@ -0,0 +1,341 @@ +"""Tests for iAqualink integration.""" + +import asyncio +import logging +from unittest.mock import AsyncMock, patch + +from iaqualink.device import ( + AqualinkAuxToggle, + AqualinkBinarySensor, + AqualinkDevice, + AqualinkLightToggle, + AqualinkSensor, + AqualinkThermostat, +) +from iaqualink.exception import AqualinkServiceException + +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN +from homeassistant.components.iaqualink.const import UPDATE_INTERVAL +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ATTR_ASSUMED_STATE, STATE_ON, STATE_UNAVAILABLE +from homeassistant.util import dt as dt_util + +from tests.common import async_fire_time_changed +from tests.components.iaqualink.conftest import get_aqualink_device, get_aqualink_system + + +async def _ffwd_next_update_interval(hass): + now = dt_util.utcnow() + async_fire_time_changed(hass, now + UPDATE_INTERVAL) + await hass.async_block_till_done() + + +async def test_setup_login_exception(hass, config_entry): + """Test setup encountering a login exception.""" + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.iaqualink.AqualinkClient.login", + side_effect=AqualinkServiceException, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.SETUP_ERROR + + +async def test_setup_login_timeout(hass, config_entry): + """Test setup encountering a timeout while logging in.""" + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.iaqualink.AqualinkClient.login", + side_effect=asyncio.TimeoutError, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_setup_systems_exception(hass, config_entry): + """Test setup encountering an exception while retrieving systems.""" + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.iaqualink.AqualinkClient.login", return_value=None + ), patch( + "homeassistant.components.iaqualink.AqualinkClient.get_systems", + side_effect=AqualinkServiceException, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_setup_no_systems_recognized(hass, config_entry): + """Test setup ending in no systems recognized.""" + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.iaqualink.AqualinkClient.login", return_value=None + ), patch( + "homeassistant.components.iaqualink.AqualinkClient.get_systems", + return_value={}, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.SETUP_ERROR + + +async def test_setup_devices_exception(hass, config_entry, client): + """Test setup encountering an exception while retrieving devices.""" + config_entry.add_to_hass(hass) + + system = get_aqualink_system(client) + systems = {system.serial: system} + + with patch( + "homeassistant.components.iaqualink.AqualinkClient.login", return_value=None + ), patch( + "homeassistant.components.iaqualink.AqualinkClient.get_systems", + return_value=systems, + ), patch.object( + system, "get_devices" + ) as mock_get_devices: + mock_get_devices.side_effect = AqualinkServiceException + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_setup_all_good_no_recognized_devices(hass, config_entry, client): + """Test setup ending in no devices recognized.""" + config_entry.add_to_hass(hass) + + system = get_aqualink_system(client) + systems = {system.serial: system} + + device = get_aqualink_device(system, AqualinkDevice) + devices = {device.name: device} + + with patch( + "homeassistant.components.iaqualink.AqualinkClient.login", return_value=None + ), patch( + "homeassistant.components.iaqualink.AqualinkClient.get_systems", + return_value=systems, + ), patch.object( + system, "get_devices" + ) as mock_get_devices: + mock_get_devices.return_value = devices + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + assert len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 0 + assert len(hass.states.async_entity_ids(CLIMATE_DOMAIN)) == 0 + assert len(hass.states.async_entity_ids(LIGHT_DOMAIN)) == 0 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 0 + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 + + assert await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.NOT_LOADED + + +async def test_setup_all_good_all_device_types(hass, config_entry, client): + """Test setup ending in one device of each type recognized.""" + config_entry.add_to_hass(hass) + + system = get_aqualink_system(client) + systems = {system.serial: system} + + devices = [ + get_aqualink_device(system, AqualinkAuxToggle), + get_aqualink_device(system, AqualinkBinarySensor), + get_aqualink_device(system, AqualinkLightToggle), + get_aqualink_device(system, AqualinkSensor), + get_aqualink_device(system, AqualinkThermostat), + ] + devices = {d.name: d for d in devices} + + system.get_devices = AsyncMock(return_value=devices) + + with patch( + "homeassistant.components.iaqualink.AqualinkClient.login", return_value=None + ), patch( + "homeassistant.components.iaqualink.AqualinkClient.get_systems", + return_value=systems, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + assert len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 1 + assert len(hass.states.async_entity_ids(CLIMATE_DOMAIN)) == 1 + assert len(hass.states.async_entity_ids(LIGHT_DOMAIN)) == 1 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 1 + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 + + assert await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.NOT_LOADED + + +async def test_multiple_updates(hass, config_entry, caplog, client): + """Test all possible results of online status transition after update.""" + config_entry.add_to_hass(hass) + + system = get_aqualink_system(client) + systems = {system.serial: system} + + system.get_devices = AsyncMock(return_value={}) + + caplog.set_level(logging.WARNING) + + with patch( + "homeassistant.components.iaqualink.AqualinkClient.login", return_value=None + ), patch( + "homeassistant.components.iaqualink.AqualinkClient.get_systems", + return_value=systems, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + def set_online_to_true(): + system.online = True + + def set_online_to_false(): + system.online = False + + system.update = AsyncMock() + + # True -> True + system.online = True + caplog.clear() + system.update.side_effect = set_online_to_true + await _ffwd_next_update_interval(hass) + assert len(caplog.records) == 0 + + # True -> False + system.online = True + caplog.clear() + system.update.side_effect = set_online_to_false + await _ffwd_next_update_interval(hass) + assert len(caplog.records) == 0 + + # True -> None / ServiceException + system.online = True + caplog.clear() + system.update.side_effect = AqualinkServiceException + await _ffwd_next_update_interval(hass) + assert len(caplog.records) == 1 + assert "Failed" in caplog.text + + # False -> False + system.online = False + caplog.clear() + system.update.side_effect = set_online_to_false + await _ffwd_next_update_interval(hass) + assert len(caplog.records) == 0 + + # False -> True + system.online = False + caplog.clear() + system.update.side_effect = set_online_to_true + await _ffwd_next_update_interval(hass) + assert len(caplog.records) == 1 + assert "Reconnected" in caplog.text + + # False -> None / ServiceException + system.online = False + caplog.clear() + system.update.side_effect = AqualinkServiceException + await _ffwd_next_update_interval(hass) + assert len(caplog.records) == 1 + assert "Failed" in caplog.text + + # None -> None / ServiceException + system.online = None + caplog.clear() + system.update.side_effect = AqualinkServiceException + await _ffwd_next_update_interval(hass) + assert len(caplog.records) == 0 + + # None -> True + system.online = None + caplog.clear() + system.update.side_effect = set_online_to_true + await _ffwd_next_update_interval(hass) + assert len(caplog.records) == 1 + assert "Reconnected" in caplog.text + + # None -> False + system.online = None + caplog.clear() + system.update.side_effect = set_online_to_false + await _ffwd_next_update_interval(hass) + assert len(caplog.records) == 0 + + assert await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.NOT_LOADED + + +async def test_entity_assumed_and_available(hass, config_entry, client): + """Test assumed_state and_available properties for all values of online.""" + config_entry.add_to_hass(hass) + + system = get_aqualink_system(client) + systems = {system.serial: system} + + light = get_aqualink_device(system, AqualinkLightToggle, data={"state": "1"}) + devices = {d.name: d for d in [light]} + system.get_devices = AsyncMock(return_value=devices) + system.update = AsyncMock() + + with patch( + "homeassistant.components.iaqualink.AqualinkClient.login", return_value=None + ), patch( + "homeassistant.components.iaqualink.AqualinkClient.get_systems", + return_value=systems, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids(LIGHT_DOMAIN)) == 1 + + name = f"{LIGHT_DOMAIN}.{light.name}" + + # None means maybe. + light.system.online = None + await _ffwd_next_update_interval(hass) + state = hass.states.get(name) + assert state.state == STATE_UNAVAILABLE + assert state.attributes.get(ATTR_ASSUMED_STATE) is True + + light.system.online = False + await _ffwd_next_update_interval(hass) + state = hass.states.get(name) + assert state.state == STATE_UNAVAILABLE + assert state.attributes.get(ATTR_ASSUMED_STATE) is True + + light.system.online = True + await _ffwd_next_update_interval(hass) + state = hass.states.get(name) + assert state.state == STATE_ON + assert state.attributes.get(ATTR_ASSUMED_STATE) is None diff --git a/tests/components/iaqualink/test_utils.py b/tests/components/iaqualink/test_utils.py new file mode 100644 index 00000000000..56b239c0d9f --- /dev/null +++ b/tests/components/iaqualink/test_utils.py @@ -0,0 +1,23 @@ +"""Tests for iAqualink integration utility functions.""" + +from iaqualink.exception import AqualinkServiceException +import pytest + +from homeassistant.components.iaqualink.utils import await_or_reraise +from homeassistant.exceptions import HomeAssistantError + +from tests.components.iaqualink.conftest import async_raises, async_returns + + +async def test_await_or_reraise(hass): + """Test await_or_reraise for all values of awaitable.""" + async_noop = async_returns(None) + await await_or_reraise(async_noop()) + + with pytest.raises(Exception): + async_ex = async_raises(Exception) + await await_or_reraise(async_ex()) + + with pytest.raises(HomeAssistantError): + async_ex = async_raises(AqualinkServiceException) + await await_or_reraise(async_ex()) From 6b0f2aa13fd49ccbc08fdbedc0935981fc697d63 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 27 Dec 2021 21:37:21 +0100 Subject: [PATCH 1064/2644] Update frontend to 20211227.0 (#62874) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 6ae1709e418..a2c7e49f6e6 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20211220.0" + "home-assistant-frontend==20211227.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index cb76eb8ada2..2d10d557361 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ ciso8601==2.2.0 cryptography==35.0.0 emoji==1.5.0 hass-nabucasa==0.50.0 -home-assistant-frontend==20211220.0 +home-assistant-frontend==20211227.0 httpx==0.21.0 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index 49acf2268e8..e03bf49c808 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -827,7 +827,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211220.0 +home-assistant-frontend==20211227.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8fc576d3430..be9aa5f7e26 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -522,7 +522,7 @@ hole==0.7.0 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211220.0 +home-assistant-frontend==20211227.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 1cfeb404b6d699b0aef32cf8204635ee98aaef3d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 27 Dec 2021 22:05:10 +0100 Subject: [PATCH 1065/2644] Add configuration flow to PVOutput (#62667) * Add configuration flow to PVOutput * Update homeassistant/components/pvoutput/sensor.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/pvoutput/sensor.py Co-authored-by: Martin Hjelmare * Use account URL placeholder Co-authored-by: Martin Hjelmare --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/pvoutput/__init__.py | 43 ++++- .../components/pvoutput/config_flow.py | 92 +++++++++ homeassistant/components/pvoutput/const.py | 25 +++ .../components/pvoutput/manifest.json | 1 + homeassistant/components/pvoutput/sensor.py | 95 +++++----- .../components/pvoutput/strings.json | 17 ++ .../components/pvoutput/translations/en.json | 17 ++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + tests/components/pvoutput/__init__.py | 1 + tests/components/pvoutput/conftest.py | 41 ++++ tests/components/pvoutput/test_config_flow.py | 178 ++++++++++++++++++ 14 files changed, 471 insertions(+), 45 deletions(-) create mode 100644 homeassistant/components/pvoutput/config_flow.py create mode 100644 homeassistant/components/pvoutput/const.py create mode 100644 homeassistant/components/pvoutput/strings.json create mode 100644 homeassistant/components/pvoutput/translations/en.json create mode 100644 tests/components/pvoutput/__init__.py create mode 100644 tests/components/pvoutput/conftest.py create mode 100644 tests/components/pvoutput/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index f409745475f..513f2eff055 100644 --- a/.coveragerc +++ b/.coveragerc @@ -863,6 +863,7 @@ omit = homeassistant/components/pushbullet/sensor.py homeassistant/components/pushover/notify.py homeassistant/components/pushsafer/notify.py + homeassistant/components/pvoutput/__init__.py homeassistant/components/pvoutput/sensor.py homeassistant/components/pyload/sensor.py homeassistant/components/qbittorrent/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index bbcfadaf3bf..5176344d743 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -720,6 +720,7 @@ tests/components/ps4/* @ktnrg45 homeassistant/components/push/* @dgomes tests/components/push/* @dgomes homeassistant/components/pvoutput/* @fabaff @frenck +tests/components/pvoutput/* @fabaff @frenck homeassistant/components/pvpc_hourly_pricing/* @azogue tests/components/pvpc_hourly_pricing/* @azogue homeassistant/components/qbittorrent/* @geoffreylagaisse diff --git a/homeassistant/components/pvoutput/__init__.py b/homeassistant/components/pvoutput/__init__.py index 0ea3aabe9eb..aaac29ef50e 100644 --- a/homeassistant/components/pvoutput/__init__.py +++ b/homeassistant/components/pvoutput/__init__.py @@ -1 +1,42 @@ -"""The pvoutput component.""" +"""The PVOutput integration.""" +from __future__ import annotations + +from pvo import PVOutput, Status + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import CONF_SYSTEM_ID, DOMAIN, LOGGER, PLATFORMS, SCAN_INTERVAL + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up PVOutput from a config entry.""" + pvoutput = PVOutput( + api_key=entry.data[CONF_API_KEY], + system_id=entry.data[CONF_SYSTEM_ID], + session=async_get_clientsession(hass), + ) + + coordinator: DataUpdateCoordinator[Status] = DataUpdateCoordinator( + hass, + LOGGER, + name=f"{DOMAIN}_{entry.data[CONF_SYSTEM_ID]}", + update_interval=SCAN_INTERVAL, + update_method=pvoutput.status, + ) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload PVOutput config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + del hass.data[DOMAIN][entry.entry_id] + return unload_ok diff --git a/homeassistant/components/pvoutput/config_flow.py b/homeassistant/components/pvoutput/config_flow.py new file mode 100644 index 00000000000..0de24e9ae0e --- /dev/null +++ b/homeassistant/components/pvoutput/config_flow.py @@ -0,0 +1,92 @@ +"""Config flow to configure the PVOutput integration.""" +from __future__ import annotations + +from typing import Any + +from pvo import PVOutput, PVOutputAuthenticationError, PVOutputError +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_API_KEY, CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import CONF_SYSTEM_ID, DOMAIN + + +async def validate_input(hass: HomeAssistant, *, api_key: str, system_id: int) -> None: + """Try using the give system id & api key against the PVOutput API.""" + session = async_get_clientsession(hass) + pvoutput = PVOutput( + session=session, + api_key=api_key, + system_id=system_id, + ) + await pvoutput.status() + + +class TailscaleFlowHandler(ConfigFlow, domain=DOMAIN): + """Config flow for PVOutput.""" + + VERSION = 1 + + imported_name: str | None = None + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + errors = {} + + if user_input is not None: + try: + await validate_input( + self.hass, + api_key=user_input[CONF_API_KEY], + system_id=user_input[CONF_SYSTEM_ID], + ) + except PVOutputAuthenticationError: + errors["base"] = "invalid_auth" + except PVOutputError: + errors["base"] = "cannot_connect" + else: + await self.async_set_unique_id(str(user_input[CONF_SYSTEM_ID])) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=self.imported_name or str(user_input[CONF_SYSTEM_ID]), + data={ + CONF_SYSTEM_ID: user_input[CONF_SYSTEM_ID], + CONF_API_KEY: user_input[CONF_API_KEY], + }, + ) + else: + user_input = {} + + return self.async_show_form( + step_id="user", + description_placeholders={ + "account_url": "https://pvoutput.org/account.jsp" + }, + data_schema=vol.Schema( + { + vol.Required( + CONF_API_KEY, default=user_input.get(CONF_API_KEY, "") + ): str, + vol.Required( + CONF_SYSTEM_ID, default=user_input.get(CONF_SYSTEM_ID, "") + ): int, + } + ), + errors=errors, + ) + + async def async_step_import(self, config: dict[str, Any]) -> FlowResult: + """Handle a flow initialized by importing a config.""" + self.imported_name = config[CONF_NAME] + return await self.async_step_user( + user_input={ + CONF_SYSTEM_ID: config[CONF_SYSTEM_ID], + CONF_API_KEY: config[CONF_API_KEY], + } + ) diff --git a/homeassistant/components/pvoutput/const.py b/homeassistant/components/pvoutput/const.py new file mode 100644 index 00000000000..dd2aca530ed --- /dev/null +++ b/homeassistant/components/pvoutput/const.py @@ -0,0 +1,25 @@ +"""Constants for the PVOutput integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging +from typing import Final + +from homeassistant.const import Platform + +DOMAIN: Final = "pvoutput" +PLATFORMS = [Platform.SENSOR] + +LOGGER = logging.getLogger(__package__) +SCAN_INTERVAL = timedelta(minutes=2) + + +ATTR_ENERGY_GENERATION = "energy_generation" +ATTR_POWER_GENERATION = "power_generation" +ATTR_ENERGY_CONSUMPTION = "energy_consumption" +ATTR_POWER_CONSUMPTION = "power_consumption" +ATTR_EFFICIENCY = "efficiency" + +CONF_SYSTEM_ID = "system_id" + +DEFAULT_NAME = "PVOutput" diff --git a/homeassistant/components/pvoutput/manifest.json b/homeassistant/components/pvoutput/manifest.json index d349aa33849..0afdbdeac28 100644 --- a/homeassistant/components/pvoutput/manifest.json +++ b/homeassistant/components/pvoutput/manifest.json @@ -2,6 +2,7 @@ "domain": "pvoutput", "name": "PVOutput", "documentation": "https://www.home-assistant.io/integrations/pvoutput", + "config_flow": true, "codeowners": ["@fabaff", "@frenck"], "requirements": ["pvo==0.1.0"], "iot_class": "cloud_polling" diff --git a/homeassistant/components/pvoutput/sensor.py b/homeassistant/components/pvoutput/sensor.py index 8341e45579d..a108f4339e2 100644 --- a/homeassistant/components/pvoutput/sensor.py +++ b/homeassistant/components/pvoutput/sensor.py @@ -1,10 +1,7 @@ """Support for getting collected information from PVOutput.""" from __future__ import annotations -from datetime import timedelta -import logging - -from pvo import PVOutput, PVOutputError, Status +from pvo import Status import voluptuous as vol from homeassistant.components.sensor import ( @@ -13,6 +10,7 @@ from homeassistant.components.sensor import ( SensorEntity, SensorStateClass, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( ATTR_TEMPERATURE, ATTR_VOLTAGE, @@ -24,20 +22,22 @@ from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) -_LOGGER = logging.getLogger(__name__) - -ATTR_ENERGY_GENERATION = "energy_generation" -ATTR_POWER_GENERATION = "power_generation" -ATTR_ENERGY_CONSUMPTION = "energy_consumption" -ATTR_POWER_CONSUMPTION = "power_consumption" -ATTR_EFFICIENCY = "efficiency" - -CONF_SYSTEM_ID = "system_id" - -DEFAULT_NAME = "PVOutput" - -SCAN_INTERVAL = timedelta(minutes=2) +from .const import ( + ATTR_EFFICIENCY, + ATTR_ENERGY_CONSUMPTION, + ATTR_ENERGY_GENERATION, + ATTR_POWER_CONSUMPTION, + ATTR_POWER_GENERATION, + CONF_SYSTEM_ID, + DEFAULT_NAME, + DOMAIN, + LOGGER, +) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -55,51 +55,58 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the PVOutput sensor.""" - pvoutput = PVOutput( - api_key=config[CONF_API_KEY], - system_id=config[CONF_SYSTEM_ID], + LOGGER.warning( + "Configuration of the PVOutput platform in YAML is deprecated and will be " + "removed in Home Assistant 2022.4; Your existing configuration " + "has been imported into the UI automatically and can be safely removed " + "from your configuration.yaml file" + ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_SYSTEM_ID: config[CONF_SYSTEM_ID], + CONF_API_KEY: config[CONF_API_KEY], + CONF_NAME: config[CONF_NAME], + }, + ) ) - try: - status = await pvoutput.status() - except PVOutputError: - _LOGGER.error("Unable to fetch data from PVOutput") - return - async_add_entities([PvoutputSensor(pvoutput, status, config[CONF_NAME])]) +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up a Tailscale binary sensors based on a config entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities([PvoutputSensor(coordinator)]) -class PvoutputSensor(SensorEntity): +class PvoutputSensor(CoordinatorEntity, SensorEntity): """Representation of a PVOutput sensor.""" _attr_state_class = SensorStateClass.TOTAL_INCREASING _attr_device_class = SensorDeviceClass.ENERGY _attr_native_unit_of_measurement = ENERGY_WATT_HOUR - def __init__(self, pvoutput: PVOutput, status: Status, name: str) -> None: - """Initialize a PVOutput sensor.""" - self._attr_name = name - self.pvoutput = pvoutput - self.status = status + coordinator: DataUpdateCoordinator[Status] @property def native_value(self) -> int | None: """Return the state of the device.""" - return self.status.energy_generation + return self.coordinator.data.energy_generation @property def extra_state_attributes(self) -> dict[str, int | float | None]: """Return the state attributes of the monitored installation.""" return { - ATTR_ENERGY_GENERATION: self.status.energy_generation, - ATTR_POWER_GENERATION: self.status.power_generation, - ATTR_ENERGY_CONSUMPTION: self.status.energy_consumption, - ATTR_POWER_CONSUMPTION: self.status.power_consumption, - ATTR_EFFICIENCY: self.status.normalized_ouput, - ATTR_TEMPERATURE: self.status.temperature, - ATTR_VOLTAGE: self.status.voltage, + ATTR_ENERGY_GENERATION: self.coordinator.data.energy_generation, + ATTR_POWER_GENERATION: self.coordinator.data.power_generation, + ATTR_ENERGY_CONSUMPTION: self.coordinator.data.energy_consumption, + ATTR_POWER_CONSUMPTION: self.coordinator.data.power_consumption, + ATTR_EFFICIENCY: self.coordinator.data.normalized_ouput, + ATTR_TEMPERATURE: self.coordinator.data.temperature, + ATTR_VOLTAGE: self.coordinator.data.voltage, } - - async def async_update(self) -> None: - """Get the latest data from the PVOutput API and updates the state.""" - self.status = await self.pvoutput.status() diff --git a/homeassistant/components/pvoutput/strings.json b/homeassistant/components/pvoutput/strings.json new file mode 100644 index 00000000000..0093181558a --- /dev/null +++ b/homeassistant/components/pvoutput/strings.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "user": { + "description": "To authenticate with PVOutput you'll need to get the API key at {account_url}.\n\nThe system IDs of registered systems are listed on that same page.", + "data": { + "system_id": "System ID", + "api_key": "[%key:common::config_flow::data::api_key%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + } + } +} diff --git a/homeassistant/components/pvoutput/translations/en.json b/homeassistant/components/pvoutput/translations/en.json new file mode 100644 index 00000000000..6b34d9d2b0d --- /dev/null +++ b/homeassistant/components/pvoutput/translations/en.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "system_id": "System ID" + }, + "description": "To authenticate with PVOutput you'll need to get the API key at {account_url}.\n\nThe system IDs of registered systems are listed on that same page." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index b2888d7d8b4..b68b00bd264 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -244,6 +244,7 @@ FLOWS = [ "progettihwsw", "prosegur", "ps4", + "pvoutput", "pvpc_hourly_pricing", "rachio", "rainforest_eagle", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index be9aa5f7e26..0b2eda93c3a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -789,6 +789,9 @@ pure-python-adb[async]==0.3.0.dev0 # homeassistant.components.pushbullet pushbullet.py==0.11.0 +# homeassistant.components.pvoutput +pvo==0.1.0 + # homeassistant.components.canary py-canary==0.5.1 diff --git a/tests/components/pvoutput/__init__.py b/tests/components/pvoutput/__init__.py new file mode 100644 index 00000000000..7b55b5c0471 --- /dev/null +++ b/tests/components/pvoutput/__init__.py @@ -0,0 +1 @@ +"""Tests for the PVOutput integration.""" diff --git a/tests/components/pvoutput/conftest.py b/tests/components/pvoutput/conftest.py new file mode 100644 index 00000000000..ad3e215570b --- /dev/null +++ b/tests/components/pvoutput/conftest.py @@ -0,0 +1,41 @@ +"""Fixtures for PVOutput integration tests.""" +from __future__ import annotations + +from collections.abc import Generator +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from homeassistant.components.pvoutput.const import CONF_SYSTEM_ID, DOMAIN +from homeassistant.const import CONF_API_KEY + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="12345", + domain=DOMAIN, + data={CONF_API_KEY: "tskey-MOCK", CONF_SYSTEM_ID: 12345}, + unique_id="12345", + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Mock setting up a config entry.""" + with patch( + "homeassistant.components.pvoutput.async_setup_entry", return_value=True + ) as mock_setup: + yield mock_setup + + +@pytest.fixture +def mock_pvoutput_config_flow() -> Generator[None, MagicMock, None]: + """Return a mocked PVOutput client.""" + with patch( + "homeassistant.components.pvoutput.config_flow.PVOutput", autospec=True + ) as pvoutput_mock: + yield pvoutput_mock.return_value diff --git a/tests/components/pvoutput/test_config_flow.py b/tests/components/pvoutput/test_config_flow.py new file mode 100644 index 00000000000..a4a7392bb91 --- /dev/null +++ b/tests/components/pvoutput/test_config_flow.py @@ -0,0 +1,178 @@ +"""Tests for the PVOutput config flow.""" + +from unittest.mock import AsyncMock, MagicMock + +from pvo import PVOutputAuthenticationError, PVOutputConnectionError + +from homeassistant.components.pvoutput.const import CONF_SYSTEM_ID, DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import CONF_API_KEY, CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from tests.common import MockConfigEntry + + +async def test_full_user_flow( + hass: HomeAssistant, + mock_pvoutput_config_flow: MagicMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test the full user configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == SOURCE_USER + assert "flow_id" in result + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_SYSTEM_ID: 12345, + CONF_API_KEY: "tadaaa", + }, + ) + + assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("title") == "12345" + assert result2.get("data") == { + CONF_SYSTEM_ID: 12345, + CONF_API_KEY: "tadaaa", + } + + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_pvoutput_config_flow.status.mock_calls) == 1 + + +async def test_full_flow_with_authentication_error( + hass: HomeAssistant, + mock_pvoutput_config_flow: MagicMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test the full user configuration flow with incorrect API key. + + This tests tests a full config flow, with a case the user enters an invalid + PVOutput API key, but recovers by entering the correct one. + """ + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == SOURCE_USER + assert "flow_id" in result + + mock_pvoutput_config_flow.status.side_effect = PVOutputAuthenticationError + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_SYSTEM_ID: 12345, + CONF_API_KEY: "invalid", + }, + ) + + assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("step_id") == SOURCE_USER + assert result2.get("errors") == {"base": "invalid_auth"} + assert "flow_id" in result2 + + assert len(mock_setup_entry.mock_calls) == 0 + assert len(mock_pvoutput_config_flow.status.mock_calls) == 1 + + mock_pvoutput_config_flow.status.side_effect = None + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + user_input={ + CONF_SYSTEM_ID: 12345, + CONF_API_KEY: "tadaaa", + }, + ) + + assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("title") == "12345" + assert result3.get("data") == { + CONF_SYSTEM_ID: 12345, + CONF_API_KEY: "tadaaa", + } + + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_pvoutput_config_flow.status.mock_calls) == 2 + + +async def test_connection_error( + hass: HomeAssistant, mock_pvoutput_config_flow: MagicMock +) -> None: + """Test API connection error.""" + mock_pvoutput_config_flow.status.side_effect = PVOutputConnectionError + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={ + CONF_SYSTEM_ID: 12345, + CONF_API_KEY: "tadaaa", + }, + ) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("errors") == {"base": "cannot_connect"} + + assert len(mock_pvoutput_config_flow.status.mock_calls) == 1 + + +async def test_already_configured( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_pvoutput_config_flow: MagicMock, +) -> None: + """Test we abort if the PVOutput system is already configured.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert "flow_id" in result + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_SYSTEM_ID: 12345, + CONF_API_KEY: "tadaaa", + }, + ) + + assert result2.get("type") == RESULT_TYPE_ABORT + assert result2.get("reason") == "already_configured" + + +async def test_import_flow( + hass: HomeAssistant, + mock_pvoutput_config_flow: MagicMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test the import configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_SYSTEM_ID: 1337, + CONF_API_KEY: "tadaaa", + CONF_NAME: "Test", + }, + ) + + assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("title") == "Test" + assert result.get("data") == { + CONF_SYSTEM_ID: 1337, + CONF_API_KEY: "tadaaa", + } + + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_pvoutput_config_flow.status.mock_calls) == 1 From b9ce82f79c81b014fb050a3e04d85b4902862e65 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Mon, 27 Dec 2021 13:28:18 -0800 Subject: [PATCH 1066/2644] Add unique id to DHCP step in Config Flow for Overkiz (#62847) --- homeassistant/components/overkiz/config_flow.py | 15 ++------------- tests/components/overkiz/test_config_flow.py | 12 +++--------- 2 files changed, 5 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/overkiz/config_flow.py b/homeassistant/components/overkiz/config_flow.py index 673a946d35f..c4748b82751 100644 --- a/homeassistant/components/overkiz/config_flow.py +++ b/homeassistant/components/overkiz/config_flow.py @@ -19,7 +19,6 @@ from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers import device_registry as dr from .const import CONF_HUB, DEFAULT_HUB, DOMAIN @@ -94,17 +93,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.debug("DHCP discovery detected gateway %s", obfuscate_id(gateway_id)) - if self._gateway_already_configured(gateway_id): - _LOGGER.debug("Gateway %s is already configured", obfuscate_id(gateway_id)) - return self.async_abort(reason="already_configured") + await self.async_set_unique_id(gateway_id) + self._abort_if_unique_id_configured() return await self.async_step_user() - - def _gateway_already_configured(self, gateway_id: str) -> bool: - """See if we already have a gateway matching the id.""" - device_registry = dr.async_get(self.hass) - return bool( - device_registry.async_get_device( - identifiers={(DOMAIN, gateway_id)}, connections=set() - ) - ) diff --git a/tests/components/overkiz/test_config_flow.py b/tests/components/overkiz/test_config_flow.py index c9c745c9665..9de94eab36c 100644 --- a/tests/components/overkiz/test_config_flow.py +++ b/tests/components/overkiz/test_config_flow.py @@ -16,7 +16,7 @@ from homeassistant.components import dhcp from homeassistant.components.overkiz.const import DOMAIN from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry, mock_device_registry +from tests.common import MockConfigEntry TEST_EMAIL = "test@testdomain.com" TEST_EMAIL2 = "test@testdomain.nl" @@ -120,7 +120,7 @@ async def test_allow_multiple_unique_entries(hass: HomeAssistant) -> None: """Test config flow allows Config Flow unique entries.""" MockConfigEntry( domain=DOMAIN, - unique_id="test2@testdomain.com", + unique_id=TEST_GATEWAY_ID2, data={"username": "test2@testdomain.com", "password": TEST_PASSWORD}, ).add_to_hass(hass) @@ -166,17 +166,11 @@ async def test_dhcp_flow_already_configured(hass: HomeAssistant) -> None: """Test that DHCP doesn't setup already configured gateways.""" config_entry = MockConfigEntry( domain=DOMAIN, - unique_id=TEST_EMAIL, + unique_id=TEST_GATEWAY_ID, data={"username": TEST_EMAIL, "password": TEST_PASSWORD, "hub": TEST_HUB}, ) config_entry.add_to_hass(hass) - device_registry = mock_device_registry(hass) - device_registry.async_get_or_create( - config_entry_id=config_entry.entry_id, - identifiers={(DOMAIN, "1234-5678-9123")}, - ) - result = await hass.config_entries.flow.async_init( DOMAIN, data=dhcp.DhcpServiceInfo( From af3d52a3e097e8559a2eec9209e5f9e731038f40 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 27 Dec 2021 22:51:31 +0100 Subject: [PATCH 1067/2644] Add myself as codeowner for Luftdaten (#62888) --- CODEOWNERS | 4 ++-- homeassistant/components/luftdaten/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 5176344d743..008faf3d3d1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -508,8 +508,8 @@ tests/components/lookin/* @ANMalko @bdraco homeassistant/components/lovelace/* @home-assistant/frontend tests/components/lovelace/* @home-assistant/frontend homeassistant/components/luci/* @mzdrale -homeassistant/components/luftdaten/* @fabaff -tests/components/luftdaten/* @fabaff +homeassistant/components/luftdaten/* @fabaff @frenck +tests/components/luftdaten/* @fabaff @frenck homeassistant/components/lupusec/* @majuss homeassistant/components/lutron/* @JonGilmore homeassistant/components/lutron_caseta/* @swails @bdraco diff --git a/homeassistant/components/luftdaten/manifest.json b/homeassistant/components/luftdaten/manifest.json index fd355bd8d3c..1a745e1060a 100644 --- a/homeassistant/components/luftdaten/manifest.json +++ b/homeassistant/components/luftdaten/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/luftdaten", "requirements": ["luftdaten==0.7.1"], - "codeowners": ["@fabaff"], + "codeowners": ["@fabaff", "@frenck"], "quality_scale": "gold", "iot_class": "cloud_polling" } From 40aa852a578dcd54cc581921857590a11b7b497b Mon Sep 17 00:00:00 2001 From: Thomas Schamm Date: Mon, 27 Dec 2021 22:54:23 +0100 Subject: [PATCH 1068/2644] Fix missing power and energy sensors for light switches in bosch_shc (#62802) --- homeassistant/components/bosch_shc/sensor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bosch_shc/sensor.py b/homeassistant/components/bosch_shc/sensor.py index 33d18ebbfd4..d62ab075167 100644 --- a/homeassistant/components/bosch_shc/sensor.py +++ b/homeassistant/components/bosch_shc/sensor.py @@ -103,7 +103,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) ) - for sensor in session.device_helper.smart_plugs: + for sensor in ( + session.device_helper.smart_plugs + session.device_helper.light_switches + ): entities.append( PowerSensor( device=sensor, From 16e9ea6ac722c7bf17defe01986c6819faf75635 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 27 Dec 2021 22:58:31 +0100 Subject: [PATCH 1069/2644] Slightly improve Open-Meteo configuration flow (#62869) --- .../components/open_meteo/config_flow.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/open_meteo/config_flow.py b/homeassistant/components/open_meteo/config_flow.py index 76d5436f565..2a7b292c363 100644 --- a/homeassistant/components/open_meteo/config_flow.py +++ b/homeassistant/components/open_meteo/config_flow.py @@ -22,21 +22,20 @@ class OpenMeteoFlowHandler(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle a flow initialized by the user.""" - if user_input is not None: - await self.async_set_unique_id(user_input[CONF_ZONE]) - self._abort_if_unique_id_configured() - - zone = self.hass.states.get(user_input[CONF_ZONE]) - return self.async_create_entry( - title=zone.name if zone else "Open-Meteo", - data={CONF_ZONE: user_input[CONF_ZONE]}, - ) - zones: dict[str, str] = { entity_id: state.name for entity_id in self.hass.states.async_entity_ids(ZONE_DOMAIN) if (state := self.hass.states.get(entity_id)) is not None } + + if user_input is not None: + await self.async_set_unique_id(user_input[CONF_ZONE]) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=zones[user_input[CONF_ZONE]], + data={CONF_ZONE: user_input[CONF_ZONE]}, + ) + zones = dict(sorted(zones.items(), key=lambda x: x[1], reverse=True)) return self.async_show_form( From 53fdcf1b6ad045609c87772a35ebdc02789f4ef7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 27 Dec 2021 23:01:00 +0100 Subject: [PATCH 1070/2644] Add basic support for EntityDescription in PVOutput (#62887) --- homeassistant/components/pvoutput/sensor.py | 68 ++++++++++++++++++--- 1 file changed, 58 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/pvoutput/sensor.py b/homeassistant/components/pvoutput/sensor.py index a108f4339e2..1cae6e53e67 100644 --- a/homeassistant/components/pvoutput/sensor.py +++ b/homeassistant/components/pvoutput/sensor.py @@ -1,6 +1,9 @@ """Support for getting collected information from PVOutput.""" from __future__ import annotations +from collections.abc import Callable +from dataclasses import dataclass + from pvo import Status import voluptuous as vol @@ -8,6 +11,7 @@ from homeassistant.components.sensor import ( PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, + SensorEntityDescription, SensorStateClass, ) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -48,6 +52,32 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) +@dataclass +class PVOutputSensorEntityDescriptionMixin: + """Mixin for required keys.""" + + value_fn: Callable[[Status], int | float | None] + + +@dataclass +class PVOutputSensorEntityDescription( + SensorEntityDescription, PVOutputSensorEntityDescriptionMixin +): + """Describes a PVOutput sensor entity.""" + + +SENSORS: tuple[PVOutputSensorEntityDescription, ...] = ( + PVOutputSensorEntityDescription( + key="energy_generation", + name="Energy Generated", + native_unit_of_measurement=ENERGY_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda status: status.energy_generation, + ), +) + + async def async_setup_platform( hass: HomeAssistant, config: ConfigType, @@ -79,28 +109,46 @@ async def async_setup_entry( entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up a Tailscale binary sensors based on a config entry.""" + """Set up a PVOutput sensors based on a config entry.""" coordinator = hass.data[DOMAIN][entry.entry_id] - async_add_entities([PvoutputSensor(coordinator)]) + async_add_entities( + PVOutputSensorEntity( + coordinator=coordinator, + description=description, + ) + for description in SENSORS + ) -class PvoutputSensor(CoordinatorEntity, SensorEntity): +class PVOutputSensorEntity(CoordinatorEntity, SensorEntity): """Representation of a PVOutput sensor.""" - _attr_state_class = SensorStateClass.TOTAL_INCREASING - _attr_device_class = SensorDeviceClass.ENERGY - _attr_native_unit_of_measurement = ENERGY_WATT_HOUR - coordinator: DataUpdateCoordinator[Status] + entity_description: PVOutputSensorEntityDescription + + def __init__( + self, + *, + coordinator: DataUpdateCoordinator, + description: PVOutputSensorEntityDescription, + ) -> None: + """Initialize a PVOutput sensor.""" + super().__init__(coordinator=coordinator) + self.entity_description = description @property - def native_value(self) -> int | None: + def native_value(self) -> int | float | None: """Return the state of the device.""" - return self.coordinator.data.energy_generation + return self.entity_description.value_fn(self.coordinator.data) @property - def extra_state_attributes(self) -> dict[str, int | float | None]: + def extra_state_attributes(self) -> dict[str, int | float | None] | None: """Return the state attributes of the monitored installation.""" + + # Only add attributes to the original sensor + if self.entity_description.key != "energy_generation": + return None + return { ATTR_ENERGY_GENERATION: self.coordinator.data.energy_generation, ATTR_POWER_GENERATION: self.coordinator.data.power_generation, From 1af31774666b522ed4b021812302eb4d0682856d Mon Sep 17 00:00:00 2001 From: Pascal Reeb Date: Mon, 27 Dec 2021 23:02:48 +0100 Subject: [PATCH 1071/2644] Fix unique_id of nuki config entry (#62840) * fix(nuki): fixed naming of nuki integration * parse_id function * migration path * fixes from ci runs * don't update title if it was changed * move to dedicated helper * use dict of params * Update homeassistant/components/nuki/__init__.py Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/nuki/__init__.py | 9 +++++++++ homeassistant/components/nuki/config_flow.py | 14 ++++++++------ homeassistant/components/nuki/helpers.py | 6 ++++++ tests/components/nuki/mock.py | 3 ++- tests/components/nuki/test_config_flow.py | 4 ++-- 5 files changed, 27 insertions(+), 9 deletions(-) create mode 100644 homeassistant/components/nuki/helpers.py diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index 2397b211e48..8bf77e7abd4 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -25,6 +25,7 @@ from .const import ( DOMAIN, ERROR_STATES, ) +from .helpers import parse_id _LOGGER = logging.getLogger(__name__) @@ -53,6 +54,14 @@ async def async_setup_entry(hass, entry): hass.data.setdefault(DOMAIN, {}) + # Migration of entry unique_id + if isinstance(entry.unique_id, int): + new_id = parse_id(entry.unique_id) + params = {"unique_id": new_id} + if entry.title == entry.unique_id: + params["title"] = new_id + hass.config_entries.async_update_entry(entry, **params) + try: bridge = await hass.async_add_executor_job( NukiBridge, diff --git a/homeassistant/components/nuki/config_flow.py b/homeassistant/components/nuki/config_flow.py index dae1d82a104..054d0aaa219 100644 --- a/homeassistant/components/nuki/config_flow.py +++ b/homeassistant/components/nuki/config_flow.py @@ -12,6 +12,7 @@ from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN from homeassistant.data_entry_flow import FlowResult from .const import DEFAULT_PORT, DEFAULT_TIMEOUT, DOMAIN +from .helpers import parse_id _LOGGER = logging.getLogger(__name__) @@ -65,7 +66,7 @@ class NukiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Prepare configuration for a DHCP discovered Nuki bridge.""" - await self.async_set_unique_id(int(discovery_info.hostname[12:], 16)) + await self.async_set_unique_id(discovery_info.hostname[12:].upper()) self._abort_if_unique_id_configured() @@ -110,7 +111,9 @@ class NukiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "unknown" if not errors: - existing_entry = await self.async_set_unique_id(info["ids"]["hardwareId"]) + existing_entry = await self.async_set_unique_id( + parse_id(info["ids"]["hardwareId"]) + ) if existing_entry: self.hass.config_entries.async_update_entry(existing_entry, data=conf) self.hass.async_create_task( @@ -139,11 +142,10 @@ class NukiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "unknown" if "base" not in errors: - await self.async_set_unique_id(info["ids"]["hardwareId"]) + bridge_id = parse_id(info["ids"]["hardwareId"]) + await self.async_set_unique_id(bridge_id) self._abort_if_unique_id_configured() - return self.async_create_entry( - title=info["ids"]["hardwareId"], data=user_input - ) + return self.async_create_entry(title=bridge_id, data=user_input) data_schema = self.discovery_schema or USER_SCHEMA return self.async_show_form( diff --git a/homeassistant/components/nuki/helpers.py b/homeassistant/components/nuki/helpers.py new file mode 100644 index 00000000000..3deedf9d8db --- /dev/null +++ b/homeassistant/components/nuki/helpers.py @@ -0,0 +1,6 @@ +"""nuki integration helpers.""" + + +def parse_id(hardware_id): + """Parse Nuki ID.""" + return hex(hardware_id).split("x")[-1].upper() diff --git a/tests/components/nuki/mock.py b/tests/components/nuki/mock.py index 30315915a73..e85d1de3933 100644 --- a/tests/components/nuki/mock.py +++ b/tests/components/nuki/mock.py @@ -7,6 +7,7 @@ HOST = "1.1.1.1" MAC = "01:23:45:67:89:ab" HW_ID = 123456789 +ID_HEX = "75BCD15" MOCK_INFO = {"ids": {"hardwareId": HW_ID}} @@ -16,7 +17,7 @@ async def setup_nuki_integration(hass): entry = MockConfigEntry( domain="nuki", - unique_id=HW_ID, + unique_id=ID_HEX, data={"host": HOST, "port": 8080, "token": "test-token"}, ) entry.add_to_hass(hass) diff --git a/tests/components/nuki/test_config_flow.py b/tests/components/nuki/test_config_flow.py index afd941ef00a..77966bd7e5f 100644 --- a/tests/components/nuki/test_config_flow.py +++ b/tests/components/nuki/test_config_flow.py @@ -39,7 +39,7 @@ async def test_form(hass): await hass.async_block_till_done() assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == 123456789 + assert result2["title"] == "75BCD15" assert result2["data"] == { "host": "1.1.1.1", "port": 8080, @@ -169,7 +169,7 @@ async def test_dhcp_flow(hass): ) assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == 123456789 + assert result2["title"] == "75BCD15" assert result2["data"] == { "host": "1.1.1.1", "port": 8080, From cee0440ab69aecf3a8aaf262a6820c1930368c91 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 27 Dec 2021 23:14:43 +0100 Subject: [PATCH 1072/2644] Add unique ID to PVOutput entities (#62890) --- homeassistant/components/pvoutput/sensor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/pvoutput/sensor.py b/homeassistant/components/pvoutput/sensor.py index 1cae6e53e67..5f3765c688e 100644 --- a/homeassistant/components/pvoutput/sensor.py +++ b/homeassistant/components/pvoutput/sensor.py @@ -115,6 +115,7 @@ async def async_setup_entry( PVOutputSensorEntity( coordinator=coordinator, description=description, + system_id=entry.data[CONF_SYSTEM_ID], ) for description in SENSORS ) @@ -131,10 +132,12 @@ class PVOutputSensorEntity(CoordinatorEntity, SensorEntity): *, coordinator: DataUpdateCoordinator, description: PVOutputSensorEntityDescription, + system_id: str, ) -> None: """Initialize a PVOutput sensor.""" super().__init__(coordinator=coordinator) self.entity_description = description + self._attr_unique_id = f"{system_id}_{description.key}" @property def native_value(self) -> int | float | None: From e01b0a36250ee6de0c755cabada95fbe4f45cb6b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 27 Dec 2021 23:42:24 +0100 Subject: [PATCH 1073/2644] Add setup type hints to sonos, unifi and wake_on_lan (#62876) Co-authored-by: epenet --- homeassistant/components/sonos/__init__.py | 3 ++- homeassistant/components/sonos/binary_sensor.py | 9 ++++++++- homeassistant/components/sonos/number.py | 10 ++++++++-- homeassistant/components/sonos/sensor.py | 10 ++++++++-- homeassistant/components/sonos/switch.py | 10 ++++++++-- homeassistant/components/unifi/__init__.py | 14 +++++++++----- homeassistant/components/unifi/device_tracker.py | 10 ++++++++-- homeassistant/components/unifi/sensor.py | 11 ++++++++--- homeassistant/components/unifi/switch.py | 10 ++++++++-- homeassistant/components/wake_on_lan/__init__.py | 4 +++- homeassistant/components/wake_on_lan/switch.py | 12 +++++++++++- 11 files changed, 81 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 32ae234434e..033c6e046c8 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -25,6 +25,7 @@ from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval, call_later +from homeassistant.helpers.typing import ConfigType from .alarms import SonosAlarms from .const import ( @@ -97,7 +98,7 @@ class SonosData: self.mdns_names: dict[str, str] = {} -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Sonos component.""" conf = config.get(DOMAIN) diff --git a/homeassistant/components/sonos/binary_sensor.py b/homeassistant/components/sonos/binary_sensor.py index e3545552bee..5c299c07562 100644 --- a/homeassistant/components/sonos/binary_sensor.py +++ b/homeassistant/components/sonos/binary_sensor.py @@ -8,8 +8,11 @@ from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, ) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import SONOS_CREATE_BATTERY from .entity import SonosEntity @@ -20,7 +23,11 @@ ATTR_BATTERY_POWER_SOURCE = "power_source" _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up Sonos from a config entry.""" async def _async_create_entity(speaker: SonosSpeaker) -> None: diff --git a/homeassistant/components/sonos/number.py b/homeassistant/components/sonos/number.py index afe7effed53..d62e09f0b49 100644 --- a/homeassistant/components/sonos/number.py +++ b/homeassistant/components/sonos/number.py @@ -4,9 +4,11 @@ from __future__ import annotations import logging from homeassistant.components.number import NumberEntity -from homeassistant.core import callback +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 .const import SONOS_CREATE_LEVELS from .entity import SonosEntity @@ -18,7 +20,11 @@ LEVEL_TYPES = ("bass", "treble") _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the Sonos number platform from a config entry.""" @callback diff --git a/homeassistant/components/sonos/sensor.py b/homeassistant/components/sonos/sensor.py index d735a7be1e3..4e86edc2ca3 100644 --- a/homeassistant/components/sonos/sensor.py +++ b/homeassistant/components/sonos/sensor.py @@ -4,10 +4,12 @@ from __future__ import annotations import logging from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE -from homeassistant.core import callback +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 .const import SONOS_CREATE_AUDIO_FORMAT_SENSOR, SONOS_CREATE_BATTERY from .entity import SonosEntity @@ -16,7 +18,11 @@ from .speaker import SonosSpeaker _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up Sonos from a config entry.""" @callback diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index c82069c087a..84a0eb6c324 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -7,11 +7,13 @@ import logging from soco.exceptions import SoCoException, SoCoSlaveException, SoCoUPnPException from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TIME -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( DATA_SONOS, @@ -81,7 +83,11 @@ FEATURE_ICONS = { } -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up Sonos from a config entry.""" async def _async_create_alarms(speaker: SonosSpeaker, alarm_ids: list[str]) -> None: diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index 180f1d6752f..a396f8ce38f 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -1,8 +1,10 @@ """Integration to UniFi Network and its various features.""" +from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.typing import ConfigType from .const import ( ATTR_MANUFACTURER, @@ -19,7 +21,7 @@ STORAGE_KEY = "unifi_data" STORAGE_VERSION = 1 -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Integration doesn't support configuration through configuration.yaml.""" hass.data[UNIFI_WIRELESS_CLIENTS] = wireless_clients = UnifiWirelessClients(hass) await wireless_clients.async_load() @@ -27,7 +29,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, config_entry): +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up the UniFi Network integration.""" hass.data.setdefault(UNIFI_DOMAIN, {}) @@ -71,7 +73,7 @@ async def async_setup_entry(hass, config_entry): return True -async def async_unload_entry(hass, config_entry): +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" controller = hass.data[UNIFI_DOMAIN].pop(config_entry.entry_id) @@ -81,7 +83,9 @@ async def async_unload_entry(hass, config_entry): return await controller.async_reset() -async def async_flatten_entry_data(hass, config_entry): +async def async_flatten_entry_data( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: """Simpler configuration structure for entry data. Keep controller key layer in case user rollbacks. diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 035d8b0ae87..a27c4de2244 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -18,12 +18,14 @@ from aiounifi.events import ( from homeassistant.components.device_tracker import DOMAIN from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.components.device_tracker.const import SOURCE_TYPE_ROUTER +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_NAME -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util from .const import ATTR_MANUFACTURER, DOMAIN as UNIFI_DOMAIN @@ -71,7 +73,11 @@ WIRELESS_CONNECTION = ( ) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up device tracker for UniFi Network integration.""" controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] controller.entities[DOMAIN] = {CLIENT_TRACKER: set(), DEVICE_TRACKER: set()} diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index deae3be3485..7f825ed8052 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -3,14 +3,15 @@ Support for bandwidth sensors of network clients. Support for uptime sensors of network clients. """ - from datetime import datetime, timedelta from homeassistant.components.sensor import DOMAIN, SensorDeviceClass, SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import DATA_MEGABYTES -from homeassistant.core import callback +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 import homeassistant.util.dt as dt_util from .const import DOMAIN as UNIFI_DOMAIN @@ -21,7 +22,11 @@ TX_SENSOR = "tx" UPTIME_SENSOR = "uptime" -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up sensors for UniFi Network integration.""" controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] controller.entities[DOMAIN] = { diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 638ff0aa397..fdb4c6af3da 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -15,10 +15,12 @@ from aiounifi.events import ( ) from homeassistant.components.switch import DOMAIN, SwitchEntity -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_registry import async_entries_for_config_entry from homeassistant.helpers.restore_state import RestoreEntity @@ -34,7 +36,11 @@ CLIENT_BLOCKED = (WIRED_CLIENT_BLOCKED, WIRELESS_CLIENT_BLOCKED) CLIENT_UNBLOCKED = (WIRED_CLIENT_UNBLOCKED, WIRELESS_CLIENT_UNBLOCKED) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up switches for UniFi Network integration. Switches are controlling network access and switch ports with POE. diff --git a/homeassistant/components/wake_on_lan/__init__.py b/homeassistant/components/wake_on_lan/__init__.py index dba25b44d75..e788f6c0124 100644 --- a/homeassistant/components/wake_on_lan/__init__.py +++ b/homeassistant/components/wake_on_lan/__init__.py @@ -6,7 +6,9 @@ import voluptuous as vol import wakeonlan from homeassistant.const import CONF_BROADCAST_ADDRESS, CONF_BROADCAST_PORT, CONF_MAC +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType from .const import DOMAIN @@ -23,7 +25,7 @@ WAKE_ON_LAN_SEND_MAGIC_PACKET_SCHEMA = vol.Schema( ) -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the wake on LAN component.""" async def send_magic_packet(call): diff --git a/homeassistant/components/wake_on_lan/switch.py b/homeassistant/components/wake_on_lan/switch.py index f7e5426c73f..4bf2c650396 100644 --- a/homeassistant/components/wake_on_lan/switch.py +++ b/homeassistant/components/wake_on_lan/switch.py @@ -1,4 +1,6 @@ """Support for wake on lan.""" +from __future__ import annotations + import logging import platform import subprocess as sp @@ -14,9 +16,12 @@ from homeassistant.const import ( CONF_MAC, CONF_NAME, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.script import Script +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import DOMAIN @@ -39,7 +44,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform(hass, config, add_entities, discovery_info=None): +def setup_platform( + hass: HomeAssistant, + config: ConfigType, + add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Set up a wake on lan switch.""" broadcast_address = config.get(CONF_BROADCAST_ADDRESS) broadcast_port = config.get(CONF_BROADCAST_PORT) From 942f58593b98b87981b4b2e910ab1131c58b7e07 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 27 Dec 2021 23:55:46 +0100 Subject: [PATCH 1074/2644] Add type hint to adguard service calls (#62893) Co-authored-by: epenet --- homeassistant/components/adguard/__init__.py | 26 +++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index 9b419c444ce..8c90efcd44c 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -18,7 +18,7 @@ from homeassistant.const import ( CONF_VERIFY_SSL, Platform, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -72,31 +72,27 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_setup_platforms(entry, PLATFORMS) - async def add_url(call) -> None: + async def add_url(call: ServiceCall) -> None: """Service call to add a new filter subscription to AdGuard Home.""" await adguard.filtering.add_url( - allowlist=False, name=call.data.get(CONF_NAME), url=call.data.get(CONF_URL) + allowlist=False, name=call.data[CONF_NAME], url=call.data[CONF_URL] ) - async def remove_url(call) -> None: + async def remove_url(call: ServiceCall) -> None: """Service call to remove a filter subscription from AdGuard Home.""" - await adguard.filtering.remove_url(allowlist=False, url=call.data.get(CONF_URL)) + await adguard.filtering.remove_url(allowlist=False, url=call.data[CONF_URL]) - async def enable_url(call) -> None: + async def enable_url(call: ServiceCall) -> None: """Service call to enable a filter subscription in AdGuard Home.""" - await adguard.filtering.enable_url(allowlist=False, url=call.data.get(CONF_URL)) + await adguard.filtering.enable_url(allowlist=False, url=call.data[CONF_URL]) - async def disable_url(call) -> None: + async def disable_url(call: ServiceCall) -> None: """Service call to disable a filter subscription in AdGuard Home.""" - await adguard.filtering.disable_url( - allowlist=False, url=call.data.get(CONF_URL) - ) + await adguard.filtering.disable_url(allowlist=False, url=call.data[CONF_URL]) - async def refresh(call) -> None: + async def refresh(call: ServiceCall) -> None: """Service call to refresh the filter subscriptions in AdGuard Home.""" - await adguard.filtering.refresh( - allowlist=False, force=call.data.get(CONF_FORCE) - ) + await adguard.filtering.refresh(allowlist=False, force=call.data[CONF_FORCE]) hass.services.async_register( DOMAIN, SERVICE_ADD_URL, add_url, schema=SERVICE_ADD_URL_SCHEMA From de64622f3b20825270d55a57a9e6dc84d279ef19 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Dec 2021 00:14:20 +0100 Subject: [PATCH 1075/2644] Ensure service calls are typed [a-d] (#62891) Co-authored-by: epenet --- homeassistant/components/alert/__init__.py | 3 ++- homeassistant/components/ambiclimate/climate.py | 7 ++++--- homeassistant/components/bluesound/media_player.py | 4 ++-- homeassistant/components/cloudflare/__init__.py | 4 ++-- homeassistant/components/color_extractor/__init__.py | 3 ++- homeassistant/components/configurator/__init__.py | 4 ++-- homeassistant/components/conversation/__init__.py | 2 +- homeassistant/components/duckdns/__init__.py | 4 ++-- homeassistant/components/dynalite/__init__.py | 2 +- 9 files changed, 18 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/alert/__init__.py b/homeassistant/components/alert/__init__.py index e5ee7ee6911..871778a9c11 100644 --- a/homeassistant/components/alert/__init__.py +++ b/homeassistant/components/alert/__init__.py @@ -23,6 +23,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) +from homeassistant.core import ServiceCall from homeassistant.helpers import event, service import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import ToggleEntity @@ -117,7 +118,7 @@ async def async_setup(hass, config): if not entities: return False - async def async_handle_alert_service(service_call): + async def async_handle_alert_service(service_call: ServiceCall) -> None: """Handle calls to alert services.""" alert_ids = await service.async_extract_entity_ids(hass, service_call) diff --git a/homeassistant/components/ambiclimate/climate.py b/homeassistant/components/ambiclimate/climate.py index 67fd3adeec7..b7890257ca3 100644 --- a/homeassistant/components/ambiclimate/climate.py +++ b/homeassistant/components/ambiclimate/climate.py @@ -19,6 +19,7 @@ from homeassistant.const import ( CONF_CLIENT_SECRET, TEMP_CELSIUS, ) +from homeassistant.core import ServiceCall from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import DeviceInfo @@ -96,7 +97,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(devs, True) - async def send_comfort_feedback(service): + async def send_comfort_feedback(service: ServiceCall) -> None: """Send comfort feedback.""" device_name = service.data[ATTR_NAME] device = data_connection.find_device_by_room_name(device_name) @@ -110,7 +111,7 @@ async def async_setup_entry(hass, entry, async_add_entities): schema=SEND_COMFORT_FEEDBACK_SCHEMA, ) - async def set_comfort_mode(service): + async def set_comfort_mode(service: ServiceCall) -> None: """Set comfort mode.""" device_name = service.data[ATTR_NAME] device = data_connection.find_device_by_room_name(device_name) @@ -121,7 +122,7 @@ async def async_setup_entry(hass, entry, async_add_entities): DOMAIN, SERVICE_COMFORT_MODE, set_comfort_mode, schema=SET_COMFORT_MODE_SCHEMA ) - async def set_temperature_mode(service): + async def set_temperature_mode(service: ServiceCall) -> None: """Set temperature mode.""" device_name = service.data[ATTR_NAME] device = data_connection.find_device_by_room_name(device_name) diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index c91a2dedca3..9884461e0a9 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -44,7 +44,7 @@ from homeassistant.const import ( STATE_PAUSED, STATE_PLAYING, ) -from homeassistant.core import callback +from homeassistant.core import ServiceCall, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval @@ -172,7 +172,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= host.get(CONF_NAME), ) - async def async_service_handler(service): + async def async_service_handler(service: ServiceCall) -> None: """Map services to method of Bluesound devices.""" if not (method := SERVICE_TO_METHOD.get(service.service)): return diff --git a/homeassistant/components/cloudflare/__init__.py b/homeassistant/components/cloudflare/__init__.py index dc0782aa9c0..df5b08e9395 100644 --- a/homeassistant/components/cloudflare/__init__.py +++ b/homeassistant/components/cloudflare/__init__.py @@ -13,7 +13,7 @@ from pycfdns.exceptions import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_TOKEN, CONF_ZONE -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -49,7 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except CloudflareException as error: _LOGGER.error("Error updating zone %s: %s", entry.data[CONF_ZONE], error) - async def update_records_service(call): + async def update_records_service(call: ServiceCall) -> None: """Set up service for manual trigger.""" try: await _async_update_cloudflare(cfupdate, zone_id) diff --git a/homeassistant/components/color_extractor/__init__.py b/homeassistant/components/color_extractor/__init__.py index 4d8118483b6..73e8e09101c 100644 --- a/homeassistant/components/color_extractor/__init__.py +++ b/homeassistant/components/color_extractor/__init__.py @@ -15,6 +15,7 @@ from homeassistant.components.light import ( LIGHT_TURN_ON_SCHEMA, SERVICE_TURN_ON as LIGHT_SERVICE_TURN_ON, ) +from homeassistant.core import ServiceCall from homeassistant.helpers import aiohttp_client import homeassistant.helpers.config_validation as cv @@ -58,7 +59,7 @@ def _get_color(file_handler) -> tuple: async def async_setup(hass, hass_config): """Set up services for color_extractor integration.""" - async def async_handle_service(service_call): + async def async_handle_service(service_call: ServiceCall) -> None: """Decide which color_extractor method to call based on service.""" service_data = dict(service_call.data) diff --git a/homeassistant/components/configurator/__init__.py b/homeassistant/components/configurator/__init__.py index b94483122d0..edd00fec486 100644 --- a/homeassistant/components/configurator/__init__.py +++ b/homeassistant/components/configurator/__init__.py @@ -14,7 +14,7 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, EVENT_TIME_CHANGED, ) -from homeassistant.core import Event, callback as async_callback +from homeassistant.core import Event, ServiceCall, callback as async_callback from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.loader import bind_hass from homeassistant.util.async_ import run_callback_threadsafe @@ -212,7 +212,7 @@ class Configurator: self.hass.bus.async_listen_once(EVENT_TIME_CHANGED, deferred_remove) - async def async_handle_service_call(self, call): + async def async_handle_service_call(self, call: ServiceCall) -> None: """Handle a configure service call.""" request_id = call.data.get(ATTR_CONFIGURE_ID) diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index 401d240957e..75bbd4d93aa 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -55,7 +55,7 @@ async def async_setup(hass, config): """Register the process service.""" hass.data[DATA_CONFIG] = config - async def handle_service(service): + async def handle_service(service: core.ServiceCall) -> None: """Parse text into commands.""" text = service.data[ATTR_TEXT] _LOGGER.debug("Processing: <%s>", text) diff --git a/homeassistant/components/duckdns/__init__.py b/homeassistant/components/duckdns/__init__.py index 2271b107b36..42f36c5435f 100644 --- a/homeassistant/components/duckdns/__init__.py +++ b/homeassistant/components/duckdns/__init__.py @@ -5,7 +5,7 @@ import logging import voluptuous as vol from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DOMAIN -from homeassistant.core import CALLBACK_TYPE, callback +from homeassistant.core import CALLBACK_TYPE, ServiceCall, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_call_later @@ -58,7 +58,7 @@ async def async_setup(hass, config): ) async_track_time_interval_backoff(hass, update_domain_interval, intervals) - async def update_domain_service(call): + async def update_domain_service(call: ServiceCall) -> None: """Update the DuckDNS entry.""" await _update_duckdns(session, domain, token, txt=call.data[ATTR_TXT]) diff --git a/homeassistant/components/dynalite/__init__.py b/homeassistant/components/dynalite/__init__.py index 49e742519fd..35f76354eab 100644 --- a/homeassistant/components/dynalite/__init__.py +++ b/homeassistant/components/dynalite/__init__.py @@ -208,7 +208,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) ) - async def dynalite_service(service_call: ServiceCall): + async def dynalite_service(service_call: ServiceCall) -> None: data = service_call.data host = data.get(ATTR_HOST, "") bridges = [] From cb135bc88907ca3cb22fcd407d4b2cae8615d0e9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Dec 2021 00:15:40 +0100 Subject: [PATCH 1076/2644] Add basic type hints to xiaomi_miio (#62889) Co-authored-by: epenet --- homeassistant/components/xiaomi_miio/air_quality.py | 9 ++++++++- .../components/xiaomi_miio/alarm_control_panel.py | 10 ++++++++-- .../components/xiaomi_miio/binary_sensor.py | 10 ++++++++-- homeassistant/components/xiaomi_miio/fan.py | 12 +++++++++--- homeassistant/components/xiaomi_miio/humidifier.py | 10 ++++++++-- homeassistant/components/xiaomi_miio/light.py | 11 +++++++++-- homeassistant/components/xiaomi_miio/number.py | 10 ++++++++-- homeassistant/components/xiaomi_miio/remote.py | 12 +++++++++++- homeassistant/components/xiaomi_miio/select.py | 10 ++++++++-- homeassistant/components/xiaomi_miio/sensor.py | 10 ++++++++-- homeassistant/components/xiaomi_miio/switch.py | 12 +++++++++--- homeassistant/components/xiaomi_miio/vacuum.py | 10 ++++++++-- 12 files changed, 102 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/air_quality.py b/homeassistant/components/xiaomi_miio/air_quality.py index 271beae131c..fdc62076c25 100644 --- a/homeassistant/components/xiaomi_miio/air_quality.py +++ b/homeassistant/components/xiaomi_miio/air_quality.py @@ -4,7 +4,10 @@ import logging from miio import AirQualityMonitor, AirQualityMonitorCGDN1, DeviceException from homeassistant.components.air_quality import AirQualityEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_TOKEN +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( CONF_DEVICE, @@ -236,7 +239,11 @@ DEVICE_MAP = { } -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the Xiaomi Air Quality from a config entry.""" entities = [] diff --git a/homeassistant/components/xiaomi_miio/alarm_control_panel.py b/homeassistant/components/xiaomi_miio/alarm_control_panel.py index 1fccbcf8056..f7dbf60f63a 100644 --- a/homeassistant/components/xiaomi_miio/alarm_control_panel.py +++ b/homeassistant/components/xiaomi_miio/alarm_control_panel.py @@ -1,5 +1,4 @@ """Support for Xiomi Gateway alarm control panels.""" - from functools import partial import logging @@ -9,12 +8,15 @@ from homeassistant.components.alarm_control_panel import ( SUPPORT_ALARM_ARM_AWAY, AlarmControlPanelEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMING, STATE_ALARM_DISARMED, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import CONF_GATEWAY, DOMAIN @@ -25,7 +27,11 @@ XIAOMI_STATE_DISARMED_VALUE = "off" XIAOMI_STATE_ARMING_VALUE = "oning" -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the Xiaomi Gateway Alarm from a config entry.""" entities = [] gateway = hass.data[DOMAIN][config_entry.entry_id][CONF_GATEWAY] diff --git a/homeassistant/components/xiaomi_miio/binary_sensor.py b/homeassistant/components/xiaomi_miio/binary_sensor.py index c9fd97f209b..83bea4be9b1 100644 --- a/homeassistant/components/xiaomi_miio/binary_sensor.py +++ b/homeassistant/components/xiaomi_miio/binary_sensor.py @@ -10,8 +10,10 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import VacuumCoordinatorDataAttributes from .const import ( @@ -158,7 +160,11 @@ def _setup_vacuum_sensors(hass, config_entry, async_add_entities): async_add_entities(entities) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the Xiaomi sensor from a config entry.""" entities = [] diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 07ec4613270..a12a8a6063b 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -24,9 +24,11 @@ from homeassistant.components.fan import ( SUPPORT_SET_SPEED, FanEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, ServiceCall, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.percentage import ( percentage_to_ranged_value, ranged_value_to_percentage, @@ -172,7 +174,11 @@ FAN_DIRECTIONS_MAP = { } -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the Fan from a config entry.""" entities = [] @@ -222,7 +228,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities.append(entity) - async def async_service_handler(service): + async def async_service_handler(service: ServiceCall) -> None: """Map services to methods on XiaomiAirPurifier.""" method = SERVICE_TO_METHOD[service.service] params = { diff --git a/homeassistant/components/xiaomi_miio/humidifier.py b/homeassistant/components/xiaomi_miio/humidifier.py index f674cc770b8..7c3110e190a 100644 --- a/homeassistant/components/xiaomi_miio/humidifier.py +++ b/homeassistant/components/xiaomi_miio/humidifier.py @@ -8,8 +8,10 @@ from miio.airhumidifier_mjjsq import OperationMode as AirhumidifierMjjsqOperatio from homeassistant.components.humidifier import HumidifierDeviceClass, HumidifierEntity from homeassistant.components.humidifier.const import SUPPORT_MODES +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_MODE -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.percentage import percentage_to_ranged_value from .const import ( @@ -55,7 +57,11 @@ AVAILABLE_MODES_OTHER = [ ] -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the Humidifier from a config entry.""" if not config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: return diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index 03722380a69..cae7bacaba4 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -24,9 +24,12 @@ from homeassistant.components.light import ( SUPPORT_COLOR_TEMP, LightEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_TOKEN +from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import color, dt from .const import ( @@ -109,7 +112,11 @@ SERVICE_TO_METHOD = { } -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the Xiaomi light from a config entry.""" entities = [] @@ -187,7 +194,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) return - async def async_service_handler(service): + async def async_service_handler(service: ServiceCall) -> None: """Map services to methods on Xiaomi Philips Lights.""" method = SERVICE_TO_METHOD.get(service.service) params = { diff --git a/homeassistant/components/xiaomi_miio/number.py b/homeassistant/components/xiaomi_miio/number.py index cd05218760d..b2a3f4e9f5e 100644 --- a/homeassistant/components/xiaomi_miio/number.py +++ b/homeassistant/components/xiaomi_miio/number.py @@ -4,9 +4,11 @@ from __future__ import annotations from dataclasses import dataclass from homeassistant.components.number import NumberEntity, NumberEntityDescription +from homeassistant.config_entries import ConfigEntry from homeassistant.const import DEGREE, TIME_MINUTES -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( CONF_DEVICE, @@ -228,7 +230,11 @@ OSCILLATION_ANGLE_VALUES = { } -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the Selectors from a config entry.""" entities = [] if not config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: diff --git a/homeassistant/components/xiaomi_miio/remote.py b/homeassistant/components/xiaomi_miio/remote.py index 5428d8a7bde..890adc8fcde 100644 --- a/homeassistant/components/xiaomi_miio/remote.py +++ b/homeassistant/components/xiaomi_miio/remote.py @@ -1,4 +1,6 @@ """Support for the Xiaomi IR Remote (Chuangmi IR).""" +from __future__ import annotations + import asyncio from datetime import timedelta import logging @@ -21,8 +23,11 @@ from homeassistant.const import ( CONF_TIMEOUT, CONF_TOKEN, ) +from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.dt import utcnow from .const import SERVICE_LEARN, SERVICE_SET_REMOTE_LED_OFF, SERVICE_SET_REMOTE_LED_ON @@ -58,7 +63,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Set up the Xiaomi IR Remote (Chuangmi IR) platform.""" host = config[CONF_HOST] token = config[CONF_TOKEN] diff --git a/homeassistant/components/xiaomi_miio/select.py b/homeassistant/components/xiaomi_miio/select.py index d2a806f8305..a0ff320e228 100644 --- a/homeassistant/components/xiaomi_miio/select.py +++ b/homeassistant/components/xiaomi_miio/select.py @@ -11,8 +11,10 @@ from miio.airpurifier_miot import LedBrightness as AirpurifierMiotLedBrightness from miio.fan import LedBrightness as FanLedBrightness from homeassistant.components.select import SelectEntity, SelectEntityDescription -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( CONF_DEVICE, @@ -68,7 +70,11 @@ SELECTOR_TYPES = { } -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the Selectors from a config entry.""" if not config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: return diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 1104ff90117..ce51f0355e2 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -20,6 +20,7 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( AREA_SQUARE_METERS, ATTR_BATTERY_LEVEL, @@ -37,8 +38,9 @@ from homeassistant.const import ( TIME_SECONDS, VOLUME_CUBIC_METERS, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util from . import VacuumCoordinatorDataAttributes @@ -550,7 +552,11 @@ def _setup_vacuum_sensors(hass, config_entry, async_add_entities): async_add_entities(entities) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the Xiaomi sensor from a config entry.""" entities = [] diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 1bbb5c65c49..bd6482e891b 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -15,6 +15,7 @@ from homeassistant.components.switch import ( SwitchEntity, SwitchEntityDescription, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, @@ -22,9 +23,10 @@ from homeassistant.const import ( CONF_HOST, CONF_TOKEN, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, ServiceCall, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( CONF_DEVICE, @@ -274,7 +276,11 @@ SWITCH_TYPES = ( ) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the switch from a config entry.""" model = config_entry.data[CONF_MODEL] if model in (*MODELS_HUMIDIFIER, *MODELS_FAN): @@ -405,7 +411,7 @@ async def async_setup_other_entry(hass, config_entry, async_add_entities): model, ) - async def async_service_handler(service): + async def async_service_handler(service: ServiceCall) -> None: """Map services to methods on XiaomiPlugGenericSwitch.""" method = SERVICE_TO_METHOD.get(service.service) params = { diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index 2362fcf8996..a6fa6c399eb 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -26,8 +26,10 @@ from homeassistant.components.vacuum import ( SUPPORT_STOP, StateVacuumEntity, ) -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import as_utc from . import VacuumCoordinatorData @@ -98,7 +100,11 @@ STATE_CODE_TO_STATE = { } -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the Xiaomi vacuum cleaner robot from a config entry.""" entities = [] From 0bcb0a6267f10699f598c8134a18d118302b5067 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Mon, 27 Dec 2021 15:57:19 -0800 Subject: [PATCH 1077/2644] Add scene entity to Overkiz integration (#62884) --- .coveragerc | 1 + homeassistant/components/overkiz/__init__.py | 21 ++++++--- homeassistant/components/overkiz/const.py | 1 + homeassistant/components/overkiz/scene.py | 45 ++++++++++++++++++++ 4 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/overkiz/scene.py diff --git a/.coveragerc b/.coveragerc index 513f2eff055..27b4af835d6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -810,6 +810,7 @@ omit = homeassistant/components/overkiz/light.py homeassistant/components/overkiz/lock.py homeassistant/components/overkiz/number.py + homeassistant/components/overkiz/scene.py homeassistant/components/overkiz/sensor.py homeassistant/components/ovo_energy/__init__.py homeassistant/components/ovo_energy/const.py diff --git a/homeassistant/components/overkiz/__init__.py b/homeassistant/components/overkiz/__init__.py index c60a3104839..bb97f2aae81 100644 --- a/homeassistant/components/overkiz/__init__.py +++ b/homeassistant/components/overkiz/__init__.py @@ -1,6 +1,7 @@ """The Overkiz (by Somfy) integration.""" from __future__ import annotations +import asyncio from collections import defaultdict from dataclasses import dataclass import logging @@ -13,10 +14,10 @@ from pyoverkiz.exceptions import ( MaintenanceException, TooManyRequestsException, ) -from pyoverkiz.models import Device +from pyoverkiz.models import Device, Scenario from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr @@ -40,7 +41,7 @@ class HomeAssistantOverkizData: """Overkiz data stored in the Home Assistant data object.""" coordinator: OverkizDataUpdateCoordinator - platforms: dict[str, Device] + platforms: dict[Platform, Device | Scenario] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -57,7 +58,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await client.login() - setup = await client.get_setup() + + setup, scenarios = await asyncio.gather( + *[ + client.get_setup(), + client.get_scenarios(), + ] + ) except BadCredentialsException: _LOGGER.error("Invalid authentication") return False @@ -91,14 +98,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) coordinator.update_interval = UPDATE_INTERVAL_ALL_ASSUMED_STATE - platforms: dict[str, Device] = defaultdict(list) + platforms: dict[Platform, Device | Scenario] = defaultdict(list) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = HomeAssistantOverkizData( coordinator=coordinator, platforms=platforms, ) - # Map Overkiz device to Home Assistant platform + # Map Overkiz entities to Home Assistant platform + platforms[Platform.SCENE] = scenarios + for device in coordinator.data.values(): _LOGGER.debug( "The following device has been retrieved. Report an issue if not supported correctly (%s)", diff --git a/homeassistant/components/overkiz/const.py b/homeassistant/components/overkiz/const.py index 543cd1e187a..150323c19da 100644 --- a/homeassistant/components/overkiz/const.py +++ b/homeassistant/components/overkiz/const.py @@ -22,6 +22,7 @@ PLATFORMS: list[Platform] = [ Platform.LIGHT, Platform.LOCK, Platform.NUMBER, + Platform.SCENE, Platform.SENSOR, ] diff --git a/homeassistant/components/overkiz/scene.py b/homeassistant/components/overkiz/scene.py new file mode 100644 index 00000000000..f64263fb26e --- /dev/null +++ b/homeassistant/components/overkiz/scene.py @@ -0,0 +1,45 @@ +"""Support for Overkiz scenes.""" +from __future__ import annotations + +from typing import Any + +from pyoverkiz.client import OverkizClient +from pyoverkiz.models import Scenario + +from homeassistant.components.scene import Scene +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import HomeAssistantOverkizData +from .const import DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +): + """Set up the Overkiz scenes from a config entry.""" + data: HomeAssistantOverkizData = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + OverkizScene(scene, data.coordinator.client) + for scene in data.platforms[Platform.SCENE] + ) + + +class OverkizScene(Scene): + """Representation of an Overkiz Scene.""" + + def __init__(self, scenario: Scenario, client: OverkizClient) -> None: + """Initialize the scene.""" + self.scenario = scenario + self.client = client + self._attr_name = self.scenario.label + self._attr_unique_id = self.scenario.oid + + async def async_activate(self, **kwargs: Any) -> None: + """Activate the scene.""" + await self.client.execute_scenario(self.scenario.oid) From 4745e2fb3b9e9f1797015baeef2a6c4d72743be6 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 28 Dec 2021 00:14:11 +0000 Subject: [PATCH 1078/2644] [ci skip] Translation update --- .../binary_sensor/translations/ja.json | 6 ++-- .../components/elmax/translations/de.json | 2 ++ .../components/elmax/translations/en.json | 17 +++------ .../components/hassio/translations/ja.json | 2 +- .../components/iaqualink/translations/de.json | 3 +- .../components/iaqualink/translations/en.json | 3 +- .../components/pvoutput/translations/de.json | 17 +++++++++ .../unifiprotect/translations/ca.json | 3 +- .../unifiprotect/translations/de.json | 3 +- .../unifiprotect/translations/en.json | 3 +- .../unifiprotect/translations/et.json | 35 +++++++++++++++++++ .../unifiprotect/translations/ja.json | 35 +++++++++++++++++++ .../unifiprotect/translations/ru.json | 1 + .../unifiprotect/translations/tr.json | 34 ++++++++++++++++++ .../unifiprotect/translations/zh-Hant.json | 3 +- 15 files changed, 144 insertions(+), 23 deletions(-) create mode 100644 homeassistant/components/pvoutput/translations/de.json create mode 100644 homeassistant/components/unifiprotect/translations/et.json create mode 100644 homeassistant/components/unifiprotect/translations/ja.json create mode 100644 homeassistant/components/unifiprotect/translations/tr.json diff --git a/homeassistant/components/binary_sensor/translations/ja.json b/homeassistant/components/binary_sensor/translations/ja.json index c7a20493152..4931047e4ca 100644 --- a/homeassistant/components/binary_sensor/translations/ja.json +++ b/homeassistant/components/binary_sensor/translations/ja.json @@ -41,7 +41,7 @@ "is_plugged_in": "{entity_name} \u304c\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u3059", "is_powered": "{entity_name} \u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u307e\u3059", "is_present": "{entity_name} \u304c\u5b58\u5728\u3057\u307e\u3059", - "is_problem": "{entity_name} \u304c\u554f\u984c\u3092\u691c\u51fa\u3057\u3066\u3044\u307e\u3059", + "is_problem": "{entity_name} \u304c\u3001\u554f\u984c\u3092\u691c\u51fa\u3057\u3066\u3044\u307e\u3059", "is_running": "{entity_name} \u304c\u5b9f\u884c\u3055\u308c\u3066\u3044\u307e\u3059", "is_smoke": "{entity_name} \u304c\u7159\u3092\u691c\u77e5\u3057\u3066\u3044\u307e\u3059", "is_sound": "{entity_name} \u304c\u97f3\u3092\u691c\u77e5\u3057\u3066\u3044\u307e\u3059", @@ -91,7 +91,7 @@ "plugged_in": "{entity_name} \u304c\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u3059", "powered": "{entity_name} \u96fb\u6e90", "present": "{entity_name} \u304c\u5b58\u5728", - "problem": "{entity_name} \u304c\u554f\u984c\u306e\u691c\u51fa\u3092\u958b\u59cb\u3057\u307e\u3057\u305f", + "problem": "{entity_name} \u304c\u3001\u554f\u984c\u306e\u691c\u51fa\u3092\u958b\u59cb\u3057\u307e\u3057\u305f", "running": "{entity_name} \u306e\u5b9f\u884c\u3092\u958b\u59cb", "smoke": "{entity_name} \u304c\u7159\u306e\u691c\u51fa\u3092\u958b\u59cb\u3057\u307e\u3057\u305f", "sound": "{entity_name} \u304c\u97f3\u306e\u691c\u51fa\u3092\u958b\u59cb\u3057\u307e\u3057\u305f", @@ -186,7 +186,7 @@ "on": "\u30d7\u30e9\u30b0\u30a4\u30f3" }, "presence": { - "off": "\u5916\u51fa", + "off": "\u96e2\u5e2d(away)", "on": "\u5728\u5b85" }, "problem": { diff --git a/homeassistant/components/elmax/translations/de.json b/homeassistant/components/elmax/translations/de.json index aa66cae4b19..241802e82d4 100644 --- a/homeassistant/components/elmax/translations/de.json +++ b/homeassistant/components/elmax/translations/de.json @@ -5,9 +5,11 @@ }, "error": { "bad_auth": "Ung\u00fcltige Authentifizierung", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "invalid_pin": "Die angegebene Pin ist ung\u00fcltig", "network_error": "Ein Netzwerkfehler ist aufgetreten", "no_panel_online": "Es wurde kein Elmax-Bedienfeld gefunden, das online ist.", + "unknown": "Unerwarteter Fehler", "unknown_error": "Ein unerwarteter Fehler ist aufgetreten" }, "step": { diff --git a/homeassistant/components/elmax/translations/en.json b/homeassistant/components/elmax/translations/en.json index 6a73dfa2c07..60da5d88dc3 100644 --- a/homeassistant/components/elmax/translations/en.json +++ b/homeassistant/components/elmax/translations/en.json @@ -1,15 +1,16 @@ { "config": { "abort": { - "single_instance_allowed": "There already is an integration for that Elmaxc panel." + "already_configured": "Device is already configured" }, "error": { + "bad_auth": "Invalid authentication", "invalid_auth": "Invalid authentication", "invalid_pin": "The provided pin is invalid", "network_error": "A network error occurred", "no_panel_online": "No online Elmax control panel was found.", - "reauth_panel_disappeared": "The panel is no longer associated to your account.", - "unknown": "Unexpected error" + "unknown": "Unexpected error", + "unknown_error": "An unexpected error occurred" }, "step": { "panels": { @@ -21,16 +22,6 @@ "description": "Select which panel you would like to control with this integration. Please note that the panel must be ON in order to be configured.", "title": "Panel selection" }, - "reauth_confirm": { - "data": { - "username": "Username", - "password": "Password", - "panel_id": "Panel ID", - "panel_pin": "PIN Code" - }, - "description": "Please authenticate again to the Elmax cloud.", - "title": "Re-Authenticate" - }, "user": { "data": { "password": "Password", diff --git a/homeassistant/components/hassio/translations/ja.json b/homeassistant/components/hassio/translations/ja.json index 0f704ceba19..dbf79471303 100644 --- a/homeassistant/components/hassio/translations/ja.json +++ b/homeassistant/components/hassio/translations/ja.json @@ -5,7 +5,7 @@ "disk_total": "\u30c7\u30a3\u30b9\u30af\u5408\u8a08", "disk_used": "\u4f7f\u7528\u6e08\u307f\u30c7\u30a3\u30b9\u30af", "docker_version": "Docker\u306e\u30d0\u30fc\u30b8\u30e7\u30f3", - "healthy": "\u5143\u6c17", + "healthy": "\u5065\u5168\u6027", "host_os": "\u30db\u30b9\u30c8\u30aa\u30da\u30ec\u30fc\u30c6\u30a3\u30f3\u30b0\u30b7\u30b9\u30c6\u30e0", "installed_addons": "\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u6e08\u307f\u306e\u30a2\u30c9\u30aa\u30f3", "supervisor_api": "Supervisor API", diff --git a/homeassistant/components/iaqualink/translations/de.json b/homeassistant/components/iaqualink/translations/de.json index 0a678baf7ca..79a845ea668 100644 --- a/homeassistant/components/iaqualink/translations/de.json +++ b/homeassistant/components/iaqualink/translations/de.json @@ -4,7 +4,8 @@ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen" + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "step": { "user": { diff --git a/homeassistant/components/iaqualink/translations/en.json b/homeassistant/components/iaqualink/translations/en.json index a2a8719f9b7..c493623398e 100644 --- a/homeassistant/components/iaqualink/translations/en.json +++ b/homeassistant/components/iaqualink/translations/en.json @@ -4,7 +4,8 @@ "single_instance_allowed": "Already configured. Only a single configuration possible." }, "error": { - "cannot_connect": "Failed to connect" + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication" }, "step": { "user": { diff --git a/homeassistant/components/pvoutput/translations/de.json b/homeassistant/components/pvoutput/translations/de.json new file mode 100644 index 00000000000..354e97139d9 --- /dev/null +++ b/homeassistant/components/pvoutput/translations/de.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung" + }, + "step": { + "user": { + "data": { + "api_key": "API-Schl\u00fcssel", + "system_id": "System-ID" + }, + "description": "Um sich bei PVOutput zu authentifizieren, musst du den API-Schl\u00fcssel unter {account_url} abrufen.\n\nDie System-IDs der registrierten Systeme sind auf der gleichen Seite aufgef\u00fchrt." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifiprotect/translations/ca.json b/homeassistant/components/unifiprotect/translations/ca.json index 3e39ebbeac5..d1fea9167d1 100644 --- a/homeassistant/components/unifiprotect/translations/ca.json +++ b/homeassistant/components/unifiprotect/translations/ca.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "protect_version": "La versi\u00f3 m\u00ednima necess\u00e0ria \u00e9s la v1.20.0. Actualitza UniFi Protect i torna-ho a provar.", "unknown": "Error inesperat" }, "step": { @@ -26,7 +27,7 @@ "disable_rtsp": "Desactiva el flux RTSP", "override_connection_host": "Substitueix l'amfitri\u00f3 de connexi\u00f3" }, - "description": "Les m\u00e8triques en temps real nom\u00e9s s'haurien d'activar si has habilitat sensors de diagn\u00f2stic i vols que s'actualitzin en temps real. Si no actives aquesta opci\u00f3, els sensors s'actualitzaran cada 15 minuts.", + "description": "Les m\u00e8triques en temps real nom\u00e9s s'haurien d'activar si has habilitat sensors de diagn\u00f2stic i vols que s'actualitzin en temps real. Si no s'activa aquesta opci\u00f3, els sensors s'actualitzaran cada 15 minuts.", "title": "Opcions d'UniFi Protect" } } diff --git a/homeassistant/components/unifiprotect/translations/de.json b/homeassistant/components/unifiprotect/translations/de.json index 09d9a9e7c8d..3b5194bbd11 100644 --- a/homeassistant/components/unifiprotect/translations/de.json +++ b/homeassistant/components/unifiprotect/translations/de.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", + "protect_version": "Die erforderliche Mindestversion ist v1.20.0. Bitte aktualisiere UniFi Protect und versuche es dann erneut.", "unknown": "Unerwarteter Fehler" }, "step": { @@ -26,7 +27,7 @@ "disable_rtsp": "RTSP-Stream deaktivieren", "override_connection_host": "Verbindungshost \u00fcberschreiben" }, - "description": "Die Option Echtzeitmetriken sollte nur aktiviert werden, wenn du die Diagnosesensoren aktiviert hast und m\u00f6chtest, dass sie in Echtzeit aktualisiert werden. Wenn sie nicht aktiviert sind, werden sie nur alle 15 Minuten aktualisiert.", + "description": "Die Option Echtzeit-Metriken sollte nur aktiviert werden, wenn du die Diagnosesensoren aktiviert hast und diese in Echtzeit aktualisiert werden sollen. Wenn sie nicht aktiviert ist, werden sie nur einmal alle 15 Minuten aktualisiert.", "title": "UniFi Protect-Optionen" } } diff --git a/homeassistant/components/unifiprotect/translations/en.json b/homeassistant/components/unifiprotect/translations/en.json index a28b9af1e32..ec696a123e9 100644 --- a/homeassistant/components/unifiprotect/translations/en.json +++ b/homeassistant/components/unifiprotect/translations/en.json @@ -6,7 +6,8 @@ "error": { "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication", - "protect_version": "Minimum required version is v1.20.0. Please upgrade UniFi Protect and then retry." + "protect_version": "Minimum required version is v1.20.0. Please upgrade UniFi Protect and then retry.", + "unknown": "Unexpected error" }, "step": { "user": { diff --git a/homeassistant/components/unifiprotect/translations/et.json b/homeassistant/components/unifiprotect/translations/et.json new file mode 100644 index 00000000000..b2903f8e2e7 --- /dev/null +++ b/homeassistant/components/unifiprotect/translations/et.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "protect_version": "Minimaalne n\u00f5utav versioon on v1.20.0. Uuenda UniFi Protecti ja proovi seej\u00e4rel uuesti.", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "all_updates": "Reaalajas m\u00f5\u00f5dikud (HOIATUS: suurendab oluliselt CPU kasutust)", + "disable_rtsp": "Keela RTSP voog", + "override_connection_host": "\u00dchenduse hosti alistamine" + }, + "description": "Reaalajas m\u00f5\u00f5dikute valik tuleks lubada ainult siis kui oled diagnostikaandurid sisse l\u00fclitanud ja soovid, et neid uuendatakse reaalajas. Kui see ei ole lubatud, uuendatakse neid ainult \u00fcks kord 15 minuti tagant.", + "title": "UniFi Protecti suvandid" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifiprotect/translations/ja.json b/homeassistant/components/unifiprotect/translations/ja.json new file mode 100644 index 00000000000..e6e0b4e58a0 --- /dev/null +++ b/homeassistant/components/unifiprotect/translations/ja.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "protect_version": "\u6700\u4f4e\u9650\u5fc5\u8981\u306a\u30d0\u30fc\u30b8\u30e7\u30f3\u306fv1.20.0\u3067\u3059\u3002UniFi Protect\u3092\u30a2\u30c3\u30d7\u30b0\u30ec\u30fc\u30c9\u3057\u3066\u304b\u3089\u518d\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "all_updates": "\u30ea\u30a2\u30eb\u30bf\u30a4\u30e0\u30e1\u30c8\u30ea\u30c3\u30af(Realtime metrics)(\u8b66\u544a: CPU\u4f7f\u7528\u7387\u304c\u5927\u5e45\u306b\u5897\u52a0\u3057\u307e\u3059)", + "disable_rtsp": "RTSP\u30b9\u30c8\u30ea\u30fc\u30e0\u3092\u7121\u52b9\u306b\u3059\u308b", + "override_connection_host": "\u63a5\u7d9a\u30db\u30b9\u30c8\u3092\u4e0a\u66f8\u304d" + }, + "description": "\u30ea\u30a2\u30eb\u30bf\u30a4\u30e0\u30e1\u30c8\u30ea\u30c3\u30af \u30aa\u30d7\u30b7\u30e7\u30f3(Realtime metrics option)\u306f\u3001\u8a3a\u65ad\u30bb\u30f3\u30b5\u30fc(diagnostics sensors)\u3092\u6709\u52b9\u306b\u3057\u3066\u3044\u3066\u3001\u30ea\u30a2\u30eb\u30bf\u30a4\u30e0\u3067\u66f4\u65b0\u3057\u305f\u3044\u5834\u5408\u306b\u306e\u307f\u6709\u52b9\u306b\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u6709\u52b9\u306b\u3055\u308c\u3066\u3044\u306a\u3044\u5834\u5408\u306b\u306f\u300115\u5206\u3054\u3068\u306b1\u56de\u3060\u3051\u66f4\u65b0\u3055\u308c\u307e\u3059\u3002", + "title": "UniFi Protect\u30aa\u30d7\u30b7\u30e7\u30f3" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifiprotect/translations/ru.json b/homeassistant/components/unifiprotect/translations/ru.json index 2c5b1b67755..5ba3ed8b026 100644 --- a/homeassistant/components/unifiprotect/translations/ru.json +++ b/homeassistant/components/unifiprotect/translations/ru.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "protect_version": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0432\u0435\u0440\u0441\u0438\u044f 1.20.0 \u0438\u043b\u0438 \u0432\u044b\u0448\u0435. \u041e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 UniFi Protect \u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/unifiprotect/translations/tr.json b/homeassistant/components/unifiprotect/translations/tr.json new file mode 100644 index 00000000000..f642e5d72a0 --- /dev/null +++ b/homeassistant/components/unifiprotect/translations/tr.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "Sunucu", + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "all_updates": "Ger\u00e7ek zamanl\u0131 \u00f6l\u00e7\u00fcmler (UYARI: CPU kullan\u0131m\u0131n\u0131 b\u00fcy\u00fck \u00f6l\u00e7\u00fcde art\u0131r\u0131r)", + "disable_rtsp": "RTSP ak\u0131\u015f\u0131n\u0131 devre d\u0131\u015f\u0131 b\u0131rak\u0131n", + "override_connection_host": "Ba\u011flant\u0131 Ana Bilgisayar\u0131n\u0131 Ge\u00e7ersiz K\u0131l" + }, + "description": "Ger\u00e7ek zamanl\u0131 \u00f6l\u00e7\u00fcmler se\u00e7ene\u011fi, yaln\u0131zca tan\u0131lama sens\u00f6rlerini etkinle\u015ftirdiyseniz ve bunlar\u0131n ger\u00e7ek zamanl\u0131 olarak g\u00fcncellenmesini istiyorsan\u0131z etkinle\u015ftirilmelidir. Etkinle\u015ftirilmemi\u015fse, yaln\u0131zca her 15 dakikada bir g\u00fcncellenirler.", + "title": "UniFi Koruma Se\u00e7enekleri" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifiprotect/translations/zh-Hant.json b/homeassistant/components/unifiprotect/translations/zh-Hant.json index 0c8109602a0..3fc85b46f91 100644 --- a/homeassistant/components/unifiprotect/translations/zh-Hant.json +++ b/homeassistant/components/unifiprotect/translations/zh-Hant.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "protect_version": "\u6240\u9700\u6700\u4f4e\u7248\u672c\u70ba V1.20.0\u3002\u8acb\u66f4\u65b0 UniFi \u76e3\u63a7\u5f8c\u518d\u91cd\u8a66\u4e00\u6b21\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { @@ -27,7 +28,7 @@ "override_connection_host": "\u7f6e\u63db\u9023\u7dda\u4e3b\u6a5f\u7aef" }, "description": "\u50c5\u6709\u7576\u958b\u555f\u8a3a\u65b7\u50b3\u611f\u5668\u3001\u4e26\u9700\u8981\u5373\u6642\u66f4\u65b0\u6642\uff0c\u624d\u5efa\u8b70\u958b\u555f\u5373\u6642\u6307\u6a19\u9078\u9805\u3002\u672a\u555f\u7528\u72c0\u6cc1\u4e0b\u70ba\u6bcf 15 \u5206\u9418\u66f4\u65b0\u4e00\u6b21\u3002", - "title": "UniFi \u4fdd\u8b77\u9078\u9805" + "title": "UniFi \u76e3\u63a7\u9078\u9805" } } } From d63b7bc5f1bc62ffaa82cd54f31d61ae086aadab Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 28 Dec 2021 08:33:40 +0100 Subject: [PATCH 1079/2644] Extract attribute into sensor for PVOutput (#62894) --- homeassistant/components/pvoutput/sensor.py | 52 +++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/homeassistant/components/pvoutput/sensor.py b/homeassistant/components/pvoutput/sensor.py index 5f3765c688e..af89a2b4a0a 100644 --- a/homeassistant/components/pvoutput/sensor.py +++ b/homeassistant/components/pvoutput/sensor.py @@ -20,7 +20,12 @@ from homeassistant.const import ( ATTR_VOLTAGE, CONF_API_KEY, CONF_NAME, + ELECTRIC_POTENTIAL_VOLT, + ENERGY_KILO_WATT_HOUR, ENERGY_WATT_HOUR, + POWER_KILO_WATT, + POWER_WATT, + TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -67,6 +72,14 @@ class PVOutputSensorEntityDescription( SENSORS: tuple[PVOutputSensorEntityDescription, ...] = ( + PVOutputSensorEntityDescription( + key="energy_consumption", + name="Energy Consumed", + native_unit_of_measurement=ENERGY_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda status: status.energy_consumption, + ), PVOutputSensorEntityDescription( key="energy_generation", name="Energy Generated", @@ -75,6 +88,45 @@ SENSORS: tuple[PVOutputSensorEntityDescription, ...] = ( state_class=SensorStateClass.TOTAL_INCREASING, value_fn=lambda status: status.energy_generation, ), + PVOutputSensorEntityDescription( + key="normalized_ouput", + name="Efficiency", + native_unit_of_measurement=f"{ENERGY_KILO_WATT_HOUR}/{POWER_KILO_WATT}", + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda status: status.normalized_ouput, + ), + PVOutputSensorEntityDescription( + key="power_consumption", + name="Power Consumed", + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda status: status.power_consumption, + ), + PVOutputSensorEntityDescription( + key="power_generation", + name="Power Generated", + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda status: status.power_generation, + ), + PVOutputSensorEntityDescription( + key="temperature", + name="Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda status: status.temperature, + ), + PVOutputSensorEntityDescription( + key="voltage", + name="Voltage", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda status: status.voltage, + ), ) From 68acf13f4876527d12ae0947ea583a428348a5e1 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Tue, 28 Dec 2021 07:56:07 +0000 Subject: [PATCH 1080/2644] Add basic type hints to nissan_leaf (#62904) --- .../components/nissan_leaf/binary_sensor.py | 14 ++++++++++++-- homeassistant/components/nissan_leaf/sensor.py | 14 ++++++++++++-- homeassistant/components/nissan_leaf/switch.py | 12 +++++++++++- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/nissan_leaf/binary_sensor.py b/homeassistant/components/nissan_leaf/binary_sensor.py index 5212eec3e82..2fd692f5518 100644 --- a/homeassistant/components/nissan_leaf/binary_sensor.py +++ b/homeassistant/components/nissan_leaf/binary_sensor.py @@ -1,22 +1,32 @@ """Plugged In Status Support for the Nissan Leaf.""" +from __future__ import annotations + import logging from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, ) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import DATA_CHARGING, DATA_LEAF, DATA_PLUGGED_IN, LeafEntity _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): +def setup_platform( + hass: HomeAssistant, + config: ConfigType, + add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Set up of a Nissan Leaf binary sensor.""" if discovery_info is None: return - devices = [] + devices: list[LeafEntity] = [] for vin, datastore in hass.data[DATA_LEAF].items(): _LOGGER.debug("Adding binary_sensors for vin=%s", vin) devices.append(LeafPluggedInSensor(datastore)) diff --git a/homeassistant/components/nissan_leaf/sensor.py b/homeassistant/components/nissan_leaf/sensor.py index 8d43e9bcf85..416684fad5e 100644 --- a/homeassistant/components/nissan_leaf/sensor.py +++ b/homeassistant/components/nissan_leaf/sensor.py @@ -1,9 +1,14 @@ """Battery Charge and Range Support for the Nissan Leaf.""" +from __future__ import annotations + import logging from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.const import PERCENTAGE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.icon import icon_for_battery_level +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.distance import LENGTH_KILOMETERS, LENGTH_MILES from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM @@ -21,12 +26,17 @@ _LOGGER = logging.getLogger(__name__) ICON_RANGE = "mdi:speedometer" -def setup_platform(hass, config, add_devices, discovery_info=None): +def setup_platform( + hass: HomeAssistant, + config: ConfigType, + add_devices: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Sensors setup.""" if discovery_info is None: return - devices = [] + devices: list[LeafEntity] = [] for vin, datastore in hass.data[DATA_LEAF].items(): _LOGGER.debug("Adding sensors for vin=%s", vin) devices.append(LeafBatterySensor(datastore)) diff --git a/homeassistant/components/nissan_leaf/switch.py b/homeassistant/components/nissan_leaf/switch.py index 2b8d557c2dd..ede0ae459c4 100644 --- a/homeassistant/components/nissan_leaf/switch.py +++ b/homeassistant/components/nissan_leaf/switch.py @@ -1,14 +1,24 @@ """Charge and Climate Control Support for the Nissan Leaf.""" +from __future__ import annotations + import logging +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import DATA_CLIMATE, DATA_LEAF, LeafEntity _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_devices, discovery_info=None): +def setup_platform( + hass: HomeAssistant, + config: ConfigType, + add_devices: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Nissan Leaf switch platform setup.""" if discovery_info is None: return From 41c497ee6e4d32e3316af653f5f03b3ba679eb9c Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Tue, 28 Dec 2021 01:10:39 -0800 Subject: [PATCH 1081/2644] Add binary sensor entity to Overkiz integration (#62913) --- .coveragerc | 1 + .../components/overkiz/binary_sensor.py | 149 ++++++++++++++++++ homeassistant/components/overkiz/const.py | 1 + 3 files changed, 151 insertions(+) create mode 100644 homeassistant/components/overkiz/binary_sensor.py diff --git a/.coveragerc b/.coveragerc index 27b4af835d6..07cac3d3da1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -803,6 +803,7 @@ omit = homeassistant/components/osramlightify/light.py homeassistant/components/otp/sensor.py homeassistant/components/overkiz/__init__.py + homeassistant/components/overkiz/binary_sensor.py homeassistant/components/overkiz/button.py homeassistant/components/overkiz/coordinator.py homeassistant/components/overkiz/entity.py diff --git a/homeassistant/components/overkiz/binary_sensor.py b/homeassistant/components/overkiz/binary_sensor.py new file mode 100644 index 00000000000..8f4fc8be948 --- /dev/null +++ b/homeassistant/components/overkiz/binary_sensor.py @@ -0,0 +1,149 @@ +"""Support for Overkiz binary sensors.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from pyoverkiz.enums import OverkizCommandParam, OverkizState + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import HomeAssistantOverkizData +from .const import DOMAIN, IGNORED_OVERKIZ_DEVICES +from .entity import OverkizDescriptiveEntity + + +@dataclass +class OverkizBinarySensorDescriptionMixin: + """Define an entity description mixin for binary sensor entities.""" + + value_fn: Callable[[str], bool] + + +@dataclass +class OverkizBinarySensorDescription( + BinarySensorEntityDescription, OverkizBinarySensorDescriptionMixin +): + """Class to describe an Overkiz binary sensor.""" + + +BINARY_SENSOR_DESCRIPTIONS: list[OverkizBinarySensorDescription] = [ + # RainSensor/RainSensor + OverkizBinarySensorDescription( + key=OverkizState.CORE_RAIN, + name="Rain", + icon="mdi:weather-rainy", + value_fn=lambda state: state == OverkizCommandParam.DETECTED, + ), + # SmokeSensor/SmokeSensor + OverkizBinarySensorDescription( + key=OverkizState.CORE_SMOKE, + name="Smoke", + device_class=BinarySensorDeviceClass.SMOKE, + value_fn=lambda state: state == OverkizCommandParam.DETECTED, + ), + # WaterSensor/WaterDetectionSensor + OverkizBinarySensorDescription( + key=OverkizState.CORE_WATER_DETECTION, + name="Water", + icon="mdi:water", + value_fn=lambda state: state == OverkizCommandParam.DETECTED, + ), + # AirSensor/AirFlowSensor + OverkizBinarySensorDescription( + key=OverkizState.CORE_GAS_DETECTION, + name="Gas", + device_class=BinarySensorDeviceClass.GAS, + value_fn=lambda state: state == OverkizCommandParam.DETECTED, + ), + # OccupancySensor/OccupancySensor + # OccupancySensor/MotionSensor + OverkizBinarySensorDescription( + key=OverkizState.CORE_OCCUPANCY, + name="Occupancy", + device_class=BinarySensorDeviceClass.OCCUPANCY, + value_fn=lambda state: state == OverkizCommandParam.PERSON_INSIDE, + ), + # ContactSensor/WindowWithTiltSensor + OverkizBinarySensorDescription( + key=OverkizState.CORE_VIBRATION, + name="Vibration", + device_class=BinarySensorDeviceClass.VIBRATION, + value_fn=lambda state: state == OverkizCommandParam.DETECTED, + ), + # ContactSensor/ContactSensor + OverkizBinarySensorDescription( + key=OverkizState.CORE_CONTACT, + name="Contact", + device_class=BinarySensorDeviceClass.DOOR, + value_fn=lambda state: state == OverkizCommandParam.OPEN, + ), + # Siren/SirenStatus + OverkizBinarySensorDescription( + key=OverkizState.CORE_ASSEMBLY, + name="Assembly", + device_class=BinarySensorDeviceClass.PROBLEM, + value_fn=lambda state: state == OverkizCommandParam.OPEN, + ), + # Unknown + OverkizBinarySensorDescription( + key=OverkizState.IO_VIBRATION_DETECTED, + name="Vibration", + device_class=BinarySensorDeviceClass.VIBRATION, + value_fn=lambda state: state == OverkizCommandParam.DETECTED, + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +): + """Set up the Overkiz binary sensors from a config entry.""" + data: HomeAssistantOverkizData = hass.data[DOMAIN][entry.entry_id] + entities: list[OverkizBinarySensor] = [] + + key_supported_states = { + description.key: description for description in BINARY_SENSOR_DESCRIPTIONS + } + + for device in data.coordinator.data.values(): + if ( + device.widget in IGNORED_OVERKIZ_DEVICES + or device.ui_class in IGNORED_OVERKIZ_DEVICES + ): + continue + + for state in device.definition.states: + if description := key_supported_states.get(state.qualified_name): + entities.append( + OverkizBinarySensor( + device.device_url, + data.coordinator, + description, + ) + ) + + async_add_entities(entities) + + +class OverkizBinarySensor(OverkizDescriptiveEntity, BinarySensorEntity): + """Representation of an Overkiz Binary Sensor.""" + + entity_description: OverkizBinarySensorDescription + + @property + def is_on(self) -> bool | None: + """Return the state of the sensor.""" + if state := self.device.states.get(self.entity_description.key): + return self.entity_description.value_fn(state.value) + + return None diff --git a/homeassistant/components/overkiz/const.py b/homeassistant/components/overkiz/const.py index 150323c19da..09eccdd30d9 100644 --- a/homeassistant/components/overkiz/const.py +++ b/homeassistant/components/overkiz/const.py @@ -18,6 +18,7 @@ UPDATE_INTERVAL: Final = timedelta(seconds=30) UPDATE_INTERVAL_ALL_ASSUMED_STATE: Final = timedelta(minutes=60) PLATFORMS: list[Platform] = [ + Platform.BINARY_SENSOR, Platform.BUTTON, Platform.LIGHT, Platform.LOCK, From f6c1266af640133fbcc23da98424b7dfaa0e0c75 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 28 Dec 2021 11:38:42 +0100 Subject: [PATCH 1082/2644] Use shorthand attributes in the CPU Speed integration (#62896) --- homeassistant/components/cpuspeed/sensor.py | 32 ++++----------------- 1 file changed, 6 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/cpuspeed/sensor.py b/homeassistant/components/cpuspeed/sensor.py index c34ea939de7..d26e3278396 100644 --- a/homeassistant/components/cpuspeed/sensor.py +++ b/homeassistant/components/cpuspeed/sensor.py @@ -15,8 +15,6 @@ HZ_ADVERTISED = "hz_advertised" DEFAULT_NAME = "CPU speed" -ICON = "mdi:pulse" - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( {vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string} ) @@ -31,27 +29,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class CpuSpeedSensor(SensorEntity): """Representation of a CPU sensor.""" + _attr_native_unit_of_measurement = FREQUENCY_GIGAHERTZ + _attr_icon = "mdi:pulse" + def __init__(self, name): """Initialize the CPU sensor.""" - self._name = name - self._state = None + self._attr_name = name self.info = None - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state - - @property - def native_unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return FREQUENCY_GIGAHERTZ - @property def extra_state_attributes(self): """Return the state attributes.""" @@ -64,15 +49,10 @@ class CpuSpeedSensor(SensorEntity): attrs[ATTR_HZ] = round(self.info[HZ_ADVERTISED][0] / 10 ** 9, 2) return attrs - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return ICON - def update(self): """Get the latest data and updates the state.""" self.info = cpuinfo.get_cpu_info() if HZ_ACTUAL in self.info: - self._state = round(float(self.info[HZ_ACTUAL][0]) / 10 ** 9, 2) + self._attr_native_value = round(float(self.info[HZ_ACTUAL][0]) / 10 ** 9, 2) else: - self._state = None + self._attr_native_value = None From a19c95e4bd41c02f0a5bbca8571dfd1a1133ed41 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Dec 2021 13:10:17 +0100 Subject: [PATCH 1083/2644] Ensure service calls are typed [o-r] (#62920) Co-authored-by: epenet --- .../components/opentherm_gw/__init__.py | 23 ++++++++++--------- homeassistant/components/ozw/services.py | 10 ++++---- homeassistant/components/plex/services.py | 7 +++--- homeassistant/components/ps4/__init__.py | 4 ++-- homeassistant/components/rachio/device.py | 7 +++--- homeassistant/components/rachio/switch.py | 4 ++-- homeassistant/components/recorder/__init__.py | 10 ++++---- homeassistant/components/rest/__init__.py | 4 ++-- .../components/rest_command/__init__.py | 4 ++-- homeassistant/components/rflink/__init__.py | 4 ++-- homeassistant/components/rfxtrx/__init__.py | 4 ++-- homeassistant/components/ring/__init__.py | 4 ++-- 12 files changed, 44 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index 962379729db..dbb7941154c 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -22,6 +22,7 @@ from homeassistant.const import ( PRECISION_WHOLE, Platform, ) +from homeassistant.core import ServiceCall import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import ( async_get_registry as async_get_dev_reg, @@ -246,7 +247,7 @@ def register_services(hass): } ) - async def reset_gateway(call): + async def reset_gateway(call: ServiceCall) -> None: """Reset the OpenTherm Gateway.""" gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][call.data[ATTR_GW_ID]] mode_rst = gw_vars.OTGW_MODE_RESET @@ -258,7 +259,7 @@ def register_services(hass): DOMAIN, SERVICE_RESET_GATEWAY, reset_gateway, service_reset_schema ) - async def set_ch_ovrd(call): + async def set_ch_ovrd(call: ServiceCall) -> None: """Set the central heating override on the OpenTherm Gateway.""" gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][call.data[ATTR_GW_ID]] await gw_dev.gateway.set_ch_enable_bit(1 if call.data[ATTR_CH_OVRD] else 0) @@ -270,7 +271,7 @@ def register_services(hass): service_set_central_heating_ovrd_schema, ) - async def set_control_setpoint(call): + async def set_control_setpoint(call: ServiceCall) -> None: """Set the control setpoint on the OpenTherm Gateway.""" gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][call.data[ATTR_GW_ID]] gw_var = gw_vars.DATA_CONTROL_SETPOINT @@ -285,7 +286,7 @@ def register_services(hass): service_set_control_setpoint_schema, ) - async def set_dhw_ovrd(call): + async def set_dhw_ovrd(call: ServiceCall) -> None: """Set the domestic hot water override on the OpenTherm Gateway.""" gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][call.data[ATTR_GW_ID]] gw_var = gw_vars.OTGW_DHW_OVRD @@ -300,7 +301,7 @@ def register_services(hass): service_set_hot_water_ovrd_schema, ) - async def set_dhw_setpoint(call): + async def set_dhw_setpoint(call: ServiceCall) -> None: """Set the domestic hot water setpoint on the OpenTherm Gateway.""" gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][call.data[ATTR_GW_ID]] gw_var = gw_vars.DATA_DHW_SETPOINT @@ -315,7 +316,7 @@ def register_services(hass): service_set_hot_water_setpoint_schema, ) - async def set_device_clock(call): + async def set_device_clock(call: ServiceCall) -> None: """Set the clock on the OpenTherm Gateway.""" gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][call.data[ATTR_GW_ID]] attr_date = call.data[ATTR_DATE] @@ -326,7 +327,7 @@ def register_services(hass): DOMAIN, SERVICE_SET_CLOCK, set_device_clock, service_set_clock_schema ) - async def set_gpio_mode(call): + async def set_gpio_mode(call: ServiceCall) -> None: """Set the OpenTherm Gateway GPIO modes.""" gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][call.data[ATTR_GW_ID]] gpio_id = call.data[ATTR_ID] @@ -340,7 +341,7 @@ def register_services(hass): DOMAIN, SERVICE_SET_GPIO_MODE, set_gpio_mode, service_set_gpio_mode_schema ) - async def set_led_mode(call): + async def set_led_mode(call: ServiceCall) -> None: """Set the OpenTherm Gateway LED modes.""" gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][call.data[ATTR_GW_ID]] led_id = call.data[ATTR_ID] @@ -354,7 +355,7 @@ def register_services(hass): DOMAIN, SERVICE_SET_LED_MODE, set_led_mode, service_set_led_mode_schema ) - async def set_max_mod(call): + async def set_max_mod(call: ServiceCall) -> None: """Set the max modulation level.""" gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][call.data[ATTR_GW_ID]] gw_var = gw_vars.DATA_SLAVE_MAX_RELATIVE_MOD @@ -370,7 +371,7 @@ def register_services(hass): DOMAIN, SERVICE_SET_MAX_MOD, set_max_mod, service_set_max_mod_schema ) - async def set_outside_temp(call): + async def set_outside_temp(call: ServiceCall) -> None: """Provide the outside temperature to the OpenTherm Gateway.""" gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][call.data[ATTR_GW_ID]] gw_var = gw_vars.DATA_OUTSIDE_TEMP @@ -382,7 +383,7 @@ def register_services(hass): DOMAIN, SERVICE_SET_OAT, set_outside_temp, service_set_oat_schema ) - async def set_setback_temp(call): + async def set_setback_temp(call: ServiceCall) -> None: """Set the OpenTherm Gateway SetBack temperature.""" gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][call.data[ATTR_GW_ID]] gw_var = gw_vars.OTGW_SB_TEMP diff --git a/homeassistant/components/ozw/services.py b/homeassistant/components/ozw/services.py index e1f71e636b3..b6e54b4baa1 100644 --- a/homeassistant/components/ozw/services.py +++ b/homeassistant/components/ozw/services.py @@ -5,7 +5,7 @@ from openzwavemqtt.const import ATTR_LABEL, ATTR_POSITION, ATTR_VALUE from openzwavemqtt.util.node import get_node_from_manager, set_config_parameter import voluptuous as vol -from homeassistant.core import callback +from homeassistant.core import ServiceCall, callback import homeassistant.helpers.config_validation as cv from . import const @@ -86,7 +86,7 @@ class ZWaveServices: ) @callback - def async_set_config_parameter(self, service): + def async_set_config_parameter(self, service: ServiceCall) -> None: """Set a config parameter to a node.""" instance_id = service.data[const.ATTR_INSTANCE_ID] node_id = service.data[const.ATTR_NODE_ID] @@ -106,7 +106,7 @@ class ZWaveServices: ) @callback - def async_add_node(self, service): + def async_add_node(self, service: ServiceCall) -> None: """Enter inclusion mode on the controller.""" instance_id = service.data[const.ATTR_INSTANCE_ID] secure = service.data[const.ATTR_SECURE] @@ -116,7 +116,7 @@ class ZWaveServices: instance.add_node(secure) @callback - def async_remove_node(self, service): + def async_remove_node(self, service: ServiceCall) -> None: """Enter exclusion mode on the controller.""" instance_id = service.data[const.ATTR_INSTANCE_ID] instance = self._manager.get_instance(instance_id) @@ -125,7 +125,7 @@ class ZWaveServices: instance.remove_node() @callback - def async_cancel_command(self, service): + def async_cancel_command(self, service: ServiceCall) -> None: """Tell the controller to cancel an add or remove command.""" instance_id = service.data[const.ATTR_INSTANCE_ID] instance = self._manager.get_instance(instance_id) diff --git a/homeassistant/components/plex/services.py b/homeassistant/components/plex/services.py index e07d94f5a1f..24cd26e0651 100644 --- a/homeassistant/components/plex/services.py +++ b/homeassistant/components/plex/services.py @@ -5,6 +5,7 @@ import logging from plexapi.exceptions import BadRequest, NotFound import voluptuous as vol +from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -26,10 +27,10 @@ _LOGGER = logging.getLogger(__package__) async def async_setup_services(hass): """Set up services for the Plex component.""" - async def async_refresh_library_service(service_call): + async def async_refresh_library_service(service_call: ServiceCall) -> None: await hass.async_add_executor_job(refresh_library, hass, service_call) - async def async_scan_clients_service(_): + async def async_scan_clients_service(_: ServiceCall) -> None: _LOGGER.debug("Scanning for new Plex clients") for server_id in hass.data[DOMAIN][SERVERS]: async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) @@ -47,7 +48,7 @@ async def async_setup_services(hass): return True -def refresh_library(hass, service_call): +def refresh_library(hass: HomeAssistant, service_call: ServiceCall) -> None: """Scan a Plex library for new and updated media.""" plex_server_name = service_call.data.get("server_name") library_name = service_call.data["library_name"] diff --git a/homeassistant/components/ps4/__init__.py b/homeassistant/components/ps4/__init__.py index caab9e1dd26..00310d52735 100644 --- a/homeassistant/components/ps4/__init__.py +++ b/homeassistant/components/ps4/__init__.py @@ -19,7 +19,7 @@ from homeassistant.const import ( CONF_TOKEN, Platform, ) -from homeassistant.core import HomeAssistant, split_entity_id +from homeassistant.core import HomeAssistant, ServiceCall, split_entity_id from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_registry from homeassistant.util import location @@ -217,7 +217,7 @@ def _reformat_data(hass: HomeAssistant, games: dict, unique_id: str) -> dict: def service_handle(hass: HomeAssistant): """Handle for services.""" - async def async_service_command(call): + async def async_service_command(call: ServiceCall) -> None: """Service for sending commands.""" entity_ids = call.data[ATTR_ENTITY_ID] command = call.data[ATTR_COMMAND] diff --git a/homeassistant/components/rachio/device.py b/homeassistant/components/rachio/device.py index 2124e5736a2..ff7c0535295 100644 --- a/homeassistant/components/rachio/device.py +++ b/homeassistant/components/rachio/device.py @@ -7,6 +7,7 @@ import logging import voluptuous as vol from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import ServiceCall from homeassistant.helpers import config_validation as cv from .const import ( @@ -73,7 +74,7 @@ class RachioPerson: all_devices = [rachio_iro.name for rachio_iro in self._controllers] - def pause_water(service): + def pause_water(service: ServiceCall) -> None: """Service to pause watering on all or specific controllers.""" duration = service.data[ATTR_DURATION] devices = service.data.get(ATTR_DEVICES, all_devices) @@ -81,14 +82,14 @@ class RachioPerson: if iro.name in devices: iro.pause_watering(duration) - def resume_water(service): + def resume_water(service: ServiceCall) -> None: """Service to resume watering on all or specific controllers.""" devices = service.data.get(ATTR_DEVICES, all_devices) for iro in self._controllers: if iro.name in devices: iro.resume_watering() - def stop_water(service): + def stop_water(service: ServiceCall) -> None: """Service to stop watering on all or specific controllers.""" devices = service.data.get(ATTR_DEVICES, all_devices) for iro in self._controllers: diff --git a/homeassistant/components/rachio/switch.py b/homeassistant/components/rachio/switch.py index de897cb7f07..0108362f168 100644 --- a/homeassistant/components/rachio/switch.py +++ b/homeassistant/components/rachio/switch.py @@ -8,7 +8,7 @@ import voluptuous as vol from homeassistant.components.switch import SwitchEntity from homeassistant.const import ATTR_ENTITY_ID, ATTR_ID -from homeassistant.core import callback +from homeassistant.core import ServiceCall, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -102,7 +102,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities) _LOGGER.info("%d Rachio switch(es) added", len(entities)) - def start_multiple(service): + def start_multiple(service: ServiceCall) -> None: """Service to start multiple zones in sequence.""" zones_list = [] person = hass.data[DOMAIN_RACHIO][config_entry.entry_id] diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index f9dde1f17c9..1589e767fa7 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -32,7 +32,7 @@ from homeassistant.const import ( EVENT_TIME_CHANGED, MATCH_ALL, ) -from homeassistant.core import CoreState, HomeAssistant, callback +from homeassistant.core import CoreState, HomeAssistant, ServiceCall, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import ( INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA, @@ -284,7 +284,7 @@ async def _process_recorder_platform(hass, domain, platform): def _async_register_services(hass, instance): """Register recorder services.""" - async def async_handle_purge_service(service): + async def async_handle_purge_service(service: ServiceCall) -> None: """Handle calls to the purge service.""" instance.do_adhoc_purge(**service.data) @@ -292,7 +292,7 @@ def _async_register_services(hass, instance): DOMAIN, SERVICE_PURGE, async_handle_purge_service, schema=SERVICE_PURGE_SCHEMA ) - async def async_handle_purge_entities_service(service): + async def async_handle_purge_entities_service(service: ServiceCall) -> None: """Handle calls to the purge entities service.""" entity_ids = await async_extract_entity_ids(hass, service) domains = service.data.get(ATTR_DOMAINS, []) @@ -307,7 +307,7 @@ def _async_register_services(hass, instance): schema=SERVICE_PURGE_ENTITIES_SCHEMA, ) - async def async_handle_enable_service(service): + async def async_handle_enable_service(service: ServiceCall) -> None: instance.set_enable(True) hass.services.async_register( @@ -317,7 +317,7 @@ def _async_register_services(hass, instance): schema=SERVICE_ENABLE_SCHEMA, ) - async def async_handle_disable_service(service): + async def async_handle_disable_service(service: ServiceCall) -> None: instance.set_enable(False) hass.services.async_register( diff --git a/homeassistant/components/rest/__init__.py b/homeassistant/components/rest/__init__.py index b55d9c6d844..fc07d5c9b67 100644 --- a/homeassistant/components/rest/__init__.py +++ b/homeassistant/components/rest/__init__.py @@ -24,7 +24,7 @@ from homeassistant.const import ( HTTP_DIGEST_AUTHENTICATION, SERVICE_RELOAD, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import discovery, template from homeassistant.helpers.entity_component import ( DEFAULT_SCAN_INTERVAL, @@ -49,7 +49,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: component = EntityComponent(_LOGGER, DOMAIN, hass) _async_setup_shared_data(hass) - async def reload_service_handler(service): + async def reload_service_handler(service: ServiceCall) -> None: """Remove all user-defined groups and load new ones from config.""" if (conf := await component.async_prepare_reload()) is None: return diff --git a/homeassistant/components/rest_command/__init__.py b/homeassistant/components/rest_command/__init__.py index 9f792d5c1a2..43754358a78 100644 --- a/homeassistant/components/rest_command/__init__.py +++ b/homeassistant/components/rest_command/__init__.py @@ -17,7 +17,7 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, ) -from homeassistant.core import callback +from homeassistant.core import ServiceCall, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -88,7 +88,7 @@ async def async_setup(hass, config): if CONF_CONTENT_TYPE in command_config: content_type = command_config[CONF_CONTENT_TYPE] - async def async_service_handler(service): + async def async_service_handler(service: ServiceCall) -> None: """Execute a shell command service.""" payload = None if template_payload: diff --git a/homeassistant/components/rflink/__init__.py b/homeassistant/components/rflink/__init__.py index 6a59212d6c1..7092cf1c811 100644 --- a/homeassistant/components/rflink/__init__.py +++ b/homeassistant/components/rflink/__init__.py @@ -18,7 +18,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, STATE_ON, ) -from homeassistant.core import CoreState, callback +from homeassistant.core import CoreState, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( @@ -134,7 +134,7 @@ async def async_setup(hass, config): # Allow platform to specify function to register new unknown devices hass.data[DATA_DEVICE_REGISTER] = {} - async def async_send_command(call): + async def async_send_command(call: ServiceCall) -> None: """Send Rflink command.""" _LOGGER.debug("Rflink command for %s", str(call.data)) if not ( diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index c93e686701f..10be57fd247 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -24,7 +24,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, Platform, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, ServiceCall, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceRegistry from homeassistant.helpers.entity import DeviceInfo, Entity @@ -234,7 +234,7 @@ async def async_setup_internal(hass, entry: config_entries.ConfigEntry): rfx_object.event_callback = lambda event: hass.add_job(async_handle_receive, event) - def send(call): + def send(call: ServiceCall) -> None: event = call.data[ATTR_EVENT] rfx_object.transport.send(event) diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py index db383b9228f..2e84a00e378 100644 --- a/homeassistant/components/ring/__init__.py +++ b/homeassistant/components/ring/__init__.py @@ -12,7 +12,7 @@ import requests from ring_doorbell import Auth, Ring from homeassistant.const import Platform, __version__ -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers.event import async_track_time_interval from homeassistant.util.async_ import run_callback_threadsafe @@ -111,7 +111,7 @@ async def async_setup_entry(hass, entry): if hass.services.has_service(DOMAIN, "update"): return True - async def async_refresh_all(_): + async def async_refresh_all(_: ServiceCall) -> None: """Refresh all ring data.""" for info in hass.data[DOMAIN].values(): await info["device_data"].async_refresh_all() From 05ac2d4c3ab9675a4ff4ee7c84be8f82b7724fe9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Dec 2021 13:12:06 +0100 Subject: [PATCH 1084/2644] Ensure service calls are typed [v-z] (#62923) Co-authored-by: epenet --- homeassistant/components/velux/__init__.py | 4 ++-- homeassistant/components/vesync/__init__.py | 3 ++- homeassistant/components/wake_on_lan/__init__.py | 4 ++-- homeassistant/components/webostv/__init__.py | 5 +++-- homeassistant/components/zwave/__init__.py | 6 ++++-- homeassistant/components/zwave/lock.py | 8 ++++---- 6 files changed, 17 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/velux/__init__.py b/homeassistant/components/velux/__init__.py index 6783c71b3cd..e763a7003d5 100644 --- a/homeassistant/components/velux/__init__.py +++ b/homeassistant/components/velux/__init__.py @@ -5,7 +5,7 @@ from pyvlx import PyVLX, PyVLXException import voluptuous as vol from homeassistant.const import CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import callback +from homeassistant.core import ServiceCall, callback from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -60,7 +60,7 @@ class VeluxModule: _LOGGER.debug("Velux interface terminated") await self.pyvlx.disconnect() - async def async_reboot_gateway(service_call): + async def async_reboot_gateway(service_call: ServiceCall) -> None: await self.pyvlx.reboot_gateway() self._hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) diff --git a/homeassistant/components/vesync/__init__.py b/homeassistant/components/vesync/__init__.py index e62436fce27..41de93da092 100644 --- a/homeassistant/components/vesync/__init__.py +++ b/homeassistant/components/vesync/__init__.py @@ -4,6 +4,7 @@ import logging from pyvesync import VeSync from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform +from homeassistant.core import ServiceCall from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -66,7 +67,7 @@ async def async_setup_entry(hass, config_entry): lights.extend(device_dict[VS_LIGHTS]) hass.async_create_task(forward_setup(config_entry, Platform.LIGHT)) - async def async_new_device_discovery(service): + async def async_new_device_discovery(service: ServiceCall) -> None: """Discover if new devices should be added.""" manager = hass.data[DOMAIN][VS_MANAGER] switches = hass.data[DOMAIN][VS_SWITCHES] diff --git a/homeassistant/components/wake_on_lan/__init__.py b/homeassistant/components/wake_on_lan/__init__.py index e788f6c0124..aae640381a2 100644 --- a/homeassistant/components/wake_on_lan/__init__.py +++ b/homeassistant/components/wake_on_lan/__init__.py @@ -6,7 +6,7 @@ import voluptuous as vol import wakeonlan from homeassistant.const import CONF_BROADCAST_ADDRESS, CONF_BROADCAST_PORT, CONF_MAC -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType @@ -28,7 +28,7 @@ WAKE_ON_LAN_SEND_MAGIC_PACKET_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the wake on LAN component.""" - async def send_magic_packet(call): + async def send_magic_packet(call: ServiceCall) -> None: """Send magic packet to wake up a device.""" mac_address = call.data.get(CONF_MAC) broadcast_address = call.data.get(CONF_BROADCAST_ADDRESS) diff --git a/homeassistant/components/webostv/__init__.py b/homeassistant/components/webostv/__init__.py index db5a618ff5c..3675e33bf39 100644 --- a/homeassistant/components/webostv/__init__.py +++ b/homeassistant/components/webostv/__init__.py @@ -19,6 +19,7 @@ from homeassistant.const import ( CONF_NAME, EVENT_HOMEASSISTANT_STOP, ) +from homeassistant.core import ServiceCall import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -86,8 +87,8 @@ async def async_setup(hass, config): """Set up the LG WebOS TV platform.""" hass.data[DOMAIN] = {} - async def async_service_handler(service): - method = SERVICE_TO_METHOD.get(service.service) + async def async_service_handler(service: ServiceCall) -> None: + method = SERVICE_TO_METHOD[service.service] data = service.data.copy() data["method"] = method["method"] async_dispatcher_send(hass, DOMAIN, data) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index cd15f632d0b..99548eff53e 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -1,5 +1,7 @@ """Support for Z-Wave.""" # pylint: disable=import-outside-toplevel +from __future__ import annotations + import asyncio import copy from importlib import import_module @@ -17,7 +19,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, Platform, ) -from homeassistant.core import CoreState, HomeAssistant, callback +from homeassistant.core import CoreState, Event, HomeAssistant, ServiceCall, callback from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import ( @@ -876,7 +878,7 @@ async def async_setup_entry( # noqa: C901 _LOGGER.info("Sending %s test-messages to node %s", messages, node_id) node.test(messages) - def start_zwave(_service_or_event): + def start_zwave(_service_or_event: ServiceCall | Event) -> None: """Startup Z-Wave network.""" _LOGGER.info("Starting Z-Wave network") network.start() diff --git a/homeassistant/components/zwave/lock.py b/homeassistant/components/zwave/lock.py index bc49f9c0bd2..81e7c066b5d 100644 --- a/homeassistant/components/zwave/lock.py +++ b/homeassistant/components/zwave/lock.py @@ -4,7 +4,7 @@ import logging import voluptuous as vol from homeassistant.components.lock import DOMAIN, LockEntity -from homeassistant.core import callback +from homeassistant.core import ServiceCall, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -169,7 +169,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): network = hass.data[const.DATA_NETWORK] - def set_usercode(service): + def set_usercode(service: ServiceCall) -> None: """Set the usercode to index X on the lock.""" node_id = service.data.get(const.ATTR_NODE_ID) lock_node = network.nodes[node_id] @@ -193,7 +193,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): value.data = str(usercode) break - def get_usercode(service): + def get_usercode(service: ServiceCall) -> None: """Get a usercode at index X on the lock.""" node_id = service.data.get(const.ATTR_NODE_ID) lock_node = network.nodes[node_id] @@ -207,7 +207,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): _LOGGER.info("Usercode at slot %s is: %s", value.index, value.data) break - def clear_usercode(service): + def clear_usercode(service: ServiceCall) -> None: """Set usercode to slot X on the lock.""" node_id = service.data.get(const.ATTR_NODE_ID) lock_node = network.nodes[node_id] From 7b5a159899fbc3aa323468ffa65335f6c292b751 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 28 Dec 2021 13:19:36 +0100 Subject: [PATCH 1085/2644] Add strict typing to CPU Speed (#62924) --- .strict-typing | 1 + homeassistant/components/cpuspeed/sensor.py | 44 ++++++++++++++------- mypy.ini | 11 ++++++ 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/.strict-typing b/.strict-typing index cc64f5ca5b0..3117c6ed908 100644 --- a/.strict-typing +++ b/.strict-typing @@ -31,6 +31,7 @@ homeassistant.components.camera.* homeassistant.components.canary.* homeassistant.components.cover.* homeassistant.components.crownstone.* +homeassistant.components.cpuspeed.* homeassistant.components.device_automation.* homeassistant.components.device_tracker.* homeassistant.components.devolo_home_control.* diff --git a/homeassistant/components/cpuspeed/sensor.py b/homeassistant/components/cpuspeed/sensor.py index d26e3278396..e832d35fbd8 100644 --- a/homeassistant/components/cpuspeed/sensor.py +++ b/homeassistant/components/cpuspeed/sensor.py @@ -1,10 +1,17 @@ """Support for displaying the current CPU speed.""" +from __future__ import annotations + +from typing import Any + from cpuinfo import cpuinfo import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_NAME, FREQUENCY_GIGAHERTZ +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType ATTR_BRAND = "brand" ATTR_HZ = "ghz_advertised" @@ -20,10 +27,15 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Set up the CPU speed sensor.""" name = config[CONF_NAME] - add_entities([CpuSpeedSensor(name)], True) + async_add_entities([CpuSpeedSensor(name)], True) class CpuSpeedSensor(SensorEntity): @@ -32,27 +44,29 @@ class CpuSpeedSensor(SensorEntity): _attr_native_unit_of_measurement = FREQUENCY_GIGAHERTZ _attr_icon = "mdi:pulse" - def __init__(self, name): + def __init__(self, name: str) -> None: """Initialize the CPU sensor.""" self._attr_name = name - self.info = None + self.info: dict[str, Any] | None = None @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, float | str | None] | None: """Return the state attributes.""" - if self.info is not None: - attrs = { - ATTR_ARCH: self.info["arch_string_raw"], - ATTR_BRAND: self.info["brand_raw"], - } - if HZ_ADVERTISED in self.info: - attrs[ATTR_HZ] = round(self.info[HZ_ADVERTISED][0] / 10 ** 9, 2) - return attrs + if self.info is None: + return None - def update(self): + attrs = { + ATTR_ARCH: self.info["arch_string_raw"], + ATTR_BRAND: self.info["brand_raw"], + } + if HZ_ADVERTISED in self.info: + attrs[ATTR_HZ] = round(self.info[HZ_ADVERTISED][0] / 10 ** 9, 2) + return attrs + + def update(self) -> None: """Get the latest data and updates the state.""" self.info = cpuinfo.get_cpu_info() - if HZ_ACTUAL in self.info: + if self.info is not None and HZ_ACTUAL in self.info: self._attr_native_value = round(float(self.info[HZ_ACTUAL][0]) / 10 ** 9, 2) else: self._attr_native_value = None diff --git a/mypy.ini b/mypy.ini index 4cfa08ca38a..53b022dd98c 100644 --- a/mypy.ini +++ b/mypy.ini @@ -352,6 +352,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.cpuspeed.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.device_automation.*] check_untyped_defs = true disallow_incomplete_defs = true From fb272f58fb777496bc6bd129c61199d71ed9fcc6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Dec 2021 14:19:14 +0100 Subject: [PATCH 1086/2644] Ensure service calls are typed [h-i] (#62914) Co-authored-by: epenet --- homeassistant/components/habitica/__init__.py | 4 +-- .../components/hangouts/hangouts_bot.py | 12 +++++--- homeassistant/components/hassio/__init__.py | 11 ++++++-- homeassistant/components/heos/services.py | 6 ++-- homeassistant/components/homekit/__init__.py | 6 ++-- .../components/homematicip_cloud/services.py | 2 +- homeassistant/components/html5/notify.py | 3 +- homeassistant/components/hue/services.py | 2 +- homeassistant/components/icloud/__init__.py | 12 ++++---- homeassistant/components/ifttt/__init__.py | 3 +- .../components/image_processing/__init__.py | 4 +-- homeassistant/components/insteon/utils.py | 28 +++++++++---------- homeassistant/components/iperf3/__init__.py | 3 +- homeassistant/components/isy994/services.py | 22 +++++++-------- 14 files changed, 65 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py index af67178185d..3cadd6897d2 100644 --- a/homeassistant/components/habitica/__init__.py +++ b/homeassistant/components/habitica/__init__.py @@ -14,7 +14,7 @@ from homeassistant.const import ( CONF_URL, Platform, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import ConfigType @@ -111,7 +111,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: def __call__(self, **kwargs): return super().__call__(websession, **kwargs) - async def handle_api_call(call): + async def handle_api_call(call: ServiceCall) -> None: name = call.data[ATTR_NAME] path = call.data[ATTR_PATH] entries = hass.config_entries.async_entries(DOMAIN) diff --git a/homeassistant/components/hangouts/hangouts_bot.py b/homeassistant/components/hangouts/hangouts_bot.py index 5c0625411ae..c29fd9b2cdf 100644 --- a/homeassistant/components/hangouts/hangouts_bot.py +++ b/homeassistant/components/hangouts/hangouts_bot.py @@ -1,4 +1,6 @@ """The Hangouts Bot.""" +from __future__ import annotations + import asyncio from contextlib import suppress from http import HTTPStatus @@ -9,7 +11,7 @@ import aiohttp import hangups from hangups import ChatMessageEvent, ChatMessageSegment, Client, get_auth, hangouts_pb2 -from homeassistant.core import callback +from homeassistant.core import ServiceCall, callback from homeassistant.helpers import dispatcher, intent from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -326,7 +328,7 @@ class HangoutsBot: self.hass, EVENT_HANGOUTS_CONVERSATIONS_CHANGED, conversations ) - async def async_handle_send_message(self, service): + async def async_handle_send_message(self, service: ServiceCall) -> None: """Handle the send_message service.""" await self._async_send_message( service.data[ATTR_MESSAGE], @@ -334,11 +336,13 @@ class HangoutsBot: service.data.get(ATTR_DATA, {}), ) - async def async_handle_update_users_and_conversations(self, _=None): + async def async_handle_update_users_and_conversations( + self, service: ServiceCall | None = None + ) -> None: """Handle the update_users_and_conversations service.""" await self._async_list_conversations() - async def async_handle_reconnect(self, _=None): + async def async_handle_reconnect(self, service: ServiceCall | None = None) -> None: """Handle the reconnect service.""" await self.async_disconnect() await self.async_connect() diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 78927d2f322..15d0a885d60 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -25,7 +25,12 @@ from homeassistant.const import ( SERVICE_HOMEASSISTANT_STOP, Platform, ) -from homeassistant.core import DOMAIN as HASS_DOMAIN, HomeAssistant, callback +from homeassistant.core import ( + DOMAIN as HASS_DOMAIN, + HomeAssistant, + ServiceCall, + callback, +) from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, recorder from homeassistant.helpers.device_registry import ( @@ -488,7 +493,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: await push_config(None) - async def async_service_handler(service): + async def async_service_handler(service: ServiceCall) -> None: """Handle service calls for Hass.io.""" api_endpoint = MAP_SERVICE_API[service.service] @@ -566,7 +571,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: # Fetch data await update_info_data(None) - async def async_handle_core_service(call): + async def async_handle_core_service(call: ServiceCall) -> None: """Service handler for handling core services.""" if call.service in SHUTDOWN_SERVICES and recorder.async_migration_in_progress( hass diff --git a/homeassistant/components/heos/services.py b/homeassistant/components/heos/services.py index 68328f3e1a2..9331f786f9d 100644 --- a/homeassistant/components/heos/services.py +++ b/homeassistant/components/heos/services.py @@ -5,7 +5,7 @@ import logging from pyheos import CommandFailedError, Heos, HeosError, const import voluptuous as vol -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv from .const import ( @@ -47,7 +47,7 @@ def remove(hass: HomeAssistant): hass.services.async_remove(DOMAIN, SERVICE_SIGN_OUT) -async def _sign_in_handler(controller, service): +async def _sign_in_handler(controller: Heos, service: ServiceCall) -> None: """Sign in to the HEOS account.""" if controller.connection_state != const.STATE_CONNECTED: _LOGGER.error("Unable to sign in because HEOS is not connected") @@ -62,7 +62,7 @@ async def _sign_in_handler(controller, service): _LOGGER.error("Unable to sign in: %s", err) -async def _sign_out_handler(controller, service): +async def _sign_out_handler(controller: Heos, service: ServiceCall) -> None: """Sign out of the HEOS account.""" if controller.connection_state != const.STATE_CONNECTED: _LOGGER.error("Unable to sign out because HEOS is not connected") diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 02232aee1e8..55781fdb1c6 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -37,7 +37,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, SERVICE_RELOAD, ) -from homeassistant.core import CoreState, HomeAssistant, callback +from homeassistant.core import CoreState, HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError, Unauthorized from homeassistant.helpers import device_registry, entity_registry import homeassistant.helpers.config_validation as cv @@ -365,7 +365,7 @@ def _async_register_events_and_services(hass: HomeAssistant): """Register events and services for HomeKit.""" hass.http.register_view(HomeKitPairingQRView) - async def async_handle_homekit_reset_accessory(service): + async def async_handle_homekit_reset_accessory(service: ServiceCall) -> None: """Handle reset accessory HomeKit service call.""" for homekit in _async_all_homekit_instances(hass): if homekit.status != STATUS_RUNNING: @@ -385,7 +385,7 @@ def _async_register_events_and_services(hass: HomeAssistant): schema=RESET_ACCESSORY_SERVICE_SCHEMA, ) - async def async_handle_homekit_unpair(service): + async def async_handle_homekit_unpair(service: ServiceCall) -> None: """Handle unpair HomeKit service call.""" referenced = async_extract_referenced_entity_ids(hass, service) dev_reg = device_registry.async_get(hass) diff --git a/homeassistant/components/homematicip_cloud/services.py b/homeassistant/components/homematicip_cloud/services.py index 88c14c648d8..a8393ff88ac 100644 --- a/homeassistant/components/homematicip_cloud/services.py +++ b/homeassistant/components/homematicip_cloud/services.py @@ -114,7 +114,7 @@ async def async_setup_services(hass: HomeAssistant) -> None: return @verify_domain_control(hass, HMIPC_DOMAIN) - async def async_call_hmipc_service(service: ServiceCall): + async def async_call_hmipc_service(service: ServiceCall) -> None: """Call correct HomematicIP Cloud service.""" service_name = service.service diff --git a/homeassistant/components/html5/notify.py b/homeassistant/components/html5/notify.py index 6c3a10757bb..92663bcdf90 100644 --- a/homeassistant/components/html5/notify.py +++ b/homeassistant/components/html5/notify.py @@ -28,6 +28,7 @@ from homeassistant.components.notify import ( BaseNotificationService, ) from homeassistant.const import ATTR_NAME, URL_ROOT +from homeassistant.core import ServiceCall from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.util import ensure_unique_string @@ -405,7 +406,7 @@ class HTML5NotificationService(BaseNotificationService): self.registrations = registrations self.registrations_json_path = json_path - async def async_dismiss_message(service): + async def async_dismiss_message(service: ServiceCall) -> None: """Handle dismissing notification message service calls.""" kwargs = {} diff --git a/homeassistant/components/hue/services.py b/homeassistant/components/hue/services.py index 6e68bbffbb8..652ec52ebed 100644 --- a/homeassistant/components/hue/services.py +++ b/homeassistant/components/hue/services.py @@ -27,7 +27,7 @@ LOGGER = logging.getLogger(__name__) def async_register_services(hass: HomeAssistant) -> None: """Register services for Hue integration.""" - async def hue_activate_scene(call: ServiceCall, skip_reload=True): + async def hue_activate_scene(call: ServiceCall, skip_reload=True) -> None: """Handle activation of Hue scene.""" # Get parameters group_name = call.data[ATTR_GROUP_NAME] diff --git a/homeassistant/components/icloud/__init__.py b/homeassistant/components/icloud/__init__.py index 7054a0f0ad1..5340ff8a4fb 100644 --- a/homeassistant/components/icloud/__init__.py +++ b/homeassistant/components/icloud/__init__.py @@ -4,9 +4,9 @@ import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ConfigType, ServiceDataType +from homeassistant.helpers.typing import ConfigType from homeassistant.util import slugify from .account import IcloudAccount @@ -134,7 +134,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_setup_platforms(entry, PLATFORMS) - def play_sound(service: ServiceDataType) -> None: + def play_sound(service: ServiceCall) -> None: """Play sound on the device.""" account = service.data[ATTR_ACCOUNT] device_name = service.data.get(ATTR_DEVICE_NAME) @@ -143,7 +143,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for device in _get_account(account).get_devices_with_name(device_name): device.play_sound() - def display_message(service: ServiceDataType) -> None: + def display_message(service: ServiceCall) -> None: """Display a message on the device.""" account = service.data[ATTR_ACCOUNT] device_name = service.data.get(ATTR_DEVICE_NAME) @@ -154,7 +154,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for device in _get_account(account).get_devices_with_name(device_name): device.display_message(message, sound) - def lost_device(service: ServiceDataType) -> None: + def lost_device(service: ServiceCall) -> None: """Make the device in lost state.""" account = service.data[ATTR_ACCOUNT] device_name = service.data.get(ATTR_DEVICE_NAME) @@ -165,7 +165,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for device in _get_account(account).get_devices_with_name(device_name): device.lost_device(number, message) - def update_account(service: ServiceDataType) -> None: + def update_account(service: ServiceCall) -> None: """Call the update function of an iCloud account.""" if (account := service.data.get(ATTR_ACCOUNT)) is None: for account in hass.data[DOMAIN].values(): diff --git a/homeassistant/components/ifttt/__init__.py b/homeassistant/components/ifttt/__init__.py index 68d8159b6e4..a9c682d40ad 100644 --- a/homeassistant/components/ifttt/__init__.py +++ b/homeassistant/components/ifttt/__init__.py @@ -8,6 +8,7 @@ import requests import voluptuous as vol from homeassistant.const import CONF_WEBHOOK_ID +from homeassistant.core import ServiceCall from homeassistant.helpers import config_entry_flow import homeassistant.helpers.config_validation as cv @@ -57,7 +58,7 @@ async def async_setup(hass, config): if isinstance(api_keys, str): api_keys = {"default": api_keys} - def trigger_service(call): + def trigger_service(call: ServiceCall) -> None: """Handle IFTTT trigger service calls.""" event = call.data[ATTR_EVENT] targets = call.data.get(ATTR_TARGET, list(api_keys)) diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index 5c09e0b0bc7..b2f9e339757 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -13,7 +13,7 @@ from homeassistant.const import ( CONF_NAME, CONF_SOURCE, ) -from homeassistant.core import callback +from homeassistant.core import ServiceCall, callback from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import make_entity_service_schema @@ -75,7 +75,7 @@ async def async_setup(hass, config): await component.async_setup(config) - async def async_scan_service(service): + async def async_scan_service(service: ServiceCall) -> None: """Service handler for scan.""" image_entities = await component.async_extract_from_service(service) diff --git a/homeassistant/components/insteon/utils.py b/homeassistant/components/insteon/utils.py index ace24644523..1599975f462 100644 --- a/homeassistant/components/insteon/utils.py +++ b/homeassistant/components/insteon/utils.py @@ -27,7 +27,7 @@ from homeassistant.const import ( CONF_PLATFORM, ENTITY_MATCH_ALL, ) -from homeassistant.core import callback +from homeassistant.core import ServiceCall, callback from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -166,19 +166,19 @@ def async_register_services(hass): save_lock = asyncio.Lock() - async def async_srv_add_all_link(service): + async def async_srv_add_all_link(service: ServiceCall) -> None: """Add an INSTEON All-Link between two devices.""" - group = service.data.get(SRV_ALL_LINK_GROUP) - mode = service.data.get(SRV_ALL_LINK_MODE) + group = service.data[SRV_ALL_LINK_GROUP] + mode = service.data[SRV_ALL_LINK_MODE] link_mode = mode.lower() == SRV_CONTROLLER await async_enter_linking_mode(link_mode, group) - async def async_srv_del_all_link(service): + async def async_srv_del_all_link(service: ServiceCall) -> None: """Delete an INSTEON All-Link between two devices.""" group = service.data.get(SRV_ALL_LINK_GROUP) await async_enter_unlinking_mode(group) - async def async_srv_load_aldb(service): + async def async_srv_load_aldb(service: ServiceCall) -> None: """Load the device All-Link database.""" entity_id = service.data[CONF_ENTITY_ID] reload = service.data[SRV_LOAD_DB_RELOAD] @@ -204,7 +204,7 @@ def async_register_services(hass): _LOGGER.debug("Saving Insteon devices") await devices.async_save(hass.config.config_dir) - def print_aldb(service): + def print_aldb(service: ServiceCall) -> None: """Print the All-Link Database for a device.""" # For now this sends logs to the log file. # Future direction is to create an INSTEON control panel. @@ -212,39 +212,39 @@ def async_register_services(hass): signal = f"{entity_id}_{SIGNAL_PRINT_ALDB}" dispatcher_send(hass, signal) - def print_im_aldb(service): + def print_im_aldb(service: ServiceCall) -> None: """Print the All-Link Database for a device.""" # For now this sends logs to the log file. # Future direction is to create an INSTEON control panel. print_aldb_to_log(devices.modem.aldb) - async def async_srv_x10_all_units_off(service): + async def async_srv_x10_all_units_off(service: ServiceCall) -> None: """Send the X10 All Units Off command.""" housecode = service.data.get(SRV_HOUSECODE) await async_x10_all_units_off(housecode) - async def async_srv_x10_all_lights_off(service): + async def async_srv_x10_all_lights_off(service: ServiceCall) -> None: """Send the X10 All Lights Off command.""" housecode = service.data.get(SRV_HOUSECODE) await async_x10_all_lights_off(housecode) - async def async_srv_x10_all_lights_on(service): + async def async_srv_x10_all_lights_on(service: ServiceCall) -> None: """Send the X10 All Lights On command.""" housecode = service.data.get(SRV_HOUSECODE) await async_x10_all_lights_on(housecode) - async def async_srv_scene_on(service): + async def async_srv_scene_on(service: ServiceCall) -> None: """Trigger an INSTEON scene ON.""" group = service.data.get(SRV_ALL_LINK_GROUP) await async_trigger_scene_on(group) - async def async_srv_scene_off(service): + async def async_srv_scene_off(service: ServiceCall) -> None: """Trigger an INSTEON scene ON.""" group = service.data.get(SRV_ALL_LINK_GROUP) await async_trigger_scene_off(group) @callback - def async_add_default_links(service): + def async_add_default_links(service: ServiceCall) -> None: """Add the default All-Link entries to a device.""" entity_id = service.data[CONF_ENTITY_ID] signal = f"{entity_id}_{SIGNAL_ADD_DEFAULT_LINKS}" diff --git a/homeassistant/components/iperf3/__init__.py b/homeassistant/components/iperf3/__init__.py index 04dabc013e7..14b1b37c005 100644 --- a/homeassistant/components/iperf3/__init__.py +++ b/homeassistant/components/iperf3/__init__.py @@ -20,6 +20,7 @@ from homeassistant.const import ( CONF_SCAN_INTERVAL, DATA_RATE_MEGABITS_PER_SECOND, ) +from homeassistant.core import ServiceCall import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import dispatcher_send @@ -103,7 +104,7 @@ async def async_setup(hass, config): if not conf[CONF_MANUAL]: async_track_time_interval(hass, data.update, conf[CONF_SCAN_INTERVAL]) - def update(call): + def update(call: ServiceCall) -> None: """Service call to manually update the data.""" called_host = call.data[ATTR_HOST] if called_host in hass.data[DOMAIN]: diff --git a/homeassistant/components/isy994/services.py b/homeassistant/components/isy994/services.py index 5b18d6cd33a..a1dff594a1f 100644 --- a/homeassistant/components/isy994/services.py +++ b/homeassistant/components/isy994/services.py @@ -182,7 +182,7 @@ def async_setup_services(hass: HomeAssistant): # noqa: C901 # Integration-level services have already been added. Return. return - async def async_system_query_service_handler(service): + async def async_system_query_service_handler(service: ServiceCall) -> None: """Handle a system query service call.""" address = service.data.get(CONF_ADDRESS) isy_name = service.data.get(CONF_ISY) @@ -206,7 +206,7 @@ def async_setup_services(hass: HomeAssistant): # noqa: C901 ) await isy.query() - async def async_run_network_resource_service_handler(service): + async def async_run_network_resource_service_handler(service: ServiceCall) -> None: """Handle a network resource service call.""" address = service.data.get(CONF_ADDRESS) name = service.data.get(CONF_NAME) @@ -230,7 +230,7 @@ def async_setup_services(hass: HomeAssistant): # noqa: C901 "Could not run network resource command; not found or enabled on the ISY" ) - async def async_send_program_command_service_handler(service): + async def async_send_program_command_service_handler(service: ServiceCall) -> None: """Handle a send program command service call.""" address = service.data.get(CONF_ADDRESS) name = service.data.get(CONF_NAME) @@ -251,7 +251,7 @@ def async_setup_services(hass: HomeAssistant): # noqa: C901 return _LOGGER.error("Could not send program command; not found or enabled on the ISY") - async def async_set_variable_service_handler(service): + async def async_set_variable_service_handler(service: ServiceCall) -> None: """Handle a set variable service call.""" address = service.data.get(CONF_ADDRESS) vtype = service.data.get(CONF_TYPE) @@ -275,7 +275,7 @@ def async_setup_services(hass: HomeAssistant): # noqa: C901 _LOGGER.error("Could not set variable value; not found or enabled on the ISY") @callback - def async_cleanup_registry_entries(service) -> None: + def async_cleanup_registry_entries(service: ServiceCall) -> None: """Remove extra entities that are no longer part of the integration.""" entity_registry = er.async_get(hass) config_ids = [] @@ -327,7 +327,7 @@ def async_setup_services(hass: HomeAssistant): # noqa: C901 len(extra_entities), ) - async def async_reload_config_entries(service) -> None: + async def async_reload_config_entries(service: ServiceCall) -> None: """Trigger a reload of all ISY994 config entries.""" for config_entry_id in hass.data[DOMAIN]: hass.async_create_task(hass.config_entries.async_reload(config_entry_id)) @@ -370,7 +370,7 @@ def async_setup_services(hass: HomeAssistant): # noqa: C901 domain=DOMAIN, service=SERVICE_RELOAD, service_func=async_reload_config_entries ) - async def _async_send_raw_node_command(call: ServiceCall): + async def _async_send_raw_node_command(call: ServiceCall) -> None: await hass.helpers.service.entity_service_call( async_get_platforms(hass, DOMAIN), "async_send_raw_node_command", call ) @@ -382,7 +382,7 @@ def async_setup_services(hass: HomeAssistant): # noqa: C901 service_func=_async_send_raw_node_command, ) - async def _async_send_node_command(call: ServiceCall): + async def _async_send_node_command(call: ServiceCall) -> None: await hass.helpers.service.entity_service_call( async_get_platforms(hass, DOMAIN), "async_send_node_command", call ) @@ -394,7 +394,7 @@ def async_setup_services(hass: HomeAssistant): # noqa: C901 service_func=_async_send_node_command, ) - async def _async_get_zwave_parameter(call: ServiceCall): + async def _async_get_zwave_parameter(call: ServiceCall) -> None: await hass.helpers.service.entity_service_call( async_get_platforms(hass, DOMAIN), "async_get_zwave_parameter", call ) @@ -406,7 +406,7 @@ def async_setup_services(hass: HomeAssistant): # noqa: C901 service_func=_async_get_zwave_parameter, ) - async def _async_set_zwave_parameter(call: ServiceCall): + async def _async_set_zwave_parameter(call: ServiceCall) -> None: await hass.helpers.service.entity_service_call( async_get_platforms(hass, DOMAIN), "async_set_zwave_parameter", call ) @@ -418,7 +418,7 @@ def async_setup_services(hass: HomeAssistant): # noqa: C901 service_func=_async_set_zwave_parameter, ) - async def _async_rename_node(call: ServiceCall): + async def _async_rename_node(call: ServiceCall) -> None: await hass.helpers.service.entity_service_call( async_get_platforms(hass, DOMAIN), "async_rename_node", call ) From 656d383ba6a59e76b4605fa4fc2a444ee10d995f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Dec 2021 14:23:01 +0100 Subject: [PATCH 1087/2644] Ensure service calls are typed [e-g] (#62912) Co-authored-by: epenet --- homeassistant/components/ecobee/climate.py | 9 +++++---- homeassistant/components/elkm1/__init__.py | 8 ++++---- homeassistant/components/envisalink/__init__.py | 4 ++-- .../components/envisalink/alarm_control_panel.py | 8 ++++---- homeassistant/components/evohome/__init__.py | 8 ++++---- homeassistant/components/ffmpeg/__init__.py | 4 ++-- homeassistant/components/flux/switch.py | 5 ++++- homeassistant/components/freebox/__init__.py | 4 ++-- homeassistant/components/geniushub/__init__.py | 4 ++-- .../components/google_assistant/__init__.py | 2 +- homeassistant/components/group/__init__.py | 14 ++++++++++---- 11 files changed, 40 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 473ba0cdd58..fa3a00e1175 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -37,6 +37,7 @@ from homeassistant.const import ( STATE_ON, TEMP_FAHRENHEIT, ) +from homeassistant.core import ServiceCall from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import DeviceInfo @@ -198,7 +199,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): platform = entity_platform.async_get_current_platform() - def create_vacation_service(service): + def create_vacation_service(service: ServiceCall) -> None: """Create a vacation on the target thermostat.""" entity_id = service.data[ATTR_ENTITY_ID] @@ -208,7 +209,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): thermostat.schedule_update_ha_state(True) break - def delete_vacation_service(service): + def delete_vacation_service(service: ServiceCall) -> None: """Delete a vacation on the target thermostat.""" entity_id = service.data[ATTR_ENTITY_ID] vacation_name = service.data[ATTR_VACATION_NAME] @@ -219,7 +220,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): thermostat.schedule_update_ha_state(True) break - def fan_min_on_time_set_service(service): + def fan_min_on_time_set_service(service: ServiceCall) -> None: """Set the minimum fan on time on the target thermostats.""" entity_id = service.data.get(ATTR_ENTITY_ID) fan_min_on_time = service.data[ATTR_FAN_MIN_ON_TIME] @@ -236,7 +237,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): thermostat.schedule_update_ha_state(True) - def resume_program_set_service(service): + def resume_program_set_service(service: ServiceCall) -> None: """Resume the program on the target thermostats.""" entity_id = service.data.get(ATTR_ENTITY_ID) resume_all = service.data.get(ATTR_RESUME_ALL) diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 8714a41a9a7..6f586423552 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -24,7 +24,7 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, Platform, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import DeviceInfo, Entity @@ -342,13 +342,13 @@ def _create_elk_services(hass): raise HomeAssistantError(f"No ElkM1 with prefix '{prefix}' found") return elk - def _speak_word_service(service): + def _speak_word_service(service: ServiceCall) -> None: _getelk(service).panel.speak_word(service.data["number"]) - def _speak_phrase_service(service): + def _speak_phrase_service(service: ServiceCall) -> None: _getelk(service).panel.speak_phrase(service.data["number"]) - def _set_time_service(service): + def _set_time_service(service: ServiceCall) -> None: _getelk(service).panel.set_time(dt_util.now()) hass.services.async_register( diff --git a/homeassistant/components/envisalink/__init__.py b/homeassistant/components/envisalink/__init__.py index d5a8b39e8f9..433f515469f 100644 --- a/homeassistant/components/envisalink/__init__.py +++ b/homeassistant/components/envisalink/__init__.py @@ -11,7 +11,7 @@ from homeassistant.const import ( CONF_TIMEOUT, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import callback +from homeassistant.core import ServiceCall, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -181,7 +181,7 @@ async def async_setup(hass, config): _LOGGER.info("Shutting down Envisalink") controller.stop() - async def handle_custom_function(call): + async def handle_custom_function(call: ServiceCall) -> None: """Handle custom/PGM service.""" custom_function = call.data.get(ATTR_CUSTOM_FUNCTION) partition = call.data.get(ATTR_PARTITION) diff --git a/homeassistant/components/envisalink/alarm_control_panel.py b/homeassistant/components/envisalink/alarm_control_panel.py index dff434a68ee..d93e980064d 100644 --- a/homeassistant/components/envisalink/alarm_control_panel.py +++ b/homeassistant/components/envisalink/alarm_control_panel.py @@ -24,7 +24,7 @@ from homeassistant.const import ( STATE_ALARM_TRIGGERED, STATE_UNKNOWN, ) -from homeassistant.core import callback +from homeassistant.core import ServiceCall, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -74,10 +74,10 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(devices) @callback - def alarm_keypress_handler(service): + def alarm_keypress_handler(service: ServiceCall) -> None: """Map services to methods on Alarm.""" - entity_ids = service.data.get(ATTR_ENTITY_ID) - keypress = service.data.get(ATTR_KEYPRESS) + entity_ids = service.data[ATTR_ENTITY_ID] + keypress = service.data[ATTR_KEYPRESS] target_devices = [ device for device in devices if device.entity_id in entity_ids diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index d7b1407642d..fa394079e2f 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -22,7 +22,7 @@ from homeassistant.const import ( CONF_USERNAME, TEMP_CELSIUS, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform @@ -274,12 +274,12 @@ def setup_service_functions(hass: HomeAssistant, broker): """ @verify_domain_control(hass, DOMAIN) - async def force_refresh(call) -> None: + async def force_refresh(call: ServiceCall) -> None: """Obtain the latest state data via the vendor's RESTful API.""" await broker.async_update() @verify_domain_control(hass, DOMAIN) - async def set_system_mode(call) -> None: + async def set_system_mode(call: ServiceCall) -> None: """Set the system mode.""" payload = { "unique_id": broker.tcs.systemId, @@ -289,7 +289,7 @@ def setup_service_functions(hass: HomeAssistant, broker): async_dispatcher_send(hass, DOMAIN, payload) @verify_domain_control(hass, DOMAIN) - async def set_zone_override(call) -> None: + async def set_zone_override(call: ServiceCall) -> None: """Set the zone override (setpoint).""" entity_id = call.data[ATTR_ENTITY_ID] diff --git a/homeassistant/components/ffmpeg/__init__.py b/homeassistant/components/ffmpeg/__init__.py index 7da1bc572f5..31650598371 100644 --- a/homeassistant/components/ffmpeg/__init__.py +++ b/homeassistant/components/ffmpeg/__init__.py @@ -13,7 +13,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, ServiceCall, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -64,7 +64,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await manager.async_get_version() # Register service - async def async_service_handle(service): + async def async_service_handle(service: ServiceCall) -> None: """Handle service ffmpeg process.""" entity_ids = service.data.get(ATTR_ENTITY_ID) diff --git a/homeassistant/components/flux/switch.py b/homeassistant/components/flux/switch.py index ab0d296928f..2a6801d8966 100644 --- a/homeassistant/components/flux/switch.py +++ b/homeassistant/components/flux/switch.py @@ -3,6 +3,8 @@ Flux for Home-Assistant. The idea was taken from https://github.com/KpaBap/hue-flux/ """ +from __future__ import annotations + import datetime import logging @@ -32,6 +34,7 @@ from homeassistant.const import ( SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, ) +from homeassistant.core import ServiceCall from homeassistant.helpers import config_validation as cv, event from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.sun import get_astral_event_date @@ -159,7 +162,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) async_add_entities([flux]) - async def async_update(call=None): + async def async_update(call: ServiceCall | None = None) -> None: """Update lights.""" await flux.async_flux_update() diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py index c343e8d629c..565d43b2a41 100644 --- a/homeassistant/components/freebox/__init__.py +++ b/homeassistant/components/freebox/__init__.py @@ -5,7 +5,7 @@ import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv from .const import DOMAIN, PLATFORMS, SERVICE_REBOOT @@ -50,7 +50,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_setup_platforms(entry, PLATFORMS) # Services - async def async_reboot(call): + async def async_reboot(call: ServiceCall) -> None: """Handle reboot service call.""" await router.reboot() diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py index cad80e8d707..72a759427f5 100644 --- a/homeassistant/components/geniushub/__init__.py +++ b/homeassistant/components/geniushub/__init__.py @@ -19,7 +19,7 @@ from homeassistant.const import ( CONF_USERNAME, TEMP_CELSIUS, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.discovery import async_load_platform @@ -133,7 +133,7 @@ def setup_service_functions(hass: HomeAssistant, broker): """Set up the service functions.""" @verify_domain_control(hass, DOMAIN) - async def set_zone_mode(call) -> None: + async def set_zone_mode(call: ServiceCall) -> None: """Set the system mode.""" entity_id = call.data[ATTR_ENTITY_ID] diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index 1e0c0a06114..dca436d8e2a 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -105,7 +105,7 @@ async def async_setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool: if google_config.should_report_state: google_config.async_enable_report_state() - async def request_sync_service_handler(call: ServiceCall): + async def request_sync_service_handler(call: ServiceCall) -> None: """Handle request sync service calls.""" agent_user_id = call.data.get("agent_user_id") or call.context.user_id diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index e3816d52d60..a609009fe80 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -26,7 +26,13 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.core import CoreState, HomeAssistant, callback, split_entity_id +from homeassistant.core import ( + CoreState, + HomeAssistant, + ServiceCall, + callback, + split_entity_id, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.helpers.entity_component import EntityComponent @@ -220,7 +226,7 @@ async def async_setup(hass, config): await _async_process_config(hass, config, component) - async def reload_service_handler(service): + async def reload_service_handler(service: ServiceCall) -> None: """Remove all user-defined groups and load new ones from config.""" auto = list(filter(lambda e: not e.user_defined, component.entities)) @@ -238,12 +244,12 @@ async def async_setup(hass, config): service_lock = asyncio.Lock() - async def locked_service_handler(service): + async def locked_service_handler(service: ServiceCall) -> None: """Handle a service with an async lock.""" async with service_lock: await groups_service_handler(service) - async def groups_service_handler(service): + async def groups_service_handler(service: ServiceCall) -> None: """Handle dynamic group service functions.""" object_id = service.data[ATTR_OBJECT_ID] entity_id = f"{DOMAIN}.{object_id}" From 92ace6c2e8adf576176b41e9e7a1561499d9039a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Dec 2021 14:23:42 +0100 Subject: [PATCH 1088/2644] Ensure service calls are typed [s-u] (#62922) Co-authored-by: epenet --- homeassistant/components/sabnzbd/__init__.py | 4 ++-- homeassistant/components/screenlogic/services.py | 2 +- homeassistant/components/script/__init__.py | 12 ++++++------ homeassistant/components/sensibo/climate.py | 4 ++-- homeassistant/components/shopping_list/__init__.py | 14 +++++++------- homeassistant/components/snips/__init__.py | 9 +++++---- .../components/speedtestdotnet/__init__.py | 4 ++-- homeassistant/components/starline/__init__.py | 10 ++++++---- homeassistant/components/system_bridge/__init__.py | 10 +++++----- homeassistant/components/system_log/__init__.py | 4 ++-- homeassistant/components/telegram_bot/__init__.py | 3 ++- homeassistant/components/transmission/__init__.py | 9 +++++---- homeassistant/components/tts/__init__.py | 6 +++--- homeassistant/components/unifi/services.py | 4 ++-- 14 files changed, 50 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/sabnzbd/__init__.py b/homeassistant/components/sabnzbd/__init__.py index f2b1ce882c7..3cb183a6e42 100644 --- a/homeassistant/components/sabnzbd/__init__.py +++ b/homeassistant/components/sabnzbd/__init__.py @@ -22,7 +22,7 @@ from homeassistant.const import ( DATA_MEGABYTES, DATA_RATE_MEGABYTES_PER_SECOND, ) -from homeassistant.core import callback +from homeassistant.core import ServiceCall, callback from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -225,7 +225,7 @@ def async_setup_sabnzbd(hass, sab_api, config, name): discovery.async_load_platform(hass, "sensor", DOMAIN, {}, config) ) - async def async_service_handler(service): + async def async_service_handler(service: ServiceCall) -> None: """Handle service calls.""" if service.service == SERVICE_PAUSE: await sab_api_data.async_pause_queue() diff --git a/homeassistant/components/screenlogic/services.py b/homeassistant/components/screenlogic/services.py index c046a4478fe..62027a41949 100644 --- a/homeassistant/components/screenlogic/services.py +++ b/homeassistant/components/screenlogic/services.py @@ -40,7 +40,7 @@ def async_load_screenlogic_services(hass: HomeAssistant): if hass.config_entries.async_get_entry(entry_id).domain == DOMAIN ] - async def async_set_color_mode(service_call: ServiceCall): + async def async_set_color_mode(service_call: ServiceCall) -> None: if not ( screenlogic_entry_ids := await extract_screenlogic_config_entry_ids( service_call diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index a8ffb271336..991769bd5c4 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -26,7 +26,7 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_ON, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import extract_domain_configs import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import make_entity_service_schema @@ -170,14 +170,14 @@ async def async_setup(hass, config): if not await _async_process_config(hass, config, component): await async_get_blueprints(hass).async_populate() - async def reload_service(service): + async def reload_service(service: ServiceCall) -> None: """Call a service to reload scripts.""" if (conf := await component.async_prepare_reload()) is None: return await _async_process_config(hass, conf, component) - async def turn_on_service(service): + async def turn_on_service(service: ServiceCall) -> None: """Call a service to turn script on.""" variables = service.data.get(ATTR_VARIABLES) for script_entity in await component.async_extract_from_service(service): @@ -185,7 +185,7 @@ async def async_setup(hass, config): variables=variables, context=service.context, wait=False ) - async def turn_off_service(service): + async def turn_off_service(service: ServiceCall) -> None: """Cancel a script.""" # Stopping a script is ok to be done in parallel script_entities = await component.async_extract_from_service(service) @@ -200,7 +200,7 @@ async def async_setup(hass, config): ] ) - async def toggle_service(service): + async def toggle_service(service: ServiceCall) -> None: """Toggle a script.""" for script_entity in await component.async_extract_from_service(service): await script_entity.async_toggle(context=service.context, wait=False) @@ -266,7 +266,7 @@ async def _async_process_config(hass, config, component) -> bool: await component.async_add_entities(entities) - async def service_handler(service): + async def service_handler(service: ServiceCall) -> None: """Execute a service call to script.